@tinycloud/cli 1.2.6-beta.4 → 1.3.0-beta.6
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 +1413 -696
- 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,78 @@ 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
|
+
});
|
|
19237
|
+
var writableStores = /* @__PURE__ */ new Map();
|
|
19238
|
+
function releaseWritable(shared) {
|
|
19239
|
+
if (shared.references === 0 || --shared.references !== 0) return;
|
|
19240
|
+
if (writableStores.get(shared.key) === shared) writableStores.delete(shared.key);
|
|
19241
|
+
try {
|
|
19242
|
+
shared.db.close();
|
|
19243
|
+
} finally {
|
|
19244
|
+
if (shared.directoryFd !== void 0) closeSync(shared.directoryFd);
|
|
19245
|
+
}
|
|
19246
|
+
}
|
|
19247
|
+
var sharedStores = new FinalizationRegistry((shared) => {
|
|
19248
|
+
try {
|
|
19249
|
+
releaseWritable(shared);
|
|
19250
|
+
} catch {
|
|
19251
|
+
}
|
|
19252
|
+
});
|
|
19253
|
+
async function acquireWritableMutation(shared) {
|
|
19254
|
+
const previous = shared.mutationTail;
|
|
19255
|
+
let release;
|
|
19256
|
+
shared.mutationTail = new Promise((resolve22) => {
|
|
19257
|
+
release = resolve22;
|
|
19258
|
+
});
|
|
19259
|
+
await previous;
|
|
19260
|
+
return release;
|
|
19261
|
+
}
|
|
19179
19262
|
var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
19180
19263
|
dir;
|
|
19181
|
-
#
|
|
19264
|
+
#database;
|
|
19265
|
+
get #db() {
|
|
19266
|
+
if (this.#closed) throw new ReplicaError2(ReplicaErrorCode2.CLOSED, "The replica store is closed.");
|
|
19267
|
+
return this.#database;
|
|
19268
|
+
}
|
|
19269
|
+
#closed = false;
|
|
19270
|
+
#closing = false;
|
|
19271
|
+
#closePromise;
|
|
19272
|
+
#activeMutations = 0;
|
|
19273
|
+
#drained;
|
|
19274
|
+
#shared;
|
|
19275
|
+
#pinnedDir;
|
|
19276
|
+
#directoryFd;
|
|
19182
19277
|
#holder = `${process.pid}-${randomBytes2(6).toString("hex")}`;
|
|
19183
19278
|
#now;
|
|
19184
19279
|
#faults;
|
|
19185
19280
|
#guard;
|
|
19186
19281
|
/** Identity of the database file this store opened: a deleted-and-recreated replica is a different file. */
|
|
19187
19282
|
#ino;
|
|
19188
|
-
|
|
19283
|
+
#dev;
|
|
19284
|
+
#namedAncestors;
|
|
19285
|
+
constructor(dir, db, identity, options, directoryFd, namedAncestors = [], shared) {
|
|
19189
19286
|
this.dir = dir;
|
|
19190
|
-
this.#
|
|
19191
|
-
this.#
|
|
19287
|
+
this.#pinnedDir = shared?.pinnedDir ?? (directoryFd !== void 0 && process.platform === "linux" ? `/proc/self/fd/${directoryFd}` : dir);
|
|
19288
|
+
this.#directoryFd = directoryFd;
|
|
19289
|
+
this.#shared = shared;
|
|
19290
|
+
if (shared) {
|
|
19291
|
+
shared.references++;
|
|
19292
|
+
sharedStores.register(this, shared, this);
|
|
19293
|
+
} else if (directoryFd !== void 0) {
|
|
19294
|
+
pinnedDirectories.register(this, directoryFd, this);
|
|
19295
|
+
}
|
|
19296
|
+
this.#database = db;
|
|
19297
|
+
this.#ino = identity.ino;
|
|
19298
|
+
this.#dev = identity.dev;
|
|
19192
19299
|
this.#now = options.now ?? Date.now;
|
|
19193
19300
|
this.#faults = options[FAULTS] ?? {};
|
|
19194
19301
|
this.#guard = options.guard ?? ((section) => section());
|
|
19302
|
+
this.#namedAncestors = namedAncestors;
|
|
19195
19303
|
}
|
|
19196
19304
|
/**
|
|
19197
19305
|
* Read-only status snapshot; does not create a replica, migrate, recover
|
|
@@ -19204,9 +19312,10 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19204
19312
|
};
|
|
19205
19313
|
checkAbort();
|
|
19206
19314
|
const dbPath = join7(dir, "replica.db");
|
|
19207
|
-
let
|
|
19315
|
+
let identity;
|
|
19208
19316
|
try {
|
|
19209
|
-
|
|
19317
|
+
identity = await lstat2(dbPath);
|
|
19318
|
+
if (!identity.isFile()) return null;
|
|
19210
19319
|
} catch (error) {
|
|
19211
19320
|
if (errnoOf(error) === "ENOENT") return null;
|
|
19212
19321
|
throw storageError(error, "Inspecting the replica");
|
|
@@ -19219,14 +19328,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19219
19328
|
db = opener(dbPath, { readonly: true });
|
|
19220
19329
|
db.exec("PRAGMA busy_timeout = 0");
|
|
19221
19330
|
checkAbort();
|
|
19222
|
-
const store = new _SqliteReplicaStore(dir, db,
|
|
19331
|
+
const store = new _SqliteReplicaStore(dir, db, identity, { create: false, ...options.now ? { now: options.now } : {} });
|
|
19223
19332
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
19224
19333
|
const before = db.get("PRAGMA data_version").data_version;
|
|
19225
19334
|
const state2 = await store.open();
|
|
19226
19335
|
checkAbort();
|
|
19227
19336
|
if (state2 === null) return null;
|
|
19228
19337
|
const status = await store.status();
|
|
19229
|
-
await new Promise((
|
|
19338
|
+
await new Promise((resolve22) => setImmediate(resolve22));
|
|
19230
19339
|
checkAbort();
|
|
19231
19340
|
const after = db.get("PRAGMA data_version").data_version;
|
|
19232
19341
|
if (before === after) return { state: state2, status };
|
|
@@ -19246,95 +19355,233 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19246
19355
|
* REPLICA_NOT_FOUND and nothing is written to disk.
|
|
19247
19356
|
*/
|
|
19248
19357
|
static async open(dir, options) {
|
|
19249
|
-
const
|
|
19358
|
+
const namedDbPath = join7(dir, "replica.db");
|
|
19250
19359
|
if (!options.create) {
|
|
19251
|
-
const exists = await
|
|
19252
|
-
() =>
|
|
19360
|
+
const exists = await lstat2(namedDbPath).then(
|
|
19361
|
+
(stats) => stats.isFile(),
|
|
19253
19362
|
() => false
|
|
19254
19363
|
);
|
|
19255
|
-
if (!exists) throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, `No replica at ${dir}.`);
|
|
19364
|
+
if (!exists) throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, `No regular replica database at ${dir}.`);
|
|
19256
19365
|
}
|
|
19257
|
-
const
|
|
19366
|
+
const releaseOpen = await acquireSqliteOpen();
|
|
19258
19367
|
let store;
|
|
19368
|
+
let directoryFd;
|
|
19259
19369
|
try {
|
|
19370
|
+
if (options.create) await mkdir2(dir, { recursive: true, mode: 448 });
|
|
19371
|
+
if (process.platform === "linux") {
|
|
19372
|
+
directoryFd = openSync(dir, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW);
|
|
19373
|
+
}
|
|
19374
|
+
const opened = directoryFd === void 0 ? lstatSync(dir, { bigint: true }) : fstatSync(directoryFd, { bigint: true });
|
|
19375
|
+
if (!opened.isDirectory() || opened.isSymbolicLink() || options.expectedDirectory && (opened.dev !== BigInt(options.expectedDirectory.dev) || opened.ino !== BigInt(options.expectedDirectory.ino))) {
|
|
19376
|
+
throw new Error("The replica directory changed before SQLite opened it.");
|
|
19377
|
+
}
|
|
19378
|
+
const namedAncestors = process.platform === "linux" ? [] : await captureNamedPath(dir);
|
|
19379
|
+
if (namedAncestors.length && (namedAncestors[namedAncestors.length - 1].dev !== opened.dev || namedAncestors[namedAncestors.length - 1].ino !== opened.ino)) {
|
|
19380
|
+
throw new Error("The replica directory changed while checking its ancestry.");
|
|
19381
|
+
}
|
|
19382
|
+
const pinnedDir = directoryFd === void 0 ? dir : `/proc/self/fd/${directoryFd}`;
|
|
19383
|
+
const dbPath = join7(pinnedDir, "replica.db");
|
|
19260
19384
|
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
|
-
);
|
|
19385
|
+
await mkdir2(join7(pinnedDir, "blobs", ".tmp"), { recursive: true, mode: 448 });
|
|
19386
|
+
await Promise.all([chmod3(pinnedDir, 448), chmod3(join7(pinnedDir, "blobs"), 448), chmod3(join7(pinnedDir, "blobs", ".tmp"), 448)]);
|
|
19387
|
+
const handle = await open2(dbPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_RDWR | fsConstants.O_NOFOLLOW, 384).catch((error) => {
|
|
19388
|
+
if (errnoOf(error) === "EEXIST") return null;
|
|
19389
|
+
throw error;
|
|
19390
|
+
});
|
|
19391
|
+
if (handle) {
|
|
19392
|
+
try {
|
|
19393
|
+
await handle.chmod(384);
|
|
19394
|
+
} finally {
|
|
19395
|
+
await handle.close();
|
|
19396
|
+
}
|
|
19397
|
+
}
|
|
19281
19398
|
}
|
|
19282
|
-
|
|
19283
|
-
|
|
19284
|
-
|
|
19285
|
-
|
|
19286
|
-
|
|
19399
|
+
const leaf = await open2(dbPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
19400
|
+
let db;
|
|
19401
|
+
try {
|
|
19402
|
+
const identity = await leaf.stat();
|
|
19403
|
+
const named = await lstat2(dbPath);
|
|
19404
|
+
if (!identity.isFile() || !named.isFile() || named.dev !== identity.dev || named.ino !== identity.ino) {
|
|
19405
|
+
throw new Error("The replica database changed before SQLite opened it.");
|
|
19406
|
+
}
|
|
19407
|
+
const key = `${opened.dev}:${opened.ino}`;
|
|
19408
|
+
const active = writableStores.get(key);
|
|
19409
|
+
if (active && (active.dev !== identity.dev || active.ino !== identity.ino)) {
|
|
19410
|
+
throw new Error("The replica database changed while another controller holds it open.");
|
|
19411
|
+
}
|
|
19412
|
+
if (active && !options.sqlite) {
|
|
19413
|
+
const namedDirectory = lstatSync(dir, { bigint: true });
|
|
19414
|
+
if (!namedDirectory.isDirectory() || namedDirectory.dev !== opened.dev || namedDirectory.ino !== opened.ino) {
|
|
19415
|
+
throw new Error("The replica directory changed before its checked SQLite handle could be shared.");
|
|
19416
|
+
}
|
|
19417
|
+
const pinned = active.directoryFd === void 0 ? lstatSync(active.pinnedDir, { bigint: true }) : fstatSync(active.directoryFd, { bigint: true });
|
|
19418
|
+
const activeLeaf = lstatSync(join7(active.pinnedDir, "replica.db"));
|
|
19419
|
+
if (!pinned.isDirectory() || pinned.dev !== opened.dev || pinned.ino !== opened.ino || !activeLeaf.isFile() || activeLeaf.dev !== identity.dev || activeLeaf.ino !== identity.ino || active.directoryFd === void 0 && active.namedPath !== dir) {
|
|
19420
|
+
throw new Error("The live replica changed before its checked SQLite handle could be shared.");
|
|
19421
|
+
}
|
|
19422
|
+
if (directoryFd !== void 0) {
|
|
19423
|
+
closeSync(directoryFd);
|
|
19424
|
+
directoryFd = void 0;
|
|
19425
|
+
}
|
|
19426
|
+
store = new _SqliteReplicaStore(dir, active.db, identity, options, active.directoryFd, namedAncestors, active);
|
|
19427
|
+
}
|
|
19428
|
+
if (!store) {
|
|
19429
|
+
const opener = options.sqlite ?? await loadSqlite();
|
|
19430
|
+
const previousFds = process.platform === "linux" && !options.sqlite ? new Map(readdirSync2("/proc/self/fd").map((fd) => {
|
|
19431
|
+
try {
|
|
19432
|
+
return [fd, readlinkSync(`/proc/self/fd/${fd}`)];
|
|
19433
|
+
} catch {
|
|
19434
|
+
return [fd, ""];
|
|
19435
|
+
}
|
|
19436
|
+
})) : null;
|
|
19437
|
+
db = opener(dbPath);
|
|
19438
|
+
const afterOpen = lstatSync(dbPath);
|
|
19439
|
+
if (!afterOpen.isFile() || afterOpen.dev !== identity.dev || afterOpen.ino !== identity.ino) {
|
|
19440
|
+
throw new Error("The replica database changed while SQLite opened it.");
|
|
19441
|
+
}
|
|
19442
|
+
if (previousFds) db.get("SELECT count(*) FROM sqlite_master");
|
|
19443
|
+
if (previousFds) {
|
|
19444
|
+
const databases = readdirSync2("/proc/self/fd").filter((fd) => {
|
|
19445
|
+
try {
|
|
19446
|
+
const target = readlinkSync(`/proc/self/fd/${fd}`);
|
|
19447
|
+
return target.endsWith("/replica.db") && previousFds.get(fd) !== target;
|
|
19448
|
+
} catch {
|
|
19449
|
+
return false;
|
|
19450
|
+
}
|
|
19451
|
+
});
|
|
19452
|
+
if (databases.length === 0 || databases.some((fd) => {
|
|
19453
|
+
try {
|
|
19454
|
+
const actual = fstatSync(Number(fd));
|
|
19455
|
+
return actual.dev !== identity.dev || actual.ino !== identity.ino;
|
|
19456
|
+
} catch {
|
|
19457
|
+
return true;
|
|
19458
|
+
}
|
|
19459
|
+
})) {
|
|
19460
|
+
throw new Error("SQLite opened a different replica database.");
|
|
19461
|
+
}
|
|
19462
|
+
}
|
|
19463
|
+
const after = lstatSync(dbPath);
|
|
19464
|
+
if (!after.isFile() || after.dev !== identity.dev || after.ino !== identity.ino) {
|
|
19465
|
+
throw new Error("The replica database changed while SQLite opened it.");
|
|
19466
|
+
}
|
|
19467
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
19468
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
19469
|
+
db.exec("PRAGMA synchronous = FULL");
|
|
19470
|
+
db.exec("PRAGMA secure_delete = ON");
|
|
19471
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
19472
|
+
const version = db.get("PRAGMA user_version")?.user_version ?? 0;
|
|
19473
|
+
if (version > SCHEMA_VERSION) {
|
|
19474
|
+
throw new ReplicaError2(
|
|
19475
|
+
ReplicaErrorCode2.STORAGE_ERROR,
|
|
19476
|
+
`The replica at ${dir} was written by a newer tc (schema ${version}); upgrade tc to read it.`
|
|
19477
|
+
);
|
|
19478
|
+
}
|
|
19479
|
+
if (version < SCHEMA_VERSION) {
|
|
19480
|
+
db.exec("BEGIN IMMEDIATE");
|
|
19481
|
+
db.exec(SCHEMA);
|
|
19482
|
+
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
19483
|
+
db.exec("COMMIT");
|
|
19484
|
+
}
|
|
19485
|
+
const shared = options.sqlite ? void 0 : {
|
|
19486
|
+
key,
|
|
19487
|
+
db,
|
|
19488
|
+
directoryFd,
|
|
19489
|
+
pinnedDir,
|
|
19490
|
+
namedPath: dir,
|
|
19491
|
+
dev: identity.dev,
|
|
19492
|
+
ino: identity.ino,
|
|
19493
|
+
references: 0,
|
|
19494
|
+
mutationTail: Promise.resolve()
|
|
19495
|
+
};
|
|
19496
|
+
store = new _SqliteReplicaStore(dir, db, identity, options, directoryFd, namedAncestors, shared);
|
|
19497
|
+
if (shared) writableStores.set(key, shared);
|
|
19498
|
+
db = void 0;
|
|
19499
|
+
}
|
|
19500
|
+
} finally {
|
|
19501
|
+
try {
|
|
19502
|
+
db?.close();
|
|
19503
|
+
} finally {
|
|
19504
|
+
await leaf.close();
|
|
19505
|
+
}
|
|
19287
19506
|
}
|
|
19288
|
-
store = new _SqliteReplicaStore(dir, db, ino, options);
|
|
19289
19507
|
} catch (error) {
|
|
19508
|
+
if (store) await store.close().catch(() => void 0);
|
|
19509
|
+
else if (directoryFd !== void 0) closeSync(directoryFd);
|
|
19290
19510
|
throw storageError(error, `Opening the replica at ${dir}`);
|
|
19511
|
+
} finally {
|
|
19512
|
+
releaseOpen();
|
|
19291
19513
|
}
|
|
19292
|
-
|
|
19293
|
-
|
|
19294
|
-
await store.#
|
|
19514
|
+
try {
|
|
19515
|
+
const row = store.#row();
|
|
19516
|
+
if (row?.revoked_detail != null && (row.purge_pending === 1 || await store.#holdsContentFiles().catch(() => true))) {
|
|
19517
|
+
await store.#finishRevocationPurge().catch(() => void 0);
|
|
19518
|
+
}
|
|
19519
|
+
return store;
|
|
19520
|
+
} catch (error) {
|
|
19521
|
+
await store.close().catch(() => void 0);
|
|
19522
|
+
throw error;
|
|
19295
19523
|
}
|
|
19296
|
-
return store;
|
|
19297
19524
|
}
|
|
19298
19525
|
#row() {
|
|
19299
19526
|
return this.#db.get("SELECT * FROM replica WHERE id = 1");
|
|
19300
19527
|
}
|
|
19301
19528
|
/** Refuse to touch the filesystem for a replica that was deleted (or replaced) under us. */
|
|
19302
19529
|
async #assertPresent() {
|
|
19303
|
-
const
|
|
19304
|
-
|
|
19305
|
-
() => void 0
|
|
19306
|
-
);
|
|
19307
|
-
if (ino !== this.#ino) {
|
|
19530
|
+
const identity = await lstat2(join7(this.#pinnedDir, "replica.db")).catch(() => null);
|
|
19531
|
+
if (!identity?.isFile() || identity.dev !== this.#dev || identity.ino !== this.#ino) {
|
|
19308
19532
|
throw new ReplicaError2(
|
|
19309
19533
|
ReplicaErrorCode2.NOT_FOUND,
|
|
19310
19534
|
`The replica at ${this.dir} was deleted while this command ran (its profile was deleted or the replica purged); nothing more was written.`
|
|
19311
19535
|
);
|
|
19312
19536
|
}
|
|
19313
19537
|
}
|
|
19538
|
+
/** New calls stop at close; accepted mutation sections may still finish their transactions. */
|
|
19539
|
+
#assertAccepting() {
|
|
19540
|
+
if (this.#closing || this.#closed) {
|
|
19541
|
+
throw new ReplicaError2(ReplicaErrorCode2.CLOSED, "The replica store is closed.");
|
|
19542
|
+
}
|
|
19543
|
+
}
|
|
19314
19544
|
/** A short mutation section: guarded (profile lock) and checked against the database this store opened. */
|
|
19315
19545
|
#mutate(section) {
|
|
19316
|
-
|
|
19317
|
-
|
|
19318
|
-
|
|
19319
|
-
|
|
19546
|
+
this.#assertAccepting();
|
|
19547
|
+
this.#activeMutations++;
|
|
19548
|
+
return (async () => {
|
|
19549
|
+
try {
|
|
19550
|
+
return await this.#guard(async () => {
|
|
19551
|
+
await this.#assertPresent();
|
|
19552
|
+
return section();
|
|
19553
|
+
});
|
|
19554
|
+
} finally {
|
|
19555
|
+
if (--this.#activeMutations === 0) {
|
|
19556
|
+
const drained = this.#drained;
|
|
19557
|
+
this.#drained = void 0;
|
|
19558
|
+
drained?.();
|
|
19559
|
+
}
|
|
19560
|
+
}
|
|
19561
|
+
})();
|
|
19320
19562
|
}
|
|
19321
19563
|
/** Run `body` in a write transaction (BEGIN IMMEDIATE). Callers outside a mutation section use `#guardedWrite`. */
|
|
19322
|
-
#write(action, body) {
|
|
19323
|
-
|
|
19324
|
-
this.#db.exec("BEGIN IMMEDIATE");
|
|
19325
|
-
} catch (error) {
|
|
19326
|
-
throw storageError(error, action);
|
|
19327
|
-
}
|
|
19564
|
+
async #write(action, body) {
|
|
19565
|
+
const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
|
|
19328
19566
|
try {
|
|
19329
|
-
const result = body();
|
|
19330
|
-
this.#db.exec("COMMIT");
|
|
19331
|
-
return result;
|
|
19332
|
-
} catch (error) {
|
|
19333
19567
|
try {
|
|
19334
|
-
this.#db.exec("
|
|
19335
|
-
} catch {
|
|
19568
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
19569
|
+
} catch (error) {
|
|
19570
|
+
throw storageError(error, action);
|
|
19571
|
+
}
|
|
19572
|
+
try {
|
|
19573
|
+
const result = body();
|
|
19574
|
+
this.#db.exec("COMMIT");
|
|
19575
|
+
return result;
|
|
19576
|
+
} catch (error) {
|
|
19577
|
+
try {
|
|
19578
|
+
this.#db.exec("ROLLBACK");
|
|
19579
|
+
} catch {
|
|
19580
|
+
}
|
|
19581
|
+
throw storageError(error, action);
|
|
19336
19582
|
}
|
|
19337
|
-
|
|
19583
|
+
} finally {
|
|
19584
|
+
release?.();
|
|
19338
19585
|
}
|
|
19339
19586
|
}
|
|
19340
19587
|
/** One write transaction as its own mutation section. */
|
|
@@ -19361,6 +19608,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19361
19608
|
}
|
|
19362
19609
|
}
|
|
19363
19610
|
async open() {
|
|
19611
|
+
this.#assertAccepting();
|
|
19364
19612
|
let row;
|
|
19365
19613
|
try {
|
|
19366
19614
|
row = this.#row();
|
|
@@ -19522,6 +19770,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19522
19770
|
});
|
|
19523
19771
|
}
|
|
19524
19772
|
async hasContent(hashes) {
|
|
19773
|
+
this.#assertAccepting();
|
|
19525
19774
|
const held = /* @__PURE__ */ new Set();
|
|
19526
19775
|
for (let start = 0; start < hashes.length; start += 500) {
|
|
19527
19776
|
const chunk = hashes.slice(start, start + 500);
|
|
@@ -19534,7 +19783,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19534
19783
|
return held;
|
|
19535
19784
|
}
|
|
19536
19785
|
#blobPath(hash) {
|
|
19537
|
-
return join7(this
|
|
19786
|
+
return join7(this.#pinnedDir, "blobs", hash.slice(0, 2), hash);
|
|
19538
19787
|
}
|
|
19539
19788
|
/**
|
|
19540
19789
|
* Make every blob durable before any row references it: write to a fresh
|
|
@@ -19551,11 +19800,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19551
19800
|
() => false
|
|
19552
19801
|
);
|
|
19553
19802
|
if (exists) continue;
|
|
19554
|
-
const shard = join7(this
|
|
19803
|
+
const shard = join7(this.#pinnedDir, "blobs", hash.slice(0, 2));
|
|
19555
19804
|
await mkdir2(shard, { mode: 448 }).catch((error) => {
|
|
19556
19805
|
if (errnoOf(error) !== "EEXIST") throw error;
|
|
19557
19806
|
});
|
|
19558
|
-
const temp = join7(this
|
|
19807
|
+
const temp = join7(this.#pinnedDir, "blobs", ".tmp", `${hash}.${randomBytes2(6).toString("hex")}`);
|
|
19559
19808
|
const handle = await open2(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 384);
|
|
19560
19809
|
try {
|
|
19561
19810
|
await handle.writeFile(bytes);
|
|
@@ -19571,7 +19820,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19571
19820
|
await unlink(temp);
|
|
19572
19821
|
}
|
|
19573
19822
|
dirs.add(shard);
|
|
19574
|
-
dirs.add(join7(this
|
|
19823
|
+
dirs.add(join7(this.#pinnedDir, "blobs"));
|
|
19575
19824
|
}
|
|
19576
19825
|
for (const dir of dirs) {
|
|
19577
19826
|
const handle = await open2(dir, fsConstants.O_RDONLY);
|
|
@@ -19604,7 +19853,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19604
19853
|
}
|
|
19605
19854
|
await this.#faults.afterBlobs?.();
|
|
19606
19855
|
try {
|
|
19607
|
-
this.#write("Committing a sync page", () => {
|
|
19856
|
+
await this.#write("Committing a sync page", () => {
|
|
19608
19857
|
this.#checkLease(t);
|
|
19609
19858
|
const now = this.#now();
|
|
19610
19859
|
if (p.window.notBefore !== null && now < Date.parse(p.window.notBefore)) {
|
|
@@ -19679,7 +19928,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19679
19928
|
/** Unlink every blob not in `keep`, and every temp file. Only a missing directory counts as empty. */
|
|
19680
19929
|
async #unlinkBlobs(keep) {
|
|
19681
19930
|
let removed = 0;
|
|
19682
|
-
const root = join7(this
|
|
19931
|
+
const root = join7(this.#pinnedDir, "blobs");
|
|
19683
19932
|
for (const shard of await entriesOf(root)) {
|
|
19684
19933
|
const shardPath = join7(root, shard);
|
|
19685
19934
|
if (shard === ".tmp") {
|
|
@@ -19698,13 +19947,13 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19698
19947
|
await rm3(join7(shardPath, name), { force: true });
|
|
19699
19948
|
removed += 1;
|
|
19700
19949
|
}
|
|
19701
|
-
if (kept === 0) await
|
|
19950
|
+
if (kept === 0) await rmdir(shardPath).catch(() => void 0);
|
|
19702
19951
|
}
|
|
19703
19952
|
return removed;
|
|
19704
19953
|
}
|
|
19705
19954
|
/** Whether any content file (a blob or a temp file) is on disk. */
|
|
19706
19955
|
async #holdsContentFiles() {
|
|
19707
|
-
const root = join7(this
|
|
19956
|
+
const root = join7(this.#pinnedDir, "blobs");
|
|
19708
19957
|
for (const shard of await entriesOf(root)) {
|
|
19709
19958
|
if ((await entriesOf(join7(root, shard))).length > 0) return true;
|
|
19710
19959
|
}
|
|
@@ -19712,13 +19961,16 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19712
19961
|
}
|
|
19713
19962
|
async collectGarbage(t) {
|
|
19714
19963
|
return this.#mutate(async () => {
|
|
19964
|
+
const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
|
|
19715
19965
|
try {
|
|
19716
19966
|
this.#db.exec("BEGIN IMMEDIATE");
|
|
19717
19967
|
} catch (error) {
|
|
19968
|
+
release?.();
|
|
19718
19969
|
throw storageError(error, "Collecting unused content");
|
|
19719
19970
|
}
|
|
19720
19971
|
try {
|
|
19721
19972
|
this.#checkLease(t);
|
|
19973
|
+
await this.#faults.afterGarbageCollectionBegin?.();
|
|
19722
19974
|
const keep = new Set(
|
|
19723
19975
|
this.#db.all("SELECT DISTINCT hash FROM entry WHERE content = 1").map((row) => row.hash)
|
|
19724
19976
|
);
|
|
@@ -19731,12 +19983,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19731
19983
|
} catch {
|
|
19732
19984
|
}
|
|
19733
19985
|
throw storageError(error, "Collecting unused content");
|
|
19986
|
+
} finally {
|
|
19987
|
+
release?.();
|
|
19734
19988
|
}
|
|
19735
19989
|
});
|
|
19736
19990
|
}
|
|
19737
19991
|
async reset(t, reason, options = {}) {
|
|
19738
19992
|
await this.#mutate(async () => {
|
|
19739
|
-
this.#write("Resetting the replica", () => {
|
|
19993
|
+
await this.#write("Resetting the replica", () => {
|
|
19740
19994
|
this.#checkLease(t);
|
|
19741
19995
|
this.#db.run("DELETE FROM entry");
|
|
19742
19996
|
this.#db.run(
|
|
@@ -19763,15 +20017,102 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19763
20017
|
*/
|
|
19764
20018
|
async destroy(t) {
|
|
19765
20019
|
await this.#mutate(async () => {
|
|
20020
|
+
const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
|
|
19766
20021
|
try {
|
|
19767
20022
|
this.#db.exec("BEGIN IMMEDIATE");
|
|
19768
20023
|
} catch (error) {
|
|
20024
|
+
release?.();
|
|
19769
20025
|
throw storageError(error, "Removing the replica");
|
|
19770
20026
|
}
|
|
19771
20027
|
try {
|
|
19772
20028
|
this.#checkLease(t, { allowRevoked: true });
|
|
20029
|
+
const expected = this.#directoryFd === void 0 ? this.#namedAncestors.at(-1) : fstatSync(this.#directoryFd, { bigint: true });
|
|
20030
|
+
if (!expected) {
|
|
20031
|
+
throw new ReplicaError2(ReplicaErrorCode2.RUNTIME_UNSUPPORTED, "Replica removal requires an opened or identity-checked directory.");
|
|
20032
|
+
}
|
|
20033
|
+
const assertDirectory2 = async () => {
|
|
20034
|
+
const current = await lstat2(this.dir, { bigint: true });
|
|
20035
|
+
if (!current.isDirectory() || current.dev !== expected.dev || current.ino !== expected.ino) {
|
|
20036
|
+
throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica directory changed during removal.");
|
|
20037
|
+
}
|
|
20038
|
+
return current;
|
|
20039
|
+
};
|
|
20040
|
+
const pinned = await assertDirectory2();
|
|
19773
20041
|
await this.#assertPresent();
|
|
19774
|
-
|
|
20042
|
+
if (process.platform === "linux") {
|
|
20043
|
+
for (const entry of await readdir3(this.#pinnedDir)) {
|
|
20044
|
+
await rm3(join7(this.#pinnedDir, entry), { recursive: true, force: true });
|
|
20045
|
+
}
|
|
20046
|
+
await assertDirectory2();
|
|
20047
|
+
await rmdir(this.dir);
|
|
20048
|
+
} else {
|
|
20049
|
+
const ancestors = this.#namedAncestors;
|
|
20050
|
+
const checkAncestors = async () => {
|
|
20051
|
+
for (const expected2 of ancestors) {
|
|
20052
|
+
const current = await lstat2(expected2.path, { bigint: true });
|
|
20053
|
+
if (current.dev !== expected2.dev || current.ino !== expected2.ino) {
|
|
20054
|
+
throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica ancestry changed during removal.");
|
|
20055
|
+
}
|
|
20056
|
+
}
|
|
20057
|
+
};
|
|
20058
|
+
const directories = /* @__PURE__ */ new Map([[this.dir, pinned]]);
|
|
20059
|
+
const files = /* @__PURE__ */ new Map();
|
|
20060
|
+
const check = async (path, expected2, directory) => {
|
|
20061
|
+
if (path !== this.dir) {
|
|
20062
|
+
const parent = dirname3(path);
|
|
20063
|
+
await check(parent, directories.get(parent), true);
|
|
20064
|
+
}
|
|
20065
|
+
await checkAncestors();
|
|
20066
|
+
const current = await lstat2(path, { bigint: true });
|
|
20067
|
+
if ((directory ? !current.isDirectory() : !current.isFile()) || current.dev !== expected2.dev || current.ino !== expected2.ino) {
|
|
20068
|
+
throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica layout changed during removal.");
|
|
20069
|
+
}
|
|
20070
|
+
};
|
|
20071
|
+
const record2 = async (path, directory) => {
|
|
20072
|
+
const parent = dirname3(path);
|
|
20073
|
+
await check(parent, directories.get(parent), true);
|
|
20074
|
+
const current = await lstat2(path, { bigint: true });
|
|
20075
|
+
if (!(directory ? current.isDirectory() : current.isFile())) {
|
|
20076
|
+
throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica layout during removal.");
|
|
20077
|
+
}
|
|
20078
|
+
(directory ? directories : files).set(path, current);
|
|
20079
|
+
};
|
|
20080
|
+
const blobs = join7(this.dir, "blobs");
|
|
20081
|
+
for (const name of await readdir3(this.dir)) {
|
|
20082
|
+
if (name === "blobs") await record2(blobs, true);
|
|
20083
|
+
else if (["replica.db", "replica.db-wal", "replica.db-shm"].includes(name)) await record2(join7(this.dir, name), false);
|
|
20084
|
+
else throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica layout during removal.");
|
|
20085
|
+
}
|
|
20086
|
+
if (directories.has(blobs)) {
|
|
20087
|
+
await check(blobs, directories.get(blobs), true);
|
|
20088
|
+
for (const shard of await readdir3(blobs)) {
|
|
20089
|
+
if (shard !== ".tmp" && !/^[0-9a-f]{2}$/.test(shard)) {
|
|
20090
|
+
throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica blob shard during removal.");
|
|
20091
|
+
}
|
|
20092
|
+
const shardPath = join7(blobs, shard);
|
|
20093
|
+
await record2(shardPath, true);
|
|
20094
|
+
await check(shardPath, directories.get(shardPath), true);
|
|
20095
|
+
for (const name of await readdir3(shardPath)) {
|
|
20096
|
+
if (shard === ".tmp" ? !/^[0-9a-f]{64}\.[0-9a-f]{12}$/.test(name) : !isHash(name) || !name.startsWith(shard)) {
|
|
20097
|
+
throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica blob file during removal.");
|
|
20098
|
+
}
|
|
20099
|
+
await record2(join7(shardPath, name), false);
|
|
20100
|
+
}
|
|
20101
|
+
}
|
|
20102
|
+
}
|
|
20103
|
+
for (const [path, identity] of files) {
|
|
20104
|
+
await check(path, identity, false);
|
|
20105
|
+
await unlink(path);
|
|
20106
|
+
}
|
|
20107
|
+
for (const [path, identity] of [...directories].reverse()) {
|
|
20108
|
+
if (path === this.dir) continue;
|
|
20109
|
+
await check(path, identity, true);
|
|
20110
|
+
await rmdir(path);
|
|
20111
|
+
}
|
|
20112
|
+
await assertDirectory2();
|
|
20113
|
+
await checkAncestors();
|
|
20114
|
+
await rmdir(this.dir);
|
|
20115
|
+
}
|
|
19775
20116
|
} catch (error) {
|
|
19776
20117
|
throw storageError(error, "Removing the replica");
|
|
19777
20118
|
} finally {
|
|
@@ -19779,6 +20120,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19779
20120
|
this.#db.exec("ROLLBACK");
|
|
19780
20121
|
} catch {
|
|
19781
20122
|
}
|
|
20123
|
+
release?.();
|
|
19782
20124
|
}
|
|
19783
20125
|
});
|
|
19784
20126
|
}
|
|
@@ -19822,7 +20164,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19822
20164
|
* when both are done.
|
|
19823
20165
|
*/
|
|
19824
20166
|
async #purgeContent() {
|
|
19825
|
-
this.#write("Starting the revocation purge", () => {
|
|
20167
|
+
await this.#write("Starting the revocation purge", () => {
|
|
19826
20168
|
this.#db.run("UPDATE replica SET purge_pending = 1 WHERE id = 1 AND revoked_detail IS NOT NULL");
|
|
19827
20169
|
});
|
|
19828
20170
|
try {
|
|
@@ -19833,7 +20175,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19833
20175
|
if (!this.#checkpoint()) {
|
|
19834
20176
|
throw new ReplicaError2(ReplicaErrorCode2.BUSY, "A reader holds the replica's write-ahead log; the purge finishes on the next open.");
|
|
19835
20177
|
}
|
|
19836
|
-
this.#write("Finishing the revocation purge", () => {
|
|
20178
|
+
await this.#write("Finishing the revocation purge", () => {
|
|
19837
20179
|
this.#db.run("UPDATE replica SET purge_pending = 0 WHERE id = 1 AND revoked_detail IS NOT NULL");
|
|
19838
20180
|
});
|
|
19839
20181
|
}
|
|
@@ -19843,6 +20185,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19843
20185
|
});
|
|
19844
20186
|
}
|
|
19845
20187
|
async get(key) {
|
|
20188
|
+
this.#assertAccepting();
|
|
19846
20189
|
try {
|
|
19847
20190
|
const row = this.#db.get("SELECT key, deleted, etag, hash, metadata, content FROM entry WHERE key = ?", key);
|
|
19848
20191
|
return row === void 0 ? void 0 : entryOf(row);
|
|
@@ -19851,6 +20194,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19851
20194
|
}
|
|
19852
20195
|
}
|
|
19853
20196
|
async list(o) {
|
|
20197
|
+
this.#assertAccepting();
|
|
19854
20198
|
const where = ["deleted = 0"];
|
|
19855
20199
|
const params = [];
|
|
19856
20200
|
if (o.prefix !== void 0 && o.prefix !== "") {
|
|
@@ -19936,8 +20280,31 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19936
20280
|
lastReset: state2.lastReset
|
|
19937
20281
|
};
|
|
19938
20282
|
}
|
|
19939
|
-
|
|
19940
|
-
this.#
|
|
20283
|
+
close() {
|
|
20284
|
+
if (this.#closePromise) return this.#closePromise;
|
|
20285
|
+
this.#closing = true;
|
|
20286
|
+
this.#closePromise = (async () => {
|
|
20287
|
+
if (this.#activeMutations !== 0) {
|
|
20288
|
+
await new Promise((resolve22) => {
|
|
20289
|
+
this.#drained = resolve22;
|
|
20290
|
+
});
|
|
20291
|
+
}
|
|
20292
|
+
this.#closed = true;
|
|
20293
|
+
const fd = this.#directoryFd;
|
|
20294
|
+
this.#directoryFd = void 0;
|
|
20295
|
+
if (this.#shared) {
|
|
20296
|
+
sharedStores.unregister(this);
|
|
20297
|
+
releaseWritable(this.#shared);
|
|
20298
|
+
} else {
|
|
20299
|
+
if (fd !== void 0) pinnedDirectories.unregister(this);
|
|
20300
|
+
try {
|
|
20301
|
+
this.#database.close();
|
|
20302
|
+
} finally {
|
|
20303
|
+
if (fd !== void 0) closeSync(fd);
|
|
20304
|
+
}
|
|
20305
|
+
}
|
|
20306
|
+
})();
|
|
20307
|
+
return this.#closePromise;
|
|
19941
20308
|
}
|
|
19942
20309
|
};
|
|
19943
20310
|
|
|
@@ -19945,229 +20312,388 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19945
20312
|
init_errors();
|
|
19946
20313
|
init_constants();
|
|
19947
20314
|
var SYNC_LEASE_MS = 6e4;
|
|
19948
|
-
function refuseUnsafeReplicaPath(profile, detail) {
|
|
20315
|
+
function refuseUnsafeReplicaPath(profile, detail, operation = "logout") {
|
|
19949
20316
|
return new CLIError(
|
|
19950
20317
|
"REPLICA_PURGE_FAILED",
|
|
19951
|
-
`Logout for profile "${profile}" cleared the session, but ${detail}; the replicas were left untouched.`,
|
|
20318
|
+
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
20319
|
ExitCode.ERROR
|
|
19953
20320
|
);
|
|
19954
20321
|
}
|
|
19955
|
-
async function safeReplicaRoot(profile, root) {
|
|
20322
|
+
async function safeReplicaRoot(profile, root, operation = "logout") {
|
|
19956
20323
|
let rootStats;
|
|
19957
20324
|
try {
|
|
19958
|
-
rootStats = await
|
|
20325
|
+
rootStats = root.startsWith("/proc/self/fd/") ? await stat3(root) : await lstat3(root);
|
|
19959
20326
|
} catch (error) {
|
|
19960
20327
|
if (error?.code === "ENOENT") return null;
|
|
19961
20328
|
throw error;
|
|
19962
20329
|
}
|
|
19963
20330
|
if (rootStats.isSymbolicLink()) {
|
|
19964
|
-
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" is a symlink
|
|
20331
|
+
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" is a symlink`, operation);
|
|
19965
20332
|
}
|
|
19966
20333
|
if (!rootStats.isDirectory()) {
|
|
19967
|
-
throw refuseUnsafeReplicaPath(profile, `the replicas path "${root}" is not a real directory
|
|
20334
|
+
throw refuseUnsafeReplicaPath(profile, `the replicas path "${root}" is not a real directory`, operation);
|
|
19968
20335
|
}
|
|
19969
|
-
const [profileRealPath, rootRealPath] = await Promise.all([realpath(
|
|
20336
|
+
const [profileRealPath, rootRealPath] = await Promise.all([realpath(profilePath4(profile)), realpath(root)]);
|
|
19970
20337
|
if (!rootRealPath.startsWith(profileRealPath + sep)) {
|
|
19971
|
-
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" resolves outside the selected profile
|
|
20338
|
+
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" resolves outside the selected profile`, operation);
|
|
19972
20339
|
}
|
|
19973
20340
|
return rootRealPath;
|
|
19974
20341
|
}
|
|
19975
|
-
async function safeReplicaEntry(profile, root, entry) {
|
|
20342
|
+
async function safeReplicaEntry(profile, root, entry, operation = "logout") {
|
|
19976
20343
|
const entryPath = join8(root, entry.name);
|
|
19977
|
-
const rootRealPath = await safeReplicaRoot(profile, root);
|
|
20344
|
+
const rootRealPath = await safeReplicaRoot(profile, root, operation);
|
|
19978
20345
|
if (rootRealPath === null) {
|
|
19979
|
-
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" disappeared
|
|
20346
|
+
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" disappeared`, operation);
|
|
19980
20347
|
}
|
|
19981
|
-
const entryStats = await
|
|
20348
|
+
const entryStats = await lstat3(entryPath);
|
|
19982
20349
|
if (entry.isSymbolicLink() || entryStats.isSymbolicLink()) {
|
|
19983
|
-
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is a symlink
|
|
20350
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is a symlink`, operation);
|
|
19984
20351
|
}
|
|
19985
20352
|
if (!entry.isDirectory() || !entryStats.isDirectory()) {
|
|
19986
|
-
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is not a real directory
|
|
20353
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is not a real directory`, operation);
|
|
19987
20354
|
}
|
|
19988
20355
|
const entryRealPath = await realpath(entryPath);
|
|
19989
20356
|
if (!entryRealPath.startsWith(rootRealPath + sep)) {
|
|
19990
|
-
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" resolves outside the replicas directory
|
|
20357
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" resolves outside the replicas directory`, operation);
|
|
19991
20358
|
}
|
|
19992
|
-
return entryPath;
|
|
20359
|
+
return { path: entryPath, identity: { dev: entryStats.dev, ino: entryStats.ino } };
|
|
19993
20360
|
}
|
|
19994
|
-
async function
|
|
20361
|
+
async function inspectLegacyReplica(profile, root, entry) {
|
|
20362
|
+
const { path, identity } = await safeReplicaEntry(profile, root, entry);
|
|
20363
|
+
const handle = await open3(path, fsConstants2.O_RDONLY | fsConstants2.O_DIRECTORY | fsConstants2.O_NOFOLLOW);
|
|
20364
|
+
try {
|
|
20365
|
+
const pinnedEntry = await handle.stat();
|
|
20366
|
+
if (pinnedEntry.dev !== identity.dev || pinnedEntry.ino !== identity.ino) {
|
|
20367
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" changed during inspection`);
|
|
20368
|
+
}
|
|
20369
|
+
const children = await readdir4(`/proc/self/fd/${handle.fd}`, { withFileTypes: true });
|
|
20370
|
+
if (children.length > 0 && !children.some((child) => child.name === "replica.db" && child.isFile())) {
|
|
20371
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" has files but no regular replica database`);
|
|
20372
|
+
}
|
|
20373
|
+
return { path, identity, empty: children.length === 0 };
|
|
20374
|
+
} finally {
|
|
20375
|
+
await handle.close();
|
|
20376
|
+
}
|
|
20377
|
+
}
|
|
20378
|
+
async function pinReplicaRoot(profile, named, operation) {
|
|
20379
|
+
const initial = await lstat3(named).catch((error) => {
|
|
20380
|
+
if (error.code === "ENOENT") return null;
|
|
20381
|
+
throw error;
|
|
20382
|
+
});
|
|
20383
|
+
if (await safeReplicaRoot(profile, named, operation) === null) return null;
|
|
20384
|
+
if (process.platform !== "linux") {
|
|
20385
|
+
throw refuseUnsafeReplicaPath(profile, "pinned replica removal is unavailable on this platform", operation);
|
|
20386
|
+
}
|
|
20387
|
+
const handle = await open3(named, fsConstants2.O_RDONLY | fsConstants2.O_DIRECTORY | fsConstants2.O_NOFOLLOW);
|
|
20388
|
+
try {
|
|
20389
|
+
const pinned = await handle.stat();
|
|
20390
|
+
const assertNamed = async () => {
|
|
20391
|
+
const current = await lstat3(named);
|
|
20392
|
+
if (!current.isDirectory() || current.dev !== initial?.dev || current.ino !== initial?.ino || current.dev !== pinned.dev || current.ino !== pinned.ino) {
|
|
20393
|
+
throw refuseUnsafeReplicaPath(profile, "the replicas directory changed during cleanup", operation);
|
|
20394
|
+
}
|
|
20395
|
+
};
|
|
20396
|
+
await assertNamed();
|
|
20397
|
+
return { path: `/proc/self/fd/${handle.fd}`, assertNamed, close: () => handle.close() };
|
|
20398
|
+
} catch (error) {
|
|
20399
|
+
await handle.close();
|
|
20400
|
+
throw error;
|
|
20401
|
+
}
|
|
20402
|
+
}
|
|
20403
|
+
async function withPinnedReplicaForReset(profile, name, action) {
|
|
19995
20404
|
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 = [];
|
|
20405
|
+
const root = join8(profilePath4(profile), "replicas");
|
|
20406
|
+
const pinned = await pinReplicaRoot(profile, root, "reset");
|
|
20407
|
+
if (!pinned) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas.`);
|
|
20003
20408
|
try {
|
|
20004
|
-
|
|
20005
|
-
|
|
20006
|
-
|
|
20007
|
-
|
|
20008
|
-
|
|
20009
|
-
|
|
20010
|
-
|
|
20011
|
-
|
|
20012
|
-
|
|
20013
|
-
|
|
20409
|
+
const fdRoot = pinned.path;
|
|
20410
|
+
const entry = (await readdir4(fdRoot, { withFileTypes: true })).find((item) => item.name === name);
|
|
20411
|
+
if (!entry) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Replica "${name}" was not found.`);
|
|
20412
|
+
const { path, identity: initial } = await safeReplicaEntry(profile, fdRoot, entry, "reset");
|
|
20413
|
+
const recheck = async () => {
|
|
20414
|
+
const current = await lstat3(path);
|
|
20415
|
+
if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== initial.dev || current.ino !== initial.ino) {
|
|
20416
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${name}" changed during removal`, "reset");
|
|
20417
|
+
}
|
|
20418
|
+
};
|
|
20419
|
+
await recheck();
|
|
20420
|
+
return await action(path, recheck, { dev: initial.dev, ino: initial.ino });
|
|
20421
|
+
} finally {
|
|
20422
|
+
await pinned.close();
|
|
20423
|
+
}
|
|
20424
|
+
});
|
|
20425
|
+
}
|
|
20426
|
+
async function removeProfileReplicas(profile, heldLeases, preflightRoot) {
|
|
20427
|
+
return withProfileLock4(profile, async () => {
|
|
20428
|
+
const namedRoot = join8(profilePath4(profile), "replicas");
|
|
20429
|
+
const pinned = preflightRoot ?? await pinReplicaRoot(profile, namedRoot, "logout");
|
|
20430
|
+
if (!pinned) return [];
|
|
20431
|
+
if (preflightRoot) await pinned.assertNamed();
|
|
20432
|
+
try {
|
|
20433
|
+
const root = pinned.path;
|
|
20434
|
+
const replicas = (await readdir4(root, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
20435
|
+
if (replicas.length === 0) return [];
|
|
20436
|
+
const checked = await Promise.all(replicas.map(async (entry) => ({
|
|
20437
|
+
entry,
|
|
20438
|
+
...await inspectLegacyReplica(profile, root, entry)
|
|
20439
|
+
})));
|
|
20440
|
+
const removed = [];
|
|
20441
|
+
const opened = [];
|
|
20442
|
+
try {
|
|
20443
|
+
for (const { entry, identity, empty: empty4 } of checked) {
|
|
20444
|
+
if (!empty4) continue;
|
|
20445
|
+
await pinned.assertNamed();
|
|
20446
|
+
const current = await safeReplicaEntry(profile, root, entry);
|
|
20447
|
+
if (current.identity.dev !== identity.dev || current.identity.ino !== identity.ino) {
|
|
20448
|
+
throw refuseUnsafeReplicaPath(profile, `empty replica entry "${entry.name}" changed during cleanup`);
|
|
20449
|
+
}
|
|
20450
|
+
const handle = await open3(current.path, fsConstants2.O_RDONLY | fsConstants2.O_DIRECTORY | fsConstants2.O_NOFOLLOW);
|
|
20451
|
+
try {
|
|
20452
|
+
const pinnedEntry = await handle.stat();
|
|
20453
|
+
if (pinnedEntry.dev !== identity.dev || pinnedEntry.ino !== identity.ino || (await readdir4(`/proc/self/fd/${handle.fd}`)).length !== 0) {
|
|
20454
|
+
throw refuseUnsafeReplicaPath(profile, `empty replica entry "${entry.name}" changed during cleanup`);
|
|
20455
|
+
}
|
|
20456
|
+
await pinned.assertNamed();
|
|
20457
|
+
const namedEntry = await lstat3(current.path);
|
|
20458
|
+
if (!namedEntry.isDirectory() || namedEntry.dev !== pinnedEntry.dev || namedEntry.ino !== pinnedEntry.ino) {
|
|
20459
|
+
throw refuseUnsafeReplicaPath(profile, `empty replica entry "${entry.name}" changed at removal`);
|
|
20460
|
+
}
|
|
20461
|
+
await rmdir2(current.path);
|
|
20462
|
+
} finally {
|
|
20463
|
+
await handle.close();
|
|
20464
|
+
}
|
|
20465
|
+
removed.push(entry.name);
|
|
20466
|
+
}
|
|
20467
|
+
if (removed.length === replicas.length) return removed;
|
|
20468
|
+
await loadSqlite();
|
|
20469
|
+
for (const { entry, empty: empty4 } of checked) {
|
|
20470
|
+
if (empty4) continue;
|
|
20471
|
+
const { path: entryPath, identity: expectedDirectory } = await safeReplicaEntry(profile, root, entry);
|
|
20472
|
+
const store = await SqliteReplicaStore.open(entryPath, {
|
|
20473
|
+
create: false,
|
|
20474
|
+
expectedDirectory,
|
|
20475
|
+
guard: async (action) => {
|
|
20476
|
+
try {
|
|
20477
|
+
return await withProfileLock4(profile, async () => {
|
|
20478
|
+
await safeReplicaEntry(profile, root, entry);
|
|
20479
|
+
return action();
|
|
20480
|
+
}, { requireProfile: true });
|
|
20481
|
+
} catch (error) {
|
|
20482
|
+
if (error instanceof ProfileDeletedError2) {
|
|
20483
|
+
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" was deleted; its replicas are gone and nothing was written.`);
|
|
20484
|
+
}
|
|
20485
|
+
throw error;
|
|
20014
20486
|
}
|
|
20015
|
-
throw error;
|
|
20016
20487
|
}
|
|
20488
|
+
});
|
|
20489
|
+
opened.push({ name: entry.name, store, lease: null });
|
|
20490
|
+
if (await store.open() === null) {
|
|
20491
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
|
|
20017
20492
|
}
|
|
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);
|
|
20493
|
+
const lease = heldLeases?.get(join8(namedRoot, entry.name)) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20494
|
+
if (lease === null) {
|
|
20495
|
+
throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
20496
|
+
}
|
|
20497
|
+
opened[opened.length - 1].lease = lease;
|
|
20026
20498
|
}
|
|
20027
|
-
|
|
20028
|
-
|
|
20029
|
-
|
|
20030
|
-
|
|
20031
|
-
|
|
20032
|
-
|
|
20033
|
-
|
|
20034
|
-
|
|
20035
|
-
|
|
20036
|
-
()
|
|
20037
|
-
(
|
|
20038
|
-
|
|
20039
|
-
|
|
20040
|
-
|
|
20041
|
-
|
|
20042
|
-
|
|
20043
|
-
|
|
20044
|
-
|
|
20045
|
-
{ replicasRemoved: removed, replicasRemaining: remaining }
|
|
20046
|
-
);
|
|
20499
|
+
for (const replica of opened) {
|
|
20500
|
+
try {
|
|
20501
|
+
await replica.store.destroy(replica.lease);
|
|
20502
|
+
removed.push(replica.name);
|
|
20503
|
+
} catch (error) {
|
|
20504
|
+
const replicaStillExists = await lstat3(join8(root, replica.name)).then(
|
|
20505
|
+
() => true,
|
|
20506
|
+
(statError) => statError.code !== "ENOENT"
|
|
20507
|
+
);
|
|
20508
|
+
if (!replicaStillExists) removed.push(replica.name);
|
|
20509
|
+
const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
|
|
20510
|
+
throw new CLIError(
|
|
20511
|
+
"REPLICA_PURGE_FAILED",
|
|
20512
|
+
`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)}`,
|
|
20513
|
+
ExitCode.ERROR,
|
|
20514
|
+
{ replicasRemoved: removed, replicasRemaining: remaining }
|
|
20515
|
+
);
|
|
20516
|
+
}
|
|
20047
20517
|
}
|
|
20518
|
+
return removed;
|
|
20519
|
+
} catch (error) {
|
|
20520
|
+
if (error instanceof CLIError) throw error;
|
|
20521
|
+
throw new CLIError(
|
|
20522
|
+
"REPLICA_PURGE_FAILED",
|
|
20523
|
+
`Logout cleared the session, but replica cleanup could not finish. Removed: ${removed.length ? removed.join(", ") : "none"}. ${error instanceof Error ? error.message : String(error)}`,
|
|
20524
|
+
ExitCode.ERROR
|
|
20525
|
+
);
|
|
20526
|
+
} finally {
|
|
20527
|
+
await Promise.all(opened.map(async ({ name, store, lease }) => {
|
|
20528
|
+
if (lease !== null && heldLeases?.has(join8(namedRoot, name)) !== true) await store.releaseLease(lease).catch(() => void 0);
|
|
20529
|
+
await store.close().catch(() => void 0);
|
|
20530
|
+
}));
|
|
20048
20531
|
}
|
|
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
20532
|
} 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
|
-
}));
|
|
20533
|
+
if (!preflightRoot) await pinned.close();
|
|
20062
20534
|
}
|
|
20063
20535
|
});
|
|
20064
20536
|
}
|
|
20065
|
-
async function
|
|
20537
|
+
async function assertSafeFlagReplica(profile, root, partition, name) {
|
|
20538
|
+
const rootReal = await safeReplicaRoot(profile, root);
|
|
20539
|
+
if (rootReal === null) throw refuseUnsafeReplicaPath(profile, `the replication root "${root}" disappeared`);
|
|
20540
|
+
const partitionPath = join8(root, partition);
|
|
20541
|
+
const partitionStats = await lstat3(partitionPath);
|
|
20542
|
+
if (partitionStats.isSymbolicLink() || !partitionStats.isDirectory()) {
|
|
20543
|
+
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition}" is not a real directory`);
|
|
20544
|
+
}
|
|
20545
|
+
const partitionReal = await realpath(partitionPath);
|
|
20546
|
+
if (!partitionReal.startsWith(rootReal + sep)) {
|
|
20547
|
+
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition}" resolves outside the replication root`);
|
|
20548
|
+
}
|
|
20549
|
+
const replicasRoot = join8(partitionPath, "replicas");
|
|
20550
|
+
const replicasStats = await lstat3(replicasRoot);
|
|
20551
|
+
if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
|
|
20552
|
+
throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot}" is not a real directory`);
|
|
20553
|
+
}
|
|
20554
|
+
const replicaPath = join8(replicasRoot, name);
|
|
20555
|
+
const replicaStats = await lstat3(replicaPath);
|
|
20556
|
+
if (replicaStats.isSymbolicLink() || !replicaStats.isDirectory()) {
|
|
20557
|
+
throw refuseUnsafeReplicaPath(profile, `replication replica "${name}" is not a real directory`);
|
|
20558
|
+
}
|
|
20559
|
+
const replicaReal = await realpath(replicaPath);
|
|
20560
|
+
if (!replicaReal.startsWith(partitionReal + sep)) {
|
|
20561
|
+
throw refuseUnsafeReplicaPath(profile, `replication replica "${name}" resolves outside the partition`);
|
|
20562
|
+
}
|
|
20563
|
+
const database = await lstat3(join8(replicaPath, "replica.db"));
|
|
20564
|
+
if (database.isSymbolicLink() || !database.isFile()) {
|
|
20565
|
+
throw refuseUnsafeReplicaPath(profile, `replication database "${name}" is not a regular file`);
|
|
20566
|
+
}
|
|
20567
|
+
return { dev: replicaStats.dev, ino: replicaStats.ino };
|
|
20568
|
+
}
|
|
20569
|
+
async function removeProfileReplication(profile, heldLeases, preflightRoot) {
|
|
20066
20570
|
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;
|
|
20571
|
+
const namedRoot = join8(profilePath4(profile), "replication");
|
|
20572
|
+
const pinned = preflightRoot ?? await pinReplicaRoot(profile, namedRoot, "logout");
|
|
20573
|
+
if (!pinned) return [];
|
|
20574
|
+
if (preflightRoot) await pinned.assertNamed();
|
|
20075
20575
|
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`);
|
|
20576
|
+
const root = pinned.path;
|
|
20577
|
+
const rootReal = await safeReplicaRoot(profile, root);
|
|
20578
|
+
const profileReal = await realpath(profilePath4(profile));
|
|
20579
|
+
const partitions = await readdir4(root, { withFileTypes: true });
|
|
20580
|
+
const opened = [];
|
|
20581
|
+
const removed = [];
|
|
20582
|
+
let deleting = false;
|
|
20583
|
+
try {
|
|
20584
|
+
for (const partition of partitions) {
|
|
20585
|
+
const partitionPath = join8(root, partition.name);
|
|
20586
|
+
const partitionStats = await lstat3(partitionPath);
|
|
20587
|
+
if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
|
|
20588
|
+
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
|
|
20095
20589
|
}
|
|
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`);
|
|
20590
|
+
if (partitionStats.isDirectory()) {
|
|
20591
|
+
const partitionReal = await realpath(partitionPath);
|
|
20592
|
+
if (!partitionReal.startsWith(rootReal + sep) || !partitionReal.startsWith(profileReal + sep)) {
|
|
20593
|
+
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the selected profile`);
|
|
20102
20594
|
}
|
|
20103
|
-
const
|
|
20104
|
-
|
|
20105
|
-
|
|
20595
|
+
const replicasRoot = join8(partitionPath, "replicas");
|
|
20596
|
+
const replicasStats = await lstat3(replicasRoot).catch((error) => {
|
|
20597
|
+
if (error.code === "ENOENT") return null;
|
|
20598
|
+
throw error;
|
|
20599
|
+
});
|
|
20600
|
+
if (!replicasStats) continue;
|
|
20601
|
+
if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
|
|
20602
|
+
throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot}" is not a real directory`);
|
|
20603
|
+
}
|
|
20604
|
+
const replicas = await readdir4(replicasRoot, { withFileTypes: true });
|
|
20605
|
+
for (const replica of replicas) {
|
|
20606
|
+
const replicaPath = join8(replicasRoot, replica.name);
|
|
20607
|
+
const replicaStats = await lstat3(replicaPath);
|
|
20608
|
+
if (replica.isSymbolicLink() || replicaStats.isSymbolicLink() || !replicaStats.isDirectory()) {
|
|
20609
|
+
throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
|
|
20610
|
+
}
|
|
20611
|
+
const replicaReal = await realpath(replicaPath);
|
|
20612
|
+
if (!replicaReal.startsWith(rootReal + sep)) {
|
|
20613
|
+
throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" resolves outside the replication root`);
|
|
20614
|
+
}
|
|
20615
|
+
const database = await lstat3(join8(replicaPath, "replica.db")).catch((error) => {
|
|
20616
|
+
if (error.code === "ENOENT") return null;
|
|
20617
|
+
throw error;
|
|
20618
|
+
});
|
|
20619
|
+
if (!database) continue;
|
|
20620
|
+
if (database.isSymbolicLink() || !database.isFile()) {
|
|
20621
|
+
throw refuseUnsafeReplicaPath(profile, `replication database "${replica.name}" is not a regular file`);
|
|
20622
|
+
}
|
|
20106
20623
|
}
|
|
20107
|
-
|
|
20624
|
+
} else if (!partitionStats.isFile()) {
|
|
20625
|
+
throw refuseUnsafeReplicaPath(profile, `replication path "${partition.name}" is not a regular file or directory`);
|
|
20626
|
+
}
|
|
20627
|
+
}
|
|
20628
|
+
const hasReplicaDatabase = await Promise.all(partitions.filter((entry) => entry.isDirectory()).map(async (partition) => {
|
|
20629
|
+
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)));
|
|
20630
|
+
})).then((values) => values.some((partition) => partition.some(Boolean)));
|
|
20631
|
+
if (hasReplicaDatabase) await loadSqlite();
|
|
20632
|
+
for (const partition of partitions.filter((entry) => entry.isDirectory())) {
|
|
20633
|
+
const replicasRoot = join8(root, partition.name, "replicas");
|
|
20634
|
+
for (const entry of await readdir4(replicasRoot, { withFileTypes: true }).catch(() => [])) {
|
|
20635
|
+
if (!entry.isDirectory()) continue;
|
|
20636
|
+
const database = await lstat3(join8(replicasRoot, entry.name, "replica.db")).catch((error) => {
|
|
20108
20637
|
if (error.code === "ENOENT") return null;
|
|
20109
20638
|
throw error;
|
|
20110
20639
|
});
|
|
20111
20640
|
if (!database) continue;
|
|
20112
|
-
|
|
20113
|
-
|
|
20641
|
+
const name = `replication/${partition.name}/${entry.name}`;
|
|
20642
|
+
const expectedDirectory = await assertSafeFlagReplica(profile, root, partition.name, entry.name);
|
|
20643
|
+
const store = await SqliteReplicaStore.open(join8(replicasRoot, entry.name), {
|
|
20644
|
+
create: false,
|
|
20645
|
+
expectedDirectory,
|
|
20646
|
+
guard: async (action) => withProfileLock4(profile, async () => {
|
|
20647
|
+
await assertSafeFlagReplica(profile, root, partition.name, entry.name);
|
|
20648
|
+
return action();
|
|
20649
|
+
}, { requireProfile: true })
|
|
20650
|
+
});
|
|
20651
|
+
try {
|
|
20652
|
+
if (await store.open() === null) {
|
|
20653
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "${name}" has no initialized store.`, ExitCode.ERROR);
|
|
20654
|
+
}
|
|
20655
|
+
const leasePath = join8(namedRoot, partition.name, "replicas", entry.name);
|
|
20656
|
+
const lease = heldLeases?.get(leasePath) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20657
|
+
if (lease === null) {
|
|
20658
|
+
throw new CLIError("REPLICA_BUSY", `Cannot log out: flag replica "${name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
20659
|
+
}
|
|
20660
|
+
opened.push({ name, path: leasePath, store, lease });
|
|
20661
|
+
} catch (error) {
|
|
20662
|
+
await store.close().catch(() => void 0);
|
|
20663
|
+
throw error;
|
|
20114
20664
|
}
|
|
20115
20665
|
}
|
|
20116
|
-
} else if (!partitionStats.isFile()) {
|
|
20117
|
-
throw refuseUnsafeReplicaPath(profile, `replication path "${partition.name}" is not a regular file or directory`);
|
|
20118
20666
|
}
|
|
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 });
|
|
20667
|
+
deleting = true;
|
|
20668
|
+
for (const item of opened) {
|
|
20669
|
+
await item.store.destroy(item.lease);
|
|
20670
|
+
removed.push(item.name);
|
|
20145
20671
|
}
|
|
20672
|
+
await pinned.assertNamed();
|
|
20673
|
+
for (const entry of await readdir4(root)) await rm4(join8(root, entry), { recursive: true, force: true });
|
|
20674
|
+
await pinned.assertNamed();
|
|
20675
|
+
await rmdir2(namedRoot);
|
|
20676
|
+
return removed;
|
|
20677
|
+
} catch (error) {
|
|
20678
|
+
if (deleting) {
|
|
20679
|
+
const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
|
|
20680
|
+
throw new CLIError(
|
|
20681
|
+
"REPLICA_PURGE_FAILED",
|
|
20682
|
+
`Logout cleared the session, but replication cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
|
|
20683
|
+
ExitCode.ERROR,
|
|
20684
|
+
{ replicasRemoved: removed, replicasRemaining: remaining }
|
|
20685
|
+
);
|
|
20686
|
+
}
|
|
20687
|
+
if (error instanceof CLIError) throw error;
|
|
20688
|
+
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);
|
|
20689
|
+
} finally {
|
|
20690
|
+
await Promise.all(opened.map(async ({ path, store, lease }) => {
|
|
20691
|
+
if (heldLeases?.has(path) !== true) await store.releaseLease(lease).catch(() => void 0);
|
|
20692
|
+
await store.close().catch(() => void 0);
|
|
20693
|
+
}));
|
|
20146
20694
|
}
|
|
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
20695
|
} finally {
|
|
20167
|
-
await
|
|
20168
|
-
if (heldLeases?.has(path) !== true) await store.releaseLease(lease).catch(() => void 0);
|
|
20169
|
-
await store.close().catch(() => void 0);
|
|
20170
|
-
}));
|
|
20696
|
+
if (!preflightRoot) await pinned.close();
|
|
20171
20697
|
}
|
|
20172
20698
|
});
|
|
20173
20699
|
}
|
|
@@ -20175,36 +20701,54 @@ async function acquireLogoutLeases(profile) {
|
|
|
20175
20701
|
return withProfileLock4(profile, async () => {
|
|
20176
20702
|
let sqliteLoaded = false;
|
|
20177
20703
|
const opened = [];
|
|
20704
|
+
const roots = /* @__PURE__ */ new Map();
|
|
20178
20705
|
try {
|
|
20179
|
-
const
|
|
20180
|
-
|
|
20706
|
+
const legacyNamed = join8(profilePath4(profile), "replicas");
|
|
20707
|
+
const legacyPin = await pinReplicaRoot(profile, legacyNamed, "logout");
|
|
20708
|
+
if (legacyPin) {
|
|
20709
|
+
roots.set("replicas", legacyPin);
|
|
20710
|
+
const legacyRoot = legacyPin.path;
|
|
20181
20711
|
const entries = (await readdir4(legacyRoot, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
20182
20712
|
for (const entry of entries) {
|
|
20183
|
-
const path = await
|
|
20713
|
+
const { path, identity: expectedDirectory, empty: empty4 } = await inspectLegacyReplica(profile, legacyRoot, entry);
|
|
20714
|
+
if (empty4) continue;
|
|
20184
20715
|
if (!sqliteLoaded) {
|
|
20185
20716
|
await loadSqlite();
|
|
20186
20717
|
sqliteLoaded = true;
|
|
20187
20718
|
}
|
|
20188
|
-
const store = await SqliteReplicaStore.open(path, {
|
|
20189
|
-
|
|
20190
|
-
|
|
20191
|
-
|
|
20192
|
-
|
|
20193
|
-
|
|
20194
|
-
|
|
20195
|
-
|
|
20196
|
-
|
|
20719
|
+
const store = await SqliteReplicaStore.open(path, {
|
|
20720
|
+
create: false,
|
|
20721
|
+
expectedDirectory,
|
|
20722
|
+
guard: async (action) => withProfileLock4(profile, async () => {
|
|
20723
|
+
await safeReplicaEntry(profile, legacyRoot, entry);
|
|
20724
|
+
return action();
|
|
20725
|
+
}, { requireProfile: true })
|
|
20726
|
+
});
|
|
20727
|
+
try {
|
|
20728
|
+
if (await store.open() === null) {
|
|
20729
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
|
|
20730
|
+
}
|
|
20731
|
+
const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20732
|
+
if (lease === null) {
|
|
20733
|
+
throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
20734
|
+
}
|
|
20735
|
+
opened.push({ path: join8(legacyNamed, entry.name), store, lease });
|
|
20736
|
+
} catch (error) {
|
|
20737
|
+
await store.close().catch(() => void 0);
|
|
20738
|
+
throw error;
|
|
20197
20739
|
}
|
|
20198
|
-
opened.push({ path, store, lease });
|
|
20199
20740
|
}
|
|
20200
20741
|
}
|
|
20201
|
-
const
|
|
20202
|
-
const
|
|
20203
|
-
if (
|
|
20742
|
+
const replicationNamed = join8(profilePath4(profile), "replication");
|
|
20743
|
+
const replicationPin = await pinReplicaRoot(profile, replicationNamed, "logout");
|
|
20744
|
+
if (replicationPin) {
|
|
20745
|
+
roots.set("replication", replicationPin);
|
|
20746
|
+
const replicationRoot = replicationPin.path;
|
|
20747
|
+
const replicationReal = await safeReplicaRoot(profile, replicationRoot);
|
|
20204
20748
|
const partitions = await readdir4(replicationRoot, { withFileTypes: true });
|
|
20205
20749
|
for (const partition of partitions) {
|
|
20206
20750
|
const partitionPath = join8(replicationRoot, partition.name);
|
|
20207
|
-
const partitionStats = await
|
|
20751
|
+
const partitionStats = await lstat3(partitionPath);
|
|
20208
20752
|
if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
|
|
20209
20753
|
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
|
|
20210
20754
|
}
|
|
@@ -20216,22 +20760,22 @@ async function acquireLogoutLeases(profile) {
|
|
|
20216
20760
|
if (!partitionReal.startsWith(replicationReal + sep)) {
|
|
20217
20761
|
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the replication root`);
|
|
20218
20762
|
}
|
|
20219
|
-
const
|
|
20220
|
-
const replicasStats = await
|
|
20763
|
+
const replicasRoot = join8(partitionPath, "replicas");
|
|
20764
|
+
const replicasStats = await lstat3(replicasRoot).catch((error) => {
|
|
20221
20765
|
if (error.code === "ENOENT") return null;
|
|
20222
20766
|
throw error;
|
|
20223
20767
|
});
|
|
20224
20768
|
if (!replicasStats) continue;
|
|
20225
20769
|
if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
|
|
20226
|
-
throw refuseUnsafeReplicaPath(profile, `replication replicas path "${
|
|
20770
|
+
throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot}" is not a real directory`);
|
|
20227
20771
|
}
|
|
20228
|
-
for (const replica of await readdir4(
|
|
20229
|
-
const path = join8(
|
|
20230
|
-
const stats = await
|
|
20772
|
+
for (const replica of await readdir4(replicasRoot, { withFileTypes: true })) {
|
|
20773
|
+
const path = join8(replicasRoot, replica.name);
|
|
20774
|
+
const stats = await lstat3(path);
|
|
20231
20775
|
if (!replica.isDirectory() || replica.isSymbolicLink() || stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
20232
20776
|
throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
|
|
20233
20777
|
}
|
|
20234
|
-
const database = await
|
|
20778
|
+
const database = await lstat3(join8(path, "replica.db")).catch((error) => {
|
|
20235
20779
|
if (error.code === "ENOENT") return null;
|
|
20236
20780
|
throw error;
|
|
20237
20781
|
});
|
|
@@ -20247,26 +20791,38 @@ async function acquireLogoutLeases(profile) {
|
|
|
20247
20791
|
await loadSqlite();
|
|
20248
20792
|
sqliteLoaded = true;
|
|
20249
20793
|
}
|
|
20250
|
-
const
|
|
20251
|
-
|
|
20252
|
-
|
|
20253
|
-
|
|
20254
|
-
|
|
20255
|
-
|
|
20256
|
-
|
|
20257
|
-
|
|
20258
|
-
|
|
20794
|
+
const expectedDirectory = await assertSafeFlagReplica(profile, replicationRoot, partition.name, replica.name);
|
|
20795
|
+
const store = await SqliteReplicaStore.open(path, {
|
|
20796
|
+
create: false,
|
|
20797
|
+
expectedDirectory,
|
|
20798
|
+
guard: async (action) => withProfileLock4(profile, async () => {
|
|
20799
|
+
await assertSafeFlagReplica(profile, replicationRoot, partition.name, replica.name);
|
|
20800
|
+
return action();
|
|
20801
|
+
}, { requireProfile: true })
|
|
20802
|
+
});
|
|
20803
|
+
try {
|
|
20804
|
+
if (await store.open() === null) {
|
|
20805
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "replication/${partition.name}/${replica.name}" has no initialized store.`, ExitCode.ERROR);
|
|
20806
|
+
}
|
|
20807
|
+
const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20808
|
+
if (lease === null) {
|
|
20809
|
+
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);
|
|
20810
|
+
}
|
|
20811
|
+
opened.push({ path: join8(replicationNamed, partition.name, "replicas", replica.name), store, lease });
|
|
20812
|
+
} catch (error) {
|
|
20813
|
+
await store.close().catch(() => void 0);
|
|
20814
|
+
throw error;
|
|
20259
20815
|
}
|
|
20260
|
-
opened.push({ path, store, lease });
|
|
20261
20816
|
}
|
|
20262
20817
|
}
|
|
20263
20818
|
}
|
|
20264
|
-
return opened;
|
|
20819
|
+
return { stores: opened, roots };
|
|
20265
20820
|
} catch (error) {
|
|
20266
20821
|
await Promise.all(opened.map(async ({ store, lease }) => {
|
|
20267
20822
|
await store.releaseLease(lease).catch(() => void 0);
|
|
20268
20823
|
await store.close().catch(() => void 0);
|
|
20269
20824
|
}));
|
|
20825
|
+
await Promise.all([...roots.values()].map((root) => root.close()));
|
|
20270
20826
|
throw error;
|
|
20271
20827
|
}
|
|
20272
20828
|
});
|
|
@@ -20274,31 +20830,32 @@ async function acquireLogoutLeases(profile) {
|
|
|
20274
20830
|
async function removeProfileReplicasAndReplication(profile) {
|
|
20275
20831
|
return withProfileLock4(profile, async () => {
|
|
20276
20832
|
const acquired = await acquireLogoutLeases(profile);
|
|
20277
|
-
const heldLeases = new Map(acquired.map(({ path, lease }) => [path, lease]));
|
|
20833
|
+
const heldLeases = new Map(acquired.stores.map(({ path, lease }) => [path, lease]));
|
|
20278
20834
|
let legacyRemoved = [];
|
|
20279
20835
|
try {
|
|
20280
|
-
legacyRemoved = await removeProfileReplicas(profile, heldLeases);
|
|
20281
|
-
const replication = await removeProfileReplication(profile, heldLeases);
|
|
20836
|
+
legacyRemoved = await removeProfileReplicas(profile, heldLeases, acquired.roots.get("replicas"));
|
|
20837
|
+
const replication = await removeProfileReplication(profile, heldLeases, acquired.roots.get("replication"));
|
|
20282
20838
|
return [...legacyRemoved, ...replication];
|
|
20283
20839
|
} catch (error) {
|
|
20284
20840
|
const partialRemoved = error instanceof CLIError && Array.isArray(error.metadata?.replicasRemoved) ? error.metadata.replicasRemoved.filter((name) => typeof name === "string") : [];
|
|
20285
20841
|
const removed = [.../* @__PURE__ */ new Set([...legacyRemoved, ...partialRemoved])];
|
|
20286
20842
|
const removedSet = new Set(removed);
|
|
20287
|
-
const remaining = acquired.map(({ path }) => {
|
|
20288
|
-
const name = relative(
|
|
20843
|
+
const remaining = acquired.stores.map(({ path }) => {
|
|
20844
|
+
const name = relative(profilePath4(profile), path).split(sep).join("/");
|
|
20289
20845
|
return name.startsWith("replication/") ? name.replace("/replicas/", "/") : name.split("/").at(-1);
|
|
20290
20846
|
}).filter((name) => !removedSet.has(name));
|
|
20291
20847
|
throw new CLIError(
|
|
20292
20848
|
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"}
|
|
20849
|
+
`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
20850
|
error instanceof CLIError ? error.exitCode : ExitCode.ERROR,
|
|
20295
20851
|
{ replicasRemoved: removed, replicasRemaining: remaining }
|
|
20296
20852
|
);
|
|
20297
20853
|
} finally {
|
|
20298
|
-
await Promise.all(acquired.map(async ({ store, lease }) => {
|
|
20854
|
+
await Promise.all(acquired.stores.map(async ({ store, lease }) => {
|
|
20299
20855
|
await store.releaseLease(lease).catch(() => void 0);
|
|
20300
20856
|
await store.close().catch(() => void 0);
|
|
20301
20857
|
}));
|
|
20858
|
+
await Promise.all([...acquired.roots.values()].map((root) => root.close()));
|
|
20302
20859
|
}
|
|
20303
20860
|
});
|
|
20304
20861
|
}
|
|
@@ -20469,7 +21026,7 @@ function shouldOpenBrowser(options) {
|
|
|
20469
21026
|
return env !== "1" && env !== "true";
|
|
20470
21027
|
}
|
|
20471
21028
|
async function callbackFlow(did, options = {}) {
|
|
20472
|
-
return new Promise((
|
|
21029
|
+
return new Promise((resolve6, reject) => {
|
|
20473
21030
|
let timeout;
|
|
20474
21031
|
let settled = false;
|
|
20475
21032
|
let rl;
|
|
@@ -20483,7 +21040,7 @@ async function callbackFlow(did, options = {}) {
|
|
|
20483
21040
|
rl.close();
|
|
20484
21041
|
}
|
|
20485
21042
|
if (result.data) {
|
|
20486
|
-
|
|
21043
|
+
resolve6(result.data);
|
|
20487
21044
|
} else {
|
|
20488
21045
|
reject(result.error);
|
|
20489
21046
|
}
|
|
@@ -20562,8 +21119,8 @@ async function callbackFlow(did, options = {}) {
|
|
|
20562
21119
|
}
|
|
20563
21120
|
if (openBrowser) {
|
|
20564
21121
|
try {
|
|
20565
|
-
const
|
|
20566
|
-
await
|
|
21122
|
+
const open9 = (await import("open")).default;
|
|
21123
|
+
await open9(authUrl);
|
|
20567
21124
|
} catch {
|
|
20568
21125
|
settle({ error: new Error("Failed to open browser") });
|
|
20569
21126
|
return;
|
|
@@ -20607,13 +21164,13 @@ Open this URL in a browser to authenticate:
|
|
|
20607
21164
|
input: process.stdin,
|
|
20608
21165
|
output: process.stderr
|
|
20609
21166
|
});
|
|
20610
|
-
return new Promise((
|
|
21167
|
+
return new Promise((resolve6, reject) => {
|
|
20611
21168
|
let answered = false;
|
|
20612
21169
|
rl.on("line", (input) => {
|
|
20613
21170
|
if (answered || input.trim() === "") return;
|
|
20614
21171
|
answered = true;
|
|
20615
21172
|
rl.close();
|
|
20616
|
-
void delegationFromInput(input, did, options).then(
|
|
21173
|
+
void delegationFromInput(input, did, options).then(resolve6, reject);
|
|
20617
21174
|
});
|
|
20618
21175
|
rl.on("close", () => {
|
|
20619
21176
|
if (answered) return;
|
|
@@ -21514,7 +22071,7 @@ async function acquireDeviceDelegation(input) {
|
|
|
21514
22071
|
expiresAt: new Date(deadline).toISOString().replace(/\.\d{3}Z$/, "Z")
|
|
21515
22072
|
});
|
|
21516
22073
|
let interval = started.interval;
|
|
21517
|
-
const wait = input.wait ?? ((milliseconds) => new Promise((
|
|
22074
|
+
const wait = input.wait ?? ((milliseconds) => new Promise((resolve6) => setTimeout(resolve6, milliseconds)));
|
|
21518
22075
|
while (Date.now() < deadline) {
|
|
21519
22076
|
await wait(interval * 1e3);
|
|
21520
22077
|
let response;
|
|
@@ -22150,7 +22707,7 @@ async function promptAuthMethod() {
|
|
|
22150
22707
|
input: process.stdin,
|
|
22151
22708
|
output: process.stderr
|
|
22152
22709
|
});
|
|
22153
|
-
return new Promise((
|
|
22710
|
+
return new Promise((resolve6) => {
|
|
22154
22711
|
process.stderr.write("\n" + theme.heading("Choose authentication method:") + "\n");
|
|
22155
22712
|
process.stderr.write(` ${theme.accent("1)")} OpenKey ${theme.muted("(browser-based, for interactive use)")}
|
|
22156
22713
|
`);
|
|
@@ -22161,9 +22718,9 @@ async function promptAuthMethod() {
|
|
|
22161
22718
|
rl.close();
|
|
22162
22719
|
const trimmed = answer.trim();
|
|
22163
22720
|
if (trimmed === "2" || trimmed.toLowerCase() === "local") {
|
|
22164
|
-
|
|
22721
|
+
resolve6("local");
|
|
22165
22722
|
} else {
|
|
22166
|
-
|
|
22723
|
+
resolve6("openkey");
|
|
22167
22724
|
}
|
|
22168
22725
|
});
|
|
22169
22726
|
});
|
|
@@ -22865,7 +23422,7 @@ function describeSession(state2, expiresAt) {
|
|
|
22865
23422
|
}
|
|
22866
23423
|
async function emitPermissionRequestArtifact(artifact, emitOption) {
|
|
22867
23424
|
if (typeof emitOption === "string" && emitOption.length > 0) {
|
|
22868
|
-
await mkdir3(
|
|
23425
|
+
await mkdir3(dirname4(emitOption), { recursive: true });
|
|
22869
23426
|
await writeFile2(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
22870
23427
|
outputJson({
|
|
22871
23428
|
emitted: true,
|
|
@@ -22901,13 +23458,13 @@ async function readStdin() {
|
|
|
22901
23458
|
return Buffer.concat(chunks).toString("utf8");
|
|
22902
23459
|
}
|
|
22903
23460
|
function readUrl(source) {
|
|
22904
|
-
return new Promise((
|
|
23461
|
+
return new Promise((resolve6, reject) => {
|
|
22905
23462
|
const getter = source.startsWith("https://") ? httpsGet : httpGet;
|
|
22906
23463
|
const request = getter(source, (response) => {
|
|
22907
23464
|
const status = response.statusCode ?? 0;
|
|
22908
23465
|
if (status >= 300 && status < 400 && response.headers.location) {
|
|
22909
23466
|
response.resume();
|
|
22910
|
-
readUrl(new URL(response.headers.location, source).toString()).then(
|
|
23467
|
+
readUrl(new URL(response.headers.location, source).toString()).then(resolve6, reject);
|
|
22911
23468
|
return;
|
|
22912
23469
|
}
|
|
22913
23470
|
if (status < 200 || status >= 300) {
|
|
@@ -22923,7 +23480,7 @@ function readUrl(source) {
|
|
|
22923
23480
|
response.on("data", (chunk) => {
|
|
22924
23481
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
22925
23482
|
});
|
|
22926
|
-
response.on("end", () =>
|
|
23483
|
+
response.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
|
|
22927
23484
|
});
|
|
22928
23485
|
request.on("error", reject);
|
|
22929
23486
|
});
|
|
@@ -23163,7 +23720,7 @@ function permissionGrantReason(context, permissions) {
|
|
|
23163
23720
|
return `${context} Requested: ${summary}${more}.`;
|
|
23164
23721
|
}
|
|
23165
23722
|
function execCapturedCommand(command) {
|
|
23166
|
-
return new Promise((
|
|
23723
|
+
return new Promise((resolve6, reject) => {
|
|
23167
23724
|
const child = spawn(process.execPath, [process.argv[1], ...command.argv], {
|
|
23168
23725
|
cwd: command.cwd,
|
|
23169
23726
|
env: process.env,
|
|
@@ -23182,7 +23739,7 @@ function execCapturedCommand(command) {
|
|
|
23182
23739
|
if (code3 && code3 !== 0) {
|
|
23183
23740
|
process.exitCode = code3;
|
|
23184
23741
|
}
|
|
23185
|
-
|
|
23742
|
+
resolve6();
|
|
23186
23743
|
});
|
|
23187
23744
|
});
|
|
23188
23745
|
}
|
|
@@ -23211,8 +23768,8 @@ async function confirmPermissionRequest(permissions) {
|
|
|
23211
23768
|
input: process.stdin,
|
|
23212
23769
|
output: process.stderr
|
|
23213
23770
|
});
|
|
23214
|
-
const answer = await new Promise((
|
|
23215
|
-
rl.question("Approve local-key delegation? [y/N] ",
|
|
23771
|
+
const answer = await new Promise((resolve6) => {
|
|
23772
|
+
rl.question("Approve local-key delegation? [y/N] ", resolve6);
|
|
23216
23773
|
});
|
|
23217
23774
|
rl.close();
|
|
23218
23775
|
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
@@ -24057,7 +24614,7 @@ init_errors();
|
|
|
24057
24614
|
init_sdk();
|
|
24058
24615
|
init_theme();
|
|
24059
24616
|
import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
|
|
24060
|
-
import { resolve } from "path";
|
|
24617
|
+
import { resolve as resolve2 } from "path";
|
|
24061
24618
|
function registerDuckdbCommand(program) {
|
|
24062
24619
|
const duckdb = program.command("duckdb").description("DuckDB database operations");
|
|
24063
24620
|
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 +24726,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
24169
24726
|
}
|
|
24170
24727
|
const blob = result.data;
|
|
24171
24728
|
const buffer = Buffer.from(await blob.arrayBuffer());
|
|
24172
|
-
const outputPath =
|
|
24729
|
+
const outputPath = resolve2(options.output);
|
|
24173
24730
|
await writeFile3(outputPath, buffer);
|
|
24174
24731
|
outputJson({
|
|
24175
24732
|
file: outputPath,
|
|
@@ -24185,7 +24742,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
24185
24742
|
const globalOpts = cmd.optsWithGlobals();
|
|
24186
24743
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
24187
24744
|
const node = await ensureAuthenticated(ctx);
|
|
24188
|
-
const filePath =
|
|
24745
|
+
const filePath = resolve2(file);
|
|
24189
24746
|
const bytes = new Uint8Array(await readFile6(filePath));
|
|
24190
24747
|
const result = await withSpinner(
|
|
24191
24748
|
"Importing database...",
|
|
@@ -24327,11 +24884,11 @@ init_storage();
|
|
|
24327
24884
|
init_errors();
|
|
24328
24885
|
init_constants();
|
|
24329
24886
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
24330
|
-
import { lstat as
|
|
24331
|
-
import { basename as basename2, dirname as
|
|
24887
|
+
import { lstat as lstat4, open as open4, rename as rename2, rm as rm5 } from "fs/promises";
|
|
24888
|
+
import { basename as basename2, dirname as dirname5, join as join9 } from "path";
|
|
24332
24889
|
async function validatePrivateOutput(path, label = "output") {
|
|
24333
24890
|
try {
|
|
24334
|
-
const destination = await
|
|
24891
|
+
const destination = await lstat4(path);
|
|
24335
24892
|
if (!destination.isFile()) {
|
|
24336
24893
|
throw new CLIError("INVALID_ARGUMENT", `${label} "${path}" must be a regular file, not a symlink, directory, or device.`, ExitCode.USAGE_ERROR);
|
|
24337
24894
|
}
|
|
@@ -24339,7 +24896,7 @@ async function validatePrivateOutput(path, label = "output") {
|
|
|
24339
24896
|
if (error?.code !== "ENOENT") throw error;
|
|
24340
24897
|
}
|
|
24341
24898
|
try {
|
|
24342
|
-
const parent = await
|
|
24899
|
+
const parent = await lstat4(dirname5(path));
|
|
24343
24900
|
if (!parent.isDirectory()) {
|
|
24344
24901
|
throw new CLIError("INVALID_ARGUMENT", `${label} "${path}" requires an existing directory parent.`, ExitCode.USAGE_ERROR);
|
|
24345
24902
|
}
|
|
@@ -24350,11 +24907,11 @@ async function validatePrivateOutput(path, label = "output") {
|
|
|
24350
24907
|
}
|
|
24351
24908
|
async function writePrivateOutput(path, value, label = "output") {
|
|
24352
24909
|
await validatePrivateOutput(path, label);
|
|
24353
|
-
const parentPath =
|
|
24910
|
+
const parentPath = dirname5(path);
|
|
24354
24911
|
const temp = join9(parentPath, `.${basename2(path)}.${randomUUID2()}.tmp`);
|
|
24355
24912
|
let created = false;
|
|
24356
24913
|
try {
|
|
24357
|
-
const handle = await
|
|
24914
|
+
const handle = await open4(temp, "wx", PRIVATE_FILE_MODE);
|
|
24358
24915
|
created = true;
|
|
24359
24916
|
try {
|
|
24360
24917
|
await handle.writeFile(value);
|
|
@@ -24369,7 +24926,7 @@ async function writePrivateOutput(path, value, label = "output") {
|
|
|
24369
24926
|
throw new CLIError("ERROR", `Could not write ${label.toLowerCase()} "${path}"${code3}.`, ExitCode.ERROR);
|
|
24370
24927
|
}
|
|
24371
24928
|
try {
|
|
24372
|
-
const parent = await
|
|
24929
|
+
const parent = await open4(parentPath, "r");
|
|
24373
24930
|
try {
|
|
24374
24931
|
await parent.sync();
|
|
24375
24932
|
} finally {
|
|
@@ -25009,8 +25566,8 @@ Examples:
|
|
|
25009
25566
|
try {
|
|
25010
25567
|
if (isInteractive()) {
|
|
25011
25568
|
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] `,
|
|
25569
|
+
const answer = await new Promise((resolve6) => {
|
|
25570
|
+
rl.question(`Delete profile "${name}"? This cannot be undone. [y/N] `, resolve6);
|
|
25014
25571
|
});
|
|
25015
25572
|
rl.close();
|
|
25016
25573
|
if (answer.toLowerCase() !== "y") {
|
|
@@ -25045,22 +25602,19 @@ function parseOperatorType(raw) {
|
|
|
25045
25602
|
}
|
|
25046
25603
|
|
|
25047
25604
|
// src/commands/replica.ts
|
|
25048
|
-
init_sdk();
|
|
25049
|
-
init_replication_registry();
|
|
25050
25605
|
import { CLI_NAME as CLI_NAME22 } from "@tinycloud/operations/cli-name";
|
|
25051
25606
|
import { randomBytes as randomBytes6 } from "crypto";
|
|
25052
|
-
import { constants as
|
|
25053
|
-
import { open as
|
|
25054
|
-
import { basename as basename3, dirname as
|
|
25607
|
+
import { constants as fsConstants3, lstatSync as lstatSync2 } from "fs";
|
|
25608
|
+
import { mkdir as mkdir4, open as open5, readdir as readdir5, rename as rename3, unlink as unlink2, lstat as lstat5 } from "fs/promises";
|
|
25609
|
+
import { basename as basename3, dirname as dirname6, join as join10, resolve as resolve3 } from "path";
|
|
25055
25610
|
|
|
25056
25611
|
// 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
25612
|
function percentile(values, fraction) {
|
|
25059
25613
|
if (values.length === 0) return null;
|
|
25060
25614
|
values.sort((left, right) => left - right);
|
|
25061
25615
|
return values[Math.ceil(fraction * values.length) - 1] ?? values[values.length - 1];
|
|
25062
25616
|
}
|
|
25063
|
-
function createReplicationReport(since, events,
|
|
25617
|
+
function createReplicationReport(since, events, partitions) {
|
|
25064
25618
|
const reads = events.filter((event) => event.type === "replication.read");
|
|
25065
25619
|
const replicaReads = reads.filter((event) => event.source === "replica");
|
|
25066
25620
|
const sourceLatency = /* @__PURE__ */ new Map();
|
|
@@ -25103,9 +25657,9 @@ function createReplicationReport(since, events, replicas, partitions, warning) {
|
|
|
25103
25657
|
syncs,
|
|
25104
25658
|
writes,
|
|
25105
25659
|
divergences,
|
|
25106
|
-
replicas,
|
|
25660
|
+
replicas: [],
|
|
25107
25661
|
partitions,
|
|
25108
|
-
|
|
25662
|
+
replicasUnavailable: true
|
|
25109
25663
|
};
|
|
25110
25664
|
}
|
|
25111
25665
|
function renderReplicationReport(report) {
|
|
@@ -25118,34 +25672,30 @@ function renderReplicationReport(report) {
|
|
|
25118
25672
|
`Writes: ${Object.entries(report.writes).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
|
|
25119
25673
|
`Read source/reason: ${Object.entries(report.readsBySourceReason).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
|
|
25120
25674
|
`Divergences: ${report.totals.divergences}; showing ${report.divergences.length} of ${report.totals.divergences}`,
|
|
25121
|
-
|
|
25675
|
+
"Replicas: unavailable (read-only report)"
|
|
25122
25676
|
];
|
|
25123
25677
|
for (const divergence of report.divergences) {
|
|
25124
25678
|
if (divergence.type === "replication.divergence") {
|
|
25125
25679
|
lines.push(` ${divergence.space} ${divergence.key}: ${divergence.kind} stalenessMs=${divergence.stalenessMs}`);
|
|
25126
25680
|
}
|
|
25127
25681
|
}
|
|
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
25682
|
for (const partition of report.partitions) {
|
|
25134
25683
|
lines.push(`Partition ${partition.idHash}: host=${partition.host ?? "unknown"} space=${partition.space ?? "unknown"} pinned=${partition.pinned}`);
|
|
25135
25684
|
}
|
|
25136
25685
|
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
25686
|
return lines.join("\n");
|
|
25139
25687
|
}
|
|
25140
25688
|
|
|
25141
25689
|
// src/commands/replica.ts
|
|
25142
25690
|
init_dist3();
|
|
25143
25691
|
import {
|
|
25692
|
+
ProfileSymlinkError as ProfileSymlinkError3,
|
|
25144
25693
|
ProfileDeletedError as ProfileDeletedError3,
|
|
25145
|
-
profilePath as
|
|
25694
|
+
profilePath as profilePath5,
|
|
25146
25695
|
readAdditionalDelegations as readAdditionalDelegations2,
|
|
25147
25696
|
readSession as readSession2,
|
|
25148
|
-
withProfileLock as withProfileLock5
|
|
25697
|
+
withProfileLock as withProfileLock5,
|
|
25698
|
+
withPinnedProfileDirectory as withPinnedProfileDirectory3
|
|
25149
25699
|
} from "@tinycloud/operations/state";
|
|
25150
25700
|
import { ownerSessionLacksProof as ownerSessionLacksProof4 } from "@tinycloud/operations/profile";
|
|
25151
25701
|
init_profiles();
|
|
@@ -25201,9 +25751,9 @@ function toCliError(error) {
|
|
|
25201
25751
|
const hint = error.code === ReplicaErrorCode.GRANT_MISSING && typeof error.detail?.hint === "string" ? error.detail.hint : void 0;
|
|
25202
25752
|
return new CLIError(error.code, error.message, EXIT_BY_CODE[error.code], hint === void 0 ? void 0 : { hint });
|
|
25203
25753
|
}
|
|
25204
|
-
async function run(action) {
|
|
25754
|
+
async function run(action, needsSqlite = true) {
|
|
25205
25755
|
try {
|
|
25206
|
-
await loadSqlite();
|
|
25756
|
+
if (needsSqlite) await loadSqlite();
|
|
25207
25757
|
await action();
|
|
25208
25758
|
} catch (error) {
|
|
25209
25759
|
return handleError(toCliError(error));
|
|
@@ -25216,20 +25766,25 @@ async function profileName(cmd) {
|
|
|
25216
25766
|
const config = await ProfileManager.getConfig().catch(() => void 0);
|
|
25217
25767
|
return config?.defaultProfile ?? DEFAULT_PROFILE;
|
|
25218
25768
|
}
|
|
25219
|
-
function replicasRoot(profile) {
|
|
25220
|
-
return join10(profilePath4(profile), "replicas");
|
|
25221
|
-
}
|
|
25222
25769
|
async function replicaNames(profile) {
|
|
25223
|
-
|
|
25224
|
-
|
|
25225
|
-
|
|
25226
|
-
|
|
25227
|
-
()
|
|
25228
|
-
() =>
|
|
25229
|
-
|
|
25230
|
-
|
|
25231
|
-
|
|
25232
|
-
|
|
25770
|
+
return await withReplicaRoot(profile, async (root) => {
|
|
25771
|
+
const names = [];
|
|
25772
|
+
for (const entry of await readdir5(root, { withFileTypes: true })) {
|
|
25773
|
+
if (entry.isSymbolicLink()) throw localPathChanged(profile);
|
|
25774
|
+
if (!NAME.test(entry.name) || !entry.isDirectory()) continue;
|
|
25775
|
+
const present = await withPinnedLocalDirectory(profile, root, entry.name, async (directory) => {
|
|
25776
|
+
const database = await lstat5(join10(directory, "replica.db"), { bigint: true }).catch((error) => {
|
|
25777
|
+
if (error.code === "ENOENT") return null;
|
|
25778
|
+
throw error;
|
|
25779
|
+
});
|
|
25780
|
+
if (database && (database.isSymbolicLink() || !database.isFile())) throw localPathChanged(profile);
|
|
25781
|
+
return database !== null;
|
|
25782
|
+
});
|
|
25783
|
+
if (present === null) throw localPathChanged(profile);
|
|
25784
|
+
if (present) names.push(entry.name);
|
|
25785
|
+
}
|
|
25786
|
+
return names.sort();
|
|
25787
|
+
}) ?? [];
|
|
25233
25788
|
}
|
|
25234
25789
|
function assertName(name) {
|
|
25235
25790
|
if (!NAME.test(name)) {
|
|
@@ -25237,6 +25792,67 @@ function assertName(name) {
|
|
|
25237
25792
|
}
|
|
25238
25793
|
return name;
|
|
25239
25794
|
}
|
|
25795
|
+
async function withReplicaRoot(profile, action, create4 = false) {
|
|
25796
|
+
const profileDirectory = profilePath5(profile);
|
|
25797
|
+
const readRoot = async (directory, recheckProfile) => {
|
|
25798
|
+
if (create4) {
|
|
25799
|
+
if (recheckProfile) await recheckProfile();
|
|
25800
|
+
await mkdir4(join10(directory, "replicas"), { mode: 448 }).catch((error) => {
|
|
25801
|
+
if (error.code !== "EEXIST") throw error;
|
|
25802
|
+
});
|
|
25803
|
+
}
|
|
25804
|
+
return withPinnedLocalDirectory(
|
|
25805
|
+
profile,
|
|
25806
|
+
directory,
|
|
25807
|
+
"replicas",
|
|
25808
|
+
(root, _named, _identity, recheckRoot) => action(root, recheckRoot, recheckProfile)
|
|
25809
|
+
);
|
|
25810
|
+
};
|
|
25811
|
+
if (process.platform === "linux") return readRoot(profileDirectory);
|
|
25812
|
+
const checked = await withPinnedLocalDirectory(
|
|
25813
|
+
profile,
|
|
25814
|
+
dirname6(profileDirectory),
|
|
25815
|
+
basename3(profileDirectory),
|
|
25816
|
+
(directory, _named, _identity, recheckProfile) => readRoot(directory, recheckProfile)
|
|
25817
|
+
);
|
|
25818
|
+
if (checked === null) throw localPathChanged(profile);
|
|
25819
|
+
return checked;
|
|
25820
|
+
}
|
|
25821
|
+
async function withReplicaDirectory(profile, name, action, create4 = false) {
|
|
25822
|
+
assertName(name);
|
|
25823
|
+
const result = await withReplicaRoot(profile, async (root, recheckRoot, recheckProfile) => {
|
|
25824
|
+
if (create4) {
|
|
25825
|
+
await recheckRoot();
|
|
25826
|
+
await mkdir4(join10(root, name), { mode: 448 }).catch((error) => {
|
|
25827
|
+
if (error.code !== "EEXIST") throw error;
|
|
25828
|
+
});
|
|
25829
|
+
}
|
|
25830
|
+
const opened = await withPinnedLocalDirectory(
|
|
25831
|
+
profile,
|
|
25832
|
+
root,
|
|
25833
|
+
name,
|
|
25834
|
+
async (_directory, path, expectedDirectory, recheckChild) => {
|
|
25835
|
+
await recheckRoot();
|
|
25836
|
+
await recheckChild();
|
|
25837
|
+
const database = await lstat5(join10(_directory, "replica.db"), { bigint: true }).catch((error) => {
|
|
25838
|
+
if (error.code === "ENOENT") return null;
|
|
25839
|
+
throw error;
|
|
25840
|
+
});
|
|
25841
|
+
if (database && (database.isSymbolicLink() || !database.isFile())) throw localPathChanged(profile);
|
|
25842
|
+
if (recheckProfile) {
|
|
25843
|
+
await recheckProfile();
|
|
25844
|
+
await recheckRoot();
|
|
25845
|
+
await recheckChild();
|
|
25846
|
+
}
|
|
25847
|
+
return action(path, expectedDirectory);
|
|
25848
|
+
}
|
|
25849
|
+
);
|
|
25850
|
+
if (opened === null) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Replica "${name}" was not found.`);
|
|
25851
|
+
return opened;
|
|
25852
|
+
}, create4);
|
|
25853
|
+
if (result === null) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas.`);
|
|
25854
|
+
return result;
|
|
25855
|
+
}
|
|
25240
25856
|
async function existingReplicaName(profile, option) {
|
|
25241
25857
|
if (option !== void 0) return assertName(option);
|
|
25242
25858
|
const names = await replicaNames(profile);
|
|
@@ -25258,17 +25874,37 @@ function profileGuard(profile) {
|
|
|
25258
25874
|
}
|
|
25259
25875
|
};
|
|
25260
25876
|
}
|
|
25261
|
-
async function
|
|
25262
|
-
return
|
|
25263
|
-
}
|
|
25264
|
-
|
|
25265
|
-
|
|
25266
|
-
|
|
25267
|
-
|
|
25268
|
-
|
|
25269
|
-
return store;
|
|
25877
|
+
async function withOpenReplica(profile, name, action, guard = profileGuard(profile)) {
|
|
25878
|
+
return withReplicaDirectory(profile, name, async (path, expectedDirectory) => {
|
|
25879
|
+
const store = await SqliteReplicaStore.open(path, { create: false, guard, expectedDirectory });
|
|
25880
|
+
try {
|
|
25881
|
+
return await action(store);
|
|
25882
|
+
} finally {
|
|
25883
|
+
await store.close();
|
|
25884
|
+
}
|
|
25270
25885
|
});
|
|
25271
25886
|
}
|
|
25887
|
+
function assertLegacyLogoutClear(profile) {
|
|
25888
|
+
try {
|
|
25889
|
+
lstatSync2(join10(profilePath5(profile), "replica-logout-pending.json"));
|
|
25890
|
+
} catch (error) {
|
|
25891
|
+
if (error?.code === "ENOENT") return;
|
|
25892
|
+
}
|
|
25893
|
+
throw new CLIError(
|
|
25894
|
+
"REPLICA_GATED",
|
|
25895
|
+
"A logout is pending or its state cannot be checked for this profile.",
|
|
25896
|
+
ExitCode.PERMISSION_DENIED
|
|
25897
|
+
);
|
|
25898
|
+
}
|
|
25899
|
+
async function withLegacyReplicaRead(profile, action) {
|
|
25900
|
+
await withProfileLock5(profile, async () => {
|
|
25901
|
+
const checkDelivery = async () => {
|
|
25902
|
+
assertLegacyLogoutClear(profile);
|
|
25903
|
+
};
|
|
25904
|
+
await checkDelivery();
|
|
25905
|
+
await action(checkDelivery);
|
|
25906
|
+
}, { requireProfile: true });
|
|
25907
|
+
}
|
|
25272
25908
|
async function storedGrants(profile) {
|
|
25273
25909
|
const authorizations = [];
|
|
25274
25910
|
const session = await readSession2(profile).catch(() => null);
|
|
@@ -25398,9 +26034,9 @@ function describeStatus(status) {
|
|
|
25398
26034
|
].join("\n");
|
|
25399
26035
|
}
|
|
25400
26036
|
async function writeFileAtomic(path, bytes) {
|
|
25401
|
-
const target =
|
|
25402
|
-
const temp = join10(
|
|
25403
|
-
const handle = await
|
|
26037
|
+
const target = resolve3(path);
|
|
26038
|
+
const temp = join10(dirname6(target), `.${basename3(target)}.${randomBytes6(6).toString("hex")}.tmp`);
|
|
26039
|
+
const handle = await open5(temp, fsConstants3.O_CREAT | fsConstants3.O_EXCL | fsConstants3.O_WRONLY, 384);
|
|
25404
26040
|
try {
|
|
25405
26041
|
await handle.writeFile(bytes);
|
|
25406
26042
|
await handle.sync();
|
|
@@ -25425,14 +26061,86 @@ function replicaSecretsWarning(space, prefix, secretsAllowed) {
|
|
|
25425
26061
|
if (!secretsAllowed || !requiresSecretsOptIn(space, prefix)) return void 0;
|
|
25426
26062
|
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
26063
|
}
|
|
25428
|
-
|
|
25429
|
-
|
|
26064
|
+
function localFileId(value) {
|
|
26065
|
+
if (typeof value === "bigint") return value >= 0n ? value : null;
|
|
26066
|
+
return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
|
|
26067
|
+
}
|
|
26068
|
+
function sameLocalEntry(left, right) {
|
|
26069
|
+
const dev = localFileId(left.dev);
|
|
26070
|
+
const ino = localFileId(left.ino);
|
|
26071
|
+
return dev !== null && ino !== null && dev === localFileId(right.dev) && ino === localFileId(right.ino);
|
|
26072
|
+
}
|
|
26073
|
+
function localPathChanged(profile) {
|
|
26074
|
+
return new ProfileSymlinkError3(profile, "its local replica files contain a symlink or changed while reading");
|
|
26075
|
+
}
|
|
26076
|
+
async function assertNamedLocalEntry(profile, path, expected, kind) {
|
|
26077
|
+
const current = await lstat5(path, { bigint: true }).catch(() => null);
|
|
26078
|
+
if (!current || current.isSymbolicLink() || (kind === "directory" ? !current.isDirectory() : !current.isFile()) || !sameLocalEntry(expected, current)) throw localPathChanged(profile);
|
|
26079
|
+
}
|
|
26080
|
+
async function withPinnedLocalDirectory(profile, parent, name, action) {
|
|
26081
|
+
const path = join10(parent, name);
|
|
26082
|
+
const before = await lstat5(path, { bigint: true }).catch((error) => {
|
|
26083
|
+
if (error.code === "ENOENT") return null;
|
|
26084
|
+
throw error;
|
|
26085
|
+
});
|
|
26086
|
+
if (!before) return null;
|
|
26087
|
+
if (before.isSymbolicLink() || !before.isDirectory()) throw localPathChanged(profile);
|
|
26088
|
+
const handle = await open5(path, fsConstants3.O_RDONLY | fsConstants3.O_DIRECTORY | fsConstants3.O_NOFOLLOW).catch((error) => {
|
|
26089
|
+
if (error.code === "ELOOP" || error.code === "ENOTDIR" || error.code === "ENOENT") throw localPathChanged(profile);
|
|
26090
|
+
throw error;
|
|
26091
|
+
});
|
|
26092
|
+
try {
|
|
26093
|
+
const opened = await handle.stat({ bigint: true });
|
|
26094
|
+
if (!opened.isDirectory() || !sameLocalEntry(before, opened)) throw localPathChanged(profile);
|
|
26095
|
+
const dev = localFileId(opened.dev);
|
|
26096
|
+
const ino = localFileId(opened.ino);
|
|
26097
|
+
if (dev === null || ino === null || dev > BigInt(Number.MAX_SAFE_INTEGER) || ino > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
26098
|
+
throw localPathChanged(profile);
|
|
26099
|
+
}
|
|
26100
|
+
const pinned = process.platform === "linux" ? `/proc/self/fd/${handle.fd}` : path;
|
|
26101
|
+
const recheck = () => assertNamedLocalEntry(profile, path, opened, "directory");
|
|
26102
|
+
const result = await action(pinned, path, { dev: Number(dev), ino: Number(ino) }, recheck);
|
|
26103
|
+
await recheck();
|
|
26104
|
+
return result;
|
|
26105
|
+
} finally {
|
|
26106
|
+
await handle.close();
|
|
26107
|
+
}
|
|
26108
|
+
}
|
|
26109
|
+
async function readReportFile(profile, parent, name) {
|
|
26110
|
+
const path = join10(parent, name);
|
|
26111
|
+
const before = await lstat5(path, { bigint: true }).catch((error) => {
|
|
26112
|
+
if (error.code === "ENOENT") return null;
|
|
26113
|
+
throw error;
|
|
26114
|
+
});
|
|
26115
|
+
if (!before) return null;
|
|
26116
|
+
if (before.isSymbolicLink() || !before.isFile()) throw localPathChanged(profile);
|
|
26117
|
+
const handle = await open5(path, fsConstants3.O_RDONLY | fsConstants3.O_NOFOLLOW).catch((error) => {
|
|
26118
|
+
if (error.code === "ELOOP" || error.code === "ENOTDIR" || error.code === "ENOENT") throw localPathChanged(profile);
|
|
26119
|
+
throw error;
|
|
26120
|
+
});
|
|
26121
|
+
try {
|
|
26122
|
+
const opened = await handle.stat({ bigint: true });
|
|
26123
|
+
if (!opened.isFile() || !sameLocalEntry(before, opened)) throw localPathChanged(profile);
|
|
26124
|
+
const data = await handle.readFile("utf8");
|
|
26125
|
+
await assertNamedLocalEntry(profile, path, opened, "file");
|
|
26126
|
+
return data;
|
|
26127
|
+
} finally {
|
|
26128
|
+
await handle.close();
|
|
26129
|
+
}
|
|
26130
|
+
}
|
|
26131
|
+
async function readReportJson(profile, parent, name) {
|
|
26132
|
+
const data = await readReportFile(profile, parent, name);
|
|
26133
|
+
if (data === null) return null;
|
|
26134
|
+
try {
|
|
26135
|
+
return JSON.parse(data);
|
|
26136
|
+
} catch {
|
|
26137
|
+
return null;
|
|
26138
|
+
}
|
|
26139
|
+
}
|
|
26140
|
+
async function readReplicationEvents(profile, root, sinceMs) {
|
|
25430
26141
|
const events = [];
|
|
25431
26142
|
for (const name of ["events.jsonl.1", "events.jsonl"]) {
|
|
25432
|
-
const data = await
|
|
25433
|
-
if (error.code === "ENOENT") return "";
|
|
25434
|
-
throw error;
|
|
25435
|
-
});
|
|
26143
|
+
const data = await readReportFile(profile, root, name) ?? "";
|
|
25436
26144
|
for (const line of data.split("\n")) {
|
|
25437
26145
|
if (!line) continue;
|
|
25438
26146
|
try {
|
|
@@ -25444,27 +26152,26 @@ async function readReplicationEvents(profile, sinceMs) {
|
|
|
25444
26152
|
}
|
|
25445
26153
|
return events;
|
|
25446
26154
|
}
|
|
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
|
-
});
|
|
26155
|
+
async function replicationPartitions(profile, root) {
|
|
26156
|
+
const entries = await readdir5(root, { withFileTypes: true });
|
|
25453
26157
|
const partitions = [];
|
|
25454
26158
|
for (const entry of entries) {
|
|
25455
|
-
if (
|
|
25456
|
-
|
|
25457
|
-
|
|
25458
|
-
|
|
25459
|
-
|
|
25460
|
-
|
|
25461
|
-
|
|
25462
|
-
|
|
25463
|
-
|
|
25464
|
-
|
|
25465
|
-
|
|
25466
|
-
|
|
26159
|
+
if (!/^[a-z2-7]{26}$/.test(entry.name)) continue;
|
|
26160
|
+
if (entry.isSymbolicLink()) throw localPathChanged(profile);
|
|
26161
|
+
if (!entry.isDirectory()) continue;
|
|
26162
|
+
const partition = await withPinnedLocalDirectory(profile, root, entry.name, async (pinned) => {
|
|
26163
|
+
const identity = await readReportJson(profile, pinned, "identity.json");
|
|
26164
|
+
const pending = await readReportJson(profile, pinned, "pending.json");
|
|
26165
|
+
const pinnedCount = pending?.records?.filter((record2) => record2.state === "in_flight" || record2.state === "ambiguous").length ?? 0;
|
|
26166
|
+
return {
|
|
26167
|
+
idHash: entry.name,
|
|
26168
|
+
host: typeof identity?.host === "string" ? identity.host : null,
|
|
26169
|
+
space: typeof identity?.space === "string" ? identity.space : null,
|
|
26170
|
+
pinned: pinnedCount
|
|
26171
|
+
};
|
|
25467
26172
|
});
|
|
26173
|
+
if (!partition) throw localPathChanged(profile);
|
|
26174
|
+
partitions.push(partition);
|
|
25468
26175
|
}
|
|
25469
26176
|
return partitions.sort((left, right) => left.idHash.localeCompare(right.idHash));
|
|
25470
26177
|
}
|
|
@@ -25483,52 +26190,41 @@ SECRETS_OPT_IN_REQUIRED; 3 grant missing; 4 key absent, deleted, content missing
|
|
|
25483
26190
|
or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
|
|
25484
26191
|
7 node, protocol or integrity error; 10 storage full.`
|
|
25485
26192
|
);
|
|
25486
|
-
replica.command("report").description("
|
|
26193
|
+
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
26194
|
(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
26195
|
const profile = await profileName(cmd);
|
|
26196
|
+
if (options.purge) {
|
|
26197
|
+
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);
|
|
26198
|
+
}
|
|
26199
|
+
if (options.clearPending) {
|
|
26200
|
+
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);
|
|
26201
|
+
}
|
|
25492
26202
|
let durationMs;
|
|
25493
26203
|
try {
|
|
25494
26204
|
durationMs = parseDuration(options.since);
|
|
25495
26205
|
} catch (error) {
|
|
25496
26206
|
throw new CLIError("USAGE_ERROR", error instanceof Error ? error.message : String(error), ExitCode.USAGE_ERROR);
|
|
25497
26207
|
}
|
|
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)}
|
|
26208
|
+
const report = await withPinnedProfileDirectory3(profile, async (directory) => {
|
|
26209
|
+
if (directory === null) throw new CLIError("PROFILE_NOT_FOUND", `Profile "${profile}" does not exist.`, ExitCode.ERROR);
|
|
26210
|
+
const readPersistedReport = async (root) => {
|
|
26211
|
+
await replicaNames(profile);
|
|
26212
|
+
const sinceMs = Date.now() - durationMs;
|
|
26213
|
+
const persisted = await withPinnedLocalDirectory(profile, root, "replication", async (directory2) => ({
|
|
26214
|
+
events: await readReplicationEvents(profile, directory2, sinceMs),
|
|
26215
|
+
partitions: await replicationPartitions(profile, directory2)
|
|
26216
|
+
}));
|
|
26217
|
+
return createReplicationReport(options.since, persisted?.events ?? [], persisted?.partitions ?? []);
|
|
26218
|
+
};
|
|
26219
|
+
if (process.platform === "linux") return readPersistedReport(directory);
|
|
26220
|
+
const checked = await withPinnedLocalDirectory(profile, dirname6(directory), basename3(directory), readPersistedReport);
|
|
26221
|
+
if (checked === null) throw localPathChanged(profile);
|
|
26222
|
+
return checked;
|
|
26223
|
+
});
|
|
26224
|
+
if (shouldOutputJson()) outputJson(report);
|
|
26225
|
+
else process.stdout.write(`${renderReplicationReport(report)}
|
|
25529
26226
|
`);
|
|
25530
|
-
|
|
25531
|
-
})
|
|
26227
|
+
}, false)
|
|
25532
26228
|
);
|
|
25533
26229
|
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
26230
|
(options, cmd) => run(async () => {
|
|
@@ -25540,87 +26236,98 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
|
|
|
25540
26236
|
throw new CLIError("INVALID_REPLICATION_PREFIX", error instanceof Error ? error.message : String(error), ExitCode.USAGE_ERROR);
|
|
25541
26237
|
}
|
|
25542
26238
|
}
|
|
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
|
-
|
|
26239
|
+
await withPinnedProfileDirectory3(profile, async (directory) => {
|
|
26240
|
+
if (directory === null) throw new CLIError("PROFILE_NOT_FOUND", `Profile "${profile}" does not exist.`, ExitCode.ERROR);
|
|
26241
|
+
const globals = cmd.optsWithGlobals();
|
|
26242
|
+
const config = await ProfileManager.getProfile(profile);
|
|
26243
|
+
const deviceDid = config.sessionDid ?? config.did;
|
|
26244
|
+
const jwk = await ProfileManager.getKey(profile);
|
|
26245
|
+
if (jwk === null) {
|
|
26246
|
+
throw new CLIError("AUTH_REQUIRED", `Profile "${profile}" has no device key.`, ExitCode.AUTH_REQUIRED);
|
|
26247
|
+
}
|
|
26248
|
+
if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
|
|
26249
|
+
throw new CLIError("USAGE_ERROR", "--limit must be an integer from 1 through 1000.", ExitCode.USAGE_ERROR);
|
|
26250
|
+
}
|
|
26251
|
+
const existing = await replicaNames(profile);
|
|
26252
|
+
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);
|
|
26253
|
+
const grants = await storedGrants(profile);
|
|
26254
|
+
const initialConfig = existing.includes(name) ? null : (() => {
|
|
26255
|
+
const prefix = options.prefix;
|
|
26256
|
+
if (prefix === void 0) {
|
|
26257
|
+
throw new CLIError("USAGE_ERROR", "The first sync of a replica needs --prefix (and a device grant covering it).", ExitCode.USAGE_ERROR);
|
|
26258
|
+
}
|
|
26259
|
+
const space = resolveSpace(options.space, prefix, grants);
|
|
26260
|
+
if (requiresSecretsOptIn(space, prefix) && options.allowSecrets !== true) {
|
|
26261
|
+
throw new ReplicaError(
|
|
26262
|
+
ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
|
|
26263
|
+
`Replicating ${space}/kv/${prefix} copies encrypted secret material to this disk; pass --allow-secrets to opt in.`
|
|
26264
|
+
);
|
|
26265
|
+
}
|
|
26266
|
+
const host = (globals.host ?? process.env.TC_HOST ?? config.host)?.replace(/\/+$/, "");
|
|
26267
|
+
if (!host) throw new CLIError("USAGE_ERROR", "Pass --host: the profile has no host to pin.", ExitCode.USAGE_ERROR);
|
|
26268
|
+
chooseGrant(grants, deviceDid, space, prefix);
|
|
26269
|
+
return {
|
|
26270
|
+
name,
|
|
26271
|
+
replicaId: randomBytes6(16).toString("hex"),
|
|
26272
|
+
host,
|
|
26273
|
+
space,
|
|
26274
|
+
prefix,
|
|
26275
|
+
deviceDid,
|
|
26276
|
+
allowSecrets: options.allowSecrets === true,
|
|
26277
|
+
localReadPolicy: "whileGrantValid",
|
|
26278
|
+
retentionGrantCid: null
|
|
26279
|
+
};
|
|
26280
|
+
})();
|
|
26281
|
+
const { replica: replica2, report, status, warning } = await withReplicaDirectory(profile, name, async (path, expectedDirectory) => {
|
|
26282
|
+
const store = await SqliteReplicaStore.open(path, {
|
|
26283
|
+
create: initialConfig !== null,
|
|
26284
|
+
guard: profileGuard(profile),
|
|
26285
|
+
expectedDirectory
|
|
26286
|
+
});
|
|
26287
|
+
try {
|
|
26288
|
+
if (initialConfig !== null && await store.open() === null) await store.init(initialConfig);
|
|
26289
|
+
const state2 = await store.open();
|
|
26290
|
+
if (initialConfig === null) {
|
|
26291
|
+
const differs = syncOptionsDiffer(state2.config, {
|
|
26292
|
+
...options.space === void 0 ? {} : { space: options.space },
|
|
26293
|
+
...options.prefix === void 0 ? {} : { prefix: options.prefix },
|
|
26294
|
+
...globals.host === void 0 ? {} : { host: globals.host }
|
|
26295
|
+
});
|
|
26296
|
+
if (differs !== void 0) {
|
|
26297
|
+
throw new ReplicaError(
|
|
26298
|
+
ReplicaErrorCode.CONFIG_MISMATCH,
|
|
26299
|
+
`Replica ${name} already follows ${differs}. Use another --replica name, or ${CLI_NAME22} replica reset --replica ${name} --purge first.`
|
|
26300
|
+
);
|
|
26301
|
+
}
|
|
26302
|
+
}
|
|
26303
|
+
const { space, prefix, host } = state2.config;
|
|
26304
|
+
if (requiresSecretsOptIn(space, prefix) && !state2.config.allowSecrets && options.allowSecrets !== true) {
|
|
26305
|
+
throw new ReplicaError(ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED, `Replicating ${space}/kv/${prefix} needs --allow-secrets.`);
|
|
26306
|
+
}
|
|
26307
|
+
let grant;
|
|
26308
|
+
try {
|
|
26309
|
+
grant = chooseGrant(grants, state2.config.deviceDid, space, prefix);
|
|
26310
|
+
} catch (error) {
|
|
26311
|
+
if (!isReplicaError(error, ReplicaErrorCode.GRANT_MISSING) || (state2.grant ?? state2.pendingGrant) === null) throw error;
|
|
26312
|
+
}
|
|
26313
|
+
if (grant !== void 0) await store.installGrant(grant);
|
|
26314
|
+
if (options.retentionGrant !== void 0 && options.retentionGrant !== state2.config.retentionGrantCid) {
|
|
26315
|
+
await store.setRetentionGrant(options.retentionGrant === "" ? null : options.retentionGrant);
|
|
26316
|
+
}
|
|
26317
|
+
const current = await store.open();
|
|
26318
|
+
const transportFor = await grantTransports({ host, space, deviceDid: current.config.deviceDid, jwk });
|
|
26319
|
+
const warning2 = replicaSecretsWarning(space, prefix, options.allowSecrets === true || state2.config.allowSecrets);
|
|
26320
|
+
const report2 = await new Replica({ store, transportFor }).sync(options.limit === void 0 ? {} : { limit: options.limit });
|
|
26321
|
+
const status2 = await store.status();
|
|
26322
|
+
return { replica: current.config.name, report: report2, status: status2, warning: warning2 };
|
|
26323
|
+
} finally {
|
|
26324
|
+
await store.close();
|
|
26325
|
+
}
|
|
26326
|
+
}, initialConfig !== null);
|
|
25618
26327
|
if (warning !== void 0) process.stderr.write(`Warning: ${warning}
|
|
25619
26328
|
`);
|
|
25620
|
-
const report = await new Replica({ store, transportFor }).sync(options.limit === void 0 ? {} : { limit: options.limit });
|
|
25621
|
-
const status = await store.status();
|
|
25622
26329
|
if (shouldOutputJson()) {
|
|
25623
|
-
outputJson({ replica:
|
|
26330
|
+
outputJson({ replica: replica2, sync: report, status, ...warning === void 0 ? {} : { warning } });
|
|
25624
26331
|
} else {
|
|
25625
26332
|
process.stdout.write(
|
|
25626
26333
|
`${theme.success("\u2713")} Synced ${report.changes} change(s) in ${report.pages} page(s), fetched ${report.fetched} value(s).
|
|
@@ -25628,40 +26335,39 @@ ${describeStatus(status)}
|
|
|
25628
26335
|
`
|
|
25629
26336
|
);
|
|
25630
26337
|
}
|
|
25631
|
-
}
|
|
25632
|
-
await store.close();
|
|
25633
|
-
}
|
|
26338
|
+
});
|
|
25634
26339
|
})
|
|
25635
26340
|
);
|
|
25636
26341
|
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
26342
|
(key, options, cmd) => run(async () => {
|
|
25638
26343
|
const profile = await profileName(cmd);
|
|
25639
|
-
|
|
25640
|
-
|
|
25641
|
-
|
|
25642
|
-
|
|
25643
|
-
|
|
25644
|
-
|
|
25645
|
-
|
|
25646
|
-
|
|
25647
|
-
|
|
25648
|
-
|
|
25649
|
-
|
|
25650
|
-
|
|
25651
|
-
|
|
25652
|
-
|
|
25653
|
-
|
|
25654
|
-
|
|
25655
|
-
|
|
26344
|
+
await withLegacyReplicaRead(profile, async (checkDelivery) => {
|
|
26345
|
+
const result = await withOpenReplica(
|
|
26346
|
+
profile,
|
|
26347
|
+
await existingReplicaName(profile, options.replica),
|
|
26348
|
+
(store) => new Replica({ store }).get(key, { verify: options.verify !== false })
|
|
26349
|
+
);
|
|
26350
|
+
await checkDelivery();
|
|
26351
|
+
if (result.status !== "present") {
|
|
26352
|
+
const failure = READ_FAILURE[result.status];
|
|
26353
|
+
throw new CLIError(failure.code, `Key ${JSON.stringify(key)} ${failure.what}.`, failure.exit);
|
|
26354
|
+
}
|
|
26355
|
+
if (options.output) {
|
|
26356
|
+
await writeFileAtomic(options.output, result.value);
|
|
26357
|
+
outputJson({ key, written: options.output, etag: result.etag, meta: result.meta });
|
|
26358
|
+
return;
|
|
26359
|
+
}
|
|
26360
|
+
if (options.raw) {
|
|
26361
|
+
process.stdout.write(result.value);
|
|
26362
|
+
return;
|
|
26363
|
+
}
|
|
26364
|
+
if (shouldOutputJson()) {
|
|
26365
|
+
outputJson({ key, status: result.status, ...jsonValue(result.value), etag: result.etag, metadata: result.metadata, meta: result.meta });
|
|
26366
|
+
return;
|
|
26367
|
+
}
|
|
25656
26368
|
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");
|
|
26369
|
+
process.stdout.write("\n");
|
|
26370
|
+
});
|
|
25665
26371
|
})
|
|
25666
26372
|
);
|
|
25667
26373
|
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 +26376,17 @@ ${describeStatus(status)}
|
|
|
25670
26376
|
throw new CLIError("USAGE_ERROR", "--limit must be a positive integer.", ExitCode.USAGE_ERROR);
|
|
25671
26377
|
}
|
|
25672
26378
|
const profile = await profileName(cmd);
|
|
25673
|
-
|
|
25674
|
-
|
|
25675
|
-
|
|
25676
|
-
|
|
25677
|
-
|
|
25678
|
-
|
|
25679
|
-
|
|
26379
|
+
await withLegacyReplicaRead(profile, async (checkDelivery) => {
|
|
26380
|
+
const { entries, meta } = await withOpenReplica(
|
|
26381
|
+
profile,
|
|
26382
|
+
await existingReplicaName(profile, options.replica),
|
|
26383
|
+
(store) => new Replica({ store }).list({
|
|
26384
|
+
...prefix === void 0 ? {} : { prefix },
|
|
26385
|
+
...options.after === void 0 ? {} : { after: options.after },
|
|
26386
|
+
...options.limit === void 0 ? {} : { limit: options.limit }
|
|
26387
|
+
})
|
|
26388
|
+
);
|
|
26389
|
+
await checkDelivery();
|
|
25680
26390
|
if (shouldOutputJson()) {
|
|
25681
26391
|
outputJson({ keys: entries, count: entries.length, prefix: prefix ?? null, meta });
|
|
25682
26392
|
} else if (entries.length === 0) {
|
|
@@ -25689,50 +26399,57 @@ ${describeStatus(status)}
|
|
|
25689
26399
|
) + "\n"
|
|
25690
26400
|
);
|
|
25691
26401
|
}
|
|
25692
|
-
}
|
|
25693
|
-
await store.close();
|
|
25694
|
-
}
|
|
26402
|
+
});
|
|
25695
26403
|
})
|
|
25696
26404
|
);
|
|
25697
26405
|
replica.command("status").description("Show one replica's sync state, authority and counts, or all of them").option("--replica <name>", "Replica name").action(
|
|
25698
26406
|
(options, cmd) => run(async () => {
|
|
25699
26407
|
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();
|
|
26408
|
+
await withLegacyReplicaRead(profile, async (checkDelivery) => {
|
|
26409
|
+
const names = options.replica === void 0 ? await replicaNames(profile) : [assertName(options.replica)];
|
|
26410
|
+
const statuses = [];
|
|
26411
|
+
for (const name of names) {
|
|
26412
|
+
statuses.push(await withOpenReplica(profile, name, (store) => new Replica({ store }).status()));
|
|
25708
26413
|
}
|
|
25709
|
-
|
|
25710
|
-
|
|
25711
|
-
|
|
25712
|
-
|
|
25713
|
-
|
|
25714
|
-
|
|
25715
|
-
|
|
25716
|
-
|
|
26414
|
+
await checkDelivery();
|
|
26415
|
+
if (shouldOutputJson()) {
|
|
26416
|
+
outputJson(options.replica === void 0 ? { replicas: statuses } : statuses[0]);
|
|
26417
|
+
} else if (statuses.length === 0) {
|
|
26418
|
+
process.stdout.write(theme.muted(`Profile "${profile}" has no replicas.`) + "\n");
|
|
26419
|
+
} else {
|
|
26420
|
+
process.stdout.write(statuses.map(describeStatus).join("\n\n") + "\n");
|
|
26421
|
+
}
|
|
26422
|
+
});
|
|
25717
26423
|
})
|
|
25718
26424
|
);
|
|
25719
26425
|
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
26426
|
(options, cmd) => run(async () => {
|
|
25721
26427
|
const profile = await profileName(cmd);
|
|
25722
|
-
|
|
25723
|
-
|
|
25724
|
-
try {
|
|
26428
|
+
await withProfileLock5(profile, async () => {
|
|
26429
|
+
const name = await existingReplicaName(profile, options.replica);
|
|
25725
26430
|
if (options.purge) {
|
|
25726
|
-
|
|
25727
|
-
|
|
25728
|
-
|
|
26431
|
+
await withPinnedReplicaForReset(profile, name, async (path, recheck, expectedDirectory) => {
|
|
26432
|
+
const profileLock = profileGuard(profile);
|
|
26433
|
+
const safeGuard = (section) => profileLock(async () => {
|
|
26434
|
+
await recheck();
|
|
26435
|
+
return section();
|
|
26436
|
+
});
|
|
26437
|
+
const store = await SqliteReplicaStore.open(path, { create: false, guard: safeGuard, expectedDirectory });
|
|
26438
|
+
try {
|
|
26439
|
+
await recheck();
|
|
26440
|
+
const lease = await store.acquireSyncLease(6e4);
|
|
26441
|
+
if (lease === null) throw new ReplicaError(ReplicaErrorCode.BUSY, "Another process is syncing this replica.");
|
|
26442
|
+
await recheck();
|
|
26443
|
+
await store.destroy(lease);
|
|
26444
|
+
} finally {
|
|
26445
|
+
await store.close();
|
|
26446
|
+
}
|
|
26447
|
+
});
|
|
25729
26448
|
} else {
|
|
25730
|
-
await new Replica({ store }).reset("manual");
|
|
26449
|
+
await withOpenReplica(profile, name, (store) => new Replica({ store }).reset("manual"));
|
|
25731
26450
|
}
|
|
25732
|
-
|
|
25733
|
-
|
|
25734
|
-
}
|
|
25735
|
-
outputJson({ replica: name, reset: true, purged: options.purge === true });
|
|
26451
|
+
outputJson({ replica: name, reset: true, purged: options.purge === true });
|
|
26452
|
+
}, { requireProfile: true });
|
|
25736
26453
|
})
|
|
25737
26454
|
);
|
|
25738
26455
|
}
|
|
@@ -25745,7 +26462,7 @@ init_errors();
|
|
|
25745
26462
|
init_warnings();
|
|
25746
26463
|
init_constants();
|
|
25747
26464
|
import { CLI_NAME as CLI_NAME24 } from "@tinycloud/operations/cli-name";
|
|
25748
|
-
import { readFile as
|
|
26465
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
25749
26466
|
import { join as join11 } from "path";
|
|
25750
26467
|
import { homedir, hostname, userInfo } from "os";
|
|
25751
26468
|
import { appSecretScope } from "@tinycloud/sdk-core";
|
|
@@ -25774,24 +26491,24 @@ init_space();
|
|
|
25774
26491
|
// src/lib/browser.ts
|
|
25775
26492
|
init_formatter();
|
|
25776
26493
|
init_theme();
|
|
25777
|
-
import
|
|
26494
|
+
import open6 from "open";
|
|
25778
26495
|
var LAUNCH_CHECK_MS = 1e3;
|
|
25779
26496
|
async function tryOpenBrowser(url) {
|
|
25780
26497
|
let launcher;
|
|
25781
26498
|
try {
|
|
25782
|
-
launcher = await
|
|
26499
|
+
launcher = await open6(url);
|
|
25783
26500
|
} catch {
|
|
25784
26501
|
return false;
|
|
25785
26502
|
}
|
|
25786
|
-
return new Promise((
|
|
25787
|
-
const timer = setTimeout(() =>
|
|
26503
|
+
return new Promise((resolve6) => {
|
|
26504
|
+
const timer = setTimeout(() => resolve6(true), LAUNCH_CHECK_MS);
|
|
25788
26505
|
launcher.once("error", () => {
|
|
25789
26506
|
clearTimeout(timer);
|
|
25790
|
-
|
|
26507
|
+
resolve6(false);
|
|
25791
26508
|
});
|
|
25792
26509
|
launcher.once("exit", (code3) => {
|
|
25793
26510
|
clearTimeout(timer);
|
|
25794
|
-
|
|
26511
|
+
resolve6(code3 === 0);
|
|
25795
26512
|
});
|
|
25796
26513
|
});
|
|
25797
26514
|
}
|
|
@@ -25835,9 +26552,9 @@ function runProbeChild(request, options = {}) {
|
|
|
25835
26552
|
const killGraceMs = options.killGraceMs ?? KILL_GRACE_MS;
|
|
25836
26553
|
const spawn2 = options.spawn ?? nodeSpawn;
|
|
25837
26554
|
const signal = options.signal;
|
|
25838
|
-
return new Promise((
|
|
26555
|
+
return new Promise((resolve6) => {
|
|
25839
26556
|
if (signal?.aborted) {
|
|
25840
|
-
|
|
26557
|
+
resolve6({ kind: "cancelled" });
|
|
25841
26558
|
return;
|
|
25842
26559
|
}
|
|
25843
26560
|
const child = spawn2(command, args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -25873,7 +26590,7 @@ function runProbeChild(request, options = {}) {
|
|
|
25873
26590
|
const envelope = stdout.length > 0 && stdout.length <= MAX_PROBE_LINE_BYTES ? parseStatusEnvelope(stdout.toString("utf8")) : void 0;
|
|
25874
26591
|
outcome = envelope !== void 0 ? { kind: "ok", envelope } : probeFailed(stderr.toString("utf8"));
|
|
25875
26592
|
}
|
|
25876
|
-
|
|
26593
|
+
resolve6(outcome);
|
|
25877
26594
|
};
|
|
25878
26595
|
if (child.pid !== void 0) options.onSpawn?.(child.pid);
|
|
25879
26596
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -25965,15 +26682,15 @@ var TIMEOUT_RE = /^(0|\d+s|\d+m)$/;
|
|
|
25965
26682
|
var RETRYABLE_STATUS_ERRORS = { NODE_UNREACHABLE: true, NODE_ERROR: true };
|
|
25966
26683
|
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
26684
|
function abortableSleep(ms2, signal) {
|
|
25968
|
-
return new Promise((
|
|
26685
|
+
return new Promise((resolve6) => {
|
|
25969
26686
|
if (signal.aborted) {
|
|
25970
|
-
|
|
26687
|
+
resolve6();
|
|
25971
26688
|
return;
|
|
25972
26689
|
}
|
|
25973
26690
|
const done = () => {
|
|
25974
26691
|
clearTimeout(timer);
|
|
25975
26692
|
signal.removeEventListener("abort", done);
|
|
25976
|
-
|
|
26693
|
+
resolve6();
|
|
25977
26694
|
};
|
|
25978
26695
|
const timer = setTimeout(done, ms2);
|
|
25979
26696
|
signal.addEventListener("abort", done, { once: true });
|
|
@@ -26639,7 +27356,7 @@ function permissionsFromDelegation2(delegation) {
|
|
|
26639
27356
|
}
|
|
26640
27357
|
async function loadDelegationCandidates(source) {
|
|
26641
27358
|
try {
|
|
26642
|
-
const raw = JSON.parse(await
|
|
27359
|
+
const raw = JSON.parse(await readFile9(source, "utf8"));
|
|
26643
27360
|
return normalizeDelegationCandidates(raw, source);
|
|
26644
27361
|
} catch (error) {
|
|
26645
27362
|
if (error?.code !== "ENOENT") {
|
|
@@ -26659,7 +27376,7 @@ async function loadDelegationCandidates(source) {
|
|
|
26659
27376
|
}
|
|
26660
27377
|
try {
|
|
26661
27378
|
const importedPath = join11(resolveProfilesDir(), source, "additional-delegations.json");
|
|
26662
|
-
const raw = JSON.parse(await
|
|
27379
|
+
const raw = JSON.parse(await readFile9(importedPath, "utf8"));
|
|
26663
27380
|
return normalizeDelegationCandidates(raw, source);
|
|
26664
27381
|
} catch (error) {
|
|
26665
27382
|
if (error?.code === "ENOENT") {
|
|
@@ -27166,7 +27883,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
27166
27883
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
27167
27884
|
}
|
|
27168
27885
|
if (options.file) {
|
|
27169
|
-
secretValue = await
|
|
27886
|
+
secretValue = await readFile9(options.file, "utf-8");
|
|
27170
27887
|
} else if (options.stdin) {
|
|
27171
27888
|
secretValue = (await readStdin3()).toString("utf-8");
|
|
27172
27889
|
} else {
|
|
@@ -32753,7 +33470,7 @@ async function importAesKey(key32, usage) {
|
|
|
32753
33470
|
[usage]
|
|
32754
33471
|
);
|
|
32755
33472
|
}
|
|
32756
|
-
async function
|
|
33473
|
+
async function open7(blob, key32) {
|
|
32757
33474
|
if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
|
|
32758
33475
|
throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
|
|
32759
33476
|
}
|
|
@@ -33091,7 +33808,7 @@ async function resolvePolicyShare(link3, options) {
|
|
|
33091
33808
|
options.signal?.throwIfAborted();
|
|
33092
33809
|
let envelope;
|
|
33093
33810
|
try {
|
|
33094
|
-
const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await
|
|
33811
|
+
const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await open7(parsed.ciphertext, parsed.key32));
|
|
33095
33812
|
const value = JSON.parse(encoded);
|
|
33096
33813
|
if (canonicalize2(value) !== encoded) throw new Error("non-canonical envelope");
|
|
33097
33814
|
envelope = shareEnvelopeV3Schema.parse(value);
|
|
@@ -35683,9 +36400,9 @@ function receiveJson(result, path) {
|
|
|
35683
36400
|
|
|
35684
36401
|
// src/share/io.ts
|
|
35685
36402
|
import { constants } from "fs";
|
|
35686
|
-
import { lstat as
|
|
36403
|
+
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
36404
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
35688
|
-
import { basename as basename4, join as join12, resolve as
|
|
36405
|
+
import { basename as basename4, join as join12, resolve as resolve4, sep as sep2 } from "path";
|
|
35689
36406
|
init_errors();
|
|
35690
36407
|
var MAX_SHARE_STDIN_BYTES = 100 * 1024 * 1024;
|
|
35691
36408
|
var MAX_SHARE_URL_BYTES = 64 * 1024;
|
|
@@ -35722,26 +36439,26 @@ function shareFilename(value) {
|
|
|
35722
36439
|
}
|
|
35723
36440
|
}
|
|
35724
36441
|
function shareInputFilename(input, name) {
|
|
35725
|
-
return shareFilename(name ?? (input === "-" ? "stdin.md" : basename4(
|
|
36442
|
+
return shareFilename(name ?? (input === "-" ? "stdin.md" : basename4(resolve4(input))));
|
|
35726
36443
|
}
|
|
35727
36444
|
async function readShareInput(input, name, limit = MAX_SHARE_STDIN_BYTES) {
|
|
35728
36445
|
const filename = shareInputFilename(input, name);
|
|
35729
36446
|
if (input === "-") return { bytes: await readBoundedStdin(limit), filename };
|
|
35730
|
-
const path =
|
|
35731
|
-
const info = await
|
|
36447
|
+
const path = resolve4(input);
|
|
36448
|
+
const info = await stat5(path);
|
|
35732
36449
|
if (!info.isFile() || info.size > limit) throw new Error("MAX_BYTES_EXCEEDED");
|
|
35733
|
-
const bytes = new Uint8Array(await
|
|
36450
|
+
const bytes = new Uint8Array(await readFile10(path));
|
|
35734
36451
|
if (bytes.byteLength > limit) throw new Error("MAX_BYTES_EXCEEDED");
|
|
35735
36452
|
return { bytes, filename };
|
|
35736
36453
|
}
|
|
35737
36454
|
async function assertDirectory(path) {
|
|
35738
|
-
const absolute =
|
|
36455
|
+
const absolute = resolve4(path);
|
|
35739
36456
|
const segments = absolute.split(sep2).filter(Boolean);
|
|
35740
36457
|
let current = absolute.startsWith(sep2) ? sep2 : "";
|
|
35741
36458
|
for (const segment of segments) {
|
|
35742
36459
|
current = current === sep2 ? join12(current, segment) : join12(current, segment);
|
|
35743
36460
|
try {
|
|
35744
|
-
const info = await
|
|
36461
|
+
const info = await lstat6(current);
|
|
35745
36462
|
if (info.isSymbolicLink()) {
|
|
35746
36463
|
const canonical = await realpath2(current);
|
|
35747
36464
|
if (current !== "/tmp" && current !== "/var") throw new Error("OUTPUT_EXISTS");
|
|
@@ -35749,27 +36466,27 @@ async function assertDirectory(path) {
|
|
|
35749
36466
|
} else if (!info.isDirectory()) throw new Error("OUTPUT_EXISTS");
|
|
35750
36467
|
} catch (error) {
|
|
35751
36468
|
if (error.code !== "ENOENT") throw error;
|
|
35752
|
-
await
|
|
35753
|
-
const created = await
|
|
36469
|
+
await mkdir5(current, { mode: 448 });
|
|
36470
|
+
const created = await lstat6(current);
|
|
35754
36471
|
if (created.isSymbolicLink() || !created.isDirectory()) throw new Error("OUTPUT_EXISTS");
|
|
35755
36472
|
}
|
|
35756
36473
|
}
|
|
35757
36474
|
}
|
|
35758
36475
|
async function writeShareOutput(directory, filename, bytes, force) {
|
|
35759
|
-
const outputDirectory =
|
|
36476
|
+
const outputDirectory = resolve4(directory);
|
|
35760
36477
|
await assertDirectory(outputDirectory);
|
|
35761
36478
|
const safeName = safeFilename(filename);
|
|
35762
|
-
const directoryHandle = await
|
|
36479
|
+
const directoryHandle = await open8(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
|
|
35763
36480
|
const stableDirectory = await realpath2(outputDirectory);
|
|
35764
36481
|
const outputPath = join12(stableDirectory, safeName);
|
|
35765
36482
|
const directoryIdentity = await directoryHandle.stat();
|
|
35766
36483
|
const assertStableDirectory = async () => {
|
|
35767
|
-
const current = await
|
|
36484
|
+
const current = await stat5(stableDirectory);
|
|
35768
36485
|
if (current.dev !== directoryIdentity.dev || current.ino !== directoryIdentity.ino) throw new Error("OUTPUT_EXISTS");
|
|
35769
36486
|
};
|
|
35770
36487
|
await assertStableDirectory();
|
|
35771
36488
|
const stagingDirectory = await mkdtemp(join12(stableDirectory, ".tinycloud-share-stage-"));
|
|
35772
|
-
const stagingInfo = await
|
|
36489
|
+
const stagingInfo = await lstat6(stagingDirectory);
|
|
35773
36490
|
if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
|
|
35774
36491
|
const stagingPath = join12(stagingDirectory, `.tinycloud-share-${randomBytes7(16).toString("hex")}.tmp`);
|
|
35775
36492
|
let temporaryPath;
|
|
@@ -35777,13 +36494,13 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
35777
36494
|
try {
|
|
35778
36495
|
await assertStableDirectory();
|
|
35779
36496
|
try {
|
|
35780
|
-
const existing = await
|
|
36497
|
+
const existing = await lstat6(outputPath);
|
|
35781
36498
|
if (existing.isSymbolicLink()) throw new Error("UNSAFE_FILENAME");
|
|
35782
36499
|
} catch (error) {
|
|
35783
36500
|
if (error.code !== "ENOENT") throw error;
|
|
35784
36501
|
}
|
|
35785
36502
|
temporaryPath = stagingPath;
|
|
35786
|
-
handle = await
|
|
36503
|
+
handle = await open8(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
|
|
35787
36504
|
await handle.writeFile(bytes);
|
|
35788
36505
|
await handle.close();
|
|
35789
36506
|
handle = void 0;
|
|
@@ -36233,8 +36950,8 @@ init_theme();
|
|
|
36233
36950
|
import { CLI_NAME as CLI_NAME26 } from "@tinycloud/operations/cli-name";
|
|
36234
36951
|
import { ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE2 } from "@tinycloud/node-sdk";
|
|
36235
36952
|
import { randomBytes as randomBytes8 } from "crypto";
|
|
36236
|
-
import { mkdir as
|
|
36237
|
-
import { dirname as
|
|
36953
|
+
import { mkdir as mkdir6, writeFile as writeFile4 } from "fs/promises";
|
|
36954
|
+
import { dirname as dirname7 } from "path";
|
|
36238
36955
|
function didWithoutFragment2(did) {
|
|
36239
36956
|
const fragment = did.indexOf("#");
|
|
36240
36957
|
return fragment === -1 ? did : did.slice(0, fragment);
|
|
@@ -36390,7 +37107,7 @@ it directly with \`${CLI_NAME26} space host <name>\` (no request needed).
|
|
|
36390
37107
|
}
|
|
36391
37108
|
async function emitHostRequestArtifact(artifact, emitOption) {
|
|
36392
37109
|
if (typeof emitOption === "string" && emitOption.length > 0) {
|
|
36393
|
-
await
|
|
37110
|
+
await mkdir6(dirname7(emitOption), { recursive: true });
|
|
36394
37111
|
await writeFile4(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
36395
37112
|
outputJson({
|
|
36396
37113
|
emitted: true,
|
|
@@ -36416,7 +37133,7 @@ init_host();
|
|
|
36416
37133
|
init_theme();
|
|
36417
37134
|
import { CLI_NAME as CLI_NAME27 } from "@tinycloud/operations/cli-name";
|
|
36418
37135
|
import { writeFile as writeFile5 } from "fs/promises";
|
|
36419
|
-
import { resolve as
|
|
37136
|
+
import { resolve as resolve5 } from "path";
|
|
36420
37137
|
async function dbHandle(node, dbName, spaceInput, profileName2) {
|
|
36421
37138
|
const spaceUri = await resolveSpaceUri(spaceInput, profileName2);
|
|
36422
37139
|
const sql = spaceUri ? node.sqlForSpace(spaceUri) : node.sql;
|
|
@@ -36558,7 +37275,7 @@ Output:
|
|
|
36558
37275
|
}
|
|
36559
37276
|
const blob = result.data;
|
|
36560
37277
|
const buffer = Buffer.from(await blob.arrayBuffer());
|
|
36561
|
-
const outputPath =
|
|
37278
|
+
const outputPath = resolve5(options.output);
|
|
36562
37279
|
await writeFile5(outputPath, buffer);
|
|
36563
37280
|
outputJson({
|
|
36564
37281
|
file: outputPath,
|
|
@@ -37114,7 +37831,7 @@ function registerUpgradeCommand(program) {
|
|
|
37114
37831
|
}
|
|
37115
37832
|
|
|
37116
37833
|
// src/commands/vault.ts
|
|
37117
|
-
import { readFile as
|
|
37834
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
37118
37835
|
init_profiles();
|
|
37119
37836
|
init_formatter();
|
|
37120
37837
|
init_errors();
|
|
@@ -37176,7 +37893,7 @@ function registerVaultCommand(program) {
|
|
|
37176
37893
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
37177
37894
|
}
|
|
37178
37895
|
if (options.file) {
|
|
37179
|
-
putValue = new Uint8Array(await
|
|
37896
|
+
putValue = new Uint8Array(await readFile11(options.file));
|
|
37180
37897
|
} else if (options.stdin) {
|
|
37181
37898
|
putValue = new Uint8Array(await readStdin4());
|
|
37182
37899
|
} else {
|
|
@@ -37291,7 +38008,7 @@ function registerVaultCommand(program) {
|
|
|
37291
38008
|
}
|
|
37292
38009
|
|
|
37293
38010
|
// src/commands/vars.ts
|
|
37294
|
-
import { readFile as
|
|
38011
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
37295
38012
|
init_profiles();
|
|
37296
38013
|
init_formatter();
|
|
37297
38014
|
init_errors();
|
|
@@ -37396,7 +38113,7 @@ function registerVarsCommand(program) {
|
|
|
37396
38113
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
37397
38114
|
}
|
|
37398
38115
|
if (options.file) {
|
|
37399
|
-
varValue = await
|
|
38116
|
+
varValue = await readFile12(options.file, "utf-8");
|
|
37400
38117
|
} else if (options.stdin) {
|
|
37401
38118
|
varValue = (await readStdin5()).toString("utf-8");
|
|
37402
38119
|
} else {
|