@tinycloud/cli 1.2.6-beta.4 → 1.3.0-beta.5
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 +10 -3
- package/dist/index.js +41 -27
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +1261 -671
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +5 -5
- package/skills/tc-cli/REFERENCE.md +11 -5
package/dist/legacy-entry.js
CHANGED
|
@@ -170,15 +170,15 @@ function closeReplication(schedule = scheduleTimeout) {
|
|
|
170
170
|
removeSignalHandlers();
|
|
171
171
|
let resolveClosing;
|
|
172
172
|
let rejectClosing;
|
|
173
|
-
const closing = new Promise((
|
|
174
|
-
resolveClosing =
|
|
173
|
+
const closing = new Promise((resolve6, reject) => {
|
|
174
|
+
resolveClosing = resolve6;
|
|
175
175
|
rejectClosing = reject;
|
|
176
176
|
});
|
|
177
177
|
state.closing = closing;
|
|
178
178
|
void (async () => {
|
|
179
179
|
let timeout;
|
|
180
|
-
const deadline = new Promise((
|
|
181
|
-
timeout = schedule(() =>
|
|
180
|
+
const deadline = new Promise((resolve6) => {
|
|
181
|
+
timeout = schedule(() => resolve6(true), CLOSE_TIMEOUT_MS);
|
|
182
182
|
});
|
|
183
183
|
let timedOut = false;
|
|
184
184
|
const abandonQueuedControls = () => {
|
|
@@ -204,7 +204,7 @@ function closeReplication(schedule = scheduleTimeout) {
|
|
|
204
204
|
if (state.generation === generation && state.allControls.size === 0) break;
|
|
205
205
|
}
|
|
206
206
|
if (timedOut) break;
|
|
207
|
-
const turn = new Promise((
|
|
207
|
+
const turn = new Promise((resolve6) => setImmediate(() => resolve6(false)));
|
|
208
208
|
timedOut ||= await Promise.race([turn, deadline]);
|
|
209
209
|
if (timedOut) abandonQueuedControls();
|
|
210
210
|
else if (state.allControls.size === 0) break;
|
|
@@ -529,7 +529,7 @@ var init_storage2 = __esm({
|
|
|
529
529
|
// src/output/errors.ts
|
|
530
530
|
import { readFileSync, readdirSync } from "fs";
|
|
531
531
|
import { join as join2 } from "path";
|
|
532
|
-
import { ProfileDeletedError, ProfileLockTimeoutError } from "@tinycloud/operations/state";
|
|
532
|
+
import { ProfileDeletedError, ProfileLockTimeoutError, ProfileSymlinkError } from "@tinycloud/operations/state";
|
|
533
533
|
import { isSafeRecovery } from "@tinycloud/operations/recovery";
|
|
534
534
|
import { CLI_NAME as CLI_NAME2 } from "@tinycloud/operations/cli-name";
|
|
535
535
|
import {
|
|
@@ -590,6 +590,9 @@ function wrapError(error) {
|
|
|
590
590
|
if (error instanceof ProfileDeletedError) {
|
|
591
591
|
return new CLIError("PROFILE_NOT_FOUND", message);
|
|
592
592
|
}
|
|
593
|
+
if (error instanceof ProfileSymlinkError) {
|
|
594
|
+
return new CLIError("PROFILE_SYMLINK", message, ExitCode.ERROR);
|
|
595
|
+
}
|
|
593
596
|
if (verdict === void 0 && (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired"))) {
|
|
594
597
|
return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
|
|
595
598
|
}
|
|
@@ -7094,7 +7097,7 @@ async function awaitOwned(signal, operation) {
|
|
|
7094
7097
|
throw SUPERSEDED;
|
|
7095
7098
|
}
|
|
7096
7099
|
const promise = typeof operation === "function" ? operation() : operation;
|
|
7097
|
-
return new Promise((
|
|
7100
|
+
return new Promise((resolve6, reject) => {
|
|
7098
7101
|
let settled = false;
|
|
7099
7102
|
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
7100
7103
|
const onAbort = () => {
|
|
@@ -7112,7 +7115,7 @@ async function awaitOwned(signal, operation) {
|
|
|
7112
7115
|
}
|
|
7113
7116
|
settled = true;
|
|
7114
7117
|
cleanup();
|
|
7115
|
-
|
|
7118
|
+
resolve6(value);
|
|
7116
7119
|
},
|
|
7117
7120
|
(error) => {
|
|
7118
7121
|
if (settled) return;
|
|
@@ -7165,11 +7168,11 @@ function readProperty(value, key) {
|
|
|
7165
7168
|
}
|
|
7166
7169
|
}
|
|
7167
7170
|
function sleep(delayMs, _attempt, signal) {
|
|
7168
|
-
return new Promise((
|
|
7171
|
+
return new Promise((resolve6) => {
|
|
7169
7172
|
const finish = () => {
|
|
7170
7173
|
clearTimeout(timer);
|
|
7171
7174
|
signal.removeEventListener("abort", stop);
|
|
7172
|
-
|
|
7175
|
+
resolve6();
|
|
7173
7176
|
};
|
|
7174
7177
|
const timer = setTimeout(finish, delayMs);
|
|
7175
7178
|
timer.unref?.();
|
|
@@ -7182,13 +7185,13 @@ function sleep(delayMs, _attempt, signal) {
|
|
|
7182
7185
|
});
|
|
7183
7186
|
}
|
|
7184
7187
|
function abortPromise(signal) {
|
|
7185
|
-
let
|
|
7188
|
+
let resolve6;
|
|
7186
7189
|
const promise = new Promise((done) => {
|
|
7187
|
-
|
|
7190
|
+
resolve6 = done;
|
|
7188
7191
|
});
|
|
7189
|
-
const onAbort = () =>
|
|
7192
|
+
const onAbort = () => resolve6();
|
|
7190
7193
|
if (signal.aborted) {
|
|
7191
|
-
|
|
7194
|
+
resolve6();
|
|
7192
7195
|
} else {
|
|
7193
7196
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
7194
7197
|
}
|
|
@@ -7491,7 +7494,7 @@ function isBrowser() {
|
|
|
7491
7494
|
}
|
|
7492
7495
|
}
|
|
7493
7496
|
function openDB() {
|
|
7494
|
-
return new Promise((
|
|
7497
|
+
return new Promise((resolve6, reject) => {
|
|
7495
7498
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
7496
7499
|
request.onupgradeneeded = () => {
|
|
7497
7500
|
const db = request.result;
|
|
@@ -7499,43 +7502,43 @@ function openDB() {
|
|
|
7499
7502
|
db.createObjectStore(STORE_NAME);
|
|
7500
7503
|
}
|
|
7501
7504
|
};
|
|
7502
|
-
request.onsuccess = () =>
|
|
7505
|
+
request.onsuccess = () => resolve6(request.result);
|
|
7503
7506
|
request.onerror = () => reject(request.error);
|
|
7504
7507
|
});
|
|
7505
7508
|
}
|
|
7506
7509
|
function idbGet(db, key) {
|
|
7507
|
-
return new Promise((
|
|
7510
|
+
return new Promise((resolve6, reject) => {
|
|
7508
7511
|
const tx = db.transaction(STORE_NAME, "readonly");
|
|
7509
7512
|
const store = tx.objectStore(STORE_NAME);
|
|
7510
7513
|
const req = store.get(key);
|
|
7511
|
-
req.onsuccess = () =>
|
|
7514
|
+
req.onsuccess = () => resolve6(req.result);
|
|
7512
7515
|
req.onerror = () => reject(req.error);
|
|
7513
7516
|
});
|
|
7514
7517
|
}
|
|
7515
7518
|
function idbPut(db, key, value) {
|
|
7516
|
-
return new Promise((
|
|
7519
|
+
return new Promise((resolve6, reject) => {
|
|
7517
7520
|
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
7518
7521
|
const store = tx.objectStore(STORE_NAME);
|
|
7519
7522
|
const req = store.put(value, key);
|
|
7520
|
-
req.onsuccess = () =>
|
|
7523
|
+
req.onsuccess = () => resolve6();
|
|
7521
7524
|
req.onerror = () => reject(req.error);
|
|
7522
7525
|
});
|
|
7523
7526
|
}
|
|
7524
7527
|
function idbDelete(db, key) {
|
|
7525
|
-
return new Promise((
|
|
7528
|
+
return new Promise((resolve6, reject) => {
|
|
7526
7529
|
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
7527
7530
|
const store = tx.objectStore(STORE_NAME);
|
|
7528
7531
|
const req = store.delete(key);
|
|
7529
|
-
req.onsuccess = () =>
|
|
7532
|
+
req.onsuccess = () => resolve6();
|
|
7530
7533
|
req.onerror = () => reject(req.error);
|
|
7531
7534
|
});
|
|
7532
7535
|
}
|
|
7533
7536
|
function idbKeys(db) {
|
|
7534
|
-
return new Promise((
|
|
7537
|
+
return new Promise((resolve6, reject) => {
|
|
7535
7538
|
const tx = db.transaction(STORE_NAME, "readonly");
|
|
7536
7539
|
const store = tx.objectStore(STORE_NAME);
|
|
7537
7540
|
const req = store.getAllKeys();
|
|
7538
|
-
req.onsuccess = () =>
|
|
7541
|
+
req.onsuccess = () => resolve6(req.result.filter((k) => typeof k === "string"));
|
|
7539
7542
|
req.onerror = () => reject(req.error);
|
|
7540
7543
|
});
|
|
7541
7544
|
}
|
|
@@ -11594,8 +11597,8 @@ var init_dist2 = __esm({
|
|
|
11594
11597
|
if (this.closed) {
|
|
11595
11598
|
return Promise.resolve({ value: void 0, done: true });
|
|
11596
11599
|
}
|
|
11597
|
-
return new Promise((
|
|
11598
|
-
this.waiters.push({ resolve:
|
|
11600
|
+
return new Promise((resolve6, reject) => {
|
|
11601
|
+
this.waiters.push({ resolve: resolve6, reject });
|
|
11599
11602
|
});
|
|
11600
11603
|
}
|
|
11601
11604
|
};
|
|
@@ -11991,7 +11994,7 @@ var init_dist2 = __esm({
|
|
|
11991
11994
|
} finally {
|
|
11992
11995
|
for (const abort of aborts) abort.dispose();
|
|
11993
11996
|
}
|
|
11994
|
-
await new Promise((
|
|
11997
|
+
await new Promise((resolve6) => setTimeout(resolve6, 0));
|
|
11995
11998
|
}
|
|
11996
11999
|
/** The stream the current subscribers need, or undefined when none remain. */
|
|
11997
12000
|
plan() {
|
|
@@ -14362,12 +14365,14 @@ import { resolveSecretsOwner } from "@tinycloud/operations/secret-request";
|
|
|
14362
14365
|
import {
|
|
14363
14366
|
additionalDelegationsPath as sharedAdditionalDelegationsPath,
|
|
14364
14367
|
authRequestsPath as sharedAuthRequestsPath,
|
|
14368
|
+
profilePath,
|
|
14365
14369
|
profileStoreMetadataPath,
|
|
14366
14370
|
readAdditionalDelegations,
|
|
14367
14371
|
readAuthRequests,
|
|
14368
14372
|
refuseWriteToDeletedProfile,
|
|
14369
14373
|
updateProfileStore,
|
|
14370
14374
|
withProfileLock,
|
|
14375
|
+
withPinnedProfileDirectory,
|
|
14371
14376
|
writeJsonAtomic
|
|
14372
14377
|
} from "@tinycloud/operations/state";
|
|
14373
14378
|
function isCompatiblePermissionRequestArtifact(value) {
|
|
@@ -14385,10 +14390,10 @@ function permissionRequestsPath(profile) {
|
|
|
14385
14390
|
return sharedAuthRequestsPath(profile);
|
|
14386
14391
|
}
|
|
14387
14392
|
function grantHistoryPath(profile) {
|
|
14388
|
-
return join3(
|
|
14393
|
+
return join3(profilePath(profile), "auth-grants.jsonl");
|
|
14389
14394
|
}
|
|
14390
14395
|
function localGrantArtifactsPath(profile) {
|
|
14391
|
-
return join3(
|
|
14396
|
+
return join3(profilePath(profile), "auth-grant-artifacts.json");
|
|
14392
14397
|
}
|
|
14393
14398
|
function createPermissionRequestArtifact(params) {
|
|
14394
14399
|
return buildPermissionRequestArtifact({
|
|
@@ -14415,22 +14420,30 @@ async function loadAdditionalDelegations(profile) {
|
|
|
14415
14420
|
return readAdditionalDelegations(profile);
|
|
14416
14421
|
}
|
|
14417
14422
|
async function loadLocalGrantArtifacts(profile) {
|
|
14418
|
-
|
|
14419
|
-
|
|
14420
|
-
|
|
14421
|
-
|
|
14422
|
-
parsed
|
|
14423
|
-
|
|
14424
|
-
|
|
14425
|
-
|
|
14426
|
-
|
|
14427
|
-
|
|
14428
|
-
(
|
|
14429
|
-
|
|
14423
|
+
return withPinnedProfileDirectory(profile, async (directory) => {
|
|
14424
|
+
if (!directory) return [];
|
|
14425
|
+
const path = localGrantArtifactsPath(profile);
|
|
14426
|
+
if (!await fileExists(path)) return [];
|
|
14427
|
+
let parsed;
|
|
14428
|
+
try {
|
|
14429
|
+
parsed = JSON.parse(await readFile2(path, "utf8"));
|
|
14430
|
+
} catch {
|
|
14431
|
+
return [];
|
|
14432
|
+
}
|
|
14433
|
+
if (!Array.isArray(parsed)) return [];
|
|
14434
|
+
const records = parsed;
|
|
14435
|
+
return records.filter((value) => {
|
|
14436
|
+
if (value === null || typeof value !== "object" || !("delegationCid" in value) || typeof value.delegationCid !== "string" || !("delegation" in value) || value.delegation === null || typeof value.delegation !== "object") return false;
|
|
14437
|
+
const delegation = value.delegation;
|
|
14438
|
+
if (!("cid" in delegation) || delegation.cid !== value.delegationCid || !("delegationHeader" in delegation) || delegation.delegationHeader === null || typeof delegation.delegationHeader !== "object") return false;
|
|
14439
|
+
const header = delegation.delegationHeader;
|
|
14440
|
+
return "Authorization" in header && typeof header.Authorization === "string";
|
|
14441
|
+
});
|
|
14442
|
+
});
|
|
14430
14443
|
}
|
|
14431
14444
|
async function appendLocalGrantArtifact(profile, delegation) {
|
|
14432
|
-
await ProfileManager.ensureProfileDir(profile);
|
|
14433
14445
|
await withProfileLock(profile, async () => {
|
|
14446
|
+
await ProfileManager.ensureProfileDir(profile);
|
|
14434
14447
|
await refuseWriteToDeletedProfile(profile);
|
|
14435
14448
|
const records = await loadLocalGrantArtifacts(profile);
|
|
14436
14449
|
if (records.some((record2) => record2.delegationCid === delegation.cid)) return;
|
|
@@ -14523,21 +14536,27 @@ async function cliGrantRecord(node, delegation, permissions, host) {
|
|
|
14523
14536
|
);
|
|
14524
14537
|
}
|
|
14525
14538
|
async function appendGrantHistory(profile, entry) {
|
|
14526
|
-
await
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
|
|
14530
|
-
|
|
14531
|
-
|
|
14532
|
-
|
|
14533
|
-
|
|
14534
|
-
|
|
14539
|
+
await withProfileLock(profile, async () => {
|
|
14540
|
+
await ProfileManager.ensureProfileDir(profile);
|
|
14541
|
+
await refuseWriteToDeletedProfile(profile);
|
|
14542
|
+
const line = JSON.stringify({
|
|
14543
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14544
|
+
profile,
|
|
14545
|
+
...entry
|
|
14546
|
+
}) + "\n";
|
|
14547
|
+
const path = grantHistoryPath(profile);
|
|
14548
|
+
await appendFile(path, line, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
14549
|
+
await chmod(path, PRIVATE_FILE_MODE);
|
|
14550
|
+
});
|
|
14535
14551
|
}
|
|
14536
14552
|
async function readGrantHistory(profile) {
|
|
14537
|
-
|
|
14538
|
-
|
|
14539
|
-
|
|
14540
|
-
|
|
14553
|
+
return withPinnedProfileDirectory(profile, async (directory) => {
|
|
14554
|
+
if (!directory) return [];
|
|
14555
|
+
const path = grantHistoryPath(profile);
|
|
14556
|
+
if (!await fileExists(path)) return [];
|
|
14557
|
+
const raw = await readFile2(path, "utf8");
|
|
14558
|
+
return raw.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
|
|
14559
|
+
});
|
|
14541
14560
|
}
|
|
14542
14561
|
async function parseCapSpec(spec, profile) {
|
|
14543
14562
|
const firstColon = spec.indexOf(":");
|
|
@@ -14786,7 +14805,6 @@ var init_permissions = __esm({
|
|
|
14786
14805
|
"src/lib/permissions.ts"() {
|
|
14787
14806
|
"use strict";
|
|
14788
14807
|
init_manifest();
|
|
14789
|
-
init_constants();
|
|
14790
14808
|
init_storage();
|
|
14791
14809
|
init_profiles();
|
|
14792
14810
|
init_errors();
|
|
@@ -17765,7 +17783,7 @@ var init_dist3 = __esm({
|
|
|
17765
17783
|
// src/lib/replication-config.ts
|
|
17766
17784
|
import { join as join5 } from "path";
|
|
17767
17785
|
import { CLI_NAME as CLI_NAME6 } from "@tinycloud/operations/cli-name";
|
|
17768
|
-
import { profilePath, withProfileLock as withProfileLock2 } from "@tinycloud/operations/state";
|
|
17786
|
+
import { profilePath as profilePath2, withProfileLock as withProfileLock2 } from "@tinycloud/operations/state";
|
|
17769
17787
|
import { sqliteReplicaStorage } from "@tinycloud/node-sdk";
|
|
17770
17788
|
function createCliReplicationConfig(profile, settings, output2, enabled) {
|
|
17771
17789
|
if (!enabled || !settings || settings.prefixes.length === 0) return void 0;
|
|
@@ -17783,7 +17801,7 @@ function createCliReplicationConfig(profile, settings, output2, enabled) {
|
|
|
17783
17801
|
);
|
|
17784
17802
|
}
|
|
17785
17803
|
}
|
|
17786
|
-
const root =
|
|
17804
|
+
const root = profilePath2(profile);
|
|
17787
17805
|
return {
|
|
17788
17806
|
enabled: true,
|
|
17789
17807
|
prefixes: settings.prefixes,
|
|
@@ -18269,17 +18287,21 @@ var init_host = __esm({
|
|
|
18269
18287
|
});
|
|
18270
18288
|
|
|
18271
18289
|
// src/config/profiles.ts
|
|
18272
|
-
import { chmod as chmod2, lstat, readdir as readdir2, rm as rm2
|
|
18290
|
+
import { chmod as chmod2, lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
|
|
18273
18291
|
import { CLI_NAME as CLI_NAME9 } from "@tinycloud/operations/cli-name";
|
|
18274
18292
|
import { join as join6 } from "path";
|
|
18275
18293
|
import {
|
|
18276
|
-
|
|
18294
|
+
assertRealProfileDirectory,
|
|
18295
|
+
ProfileSymlinkError as ProfileSymlinkError2,
|
|
18296
|
+
namedProfilePath,
|
|
18297
|
+
profilePath as profilePath3,
|
|
18277
18298
|
readSession,
|
|
18278
18299
|
recordProfileDeletion,
|
|
18279
18300
|
refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
|
|
18280
18301
|
removeSession,
|
|
18281
18302
|
withProfileLock as withProfileLock3,
|
|
18282
|
-
writeSession
|
|
18303
|
+
writeSession,
|
|
18304
|
+
withPinnedProfileDirectory as withPinnedProfileDirectory2
|
|
18283
18305
|
} from "@tinycloud/operations/state";
|
|
18284
18306
|
var ProfileManager;
|
|
18285
18307
|
var init_profiles = __esm({
|
|
@@ -18321,8 +18343,13 @@ var init_profiles = __esm({
|
|
|
18321
18343
|
}
|
|
18322
18344
|
/** Owner-only profile directory (0700), created or tightened before any write into it. */
|
|
18323
18345
|
static async ensureProfileDir(name) {
|
|
18346
|
+
const profileDir = profilePath3(name);
|
|
18347
|
+
if (profileDir !== join6(PROFILES_DIR, name)) {
|
|
18348
|
+
await chmod2(profileDir, PRIVATE_DIR_MODE);
|
|
18349
|
+
return profileDir;
|
|
18350
|
+
}
|
|
18324
18351
|
await _ProfileManager.ensureConfigDir();
|
|
18325
|
-
|
|
18352
|
+
await assertRealProfileDirectory(name);
|
|
18326
18353
|
await ensureDir(profileDir);
|
|
18327
18354
|
await chmod2(profileDir, PRIVATE_DIR_MODE);
|
|
18328
18355
|
return profileDir;
|
|
@@ -18351,8 +18378,7 @@ var init_profiles = __esm({
|
|
|
18351
18378
|
* Throws CLIError if the profile doesn't exist.
|
|
18352
18379
|
*/
|
|
18353
18380
|
static async getProfile(name) {
|
|
18354
|
-
const
|
|
18355
|
-
const profile = await readJson(profilePath5);
|
|
18381
|
+
const profile = await withPinnedProfileDirectory2(name, (directory) => directory ? readJson(join6(directory, "profile.json")) : Promise.resolve(null));
|
|
18356
18382
|
if (!profile) {
|
|
18357
18383
|
throw new CLIError(
|
|
18358
18384
|
"PROFILE_NOT_FOUND",
|
|
@@ -18372,13 +18398,13 @@ var init_profiles = __esm({
|
|
|
18372
18398
|
}
|
|
18373
18399
|
/** Removes profile.json under the profile lock (rollback of a profile a failed login created). */
|
|
18374
18400
|
static async removeProfileConfig(name) {
|
|
18375
|
-
await _ProfileManager.withLock(name, () => rm2(join6(
|
|
18401
|
+
await _ProfileManager.withLock(name, () => rm2(join6(profilePath3(name), "profile.json"), { force: true }));
|
|
18376
18402
|
}
|
|
18377
18403
|
/**
|
|
18378
18404
|
* Returns true if a profile directory exists.
|
|
18379
18405
|
*/
|
|
18380
18406
|
static async profileExists(name) {
|
|
18381
|
-
return fileExists(join6(
|
|
18407
|
+
return withPinnedProfileDirectory2(name, (directory) => directory ? fileExists(join6(directory, "profile.json")) : Promise.resolve(false));
|
|
18382
18408
|
}
|
|
18383
18409
|
/**
|
|
18384
18410
|
* Returns an array of profile directory names.
|
|
@@ -18402,7 +18428,7 @@ var init_profiles = __esm({
|
|
|
18402
18428
|
static async deleteProfile(name) {
|
|
18403
18429
|
let profileDir;
|
|
18404
18430
|
try {
|
|
18405
|
-
profileDir =
|
|
18431
|
+
profileDir = namedProfilePath(name);
|
|
18406
18432
|
} catch {
|
|
18407
18433
|
throw new CLIError(
|
|
18408
18434
|
"INVALID_PROFILE_NAME",
|
|
@@ -18427,23 +18453,21 @@ var init_profiles = __esm({
|
|
|
18427
18453
|
return;
|
|
18428
18454
|
}
|
|
18429
18455
|
await _ProfileManager.withLock(name, async () => {
|
|
18430
|
-
|
|
18431
|
-
for (const
|
|
18432
|
-
|
|
18456
|
+
const directory = profilePath3(name);
|
|
18457
|
+
for (const file of ["session.json", "key.json"]) await rm2(join6(directory, file), { force: true });
|
|
18458
|
+
for (const entry of await readdir2(directory)) {
|
|
18459
|
+
if (entry !== ".lock" && entry !== "profile.json") await rm2(join6(directory, entry), { recursive: true, force: true });
|
|
18433
18460
|
}
|
|
18434
|
-
await rm2(join6(
|
|
18461
|
+
await rm2(join6(directory, "profile.json"), { force: true });
|
|
18435
18462
|
await recordProfileDeletion(name);
|
|
18436
18463
|
});
|
|
18437
|
-
await rmdir(profileDir).catch((error) => {
|
|
18438
|
-
if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY" && error.code !== "EEXIST") throw error;
|
|
18439
|
-
});
|
|
18440
18464
|
}
|
|
18441
18465
|
// ── Key management ──────────────────────────────────────────────────
|
|
18442
18466
|
/**
|
|
18443
18467
|
* Returns the parsed JWK for a profile, or null if no key exists.
|
|
18444
18468
|
*/
|
|
18445
18469
|
static async getKey(name) {
|
|
18446
|
-
return readJson(join6(
|
|
18470
|
+
return withPinnedProfileDirectory2(name, (directory) => directory ? readJson(join6(directory, "key.json")) : Promise.resolve(null));
|
|
18447
18471
|
}
|
|
18448
18472
|
/**
|
|
18449
18473
|
* Saves a JWK key under the profile lock (0600, in an owner-only profile
|
|
@@ -18458,7 +18482,7 @@ var init_profiles = __esm({
|
|
|
18458
18482
|
}
|
|
18459
18483
|
/** Removes key.json under the profile lock (rollback of a key a failed login created). */
|
|
18460
18484
|
static async removeKey(name) {
|
|
18461
|
-
await _ProfileManager.withLock(name, () => rm2(join6(
|
|
18485
|
+
await _ProfileManager.withLock(name, () => rm2(join6(profilePath3(name), "key.json"), { force: true }));
|
|
18462
18486
|
}
|
|
18463
18487
|
// ── Session management ──────────────────────────────────────────────
|
|
18464
18488
|
/**
|
|
@@ -18485,10 +18509,13 @@ var init_profiles = __esm({
|
|
|
18485
18509
|
* created or tightened to 0700.
|
|
18486
18510
|
*/
|
|
18487
18511
|
static async getCacheDir(name) {
|
|
18488
|
-
const
|
|
18489
|
-
|
|
18490
|
-
|
|
18491
|
-
|
|
18512
|
+
const alreadyPinned = profilePath3(name) !== namedProfilePath(name);
|
|
18513
|
+
return _ProfileManager.withLock(name, async () => {
|
|
18514
|
+
const cacheDir = join6(await _ProfileManager.ensureProfileDir(name), "cache");
|
|
18515
|
+
await ensureDir(cacheDir);
|
|
18516
|
+
await chmod2(cacheDir, PRIVATE_DIR_MODE);
|
|
18517
|
+
return alreadyPinned ? cacheDir : join6(namedProfilePath(name), "cache");
|
|
18518
|
+
});
|
|
18492
18519
|
}
|
|
18493
18520
|
// ── Resolution helpers ──────────────────────────────────────────────
|
|
18494
18521
|
/**
|
|
@@ -18512,7 +18539,8 @@ var init_profiles = __esm({
|
|
|
18512
18539
|
try {
|
|
18513
18540
|
profileConfig = await _ProfileManager.getProfile(profile);
|
|
18514
18541
|
profileHost = profileConfig.host;
|
|
18515
|
-
} catch {
|
|
18542
|
+
} catch (error) {
|
|
18543
|
+
if (error instanceof ProfileSymlinkError2) throw error;
|
|
18516
18544
|
}
|
|
18517
18545
|
const envReplication = process.env.TC_REPLICATION?.toLowerCase();
|
|
18518
18546
|
const envEnabled = envReplication === "1" || envReplication === "true" || envReplication === "on";
|
|
@@ -18876,22 +18904,23 @@ import { get as httpGet } from "http";
|
|
|
18876
18904
|
import { get as httpsGet } from "https";
|
|
18877
18905
|
import { spawn } from "child_process";
|
|
18878
18906
|
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
|
|
18879
|
-
import { dirname as
|
|
18907
|
+
import { dirname as dirname4 } from "path";
|
|
18880
18908
|
import { createInterface as createInterface2 } from "readline";
|
|
18881
18909
|
import { grantAuthRequest, principalDidEquals, signedCompactUcanExpiry as signedCompactUcanExpiry3 } from "@tinycloud/node-sdk";
|
|
18882
18910
|
import { invokeOperation } from "@tinycloud/operations";
|
|
18883
18911
|
|
|
18884
18912
|
// src/lib/profile-replicas.ts
|
|
18885
18913
|
init_dist3();
|
|
18886
|
-
import { lstat as
|
|
18914
|
+
import { lstat as lstat3, open as open3, readdir as readdir4, realpath, rm as rm4, rmdir as rmdir2, stat as stat3 } from "fs/promises";
|
|
18915
|
+
import { constants as fsConstants2 } from "fs";
|
|
18887
18916
|
import { join as join8, relative, sep } from "path";
|
|
18888
|
-
import { ProfileDeletedError as ProfileDeletedError2, profilePath as
|
|
18917
|
+
import { ProfileDeletedError as ProfileDeletedError2, profilePath as profilePath4, withProfileLock as withProfileLock4 } from "@tinycloud/operations/state";
|
|
18889
18918
|
|
|
18890
18919
|
// ../replica/dist/sqlite.js
|
|
18891
18920
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
18892
|
-
import { constants as fsConstants } from "fs";
|
|
18893
|
-
import { chmod as chmod3, link, mkdir as mkdir2, open as open2, readFile as readFile4, readdir as readdir3, rm as rm3, rmdir
|
|
18894
|
-
import { join as join7 } from "path";
|
|
18921
|
+
import { closeSync, constants as fsConstants, fstatSync, lstatSync, openSync, readdirSync as readdirSync2, readlinkSync } from "fs";
|
|
18922
|
+
import { chmod as chmod3, link, lstat as lstat2, mkdir as mkdir2, open as open2, readFile as readFile4, readdir as readdir3, rm as rm3, rmdir, stat as stat2, unlink } from "fs/promises";
|
|
18923
|
+
import { dirname as dirname3, join as join7, resolve } from "path";
|
|
18895
18924
|
var ReplicaErrorCode2 = {
|
|
18896
18925
|
/** Another process holds the sync lease. */
|
|
18897
18926
|
BUSY: "REPLICA_BUSY",
|
|
@@ -19078,6 +19107,29 @@ async function loadSqlite() {
|
|
|
19078
19107
|
return (path, options) => wrap(options?.readonly ? new DatabaseSync(path, { readOnly: true }) : new DatabaseSync(path));
|
|
19079
19108
|
}
|
|
19080
19109
|
var SCHEMA_VERSION = 1;
|
|
19110
|
+
var sqliteOpenQueue = Promise.resolve();
|
|
19111
|
+
async function acquireSqliteOpen() {
|
|
19112
|
+
const previous = sqliteOpenQueue;
|
|
19113
|
+
let release;
|
|
19114
|
+
sqliteOpenQueue = new Promise((resolve22) => {
|
|
19115
|
+
release = resolve22;
|
|
19116
|
+
});
|
|
19117
|
+
await previous;
|
|
19118
|
+
return release;
|
|
19119
|
+
}
|
|
19120
|
+
async function captureNamedPath(path) {
|
|
19121
|
+
const names = [];
|
|
19122
|
+
for (let current = resolve(path); ; current = dirname3(current)) {
|
|
19123
|
+
names.push(current);
|
|
19124
|
+
if (dirname3(current) === current) break;
|
|
19125
|
+
}
|
|
19126
|
+
const identities = [];
|
|
19127
|
+
for (const name of names.reverse()) {
|
|
19128
|
+
const stats = await lstat2(name, { bigint: true });
|
|
19129
|
+
identities.push({ path: name, dev: stats.dev, ino: stats.ino });
|
|
19130
|
+
}
|
|
19131
|
+
return identities;
|
|
19132
|
+
}
|
|
19081
19133
|
var DURABILITY = "sqlite-wal-synchronous-full+fsynced-blobs";
|
|
19082
19134
|
var SCHEMA = `
|
|
19083
19135
|
CREATE TABLE IF NOT EXISTS replica (
|
|
@@ -19176,22 +19228,37 @@ async function entriesOf(path) {
|
|
|
19176
19228
|
}
|
|
19177
19229
|
}
|
|
19178
19230
|
var FAULTS = /* @__PURE__ */ Symbol("tinycloud.replica.faults");
|
|
19231
|
+
var pinnedDirectories = new FinalizationRegistry((fd) => {
|
|
19232
|
+
try {
|
|
19233
|
+
closeSync(fd);
|
|
19234
|
+
} catch {
|
|
19235
|
+
}
|
|
19236
|
+
});
|
|
19179
19237
|
var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
19180
19238
|
dir;
|
|
19181
19239
|
#db;
|
|
19240
|
+
#pinnedDir;
|
|
19241
|
+
#directoryFd;
|
|
19182
19242
|
#holder = `${process.pid}-${randomBytes2(6).toString("hex")}`;
|
|
19183
19243
|
#now;
|
|
19184
19244
|
#faults;
|
|
19185
19245
|
#guard;
|
|
19186
19246
|
/** Identity of the database file this store opened: a deleted-and-recreated replica is a different file. */
|
|
19187
19247
|
#ino;
|
|
19188
|
-
|
|
19248
|
+
#dev;
|
|
19249
|
+
#namedAncestors;
|
|
19250
|
+
constructor(dir, db, identity, options, directoryFd, namedAncestors = []) {
|
|
19189
19251
|
this.dir = dir;
|
|
19252
|
+
this.#pinnedDir = directoryFd !== void 0 && process.platform === "linux" ? `/proc/self/fd/${directoryFd}` : dir;
|
|
19253
|
+
this.#directoryFd = directoryFd;
|
|
19254
|
+
if (directoryFd !== void 0) pinnedDirectories.register(this, directoryFd, this);
|
|
19190
19255
|
this.#db = db;
|
|
19191
|
-
this.#ino = ino;
|
|
19256
|
+
this.#ino = identity.ino;
|
|
19257
|
+
this.#dev = identity.dev;
|
|
19192
19258
|
this.#now = options.now ?? Date.now;
|
|
19193
19259
|
this.#faults = options[FAULTS] ?? {};
|
|
19194
19260
|
this.#guard = options.guard ?? ((section) => section());
|
|
19261
|
+
this.#namedAncestors = namedAncestors;
|
|
19195
19262
|
}
|
|
19196
19263
|
/**
|
|
19197
19264
|
* Read-only status snapshot; does not create a replica, migrate, recover
|
|
@@ -19204,9 +19271,10 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19204
19271
|
};
|
|
19205
19272
|
checkAbort();
|
|
19206
19273
|
const dbPath = join7(dir, "replica.db");
|
|
19207
|
-
let
|
|
19274
|
+
let identity;
|
|
19208
19275
|
try {
|
|
19209
|
-
|
|
19276
|
+
identity = await lstat2(dbPath);
|
|
19277
|
+
if (!identity.isFile()) return null;
|
|
19210
19278
|
} catch (error) {
|
|
19211
19279
|
if (errnoOf(error) === "ENOENT") return null;
|
|
19212
19280
|
throw storageError(error, "Inspecting the replica");
|
|
@@ -19219,14 +19287,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19219
19287
|
db = opener(dbPath, { readonly: true });
|
|
19220
19288
|
db.exec("PRAGMA busy_timeout = 0");
|
|
19221
19289
|
checkAbort();
|
|
19222
|
-
const store = new _SqliteReplicaStore(dir, db,
|
|
19290
|
+
const store = new _SqliteReplicaStore(dir, db, identity, { create: false, ...options.now ? { now: options.now } : {} });
|
|
19223
19291
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
19224
19292
|
const before = db.get("PRAGMA data_version").data_version;
|
|
19225
19293
|
const state2 = await store.open();
|
|
19226
19294
|
checkAbort();
|
|
19227
19295
|
if (state2 === null) return null;
|
|
19228
19296
|
const status = await store.status();
|
|
19229
|
-
await new Promise((
|
|
19297
|
+
await new Promise((resolve22) => setImmediate(resolve22));
|
|
19230
19298
|
checkAbort();
|
|
19231
19299
|
const after = db.get("PRAGMA data_version").data_version;
|
|
19232
19300
|
if (before === after) return { state: state2, status };
|
|
@@ -19246,65 +19314,145 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19246
19314
|
* REPLICA_NOT_FOUND and nothing is written to disk.
|
|
19247
19315
|
*/
|
|
19248
19316
|
static async open(dir, options) {
|
|
19249
|
-
const
|
|
19317
|
+
const namedDbPath = join7(dir, "replica.db");
|
|
19250
19318
|
if (!options.create) {
|
|
19251
|
-
const exists = await
|
|
19252
|
-
() =>
|
|
19319
|
+
const exists = await lstat2(namedDbPath).then(
|
|
19320
|
+
(stats) => stats.isFile(),
|
|
19253
19321
|
() => false
|
|
19254
19322
|
);
|
|
19255
|
-
if (!exists) throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, `No replica at ${dir}.`);
|
|
19323
|
+
if (!exists) throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, `No regular replica database at ${dir}.`);
|
|
19256
19324
|
}
|
|
19257
19325
|
const opener = options.sqlite ?? await loadSqlite();
|
|
19326
|
+
const releaseOpen = await acquireSqliteOpen();
|
|
19258
19327
|
let store;
|
|
19328
|
+
let directoryFd;
|
|
19259
19329
|
try {
|
|
19330
|
+
if (options.create) await mkdir2(dir, { recursive: true, mode: 448 });
|
|
19331
|
+
if (process.platform === "linux") {
|
|
19332
|
+
directoryFd = openSync(dir, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW);
|
|
19333
|
+
}
|
|
19334
|
+
const opened = directoryFd === void 0 ? lstatSync(dir, { bigint: true }) : fstatSync(directoryFd, { bigint: true });
|
|
19335
|
+
if (!opened.isDirectory() || opened.isSymbolicLink() || options.expectedDirectory && (opened.dev !== BigInt(options.expectedDirectory.dev) || opened.ino !== BigInt(options.expectedDirectory.ino))) {
|
|
19336
|
+
throw new Error("The replica directory changed before SQLite opened it.");
|
|
19337
|
+
}
|
|
19338
|
+
const namedAncestors = process.platform === "linux" ? [] : await captureNamedPath(dir);
|
|
19339
|
+
if (namedAncestors.length && (namedAncestors[namedAncestors.length - 1].dev !== opened.dev || namedAncestors[namedAncestors.length - 1].ino !== opened.ino)) {
|
|
19340
|
+
throw new Error("The replica directory changed while checking its ancestry.");
|
|
19341
|
+
}
|
|
19342
|
+
const pinnedDir = directoryFd === void 0 ? dir : `/proc/self/fd/${directoryFd}`;
|
|
19343
|
+
const dbPath = join7(pinnedDir, "replica.db");
|
|
19260
19344
|
if (options.create) {
|
|
19261
|
-
await mkdir2(join7(
|
|
19262
|
-
await Promise.all([chmod3(
|
|
19263
|
-
const handle = await open2(dbPath, fsConstants.O_CREAT | fsConstants.O_RDWR, 384)
|
|
19264
|
-
|
|
19265
|
-
|
|
19266
|
-
|
|
19267
|
-
|
|
19268
|
-
|
|
19269
|
-
|
|
19270
|
-
|
|
19271
|
-
|
|
19272
|
-
|
|
19273
|
-
|
|
19274
|
-
const version = db.get("PRAGMA user_version")?.user_version ?? 0;
|
|
19275
|
-
if (version > SCHEMA_VERSION) {
|
|
19276
|
-
db.close();
|
|
19277
|
-
throw new ReplicaError2(
|
|
19278
|
-
ReplicaErrorCode2.STORAGE_ERROR,
|
|
19279
|
-
`The replica at ${dir} was written by a newer tc (schema ${version}); upgrade tc to read it.`
|
|
19280
|
-
);
|
|
19345
|
+
await mkdir2(join7(pinnedDir, "blobs", ".tmp"), { recursive: true, mode: 448 });
|
|
19346
|
+
await Promise.all([chmod3(pinnedDir, 448), chmod3(join7(pinnedDir, "blobs"), 448), chmod3(join7(pinnedDir, "blobs", ".tmp"), 448)]);
|
|
19347
|
+
const handle = await open2(dbPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_RDWR | fsConstants.O_NOFOLLOW, 384).catch((error) => {
|
|
19348
|
+
if (errnoOf(error) === "EEXIST") return null;
|
|
19349
|
+
throw error;
|
|
19350
|
+
});
|
|
19351
|
+
if (handle) {
|
|
19352
|
+
try {
|
|
19353
|
+
await handle.chmod(384);
|
|
19354
|
+
} finally {
|
|
19355
|
+
await handle.close();
|
|
19356
|
+
}
|
|
19357
|
+
}
|
|
19281
19358
|
}
|
|
19282
|
-
|
|
19283
|
-
|
|
19284
|
-
|
|
19285
|
-
|
|
19286
|
-
|
|
19359
|
+
const leaf = await open2(dbPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
19360
|
+
let db;
|
|
19361
|
+
try {
|
|
19362
|
+
const identity = await leaf.stat();
|
|
19363
|
+
const named = await lstat2(dbPath);
|
|
19364
|
+
if (!identity.isFile() || !named.isFile() || named.dev !== identity.dev || named.ino !== identity.ino) {
|
|
19365
|
+
throw new Error("The replica database changed before SQLite opened it.");
|
|
19366
|
+
}
|
|
19367
|
+
const previousFds = process.platform === "linux" && !options.sqlite ? new Map(readdirSync2("/proc/self/fd").map((fd) => {
|
|
19368
|
+
try {
|
|
19369
|
+
return [fd, readlinkSync(`/proc/self/fd/${fd}`)];
|
|
19370
|
+
} catch {
|
|
19371
|
+
return [fd, ""];
|
|
19372
|
+
}
|
|
19373
|
+
})) : null;
|
|
19374
|
+
db = opener(dbPath);
|
|
19375
|
+
const afterOpen = lstatSync(dbPath);
|
|
19376
|
+
if (!afterOpen.isFile() || afterOpen.dev !== identity.dev || afterOpen.ino !== identity.ino) {
|
|
19377
|
+
throw new Error("The replica database changed while SQLite opened it.");
|
|
19378
|
+
}
|
|
19379
|
+
if (previousFds) db.get("SELECT count(*) FROM sqlite_master");
|
|
19380
|
+
if (previousFds) {
|
|
19381
|
+
const databases = readdirSync2("/proc/self/fd").filter((fd) => {
|
|
19382
|
+
try {
|
|
19383
|
+
const target = readlinkSync(`/proc/self/fd/${fd}`);
|
|
19384
|
+
return target.endsWith("/replica.db") && previousFds.get(fd) !== target;
|
|
19385
|
+
} catch {
|
|
19386
|
+
return false;
|
|
19387
|
+
}
|
|
19388
|
+
});
|
|
19389
|
+
if (databases.length === 0 || databases.some((fd) => {
|
|
19390
|
+
try {
|
|
19391
|
+
const actual = fstatSync(Number(fd));
|
|
19392
|
+
return actual.dev !== identity.dev || actual.ino !== identity.ino;
|
|
19393
|
+
} catch {
|
|
19394
|
+
return true;
|
|
19395
|
+
}
|
|
19396
|
+
})) {
|
|
19397
|
+
throw new Error("SQLite opened a different replica database.");
|
|
19398
|
+
}
|
|
19399
|
+
}
|
|
19400
|
+
const after = lstatSync(dbPath);
|
|
19401
|
+
if (!after.isFile() || after.dev !== identity.dev || after.ino !== identity.ino) {
|
|
19402
|
+
throw new Error("The replica database changed while SQLite opened it.");
|
|
19403
|
+
}
|
|
19404
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
19405
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
19406
|
+
db.exec("PRAGMA synchronous = FULL");
|
|
19407
|
+
db.exec("PRAGMA secure_delete = ON");
|
|
19408
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
19409
|
+
const version = db.get("PRAGMA user_version")?.user_version ?? 0;
|
|
19410
|
+
if (version > SCHEMA_VERSION) {
|
|
19411
|
+
throw new ReplicaError2(
|
|
19412
|
+
ReplicaErrorCode2.STORAGE_ERROR,
|
|
19413
|
+
`The replica at ${dir} was written by a newer tc (schema ${version}); upgrade tc to read it.`
|
|
19414
|
+
);
|
|
19415
|
+
}
|
|
19416
|
+
if (version < SCHEMA_VERSION) {
|
|
19417
|
+
db.exec("BEGIN IMMEDIATE");
|
|
19418
|
+
db.exec(SCHEMA);
|
|
19419
|
+
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
19420
|
+
db.exec("COMMIT");
|
|
19421
|
+
}
|
|
19422
|
+
store = new _SqliteReplicaStore(dir, db, identity, options, directoryFd, namedAncestors);
|
|
19423
|
+
db = void 0;
|
|
19424
|
+
} finally {
|
|
19425
|
+
try {
|
|
19426
|
+
db?.close();
|
|
19427
|
+
} finally {
|
|
19428
|
+
await leaf.close();
|
|
19429
|
+
}
|
|
19287
19430
|
}
|
|
19288
|
-
store = new _SqliteReplicaStore(dir, db, ino, options);
|
|
19289
19431
|
} catch (error) {
|
|
19432
|
+
if (store) await store.close().catch(() => void 0);
|
|
19433
|
+
else if (directoryFd !== void 0) closeSync(directoryFd);
|
|
19290
19434
|
throw storageError(error, `Opening the replica at ${dir}`);
|
|
19435
|
+
} finally {
|
|
19436
|
+
releaseOpen();
|
|
19291
19437
|
}
|
|
19292
|
-
|
|
19293
|
-
|
|
19294
|
-
await store.#
|
|
19438
|
+
try {
|
|
19439
|
+
const row = store.#row();
|
|
19440
|
+
if (row?.revoked_detail != null && (row.purge_pending === 1 || await store.#holdsContentFiles().catch(() => true))) {
|
|
19441
|
+
await store.#finishRevocationPurge().catch(() => void 0);
|
|
19442
|
+
}
|
|
19443
|
+
return store;
|
|
19444
|
+
} catch (error) {
|
|
19445
|
+
await store.close().catch(() => void 0);
|
|
19446
|
+
throw error;
|
|
19295
19447
|
}
|
|
19296
|
-
return store;
|
|
19297
19448
|
}
|
|
19298
19449
|
#row() {
|
|
19299
19450
|
return this.#db.get("SELECT * FROM replica WHERE id = 1");
|
|
19300
19451
|
}
|
|
19301
19452
|
/** Refuse to touch the filesystem for a replica that was deleted (or replaced) under us. */
|
|
19302
19453
|
async #assertPresent() {
|
|
19303
|
-
const
|
|
19304
|
-
|
|
19305
|
-
() => void 0
|
|
19306
|
-
);
|
|
19307
|
-
if (ino !== this.#ino) {
|
|
19454
|
+
const identity = await lstat2(join7(this.#pinnedDir, "replica.db")).catch(() => null);
|
|
19455
|
+
if (!identity?.isFile() || identity.dev !== this.#dev || identity.ino !== this.#ino) {
|
|
19308
19456
|
throw new ReplicaError2(
|
|
19309
19457
|
ReplicaErrorCode2.NOT_FOUND,
|
|
19310
19458
|
`The replica at ${this.dir} was deleted while this command ran (its profile was deleted or the replica purged); nothing more was written.`
|
|
@@ -19534,7 +19682,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19534
19682
|
return held;
|
|
19535
19683
|
}
|
|
19536
19684
|
#blobPath(hash) {
|
|
19537
|
-
return join7(this
|
|
19685
|
+
return join7(this.#pinnedDir, "blobs", hash.slice(0, 2), hash);
|
|
19538
19686
|
}
|
|
19539
19687
|
/**
|
|
19540
19688
|
* Make every blob durable before any row references it: write to a fresh
|
|
@@ -19551,11 +19699,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19551
19699
|
() => false
|
|
19552
19700
|
);
|
|
19553
19701
|
if (exists) continue;
|
|
19554
|
-
const shard = join7(this
|
|
19702
|
+
const shard = join7(this.#pinnedDir, "blobs", hash.slice(0, 2));
|
|
19555
19703
|
await mkdir2(shard, { mode: 448 }).catch((error) => {
|
|
19556
19704
|
if (errnoOf(error) !== "EEXIST") throw error;
|
|
19557
19705
|
});
|
|
19558
|
-
const temp = join7(this
|
|
19706
|
+
const temp = join7(this.#pinnedDir, "blobs", ".tmp", `${hash}.${randomBytes2(6).toString("hex")}`);
|
|
19559
19707
|
const handle = await open2(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 384);
|
|
19560
19708
|
try {
|
|
19561
19709
|
await handle.writeFile(bytes);
|
|
@@ -19571,7 +19719,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19571
19719
|
await unlink(temp);
|
|
19572
19720
|
}
|
|
19573
19721
|
dirs.add(shard);
|
|
19574
|
-
dirs.add(join7(this
|
|
19722
|
+
dirs.add(join7(this.#pinnedDir, "blobs"));
|
|
19575
19723
|
}
|
|
19576
19724
|
for (const dir of dirs) {
|
|
19577
19725
|
const handle = await open2(dir, fsConstants.O_RDONLY);
|
|
@@ -19679,7 +19827,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19679
19827
|
/** Unlink every blob not in `keep`, and every temp file. Only a missing directory counts as empty. */
|
|
19680
19828
|
async #unlinkBlobs(keep) {
|
|
19681
19829
|
let removed = 0;
|
|
19682
|
-
const root = join7(this
|
|
19830
|
+
const root = join7(this.#pinnedDir, "blobs");
|
|
19683
19831
|
for (const shard of await entriesOf(root)) {
|
|
19684
19832
|
const shardPath = join7(root, shard);
|
|
19685
19833
|
if (shard === ".tmp") {
|
|
@@ -19698,13 +19846,13 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19698
19846
|
await rm3(join7(shardPath, name), { force: true });
|
|
19699
19847
|
removed += 1;
|
|
19700
19848
|
}
|
|
19701
|
-
if (kept === 0) await
|
|
19849
|
+
if (kept === 0) await rmdir(shardPath).catch(() => void 0);
|
|
19702
19850
|
}
|
|
19703
19851
|
return removed;
|
|
19704
19852
|
}
|
|
19705
19853
|
/** Whether any content file (a blob or a temp file) is on disk. */
|
|
19706
19854
|
async #holdsContentFiles() {
|
|
19707
|
-
const root = join7(this
|
|
19855
|
+
const root = join7(this.#pinnedDir, "blobs");
|
|
19708
19856
|
for (const shard of await entriesOf(root)) {
|
|
19709
19857
|
if ((await entriesOf(join7(root, shard))).length > 0) return true;
|
|
19710
19858
|
}
|
|
@@ -19770,8 +19918,93 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19770
19918
|
}
|
|
19771
19919
|
try {
|
|
19772
19920
|
this.#checkLease(t, { allowRevoked: true });
|
|
19921
|
+
const expected = this.#directoryFd === void 0 ? this.#namedAncestors.at(-1) : fstatSync(this.#directoryFd, { bigint: true });
|
|
19922
|
+
if (!expected) {
|
|
19923
|
+
throw new ReplicaError2(ReplicaErrorCode2.RUNTIME_UNSUPPORTED, "Replica removal requires an opened or identity-checked directory.");
|
|
19924
|
+
}
|
|
19925
|
+
const assertDirectory2 = async () => {
|
|
19926
|
+
const current = await lstat2(this.dir, { bigint: true });
|
|
19927
|
+
if (!current.isDirectory() || current.dev !== expected.dev || current.ino !== expected.ino) {
|
|
19928
|
+
throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica directory changed during removal.");
|
|
19929
|
+
}
|
|
19930
|
+
return current;
|
|
19931
|
+
};
|
|
19932
|
+
const pinned = await assertDirectory2();
|
|
19773
19933
|
await this.#assertPresent();
|
|
19774
|
-
|
|
19934
|
+
if (process.platform === "linux") {
|
|
19935
|
+
for (const entry of await readdir3(this.#pinnedDir)) {
|
|
19936
|
+
await rm3(join7(this.#pinnedDir, entry), { recursive: true, force: true });
|
|
19937
|
+
}
|
|
19938
|
+
await assertDirectory2();
|
|
19939
|
+
await rmdir(this.dir);
|
|
19940
|
+
} else {
|
|
19941
|
+
const ancestors = this.#namedAncestors;
|
|
19942
|
+
const checkAncestors = async () => {
|
|
19943
|
+
for (const expected2 of ancestors) {
|
|
19944
|
+
const current = await lstat2(expected2.path, { bigint: true });
|
|
19945
|
+
if (current.dev !== expected2.dev || current.ino !== expected2.ino) {
|
|
19946
|
+
throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica ancestry changed during removal.");
|
|
19947
|
+
}
|
|
19948
|
+
}
|
|
19949
|
+
};
|
|
19950
|
+
const directories = /* @__PURE__ */ new Map([[this.dir, pinned]]);
|
|
19951
|
+
const files = /* @__PURE__ */ new Map();
|
|
19952
|
+
const check = async (path, expected2, directory) => {
|
|
19953
|
+
if (path !== this.dir) {
|
|
19954
|
+
const parent = dirname3(path);
|
|
19955
|
+
await check(parent, directories.get(parent), true);
|
|
19956
|
+
}
|
|
19957
|
+
await checkAncestors();
|
|
19958
|
+
const current = await lstat2(path, { bigint: true });
|
|
19959
|
+
if ((directory ? !current.isDirectory() : !current.isFile()) || current.dev !== expected2.dev || current.ino !== expected2.ino) {
|
|
19960
|
+
throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica layout changed during removal.");
|
|
19961
|
+
}
|
|
19962
|
+
};
|
|
19963
|
+
const record2 = async (path, directory) => {
|
|
19964
|
+
const parent = dirname3(path);
|
|
19965
|
+
await check(parent, directories.get(parent), true);
|
|
19966
|
+
const current = await lstat2(path, { bigint: true });
|
|
19967
|
+
if (!(directory ? current.isDirectory() : current.isFile())) {
|
|
19968
|
+
throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica layout during removal.");
|
|
19969
|
+
}
|
|
19970
|
+
(directory ? directories : files).set(path, current);
|
|
19971
|
+
};
|
|
19972
|
+
const blobs = join7(this.dir, "blobs");
|
|
19973
|
+
for (const name of await readdir3(this.dir)) {
|
|
19974
|
+
if (name === "blobs") await record2(blobs, true);
|
|
19975
|
+
else if (["replica.db", "replica.db-wal", "replica.db-shm"].includes(name)) await record2(join7(this.dir, name), false);
|
|
19976
|
+
else throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica layout during removal.");
|
|
19977
|
+
}
|
|
19978
|
+
if (directories.has(blobs)) {
|
|
19979
|
+
await check(blobs, directories.get(blobs), true);
|
|
19980
|
+
for (const shard of await readdir3(blobs)) {
|
|
19981
|
+
if (shard !== ".tmp" && !/^[0-9a-f]{2}$/.test(shard)) {
|
|
19982
|
+
throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica blob shard during removal.");
|
|
19983
|
+
}
|
|
19984
|
+
const shardPath = join7(blobs, shard);
|
|
19985
|
+
await record2(shardPath, true);
|
|
19986
|
+
await check(shardPath, directories.get(shardPath), true);
|
|
19987
|
+
for (const name of await readdir3(shardPath)) {
|
|
19988
|
+
if (shard === ".tmp" ? !/^[0-9a-f]{64}\.[0-9a-f]{12}$/.test(name) : !isHash(name) || !name.startsWith(shard)) {
|
|
19989
|
+
throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica blob file during removal.");
|
|
19990
|
+
}
|
|
19991
|
+
await record2(join7(shardPath, name), false);
|
|
19992
|
+
}
|
|
19993
|
+
}
|
|
19994
|
+
}
|
|
19995
|
+
for (const [path, identity] of files) {
|
|
19996
|
+
await check(path, identity, false);
|
|
19997
|
+
await unlink(path);
|
|
19998
|
+
}
|
|
19999
|
+
for (const [path, identity] of [...directories].reverse()) {
|
|
20000
|
+
if (path === this.dir) continue;
|
|
20001
|
+
await check(path, identity, true);
|
|
20002
|
+
await rmdir(path);
|
|
20003
|
+
}
|
|
20004
|
+
await assertDirectory2();
|
|
20005
|
+
await checkAncestors();
|
|
20006
|
+
await rmdir(this.dir);
|
|
20007
|
+
}
|
|
19775
20008
|
} catch (error) {
|
|
19776
20009
|
throw storageError(error, "Removing the replica");
|
|
19777
20010
|
} finally {
|
|
@@ -19937,7 +20170,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19937
20170
|
};
|
|
19938
20171
|
}
|
|
19939
20172
|
async close() {
|
|
19940
|
-
this.#
|
|
20173
|
+
const fd = this.#directoryFd;
|
|
20174
|
+
this.#directoryFd = void 0;
|
|
20175
|
+
if (fd !== void 0) pinnedDirectories.unregister(this);
|
|
20176
|
+
try {
|
|
20177
|
+
this.#db.close();
|
|
20178
|
+
} finally {
|
|
20179
|
+
if (fd !== void 0) closeSync(fd);
|
|
20180
|
+
}
|
|
19941
20181
|
}
|
|
19942
20182
|
};
|
|
19943
20183
|
|
|
@@ -19945,229 +20185,388 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19945
20185
|
init_errors();
|
|
19946
20186
|
init_constants();
|
|
19947
20187
|
var SYNC_LEASE_MS = 6e4;
|
|
19948
|
-
function refuseUnsafeReplicaPath(profile, detail) {
|
|
20188
|
+
function refuseUnsafeReplicaPath(profile, detail, operation = "logout") {
|
|
19949
20189
|
return new CLIError(
|
|
19950
20190
|
"REPLICA_PURGE_FAILED",
|
|
19951
|
-
`Logout for profile "${profile}" cleared the session, but ${detail}; the replicas were left untouched.`,
|
|
20191
|
+
operation === "logout" ? `Logout for profile "${profile}" cleared the session, but ${detail}; the replicas were left untouched.` : `Cannot reset a replica in profile "${profile}": ${detail}; no replica was removed.`,
|
|
19952
20192
|
ExitCode.ERROR
|
|
19953
20193
|
);
|
|
19954
20194
|
}
|
|
19955
|
-
async function safeReplicaRoot(profile, root) {
|
|
20195
|
+
async function safeReplicaRoot(profile, root, operation = "logout") {
|
|
19956
20196
|
let rootStats;
|
|
19957
20197
|
try {
|
|
19958
|
-
rootStats = await
|
|
20198
|
+
rootStats = root.startsWith("/proc/self/fd/") ? await stat3(root) : await lstat3(root);
|
|
19959
20199
|
} catch (error) {
|
|
19960
20200
|
if (error?.code === "ENOENT") return null;
|
|
19961
20201
|
throw error;
|
|
19962
20202
|
}
|
|
19963
20203
|
if (rootStats.isSymbolicLink()) {
|
|
19964
|
-
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" is a symlink
|
|
20204
|
+
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" is a symlink`, operation);
|
|
19965
20205
|
}
|
|
19966
20206
|
if (!rootStats.isDirectory()) {
|
|
19967
|
-
throw refuseUnsafeReplicaPath(profile, `the replicas path "${root}" is not a real directory
|
|
20207
|
+
throw refuseUnsafeReplicaPath(profile, `the replicas path "${root}" is not a real directory`, operation);
|
|
19968
20208
|
}
|
|
19969
|
-
const [profileRealPath, rootRealPath] = await Promise.all([realpath(
|
|
20209
|
+
const [profileRealPath, rootRealPath] = await Promise.all([realpath(profilePath4(profile)), realpath(root)]);
|
|
19970
20210
|
if (!rootRealPath.startsWith(profileRealPath + sep)) {
|
|
19971
|
-
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" resolves outside the selected profile
|
|
20211
|
+
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" resolves outside the selected profile`, operation);
|
|
19972
20212
|
}
|
|
19973
20213
|
return rootRealPath;
|
|
19974
20214
|
}
|
|
19975
|
-
async function safeReplicaEntry(profile, root, entry) {
|
|
20215
|
+
async function safeReplicaEntry(profile, root, entry, operation = "logout") {
|
|
19976
20216
|
const entryPath = join8(root, entry.name);
|
|
19977
|
-
const rootRealPath = await safeReplicaRoot(profile, root);
|
|
20217
|
+
const rootRealPath = await safeReplicaRoot(profile, root, operation);
|
|
19978
20218
|
if (rootRealPath === null) {
|
|
19979
|
-
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" disappeared
|
|
20219
|
+
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" disappeared`, operation);
|
|
19980
20220
|
}
|
|
19981
|
-
const entryStats = await
|
|
20221
|
+
const entryStats = await lstat3(entryPath);
|
|
19982
20222
|
if (entry.isSymbolicLink() || entryStats.isSymbolicLink()) {
|
|
19983
|
-
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is a symlink
|
|
20223
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is a symlink`, operation);
|
|
19984
20224
|
}
|
|
19985
20225
|
if (!entry.isDirectory() || !entryStats.isDirectory()) {
|
|
19986
|
-
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is not a real directory
|
|
20226
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is not a real directory`, operation);
|
|
19987
20227
|
}
|
|
19988
20228
|
const entryRealPath = await realpath(entryPath);
|
|
19989
20229
|
if (!entryRealPath.startsWith(rootRealPath + sep)) {
|
|
19990
|
-
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" resolves outside the replicas directory
|
|
20230
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" resolves outside the replicas directory`, operation);
|
|
20231
|
+
}
|
|
20232
|
+
return { path: entryPath, identity: { dev: entryStats.dev, ino: entryStats.ino } };
|
|
20233
|
+
}
|
|
20234
|
+
async function inspectLegacyReplica(profile, root, entry) {
|
|
20235
|
+
const { path, identity } = await safeReplicaEntry(profile, root, entry);
|
|
20236
|
+
const handle = await open3(path, fsConstants2.O_RDONLY | fsConstants2.O_DIRECTORY | fsConstants2.O_NOFOLLOW);
|
|
20237
|
+
try {
|
|
20238
|
+
const pinnedEntry = await handle.stat();
|
|
20239
|
+
if (pinnedEntry.dev !== identity.dev || pinnedEntry.ino !== identity.ino) {
|
|
20240
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" changed during inspection`);
|
|
20241
|
+
}
|
|
20242
|
+
const children = await readdir4(`/proc/self/fd/${handle.fd}`, { withFileTypes: true });
|
|
20243
|
+
if (children.length > 0 && !children.some((child) => child.name === "replica.db" && child.isFile())) {
|
|
20244
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" has files but no regular replica database`);
|
|
20245
|
+
}
|
|
20246
|
+
return { path, identity, empty: children.length === 0 };
|
|
20247
|
+
} finally {
|
|
20248
|
+
await handle.close();
|
|
20249
|
+
}
|
|
20250
|
+
}
|
|
20251
|
+
async function pinReplicaRoot(profile, named, operation) {
|
|
20252
|
+
const initial = await lstat3(named).catch((error) => {
|
|
20253
|
+
if (error.code === "ENOENT") return null;
|
|
20254
|
+
throw error;
|
|
20255
|
+
});
|
|
20256
|
+
if (await safeReplicaRoot(profile, named, operation) === null) return null;
|
|
20257
|
+
if (process.platform !== "linux") {
|
|
20258
|
+
throw refuseUnsafeReplicaPath(profile, "pinned replica removal is unavailable on this platform", operation);
|
|
20259
|
+
}
|
|
20260
|
+
const handle = await open3(named, fsConstants2.O_RDONLY | fsConstants2.O_DIRECTORY | fsConstants2.O_NOFOLLOW);
|
|
20261
|
+
try {
|
|
20262
|
+
const pinned = await handle.stat();
|
|
20263
|
+
const assertNamed = async () => {
|
|
20264
|
+
const current = await lstat3(named);
|
|
20265
|
+
if (!current.isDirectory() || current.dev !== initial?.dev || current.ino !== initial?.ino || current.dev !== pinned.dev || current.ino !== pinned.ino) {
|
|
20266
|
+
throw refuseUnsafeReplicaPath(profile, "the replicas directory changed during cleanup", operation);
|
|
20267
|
+
}
|
|
20268
|
+
};
|
|
20269
|
+
await assertNamed();
|
|
20270
|
+
return { path: `/proc/self/fd/${handle.fd}`, assertNamed, close: () => handle.close() };
|
|
20271
|
+
} catch (error) {
|
|
20272
|
+
await handle.close();
|
|
20273
|
+
throw error;
|
|
19991
20274
|
}
|
|
19992
|
-
return entryPath;
|
|
19993
20275
|
}
|
|
19994
|
-
async function
|
|
20276
|
+
async function withPinnedReplicaForReset(profile, name, action) {
|
|
19995
20277
|
return withProfileLock4(profile, async () => {
|
|
19996
|
-
const root = join8(
|
|
19997
|
-
|
|
19998
|
-
|
|
19999
|
-
if (replicas.length === 0) return [];
|
|
20000
|
-
for (const entry of replicas) await safeReplicaEntry(profile, root, entry);
|
|
20001
|
-
await loadSqlite();
|
|
20002
|
-
const opened = [];
|
|
20278
|
+
const root = join8(profilePath4(profile), "replicas");
|
|
20279
|
+
const pinned = await pinReplicaRoot(profile, root, "reset");
|
|
20280
|
+
if (!pinned) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas.`);
|
|
20003
20281
|
try {
|
|
20004
|
-
|
|
20005
|
-
|
|
20006
|
-
|
|
20007
|
-
|
|
20008
|
-
|
|
20009
|
-
|
|
20010
|
-
|
|
20011
|
-
|
|
20012
|
-
|
|
20013
|
-
|
|
20282
|
+
const fdRoot = pinned.path;
|
|
20283
|
+
const entry = (await readdir4(fdRoot, { withFileTypes: true })).find((item) => item.name === name);
|
|
20284
|
+
if (!entry) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Replica "${name}" was not found.`);
|
|
20285
|
+
const { path, identity: initial } = await safeReplicaEntry(profile, fdRoot, entry, "reset");
|
|
20286
|
+
const recheck = async () => {
|
|
20287
|
+
const current = await lstat3(path);
|
|
20288
|
+
if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== initial.dev || current.ino !== initial.ino) {
|
|
20289
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${name}" changed during removal`, "reset");
|
|
20290
|
+
}
|
|
20291
|
+
};
|
|
20292
|
+
await recheck();
|
|
20293
|
+
return await action(path, recheck, { dev: initial.dev, ino: initial.ino });
|
|
20294
|
+
} finally {
|
|
20295
|
+
await pinned.close();
|
|
20296
|
+
}
|
|
20297
|
+
});
|
|
20298
|
+
}
|
|
20299
|
+
async function removeProfileReplicas(profile, heldLeases, preflightRoot) {
|
|
20300
|
+
return withProfileLock4(profile, async () => {
|
|
20301
|
+
const namedRoot = join8(profilePath4(profile), "replicas");
|
|
20302
|
+
const pinned = preflightRoot ?? await pinReplicaRoot(profile, namedRoot, "logout");
|
|
20303
|
+
if (!pinned) return [];
|
|
20304
|
+
if (preflightRoot) await pinned.assertNamed();
|
|
20305
|
+
try {
|
|
20306
|
+
const root = pinned.path;
|
|
20307
|
+
const replicas = (await readdir4(root, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
20308
|
+
if (replicas.length === 0) return [];
|
|
20309
|
+
const checked = await Promise.all(replicas.map(async (entry) => ({
|
|
20310
|
+
entry,
|
|
20311
|
+
...await inspectLegacyReplica(profile, root, entry)
|
|
20312
|
+
})));
|
|
20313
|
+
const removed = [];
|
|
20314
|
+
const opened = [];
|
|
20315
|
+
try {
|
|
20316
|
+
for (const { entry, identity, empty: empty4 } of checked) {
|
|
20317
|
+
if (!empty4) continue;
|
|
20318
|
+
await pinned.assertNamed();
|
|
20319
|
+
const current = await safeReplicaEntry(profile, root, entry);
|
|
20320
|
+
if (current.identity.dev !== identity.dev || current.identity.ino !== identity.ino) {
|
|
20321
|
+
throw refuseUnsafeReplicaPath(profile, `empty replica entry "${entry.name}" changed during cleanup`);
|
|
20322
|
+
}
|
|
20323
|
+
const handle = await open3(current.path, fsConstants2.O_RDONLY | fsConstants2.O_DIRECTORY | fsConstants2.O_NOFOLLOW);
|
|
20324
|
+
try {
|
|
20325
|
+
const pinnedEntry = await handle.stat();
|
|
20326
|
+
if (pinnedEntry.dev !== identity.dev || pinnedEntry.ino !== identity.ino || (await readdir4(`/proc/self/fd/${handle.fd}`)).length !== 0) {
|
|
20327
|
+
throw refuseUnsafeReplicaPath(profile, `empty replica entry "${entry.name}" changed during cleanup`);
|
|
20328
|
+
}
|
|
20329
|
+
await pinned.assertNamed();
|
|
20330
|
+
const namedEntry = await lstat3(current.path);
|
|
20331
|
+
if (!namedEntry.isDirectory() || namedEntry.dev !== pinnedEntry.dev || namedEntry.ino !== pinnedEntry.ino) {
|
|
20332
|
+
throw refuseUnsafeReplicaPath(profile, `empty replica entry "${entry.name}" changed at removal`);
|
|
20333
|
+
}
|
|
20334
|
+
await rmdir2(current.path);
|
|
20335
|
+
} finally {
|
|
20336
|
+
await handle.close();
|
|
20337
|
+
}
|
|
20338
|
+
removed.push(entry.name);
|
|
20339
|
+
}
|
|
20340
|
+
if (removed.length === replicas.length) return removed;
|
|
20341
|
+
await loadSqlite();
|
|
20342
|
+
for (const { entry, empty: empty4 } of checked) {
|
|
20343
|
+
if (empty4) continue;
|
|
20344
|
+
const { path: entryPath, identity: expectedDirectory } = await safeReplicaEntry(profile, root, entry);
|
|
20345
|
+
const store = await SqliteReplicaStore.open(entryPath, {
|
|
20346
|
+
create: false,
|
|
20347
|
+
expectedDirectory,
|
|
20348
|
+
guard: async (action) => {
|
|
20349
|
+
try {
|
|
20350
|
+
return await withProfileLock4(profile, async () => {
|
|
20351
|
+
await safeReplicaEntry(profile, root, entry);
|
|
20352
|
+
return action();
|
|
20353
|
+
}, { requireProfile: true });
|
|
20354
|
+
} catch (error) {
|
|
20355
|
+
if (error instanceof ProfileDeletedError2) {
|
|
20356
|
+
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" was deleted; its replicas are gone and nothing was written.`);
|
|
20357
|
+
}
|
|
20358
|
+
throw error;
|
|
20014
20359
|
}
|
|
20015
|
-
throw error;
|
|
20016
20360
|
}
|
|
20361
|
+
});
|
|
20362
|
+
opened.push({ name: entry.name, store, lease: null });
|
|
20363
|
+
if (await store.open() === null) {
|
|
20364
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
|
|
20017
20365
|
}
|
|
20018
|
-
|
|
20019
|
-
|
|
20020
|
-
|
|
20021
|
-
|
|
20022
|
-
|
|
20023
|
-
const lease = heldLeases?.get(entryPath) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20024
|
-
if (lease === null) {
|
|
20025
|
-
throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
20366
|
+
const lease = heldLeases?.get(join8(namedRoot, entry.name)) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20367
|
+
if (lease === null) {
|
|
20368
|
+
throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
20369
|
+
}
|
|
20370
|
+
opened[opened.length - 1].lease = lease;
|
|
20026
20371
|
}
|
|
20027
|
-
|
|
20028
|
-
|
|
20029
|
-
|
|
20030
|
-
|
|
20031
|
-
|
|
20032
|
-
|
|
20033
|
-
|
|
20034
|
-
|
|
20035
|
-
|
|
20036
|
-
()
|
|
20037
|
-
(
|
|
20038
|
-
|
|
20039
|
-
|
|
20040
|
-
|
|
20041
|
-
|
|
20042
|
-
|
|
20043
|
-
|
|
20044
|
-
|
|
20045
|
-
{ replicasRemoved: removed, replicasRemaining: remaining }
|
|
20046
|
-
);
|
|
20372
|
+
for (const replica of opened) {
|
|
20373
|
+
try {
|
|
20374
|
+
await replica.store.destroy(replica.lease);
|
|
20375
|
+
removed.push(replica.name);
|
|
20376
|
+
} catch (error) {
|
|
20377
|
+
const replicaStillExists = await lstat3(join8(root, replica.name)).then(
|
|
20378
|
+
() => true,
|
|
20379
|
+
(statError) => statError.code !== "ENOENT"
|
|
20380
|
+
);
|
|
20381
|
+
if (!replicaStillExists) removed.push(replica.name);
|
|
20382
|
+
const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
|
|
20383
|
+
throw new CLIError(
|
|
20384
|
+
"REPLICA_PURGE_FAILED",
|
|
20385
|
+
`Logout cleared the session, but replica removal failed. Removed: ${removed.length ? removed.join(", ") : "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
|
|
20386
|
+
ExitCode.ERROR,
|
|
20387
|
+
{ replicasRemoved: removed, replicasRemaining: remaining }
|
|
20388
|
+
);
|
|
20389
|
+
}
|
|
20047
20390
|
}
|
|
20391
|
+
return removed;
|
|
20392
|
+
} catch (error) {
|
|
20393
|
+
if (error instanceof CLIError) throw error;
|
|
20394
|
+
throw new CLIError(
|
|
20395
|
+
"REPLICA_PURGE_FAILED",
|
|
20396
|
+
`Logout cleared the session, but replica cleanup could not finish. Removed: ${removed.length ? removed.join(", ") : "none"}. ${error instanceof Error ? error.message : String(error)}`,
|
|
20397
|
+
ExitCode.ERROR
|
|
20398
|
+
);
|
|
20399
|
+
} finally {
|
|
20400
|
+
await Promise.all(opened.map(async ({ name, store, lease }) => {
|
|
20401
|
+
if (lease !== null && heldLeases?.has(join8(namedRoot, name)) !== true) await store.releaseLease(lease).catch(() => void 0);
|
|
20402
|
+
await store.close().catch(() => void 0);
|
|
20403
|
+
}));
|
|
20048
20404
|
}
|
|
20049
|
-
return removed;
|
|
20050
|
-
} catch (error) {
|
|
20051
|
-
if (error instanceof CLIError) throw error;
|
|
20052
|
-
throw new CLIError(
|
|
20053
|
-
"REPLICA_PURGE_FAILED",
|
|
20054
|
-
`Logout cleared the session, but replica cleanup could not start; no replicas were removed. ${error instanceof Error ? error.message : String(error)}`,
|
|
20055
|
-
ExitCode.ERROR
|
|
20056
|
-
);
|
|
20057
20405
|
} finally {
|
|
20058
|
-
await
|
|
20059
|
-
if (lease !== null && heldLeases?.has(join8(root, name)) !== true) await store.releaseLease(lease).catch(() => void 0);
|
|
20060
|
-
await store.close().catch(() => void 0);
|
|
20061
|
-
}));
|
|
20406
|
+
if (!preflightRoot) await pinned.close();
|
|
20062
20407
|
}
|
|
20063
20408
|
});
|
|
20064
20409
|
}
|
|
20065
|
-
async function
|
|
20410
|
+
async function assertSafeFlagReplica(profile, root, partition, name) {
|
|
20411
|
+
const rootReal = await safeReplicaRoot(profile, root);
|
|
20412
|
+
if (rootReal === null) throw refuseUnsafeReplicaPath(profile, `the replication root "${root}" disappeared`);
|
|
20413
|
+
const partitionPath = join8(root, partition);
|
|
20414
|
+
const partitionStats = await lstat3(partitionPath);
|
|
20415
|
+
if (partitionStats.isSymbolicLink() || !partitionStats.isDirectory()) {
|
|
20416
|
+
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition}" is not a real directory`);
|
|
20417
|
+
}
|
|
20418
|
+
const partitionReal = await realpath(partitionPath);
|
|
20419
|
+
if (!partitionReal.startsWith(rootReal + sep)) {
|
|
20420
|
+
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition}" resolves outside the replication root`);
|
|
20421
|
+
}
|
|
20422
|
+
const replicasRoot = join8(partitionPath, "replicas");
|
|
20423
|
+
const replicasStats = await lstat3(replicasRoot);
|
|
20424
|
+
if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
|
|
20425
|
+
throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot}" is not a real directory`);
|
|
20426
|
+
}
|
|
20427
|
+
const replicaPath = join8(replicasRoot, name);
|
|
20428
|
+
const replicaStats = await lstat3(replicaPath);
|
|
20429
|
+
if (replicaStats.isSymbolicLink() || !replicaStats.isDirectory()) {
|
|
20430
|
+
throw refuseUnsafeReplicaPath(profile, `replication replica "${name}" is not a real directory`);
|
|
20431
|
+
}
|
|
20432
|
+
const replicaReal = await realpath(replicaPath);
|
|
20433
|
+
if (!replicaReal.startsWith(partitionReal + sep)) {
|
|
20434
|
+
throw refuseUnsafeReplicaPath(profile, `replication replica "${name}" resolves outside the partition`);
|
|
20435
|
+
}
|
|
20436
|
+
const database = await lstat3(join8(replicaPath, "replica.db"));
|
|
20437
|
+
if (database.isSymbolicLink() || !database.isFile()) {
|
|
20438
|
+
throw refuseUnsafeReplicaPath(profile, `replication database "${name}" is not a regular file`);
|
|
20439
|
+
}
|
|
20440
|
+
return { dev: replicaStats.dev, ino: replicaStats.ino };
|
|
20441
|
+
}
|
|
20442
|
+
async function removeProfileReplication(profile, heldLeases, preflightRoot) {
|
|
20066
20443
|
return withProfileLock4(profile, async () => {
|
|
20067
|
-
const
|
|
20068
|
-
const
|
|
20069
|
-
if (
|
|
20070
|
-
|
|
20071
|
-
const partitions = await readdir4(root, { withFileTypes: true });
|
|
20072
|
-
const opened = [];
|
|
20073
|
-
const removed = [];
|
|
20074
|
-
let deleting = false;
|
|
20444
|
+
const namedRoot = join8(profilePath4(profile), "replication");
|
|
20445
|
+
const pinned = preflightRoot ?? await pinReplicaRoot(profile, namedRoot, "logout");
|
|
20446
|
+
if (!pinned) return [];
|
|
20447
|
+
if (preflightRoot) await pinned.assertNamed();
|
|
20075
20448
|
try {
|
|
20076
|
-
|
|
20077
|
-
|
|
20078
|
-
|
|
20079
|
-
|
|
20080
|
-
|
|
20081
|
-
|
|
20082
|
-
|
|
20083
|
-
|
|
20084
|
-
|
|
20085
|
-
|
|
20086
|
-
|
|
20087
|
-
|
|
20088
|
-
|
|
20089
|
-
if (error.code === "ENOENT") return null;
|
|
20090
|
-
throw error;
|
|
20091
|
-
});
|
|
20092
|
-
if (!replicasStats) continue;
|
|
20093
|
-
if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
|
|
20094
|
-
throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot2}" is not a real directory`);
|
|
20449
|
+
const root = pinned.path;
|
|
20450
|
+
const rootReal = await safeReplicaRoot(profile, root);
|
|
20451
|
+
const profileReal = await realpath(profilePath4(profile));
|
|
20452
|
+
const partitions = await readdir4(root, { withFileTypes: true });
|
|
20453
|
+
const opened = [];
|
|
20454
|
+
const removed = [];
|
|
20455
|
+
let deleting = false;
|
|
20456
|
+
try {
|
|
20457
|
+
for (const partition of partitions) {
|
|
20458
|
+
const partitionPath = join8(root, partition.name);
|
|
20459
|
+
const partitionStats = await lstat3(partitionPath);
|
|
20460
|
+
if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
|
|
20461
|
+
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
|
|
20095
20462
|
}
|
|
20096
|
-
|
|
20097
|
-
|
|
20098
|
-
|
|
20099
|
-
|
|
20100
|
-
if (replica.isSymbolicLink() || replicaStats.isSymbolicLink() || !replicaStats.isDirectory()) {
|
|
20101
|
-
throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
|
|
20463
|
+
if (partitionStats.isDirectory()) {
|
|
20464
|
+
const partitionReal = await realpath(partitionPath);
|
|
20465
|
+
if (!partitionReal.startsWith(rootReal + sep) || !partitionReal.startsWith(profileReal + sep)) {
|
|
20466
|
+
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the selected profile`);
|
|
20102
20467
|
}
|
|
20103
|
-
const
|
|
20104
|
-
|
|
20105
|
-
|
|
20468
|
+
const replicasRoot = join8(partitionPath, "replicas");
|
|
20469
|
+
const replicasStats = await lstat3(replicasRoot).catch((error) => {
|
|
20470
|
+
if (error.code === "ENOENT") return null;
|
|
20471
|
+
throw error;
|
|
20472
|
+
});
|
|
20473
|
+
if (!replicasStats) continue;
|
|
20474
|
+
if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
|
|
20475
|
+
throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot}" is not a real directory`);
|
|
20106
20476
|
}
|
|
20107
|
-
const
|
|
20477
|
+
const replicas = await readdir4(replicasRoot, { withFileTypes: true });
|
|
20478
|
+
for (const replica of replicas) {
|
|
20479
|
+
const replicaPath = join8(replicasRoot, replica.name);
|
|
20480
|
+
const replicaStats = await lstat3(replicaPath);
|
|
20481
|
+
if (replica.isSymbolicLink() || replicaStats.isSymbolicLink() || !replicaStats.isDirectory()) {
|
|
20482
|
+
throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
|
|
20483
|
+
}
|
|
20484
|
+
const replicaReal = await realpath(replicaPath);
|
|
20485
|
+
if (!replicaReal.startsWith(rootReal + sep)) {
|
|
20486
|
+
throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" resolves outside the replication root`);
|
|
20487
|
+
}
|
|
20488
|
+
const database = await lstat3(join8(replicaPath, "replica.db")).catch((error) => {
|
|
20489
|
+
if (error.code === "ENOENT") return null;
|
|
20490
|
+
throw error;
|
|
20491
|
+
});
|
|
20492
|
+
if (!database) continue;
|
|
20493
|
+
if (database.isSymbolicLink() || !database.isFile()) {
|
|
20494
|
+
throw refuseUnsafeReplicaPath(profile, `replication database "${replica.name}" is not a regular file`);
|
|
20495
|
+
}
|
|
20496
|
+
}
|
|
20497
|
+
} else if (!partitionStats.isFile()) {
|
|
20498
|
+
throw refuseUnsafeReplicaPath(profile, `replication path "${partition.name}" is not a regular file or directory`);
|
|
20499
|
+
}
|
|
20500
|
+
}
|
|
20501
|
+
const hasReplicaDatabase = await Promise.all(partitions.filter((entry) => entry.isDirectory()).map(async (partition) => {
|
|
20502
|
+
return Promise.all((await readdir4(join8(root, partition.name, "replicas"), { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map((entry) => lstat3(join8(root, partition.name, "replicas", entry.name, "replica.db")).then(() => true, () => false)));
|
|
20503
|
+
})).then((values) => values.some((partition) => partition.some(Boolean)));
|
|
20504
|
+
if (hasReplicaDatabase) await loadSqlite();
|
|
20505
|
+
for (const partition of partitions.filter((entry) => entry.isDirectory())) {
|
|
20506
|
+
const replicasRoot = join8(root, partition.name, "replicas");
|
|
20507
|
+
for (const entry of await readdir4(replicasRoot, { withFileTypes: true }).catch(() => [])) {
|
|
20508
|
+
if (!entry.isDirectory()) continue;
|
|
20509
|
+
const database = await lstat3(join8(replicasRoot, entry.name, "replica.db")).catch((error) => {
|
|
20108
20510
|
if (error.code === "ENOENT") return null;
|
|
20109
20511
|
throw error;
|
|
20110
20512
|
});
|
|
20111
20513
|
if (!database) continue;
|
|
20112
|
-
|
|
20113
|
-
|
|
20514
|
+
const name = `replication/${partition.name}/${entry.name}`;
|
|
20515
|
+
const expectedDirectory = await assertSafeFlagReplica(profile, root, partition.name, entry.name);
|
|
20516
|
+
const store = await SqliteReplicaStore.open(join8(replicasRoot, entry.name), {
|
|
20517
|
+
create: false,
|
|
20518
|
+
expectedDirectory,
|
|
20519
|
+
guard: async (action) => withProfileLock4(profile, async () => {
|
|
20520
|
+
await assertSafeFlagReplica(profile, root, partition.name, entry.name);
|
|
20521
|
+
return action();
|
|
20522
|
+
}, { requireProfile: true })
|
|
20523
|
+
});
|
|
20524
|
+
try {
|
|
20525
|
+
if (await store.open() === null) {
|
|
20526
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "${name}" has no initialized store.`, ExitCode.ERROR);
|
|
20527
|
+
}
|
|
20528
|
+
const leasePath = join8(namedRoot, partition.name, "replicas", entry.name);
|
|
20529
|
+
const lease = heldLeases?.get(leasePath) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20530
|
+
if (lease === null) {
|
|
20531
|
+
throw new CLIError("REPLICA_BUSY", `Cannot log out: flag replica "${name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
20532
|
+
}
|
|
20533
|
+
opened.push({ name, path: leasePath, store, lease });
|
|
20534
|
+
} catch (error) {
|
|
20535
|
+
await store.close().catch(() => void 0);
|
|
20536
|
+
throw error;
|
|
20114
20537
|
}
|
|
20115
20538
|
}
|
|
20116
|
-
} else if (!partitionStats.isFile()) {
|
|
20117
|
-
throw refuseUnsafeReplicaPath(profile, `replication path "${partition.name}" is not a regular file or directory`);
|
|
20118
20539
|
}
|
|
20119
|
-
|
|
20120
|
-
|
|
20121
|
-
|
|
20122
|
-
|
|
20123
|
-
if (hasReplicaDatabase) await loadSqlite();
|
|
20124
|
-
for (const partition of partitions.filter((entry) => entry.isDirectory())) {
|
|
20125
|
-
const replicasRoot2 = join8(root, partition.name, "replicas");
|
|
20126
|
-
for (const entry of await readdir4(replicasRoot2, { withFileTypes: true }).catch(() => [])) {
|
|
20127
|
-
if (!entry.isDirectory()) continue;
|
|
20128
|
-
const database = await lstat2(join8(replicasRoot2, entry.name, "replica.db")).catch((error) => {
|
|
20129
|
-
if (error.code === "ENOENT") return null;
|
|
20130
|
-
throw error;
|
|
20131
|
-
});
|
|
20132
|
-
if (!database) continue;
|
|
20133
|
-
const name = `replication/${partition.name}/${entry.name}`;
|
|
20134
|
-
const store = await SqliteReplicaStore.open(join8(replicasRoot2, entry.name), { create: false, guard: async (action) => withProfileLock4(profile, action, { requireProfile: true }) });
|
|
20135
|
-
if (await store.open() === null) {
|
|
20136
|
-
await store.close();
|
|
20137
|
-
throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "${name}" has no initialized store.`, ExitCode.ERROR);
|
|
20138
|
-
}
|
|
20139
|
-
const lease = heldLeases?.get(join8(replicasRoot2, entry.name)) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20140
|
-
if (lease === null) {
|
|
20141
|
-
await store.close();
|
|
20142
|
-
throw new CLIError("REPLICA_BUSY", `Cannot log out: flag replica "${name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
20143
|
-
}
|
|
20144
|
-
opened.push({ name, path: join8(replicasRoot2, entry.name), store, lease });
|
|
20540
|
+
deleting = true;
|
|
20541
|
+
for (const item of opened) {
|
|
20542
|
+
await item.store.destroy(item.lease);
|
|
20543
|
+
removed.push(item.name);
|
|
20145
20544
|
}
|
|
20545
|
+
await pinned.assertNamed();
|
|
20546
|
+
for (const entry of await readdir4(root)) await rm4(join8(root, entry), { recursive: true, force: true });
|
|
20547
|
+
await pinned.assertNamed();
|
|
20548
|
+
await rmdir2(namedRoot);
|
|
20549
|
+
return removed;
|
|
20550
|
+
} catch (error) {
|
|
20551
|
+
if (deleting) {
|
|
20552
|
+
const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
|
|
20553
|
+
throw new CLIError(
|
|
20554
|
+
"REPLICA_PURGE_FAILED",
|
|
20555
|
+
`Logout cleared the session, but replication cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
|
|
20556
|
+
ExitCode.ERROR,
|
|
20557
|
+
{ replicasRemoved: removed, replicasRemaining: remaining }
|
|
20558
|
+
);
|
|
20559
|
+
}
|
|
20560
|
+
if (error instanceof CLIError) throw error;
|
|
20561
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but replication cleanup failed; no flag replicas were removed. ${error instanceof Error ? error.message : String(error)}`, ExitCode.ERROR);
|
|
20562
|
+
} finally {
|
|
20563
|
+
await Promise.all(opened.map(async ({ path, store, lease }) => {
|
|
20564
|
+
if (heldLeases?.has(path) !== true) await store.releaseLease(lease).catch(() => void 0);
|
|
20565
|
+
await store.close().catch(() => void 0);
|
|
20566
|
+
}));
|
|
20146
20567
|
}
|
|
20147
|
-
deleting = true;
|
|
20148
|
-
for (const item of opened) {
|
|
20149
|
-
await item.store.destroy(item.lease);
|
|
20150
|
-
removed.push(item.name);
|
|
20151
|
-
}
|
|
20152
|
-
await rm4(root, { recursive: true });
|
|
20153
|
-
return removed;
|
|
20154
|
-
} catch (error) {
|
|
20155
|
-
if (deleting) {
|
|
20156
|
-
const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
|
|
20157
|
-
throw new CLIError(
|
|
20158
|
-
"REPLICA_PURGE_FAILED",
|
|
20159
|
-
`Logout cleared the session, but replication cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
|
|
20160
|
-
ExitCode.ERROR,
|
|
20161
|
-
{ replicasRemoved: removed, replicasRemaining: remaining }
|
|
20162
|
-
);
|
|
20163
|
-
}
|
|
20164
|
-
if (error instanceof CLIError) throw error;
|
|
20165
|
-
throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but replication cleanup failed; no flag replicas were removed. ${error instanceof Error ? error.message : String(error)}`, ExitCode.ERROR);
|
|
20166
20568
|
} finally {
|
|
20167
|
-
await
|
|
20168
|
-
if (heldLeases?.has(path) !== true) await store.releaseLease(lease).catch(() => void 0);
|
|
20169
|
-
await store.close().catch(() => void 0);
|
|
20170
|
-
}));
|
|
20569
|
+
if (!preflightRoot) await pinned.close();
|
|
20171
20570
|
}
|
|
20172
20571
|
});
|
|
20173
20572
|
}
|
|
@@ -20175,36 +20574,54 @@ async function acquireLogoutLeases(profile) {
|
|
|
20175
20574
|
return withProfileLock4(profile, async () => {
|
|
20176
20575
|
let sqliteLoaded = false;
|
|
20177
20576
|
const opened = [];
|
|
20577
|
+
const roots = /* @__PURE__ */ new Map();
|
|
20178
20578
|
try {
|
|
20179
|
-
const
|
|
20180
|
-
|
|
20579
|
+
const legacyNamed = join8(profilePath4(profile), "replicas");
|
|
20580
|
+
const legacyPin = await pinReplicaRoot(profile, legacyNamed, "logout");
|
|
20581
|
+
if (legacyPin) {
|
|
20582
|
+
roots.set("replicas", legacyPin);
|
|
20583
|
+
const legacyRoot = legacyPin.path;
|
|
20181
20584
|
const entries = (await readdir4(legacyRoot, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
20182
20585
|
for (const entry of entries) {
|
|
20183
|
-
const path = await
|
|
20586
|
+
const { path, identity: expectedDirectory, empty: empty4 } = await inspectLegacyReplica(profile, legacyRoot, entry);
|
|
20587
|
+
if (empty4) continue;
|
|
20184
20588
|
if (!sqliteLoaded) {
|
|
20185
20589
|
await loadSqlite();
|
|
20186
20590
|
sqliteLoaded = true;
|
|
20187
20591
|
}
|
|
20188
|
-
const store = await SqliteReplicaStore.open(path, {
|
|
20189
|
-
|
|
20190
|
-
|
|
20191
|
-
|
|
20192
|
-
|
|
20193
|
-
|
|
20194
|
-
|
|
20195
|
-
|
|
20196
|
-
|
|
20592
|
+
const store = await SqliteReplicaStore.open(path, {
|
|
20593
|
+
create: false,
|
|
20594
|
+
expectedDirectory,
|
|
20595
|
+
guard: async (action) => withProfileLock4(profile, async () => {
|
|
20596
|
+
await safeReplicaEntry(profile, legacyRoot, entry);
|
|
20597
|
+
return action();
|
|
20598
|
+
}, { requireProfile: true })
|
|
20599
|
+
});
|
|
20600
|
+
try {
|
|
20601
|
+
if (await store.open() === null) {
|
|
20602
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
|
|
20603
|
+
}
|
|
20604
|
+
const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20605
|
+
if (lease === null) {
|
|
20606
|
+
throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
20607
|
+
}
|
|
20608
|
+
opened.push({ path: join8(legacyNamed, entry.name), store, lease });
|
|
20609
|
+
} catch (error) {
|
|
20610
|
+
await store.close().catch(() => void 0);
|
|
20611
|
+
throw error;
|
|
20197
20612
|
}
|
|
20198
|
-
opened.push({ path, store, lease });
|
|
20199
20613
|
}
|
|
20200
20614
|
}
|
|
20201
|
-
const
|
|
20202
|
-
const
|
|
20203
|
-
if (
|
|
20615
|
+
const replicationNamed = join8(profilePath4(profile), "replication");
|
|
20616
|
+
const replicationPin = await pinReplicaRoot(profile, replicationNamed, "logout");
|
|
20617
|
+
if (replicationPin) {
|
|
20618
|
+
roots.set("replication", replicationPin);
|
|
20619
|
+
const replicationRoot = replicationPin.path;
|
|
20620
|
+
const replicationReal = await safeReplicaRoot(profile, replicationRoot);
|
|
20204
20621
|
const partitions = await readdir4(replicationRoot, { withFileTypes: true });
|
|
20205
20622
|
for (const partition of partitions) {
|
|
20206
20623
|
const partitionPath = join8(replicationRoot, partition.name);
|
|
20207
|
-
const partitionStats = await
|
|
20624
|
+
const partitionStats = await lstat3(partitionPath);
|
|
20208
20625
|
if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
|
|
20209
20626
|
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
|
|
20210
20627
|
}
|
|
@@ -20216,22 +20633,22 @@ async function acquireLogoutLeases(profile) {
|
|
|
20216
20633
|
if (!partitionReal.startsWith(replicationReal + sep)) {
|
|
20217
20634
|
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the replication root`);
|
|
20218
20635
|
}
|
|
20219
|
-
const
|
|
20220
|
-
const replicasStats = await
|
|
20636
|
+
const replicasRoot = join8(partitionPath, "replicas");
|
|
20637
|
+
const replicasStats = await lstat3(replicasRoot).catch((error) => {
|
|
20221
20638
|
if (error.code === "ENOENT") return null;
|
|
20222
20639
|
throw error;
|
|
20223
20640
|
});
|
|
20224
20641
|
if (!replicasStats) continue;
|
|
20225
20642
|
if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
|
|
20226
|
-
throw refuseUnsafeReplicaPath(profile, `replication replicas path "${
|
|
20643
|
+
throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot}" is not a real directory`);
|
|
20227
20644
|
}
|
|
20228
|
-
for (const replica of await readdir4(
|
|
20229
|
-
const path = join8(
|
|
20230
|
-
const stats = await
|
|
20645
|
+
for (const replica of await readdir4(replicasRoot, { withFileTypes: true })) {
|
|
20646
|
+
const path = join8(replicasRoot, replica.name);
|
|
20647
|
+
const stats = await lstat3(path);
|
|
20231
20648
|
if (!replica.isDirectory() || replica.isSymbolicLink() || stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
20232
20649
|
throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
|
|
20233
20650
|
}
|
|
20234
|
-
const database = await
|
|
20651
|
+
const database = await lstat3(join8(path, "replica.db")).catch((error) => {
|
|
20235
20652
|
if (error.code === "ENOENT") return null;
|
|
20236
20653
|
throw error;
|
|
20237
20654
|
});
|
|
@@ -20247,26 +20664,38 @@ async function acquireLogoutLeases(profile) {
|
|
|
20247
20664
|
await loadSqlite();
|
|
20248
20665
|
sqliteLoaded = true;
|
|
20249
20666
|
}
|
|
20250
|
-
const
|
|
20251
|
-
|
|
20252
|
-
|
|
20253
|
-
|
|
20254
|
-
|
|
20255
|
-
|
|
20256
|
-
|
|
20257
|
-
|
|
20258
|
-
|
|
20667
|
+
const expectedDirectory = await assertSafeFlagReplica(profile, replicationRoot, partition.name, replica.name);
|
|
20668
|
+
const store = await SqliteReplicaStore.open(path, {
|
|
20669
|
+
create: false,
|
|
20670
|
+
expectedDirectory,
|
|
20671
|
+
guard: async (action) => withProfileLock4(profile, async () => {
|
|
20672
|
+
await assertSafeFlagReplica(profile, replicationRoot, partition.name, replica.name);
|
|
20673
|
+
return action();
|
|
20674
|
+
}, { requireProfile: true })
|
|
20675
|
+
});
|
|
20676
|
+
try {
|
|
20677
|
+
if (await store.open() === null) {
|
|
20678
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "replication/${partition.name}/${replica.name}" has no initialized store.`, ExitCode.ERROR);
|
|
20679
|
+
}
|
|
20680
|
+
const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20681
|
+
if (lease === null) {
|
|
20682
|
+
throw new CLIError("REPLICA_BUSY", `Cannot log out: flag replica "replication/${partition.name}/${replica.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
20683
|
+
}
|
|
20684
|
+
opened.push({ path: join8(replicationNamed, partition.name, "replicas", replica.name), store, lease });
|
|
20685
|
+
} catch (error) {
|
|
20686
|
+
await store.close().catch(() => void 0);
|
|
20687
|
+
throw error;
|
|
20259
20688
|
}
|
|
20260
|
-
opened.push({ path, store, lease });
|
|
20261
20689
|
}
|
|
20262
20690
|
}
|
|
20263
20691
|
}
|
|
20264
|
-
return opened;
|
|
20692
|
+
return { stores: opened, roots };
|
|
20265
20693
|
} catch (error) {
|
|
20266
20694
|
await Promise.all(opened.map(async ({ store, lease }) => {
|
|
20267
20695
|
await store.releaseLease(lease).catch(() => void 0);
|
|
20268
20696
|
await store.close().catch(() => void 0);
|
|
20269
20697
|
}));
|
|
20698
|
+
await Promise.all([...roots.values()].map((root) => root.close()));
|
|
20270
20699
|
throw error;
|
|
20271
20700
|
}
|
|
20272
20701
|
});
|
|
@@ -20274,31 +20703,32 @@ async function acquireLogoutLeases(profile) {
|
|
|
20274
20703
|
async function removeProfileReplicasAndReplication(profile) {
|
|
20275
20704
|
return withProfileLock4(profile, async () => {
|
|
20276
20705
|
const acquired = await acquireLogoutLeases(profile);
|
|
20277
|
-
const heldLeases = new Map(acquired.map(({ path, lease }) => [path, lease]));
|
|
20706
|
+
const heldLeases = new Map(acquired.stores.map(({ path, lease }) => [path, lease]));
|
|
20278
20707
|
let legacyRemoved = [];
|
|
20279
20708
|
try {
|
|
20280
|
-
legacyRemoved = await removeProfileReplicas(profile, heldLeases);
|
|
20281
|
-
const replication = await removeProfileReplication(profile, heldLeases);
|
|
20709
|
+
legacyRemoved = await removeProfileReplicas(profile, heldLeases, acquired.roots.get("replicas"));
|
|
20710
|
+
const replication = await removeProfileReplication(profile, heldLeases, acquired.roots.get("replication"));
|
|
20282
20711
|
return [...legacyRemoved, ...replication];
|
|
20283
20712
|
} catch (error) {
|
|
20284
20713
|
const partialRemoved = error instanceof CLIError && Array.isArray(error.metadata?.replicasRemoved) ? error.metadata.replicasRemoved.filter((name) => typeof name === "string") : [];
|
|
20285
20714
|
const removed = [.../* @__PURE__ */ new Set([...legacyRemoved, ...partialRemoved])];
|
|
20286
20715
|
const removedSet = new Set(removed);
|
|
20287
|
-
const remaining = acquired.map(({ path }) => {
|
|
20288
|
-
const name = relative(
|
|
20716
|
+
const remaining = acquired.stores.map(({ path }) => {
|
|
20717
|
+
const name = relative(profilePath4(profile), path).split(sep).join("/");
|
|
20289
20718
|
return name.startsWith("replication/") ? name.replace("/replicas/", "/") : name.split("/").at(-1);
|
|
20290
20719
|
}).filter((name) => !removedSet.has(name));
|
|
20291
20720
|
throw new CLIError(
|
|
20292
20721
|
error instanceof CLIError ? error.code : "REPLICA_PURGE_FAILED",
|
|
20293
|
-
`Logout cleared the session, but replica cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}
|
|
20722
|
+
`Logout cleared the session, but replica cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
|
|
20294
20723
|
error instanceof CLIError ? error.exitCode : ExitCode.ERROR,
|
|
20295
20724
|
{ replicasRemoved: removed, replicasRemaining: remaining }
|
|
20296
20725
|
);
|
|
20297
20726
|
} finally {
|
|
20298
|
-
await Promise.all(acquired.map(async ({ store, lease }) => {
|
|
20727
|
+
await Promise.all(acquired.stores.map(async ({ store, lease }) => {
|
|
20299
20728
|
await store.releaseLease(lease).catch(() => void 0);
|
|
20300
20729
|
await store.close().catch(() => void 0);
|
|
20301
20730
|
}));
|
|
20731
|
+
await Promise.all([...acquired.roots.values()].map((root) => root.close()));
|
|
20302
20732
|
}
|
|
20303
20733
|
});
|
|
20304
20734
|
}
|
|
@@ -20469,7 +20899,7 @@ function shouldOpenBrowser(options) {
|
|
|
20469
20899
|
return env !== "1" && env !== "true";
|
|
20470
20900
|
}
|
|
20471
20901
|
async function callbackFlow(did, options = {}) {
|
|
20472
|
-
return new Promise((
|
|
20902
|
+
return new Promise((resolve6, reject) => {
|
|
20473
20903
|
let timeout;
|
|
20474
20904
|
let settled = false;
|
|
20475
20905
|
let rl;
|
|
@@ -20483,7 +20913,7 @@ async function callbackFlow(did, options = {}) {
|
|
|
20483
20913
|
rl.close();
|
|
20484
20914
|
}
|
|
20485
20915
|
if (result.data) {
|
|
20486
|
-
|
|
20916
|
+
resolve6(result.data);
|
|
20487
20917
|
} else {
|
|
20488
20918
|
reject(result.error);
|
|
20489
20919
|
}
|
|
@@ -20562,8 +20992,8 @@ async function callbackFlow(did, options = {}) {
|
|
|
20562
20992
|
}
|
|
20563
20993
|
if (openBrowser) {
|
|
20564
20994
|
try {
|
|
20565
|
-
const
|
|
20566
|
-
await
|
|
20995
|
+
const open9 = (await import("open")).default;
|
|
20996
|
+
await open9(authUrl);
|
|
20567
20997
|
} catch {
|
|
20568
20998
|
settle({ error: new Error("Failed to open browser") });
|
|
20569
20999
|
return;
|
|
@@ -20607,13 +21037,13 @@ Open this URL in a browser to authenticate:
|
|
|
20607
21037
|
input: process.stdin,
|
|
20608
21038
|
output: process.stderr
|
|
20609
21039
|
});
|
|
20610
|
-
return new Promise((
|
|
21040
|
+
return new Promise((resolve6, reject) => {
|
|
20611
21041
|
let answered = false;
|
|
20612
21042
|
rl.on("line", (input) => {
|
|
20613
21043
|
if (answered || input.trim() === "") return;
|
|
20614
21044
|
answered = true;
|
|
20615
21045
|
rl.close();
|
|
20616
|
-
void delegationFromInput(input, did, options).then(
|
|
21046
|
+
void delegationFromInput(input, did, options).then(resolve6, reject);
|
|
20617
21047
|
});
|
|
20618
21048
|
rl.on("close", () => {
|
|
20619
21049
|
if (answered) return;
|
|
@@ -21514,7 +21944,7 @@ async function acquireDeviceDelegation(input) {
|
|
|
21514
21944
|
expiresAt: new Date(deadline).toISOString().replace(/\.\d{3}Z$/, "Z")
|
|
21515
21945
|
});
|
|
21516
21946
|
let interval = started.interval;
|
|
21517
|
-
const wait = input.wait ?? ((milliseconds) => new Promise((
|
|
21947
|
+
const wait = input.wait ?? ((milliseconds) => new Promise((resolve6) => setTimeout(resolve6, milliseconds)));
|
|
21518
21948
|
while (Date.now() < deadline) {
|
|
21519
21949
|
await wait(interval * 1e3);
|
|
21520
21950
|
let response;
|
|
@@ -22150,7 +22580,7 @@ async function promptAuthMethod() {
|
|
|
22150
22580
|
input: process.stdin,
|
|
22151
22581
|
output: process.stderr
|
|
22152
22582
|
});
|
|
22153
|
-
return new Promise((
|
|
22583
|
+
return new Promise((resolve6) => {
|
|
22154
22584
|
process.stderr.write("\n" + theme.heading("Choose authentication method:") + "\n");
|
|
22155
22585
|
process.stderr.write(` ${theme.accent("1)")} OpenKey ${theme.muted("(browser-based, for interactive use)")}
|
|
22156
22586
|
`);
|
|
@@ -22161,9 +22591,9 @@ async function promptAuthMethod() {
|
|
|
22161
22591
|
rl.close();
|
|
22162
22592
|
const trimmed = answer.trim();
|
|
22163
22593
|
if (trimmed === "2" || trimmed.toLowerCase() === "local") {
|
|
22164
|
-
|
|
22594
|
+
resolve6("local");
|
|
22165
22595
|
} else {
|
|
22166
|
-
|
|
22596
|
+
resolve6("openkey");
|
|
22167
22597
|
}
|
|
22168
22598
|
});
|
|
22169
22599
|
});
|
|
@@ -22865,7 +23295,7 @@ function describeSession(state2, expiresAt) {
|
|
|
22865
23295
|
}
|
|
22866
23296
|
async function emitPermissionRequestArtifact(artifact, emitOption) {
|
|
22867
23297
|
if (typeof emitOption === "string" && emitOption.length > 0) {
|
|
22868
|
-
await mkdir3(
|
|
23298
|
+
await mkdir3(dirname4(emitOption), { recursive: true });
|
|
22869
23299
|
await writeFile2(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
22870
23300
|
outputJson({
|
|
22871
23301
|
emitted: true,
|
|
@@ -22901,13 +23331,13 @@ async function readStdin() {
|
|
|
22901
23331
|
return Buffer.concat(chunks).toString("utf8");
|
|
22902
23332
|
}
|
|
22903
23333
|
function readUrl(source) {
|
|
22904
|
-
return new Promise((
|
|
23334
|
+
return new Promise((resolve6, reject) => {
|
|
22905
23335
|
const getter = source.startsWith("https://") ? httpsGet : httpGet;
|
|
22906
23336
|
const request = getter(source, (response) => {
|
|
22907
23337
|
const status = response.statusCode ?? 0;
|
|
22908
23338
|
if (status >= 300 && status < 400 && response.headers.location) {
|
|
22909
23339
|
response.resume();
|
|
22910
|
-
readUrl(new URL(response.headers.location, source).toString()).then(
|
|
23340
|
+
readUrl(new URL(response.headers.location, source).toString()).then(resolve6, reject);
|
|
22911
23341
|
return;
|
|
22912
23342
|
}
|
|
22913
23343
|
if (status < 200 || status >= 300) {
|
|
@@ -22923,7 +23353,7 @@ function readUrl(source) {
|
|
|
22923
23353
|
response.on("data", (chunk) => {
|
|
22924
23354
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
22925
23355
|
});
|
|
22926
|
-
response.on("end", () =>
|
|
23356
|
+
response.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
|
|
22927
23357
|
});
|
|
22928
23358
|
request.on("error", reject);
|
|
22929
23359
|
});
|
|
@@ -23163,7 +23593,7 @@ function permissionGrantReason(context, permissions) {
|
|
|
23163
23593
|
return `${context} Requested: ${summary}${more}.`;
|
|
23164
23594
|
}
|
|
23165
23595
|
function execCapturedCommand(command) {
|
|
23166
|
-
return new Promise((
|
|
23596
|
+
return new Promise((resolve6, reject) => {
|
|
23167
23597
|
const child = spawn(process.execPath, [process.argv[1], ...command.argv], {
|
|
23168
23598
|
cwd: command.cwd,
|
|
23169
23599
|
env: process.env,
|
|
@@ -23182,7 +23612,7 @@ function execCapturedCommand(command) {
|
|
|
23182
23612
|
if (code3 && code3 !== 0) {
|
|
23183
23613
|
process.exitCode = code3;
|
|
23184
23614
|
}
|
|
23185
|
-
|
|
23615
|
+
resolve6();
|
|
23186
23616
|
});
|
|
23187
23617
|
});
|
|
23188
23618
|
}
|
|
@@ -23211,8 +23641,8 @@ async function confirmPermissionRequest(permissions) {
|
|
|
23211
23641
|
input: process.stdin,
|
|
23212
23642
|
output: process.stderr
|
|
23213
23643
|
});
|
|
23214
|
-
const answer = await new Promise((
|
|
23215
|
-
rl.question("Approve local-key delegation? [y/N] ",
|
|
23644
|
+
const answer = await new Promise((resolve6) => {
|
|
23645
|
+
rl.question("Approve local-key delegation? [y/N] ", resolve6);
|
|
23216
23646
|
});
|
|
23217
23647
|
rl.close();
|
|
23218
23648
|
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
@@ -24057,7 +24487,7 @@ init_errors();
|
|
|
24057
24487
|
init_sdk();
|
|
24058
24488
|
init_theme();
|
|
24059
24489
|
import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
|
|
24060
|
-
import { resolve } from "path";
|
|
24490
|
+
import { resolve as resolve2 } from "path";
|
|
24061
24491
|
function registerDuckdbCommand(program) {
|
|
24062
24492
|
const duckdb = program.command("duckdb").description("DuckDB database operations");
|
|
24063
24493
|
duckdb.command("query <sql>").description("Run a SELECT query").option("--db <name>", "Database name", "default").option("--params <json>", "Bind parameters as JSON array").action(async (sqlStr, options, cmd) => {
|
|
@@ -24169,7 +24599,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
24169
24599
|
}
|
|
24170
24600
|
const blob = result.data;
|
|
24171
24601
|
const buffer = Buffer.from(await blob.arrayBuffer());
|
|
24172
|
-
const outputPath =
|
|
24602
|
+
const outputPath = resolve2(options.output);
|
|
24173
24603
|
await writeFile3(outputPath, buffer);
|
|
24174
24604
|
outputJson({
|
|
24175
24605
|
file: outputPath,
|
|
@@ -24185,7 +24615,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
24185
24615
|
const globalOpts = cmd.optsWithGlobals();
|
|
24186
24616
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
24187
24617
|
const node = await ensureAuthenticated(ctx);
|
|
24188
|
-
const filePath =
|
|
24618
|
+
const filePath = resolve2(file);
|
|
24189
24619
|
const bytes = new Uint8Array(await readFile6(filePath));
|
|
24190
24620
|
const result = await withSpinner(
|
|
24191
24621
|
"Importing database...",
|
|
@@ -24327,11 +24757,11 @@ init_storage();
|
|
|
24327
24757
|
init_errors();
|
|
24328
24758
|
init_constants();
|
|
24329
24759
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
24330
|
-
import { lstat as
|
|
24331
|
-
import { basename as basename2, dirname as
|
|
24760
|
+
import { lstat as lstat4, open as open4, rename as rename2, rm as rm5 } from "fs/promises";
|
|
24761
|
+
import { basename as basename2, dirname as dirname5, join as join9 } from "path";
|
|
24332
24762
|
async function validatePrivateOutput(path, label = "output") {
|
|
24333
24763
|
try {
|
|
24334
|
-
const destination = await
|
|
24764
|
+
const destination = await lstat4(path);
|
|
24335
24765
|
if (!destination.isFile()) {
|
|
24336
24766
|
throw new CLIError("INVALID_ARGUMENT", `${label} "${path}" must be a regular file, not a symlink, directory, or device.`, ExitCode.USAGE_ERROR);
|
|
24337
24767
|
}
|
|
@@ -24339,7 +24769,7 @@ async function validatePrivateOutput(path, label = "output") {
|
|
|
24339
24769
|
if (error?.code !== "ENOENT") throw error;
|
|
24340
24770
|
}
|
|
24341
24771
|
try {
|
|
24342
|
-
const parent = await
|
|
24772
|
+
const parent = await lstat4(dirname5(path));
|
|
24343
24773
|
if (!parent.isDirectory()) {
|
|
24344
24774
|
throw new CLIError("INVALID_ARGUMENT", `${label} "${path}" requires an existing directory parent.`, ExitCode.USAGE_ERROR);
|
|
24345
24775
|
}
|
|
@@ -24350,11 +24780,11 @@ async function validatePrivateOutput(path, label = "output") {
|
|
|
24350
24780
|
}
|
|
24351
24781
|
async function writePrivateOutput(path, value, label = "output") {
|
|
24352
24782
|
await validatePrivateOutput(path, label);
|
|
24353
|
-
const parentPath =
|
|
24783
|
+
const parentPath = dirname5(path);
|
|
24354
24784
|
const temp = join9(parentPath, `.${basename2(path)}.${randomUUID2()}.tmp`);
|
|
24355
24785
|
let created = false;
|
|
24356
24786
|
try {
|
|
24357
|
-
const handle = await
|
|
24787
|
+
const handle = await open4(temp, "wx", PRIVATE_FILE_MODE);
|
|
24358
24788
|
created = true;
|
|
24359
24789
|
try {
|
|
24360
24790
|
await handle.writeFile(value);
|
|
@@ -24369,7 +24799,7 @@ async function writePrivateOutput(path, value, label = "output") {
|
|
|
24369
24799
|
throw new CLIError("ERROR", `Could not write ${label.toLowerCase()} "${path}"${code3}.`, ExitCode.ERROR);
|
|
24370
24800
|
}
|
|
24371
24801
|
try {
|
|
24372
|
-
const parent = await
|
|
24802
|
+
const parent = await open4(parentPath, "r");
|
|
24373
24803
|
try {
|
|
24374
24804
|
await parent.sync();
|
|
24375
24805
|
} finally {
|
|
@@ -25009,8 +25439,8 @@ Examples:
|
|
|
25009
25439
|
try {
|
|
25010
25440
|
if (isInteractive()) {
|
|
25011
25441
|
const rl = createInterface3({ input: process.stdin, output: process.stderr });
|
|
25012
|
-
const answer = await new Promise((
|
|
25013
|
-
rl.question(`Delete profile "${name}"? This cannot be undone. [y/N] `,
|
|
25442
|
+
const answer = await new Promise((resolve6) => {
|
|
25443
|
+
rl.question(`Delete profile "${name}"? This cannot be undone. [y/N] `, resolve6);
|
|
25014
25444
|
});
|
|
25015
25445
|
rl.close();
|
|
25016
25446
|
if (answer.toLowerCase() !== "y") {
|
|
@@ -25045,22 +25475,19 @@ function parseOperatorType(raw) {
|
|
|
25045
25475
|
}
|
|
25046
25476
|
|
|
25047
25477
|
// src/commands/replica.ts
|
|
25048
|
-
init_sdk();
|
|
25049
|
-
init_replication_registry();
|
|
25050
25478
|
import { CLI_NAME as CLI_NAME22 } from "@tinycloud/operations/cli-name";
|
|
25051
25479
|
import { randomBytes as randomBytes6 } from "crypto";
|
|
25052
|
-
import { constants as
|
|
25053
|
-
import { open as
|
|
25054
|
-
import { basename as basename3, dirname as
|
|
25480
|
+
import { constants as fsConstants3, lstatSync as lstatSync2 } from "fs";
|
|
25481
|
+
import { mkdir as mkdir4, open as open5, readdir as readdir5, rename as rename3, unlink as unlink2, lstat as lstat5 } from "fs/promises";
|
|
25482
|
+
import { basename as basename3, dirname as dirname6, join as join10, resolve as resolve3 } from "path";
|
|
25055
25483
|
|
|
25056
25484
|
// src/lib/replication-report.ts
|
|
25057
|
-
var CLEAR_PENDING_WARNING = "Clearing pending writes stops pinning keys whose last write had an unknown outcome or never finished. If such a write did commit, or commits later, this device can serve the older value for that key until the next sync after the commit, as it would for a write from another device.";
|
|
25058
25485
|
function percentile(values, fraction) {
|
|
25059
25486
|
if (values.length === 0) return null;
|
|
25060
25487
|
values.sort((left, right) => left - right);
|
|
25061
25488
|
return values[Math.ceil(fraction * values.length) - 1] ?? values[values.length - 1];
|
|
25062
25489
|
}
|
|
25063
|
-
function createReplicationReport(since, events,
|
|
25490
|
+
function createReplicationReport(since, events, partitions) {
|
|
25064
25491
|
const reads = events.filter((event) => event.type === "replication.read");
|
|
25065
25492
|
const replicaReads = reads.filter((event) => event.source === "replica");
|
|
25066
25493
|
const sourceLatency = /* @__PURE__ */ new Map();
|
|
@@ -25103,9 +25530,9 @@ function createReplicationReport(since, events, replicas, partitions, warning) {
|
|
|
25103
25530
|
syncs,
|
|
25104
25531
|
writes,
|
|
25105
25532
|
divergences,
|
|
25106
|
-
replicas,
|
|
25533
|
+
replicas: [],
|
|
25107
25534
|
partitions,
|
|
25108
|
-
|
|
25535
|
+
replicasUnavailable: true
|
|
25109
25536
|
};
|
|
25110
25537
|
}
|
|
25111
25538
|
function renderReplicationReport(report) {
|
|
@@ -25118,34 +25545,30 @@ function renderReplicationReport(report) {
|
|
|
25118
25545
|
`Writes: ${Object.entries(report.writes).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
|
|
25119
25546
|
`Read source/reason: ${Object.entries(report.readsBySourceReason).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
|
|
25120
25547
|
`Divergences: ${report.totals.divergences}; showing ${report.divergences.length} of ${report.totals.divergences}`,
|
|
25121
|
-
|
|
25548
|
+
"Replicas: unavailable (read-only report)"
|
|
25122
25549
|
];
|
|
25123
25550
|
for (const divergence of report.divergences) {
|
|
25124
25551
|
if (divergence.type === "replication.divergence") {
|
|
25125
25552
|
lines.push(` ${divergence.space} ${divergence.key}: ${divergence.kind} stalenessMs=${divergence.stalenessMs}`);
|
|
25126
25553
|
}
|
|
25127
25554
|
}
|
|
25128
|
-
for (const replica of report.replicas) {
|
|
25129
|
-
const grant = replica.grant ? ` grant=${replica.grant.cid} parent=${replica.grant.parentCid ?? "\u2014"} expires=${replica.grant.expiresAt ?? "\u2014"}` : " grant=missing";
|
|
25130
|
-
lines.push(` ${replica.prefix}: ${replica.state}; pending in_flight=${replica.pending.inFlight} committed=${replica.pending.committed} ambiguous=${replica.pending.ambiguous}${grant}`);
|
|
25131
|
-
for (const pinned of replica.pinned) lines.push(` pinned ${pinned.key} ${pinned.state} ${pinned.op} since=${pinned.since} code=${pinned.code ?? "\u2014"} likelyOrphaned=${pinned.likelyOrphaned}`);
|
|
25132
|
-
}
|
|
25133
25555
|
for (const partition of report.partitions) {
|
|
25134
25556
|
lines.push(`Partition ${partition.idHash}: host=${partition.host ?? "unknown"} space=${partition.space ?? "unknown"} pinned=${partition.pinned}`);
|
|
25135
25557
|
}
|
|
25136
25558
|
if (report.partitions.some((partition) => partition.pinned > 100)) lines.push("Warning: a partition holds more than 100 pinned records.");
|
|
25137
|
-
if (report.warning) lines.push(`Warning: ${report.warning}`);
|
|
25138
25559
|
return lines.join("\n");
|
|
25139
25560
|
}
|
|
25140
25561
|
|
|
25141
25562
|
// src/commands/replica.ts
|
|
25142
25563
|
init_dist3();
|
|
25143
25564
|
import {
|
|
25565
|
+
ProfileSymlinkError as ProfileSymlinkError3,
|
|
25144
25566
|
ProfileDeletedError as ProfileDeletedError3,
|
|
25145
|
-
profilePath as
|
|
25567
|
+
profilePath as profilePath5,
|
|
25146
25568
|
readAdditionalDelegations as readAdditionalDelegations2,
|
|
25147
25569
|
readSession as readSession2,
|
|
25148
|
-
withProfileLock as withProfileLock5
|
|
25570
|
+
withProfileLock as withProfileLock5,
|
|
25571
|
+
withPinnedProfileDirectory as withPinnedProfileDirectory3
|
|
25149
25572
|
} from "@tinycloud/operations/state";
|
|
25150
25573
|
import { ownerSessionLacksProof as ownerSessionLacksProof4 } from "@tinycloud/operations/profile";
|
|
25151
25574
|
init_profiles();
|
|
@@ -25201,9 +25624,9 @@ function toCliError(error) {
|
|
|
25201
25624
|
const hint = error.code === ReplicaErrorCode.GRANT_MISSING && typeof error.detail?.hint === "string" ? error.detail.hint : void 0;
|
|
25202
25625
|
return new CLIError(error.code, error.message, EXIT_BY_CODE[error.code], hint === void 0 ? void 0 : { hint });
|
|
25203
25626
|
}
|
|
25204
|
-
async function run(action) {
|
|
25627
|
+
async function run(action, needsSqlite = true) {
|
|
25205
25628
|
try {
|
|
25206
|
-
await loadSqlite();
|
|
25629
|
+
if (needsSqlite) await loadSqlite();
|
|
25207
25630
|
await action();
|
|
25208
25631
|
} catch (error) {
|
|
25209
25632
|
return handleError(toCliError(error));
|
|
@@ -25216,20 +25639,25 @@ async function profileName(cmd) {
|
|
|
25216
25639
|
const config = await ProfileManager.getConfig().catch(() => void 0);
|
|
25217
25640
|
return config?.defaultProfile ?? DEFAULT_PROFILE;
|
|
25218
25641
|
}
|
|
25219
|
-
function replicasRoot(profile) {
|
|
25220
|
-
return join10(profilePath4(profile), "replicas");
|
|
25221
|
-
}
|
|
25222
25642
|
async function replicaNames(profile) {
|
|
25223
|
-
|
|
25224
|
-
|
|
25225
|
-
|
|
25226
|
-
|
|
25227
|
-
()
|
|
25228
|
-
() =>
|
|
25229
|
-
|
|
25230
|
-
|
|
25231
|
-
|
|
25232
|
-
|
|
25643
|
+
return await withReplicaRoot(profile, async (root) => {
|
|
25644
|
+
const names = [];
|
|
25645
|
+
for (const entry of await readdir5(root, { withFileTypes: true })) {
|
|
25646
|
+
if (entry.isSymbolicLink()) throw localPathChanged(profile);
|
|
25647
|
+
if (!NAME.test(entry.name) || !entry.isDirectory()) continue;
|
|
25648
|
+
const present = await withPinnedLocalDirectory(profile, root, entry.name, async (directory) => {
|
|
25649
|
+
const database = await lstat5(join10(directory, "replica.db"), { bigint: true }).catch((error) => {
|
|
25650
|
+
if (error.code === "ENOENT") return null;
|
|
25651
|
+
throw error;
|
|
25652
|
+
});
|
|
25653
|
+
if (database && (database.isSymbolicLink() || !database.isFile())) throw localPathChanged(profile);
|
|
25654
|
+
return database !== null;
|
|
25655
|
+
});
|
|
25656
|
+
if (present === null) throw localPathChanged(profile);
|
|
25657
|
+
if (present) names.push(entry.name);
|
|
25658
|
+
}
|
|
25659
|
+
return names.sort();
|
|
25660
|
+
}) ?? [];
|
|
25233
25661
|
}
|
|
25234
25662
|
function assertName(name) {
|
|
25235
25663
|
if (!NAME.test(name)) {
|
|
@@ -25237,6 +25665,67 @@ function assertName(name) {
|
|
|
25237
25665
|
}
|
|
25238
25666
|
return name;
|
|
25239
25667
|
}
|
|
25668
|
+
async function withReplicaRoot(profile, action, create4 = false) {
|
|
25669
|
+
const profileDirectory = profilePath5(profile);
|
|
25670
|
+
const readRoot = async (directory, recheckProfile) => {
|
|
25671
|
+
if (create4) {
|
|
25672
|
+
if (recheckProfile) await recheckProfile();
|
|
25673
|
+
await mkdir4(join10(directory, "replicas"), { mode: 448 }).catch((error) => {
|
|
25674
|
+
if (error.code !== "EEXIST") throw error;
|
|
25675
|
+
});
|
|
25676
|
+
}
|
|
25677
|
+
return withPinnedLocalDirectory(
|
|
25678
|
+
profile,
|
|
25679
|
+
directory,
|
|
25680
|
+
"replicas",
|
|
25681
|
+
(root, _named, _identity, recheckRoot) => action(root, recheckRoot, recheckProfile)
|
|
25682
|
+
);
|
|
25683
|
+
};
|
|
25684
|
+
if (process.platform === "linux") return readRoot(profileDirectory);
|
|
25685
|
+
const checked = await withPinnedLocalDirectory(
|
|
25686
|
+
profile,
|
|
25687
|
+
dirname6(profileDirectory),
|
|
25688
|
+
basename3(profileDirectory),
|
|
25689
|
+
(directory, _named, _identity, recheckProfile) => readRoot(directory, recheckProfile)
|
|
25690
|
+
);
|
|
25691
|
+
if (checked === null) throw localPathChanged(profile);
|
|
25692
|
+
return checked;
|
|
25693
|
+
}
|
|
25694
|
+
async function withReplicaDirectory(profile, name, action, create4 = false) {
|
|
25695
|
+
assertName(name);
|
|
25696
|
+
const result = await withReplicaRoot(profile, async (root, recheckRoot, recheckProfile) => {
|
|
25697
|
+
if (create4) {
|
|
25698
|
+
await recheckRoot();
|
|
25699
|
+
await mkdir4(join10(root, name), { mode: 448 }).catch((error) => {
|
|
25700
|
+
if (error.code !== "EEXIST") throw error;
|
|
25701
|
+
});
|
|
25702
|
+
}
|
|
25703
|
+
const opened = await withPinnedLocalDirectory(
|
|
25704
|
+
profile,
|
|
25705
|
+
root,
|
|
25706
|
+
name,
|
|
25707
|
+
async (_directory, path, expectedDirectory, recheckChild) => {
|
|
25708
|
+
await recheckRoot();
|
|
25709
|
+
await recheckChild();
|
|
25710
|
+
const database = await lstat5(join10(_directory, "replica.db"), { bigint: true }).catch((error) => {
|
|
25711
|
+
if (error.code === "ENOENT") return null;
|
|
25712
|
+
throw error;
|
|
25713
|
+
});
|
|
25714
|
+
if (database && (database.isSymbolicLink() || !database.isFile())) throw localPathChanged(profile);
|
|
25715
|
+
if (recheckProfile) {
|
|
25716
|
+
await recheckProfile();
|
|
25717
|
+
await recheckRoot();
|
|
25718
|
+
await recheckChild();
|
|
25719
|
+
}
|
|
25720
|
+
return action(path, expectedDirectory);
|
|
25721
|
+
}
|
|
25722
|
+
);
|
|
25723
|
+
if (opened === null) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Replica "${name}" was not found.`);
|
|
25724
|
+
return opened;
|
|
25725
|
+
}, create4);
|
|
25726
|
+
if (result === null) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas.`);
|
|
25727
|
+
return result;
|
|
25728
|
+
}
|
|
25240
25729
|
async function existingReplicaName(profile, option) {
|
|
25241
25730
|
if (option !== void 0) return assertName(option);
|
|
25242
25731
|
const names = await replicaNames(profile);
|
|
@@ -25258,17 +25747,37 @@ function profileGuard(profile) {
|
|
|
25258
25747
|
}
|
|
25259
25748
|
};
|
|
25260
25749
|
}
|
|
25261
|
-
async function
|
|
25262
|
-
return
|
|
25263
|
-
}
|
|
25264
|
-
|
|
25265
|
-
|
|
25266
|
-
|
|
25267
|
-
|
|
25268
|
-
|
|
25269
|
-
return store;
|
|
25750
|
+
async function withOpenReplica(profile, name, action, guard = profileGuard(profile)) {
|
|
25751
|
+
return withReplicaDirectory(profile, name, async (path, expectedDirectory) => {
|
|
25752
|
+
const store = await SqliteReplicaStore.open(path, { create: false, guard, expectedDirectory });
|
|
25753
|
+
try {
|
|
25754
|
+
return await action(store);
|
|
25755
|
+
} finally {
|
|
25756
|
+
await store.close();
|
|
25757
|
+
}
|
|
25270
25758
|
});
|
|
25271
25759
|
}
|
|
25760
|
+
function assertLegacyLogoutClear(profile) {
|
|
25761
|
+
try {
|
|
25762
|
+
lstatSync2(join10(profilePath5(profile), "replica-logout-pending.json"));
|
|
25763
|
+
} catch (error) {
|
|
25764
|
+
if (error?.code === "ENOENT") return;
|
|
25765
|
+
}
|
|
25766
|
+
throw new CLIError(
|
|
25767
|
+
"REPLICA_GATED",
|
|
25768
|
+
"A logout is pending or its state cannot be checked for this profile.",
|
|
25769
|
+
ExitCode.PERMISSION_DENIED
|
|
25770
|
+
);
|
|
25771
|
+
}
|
|
25772
|
+
async function withLegacyReplicaRead(profile, action) {
|
|
25773
|
+
await withProfileLock5(profile, async () => {
|
|
25774
|
+
const checkDelivery = async () => {
|
|
25775
|
+
assertLegacyLogoutClear(profile);
|
|
25776
|
+
};
|
|
25777
|
+
await checkDelivery();
|
|
25778
|
+
await action(checkDelivery);
|
|
25779
|
+
}, { requireProfile: true });
|
|
25780
|
+
}
|
|
25272
25781
|
async function storedGrants(profile) {
|
|
25273
25782
|
const authorizations = [];
|
|
25274
25783
|
const session = await readSession2(profile).catch(() => null);
|
|
@@ -25398,9 +25907,9 @@ function describeStatus(status) {
|
|
|
25398
25907
|
].join("\n");
|
|
25399
25908
|
}
|
|
25400
25909
|
async function writeFileAtomic(path, bytes) {
|
|
25401
|
-
const target =
|
|
25402
|
-
const temp = join10(
|
|
25403
|
-
const handle = await
|
|
25910
|
+
const target = resolve3(path);
|
|
25911
|
+
const temp = join10(dirname6(target), `.${basename3(target)}.${randomBytes6(6).toString("hex")}.tmp`);
|
|
25912
|
+
const handle = await open5(temp, fsConstants3.O_CREAT | fsConstants3.O_EXCL | fsConstants3.O_WRONLY, 384);
|
|
25404
25913
|
try {
|
|
25405
25914
|
await handle.writeFile(bytes);
|
|
25406
25915
|
await handle.sync();
|
|
@@ -25425,14 +25934,86 @@ function replicaSecretsWarning(space, prefix, secretsAllowed) {
|
|
|
25425
25934
|
if (!secretsAllowed || !requiresSecretsOptIn(space, prefix)) return void 0;
|
|
25426
25935
|
return `This replica stores the ciphertext of every secret under "${prefix}". A tinycloud.encryption/decrypt grant covers the whole encryption network, not one secret (TC-755), so whoever holds decrypt can open every replicated secret.`;
|
|
25427
25936
|
}
|
|
25428
|
-
|
|
25429
|
-
|
|
25937
|
+
function localFileId(value) {
|
|
25938
|
+
if (typeof value === "bigint") return value >= 0n ? value : null;
|
|
25939
|
+
return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
|
|
25940
|
+
}
|
|
25941
|
+
function sameLocalEntry(left, right) {
|
|
25942
|
+
const dev = localFileId(left.dev);
|
|
25943
|
+
const ino = localFileId(left.ino);
|
|
25944
|
+
return dev !== null && ino !== null && dev === localFileId(right.dev) && ino === localFileId(right.ino);
|
|
25945
|
+
}
|
|
25946
|
+
function localPathChanged(profile) {
|
|
25947
|
+
return new ProfileSymlinkError3(profile, "its local replica files contain a symlink or changed while reading");
|
|
25948
|
+
}
|
|
25949
|
+
async function assertNamedLocalEntry(profile, path, expected, kind) {
|
|
25950
|
+
const current = await lstat5(path, { bigint: true }).catch(() => null);
|
|
25951
|
+
if (!current || current.isSymbolicLink() || (kind === "directory" ? !current.isDirectory() : !current.isFile()) || !sameLocalEntry(expected, current)) throw localPathChanged(profile);
|
|
25952
|
+
}
|
|
25953
|
+
async function withPinnedLocalDirectory(profile, parent, name, action) {
|
|
25954
|
+
const path = join10(parent, name);
|
|
25955
|
+
const before = await lstat5(path, { bigint: true }).catch((error) => {
|
|
25956
|
+
if (error.code === "ENOENT") return null;
|
|
25957
|
+
throw error;
|
|
25958
|
+
});
|
|
25959
|
+
if (!before) return null;
|
|
25960
|
+
if (before.isSymbolicLink() || !before.isDirectory()) throw localPathChanged(profile);
|
|
25961
|
+
const handle = await open5(path, fsConstants3.O_RDONLY | fsConstants3.O_DIRECTORY | fsConstants3.O_NOFOLLOW).catch((error) => {
|
|
25962
|
+
if (error.code === "ELOOP" || error.code === "ENOTDIR" || error.code === "ENOENT") throw localPathChanged(profile);
|
|
25963
|
+
throw error;
|
|
25964
|
+
});
|
|
25965
|
+
try {
|
|
25966
|
+
const opened = await handle.stat({ bigint: true });
|
|
25967
|
+
if (!opened.isDirectory() || !sameLocalEntry(before, opened)) throw localPathChanged(profile);
|
|
25968
|
+
const dev = localFileId(opened.dev);
|
|
25969
|
+
const ino = localFileId(opened.ino);
|
|
25970
|
+
if (dev === null || ino === null || dev > BigInt(Number.MAX_SAFE_INTEGER) || ino > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
25971
|
+
throw localPathChanged(profile);
|
|
25972
|
+
}
|
|
25973
|
+
const pinned = process.platform === "linux" ? `/proc/self/fd/${handle.fd}` : path;
|
|
25974
|
+
const recheck = () => assertNamedLocalEntry(profile, path, opened, "directory");
|
|
25975
|
+
const result = await action(pinned, path, { dev: Number(dev), ino: Number(ino) }, recheck);
|
|
25976
|
+
await recheck();
|
|
25977
|
+
return result;
|
|
25978
|
+
} finally {
|
|
25979
|
+
await handle.close();
|
|
25980
|
+
}
|
|
25981
|
+
}
|
|
25982
|
+
async function readReportFile(profile, parent, name) {
|
|
25983
|
+
const path = join10(parent, name);
|
|
25984
|
+
const before = await lstat5(path, { bigint: true }).catch((error) => {
|
|
25985
|
+
if (error.code === "ENOENT") return null;
|
|
25986
|
+
throw error;
|
|
25987
|
+
});
|
|
25988
|
+
if (!before) return null;
|
|
25989
|
+
if (before.isSymbolicLink() || !before.isFile()) throw localPathChanged(profile);
|
|
25990
|
+
const handle = await open5(path, fsConstants3.O_RDONLY | fsConstants3.O_NOFOLLOW).catch((error) => {
|
|
25991
|
+
if (error.code === "ELOOP" || error.code === "ENOTDIR" || error.code === "ENOENT") throw localPathChanged(profile);
|
|
25992
|
+
throw error;
|
|
25993
|
+
});
|
|
25994
|
+
try {
|
|
25995
|
+
const opened = await handle.stat({ bigint: true });
|
|
25996
|
+
if (!opened.isFile() || !sameLocalEntry(before, opened)) throw localPathChanged(profile);
|
|
25997
|
+
const data = await handle.readFile("utf8");
|
|
25998
|
+
await assertNamedLocalEntry(profile, path, opened, "file");
|
|
25999
|
+
return data;
|
|
26000
|
+
} finally {
|
|
26001
|
+
await handle.close();
|
|
26002
|
+
}
|
|
26003
|
+
}
|
|
26004
|
+
async function readReportJson(profile, parent, name) {
|
|
26005
|
+
const data = await readReportFile(profile, parent, name);
|
|
26006
|
+
if (data === null) return null;
|
|
26007
|
+
try {
|
|
26008
|
+
return JSON.parse(data);
|
|
26009
|
+
} catch {
|
|
26010
|
+
return null;
|
|
26011
|
+
}
|
|
26012
|
+
}
|
|
26013
|
+
async function readReplicationEvents(profile, root, sinceMs) {
|
|
25430
26014
|
const events = [];
|
|
25431
26015
|
for (const name of ["events.jsonl.1", "events.jsonl"]) {
|
|
25432
|
-
const data = await
|
|
25433
|
-
if (error.code === "ENOENT") return "";
|
|
25434
|
-
throw error;
|
|
25435
|
-
});
|
|
26016
|
+
const data = await readReportFile(profile, root, name) ?? "";
|
|
25436
26017
|
for (const line of data.split("\n")) {
|
|
25437
26018
|
if (!line) continue;
|
|
25438
26019
|
try {
|
|
@@ -25444,27 +26025,26 @@ async function readReplicationEvents(profile, sinceMs) {
|
|
|
25444
26025
|
}
|
|
25445
26026
|
return events;
|
|
25446
26027
|
}
|
|
25447
|
-
async function replicationPartitions(profile) {
|
|
25448
|
-
const
|
|
25449
|
-
const entries = await readdir5(root, { withFileTypes: true }).catch((error) => {
|
|
25450
|
-
if (error.code === "ENOENT") return [];
|
|
25451
|
-
throw error;
|
|
25452
|
-
});
|
|
26028
|
+
async function replicationPartitions(profile, root) {
|
|
26029
|
+
const entries = await readdir5(root, { withFileTypes: true });
|
|
25453
26030
|
const partitions = [];
|
|
25454
26031
|
for (const entry of entries) {
|
|
25455
|
-
if (
|
|
25456
|
-
|
|
25457
|
-
|
|
25458
|
-
|
|
25459
|
-
|
|
25460
|
-
|
|
25461
|
-
|
|
25462
|
-
|
|
25463
|
-
|
|
25464
|
-
|
|
25465
|
-
|
|
25466
|
-
|
|
26032
|
+
if (!/^[a-z2-7]{26}$/.test(entry.name)) continue;
|
|
26033
|
+
if (entry.isSymbolicLink()) throw localPathChanged(profile);
|
|
26034
|
+
if (!entry.isDirectory()) continue;
|
|
26035
|
+
const partition = await withPinnedLocalDirectory(profile, root, entry.name, async (pinned) => {
|
|
26036
|
+
const identity = await readReportJson(profile, pinned, "identity.json");
|
|
26037
|
+
const pending = await readReportJson(profile, pinned, "pending.json");
|
|
26038
|
+
const pinnedCount = pending?.records?.filter((record2) => record2.state === "in_flight" || record2.state === "ambiguous").length ?? 0;
|
|
26039
|
+
return {
|
|
26040
|
+
idHash: entry.name,
|
|
26041
|
+
host: typeof identity?.host === "string" ? identity.host : null,
|
|
26042
|
+
space: typeof identity?.space === "string" ? identity.space : null,
|
|
26043
|
+
pinned: pinnedCount
|
|
26044
|
+
};
|
|
25467
26045
|
});
|
|
26046
|
+
if (!partition) throw localPathChanged(profile);
|
|
26047
|
+
partitions.push(partition);
|
|
25468
26048
|
}
|
|
25469
26049
|
return partitions.sort((left, right) => left.idHash.localeCompare(right.idHash));
|
|
25470
26050
|
}
|
|
@@ -25483,52 +26063,41 @@ SECRETS_OPT_IN_REQUIRED; 3 grant missing; 4 key absent, deleted, content missing
|
|
|
25483
26063
|
or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
|
|
25484
26064
|
7 node, protocol or integrity error; 10 storage full.`
|
|
25485
26065
|
);
|
|
25486
|
-
replica.command("report").description("
|
|
26066
|
+
replica.command("report").description("Read-only report of persisted replication events and identity partitions").option("--since <duration>", "Include events from the last duration (default: 24h)", "24h").option("--purge", "Refused: named replica reset --replica <name> --purge only; flag-owned purge unavailable (TC-1309)").option("--clear-pending", "Refused: no equivalent pending-clear command yet (TC-1309)").action(
|
|
25487
26067
|
(options, cmd) => run(async () => {
|
|
25488
|
-
const globals = cmd.optsWithGlobals();
|
|
25489
|
-
const context = await ProfileManager.resolveContext(globals);
|
|
25490
|
-
const node = context.replication ? await ensureAuthenticated(context) : void 0;
|
|
25491
26068
|
const profile = await profileName(cmd);
|
|
26069
|
+
if (options.purge) {
|
|
26070
|
+
throw new CLIError("REPORT_READ_ONLY", `Replica report is read-only. To remove a named local replica, run ${CLI_NAME22} replica reset --replica <name> --purge; flag-owned purge has no equivalent yet (TC-1309).`, ExitCode.USAGE_ERROR);
|
|
26071
|
+
}
|
|
26072
|
+
if (options.clearPending) {
|
|
26073
|
+
throw new CLIError("REPORT_READ_ONLY", "Clearing pending replica writes has no equivalent yet; see TC-1309 for a safe replacement command.", ExitCode.USAGE_ERROR);
|
|
26074
|
+
}
|
|
25492
26075
|
let durationMs;
|
|
25493
26076
|
try {
|
|
25494
26077
|
durationMs = parseDuration(options.since);
|
|
25495
26078
|
} catch (error) {
|
|
25496
26079
|
throw new CLIError("USAGE_ERROR", error instanceof Error ? error.message : String(error), ExitCode.USAGE_ERROR);
|
|
25497
26080
|
}
|
|
25498
|
-
const
|
|
25499
|
-
|
|
25500
|
-
|
|
25501
|
-
|
|
25502
|
-
|
|
25503
|
-
|
|
25504
|
-
|
|
25505
|
-
|
|
25506
|
-
|
|
25507
|
-
|
|
25508
|
-
|
|
25509
|
-
|
|
25510
|
-
|
|
25511
|
-
|
|
25512
|
-
|
|
25513
|
-
|
|
25514
|
-
|
|
25515
|
-
|
|
25516
|
-
const report = createReplicationReport(options.since, events, replicas, partitions, warning);
|
|
25517
|
-
if (shouldOutputJson()) {
|
|
25518
|
-
outputJson({
|
|
25519
|
-
...report,
|
|
25520
|
-
...cleared === void 0 ? {} : { cleared },
|
|
25521
|
-
...purged === void 0 ? {} : { purged }
|
|
25522
|
-
});
|
|
25523
|
-
} else {
|
|
25524
|
-
process.stdout.write(`${renderReplicationReport(report)}
|
|
25525
|
-
`);
|
|
25526
|
-
if (cleared !== void 0) process.stdout.write(`Cleared ${cleared} pending write record(s).
|
|
25527
|
-
`);
|
|
25528
|
-
if (purged !== void 0) process.stdout.write(`Purge: ${JSON.stringify(purged)}
|
|
26081
|
+
const report = await withPinnedProfileDirectory3(profile, async (directory) => {
|
|
26082
|
+
if (directory === null) throw new CLIError("PROFILE_NOT_FOUND", `Profile "${profile}" does not exist.`, ExitCode.ERROR);
|
|
26083
|
+
const readPersistedReport = async (root) => {
|
|
26084
|
+
await replicaNames(profile);
|
|
26085
|
+
const sinceMs = Date.now() - durationMs;
|
|
26086
|
+
const persisted = await withPinnedLocalDirectory(profile, root, "replication", async (directory2) => ({
|
|
26087
|
+
events: await readReplicationEvents(profile, directory2, sinceMs),
|
|
26088
|
+
partitions: await replicationPartitions(profile, directory2)
|
|
26089
|
+
}));
|
|
26090
|
+
return createReplicationReport(options.since, persisted?.events ?? [], persisted?.partitions ?? []);
|
|
26091
|
+
};
|
|
26092
|
+
if (process.platform === "linux") return readPersistedReport(directory);
|
|
26093
|
+
const checked = await withPinnedLocalDirectory(profile, dirname6(directory), basename3(directory), readPersistedReport);
|
|
26094
|
+
if (checked === null) throw localPathChanged(profile);
|
|
26095
|
+
return checked;
|
|
26096
|
+
});
|
|
26097
|
+
if (shouldOutputJson()) outputJson(report);
|
|
26098
|
+
else process.stdout.write(`${renderReplicationReport(report)}
|
|
25529
26099
|
`);
|
|
25530
|
-
|
|
25531
|
-
})
|
|
26100
|
+
}, false)
|
|
25532
26101
|
);
|
|
25533
26102
|
replica.command("sync").description("Create or update a replica from its source host").option("--replica <name>", "Replica name (default: the profile's only replica, or one named after the prefix)").option("--space <id|name>", "Space to replicate (first sync; default: the space of the sync grant)").option("--prefix <prefix>", "KV prefix to replicate, e.g. notes/ (first sync)").option("--retention-grant <cid>", "CID of a tinycloud.kv/retain grant: keep local reads after the sync grant expires").option("--limit <n>", "Feed page size (1-1000)", (value) => Number.parseInt(value, 10)).option("--allow-secrets", "Opt in to copying ciphertext from a secrets space, vault prefix, or whole space").action(
|
|
25534
26103
|
(options, cmd) => run(async () => {
|
|
@@ -25540,87 +26109,98 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
|
|
|
25540
26109
|
throw new CLIError("INVALID_REPLICATION_PREFIX", error instanceof Error ? error.message : String(error), ExitCode.USAGE_ERROR);
|
|
25541
26110
|
}
|
|
25542
26111
|
}
|
|
25543
|
-
|
|
25544
|
-
|
|
25545
|
-
|
|
25546
|
-
|
|
25547
|
-
|
|
25548
|
-
|
|
25549
|
-
|
|
25550
|
-
|
|
25551
|
-
|
|
25552
|
-
|
|
25553
|
-
|
|
25554
|
-
|
|
25555
|
-
|
|
25556
|
-
|
|
25557
|
-
|
|
25558
|
-
|
|
25559
|
-
|
|
25560
|
-
|
|
25561
|
-
|
|
25562
|
-
|
|
25563
|
-
|
|
25564
|
-
|
|
25565
|
-
|
|
25566
|
-
|
|
25567
|
-
|
|
25568
|
-
|
|
25569
|
-
|
|
25570
|
-
);
|
|
25571
|
-
|
|
25572
|
-
|
|
25573
|
-
|
|
25574
|
-
|
|
25575
|
-
|
|
25576
|
-
|
|
25577
|
-
|
|
25578
|
-
|
|
25579
|
-
|
|
25580
|
-
|
|
25581
|
-
|
|
25582
|
-
|
|
25583
|
-
|
|
25584
|
-
|
|
25585
|
-
|
|
25586
|
-
|
|
25587
|
-
|
|
25588
|
-
|
|
25589
|
-
|
|
25590
|
-
|
|
25591
|
-
|
|
25592
|
-
|
|
25593
|
-
|
|
25594
|
-
|
|
25595
|
-
|
|
25596
|
-
|
|
25597
|
-
|
|
25598
|
-
|
|
25599
|
-
|
|
25600
|
-
|
|
25601
|
-
|
|
25602
|
-
|
|
25603
|
-
|
|
25604
|
-
|
|
25605
|
-
|
|
25606
|
-
|
|
25607
|
-
|
|
25608
|
-
|
|
25609
|
-
|
|
25610
|
-
|
|
25611
|
-
|
|
25612
|
-
|
|
25613
|
-
|
|
25614
|
-
|
|
25615
|
-
|
|
25616
|
-
|
|
25617
|
-
|
|
26112
|
+
await withPinnedProfileDirectory3(profile, async (directory) => {
|
|
26113
|
+
if (directory === null) throw new CLIError("PROFILE_NOT_FOUND", `Profile "${profile}" does not exist.`, ExitCode.ERROR);
|
|
26114
|
+
const globals = cmd.optsWithGlobals();
|
|
26115
|
+
const config = await ProfileManager.getProfile(profile);
|
|
26116
|
+
const deviceDid = config.sessionDid ?? config.did;
|
|
26117
|
+
const jwk = await ProfileManager.getKey(profile);
|
|
26118
|
+
if (jwk === null) {
|
|
26119
|
+
throw new CLIError("AUTH_REQUIRED", `Profile "${profile}" has no device key.`, ExitCode.AUTH_REQUIRED);
|
|
26120
|
+
}
|
|
26121
|
+
if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
|
|
26122
|
+
throw new CLIError("USAGE_ERROR", "--limit must be an integer from 1 through 1000.", ExitCode.USAGE_ERROR);
|
|
26123
|
+
}
|
|
26124
|
+
const existing = await replicaNames(profile);
|
|
26125
|
+
const name = options.replica !== void 0 ? assertName(options.replica) : options.prefix !== void 0 ? assertName(options.prefix.replace(/\/+$/, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[^A-Za-z0-9]+/, "") || "default") : await existingReplicaName(profile, void 0);
|
|
26126
|
+
const grants = await storedGrants(profile);
|
|
26127
|
+
const initialConfig = existing.includes(name) ? null : (() => {
|
|
26128
|
+
const prefix = options.prefix;
|
|
26129
|
+
if (prefix === void 0) {
|
|
26130
|
+
throw new CLIError("USAGE_ERROR", "The first sync of a replica needs --prefix (and a device grant covering it).", ExitCode.USAGE_ERROR);
|
|
26131
|
+
}
|
|
26132
|
+
const space = resolveSpace(options.space, prefix, grants);
|
|
26133
|
+
if (requiresSecretsOptIn(space, prefix) && options.allowSecrets !== true) {
|
|
26134
|
+
throw new ReplicaError(
|
|
26135
|
+
ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
|
|
26136
|
+
`Replicating ${space}/kv/${prefix} copies encrypted secret material to this disk; pass --allow-secrets to opt in.`
|
|
26137
|
+
);
|
|
26138
|
+
}
|
|
26139
|
+
const host = (globals.host ?? process.env.TC_HOST ?? config.host)?.replace(/\/+$/, "");
|
|
26140
|
+
if (!host) throw new CLIError("USAGE_ERROR", "Pass --host: the profile has no host to pin.", ExitCode.USAGE_ERROR);
|
|
26141
|
+
chooseGrant(grants, deviceDid, space, prefix);
|
|
26142
|
+
return {
|
|
26143
|
+
name,
|
|
26144
|
+
replicaId: randomBytes6(16).toString("hex"),
|
|
26145
|
+
host,
|
|
26146
|
+
space,
|
|
26147
|
+
prefix,
|
|
26148
|
+
deviceDid,
|
|
26149
|
+
allowSecrets: options.allowSecrets === true,
|
|
26150
|
+
localReadPolicy: "whileGrantValid",
|
|
26151
|
+
retentionGrantCid: null
|
|
26152
|
+
};
|
|
26153
|
+
})();
|
|
26154
|
+
const { replica: replica2, report, status, warning } = await withReplicaDirectory(profile, name, async (path, expectedDirectory) => {
|
|
26155
|
+
const store = await SqliteReplicaStore.open(path, {
|
|
26156
|
+
create: initialConfig !== null,
|
|
26157
|
+
guard: profileGuard(profile),
|
|
26158
|
+
expectedDirectory
|
|
26159
|
+
});
|
|
26160
|
+
try {
|
|
26161
|
+
if (initialConfig !== null && await store.open() === null) await store.init(initialConfig);
|
|
26162
|
+
const state2 = await store.open();
|
|
26163
|
+
if (initialConfig === null) {
|
|
26164
|
+
const differs = syncOptionsDiffer(state2.config, {
|
|
26165
|
+
...options.space === void 0 ? {} : { space: options.space },
|
|
26166
|
+
...options.prefix === void 0 ? {} : { prefix: options.prefix },
|
|
26167
|
+
...globals.host === void 0 ? {} : { host: globals.host }
|
|
26168
|
+
});
|
|
26169
|
+
if (differs !== void 0) {
|
|
26170
|
+
throw new ReplicaError(
|
|
26171
|
+
ReplicaErrorCode.CONFIG_MISMATCH,
|
|
26172
|
+
`Replica ${name} already follows ${differs}. Use another --replica name, or ${CLI_NAME22} replica reset --replica ${name} --purge first.`
|
|
26173
|
+
);
|
|
26174
|
+
}
|
|
26175
|
+
}
|
|
26176
|
+
const { space, prefix, host } = state2.config;
|
|
26177
|
+
if (requiresSecretsOptIn(space, prefix) && !state2.config.allowSecrets && options.allowSecrets !== true) {
|
|
26178
|
+
throw new ReplicaError(ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED, `Replicating ${space}/kv/${prefix} needs --allow-secrets.`);
|
|
26179
|
+
}
|
|
26180
|
+
let grant;
|
|
26181
|
+
try {
|
|
26182
|
+
grant = chooseGrant(grants, state2.config.deviceDid, space, prefix);
|
|
26183
|
+
} catch (error) {
|
|
26184
|
+
if (!isReplicaError(error, ReplicaErrorCode.GRANT_MISSING) || (state2.grant ?? state2.pendingGrant) === null) throw error;
|
|
26185
|
+
}
|
|
26186
|
+
if (grant !== void 0) await store.installGrant(grant);
|
|
26187
|
+
if (options.retentionGrant !== void 0 && options.retentionGrant !== state2.config.retentionGrantCid) {
|
|
26188
|
+
await store.setRetentionGrant(options.retentionGrant === "" ? null : options.retentionGrant);
|
|
26189
|
+
}
|
|
26190
|
+
const current = await store.open();
|
|
26191
|
+
const transportFor = await grantTransports({ host, space, deviceDid: current.config.deviceDid, jwk });
|
|
26192
|
+
const warning2 = replicaSecretsWarning(space, prefix, options.allowSecrets === true || state2.config.allowSecrets);
|
|
26193
|
+
const report2 = await new Replica({ store, transportFor }).sync(options.limit === void 0 ? {} : { limit: options.limit });
|
|
26194
|
+
const status2 = await store.status();
|
|
26195
|
+
return { replica: current.config.name, report: report2, status: status2, warning: warning2 };
|
|
26196
|
+
} finally {
|
|
26197
|
+
await store.close();
|
|
26198
|
+
}
|
|
26199
|
+
}, initialConfig !== null);
|
|
25618
26200
|
if (warning !== void 0) process.stderr.write(`Warning: ${warning}
|
|
25619
26201
|
`);
|
|
25620
|
-
const report = await new Replica({ store, transportFor }).sync(options.limit === void 0 ? {} : { limit: options.limit });
|
|
25621
|
-
const status = await store.status();
|
|
25622
26202
|
if (shouldOutputJson()) {
|
|
25623
|
-
outputJson({ replica:
|
|
26203
|
+
outputJson({ replica: replica2, sync: report, status, ...warning === void 0 ? {} : { warning } });
|
|
25624
26204
|
} else {
|
|
25625
26205
|
process.stdout.write(
|
|
25626
26206
|
`${theme.success("\u2713")} Synced ${report.changes} change(s) in ${report.pages} page(s), fetched ${report.fetched} value(s).
|
|
@@ -25628,40 +26208,39 @@ ${describeStatus(status)}
|
|
|
25628
26208
|
`
|
|
25629
26209
|
);
|
|
25630
26210
|
}
|
|
25631
|
-
}
|
|
25632
|
-
await store.close();
|
|
25633
|
-
}
|
|
26211
|
+
});
|
|
25634
26212
|
})
|
|
25635
26213
|
);
|
|
25636
26214
|
replica.command("get <key>").description("Read a key from the local replica (never touches the network)").option("--replica <name>", "Replica name").option("--raw", "Write the value's bytes to stdout").option("-o, --output <file>", "Write the value to a file (atomically, mode 0600)").option("--no-verify", "Skip re-hashing the stored content").action(
|
|
25637
26215
|
(key, options, cmd) => run(async () => {
|
|
25638
26216
|
const profile = await profileName(cmd);
|
|
25639
|
-
|
|
25640
|
-
|
|
25641
|
-
|
|
25642
|
-
|
|
25643
|
-
|
|
25644
|
-
|
|
25645
|
-
|
|
25646
|
-
|
|
25647
|
-
|
|
25648
|
-
|
|
25649
|
-
|
|
25650
|
-
|
|
25651
|
-
|
|
25652
|
-
|
|
25653
|
-
|
|
25654
|
-
|
|
25655
|
-
|
|
26217
|
+
await withLegacyReplicaRead(profile, async (checkDelivery) => {
|
|
26218
|
+
const result = await withOpenReplica(
|
|
26219
|
+
profile,
|
|
26220
|
+
await existingReplicaName(profile, options.replica),
|
|
26221
|
+
(store) => new Replica({ store }).get(key, { verify: options.verify !== false })
|
|
26222
|
+
);
|
|
26223
|
+
await checkDelivery();
|
|
26224
|
+
if (result.status !== "present") {
|
|
26225
|
+
const failure = READ_FAILURE[result.status];
|
|
26226
|
+
throw new CLIError(failure.code, `Key ${JSON.stringify(key)} ${failure.what}.`, failure.exit);
|
|
26227
|
+
}
|
|
26228
|
+
if (options.output) {
|
|
26229
|
+
await writeFileAtomic(options.output, result.value);
|
|
26230
|
+
outputJson({ key, written: options.output, etag: result.etag, meta: result.meta });
|
|
26231
|
+
return;
|
|
26232
|
+
}
|
|
26233
|
+
if (options.raw) {
|
|
26234
|
+
process.stdout.write(result.value);
|
|
26235
|
+
return;
|
|
26236
|
+
}
|
|
26237
|
+
if (shouldOutputJson()) {
|
|
26238
|
+
outputJson({ key, status: result.status, ...jsonValue(result.value), etag: result.etag, metadata: result.metadata, meta: result.meta });
|
|
26239
|
+
return;
|
|
26240
|
+
}
|
|
25656
26241
|
process.stdout.write(result.value);
|
|
25657
|
-
|
|
25658
|
-
}
|
|
25659
|
-
if (shouldOutputJson()) {
|
|
25660
|
-
outputJson({ key, status: result.status, ...jsonValue(result.value), etag: result.etag, metadata: result.metadata, meta: result.meta });
|
|
25661
|
-
return;
|
|
25662
|
-
}
|
|
25663
|
-
process.stdout.write(result.value);
|
|
25664
|
-
process.stdout.write("\n");
|
|
26242
|
+
process.stdout.write("\n");
|
|
26243
|
+
});
|
|
25665
26244
|
})
|
|
25666
26245
|
);
|
|
25667
26246
|
replica.command("list [prefix]").description("List live keys in the local replica (never touches the network)").option("--replica <name>", "Replica name").option("--after <key>", "Start after this key").option("--limit <n>", "Maximum keys", (value) => Number.parseInt(value, 10)).action(
|
|
@@ -25670,13 +26249,17 @@ ${describeStatus(status)}
|
|
|
25670
26249
|
throw new CLIError("USAGE_ERROR", "--limit must be a positive integer.", ExitCode.USAGE_ERROR);
|
|
25671
26250
|
}
|
|
25672
26251
|
const profile = await profileName(cmd);
|
|
25673
|
-
|
|
25674
|
-
|
|
25675
|
-
|
|
25676
|
-
|
|
25677
|
-
|
|
25678
|
-
|
|
25679
|
-
|
|
26252
|
+
await withLegacyReplicaRead(profile, async (checkDelivery) => {
|
|
26253
|
+
const { entries, meta } = await withOpenReplica(
|
|
26254
|
+
profile,
|
|
26255
|
+
await existingReplicaName(profile, options.replica),
|
|
26256
|
+
(store) => new Replica({ store }).list({
|
|
26257
|
+
...prefix === void 0 ? {} : { prefix },
|
|
26258
|
+
...options.after === void 0 ? {} : { after: options.after },
|
|
26259
|
+
...options.limit === void 0 ? {} : { limit: options.limit }
|
|
26260
|
+
})
|
|
26261
|
+
);
|
|
26262
|
+
await checkDelivery();
|
|
25680
26263
|
if (shouldOutputJson()) {
|
|
25681
26264
|
outputJson({ keys: entries, count: entries.length, prefix: prefix ?? null, meta });
|
|
25682
26265
|
} else if (entries.length === 0) {
|
|
@@ -25689,50 +26272,57 @@ ${describeStatus(status)}
|
|
|
25689
26272
|
) + "\n"
|
|
25690
26273
|
);
|
|
25691
26274
|
}
|
|
25692
|
-
}
|
|
25693
|
-
await store.close();
|
|
25694
|
-
}
|
|
26275
|
+
});
|
|
25695
26276
|
})
|
|
25696
26277
|
);
|
|
25697
26278
|
replica.command("status").description("Show one replica's sync state, authority and counts, or all of them").option("--replica <name>", "Replica name").action(
|
|
25698
26279
|
(options, cmd) => run(async () => {
|
|
25699
26280
|
const profile = await profileName(cmd);
|
|
25700
|
-
|
|
25701
|
-
|
|
25702
|
-
|
|
25703
|
-
const
|
|
25704
|
-
|
|
25705
|
-
statuses.push(await new Replica({ store }).status());
|
|
25706
|
-
} finally {
|
|
25707
|
-
await store.close();
|
|
26281
|
+
await withLegacyReplicaRead(profile, async (checkDelivery) => {
|
|
26282
|
+
const names = options.replica === void 0 ? await replicaNames(profile) : [assertName(options.replica)];
|
|
26283
|
+
const statuses = [];
|
|
26284
|
+
for (const name of names) {
|
|
26285
|
+
statuses.push(await withOpenReplica(profile, name, (store) => new Replica({ store }).status()));
|
|
25708
26286
|
}
|
|
25709
|
-
|
|
25710
|
-
|
|
25711
|
-
|
|
25712
|
-
|
|
25713
|
-
|
|
25714
|
-
|
|
25715
|
-
|
|
25716
|
-
|
|
26287
|
+
await checkDelivery();
|
|
26288
|
+
if (shouldOutputJson()) {
|
|
26289
|
+
outputJson(options.replica === void 0 ? { replicas: statuses } : statuses[0]);
|
|
26290
|
+
} else if (statuses.length === 0) {
|
|
26291
|
+
process.stdout.write(theme.muted(`Profile "${profile}" has no replicas.`) + "\n");
|
|
26292
|
+
} else {
|
|
26293
|
+
process.stdout.write(statuses.map(describeStatus).join("\n\n") + "\n");
|
|
26294
|
+
}
|
|
26295
|
+
});
|
|
25717
26296
|
})
|
|
25718
26297
|
);
|
|
25719
26298
|
replica.command("reset").description("Clear a replica's entries, content and cursor (keeps its configuration and grant)").option("--replica <name>", "Replica name").option("--purge", "Remove the replica entirely").action(
|
|
25720
26299
|
(options, cmd) => run(async () => {
|
|
25721
26300
|
const profile = await profileName(cmd);
|
|
25722
|
-
|
|
25723
|
-
|
|
25724
|
-
try {
|
|
26301
|
+
await withProfileLock5(profile, async () => {
|
|
26302
|
+
const name = await existingReplicaName(profile, options.replica);
|
|
25725
26303
|
if (options.purge) {
|
|
25726
|
-
|
|
25727
|
-
|
|
25728
|
-
|
|
26304
|
+
await withPinnedReplicaForReset(profile, name, async (path, recheck, expectedDirectory) => {
|
|
26305
|
+
const profileLock = profileGuard(profile);
|
|
26306
|
+
const safeGuard = (section) => profileLock(async () => {
|
|
26307
|
+
await recheck();
|
|
26308
|
+
return section();
|
|
26309
|
+
});
|
|
26310
|
+
const store = await SqliteReplicaStore.open(path, { create: false, guard: safeGuard, expectedDirectory });
|
|
26311
|
+
try {
|
|
26312
|
+
await recheck();
|
|
26313
|
+
const lease = await store.acquireSyncLease(6e4);
|
|
26314
|
+
if (lease === null) throw new ReplicaError(ReplicaErrorCode.BUSY, "Another process is syncing this replica.");
|
|
26315
|
+
await recheck();
|
|
26316
|
+
await store.destroy(lease);
|
|
26317
|
+
} finally {
|
|
26318
|
+
await store.close();
|
|
26319
|
+
}
|
|
26320
|
+
});
|
|
25729
26321
|
} else {
|
|
25730
|
-
await new Replica({ store }).reset("manual");
|
|
26322
|
+
await withOpenReplica(profile, name, (store) => new Replica({ store }).reset("manual"));
|
|
25731
26323
|
}
|
|
25732
|
-
|
|
25733
|
-
|
|
25734
|
-
}
|
|
25735
|
-
outputJson({ replica: name, reset: true, purged: options.purge === true });
|
|
26324
|
+
outputJson({ replica: name, reset: true, purged: options.purge === true });
|
|
26325
|
+
}, { requireProfile: true });
|
|
25736
26326
|
})
|
|
25737
26327
|
);
|
|
25738
26328
|
}
|
|
@@ -25745,7 +26335,7 @@ init_errors();
|
|
|
25745
26335
|
init_warnings();
|
|
25746
26336
|
init_constants();
|
|
25747
26337
|
import { CLI_NAME as CLI_NAME24 } from "@tinycloud/operations/cli-name";
|
|
25748
|
-
import { readFile as
|
|
26338
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
25749
26339
|
import { join as join11 } from "path";
|
|
25750
26340
|
import { homedir, hostname, userInfo } from "os";
|
|
25751
26341
|
import { appSecretScope } from "@tinycloud/sdk-core";
|
|
@@ -25774,24 +26364,24 @@ init_space();
|
|
|
25774
26364
|
// src/lib/browser.ts
|
|
25775
26365
|
init_formatter();
|
|
25776
26366
|
init_theme();
|
|
25777
|
-
import
|
|
26367
|
+
import open6 from "open";
|
|
25778
26368
|
var LAUNCH_CHECK_MS = 1e3;
|
|
25779
26369
|
async function tryOpenBrowser(url) {
|
|
25780
26370
|
let launcher;
|
|
25781
26371
|
try {
|
|
25782
|
-
launcher = await
|
|
26372
|
+
launcher = await open6(url);
|
|
25783
26373
|
} catch {
|
|
25784
26374
|
return false;
|
|
25785
26375
|
}
|
|
25786
|
-
return new Promise((
|
|
25787
|
-
const timer = setTimeout(() =>
|
|
26376
|
+
return new Promise((resolve6) => {
|
|
26377
|
+
const timer = setTimeout(() => resolve6(true), LAUNCH_CHECK_MS);
|
|
25788
26378
|
launcher.once("error", () => {
|
|
25789
26379
|
clearTimeout(timer);
|
|
25790
|
-
|
|
26380
|
+
resolve6(false);
|
|
25791
26381
|
});
|
|
25792
26382
|
launcher.once("exit", (code3) => {
|
|
25793
26383
|
clearTimeout(timer);
|
|
25794
|
-
|
|
26384
|
+
resolve6(code3 === 0);
|
|
25795
26385
|
});
|
|
25796
26386
|
});
|
|
25797
26387
|
}
|
|
@@ -25835,9 +26425,9 @@ function runProbeChild(request, options = {}) {
|
|
|
25835
26425
|
const killGraceMs = options.killGraceMs ?? KILL_GRACE_MS;
|
|
25836
26426
|
const spawn2 = options.spawn ?? nodeSpawn;
|
|
25837
26427
|
const signal = options.signal;
|
|
25838
|
-
return new Promise((
|
|
26428
|
+
return new Promise((resolve6) => {
|
|
25839
26429
|
if (signal?.aborted) {
|
|
25840
|
-
|
|
26430
|
+
resolve6({ kind: "cancelled" });
|
|
25841
26431
|
return;
|
|
25842
26432
|
}
|
|
25843
26433
|
const child = spawn2(command, args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -25873,7 +26463,7 @@ function runProbeChild(request, options = {}) {
|
|
|
25873
26463
|
const envelope = stdout.length > 0 && stdout.length <= MAX_PROBE_LINE_BYTES ? parseStatusEnvelope(stdout.toString("utf8")) : void 0;
|
|
25874
26464
|
outcome = envelope !== void 0 ? { kind: "ok", envelope } : probeFailed(stderr.toString("utf8"));
|
|
25875
26465
|
}
|
|
25876
|
-
|
|
26466
|
+
resolve6(outcome);
|
|
25877
26467
|
};
|
|
25878
26468
|
if (child.pid !== void 0) options.onSpawn?.(child.pid);
|
|
25879
26469
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -25965,15 +26555,15 @@ var TIMEOUT_RE = /^(0|\d+s|\d+m)$/;
|
|
|
25965
26555
|
var RETRYABLE_STATUS_ERRORS = { NODE_UNREACHABLE: true, NODE_ERROR: true };
|
|
25966
26556
|
var EXPLICIT_KEY_HINT = `${CLI_NAME23} secrets wait never signs in, and an explicit private key has no stored session. Use a local profile signed in once with ${CLI_NAME23} --profile <profile> auth login --method local, without TC_PRIVATE_KEY.`;
|
|
25967
26557
|
function abortableSleep(ms2, signal) {
|
|
25968
|
-
return new Promise((
|
|
26558
|
+
return new Promise((resolve6) => {
|
|
25969
26559
|
if (signal.aborted) {
|
|
25970
|
-
|
|
26560
|
+
resolve6();
|
|
25971
26561
|
return;
|
|
25972
26562
|
}
|
|
25973
26563
|
const done = () => {
|
|
25974
26564
|
clearTimeout(timer);
|
|
25975
26565
|
signal.removeEventListener("abort", done);
|
|
25976
|
-
|
|
26566
|
+
resolve6();
|
|
25977
26567
|
};
|
|
25978
26568
|
const timer = setTimeout(done, ms2);
|
|
25979
26569
|
signal.addEventListener("abort", done, { once: true });
|
|
@@ -26639,7 +27229,7 @@ function permissionsFromDelegation2(delegation) {
|
|
|
26639
27229
|
}
|
|
26640
27230
|
async function loadDelegationCandidates(source) {
|
|
26641
27231
|
try {
|
|
26642
|
-
const raw = JSON.parse(await
|
|
27232
|
+
const raw = JSON.parse(await readFile9(source, "utf8"));
|
|
26643
27233
|
return normalizeDelegationCandidates(raw, source);
|
|
26644
27234
|
} catch (error) {
|
|
26645
27235
|
if (error?.code !== "ENOENT") {
|
|
@@ -26659,7 +27249,7 @@ async function loadDelegationCandidates(source) {
|
|
|
26659
27249
|
}
|
|
26660
27250
|
try {
|
|
26661
27251
|
const importedPath = join11(resolveProfilesDir(), source, "additional-delegations.json");
|
|
26662
|
-
const raw = JSON.parse(await
|
|
27252
|
+
const raw = JSON.parse(await readFile9(importedPath, "utf8"));
|
|
26663
27253
|
return normalizeDelegationCandidates(raw, source);
|
|
26664
27254
|
} catch (error) {
|
|
26665
27255
|
if (error?.code === "ENOENT") {
|
|
@@ -27166,7 +27756,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
27166
27756
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
27167
27757
|
}
|
|
27168
27758
|
if (options.file) {
|
|
27169
|
-
secretValue = await
|
|
27759
|
+
secretValue = await readFile9(options.file, "utf-8");
|
|
27170
27760
|
} else if (options.stdin) {
|
|
27171
27761
|
secretValue = (await readStdin3()).toString("utf-8");
|
|
27172
27762
|
} else {
|
|
@@ -32753,7 +33343,7 @@ async function importAesKey(key32, usage) {
|
|
|
32753
33343
|
[usage]
|
|
32754
33344
|
);
|
|
32755
33345
|
}
|
|
32756
|
-
async function
|
|
33346
|
+
async function open7(blob, key32) {
|
|
32757
33347
|
if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
|
|
32758
33348
|
throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
|
|
32759
33349
|
}
|
|
@@ -33091,7 +33681,7 @@ async function resolvePolicyShare(link3, options) {
|
|
|
33091
33681
|
options.signal?.throwIfAborted();
|
|
33092
33682
|
let envelope;
|
|
33093
33683
|
try {
|
|
33094
|
-
const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await
|
|
33684
|
+
const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await open7(parsed.ciphertext, parsed.key32));
|
|
33095
33685
|
const value = JSON.parse(encoded);
|
|
33096
33686
|
if (canonicalize2(value) !== encoded) throw new Error("non-canonical envelope");
|
|
33097
33687
|
envelope = shareEnvelopeV3Schema.parse(value);
|
|
@@ -35683,9 +36273,9 @@ function receiveJson(result, path) {
|
|
|
35683
36273
|
|
|
35684
36274
|
// src/share/io.ts
|
|
35685
36275
|
import { constants } from "fs";
|
|
35686
|
-
import { lstat as
|
|
36276
|
+
import { lstat as lstat6, mkdir as mkdir5, mkdtemp, open as open8, readFile as readFile10, realpath as realpath2, stat as stat5, link as link2, rename as rename4, rm as rm6, unlink as unlink3 } from "fs/promises";
|
|
35687
36277
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
35688
|
-
import { basename as basename4, join as join12, resolve as
|
|
36278
|
+
import { basename as basename4, join as join12, resolve as resolve4, sep as sep2 } from "path";
|
|
35689
36279
|
init_errors();
|
|
35690
36280
|
var MAX_SHARE_STDIN_BYTES = 100 * 1024 * 1024;
|
|
35691
36281
|
var MAX_SHARE_URL_BYTES = 64 * 1024;
|
|
@@ -35722,26 +36312,26 @@ function shareFilename(value) {
|
|
|
35722
36312
|
}
|
|
35723
36313
|
}
|
|
35724
36314
|
function shareInputFilename(input, name) {
|
|
35725
|
-
return shareFilename(name ?? (input === "-" ? "stdin.md" : basename4(
|
|
36315
|
+
return shareFilename(name ?? (input === "-" ? "stdin.md" : basename4(resolve4(input))));
|
|
35726
36316
|
}
|
|
35727
36317
|
async function readShareInput(input, name, limit = MAX_SHARE_STDIN_BYTES) {
|
|
35728
36318
|
const filename = shareInputFilename(input, name);
|
|
35729
36319
|
if (input === "-") return { bytes: await readBoundedStdin(limit), filename };
|
|
35730
|
-
const path =
|
|
35731
|
-
const info = await
|
|
36320
|
+
const path = resolve4(input);
|
|
36321
|
+
const info = await stat5(path);
|
|
35732
36322
|
if (!info.isFile() || info.size > limit) throw new Error("MAX_BYTES_EXCEEDED");
|
|
35733
|
-
const bytes = new Uint8Array(await
|
|
36323
|
+
const bytes = new Uint8Array(await readFile10(path));
|
|
35734
36324
|
if (bytes.byteLength > limit) throw new Error("MAX_BYTES_EXCEEDED");
|
|
35735
36325
|
return { bytes, filename };
|
|
35736
36326
|
}
|
|
35737
36327
|
async function assertDirectory(path) {
|
|
35738
|
-
const absolute =
|
|
36328
|
+
const absolute = resolve4(path);
|
|
35739
36329
|
const segments = absolute.split(sep2).filter(Boolean);
|
|
35740
36330
|
let current = absolute.startsWith(sep2) ? sep2 : "";
|
|
35741
36331
|
for (const segment of segments) {
|
|
35742
36332
|
current = current === sep2 ? join12(current, segment) : join12(current, segment);
|
|
35743
36333
|
try {
|
|
35744
|
-
const info = await
|
|
36334
|
+
const info = await lstat6(current);
|
|
35745
36335
|
if (info.isSymbolicLink()) {
|
|
35746
36336
|
const canonical = await realpath2(current);
|
|
35747
36337
|
if (current !== "/tmp" && current !== "/var") throw new Error("OUTPUT_EXISTS");
|
|
@@ -35749,27 +36339,27 @@ async function assertDirectory(path) {
|
|
|
35749
36339
|
} else if (!info.isDirectory()) throw new Error("OUTPUT_EXISTS");
|
|
35750
36340
|
} catch (error) {
|
|
35751
36341
|
if (error.code !== "ENOENT") throw error;
|
|
35752
|
-
await
|
|
35753
|
-
const created = await
|
|
36342
|
+
await mkdir5(current, { mode: 448 });
|
|
36343
|
+
const created = await lstat6(current);
|
|
35754
36344
|
if (created.isSymbolicLink() || !created.isDirectory()) throw new Error("OUTPUT_EXISTS");
|
|
35755
36345
|
}
|
|
35756
36346
|
}
|
|
35757
36347
|
}
|
|
35758
36348
|
async function writeShareOutput(directory, filename, bytes, force) {
|
|
35759
|
-
const outputDirectory =
|
|
36349
|
+
const outputDirectory = resolve4(directory);
|
|
35760
36350
|
await assertDirectory(outputDirectory);
|
|
35761
36351
|
const safeName = safeFilename(filename);
|
|
35762
|
-
const directoryHandle = await
|
|
36352
|
+
const directoryHandle = await open8(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
|
|
35763
36353
|
const stableDirectory = await realpath2(outputDirectory);
|
|
35764
36354
|
const outputPath = join12(stableDirectory, safeName);
|
|
35765
36355
|
const directoryIdentity = await directoryHandle.stat();
|
|
35766
36356
|
const assertStableDirectory = async () => {
|
|
35767
|
-
const current = await
|
|
36357
|
+
const current = await stat5(stableDirectory);
|
|
35768
36358
|
if (current.dev !== directoryIdentity.dev || current.ino !== directoryIdentity.ino) throw new Error("OUTPUT_EXISTS");
|
|
35769
36359
|
};
|
|
35770
36360
|
await assertStableDirectory();
|
|
35771
36361
|
const stagingDirectory = await mkdtemp(join12(stableDirectory, ".tinycloud-share-stage-"));
|
|
35772
|
-
const stagingInfo = await
|
|
36362
|
+
const stagingInfo = await lstat6(stagingDirectory);
|
|
35773
36363
|
if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
|
|
35774
36364
|
const stagingPath = join12(stagingDirectory, `.tinycloud-share-${randomBytes7(16).toString("hex")}.tmp`);
|
|
35775
36365
|
let temporaryPath;
|
|
@@ -35777,13 +36367,13 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
35777
36367
|
try {
|
|
35778
36368
|
await assertStableDirectory();
|
|
35779
36369
|
try {
|
|
35780
|
-
const existing = await
|
|
36370
|
+
const existing = await lstat6(outputPath);
|
|
35781
36371
|
if (existing.isSymbolicLink()) throw new Error("UNSAFE_FILENAME");
|
|
35782
36372
|
} catch (error) {
|
|
35783
36373
|
if (error.code !== "ENOENT") throw error;
|
|
35784
36374
|
}
|
|
35785
36375
|
temporaryPath = stagingPath;
|
|
35786
|
-
handle = await
|
|
36376
|
+
handle = await open8(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
|
|
35787
36377
|
await handle.writeFile(bytes);
|
|
35788
36378
|
await handle.close();
|
|
35789
36379
|
handle = void 0;
|
|
@@ -36233,8 +36823,8 @@ init_theme();
|
|
|
36233
36823
|
import { CLI_NAME as CLI_NAME26 } from "@tinycloud/operations/cli-name";
|
|
36234
36824
|
import { ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE2 } from "@tinycloud/node-sdk";
|
|
36235
36825
|
import { randomBytes as randomBytes8 } from "crypto";
|
|
36236
|
-
import { mkdir as
|
|
36237
|
-
import { dirname as
|
|
36826
|
+
import { mkdir as mkdir6, writeFile as writeFile4 } from "fs/promises";
|
|
36827
|
+
import { dirname as dirname7 } from "path";
|
|
36238
36828
|
function didWithoutFragment2(did) {
|
|
36239
36829
|
const fragment = did.indexOf("#");
|
|
36240
36830
|
return fragment === -1 ? did : did.slice(0, fragment);
|
|
@@ -36390,7 +36980,7 @@ it directly with \`${CLI_NAME26} space host <name>\` (no request needed).
|
|
|
36390
36980
|
}
|
|
36391
36981
|
async function emitHostRequestArtifact(artifact, emitOption) {
|
|
36392
36982
|
if (typeof emitOption === "string" && emitOption.length > 0) {
|
|
36393
|
-
await
|
|
36983
|
+
await mkdir6(dirname7(emitOption), { recursive: true });
|
|
36394
36984
|
await writeFile4(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
36395
36985
|
outputJson({
|
|
36396
36986
|
emitted: true,
|
|
@@ -36416,7 +37006,7 @@ init_host();
|
|
|
36416
37006
|
init_theme();
|
|
36417
37007
|
import { CLI_NAME as CLI_NAME27 } from "@tinycloud/operations/cli-name";
|
|
36418
37008
|
import { writeFile as writeFile5 } from "fs/promises";
|
|
36419
|
-
import { resolve as
|
|
37009
|
+
import { resolve as resolve5 } from "path";
|
|
36420
37010
|
async function dbHandle(node, dbName, spaceInput, profileName2) {
|
|
36421
37011
|
const spaceUri = await resolveSpaceUri(spaceInput, profileName2);
|
|
36422
37012
|
const sql = spaceUri ? node.sqlForSpace(spaceUri) : node.sql;
|
|
@@ -36558,7 +37148,7 @@ Output:
|
|
|
36558
37148
|
}
|
|
36559
37149
|
const blob = result.data;
|
|
36560
37150
|
const buffer = Buffer.from(await blob.arrayBuffer());
|
|
36561
|
-
const outputPath =
|
|
37151
|
+
const outputPath = resolve5(options.output);
|
|
36562
37152
|
await writeFile5(outputPath, buffer);
|
|
36563
37153
|
outputJson({
|
|
36564
37154
|
file: outputPath,
|
|
@@ -37114,7 +37704,7 @@ function registerUpgradeCommand(program) {
|
|
|
37114
37704
|
}
|
|
37115
37705
|
|
|
37116
37706
|
// src/commands/vault.ts
|
|
37117
|
-
import { readFile as
|
|
37707
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
37118
37708
|
init_profiles();
|
|
37119
37709
|
init_formatter();
|
|
37120
37710
|
init_errors();
|
|
@@ -37176,7 +37766,7 @@ function registerVaultCommand(program) {
|
|
|
37176
37766
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
37177
37767
|
}
|
|
37178
37768
|
if (options.file) {
|
|
37179
|
-
putValue = new Uint8Array(await
|
|
37769
|
+
putValue = new Uint8Array(await readFile11(options.file));
|
|
37180
37770
|
} else if (options.stdin) {
|
|
37181
37771
|
putValue = new Uint8Array(await readStdin4());
|
|
37182
37772
|
} else {
|
|
@@ -37291,7 +37881,7 @@ function registerVaultCommand(program) {
|
|
|
37291
37881
|
}
|
|
37292
37882
|
|
|
37293
37883
|
// src/commands/vars.ts
|
|
37294
|
-
import { readFile as
|
|
37884
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
37295
37885
|
init_profiles();
|
|
37296
37886
|
init_formatter();
|
|
37297
37887
|
init_errors();
|
|
@@ -37396,7 +37986,7 @@ function registerVarsCommand(program) {
|
|
|
37396
37986
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
37397
37987
|
}
|
|
37398
37988
|
if (options.file) {
|
|
37399
|
-
varValue = await
|
|
37989
|
+
varValue = await readFile12(options.file, "utf-8");
|
|
37400
37990
|
} else if (options.stdin) {
|
|
37401
37991
|
varValue = (await readStdin5()).toString("utf-8");
|
|
37402
37992
|
} else {
|