@tinycloud/cli 1.2.6-beta.3 → 1.3.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/dist/index.js +49 -28
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +1284 -679
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +6 -6
- 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
|
}
|
|
@@ -8540,6 +8543,8 @@ var init_dist2 = __esm({
|
|
|
8540
8543
|
REPLICATION_SYNC: "replication.sync",
|
|
8541
8544
|
REPLICATION_STATE: "replication.state",
|
|
8542
8545
|
REPLICATION_DIVERGENCE: "replication.divergence",
|
|
8546
|
+
REPLICATION_GRANT: "replication.grant",
|
|
8547
|
+
REPLICATION_LIFECYCLE: "replication.lifecycle",
|
|
8543
8548
|
/** Emitted once, on the first write rejected because storage is full. */
|
|
8544
8549
|
STORAGE_FULL: "storage.full"
|
|
8545
8550
|
};
|
|
@@ -8559,21 +8564,27 @@ var init_dist2 = __esm({
|
|
|
8559
8564
|
"fetched",
|
|
8560
8565
|
"pages",
|
|
8561
8566
|
"lagMs",
|
|
8562
|
-
"pendingCleared"
|
|
8567
|
+
"pendingCleared",
|
|
8568
|
+
"partitions",
|
|
8569
|
+
"remaining",
|
|
8570
|
+
"expiryMs"
|
|
8563
8571
|
]);
|
|
8564
|
-
SAFE_BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["authenticated", "ok", "persisted"]);
|
|
8572
|
+
SAFE_BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["authenticated", "ok", "persisted", "purgePending"]);
|
|
8565
8573
|
SAFE_REPLICATION_FIELDS = {
|
|
8566
8574
|
source: ["replica", "network", "none"],
|
|
8567
|
-
reason: ["hit", "absent", "deleted", "content_missing", "coverage_incomplete", "not_covered", "pending_write", "grant_missing", "grant_expired", "grant_revoked", "grant_not_yet_valid", "stale", "replica_error", "runtime_unsupported", "replica_unavailable", "unsupported_option", "network_cursor", "cursor_restart", "aborted", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED"],
|
|
8575
|
+
reason: ["hit", "absent", "deleted", "content_missing", "coverage_incomplete", "not_covered", "pending_write", "grant_missing", "grant_expired", "grant_revoked", "grant_not_yet_valid", "stale", "replica_error", "runtime_unsupported", "replica_unavailable", "unsupported_option", "network_cursor", "cursor_restart", "aborted", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED", "replica_gated", "sign_out", "account_switch", "consent_expired", "startup"],
|
|
8568
8576
|
op: ["get", "list", "put", "delete", "batchPut"],
|
|
8569
|
-
trigger: ["start", "interval", "stale_read", "manual"],
|
|
8577
|
+
trigger: ["start", "interval", "stale_read", "manual", "peer"],
|
|
8578
|
+
origin: ["peer"],
|
|
8570
8579
|
outcome: ["found", "not_found", "error", "committed", "failed", "ambiguous", "ok", "busy", "aborted"],
|
|
8571
|
-
class: ["offline", "authority", "storage", "node"],
|
|
8572
|
-
state: ["opened", "grant_installed", "grant_missing", "runtime_unsupported", "unavailable", "revoked", "recreated", "purged", "purge_failed", "pending_store_error", "pinned", "pending_cleared", "closed", "idle", "ready"],
|
|
8580
|
+
class: ["offline", "authority", "storage", "node", "lifecycle"],
|
|
8581
|
+
state: ["opened", "grant_installed", "grant_missing", "runtime_unsupported", "unavailable", "revoked", "recreated", "purged", "purge_failed", "pending_store_error", "pinned", "pending_cleared", "closed", "idle", "ready", "gated"],
|
|
8582
|
+
action: ["minted", "renewed", "renewal_refused", "renewal_deferred", "capped", "expired", "consent_deadline_reached", "denied", "deny_stale", "deny_failed", "database_deleted", "marker_set", "marker_cleared", "unguarded", "drained", "erased", "erase_failed", "activation_blocked", "activated", "recovered"],
|
|
8583
|
+
gate: ["not_established", "stamp_stale", "principal_mismatch", "purge_pending", "logout_pending", "gate_unavailable"],
|
|
8573
8584
|
kind: ["value", "missing_local", "extra_local", "keys", "order"],
|
|
8574
8585
|
coverage: ["empty", "bootstrapping", "complete"],
|
|
8575
8586
|
authority: ["valid", "expired", "revoked", "not-yet-valid"],
|
|
8576
|
-
code: ["NETWORK_ERROR", "TIMEOUT", "ABORTED", "REPLICA_BUSY", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED", "INTEGRITY_ERROR", "STORAGE_ERROR", "REPLICA_CLOSED", "RUNTIME_UNSUPPORTED", "GRANT_EXPIRED", "GRANT_REVOKED", "GRANT_NOT_YET_VALID", "GRANT_MISSING", "GRANT_INVALID", "SOURCE_CHANGED", "PROTOCOL_ERROR", "SCOPE_VIOLATION", "NODE_ERROR", "CONTENT_MISMATCH", "STORAGE_FULL"],
|
|
8587
|
+
code: ["NETWORK_ERROR", "TIMEOUT", "ABORTED", "REPLICA_BUSY", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED", "INTEGRITY_ERROR", "STORAGE_ERROR", "REPLICA_CLOSED", "RUNTIME_UNSUPPORTED", "GRANT_EXPIRED", "GRANT_REVOKED", "GRANT_NOT_YET_VALID", "GRANT_MISSING", "GRANT_INVALID", "SOURCE_CHANGED", "PROTOCOL_ERROR", "SCOPE_VIOLATION", "NODE_ERROR", "CONTENT_MISMATCH", "STORAGE_FULL", "REPLICA_GATED", "RESET_REQUIRED", "STORAGE_BUDGET", "CONSENT_EXPIRED"],
|
|
8577
8588
|
pendingState: ["in_flight", "committed", "ambiguous"],
|
|
8578
8589
|
strategy: ["session", "minted", "installed"],
|
|
8579
8590
|
verify: ["match", "diverged", "error", "aborted"]
|
|
@@ -11586,8 +11597,8 @@ var init_dist2 = __esm({
|
|
|
11586
11597
|
if (this.closed) {
|
|
11587
11598
|
return Promise.resolve({ value: void 0, done: true });
|
|
11588
11599
|
}
|
|
11589
|
-
return new Promise((
|
|
11590
|
-
this.waiters.push({ resolve:
|
|
11600
|
+
return new Promise((resolve6, reject) => {
|
|
11601
|
+
this.waiters.push({ resolve: resolve6, reject });
|
|
11591
11602
|
});
|
|
11592
11603
|
}
|
|
11593
11604
|
};
|
|
@@ -11983,7 +11994,7 @@ var init_dist2 = __esm({
|
|
|
11983
11994
|
} finally {
|
|
11984
11995
|
for (const abort of aborts) abort.dispose();
|
|
11985
11996
|
}
|
|
11986
|
-
await new Promise((
|
|
11997
|
+
await new Promise((resolve6) => setTimeout(resolve6, 0));
|
|
11987
11998
|
}
|
|
11988
11999
|
/** The stream the current subscribers need, or undefined when none remain. */
|
|
11989
12000
|
plan() {
|
|
@@ -14354,12 +14365,14 @@ import { resolveSecretsOwner } from "@tinycloud/operations/secret-request";
|
|
|
14354
14365
|
import {
|
|
14355
14366
|
additionalDelegationsPath as sharedAdditionalDelegationsPath,
|
|
14356
14367
|
authRequestsPath as sharedAuthRequestsPath,
|
|
14368
|
+
profilePath,
|
|
14357
14369
|
profileStoreMetadataPath,
|
|
14358
14370
|
readAdditionalDelegations,
|
|
14359
14371
|
readAuthRequests,
|
|
14360
14372
|
refuseWriteToDeletedProfile,
|
|
14361
14373
|
updateProfileStore,
|
|
14362
14374
|
withProfileLock,
|
|
14375
|
+
withPinnedProfileDirectory,
|
|
14363
14376
|
writeJsonAtomic
|
|
14364
14377
|
} from "@tinycloud/operations/state";
|
|
14365
14378
|
function isCompatiblePermissionRequestArtifact(value) {
|
|
@@ -14377,10 +14390,10 @@ function permissionRequestsPath(profile) {
|
|
|
14377
14390
|
return sharedAuthRequestsPath(profile);
|
|
14378
14391
|
}
|
|
14379
14392
|
function grantHistoryPath(profile) {
|
|
14380
|
-
return join3(
|
|
14393
|
+
return join3(profilePath(profile), "auth-grants.jsonl");
|
|
14381
14394
|
}
|
|
14382
14395
|
function localGrantArtifactsPath(profile) {
|
|
14383
|
-
return join3(
|
|
14396
|
+
return join3(profilePath(profile), "auth-grant-artifacts.json");
|
|
14384
14397
|
}
|
|
14385
14398
|
function createPermissionRequestArtifact(params) {
|
|
14386
14399
|
return buildPermissionRequestArtifact({
|
|
@@ -14407,22 +14420,30 @@ async function loadAdditionalDelegations(profile) {
|
|
|
14407
14420
|
return readAdditionalDelegations(profile);
|
|
14408
14421
|
}
|
|
14409
14422
|
async function loadLocalGrantArtifacts(profile) {
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
|
|
14414
|
-
parsed
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
|
|
14420
|
-
(
|
|
14421
|
-
|
|
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
|
+
});
|
|
14422
14443
|
}
|
|
14423
14444
|
async function appendLocalGrantArtifact(profile, delegation) {
|
|
14424
|
-
await ProfileManager.ensureProfileDir(profile);
|
|
14425
14445
|
await withProfileLock(profile, async () => {
|
|
14446
|
+
await ProfileManager.ensureProfileDir(profile);
|
|
14426
14447
|
await refuseWriteToDeletedProfile(profile);
|
|
14427
14448
|
const records = await loadLocalGrantArtifacts(profile);
|
|
14428
14449
|
if (records.some((record2) => record2.delegationCid === delegation.cid)) return;
|
|
@@ -14515,21 +14536,27 @@ async function cliGrantRecord(node, delegation, permissions, host) {
|
|
|
14515
14536
|
);
|
|
14516
14537
|
}
|
|
14517
14538
|
async function appendGrantHistory(profile, entry) {
|
|
14518
|
-
await
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
|
|
14525
|
-
|
|
14526
|
-
|
|
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
|
+
});
|
|
14527
14551
|
}
|
|
14528
14552
|
async function readGrantHistory(profile) {
|
|
14529
|
-
|
|
14530
|
-
|
|
14531
|
-
|
|
14532
|
-
|
|
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
|
+
});
|
|
14533
14560
|
}
|
|
14534
14561
|
async function parseCapSpec(spec, profile) {
|
|
14535
14562
|
const firstColon = spec.indexOf(":");
|
|
@@ -14778,7 +14805,6 @@ var init_permissions = __esm({
|
|
|
14778
14805
|
"src/lib/permissions.ts"() {
|
|
14779
14806
|
"use strict";
|
|
14780
14807
|
init_manifest();
|
|
14781
|
-
init_constants();
|
|
14782
14808
|
init_storage();
|
|
14783
14809
|
init_profiles();
|
|
14784
14810
|
init_errors();
|
|
@@ -14903,7 +14929,14 @@ function createReplicationEventSink(profileRoot, options) {
|
|
|
14903
14929
|
`);
|
|
14904
14930
|
} else if (event.type === "replication.sync") {
|
|
14905
14931
|
const pending = event.pendingCleared === void 0 ? "" : ` pendingCleared:${event.pendingCleared}`;
|
|
14906
|
-
|
|
14932
|
+
const origin = event.origin === void 0 ? "" : ` origin:${event.origin}`;
|
|
14933
|
+
process.stderr.write(`[replication] sync ${event.replica} ${event.outcome}${event.code ? ` ${event.code}` : ""}${pending}${origin}
|
|
14934
|
+
`);
|
|
14935
|
+
} else if (event.type === "replication.grant") {
|
|
14936
|
+
process.stderr.write(`[replication] grant ${event.replicaId} ${event.action}${event.code ? ` ${event.code}` : ""}${event.effectiveExpiresAt ? ` effectiveExpiresAt:${event.effectiveExpiresAt}` : ""}
|
|
14937
|
+
`);
|
|
14938
|
+
} else if (event.type === "replication.lifecycle") {
|
|
14939
|
+
process.stderr.write(`[replication] lifecycle ${event.action}${event.reason ? ` ${event.reason}` : ""}${event.gate ? ` gate:${event.gate}` : ""}${event.code ? ` ${event.code}` : ""}${event.remaining === void 0 ? "" : ` remaining:${event.remaining}`}
|
|
14907
14940
|
`);
|
|
14908
14941
|
}
|
|
14909
14942
|
}
|
|
@@ -17750,7 +17783,7 @@ var init_dist3 = __esm({
|
|
|
17750
17783
|
// src/lib/replication-config.ts
|
|
17751
17784
|
import { join as join5 } from "path";
|
|
17752
17785
|
import { CLI_NAME as CLI_NAME6 } from "@tinycloud/operations/cli-name";
|
|
17753
|
-
import { profilePath, withProfileLock as withProfileLock2 } from "@tinycloud/operations/state";
|
|
17786
|
+
import { profilePath as profilePath2, withProfileLock as withProfileLock2 } from "@tinycloud/operations/state";
|
|
17754
17787
|
import { sqliteReplicaStorage } from "@tinycloud/node-sdk";
|
|
17755
17788
|
function createCliReplicationConfig(profile, settings, output2, enabled) {
|
|
17756
17789
|
if (!enabled || !settings || settings.prefixes.length === 0) return void 0;
|
|
@@ -17768,7 +17801,7 @@ function createCliReplicationConfig(profile, settings, output2, enabled) {
|
|
|
17768
17801
|
);
|
|
17769
17802
|
}
|
|
17770
17803
|
}
|
|
17771
|
-
const root =
|
|
17804
|
+
const root = profilePath2(profile);
|
|
17772
17805
|
return {
|
|
17773
17806
|
enabled: true,
|
|
17774
17807
|
prefixes: settings.prefixes,
|
|
@@ -18254,17 +18287,21 @@ var init_host = __esm({
|
|
|
18254
18287
|
});
|
|
18255
18288
|
|
|
18256
18289
|
// src/config/profiles.ts
|
|
18257
|
-
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";
|
|
18258
18291
|
import { CLI_NAME as CLI_NAME9 } from "@tinycloud/operations/cli-name";
|
|
18259
18292
|
import { join as join6 } from "path";
|
|
18260
18293
|
import {
|
|
18261
|
-
|
|
18294
|
+
assertRealProfileDirectory,
|
|
18295
|
+
ProfileSymlinkError as ProfileSymlinkError2,
|
|
18296
|
+
namedProfilePath,
|
|
18297
|
+
profilePath as profilePath3,
|
|
18262
18298
|
readSession,
|
|
18263
18299
|
recordProfileDeletion,
|
|
18264
18300
|
refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
|
|
18265
18301
|
removeSession,
|
|
18266
18302
|
withProfileLock as withProfileLock3,
|
|
18267
|
-
writeSession
|
|
18303
|
+
writeSession,
|
|
18304
|
+
withPinnedProfileDirectory as withPinnedProfileDirectory2
|
|
18268
18305
|
} from "@tinycloud/operations/state";
|
|
18269
18306
|
var ProfileManager;
|
|
18270
18307
|
var init_profiles = __esm({
|
|
@@ -18306,8 +18343,13 @@ var init_profiles = __esm({
|
|
|
18306
18343
|
}
|
|
18307
18344
|
/** Owner-only profile directory (0700), created or tightened before any write into it. */
|
|
18308
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
|
+
}
|
|
18309
18351
|
await _ProfileManager.ensureConfigDir();
|
|
18310
|
-
|
|
18352
|
+
await assertRealProfileDirectory(name);
|
|
18311
18353
|
await ensureDir(profileDir);
|
|
18312
18354
|
await chmod2(profileDir, PRIVATE_DIR_MODE);
|
|
18313
18355
|
return profileDir;
|
|
@@ -18336,8 +18378,7 @@ var init_profiles = __esm({
|
|
|
18336
18378
|
* Throws CLIError if the profile doesn't exist.
|
|
18337
18379
|
*/
|
|
18338
18380
|
static async getProfile(name) {
|
|
18339
|
-
const
|
|
18340
|
-
const profile = await readJson(profilePath5);
|
|
18381
|
+
const profile = await withPinnedProfileDirectory2(name, (directory) => directory ? readJson(join6(directory, "profile.json")) : Promise.resolve(null));
|
|
18341
18382
|
if (!profile) {
|
|
18342
18383
|
throw new CLIError(
|
|
18343
18384
|
"PROFILE_NOT_FOUND",
|
|
@@ -18357,13 +18398,13 @@ var init_profiles = __esm({
|
|
|
18357
18398
|
}
|
|
18358
18399
|
/** Removes profile.json under the profile lock (rollback of a profile a failed login created). */
|
|
18359
18400
|
static async removeProfileConfig(name) {
|
|
18360
|
-
await _ProfileManager.withLock(name, () => rm2(join6(
|
|
18401
|
+
await _ProfileManager.withLock(name, () => rm2(join6(profilePath3(name), "profile.json"), { force: true }));
|
|
18361
18402
|
}
|
|
18362
18403
|
/**
|
|
18363
18404
|
* Returns true if a profile directory exists.
|
|
18364
18405
|
*/
|
|
18365
18406
|
static async profileExists(name) {
|
|
18366
|
-
return fileExists(join6(
|
|
18407
|
+
return withPinnedProfileDirectory2(name, (directory) => directory ? fileExists(join6(directory, "profile.json")) : Promise.resolve(false));
|
|
18367
18408
|
}
|
|
18368
18409
|
/**
|
|
18369
18410
|
* Returns an array of profile directory names.
|
|
@@ -18387,7 +18428,7 @@ var init_profiles = __esm({
|
|
|
18387
18428
|
static async deleteProfile(name) {
|
|
18388
18429
|
let profileDir;
|
|
18389
18430
|
try {
|
|
18390
|
-
profileDir =
|
|
18431
|
+
profileDir = namedProfilePath(name);
|
|
18391
18432
|
} catch {
|
|
18392
18433
|
throw new CLIError(
|
|
18393
18434
|
"INVALID_PROFILE_NAME",
|
|
@@ -18412,23 +18453,21 @@ var init_profiles = __esm({
|
|
|
18412
18453
|
return;
|
|
18413
18454
|
}
|
|
18414
18455
|
await _ProfileManager.withLock(name, async () => {
|
|
18415
|
-
|
|
18416
|
-
for (const
|
|
18417
|
-
|
|
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 });
|
|
18418
18460
|
}
|
|
18419
|
-
await rm2(join6(
|
|
18461
|
+
await rm2(join6(directory, "profile.json"), { force: true });
|
|
18420
18462
|
await recordProfileDeletion(name);
|
|
18421
18463
|
});
|
|
18422
|
-
await rmdir(profileDir).catch((error) => {
|
|
18423
|
-
if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY" && error.code !== "EEXIST") throw error;
|
|
18424
|
-
});
|
|
18425
18464
|
}
|
|
18426
18465
|
// ── Key management ──────────────────────────────────────────────────
|
|
18427
18466
|
/**
|
|
18428
18467
|
* Returns the parsed JWK for a profile, or null if no key exists.
|
|
18429
18468
|
*/
|
|
18430
18469
|
static async getKey(name) {
|
|
18431
|
-
return readJson(join6(
|
|
18470
|
+
return withPinnedProfileDirectory2(name, (directory) => directory ? readJson(join6(directory, "key.json")) : Promise.resolve(null));
|
|
18432
18471
|
}
|
|
18433
18472
|
/**
|
|
18434
18473
|
* Saves a JWK key under the profile lock (0600, in an owner-only profile
|
|
@@ -18443,7 +18482,7 @@ var init_profiles = __esm({
|
|
|
18443
18482
|
}
|
|
18444
18483
|
/** Removes key.json under the profile lock (rollback of a key a failed login created). */
|
|
18445
18484
|
static async removeKey(name) {
|
|
18446
|
-
await _ProfileManager.withLock(name, () => rm2(join6(
|
|
18485
|
+
await _ProfileManager.withLock(name, () => rm2(join6(profilePath3(name), "key.json"), { force: true }));
|
|
18447
18486
|
}
|
|
18448
18487
|
// ── Session management ──────────────────────────────────────────────
|
|
18449
18488
|
/**
|
|
@@ -18470,10 +18509,13 @@ var init_profiles = __esm({
|
|
|
18470
18509
|
* created or tightened to 0700.
|
|
18471
18510
|
*/
|
|
18472
18511
|
static async getCacheDir(name) {
|
|
18473
|
-
const
|
|
18474
|
-
|
|
18475
|
-
|
|
18476
|
-
|
|
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
|
+
});
|
|
18477
18519
|
}
|
|
18478
18520
|
// ── Resolution helpers ──────────────────────────────────────────────
|
|
18479
18521
|
/**
|
|
@@ -18497,7 +18539,8 @@ var init_profiles = __esm({
|
|
|
18497
18539
|
try {
|
|
18498
18540
|
profileConfig = await _ProfileManager.getProfile(profile);
|
|
18499
18541
|
profileHost = profileConfig.host;
|
|
18500
|
-
} catch {
|
|
18542
|
+
} catch (error) {
|
|
18543
|
+
if (error instanceof ProfileSymlinkError2) throw error;
|
|
18501
18544
|
}
|
|
18502
18545
|
const envReplication = process.env.TC_REPLICATION?.toLowerCase();
|
|
18503
18546
|
const envEnabled = envReplication === "1" || envReplication === "true" || envReplication === "on";
|
|
@@ -18861,22 +18904,23 @@ import { get as httpGet } from "http";
|
|
|
18861
18904
|
import { get as httpsGet } from "https";
|
|
18862
18905
|
import { spawn } from "child_process";
|
|
18863
18906
|
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
|
|
18864
|
-
import { dirname as
|
|
18907
|
+
import { dirname as dirname4 } from "path";
|
|
18865
18908
|
import { createInterface as createInterface2 } from "readline";
|
|
18866
18909
|
import { grantAuthRequest, principalDidEquals, signedCompactUcanExpiry as signedCompactUcanExpiry3 } from "@tinycloud/node-sdk";
|
|
18867
18910
|
import { invokeOperation } from "@tinycloud/operations";
|
|
18868
18911
|
|
|
18869
18912
|
// src/lib/profile-replicas.ts
|
|
18870
18913
|
init_dist3();
|
|
18871
|
-
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";
|
|
18872
18916
|
import { join as join8, relative, sep } from "path";
|
|
18873
|
-
import { ProfileDeletedError as ProfileDeletedError2, profilePath as
|
|
18917
|
+
import { ProfileDeletedError as ProfileDeletedError2, profilePath as profilePath4, withProfileLock as withProfileLock4 } from "@tinycloud/operations/state";
|
|
18874
18918
|
|
|
18875
18919
|
// ../replica/dist/sqlite.js
|
|
18876
18920
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
18877
|
-
import { constants as fsConstants } from "fs";
|
|
18878
|
-
import { chmod as chmod3, link, mkdir as mkdir2, open as open2, readFile as readFile4, readdir as readdir3, rm as rm3, rmdir
|
|
18879
|
-
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";
|
|
18880
18924
|
var ReplicaErrorCode2 = {
|
|
18881
18925
|
/** Another process holds the sync lease. */
|
|
18882
18926
|
BUSY: "REPLICA_BUSY",
|
|
@@ -19063,6 +19107,29 @@ async function loadSqlite() {
|
|
|
19063
19107
|
return (path, options) => wrap(options?.readonly ? new DatabaseSync(path, { readOnly: true }) : new DatabaseSync(path));
|
|
19064
19108
|
}
|
|
19065
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
|
+
}
|
|
19066
19133
|
var DURABILITY = "sqlite-wal-synchronous-full+fsynced-blobs";
|
|
19067
19134
|
var SCHEMA = `
|
|
19068
19135
|
CREATE TABLE IF NOT EXISTS replica (
|
|
@@ -19161,22 +19228,37 @@ async function entriesOf(path) {
|
|
|
19161
19228
|
}
|
|
19162
19229
|
}
|
|
19163
19230
|
var FAULTS = /* @__PURE__ */ Symbol("tinycloud.replica.faults");
|
|
19231
|
+
var pinnedDirectories = new FinalizationRegistry((fd) => {
|
|
19232
|
+
try {
|
|
19233
|
+
closeSync(fd);
|
|
19234
|
+
} catch {
|
|
19235
|
+
}
|
|
19236
|
+
});
|
|
19164
19237
|
var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
19165
19238
|
dir;
|
|
19166
19239
|
#db;
|
|
19240
|
+
#pinnedDir;
|
|
19241
|
+
#directoryFd;
|
|
19167
19242
|
#holder = `${process.pid}-${randomBytes2(6).toString("hex")}`;
|
|
19168
19243
|
#now;
|
|
19169
19244
|
#faults;
|
|
19170
19245
|
#guard;
|
|
19171
19246
|
/** Identity of the database file this store opened: a deleted-and-recreated replica is a different file. */
|
|
19172
19247
|
#ino;
|
|
19173
|
-
|
|
19248
|
+
#dev;
|
|
19249
|
+
#namedAncestors;
|
|
19250
|
+
constructor(dir, db, identity, options, directoryFd, namedAncestors = []) {
|
|
19174
19251
|
this.dir = dir;
|
|
19252
|
+
this.#pinnedDir = directoryFd !== void 0 && process.platform === "linux" ? `/proc/self/fd/${directoryFd}` : dir;
|
|
19253
|
+
this.#directoryFd = directoryFd;
|
|
19254
|
+
if (directoryFd !== void 0) pinnedDirectories.register(this, directoryFd, this);
|
|
19175
19255
|
this.#db = db;
|
|
19176
|
-
this.#ino = ino;
|
|
19256
|
+
this.#ino = identity.ino;
|
|
19257
|
+
this.#dev = identity.dev;
|
|
19177
19258
|
this.#now = options.now ?? Date.now;
|
|
19178
19259
|
this.#faults = options[FAULTS] ?? {};
|
|
19179
19260
|
this.#guard = options.guard ?? ((section) => section());
|
|
19261
|
+
this.#namedAncestors = namedAncestors;
|
|
19180
19262
|
}
|
|
19181
19263
|
/**
|
|
19182
19264
|
* Read-only status snapshot; does not create a replica, migrate, recover
|
|
@@ -19189,9 +19271,10 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19189
19271
|
};
|
|
19190
19272
|
checkAbort();
|
|
19191
19273
|
const dbPath = join7(dir, "replica.db");
|
|
19192
|
-
let
|
|
19274
|
+
let identity;
|
|
19193
19275
|
try {
|
|
19194
|
-
|
|
19276
|
+
identity = await lstat2(dbPath);
|
|
19277
|
+
if (!identity.isFile()) return null;
|
|
19195
19278
|
} catch (error) {
|
|
19196
19279
|
if (errnoOf(error) === "ENOENT") return null;
|
|
19197
19280
|
throw storageError(error, "Inspecting the replica");
|
|
@@ -19204,14 +19287,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19204
19287
|
db = opener(dbPath, { readonly: true });
|
|
19205
19288
|
db.exec("PRAGMA busy_timeout = 0");
|
|
19206
19289
|
checkAbort();
|
|
19207
|
-
const store = new _SqliteReplicaStore(dir, db,
|
|
19290
|
+
const store = new _SqliteReplicaStore(dir, db, identity, { create: false, ...options.now ? { now: options.now } : {} });
|
|
19208
19291
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
19209
19292
|
const before = db.get("PRAGMA data_version").data_version;
|
|
19210
19293
|
const state2 = await store.open();
|
|
19211
19294
|
checkAbort();
|
|
19212
19295
|
if (state2 === null) return null;
|
|
19213
19296
|
const status = await store.status();
|
|
19214
|
-
await new Promise((
|
|
19297
|
+
await new Promise((resolve22) => setImmediate(resolve22));
|
|
19215
19298
|
checkAbort();
|
|
19216
19299
|
const after = db.get("PRAGMA data_version").data_version;
|
|
19217
19300
|
if (before === after) return { state: state2, status };
|
|
@@ -19231,65 +19314,145 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19231
19314
|
* REPLICA_NOT_FOUND and nothing is written to disk.
|
|
19232
19315
|
*/
|
|
19233
19316
|
static async open(dir, options) {
|
|
19234
|
-
const
|
|
19317
|
+
const namedDbPath = join7(dir, "replica.db");
|
|
19235
19318
|
if (!options.create) {
|
|
19236
|
-
const exists = await
|
|
19237
|
-
() =>
|
|
19319
|
+
const exists = await lstat2(namedDbPath).then(
|
|
19320
|
+
(stats) => stats.isFile(),
|
|
19238
19321
|
() => false
|
|
19239
19322
|
);
|
|
19240
|
-
if (!exists) throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, `No replica at ${dir}.`);
|
|
19323
|
+
if (!exists) throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, `No regular replica database at ${dir}.`);
|
|
19241
19324
|
}
|
|
19242
19325
|
const opener = options.sqlite ?? await loadSqlite();
|
|
19326
|
+
const releaseOpen = await acquireSqliteOpen();
|
|
19243
19327
|
let store;
|
|
19328
|
+
let directoryFd;
|
|
19244
19329
|
try {
|
|
19330
|
+
if (options.create) await mkdir2(dir, { recursive: true, mode: 448 });
|
|
19331
|
+
if (process.platform === "linux") {
|
|
19332
|
+
directoryFd = openSync(dir, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW);
|
|
19333
|
+
}
|
|
19334
|
+
const opened = directoryFd === void 0 ? lstatSync(dir, { bigint: true }) : fstatSync(directoryFd, { bigint: true });
|
|
19335
|
+
if (!opened.isDirectory() || opened.isSymbolicLink() || options.expectedDirectory && (opened.dev !== BigInt(options.expectedDirectory.dev) || opened.ino !== BigInt(options.expectedDirectory.ino))) {
|
|
19336
|
+
throw new Error("The replica directory changed before SQLite opened it.");
|
|
19337
|
+
}
|
|
19338
|
+
const namedAncestors = process.platform === "linux" ? [] : await captureNamedPath(dir);
|
|
19339
|
+
if (namedAncestors.length && (namedAncestors[namedAncestors.length - 1].dev !== opened.dev || namedAncestors[namedAncestors.length - 1].ino !== opened.ino)) {
|
|
19340
|
+
throw new Error("The replica directory changed while checking its ancestry.");
|
|
19341
|
+
}
|
|
19342
|
+
const pinnedDir = directoryFd === void 0 ? dir : `/proc/self/fd/${directoryFd}`;
|
|
19343
|
+
const dbPath = join7(pinnedDir, "replica.db");
|
|
19245
19344
|
if (options.create) {
|
|
19246
|
-
await mkdir2(join7(
|
|
19247
|
-
await Promise.all([chmod3(
|
|
19248
|
-
const handle = await open2(dbPath, fsConstants.O_CREAT | fsConstants.O_RDWR, 384)
|
|
19249
|
-
|
|
19250
|
-
|
|
19251
|
-
|
|
19252
|
-
|
|
19253
|
-
|
|
19254
|
-
|
|
19255
|
-
|
|
19256
|
-
|
|
19257
|
-
|
|
19258
|
-
|
|
19259
|
-
const version = db.get("PRAGMA user_version")?.user_version ?? 0;
|
|
19260
|
-
if (version > SCHEMA_VERSION) {
|
|
19261
|
-
db.close();
|
|
19262
|
-
throw new ReplicaError2(
|
|
19263
|
-
ReplicaErrorCode2.STORAGE_ERROR,
|
|
19264
|
-
`The replica at ${dir} was written by a newer tc (schema ${version}); upgrade tc to read it.`
|
|
19265
|
-
);
|
|
19345
|
+
await mkdir2(join7(pinnedDir, "blobs", ".tmp"), { recursive: true, mode: 448 });
|
|
19346
|
+
await Promise.all([chmod3(pinnedDir, 448), chmod3(join7(pinnedDir, "blobs"), 448), chmod3(join7(pinnedDir, "blobs", ".tmp"), 448)]);
|
|
19347
|
+
const handle = await open2(dbPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_RDWR | fsConstants.O_NOFOLLOW, 384).catch((error) => {
|
|
19348
|
+
if (errnoOf(error) === "EEXIST") return null;
|
|
19349
|
+
throw error;
|
|
19350
|
+
});
|
|
19351
|
+
if (handle) {
|
|
19352
|
+
try {
|
|
19353
|
+
await handle.chmod(384);
|
|
19354
|
+
} finally {
|
|
19355
|
+
await handle.close();
|
|
19356
|
+
}
|
|
19357
|
+
}
|
|
19266
19358
|
}
|
|
19267
|
-
|
|
19268
|
-
|
|
19269
|
-
|
|
19270
|
-
|
|
19271
|
-
|
|
19359
|
+
const leaf = await open2(dbPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
19360
|
+
let db;
|
|
19361
|
+
try {
|
|
19362
|
+
const identity = await leaf.stat();
|
|
19363
|
+
const named = await lstat2(dbPath);
|
|
19364
|
+
if (!identity.isFile() || !named.isFile() || named.dev !== identity.dev || named.ino !== identity.ino) {
|
|
19365
|
+
throw new Error("The replica database changed before SQLite opened it.");
|
|
19366
|
+
}
|
|
19367
|
+
const previousFds = process.platform === "linux" && !options.sqlite ? new Map(readdirSync2("/proc/self/fd").map((fd) => {
|
|
19368
|
+
try {
|
|
19369
|
+
return [fd, readlinkSync(`/proc/self/fd/${fd}`)];
|
|
19370
|
+
} catch {
|
|
19371
|
+
return [fd, ""];
|
|
19372
|
+
}
|
|
19373
|
+
})) : null;
|
|
19374
|
+
db = opener(dbPath);
|
|
19375
|
+
const afterOpen = lstatSync(dbPath);
|
|
19376
|
+
if (!afterOpen.isFile() || afterOpen.dev !== identity.dev || afterOpen.ino !== identity.ino) {
|
|
19377
|
+
throw new Error("The replica database changed while SQLite opened it.");
|
|
19378
|
+
}
|
|
19379
|
+
if (previousFds) db.get("SELECT count(*) FROM sqlite_master");
|
|
19380
|
+
if (previousFds) {
|
|
19381
|
+
const databases = readdirSync2("/proc/self/fd").filter((fd) => {
|
|
19382
|
+
try {
|
|
19383
|
+
const target = readlinkSync(`/proc/self/fd/${fd}`);
|
|
19384
|
+
return target.endsWith("/replica.db") && previousFds.get(fd) !== target;
|
|
19385
|
+
} catch {
|
|
19386
|
+
return false;
|
|
19387
|
+
}
|
|
19388
|
+
});
|
|
19389
|
+
if (databases.length === 0 || databases.some((fd) => {
|
|
19390
|
+
try {
|
|
19391
|
+
const actual = fstatSync(Number(fd));
|
|
19392
|
+
return actual.dev !== identity.dev || actual.ino !== identity.ino;
|
|
19393
|
+
} catch {
|
|
19394
|
+
return true;
|
|
19395
|
+
}
|
|
19396
|
+
})) {
|
|
19397
|
+
throw new Error("SQLite opened a different replica database.");
|
|
19398
|
+
}
|
|
19399
|
+
}
|
|
19400
|
+
const after = lstatSync(dbPath);
|
|
19401
|
+
if (!after.isFile() || after.dev !== identity.dev || after.ino !== identity.ino) {
|
|
19402
|
+
throw new Error("The replica database changed while SQLite opened it.");
|
|
19403
|
+
}
|
|
19404
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
19405
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
19406
|
+
db.exec("PRAGMA synchronous = FULL");
|
|
19407
|
+
db.exec("PRAGMA secure_delete = ON");
|
|
19408
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
19409
|
+
const version = db.get("PRAGMA user_version")?.user_version ?? 0;
|
|
19410
|
+
if (version > SCHEMA_VERSION) {
|
|
19411
|
+
throw new ReplicaError2(
|
|
19412
|
+
ReplicaErrorCode2.STORAGE_ERROR,
|
|
19413
|
+
`The replica at ${dir} was written by a newer tc (schema ${version}); upgrade tc to read it.`
|
|
19414
|
+
);
|
|
19415
|
+
}
|
|
19416
|
+
if (version < SCHEMA_VERSION) {
|
|
19417
|
+
db.exec("BEGIN IMMEDIATE");
|
|
19418
|
+
db.exec(SCHEMA);
|
|
19419
|
+
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
19420
|
+
db.exec("COMMIT");
|
|
19421
|
+
}
|
|
19422
|
+
store = new _SqliteReplicaStore(dir, db, identity, options, directoryFd, namedAncestors);
|
|
19423
|
+
db = void 0;
|
|
19424
|
+
} finally {
|
|
19425
|
+
try {
|
|
19426
|
+
db?.close();
|
|
19427
|
+
} finally {
|
|
19428
|
+
await leaf.close();
|
|
19429
|
+
}
|
|
19272
19430
|
}
|
|
19273
|
-
store = new _SqliteReplicaStore(dir, db, ino, options);
|
|
19274
19431
|
} catch (error) {
|
|
19432
|
+
if (store) await store.close().catch(() => void 0);
|
|
19433
|
+
else if (directoryFd !== void 0) closeSync(directoryFd);
|
|
19275
19434
|
throw storageError(error, `Opening the replica at ${dir}`);
|
|
19435
|
+
} finally {
|
|
19436
|
+
releaseOpen();
|
|
19276
19437
|
}
|
|
19277
|
-
|
|
19278
|
-
|
|
19279
|
-
await store.#
|
|
19438
|
+
try {
|
|
19439
|
+
const row = store.#row();
|
|
19440
|
+
if (row?.revoked_detail != null && (row.purge_pending === 1 || await store.#holdsContentFiles().catch(() => true))) {
|
|
19441
|
+
await store.#finishRevocationPurge().catch(() => void 0);
|
|
19442
|
+
}
|
|
19443
|
+
return store;
|
|
19444
|
+
} catch (error) {
|
|
19445
|
+
await store.close().catch(() => void 0);
|
|
19446
|
+
throw error;
|
|
19280
19447
|
}
|
|
19281
|
-
return store;
|
|
19282
19448
|
}
|
|
19283
19449
|
#row() {
|
|
19284
19450
|
return this.#db.get("SELECT * FROM replica WHERE id = 1");
|
|
19285
19451
|
}
|
|
19286
19452
|
/** Refuse to touch the filesystem for a replica that was deleted (or replaced) under us. */
|
|
19287
19453
|
async #assertPresent() {
|
|
19288
|
-
const
|
|
19289
|
-
|
|
19290
|
-
() => void 0
|
|
19291
|
-
);
|
|
19292
|
-
if (ino !== this.#ino) {
|
|
19454
|
+
const identity = await lstat2(join7(this.#pinnedDir, "replica.db")).catch(() => null);
|
|
19455
|
+
if (!identity?.isFile() || identity.dev !== this.#dev || identity.ino !== this.#ino) {
|
|
19293
19456
|
throw new ReplicaError2(
|
|
19294
19457
|
ReplicaErrorCode2.NOT_FOUND,
|
|
19295
19458
|
`The replica at ${this.dir} was deleted while this command ran (its profile was deleted or the replica purged); nothing more was written.`
|
|
@@ -19519,7 +19682,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19519
19682
|
return held;
|
|
19520
19683
|
}
|
|
19521
19684
|
#blobPath(hash) {
|
|
19522
|
-
return join7(this
|
|
19685
|
+
return join7(this.#pinnedDir, "blobs", hash.slice(0, 2), hash);
|
|
19523
19686
|
}
|
|
19524
19687
|
/**
|
|
19525
19688
|
* Make every blob durable before any row references it: write to a fresh
|
|
@@ -19536,11 +19699,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19536
19699
|
() => false
|
|
19537
19700
|
);
|
|
19538
19701
|
if (exists) continue;
|
|
19539
|
-
const shard = join7(this
|
|
19702
|
+
const shard = join7(this.#pinnedDir, "blobs", hash.slice(0, 2));
|
|
19540
19703
|
await mkdir2(shard, { mode: 448 }).catch((error) => {
|
|
19541
19704
|
if (errnoOf(error) !== "EEXIST") throw error;
|
|
19542
19705
|
});
|
|
19543
|
-
const temp = join7(this
|
|
19706
|
+
const temp = join7(this.#pinnedDir, "blobs", ".tmp", `${hash}.${randomBytes2(6).toString("hex")}`);
|
|
19544
19707
|
const handle = await open2(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 384);
|
|
19545
19708
|
try {
|
|
19546
19709
|
await handle.writeFile(bytes);
|
|
@@ -19556,7 +19719,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19556
19719
|
await unlink(temp);
|
|
19557
19720
|
}
|
|
19558
19721
|
dirs.add(shard);
|
|
19559
|
-
dirs.add(join7(this
|
|
19722
|
+
dirs.add(join7(this.#pinnedDir, "blobs"));
|
|
19560
19723
|
}
|
|
19561
19724
|
for (const dir of dirs) {
|
|
19562
19725
|
const handle = await open2(dir, fsConstants.O_RDONLY);
|
|
@@ -19664,7 +19827,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19664
19827
|
/** Unlink every blob not in `keep`, and every temp file. Only a missing directory counts as empty. */
|
|
19665
19828
|
async #unlinkBlobs(keep) {
|
|
19666
19829
|
let removed = 0;
|
|
19667
|
-
const root = join7(this
|
|
19830
|
+
const root = join7(this.#pinnedDir, "blobs");
|
|
19668
19831
|
for (const shard of await entriesOf(root)) {
|
|
19669
19832
|
const shardPath = join7(root, shard);
|
|
19670
19833
|
if (shard === ".tmp") {
|
|
@@ -19683,13 +19846,13 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19683
19846
|
await rm3(join7(shardPath, name), { force: true });
|
|
19684
19847
|
removed += 1;
|
|
19685
19848
|
}
|
|
19686
|
-
if (kept === 0) await
|
|
19849
|
+
if (kept === 0) await rmdir(shardPath).catch(() => void 0);
|
|
19687
19850
|
}
|
|
19688
19851
|
return removed;
|
|
19689
19852
|
}
|
|
19690
19853
|
/** Whether any content file (a blob or a temp file) is on disk. */
|
|
19691
19854
|
async #holdsContentFiles() {
|
|
19692
|
-
const root = join7(this
|
|
19855
|
+
const root = join7(this.#pinnedDir, "blobs");
|
|
19693
19856
|
for (const shard of await entriesOf(root)) {
|
|
19694
19857
|
if ((await entriesOf(join7(root, shard))).length > 0) return true;
|
|
19695
19858
|
}
|
|
@@ -19755,8 +19918,93 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19755
19918
|
}
|
|
19756
19919
|
try {
|
|
19757
19920
|
this.#checkLease(t, { allowRevoked: true });
|
|
19921
|
+
const expected = this.#directoryFd === void 0 ? this.#namedAncestors.at(-1) : fstatSync(this.#directoryFd, { bigint: true });
|
|
19922
|
+
if (!expected) {
|
|
19923
|
+
throw new ReplicaError2(ReplicaErrorCode2.RUNTIME_UNSUPPORTED, "Replica removal requires an opened or identity-checked directory.");
|
|
19924
|
+
}
|
|
19925
|
+
const assertDirectory2 = async () => {
|
|
19926
|
+
const current = await lstat2(this.dir, { bigint: true });
|
|
19927
|
+
if (!current.isDirectory() || current.dev !== expected.dev || current.ino !== expected.ino) {
|
|
19928
|
+
throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica directory changed during removal.");
|
|
19929
|
+
}
|
|
19930
|
+
return current;
|
|
19931
|
+
};
|
|
19932
|
+
const pinned = await assertDirectory2();
|
|
19758
19933
|
await this.#assertPresent();
|
|
19759
|
-
|
|
19934
|
+
if (process.platform === "linux") {
|
|
19935
|
+
for (const entry of await readdir3(this.#pinnedDir)) {
|
|
19936
|
+
await rm3(join7(this.#pinnedDir, entry), { recursive: true, force: true });
|
|
19937
|
+
}
|
|
19938
|
+
await assertDirectory2();
|
|
19939
|
+
await rmdir(this.dir);
|
|
19940
|
+
} else {
|
|
19941
|
+
const ancestors = this.#namedAncestors;
|
|
19942
|
+
const checkAncestors = async () => {
|
|
19943
|
+
for (const expected2 of ancestors) {
|
|
19944
|
+
const current = await lstat2(expected2.path, { bigint: true });
|
|
19945
|
+
if (current.dev !== expected2.dev || current.ino !== expected2.ino) {
|
|
19946
|
+
throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica ancestry changed during removal.");
|
|
19947
|
+
}
|
|
19948
|
+
}
|
|
19949
|
+
};
|
|
19950
|
+
const directories = /* @__PURE__ */ new Map([[this.dir, pinned]]);
|
|
19951
|
+
const files = /* @__PURE__ */ new Map();
|
|
19952
|
+
const check = async (path, expected2, directory) => {
|
|
19953
|
+
if (path !== this.dir) {
|
|
19954
|
+
const parent = dirname3(path);
|
|
19955
|
+
await check(parent, directories.get(parent), true);
|
|
19956
|
+
}
|
|
19957
|
+
await checkAncestors();
|
|
19958
|
+
const current = await lstat2(path, { bigint: true });
|
|
19959
|
+
if ((directory ? !current.isDirectory() : !current.isFile()) || current.dev !== expected2.dev || current.ino !== expected2.ino) {
|
|
19960
|
+
throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica layout changed during removal.");
|
|
19961
|
+
}
|
|
19962
|
+
};
|
|
19963
|
+
const record2 = async (path, directory) => {
|
|
19964
|
+
const parent = dirname3(path);
|
|
19965
|
+
await check(parent, directories.get(parent), true);
|
|
19966
|
+
const current = await lstat2(path, { bigint: true });
|
|
19967
|
+
if (!(directory ? current.isDirectory() : current.isFile())) {
|
|
19968
|
+
throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica layout during removal.");
|
|
19969
|
+
}
|
|
19970
|
+
(directory ? directories : files).set(path, current);
|
|
19971
|
+
};
|
|
19972
|
+
const blobs = join7(this.dir, "blobs");
|
|
19973
|
+
for (const name of await readdir3(this.dir)) {
|
|
19974
|
+
if (name === "blobs") await record2(blobs, true);
|
|
19975
|
+
else if (["replica.db", "replica.db-wal", "replica.db-shm"].includes(name)) await record2(join7(this.dir, name), false);
|
|
19976
|
+
else throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica layout during removal.");
|
|
19977
|
+
}
|
|
19978
|
+
if (directories.has(blobs)) {
|
|
19979
|
+
await check(blobs, directories.get(blobs), true);
|
|
19980
|
+
for (const shard of await readdir3(blobs)) {
|
|
19981
|
+
if (shard !== ".tmp" && !/^[0-9a-f]{2}$/.test(shard)) {
|
|
19982
|
+
throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica blob shard during removal.");
|
|
19983
|
+
}
|
|
19984
|
+
const shardPath = join7(blobs, shard);
|
|
19985
|
+
await record2(shardPath, true);
|
|
19986
|
+
await check(shardPath, directories.get(shardPath), true);
|
|
19987
|
+
for (const name of await readdir3(shardPath)) {
|
|
19988
|
+
if (shard === ".tmp" ? !/^[0-9a-f]{64}\.[0-9a-f]{12}$/.test(name) : !isHash(name) || !name.startsWith(shard)) {
|
|
19989
|
+
throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica blob file during removal.");
|
|
19990
|
+
}
|
|
19991
|
+
await record2(join7(shardPath, name), false);
|
|
19992
|
+
}
|
|
19993
|
+
}
|
|
19994
|
+
}
|
|
19995
|
+
for (const [path, identity] of files) {
|
|
19996
|
+
await check(path, identity, false);
|
|
19997
|
+
await unlink(path);
|
|
19998
|
+
}
|
|
19999
|
+
for (const [path, identity] of [...directories].reverse()) {
|
|
20000
|
+
if (path === this.dir) continue;
|
|
20001
|
+
await check(path, identity, true);
|
|
20002
|
+
await rmdir(path);
|
|
20003
|
+
}
|
|
20004
|
+
await assertDirectory2();
|
|
20005
|
+
await checkAncestors();
|
|
20006
|
+
await rmdir(this.dir);
|
|
20007
|
+
}
|
|
19760
20008
|
} catch (error) {
|
|
19761
20009
|
throw storageError(error, "Removing the replica");
|
|
19762
20010
|
} finally {
|
|
@@ -19922,7 +20170,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19922
20170
|
};
|
|
19923
20171
|
}
|
|
19924
20172
|
async close() {
|
|
19925
|
-
this.#
|
|
20173
|
+
const fd = this.#directoryFd;
|
|
20174
|
+
this.#directoryFd = void 0;
|
|
20175
|
+
if (fd !== void 0) pinnedDirectories.unregister(this);
|
|
20176
|
+
try {
|
|
20177
|
+
this.#db.close();
|
|
20178
|
+
} finally {
|
|
20179
|
+
if (fd !== void 0) closeSync(fd);
|
|
20180
|
+
}
|
|
19926
20181
|
}
|
|
19927
20182
|
};
|
|
19928
20183
|
|
|
@@ -19930,229 +20185,388 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19930
20185
|
init_errors();
|
|
19931
20186
|
init_constants();
|
|
19932
20187
|
var SYNC_LEASE_MS = 6e4;
|
|
19933
|
-
function refuseUnsafeReplicaPath(profile, detail) {
|
|
20188
|
+
function refuseUnsafeReplicaPath(profile, detail, operation = "logout") {
|
|
19934
20189
|
return new CLIError(
|
|
19935
20190
|
"REPLICA_PURGE_FAILED",
|
|
19936
|
-
`Logout for profile "${profile}" cleared the session, but ${detail}; the replicas were left untouched.`,
|
|
20191
|
+
operation === "logout" ? `Logout for profile "${profile}" cleared the session, but ${detail}; the replicas were left untouched.` : `Cannot reset a replica in profile "${profile}": ${detail}; no replica was removed.`,
|
|
19937
20192
|
ExitCode.ERROR
|
|
19938
20193
|
);
|
|
19939
20194
|
}
|
|
19940
|
-
async function safeReplicaRoot(profile, root) {
|
|
20195
|
+
async function safeReplicaRoot(profile, root, operation = "logout") {
|
|
19941
20196
|
let rootStats;
|
|
19942
20197
|
try {
|
|
19943
|
-
rootStats = await
|
|
20198
|
+
rootStats = root.startsWith("/proc/self/fd/") ? await stat3(root) : await lstat3(root);
|
|
19944
20199
|
} catch (error) {
|
|
19945
20200
|
if (error?.code === "ENOENT") return null;
|
|
19946
20201
|
throw error;
|
|
19947
20202
|
}
|
|
19948
20203
|
if (rootStats.isSymbolicLink()) {
|
|
19949
|
-
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" is a symlink
|
|
20204
|
+
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" is a symlink`, operation);
|
|
19950
20205
|
}
|
|
19951
20206
|
if (!rootStats.isDirectory()) {
|
|
19952
|
-
throw refuseUnsafeReplicaPath(profile, `the replicas path "${root}" is not a real directory
|
|
20207
|
+
throw refuseUnsafeReplicaPath(profile, `the replicas path "${root}" is not a real directory`, operation);
|
|
19953
20208
|
}
|
|
19954
|
-
const [profileRealPath, rootRealPath] = await Promise.all([realpath(
|
|
20209
|
+
const [profileRealPath, rootRealPath] = await Promise.all([realpath(profilePath4(profile)), realpath(root)]);
|
|
19955
20210
|
if (!rootRealPath.startsWith(profileRealPath + sep)) {
|
|
19956
|
-
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" resolves outside the selected profile
|
|
20211
|
+
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" resolves outside the selected profile`, operation);
|
|
19957
20212
|
}
|
|
19958
20213
|
return rootRealPath;
|
|
19959
20214
|
}
|
|
19960
|
-
async function safeReplicaEntry(profile, root, entry) {
|
|
20215
|
+
async function safeReplicaEntry(profile, root, entry, operation = "logout") {
|
|
19961
20216
|
const entryPath = join8(root, entry.name);
|
|
19962
|
-
const rootRealPath = await safeReplicaRoot(profile, root);
|
|
20217
|
+
const rootRealPath = await safeReplicaRoot(profile, root, operation);
|
|
19963
20218
|
if (rootRealPath === null) {
|
|
19964
|
-
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" disappeared
|
|
20219
|
+
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" disappeared`, operation);
|
|
19965
20220
|
}
|
|
19966
|
-
const entryStats = await
|
|
20221
|
+
const entryStats = await lstat3(entryPath);
|
|
19967
20222
|
if (entry.isSymbolicLink() || entryStats.isSymbolicLink()) {
|
|
19968
|
-
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is a symlink
|
|
20223
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is a symlink`, operation);
|
|
19969
20224
|
}
|
|
19970
20225
|
if (!entry.isDirectory() || !entryStats.isDirectory()) {
|
|
19971
|
-
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is not a real directory
|
|
20226
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is not a real directory`, operation);
|
|
19972
20227
|
}
|
|
19973
20228
|
const entryRealPath = await realpath(entryPath);
|
|
19974
20229
|
if (!entryRealPath.startsWith(rootRealPath + sep)) {
|
|
19975
|
-
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" resolves outside the replicas directory
|
|
20230
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" resolves outside the replicas directory`, operation);
|
|
19976
20231
|
}
|
|
19977
|
-
return entryPath;
|
|
20232
|
+
return { path: entryPath, identity: { dev: entryStats.dev, ino: entryStats.ino } };
|
|
19978
20233
|
}
|
|
19979
|
-
async function
|
|
20234
|
+
async function inspectLegacyReplica(profile, root, entry) {
|
|
20235
|
+
const { path, identity } = await safeReplicaEntry(profile, root, entry);
|
|
20236
|
+
const handle = await open3(path, fsConstants2.O_RDONLY | fsConstants2.O_DIRECTORY | fsConstants2.O_NOFOLLOW);
|
|
20237
|
+
try {
|
|
20238
|
+
const pinnedEntry = await handle.stat();
|
|
20239
|
+
if (pinnedEntry.dev !== identity.dev || pinnedEntry.ino !== identity.ino) {
|
|
20240
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" changed during inspection`);
|
|
20241
|
+
}
|
|
20242
|
+
const children = await readdir4(`/proc/self/fd/${handle.fd}`, { withFileTypes: true });
|
|
20243
|
+
if (children.length > 0 && !children.some((child) => child.name === "replica.db" && child.isFile())) {
|
|
20244
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" has files but no regular replica database`);
|
|
20245
|
+
}
|
|
20246
|
+
return { path, identity, empty: children.length === 0 };
|
|
20247
|
+
} finally {
|
|
20248
|
+
await handle.close();
|
|
20249
|
+
}
|
|
20250
|
+
}
|
|
20251
|
+
async function pinReplicaRoot(profile, named, operation) {
|
|
20252
|
+
const initial = await lstat3(named).catch((error) => {
|
|
20253
|
+
if (error.code === "ENOENT") return null;
|
|
20254
|
+
throw error;
|
|
20255
|
+
});
|
|
20256
|
+
if (await safeReplicaRoot(profile, named, operation) === null) return null;
|
|
20257
|
+
if (process.platform !== "linux") {
|
|
20258
|
+
throw refuseUnsafeReplicaPath(profile, "pinned replica removal is unavailable on this platform", operation);
|
|
20259
|
+
}
|
|
20260
|
+
const handle = await open3(named, fsConstants2.O_RDONLY | fsConstants2.O_DIRECTORY | fsConstants2.O_NOFOLLOW);
|
|
20261
|
+
try {
|
|
20262
|
+
const pinned = await handle.stat();
|
|
20263
|
+
const assertNamed = async () => {
|
|
20264
|
+
const current = await lstat3(named);
|
|
20265
|
+
if (!current.isDirectory() || current.dev !== initial?.dev || current.ino !== initial?.ino || current.dev !== pinned.dev || current.ino !== pinned.ino) {
|
|
20266
|
+
throw refuseUnsafeReplicaPath(profile, "the replicas directory changed during cleanup", operation);
|
|
20267
|
+
}
|
|
20268
|
+
};
|
|
20269
|
+
await assertNamed();
|
|
20270
|
+
return { path: `/proc/self/fd/${handle.fd}`, assertNamed, close: () => handle.close() };
|
|
20271
|
+
} catch (error) {
|
|
20272
|
+
await handle.close();
|
|
20273
|
+
throw error;
|
|
20274
|
+
}
|
|
20275
|
+
}
|
|
20276
|
+
async function withPinnedReplicaForReset(profile, name, action) {
|
|
19980
20277
|
return withProfileLock4(profile, async () => {
|
|
19981
|
-
const root = join8(
|
|
19982
|
-
|
|
19983
|
-
|
|
19984
|
-
if (replicas.length === 0) return [];
|
|
19985
|
-
for (const entry of replicas) await safeReplicaEntry(profile, root, entry);
|
|
19986
|
-
await loadSqlite();
|
|
19987
|
-
const opened = [];
|
|
20278
|
+
const root = join8(profilePath4(profile), "replicas");
|
|
20279
|
+
const pinned = await pinReplicaRoot(profile, root, "reset");
|
|
20280
|
+
if (!pinned) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas.`);
|
|
19988
20281
|
try {
|
|
19989
|
-
|
|
19990
|
-
|
|
19991
|
-
|
|
19992
|
-
|
|
19993
|
-
|
|
19994
|
-
|
|
19995
|
-
|
|
19996
|
-
|
|
19997
|
-
|
|
19998
|
-
|
|
20282
|
+
const fdRoot = pinned.path;
|
|
20283
|
+
const entry = (await readdir4(fdRoot, { withFileTypes: true })).find((item) => item.name === name);
|
|
20284
|
+
if (!entry) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Replica "${name}" was not found.`);
|
|
20285
|
+
const { path, identity: initial } = await safeReplicaEntry(profile, fdRoot, entry, "reset");
|
|
20286
|
+
const recheck = async () => {
|
|
20287
|
+
const current = await lstat3(path);
|
|
20288
|
+
if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== initial.dev || current.ino !== initial.ino) {
|
|
20289
|
+
throw refuseUnsafeReplicaPath(profile, `replica entry "${name}" changed during removal`, "reset");
|
|
20290
|
+
}
|
|
20291
|
+
};
|
|
20292
|
+
await recheck();
|
|
20293
|
+
return await action(path, recheck, { dev: initial.dev, ino: initial.ino });
|
|
20294
|
+
} finally {
|
|
20295
|
+
await pinned.close();
|
|
20296
|
+
}
|
|
20297
|
+
});
|
|
20298
|
+
}
|
|
20299
|
+
async function removeProfileReplicas(profile, heldLeases, preflightRoot) {
|
|
20300
|
+
return withProfileLock4(profile, async () => {
|
|
20301
|
+
const namedRoot = join8(profilePath4(profile), "replicas");
|
|
20302
|
+
const pinned = preflightRoot ?? await pinReplicaRoot(profile, namedRoot, "logout");
|
|
20303
|
+
if (!pinned) return [];
|
|
20304
|
+
if (preflightRoot) await pinned.assertNamed();
|
|
20305
|
+
try {
|
|
20306
|
+
const root = pinned.path;
|
|
20307
|
+
const replicas = (await readdir4(root, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
20308
|
+
if (replicas.length === 0) return [];
|
|
20309
|
+
const checked = await Promise.all(replicas.map(async (entry) => ({
|
|
20310
|
+
entry,
|
|
20311
|
+
...await inspectLegacyReplica(profile, root, entry)
|
|
20312
|
+
})));
|
|
20313
|
+
const removed = [];
|
|
20314
|
+
const opened = [];
|
|
20315
|
+
try {
|
|
20316
|
+
for (const { entry, identity, empty: empty4 } of checked) {
|
|
20317
|
+
if (!empty4) continue;
|
|
20318
|
+
await pinned.assertNamed();
|
|
20319
|
+
const current = await safeReplicaEntry(profile, root, entry);
|
|
20320
|
+
if (current.identity.dev !== identity.dev || current.identity.ino !== identity.ino) {
|
|
20321
|
+
throw refuseUnsafeReplicaPath(profile, `empty replica entry "${entry.name}" changed during cleanup`);
|
|
20322
|
+
}
|
|
20323
|
+
const handle = await open3(current.path, fsConstants2.O_RDONLY | fsConstants2.O_DIRECTORY | fsConstants2.O_NOFOLLOW);
|
|
20324
|
+
try {
|
|
20325
|
+
const pinnedEntry = await handle.stat();
|
|
20326
|
+
if (pinnedEntry.dev !== identity.dev || pinnedEntry.ino !== identity.ino || (await readdir4(`/proc/self/fd/${handle.fd}`)).length !== 0) {
|
|
20327
|
+
throw refuseUnsafeReplicaPath(profile, `empty replica entry "${entry.name}" changed during cleanup`);
|
|
20328
|
+
}
|
|
20329
|
+
await pinned.assertNamed();
|
|
20330
|
+
const namedEntry = await lstat3(current.path);
|
|
20331
|
+
if (!namedEntry.isDirectory() || namedEntry.dev !== pinnedEntry.dev || namedEntry.ino !== pinnedEntry.ino) {
|
|
20332
|
+
throw refuseUnsafeReplicaPath(profile, `empty replica entry "${entry.name}" changed at removal`);
|
|
20333
|
+
}
|
|
20334
|
+
await rmdir2(current.path);
|
|
20335
|
+
} finally {
|
|
20336
|
+
await handle.close();
|
|
20337
|
+
}
|
|
20338
|
+
removed.push(entry.name);
|
|
20339
|
+
}
|
|
20340
|
+
if (removed.length === replicas.length) return removed;
|
|
20341
|
+
await loadSqlite();
|
|
20342
|
+
for (const { entry, empty: empty4 } of checked) {
|
|
20343
|
+
if (empty4) continue;
|
|
20344
|
+
const { path: entryPath, identity: expectedDirectory } = await safeReplicaEntry(profile, root, entry);
|
|
20345
|
+
const store = await SqliteReplicaStore.open(entryPath, {
|
|
20346
|
+
create: false,
|
|
20347
|
+
expectedDirectory,
|
|
20348
|
+
guard: async (action) => {
|
|
20349
|
+
try {
|
|
20350
|
+
return await withProfileLock4(profile, async () => {
|
|
20351
|
+
await safeReplicaEntry(profile, root, entry);
|
|
20352
|
+
return action();
|
|
20353
|
+
}, { requireProfile: true });
|
|
20354
|
+
} catch (error) {
|
|
20355
|
+
if (error instanceof ProfileDeletedError2) {
|
|
20356
|
+
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" was deleted; its replicas are gone and nothing was written.`);
|
|
20357
|
+
}
|
|
20358
|
+
throw error;
|
|
19999
20359
|
}
|
|
20000
|
-
throw error;
|
|
20001
20360
|
}
|
|
20361
|
+
});
|
|
20362
|
+
opened.push({ name: entry.name, store, lease: null });
|
|
20363
|
+
if (await store.open() === null) {
|
|
20364
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
|
|
20002
20365
|
}
|
|
20003
|
-
|
|
20004
|
-
|
|
20005
|
-
|
|
20006
|
-
|
|
20007
|
-
|
|
20008
|
-
const lease = heldLeases?.get(entryPath) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20009
|
-
if (lease === null) {
|
|
20010
|
-
throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
20366
|
+
const lease = heldLeases?.get(join8(namedRoot, entry.name)) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20367
|
+
if (lease === null) {
|
|
20368
|
+
throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
20369
|
+
}
|
|
20370
|
+
opened[opened.length - 1].lease = lease;
|
|
20011
20371
|
}
|
|
20012
|
-
|
|
20013
|
-
|
|
20014
|
-
|
|
20015
|
-
|
|
20016
|
-
|
|
20017
|
-
|
|
20018
|
-
|
|
20019
|
-
|
|
20020
|
-
|
|
20021
|
-
()
|
|
20022
|
-
(
|
|
20023
|
-
|
|
20024
|
-
|
|
20025
|
-
|
|
20026
|
-
|
|
20027
|
-
|
|
20028
|
-
|
|
20029
|
-
|
|
20030
|
-
{ replicasRemoved: removed, replicasRemaining: remaining }
|
|
20031
|
-
);
|
|
20372
|
+
for (const replica of opened) {
|
|
20373
|
+
try {
|
|
20374
|
+
await replica.store.destroy(replica.lease);
|
|
20375
|
+
removed.push(replica.name);
|
|
20376
|
+
} catch (error) {
|
|
20377
|
+
const replicaStillExists = await lstat3(join8(root, replica.name)).then(
|
|
20378
|
+
() => true,
|
|
20379
|
+
(statError) => statError.code !== "ENOENT"
|
|
20380
|
+
);
|
|
20381
|
+
if (!replicaStillExists) removed.push(replica.name);
|
|
20382
|
+
const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
|
|
20383
|
+
throw new CLIError(
|
|
20384
|
+
"REPLICA_PURGE_FAILED",
|
|
20385
|
+
`Logout cleared the session, but replica removal failed. Removed: ${removed.length ? removed.join(", ") : "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
|
|
20386
|
+
ExitCode.ERROR,
|
|
20387
|
+
{ replicasRemoved: removed, replicasRemaining: remaining }
|
|
20388
|
+
);
|
|
20389
|
+
}
|
|
20032
20390
|
}
|
|
20391
|
+
return removed;
|
|
20392
|
+
} catch (error) {
|
|
20393
|
+
if (error instanceof CLIError) throw error;
|
|
20394
|
+
throw new CLIError(
|
|
20395
|
+
"REPLICA_PURGE_FAILED",
|
|
20396
|
+
`Logout cleared the session, but replica cleanup could not finish. Removed: ${removed.length ? removed.join(", ") : "none"}. ${error instanceof Error ? error.message : String(error)}`,
|
|
20397
|
+
ExitCode.ERROR
|
|
20398
|
+
);
|
|
20399
|
+
} finally {
|
|
20400
|
+
await Promise.all(opened.map(async ({ name, store, lease }) => {
|
|
20401
|
+
if (lease !== null && heldLeases?.has(join8(namedRoot, name)) !== true) await store.releaseLease(lease).catch(() => void 0);
|
|
20402
|
+
await store.close().catch(() => void 0);
|
|
20403
|
+
}));
|
|
20033
20404
|
}
|
|
20034
|
-
return removed;
|
|
20035
|
-
} catch (error) {
|
|
20036
|
-
if (error instanceof CLIError) throw error;
|
|
20037
|
-
throw new CLIError(
|
|
20038
|
-
"REPLICA_PURGE_FAILED",
|
|
20039
|
-
`Logout cleared the session, but replica cleanup could not start; no replicas were removed. ${error instanceof Error ? error.message : String(error)}`,
|
|
20040
|
-
ExitCode.ERROR
|
|
20041
|
-
);
|
|
20042
20405
|
} finally {
|
|
20043
|
-
await
|
|
20044
|
-
if (lease !== null && heldLeases?.has(join8(root, name)) !== true) await store.releaseLease(lease).catch(() => void 0);
|
|
20045
|
-
await store.close().catch(() => void 0);
|
|
20046
|
-
}));
|
|
20406
|
+
if (!preflightRoot) await pinned.close();
|
|
20047
20407
|
}
|
|
20048
20408
|
});
|
|
20049
20409
|
}
|
|
20050
|
-
async function
|
|
20410
|
+
async function assertSafeFlagReplica(profile, root, partition, name) {
|
|
20411
|
+
const rootReal = await safeReplicaRoot(profile, root);
|
|
20412
|
+
if (rootReal === null) throw refuseUnsafeReplicaPath(profile, `the replication root "${root}" disappeared`);
|
|
20413
|
+
const partitionPath = join8(root, partition);
|
|
20414
|
+
const partitionStats = await lstat3(partitionPath);
|
|
20415
|
+
if (partitionStats.isSymbolicLink() || !partitionStats.isDirectory()) {
|
|
20416
|
+
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition}" is not a real directory`);
|
|
20417
|
+
}
|
|
20418
|
+
const partitionReal = await realpath(partitionPath);
|
|
20419
|
+
if (!partitionReal.startsWith(rootReal + sep)) {
|
|
20420
|
+
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition}" resolves outside the replication root`);
|
|
20421
|
+
}
|
|
20422
|
+
const replicasRoot = join8(partitionPath, "replicas");
|
|
20423
|
+
const replicasStats = await lstat3(replicasRoot);
|
|
20424
|
+
if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
|
|
20425
|
+
throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot}" is not a real directory`);
|
|
20426
|
+
}
|
|
20427
|
+
const replicaPath = join8(replicasRoot, name);
|
|
20428
|
+
const replicaStats = await lstat3(replicaPath);
|
|
20429
|
+
if (replicaStats.isSymbolicLink() || !replicaStats.isDirectory()) {
|
|
20430
|
+
throw refuseUnsafeReplicaPath(profile, `replication replica "${name}" is not a real directory`);
|
|
20431
|
+
}
|
|
20432
|
+
const replicaReal = await realpath(replicaPath);
|
|
20433
|
+
if (!replicaReal.startsWith(partitionReal + sep)) {
|
|
20434
|
+
throw refuseUnsafeReplicaPath(profile, `replication replica "${name}" resolves outside the partition`);
|
|
20435
|
+
}
|
|
20436
|
+
const database = await lstat3(join8(replicaPath, "replica.db"));
|
|
20437
|
+
if (database.isSymbolicLink() || !database.isFile()) {
|
|
20438
|
+
throw refuseUnsafeReplicaPath(profile, `replication database "${name}" is not a regular file`);
|
|
20439
|
+
}
|
|
20440
|
+
return { dev: replicaStats.dev, ino: replicaStats.ino };
|
|
20441
|
+
}
|
|
20442
|
+
async function removeProfileReplication(profile, heldLeases, preflightRoot) {
|
|
20051
20443
|
return withProfileLock4(profile, async () => {
|
|
20052
|
-
const
|
|
20053
|
-
const
|
|
20054
|
-
if (
|
|
20055
|
-
|
|
20056
|
-
const partitions = await readdir4(root, { withFileTypes: true });
|
|
20057
|
-
const opened = [];
|
|
20058
|
-
const removed = [];
|
|
20059
|
-
let deleting = false;
|
|
20444
|
+
const namedRoot = join8(profilePath4(profile), "replication");
|
|
20445
|
+
const pinned = preflightRoot ?? await pinReplicaRoot(profile, namedRoot, "logout");
|
|
20446
|
+
if (!pinned) return [];
|
|
20447
|
+
if (preflightRoot) await pinned.assertNamed();
|
|
20060
20448
|
try {
|
|
20061
|
-
|
|
20062
|
-
|
|
20063
|
-
|
|
20064
|
-
|
|
20065
|
-
|
|
20066
|
-
|
|
20067
|
-
|
|
20068
|
-
|
|
20069
|
-
|
|
20070
|
-
|
|
20071
|
-
|
|
20072
|
-
|
|
20073
|
-
|
|
20074
|
-
if (error.code === "ENOENT") return null;
|
|
20075
|
-
throw error;
|
|
20076
|
-
});
|
|
20077
|
-
if (!replicasStats) continue;
|
|
20078
|
-
if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
|
|
20079
|
-
throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot2}" is not a real directory`);
|
|
20449
|
+
const root = pinned.path;
|
|
20450
|
+
const rootReal = await safeReplicaRoot(profile, root);
|
|
20451
|
+
const profileReal = await realpath(profilePath4(profile));
|
|
20452
|
+
const partitions = await readdir4(root, { withFileTypes: true });
|
|
20453
|
+
const opened = [];
|
|
20454
|
+
const removed = [];
|
|
20455
|
+
let deleting = false;
|
|
20456
|
+
try {
|
|
20457
|
+
for (const partition of partitions) {
|
|
20458
|
+
const partitionPath = join8(root, partition.name);
|
|
20459
|
+
const partitionStats = await lstat3(partitionPath);
|
|
20460
|
+
if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
|
|
20461
|
+
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
|
|
20080
20462
|
}
|
|
20081
|
-
|
|
20082
|
-
|
|
20083
|
-
|
|
20084
|
-
|
|
20085
|
-
if (replica.isSymbolicLink() || replicaStats.isSymbolicLink() || !replicaStats.isDirectory()) {
|
|
20086
|
-
throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
|
|
20463
|
+
if (partitionStats.isDirectory()) {
|
|
20464
|
+
const partitionReal = await realpath(partitionPath);
|
|
20465
|
+
if (!partitionReal.startsWith(rootReal + sep) || !partitionReal.startsWith(profileReal + sep)) {
|
|
20466
|
+
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the selected profile`);
|
|
20087
20467
|
}
|
|
20088
|
-
const
|
|
20089
|
-
|
|
20090
|
-
|
|
20468
|
+
const replicasRoot = join8(partitionPath, "replicas");
|
|
20469
|
+
const replicasStats = await lstat3(replicasRoot).catch((error) => {
|
|
20470
|
+
if (error.code === "ENOENT") return null;
|
|
20471
|
+
throw error;
|
|
20472
|
+
});
|
|
20473
|
+
if (!replicasStats) continue;
|
|
20474
|
+
if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
|
|
20475
|
+
throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot}" is not a real directory`);
|
|
20091
20476
|
}
|
|
20092
|
-
const
|
|
20477
|
+
const replicas = await readdir4(replicasRoot, { withFileTypes: true });
|
|
20478
|
+
for (const replica of replicas) {
|
|
20479
|
+
const replicaPath = join8(replicasRoot, replica.name);
|
|
20480
|
+
const replicaStats = await lstat3(replicaPath);
|
|
20481
|
+
if (replica.isSymbolicLink() || replicaStats.isSymbolicLink() || !replicaStats.isDirectory()) {
|
|
20482
|
+
throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
|
|
20483
|
+
}
|
|
20484
|
+
const replicaReal = await realpath(replicaPath);
|
|
20485
|
+
if (!replicaReal.startsWith(rootReal + sep)) {
|
|
20486
|
+
throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" resolves outside the replication root`);
|
|
20487
|
+
}
|
|
20488
|
+
const database = await lstat3(join8(replicaPath, "replica.db")).catch((error) => {
|
|
20489
|
+
if (error.code === "ENOENT") return null;
|
|
20490
|
+
throw error;
|
|
20491
|
+
});
|
|
20492
|
+
if (!database) continue;
|
|
20493
|
+
if (database.isSymbolicLink() || !database.isFile()) {
|
|
20494
|
+
throw refuseUnsafeReplicaPath(profile, `replication database "${replica.name}" is not a regular file`);
|
|
20495
|
+
}
|
|
20496
|
+
}
|
|
20497
|
+
} else if (!partitionStats.isFile()) {
|
|
20498
|
+
throw refuseUnsafeReplicaPath(profile, `replication path "${partition.name}" is not a regular file or directory`);
|
|
20499
|
+
}
|
|
20500
|
+
}
|
|
20501
|
+
const hasReplicaDatabase = await Promise.all(partitions.filter((entry) => entry.isDirectory()).map(async (partition) => {
|
|
20502
|
+
return Promise.all((await readdir4(join8(root, partition.name, "replicas"), { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map((entry) => lstat3(join8(root, partition.name, "replicas", entry.name, "replica.db")).then(() => true, () => false)));
|
|
20503
|
+
})).then((values) => values.some((partition) => partition.some(Boolean)));
|
|
20504
|
+
if (hasReplicaDatabase) await loadSqlite();
|
|
20505
|
+
for (const partition of partitions.filter((entry) => entry.isDirectory())) {
|
|
20506
|
+
const replicasRoot = join8(root, partition.name, "replicas");
|
|
20507
|
+
for (const entry of await readdir4(replicasRoot, { withFileTypes: true }).catch(() => [])) {
|
|
20508
|
+
if (!entry.isDirectory()) continue;
|
|
20509
|
+
const database = await lstat3(join8(replicasRoot, entry.name, "replica.db")).catch((error) => {
|
|
20093
20510
|
if (error.code === "ENOENT") return null;
|
|
20094
20511
|
throw error;
|
|
20095
20512
|
});
|
|
20096
20513
|
if (!database) continue;
|
|
20097
|
-
|
|
20098
|
-
|
|
20514
|
+
const name = `replication/${partition.name}/${entry.name}`;
|
|
20515
|
+
const expectedDirectory = await assertSafeFlagReplica(profile, root, partition.name, entry.name);
|
|
20516
|
+
const store = await SqliteReplicaStore.open(join8(replicasRoot, entry.name), {
|
|
20517
|
+
create: false,
|
|
20518
|
+
expectedDirectory,
|
|
20519
|
+
guard: async (action) => withProfileLock4(profile, async () => {
|
|
20520
|
+
await assertSafeFlagReplica(profile, root, partition.name, entry.name);
|
|
20521
|
+
return action();
|
|
20522
|
+
}, { requireProfile: true })
|
|
20523
|
+
});
|
|
20524
|
+
try {
|
|
20525
|
+
if (await store.open() === null) {
|
|
20526
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "${name}" has no initialized store.`, ExitCode.ERROR);
|
|
20527
|
+
}
|
|
20528
|
+
const leasePath = join8(namedRoot, partition.name, "replicas", entry.name);
|
|
20529
|
+
const lease = heldLeases?.get(leasePath) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20530
|
+
if (lease === null) {
|
|
20531
|
+
throw new CLIError("REPLICA_BUSY", `Cannot log out: flag replica "${name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
20532
|
+
}
|
|
20533
|
+
opened.push({ name, path: leasePath, store, lease });
|
|
20534
|
+
} catch (error) {
|
|
20535
|
+
await store.close().catch(() => void 0);
|
|
20536
|
+
throw error;
|
|
20099
20537
|
}
|
|
20100
20538
|
}
|
|
20101
|
-
} else if (!partitionStats.isFile()) {
|
|
20102
|
-
throw refuseUnsafeReplicaPath(profile, `replication path "${partition.name}" is not a regular file or directory`);
|
|
20103
20539
|
}
|
|
20104
|
-
|
|
20105
|
-
|
|
20106
|
-
|
|
20107
|
-
|
|
20108
|
-
if (hasReplicaDatabase) await loadSqlite();
|
|
20109
|
-
for (const partition of partitions.filter((entry) => entry.isDirectory())) {
|
|
20110
|
-
const replicasRoot2 = join8(root, partition.name, "replicas");
|
|
20111
|
-
for (const entry of await readdir4(replicasRoot2, { withFileTypes: true }).catch(() => [])) {
|
|
20112
|
-
if (!entry.isDirectory()) continue;
|
|
20113
|
-
const database = await lstat2(join8(replicasRoot2, entry.name, "replica.db")).catch((error) => {
|
|
20114
|
-
if (error.code === "ENOENT") return null;
|
|
20115
|
-
throw error;
|
|
20116
|
-
});
|
|
20117
|
-
if (!database) continue;
|
|
20118
|
-
const name = `replication/${partition.name}/${entry.name}`;
|
|
20119
|
-
const store = await SqliteReplicaStore.open(join8(replicasRoot2, entry.name), { create: false, guard: async (action) => withProfileLock4(profile, action, { requireProfile: true }) });
|
|
20120
|
-
if (await store.open() === null) {
|
|
20121
|
-
await store.close();
|
|
20122
|
-
throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "${name}" has no initialized store.`, ExitCode.ERROR);
|
|
20123
|
-
}
|
|
20124
|
-
const lease = heldLeases?.get(join8(replicasRoot2, entry.name)) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20125
|
-
if (lease === null) {
|
|
20126
|
-
await store.close();
|
|
20127
|
-
throw new CLIError("REPLICA_BUSY", `Cannot log out: flag replica "${name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
20128
|
-
}
|
|
20129
|
-
opened.push({ name, path: join8(replicasRoot2, entry.name), store, lease });
|
|
20540
|
+
deleting = true;
|
|
20541
|
+
for (const item of opened) {
|
|
20542
|
+
await item.store.destroy(item.lease);
|
|
20543
|
+
removed.push(item.name);
|
|
20130
20544
|
}
|
|
20545
|
+
await pinned.assertNamed();
|
|
20546
|
+
for (const entry of await readdir4(root)) await rm4(join8(root, entry), { recursive: true, force: true });
|
|
20547
|
+
await pinned.assertNamed();
|
|
20548
|
+
await rmdir2(namedRoot);
|
|
20549
|
+
return removed;
|
|
20550
|
+
} catch (error) {
|
|
20551
|
+
if (deleting) {
|
|
20552
|
+
const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
|
|
20553
|
+
throw new CLIError(
|
|
20554
|
+
"REPLICA_PURGE_FAILED",
|
|
20555
|
+
`Logout cleared the session, but replication cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
|
|
20556
|
+
ExitCode.ERROR,
|
|
20557
|
+
{ replicasRemoved: removed, replicasRemaining: remaining }
|
|
20558
|
+
);
|
|
20559
|
+
}
|
|
20560
|
+
if (error instanceof CLIError) throw error;
|
|
20561
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but replication cleanup failed; no flag replicas were removed. ${error instanceof Error ? error.message : String(error)}`, ExitCode.ERROR);
|
|
20562
|
+
} finally {
|
|
20563
|
+
await Promise.all(opened.map(async ({ path, store, lease }) => {
|
|
20564
|
+
if (heldLeases?.has(path) !== true) await store.releaseLease(lease).catch(() => void 0);
|
|
20565
|
+
await store.close().catch(() => void 0);
|
|
20566
|
+
}));
|
|
20131
20567
|
}
|
|
20132
|
-
deleting = true;
|
|
20133
|
-
for (const item of opened) {
|
|
20134
|
-
await item.store.destroy(item.lease);
|
|
20135
|
-
removed.push(item.name);
|
|
20136
|
-
}
|
|
20137
|
-
await rm4(root, { recursive: true });
|
|
20138
|
-
return removed;
|
|
20139
|
-
} catch (error) {
|
|
20140
|
-
if (deleting) {
|
|
20141
|
-
const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
|
|
20142
|
-
throw new CLIError(
|
|
20143
|
-
"REPLICA_PURGE_FAILED",
|
|
20144
|
-
`Logout cleared the session, but replication cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
|
|
20145
|
-
ExitCode.ERROR,
|
|
20146
|
-
{ replicasRemoved: removed, replicasRemaining: remaining }
|
|
20147
|
-
);
|
|
20148
|
-
}
|
|
20149
|
-
if (error instanceof CLIError) throw error;
|
|
20150
|
-
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);
|
|
20151
20568
|
} finally {
|
|
20152
|
-
await
|
|
20153
|
-
if (heldLeases?.has(path) !== true) await store.releaseLease(lease).catch(() => void 0);
|
|
20154
|
-
await store.close().catch(() => void 0);
|
|
20155
|
-
}));
|
|
20569
|
+
if (!preflightRoot) await pinned.close();
|
|
20156
20570
|
}
|
|
20157
20571
|
});
|
|
20158
20572
|
}
|
|
@@ -20160,36 +20574,54 @@ async function acquireLogoutLeases(profile) {
|
|
|
20160
20574
|
return withProfileLock4(profile, async () => {
|
|
20161
20575
|
let sqliteLoaded = false;
|
|
20162
20576
|
const opened = [];
|
|
20577
|
+
const roots = /* @__PURE__ */ new Map();
|
|
20163
20578
|
try {
|
|
20164
|
-
const
|
|
20165
|
-
|
|
20579
|
+
const legacyNamed = join8(profilePath4(profile), "replicas");
|
|
20580
|
+
const legacyPin = await pinReplicaRoot(profile, legacyNamed, "logout");
|
|
20581
|
+
if (legacyPin) {
|
|
20582
|
+
roots.set("replicas", legacyPin);
|
|
20583
|
+
const legacyRoot = legacyPin.path;
|
|
20166
20584
|
const entries = (await readdir4(legacyRoot, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
20167
20585
|
for (const entry of entries) {
|
|
20168
|
-
const path = await
|
|
20586
|
+
const { path, identity: expectedDirectory, empty: empty4 } = await inspectLegacyReplica(profile, legacyRoot, entry);
|
|
20587
|
+
if (empty4) continue;
|
|
20169
20588
|
if (!sqliteLoaded) {
|
|
20170
20589
|
await loadSqlite();
|
|
20171
20590
|
sqliteLoaded = true;
|
|
20172
20591
|
}
|
|
20173
|
-
const store = await SqliteReplicaStore.open(path, {
|
|
20174
|
-
|
|
20175
|
-
|
|
20176
|
-
|
|
20177
|
-
|
|
20178
|
-
|
|
20179
|
-
|
|
20180
|
-
|
|
20181
|
-
|
|
20592
|
+
const store = await SqliteReplicaStore.open(path, {
|
|
20593
|
+
create: false,
|
|
20594
|
+
expectedDirectory,
|
|
20595
|
+
guard: async (action) => withProfileLock4(profile, async () => {
|
|
20596
|
+
await safeReplicaEntry(profile, legacyRoot, entry);
|
|
20597
|
+
return action();
|
|
20598
|
+
}, { requireProfile: true })
|
|
20599
|
+
});
|
|
20600
|
+
try {
|
|
20601
|
+
if (await store.open() === null) {
|
|
20602
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
|
|
20603
|
+
}
|
|
20604
|
+
const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20605
|
+
if (lease === null) {
|
|
20606
|
+
throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
20607
|
+
}
|
|
20608
|
+
opened.push({ path: join8(legacyNamed, entry.name), store, lease });
|
|
20609
|
+
} catch (error) {
|
|
20610
|
+
await store.close().catch(() => void 0);
|
|
20611
|
+
throw error;
|
|
20182
20612
|
}
|
|
20183
|
-
opened.push({ path, store, lease });
|
|
20184
20613
|
}
|
|
20185
20614
|
}
|
|
20186
|
-
const
|
|
20187
|
-
const
|
|
20188
|
-
if (
|
|
20615
|
+
const replicationNamed = join8(profilePath4(profile), "replication");
|
|
20616
|
+
const replicationPin = await pinReplicaRoot(profile, replicationNamed, "logout");
|
|
20617
|
+
if (replicationPin) {
|
|
20618
|
+
roots.set("replication", replicationPin);
|
|
20619
|
+
const replicationRoot = replicationPin.path;
|
|
20620
|
+
const replicationReal = await safeReplicaRoot(profile, replicationRoot);
|
|
20189
20621
|
const partitions = await readdir4(replicationRoot, { withFileTypes: true });
|
|
20190
20622
|
for (const partition of partitions) {
|
|
20191
20623
|
const partitionPath = join8(replicationRoot, partition.name);
|
|
20192
|
-
const partitionStats = await
|
|
20624
|
+
const partitionStats = await lstat3(partitionPath);
|
|
20193
20625
|
if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
|
|
20194
20626
|
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
|
|
20195
20627
|
}
|
|
@@ -20201,22 +20633,22 @@ async function acquireLogoutLeases(profile) {
|
|
|
20201
20633
|
if (!partitionReal.startsWith(replicationReal + sep)) {
|
|
20202
20634
|
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the replication root`);
|
|
20203
20635
|
}
|
|
20204
|
-
const
|
|
20205
|
-
const replicasStats = await
|
|
20636
|
+
const replicasRoot = join8(partitionPath, "replicas");
|
|
20637
|
+
const replicasStats = await lstat3(replicasRoot).catch((error) => {
|
|
20206
20638
|
if (error.code === "ENOENT") return null;
|
|
20207
20639
|
throw error;
|
|
20208
20640
|
});
|
|
20209
20641
|
if (!replicasStats) continue;
|
|
20210
20642
|
if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
|
|
20211
|
-
throw refuseUnsafeReplicaPath(profile, `replication replicas path "${
|
|
20643
|
+
throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot}" is not a real directory`);
|
|
20212
20644
|
}
|
|
20213
|
-
for (const replica of await readdir4(
|
|
20214
|
-
const path = join8(
|
|
20215
|
-
const stats = await
|
|
20645
|
+
for (const replica of await readdir4(replicasRoot, { withFileTypes: true })) {
|
|
20646
|
+
const path = join8(replicasRoot, replica.name);
|
|
20647
|
+
const stats = await lstat3(path);
|
|
20216
20648
|
if (!replica.isDirectory() || replica.isSymbolicLink() || stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
20217
20649
|
throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
|
|
20218
20650
|
}
|
|
20219
|
-
const database = await
|
|
20651
|
+
const database = await lstat3(join8(path, "replica.db")).catch((error) => {
|
|
20220
20652
|
if (error.code === "ENOENT") return null;
|
|
20221
20653
|
throw error;
|
|
20222
20654
|
});
|
|
@@ -20232,26 +20664,38 @@ async function acquireLogoutLeases(profile) {
|
|
|
20232
20664
|
await loadSqlite();
|
|
20233
20665
|
sqliteLoaded = true;
|
|
20234
20666
|
}
|
|
20235
|
-
const
|
|
20236
|
-
|
|
20237
|
-
|
|
20238
|
-
|
|
20239
|
-
|
|
20240
|
-
|
|
20241
|
-
|
|
20242
|
-
|
|
20243
|
-
|
|
20667
|
+
const expectedDirectory = await assertSafeFlagReplica(profile, replicationRoot, partition.name, replica.name);
|
|
20668
|
+
const store = await SqliteReplicaStore.open(path, {
|
|
20669
|
+
create: false,
|
|
20670
|
+
expectedDirectory,
|
|
20671
|
+
guard: async (action) => withProfileLock4(profile, async () => {
|
|
20672
|
+
await assertSafeFlagReplica(profile, replicationRoot, partition.name, replica.name);
|
|
20673
|
+
return action();
|
|
20674
|
+
}, { requireProfile: true })
|
|
20675
|
+
});
|
|
20676
|
+
try {
|
|
20677
|
+
if (await store.open() === null) {
|
|
20678
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "replication/${partition.name}/${replica.name}" has no initialized store.`, ExitCode.ERROR);
|
|
20679
|
+
}
|
|
20680
|
+
const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20681
|
+
if (lease === null) {
|
|
20682
|
+
throw new CLIError("REPLICA_BUSY", `Cannot log out: flag replica "replication/${partition.name}/${replica.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
20683
|
+
}
|
|
20684
|
+
opened.push({ path: join8(replicationNamed, partition.name, "replicas", replica.name), store, lease });
|
|
20685
|
+
} catch (error) {
|
|
20686
|
+
await store.close().catch(() => void 0);
|
|
20687
|
+
throw error;
|
|
20244
20688
|
}
|
|
20245
|
-
opened.push({ path, store, lease });
|
|
20246
20689
|
}
|
|
20247
20690
|
}
|
|
20248
20691
|
}
|
|
20249
|
-
return opened;
|
|
20692
|
+
return { stores: opened, roots };
|
|
20250
20693
|
} catch (error) {
|
|
20251
20694
|
await Promise.all(opened.map(async ({ store, lease }) => {
|
|
20252
20695
|
await store.releaseLease(lease).catch(() => void 0);
|
|
20253
20696
|
await store.close().catch(() => void 0);
|
|
20254
20697
|
}));
|
|
20698
|
+
await Promise.all([...roots.values()].map((root) => root.close()));
|
|
20255
20699
|
throw error;
|
|
20256
20700
|
}
|
|
20257
20701
|
});
|
|
@@ -20259,31 +20703,32 @@ async function acquireLogoutLeases(profile) {
|
|
|
20259
20703
|
async function removeProfileReplicasAndReplication(profile) {
|
|
20260
20704
|
return withProfileLock4(profile, async () => {
|
|
20261
20705
|
const acquired = await acquireLogoutLeases(profile);
|
|
20262
|
-
const heldLeases = new Map(acquired.map(({ path, lease }) => [path, lease]));
|
|
20706
|
+
const heldLeases = new Map(acquired.stores.map(({ path, lease }) => [path, lease]));
|
|
20263
20707
|
let legacyRemoved = [];
|
|
20264
20708
|
try {
|
|
20265
|
-
legacyRemoved = await removeProfileReplicas(profile, heldLeases);
|
|
20266
|
-
const replication = await removeProfileReplication(profile, heldLeases);
|
|
20709
|
+
legacyRemoved = await removeProfileReplicas(profile, heldLeases, acquired.roots.get("replicas"));
|
|
20710
|
+
const replication = await removeProfileReplication(profile, heldLeases, acquired.roots.get("replication"));
|
|
20267
20711
|
return [...legacyRemoved, ...replication];
|
|
20268
20712
|
} catch (error) {
|
|
20269
20713
|
const partialRemoved = error instanceof CLIError && Array.isArray(error.metadata?.replicasRemoved) ? error.metadata.replicasRemoved.filter((name) => typeof name === "string") : [];
|
|
20270
20714
|
const removed = [.../* @__PURE__ */ new Set([...legacyRemoved, ...partialRemoved])];
|
|
20271
20715
|
const removedSet = new Set(removed);
|
|
20272
|
-
const remaining = acquired.map(({ path }) => {
|
|
20273
|
-
const name = relative(
|
|
20716
|
+
const remaining = acquired.stores.map(({ path }) => {
|
|
20717
|
+
const name = relative(profilePath4(profile), path).split(sep).join("/");
|
|
20274
20718
|
return name.startsWith("replication/") ? name.replace("/replicas/", "/") : name.split("/").at(-1);
|
|
20275
20719
|
}).filter((name) => !removedSet.has(name));
|
|
20276
20720
|
throw new CLIError(
|
|
20277
20721
|
error instanceof CLIError ? error.code : "REPLICA_PURGE_FAILED",
|
|
20278
|
-
`Logout cleared the session, but replica cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}
|
|
20722
|
+
`Logout cleared the session, but replica cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
|
|
20279
20723
|
error instanceof CLIError ? error.exitCode : ExitCode.ERROR,
|
|
20280
20724
|
{ replicasRemoved: removed, replicasRemaining: remaining }
|
|
20281
20725
|
);
|
|
20282
20726
|
} finally {
|
|
20283
|
-
await Promise.all(acquired.map(async ({ store, lease }) => {
|
|
20727
|
+
await Promise.all(acquired.stores.map(async ({ store, lease }) => {
|
|
20284
20728
|
await store.releaseLease(lease).catch(() => void 0);
|
|
20285
20729
|
await store.close().catch(() => void 0);
|
|
20286
20730
|
}));
|
|
20731
|
+
await Promise.all([...acquired.roots.values()].map((root) => root.close()));
|
|
20287
20732
|
}
|
|
20288
20733
|
});
|
|
20289
20734
|
}
|
|
@@ -20454,7 +20899,7 @@ function shouldOpenBrowser(options) {
|
|
|
20454
20899
|
return env !== "1" && env !== "true";
|
|
20455
20900
|
}
|
|
20456
20901
|
async function callbackFlow(did, options = {}) {
|
|
20457
|
-
return new Promise((
|
|
20902
|
+
return new Promise((resolve6, reject) => {
|
|
20458
20903
|
let timeout;
|
|
20459
20904
|
let settled = false;
|
|
20460
20905
|
let rl;
|
|
@@ -20468,7 +20913,7 @@ async function callbackFlow(did, options = {}) {
|
|
|
20468
20913
|
rl.close();
|
|
20469
20914
|
}
|
|
20470
20915
|
if (result.data) {
|
|
20471
|
-
|
|
20916
|
+
resolve6(result.data);
|
|
20472
20917
|
} else {
|
|
20473
20918
|
reject(result.error);
|
|
20474
20919
|
}
|
|
@@ -20547,8 +20992,8 @@ async function callbackFlow(did, options = {}) {
|
|
|
20547
20992
|
}
|
|
20548
20993
|
if (openBrowser) {
|
|
20549
20994
|
try {
|
|
20550
|
-
const
|
|
20551
|
-
await
|
|
20995
|
+
const open9 = (await import("open")).default;
|
|
20996
|
+
await open9(authUrl);
|
|
20552
20997
|
} catch {
|
|
20553
20998
|
settle({ error: new Error("Failed to open browser") });
|
|
20554
20999
|
return;
|
|
@@ -20592,13 +21037,13 @@ Open this URL in a browser to authenticate:
|
|
|
20592
21037
|
input: process.stdin,
|
|
20593
21038
|
output: process.stderr
|
|
20594
21039
|
});
|
|
20595
|
-
return new Promise((
|
|
21040
|
+
return new Promise((resolve6, reject) => {
|
|
20596
21041
|
let answered = false;
|
|
20597
21042
|
rl.on("line", (input) => {
|
|
20598
21043
|
if (answered || input.trim() === "") return;
|
|
20599
21044
|
answered = true;
|
|
20600
21045
|
rl.close();
|
|
20601
|
-
void delegationFromInput(input, did, options).then(
|
|
21046
|
+
void delegationFromInput(input, did, options).then(resolve6, reject);
|
|
20602
21047
|
});
|
|
20603
21048
|
rl.on("close", () => {
|
|
20604
21049
|
if (answered) return;
|
|
@@ -21499,7 +21944,7 @@ async function acquireDeviceDelegation(input) {
|
|
|
21499
21944
|
expiresAt: new Date(deadline).toISOString().replace(/\.\d{3}Z$/, "Z")
|
|
21500
21945
|
});
|
|
21501
21946
|
let interval = started.interval;
|
|
21502
|
-
const wait = input.wait ?? ((milliseconds) => new Promise((
|
|
21947
|
+
const wait = input.wait ?? ((milliseconds) => new Promise((resolve6) => setTimeout(resolve6, milliseconds)));
|
|
21503
21948
|
while (Date.now() < deadline) {
|
|
21504
21949
|
await wait(interval * 1e3);
|
|
21505
21950
|
let response;
|
|
@@ -22135,7 +22580,7 @@ async function promptAuthMethod() {
|
|
|
22135
22580
|
input: process.stdin,
|
|
22136
22581
|
output: process.stderr
|
|
22137
22582
|
});
|
|
22138
|
-
return new Promise((
|
|
22583
|
+
return new Promise((resolve6) => {
|
|
22139
22584
|
process.stderr.write("\n" + theme.heading("Choose authentication method:") + "\n");
|
|
22140
22585
|
process.stderr.write(` ${theme.accent("1)")} OpenKey ${theme.muted("(browser-based, for interactive use)")}
|
|
22141
22586
|
`);
|
|
@@ -22146,9 +22591,9 @@ async function promptAuthMethod() {
|
|
|
22146
22591
|
rl.close();
|
|
22147
22592
|
const trimmed = answer.trim();
|
|
22148
22593
|
if (trimmed === "2" || trimmed.toLowerCase() === "local") {
|
|
22149
|
-
|
|
22594
|
+
resolve6("local");
|
|
22150
22595
|
} else {
|
|
22151
|
-
|
|
22596
|
+
resolve6("openkey");
|
|
22152
22597
|
}
|
|
22153
22598
|
});
|
|
22154
22599
|
});
|
|
@@ -22850,7 +23295,7 @@ function describeSession(state2, expiresAt) {
|
|
|
22850
23295
|
}
|
|
22851
23296
|
async function emitPermissionRequestArtifact(artifact, emitOption) {
|
|
22852
23297
|
if (typeof emitOption === "string" && emitOption.length > 0) {
|
|
22853
|
-
await mkdir3(
|
|
23298
|
+
await mkdir3(dirname4(emitOption), { recursive: true });
|
|
22854
23299
|
await writeFile2(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
22855
23300
|
outputJson({
|
|
22856
23301
|
emitted: true,
|
|
@@ -22886,13 +23331,13 @@ async function readStdin() {
|
|
|
22886
23331
|
return Buffer.concat(chunks).toString("utf8");
|
|
22887
23332
|
}
|
|
22888
23333
|
function readUrl(source) {
|
|
22889
|
-
return new Promise((
|
|
23334
|
+
return new Promise((resolve6, reject) => {
|
|
22890
23335
|
const getter = source.startsWith("https://") ? httpsGet : httpGet;
|
|
22891
23336
|
const request = getter(source, (response) => {
|
|
22892
23337
|
const status = response.statusCode ?? 0;
|
|
22893
23338
|
if (status >= 300 && status < 400 && response.headers.location) {
|
|
22894
23339
|
response.resume();
|
|
22895
|
-
readUrl(new URL(response.headers.location, source).toString()).then(
|
|
23340
|
+
readUrl(new URL(response.headers.location, source).toString()).then(resolve6, reject);
|
|
22896
23341
|
return;
|
|
22897
23342
|
}
|
|
22898
23343
|
if (status < 200 || status >= 300) {
|
|
@@ -22908,7 +23353,7 @@ function readUrl(source) {
|
|
|
22908
23353
|
response.on("data", (chunk) => {
|
|
22909
23354
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
22910
23355
|
});
|
|
22911
|
-
response.on("end", () =>
|
|
23356
|
+
response.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
|
|
22912
23357
|
});
|
|
22913
23358
|
request.on("error", reject);
|
|
22914
23359
|
});
|
|
@@ -23148,7 +23593,7 @@ function permissionGrantReason(context, permissions) {
|
|
|
23148
23593
|
return `${context} Requested: ${summary}${more}.`;
|
|
23149
23594
|
}
|
|
23150
23595
|
function execCapturedCommand(command) {
|
|
23151
|
-
return new Promise((
|
|
23596
|
+
return new Promise((resolve6, reject) => {
|
|
23152
23597
|
const child = spawn(process.execPath, [process.argv[1], ...command.argv], {
|
|
23153
23598
|
cwd: command.cwd,
|
|
23154
23599
|
env: process.env,
|
|
@@ -23167,7 +23612,7 @@ function execCapturedCommand(command) {
|
|
|
23167
23612
|
if (code3 && code3 !== 0) {
|
|
23168
23613
|
process.exitCode = code3;
|
|
23169
23614
|
}
|
|
23170
|
-
|
|
23615
|
+
resolve6();
|
|
23171
23616
|
});
|
|
23172
23617
|
});
|
|
23173
23618
|
}
|
|
@@ -23196,8 +23641,8 @@ async function confirmPermissionRequest(permissions) {
|
|
|
23196
23641
|
input: process.stdin,
|
|
23197
23642
|
output: process.stderr
|
|
23198
23643
|
});
|
|
23199
|
-
const answer = await new Promise((
|
|
23200
|
-
rl.question("Approve local-key delegation? [y/N] ",
|
|
23644
|
+
const answer = await new Promise((resolve6) => {
|
|
23645
|
+
rl.question("Approve local-key delegation? [y/N] ", resolve6);
|
|
23201
23646
|
});
|
|
23202
23647
|
rl.close();
|
|
23203
23648
|
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
@@ -24042,7 +24487,7 @@ init_errors();
|
|
|
24042
24487
|
init_sdk();
|
|
24043
24488
|
init_theme();
|
|
24044
24489
|
import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
|
|
24045
|
-
import { resolve } from "path";
|
|
24490
|
+
import { resolve as resolve2 } from "path";
|
|
24046
24491
|
function registerDuckdbCommand(program) {
|
|
24047
24492
|
const duckdb = program.command("duckdb").description("DuckDB database operations");
|
|
24048
24493
|
duckdb.command("query <sql>").description("Run a SELECT query").option("--db <name>", "Database name", "default").option("--params <json>", "Bind parameters as JSON array").action(async (sqlStr, options, cmd) => {
|
|
@@ -24154,7 +24599,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
24154
24599
|
}
|
|
24155
24600
|
const blob = result.data;
|
|
24156
24601
|
const buffer = Buffer.from(await blob.arrayBuffer());
|
|
24157
|
-
const outputPath =
|
|
24602
|
+
const outputPath = resolve2(options.output);
|
|
24158
24603
|
await writeFile3(outputPath, buffer);
|
|
24159
24604
|
outputJson({
|
|
24160
24605
|
file: outputPath,
|
|
@@ -24170,7 +24615,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
24170
24615
|
const globalOpts = cmd.optsWithGlobals();
|
|
24171
24616
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
24172
24617
|
const node = await ensureAuthenticated(ctx);
|
|
24173
|
-
const filePath =
|
|
24618
|
+
const filePath = resolve2(file);
|
|
24174
24619
|
const bytes = new Uint8Array(await readFile6(filePath));
|
|
24175
24620
|
const result = await withSpinner(
|
|
24176
24621
|
"Importing database...",
|
|
@@ -24312,11 +24757,11 @@ init_storage();
|
|
|
24312
24757
|
init_errors();
|
|
24313
24758
|
init_constants();
|
|
24314
24759
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
24315
|
-
import { lstat as
|
|
24316
|
-
import { basename as basename2, dirname as
|
|
24760
|
+
import { lstat as lstat4, open as open4, rename as rename2, rm as rm5 } from "fs/promises";
|
|
24761
|
+
import { basename as basename2, dirname as dirname5, join as join9 } from "path";
|
|
24317
24762
|
async function validatePrivateOutput(path, label = "output") {
|
|
24318
24763
|
try {
|
|
24319
|
-
const destination = await
|
|
24764
|
+
const destination = await lstat4(path);
|
|
24320
24765
|
if (!destination.isFile()) {
|
|
24321
24766
|
throw new CLIError("INVALID_ARGUMENT", `${label} "${path}" must be a regular file, not a symlink, directory, or device.`, ExitCode.USAGE_ERROR);
|
|
24322
24767
|
}
|
|
@@ -24324,7 +24769,7 @@ async function validatePrivateOutput(path, label = "output") {
|
|
|
24324
24769
|
if (error?.code !== "ENOENT") throw error;
|
|
24325
24770
|
}
|
|
24326
24771
|
try {
|
|
24327
|
-
const parent = await
|
|
24772
|
+
const parent = await lstat4(dirname5(path));
|
|
24328
24773
|
if (!parent.isDirectory()) {
|
|
24329
24774
|
throw new CLIError("INVALID_ARGUMENT", `${label} "${path}" requires an existing directory parent.`, ExitCode.USAGE_ERROR);
|
|
24330
24775
|
}
|
|
@@ -24335,11 +24780,11 @@ async function validatePrivateOutput(path, label = "output") {
|
|
|
24335
24780
|
}
|
|
24336
24781
|
async function writePrivateOutput(path, value, label = "output") {
|
|
24337
24782
|
await validatePrivateOutput(path, label);
|
|
24338
|
-
const parentPath =
|
|
24783
|
+
const parentPath = dirname5(path);
|
|
24339
24784
|
const temp = join9(parentPath, `.${basename2(path)}.${randomUUID2()}.tmp`);
|
|
24340
24785
|
let created = false;
|
|
24341
24786
|
try {
|
|
24342
|
-
const handle = await
|
|
24787
|
+
const handle = await open4(temp, "wx", PRIVATE_FILE_MODE);
|
|
24343
24788
|
created = true;
|
|
24344
24789
|
try {
|
|
24345
24790
|
await handle.writeFile(value);
|
|
@@ -24354,7 +24799,7 @@ async function writePrivateOutput(path, value, label = "output") {
|
|
|
24354
24799
|
throw new CLIError("ERROR", `Could not write ${label.toLowerCase()} "${path}"${code3}.`, ExitCode.ERROR);
|
|
24355
24800
|
}
|
|
24356
24801
|
try {
|
|
24357
|
-
const parent = await
|
|
24802
|
+
const parent = await open4(parentPath, "r");
|
|
24358
24803
|
try {
|
|
24359
24804
|
await parent.sync();
|
|
24360
24805
|
} finally {
|
|
@@ -24994,8 +25439,8 @@ Examples:
|
|
|
24994
25439
|
try {
|
|
24995
25440
|
if (isInteractive()) {
|
|
24996
25441
|
const rl = createInterface3({ input: process.stdin, output: process.stderr });
|
|
24997
|
-
const answer = await new Promise((
|
|
24998
|
-
rl.question(`Delete profile "${name}"? This cannot be undone. [y/N] `,
|
|
25442
|
+
const answer = await new Promise((resolve6) => {
|
|
25443
|
+
rl.question(`Delete profile "${name}"? This cannot be undone. [y/N] `, resolve6);
|
|
24999
25444
|
});
|
|
25000
25445
|
rl.close();
|
|
25001
25446
|
if (answer.toLowerCase() !== "y") {
|
|
@@ -25030,22 +25475,19 @@ function parseOperatorType(raw) {
|
|
|
25030
25475
|
}
|
|
25031
25476
|
|
|
25032
25477
|
// src/commands/replica.ts
|
|
25033
|
-
init_sdk();
|
|
25034
|
-
init_replication_registry();
|
|
25035
25478
|
import { CLI_NAME as CLI_NAME22 } from "@tinycloud/operations/cli-name";
|
|
25036
25479
|
import { randomBytes as randomBytes6 } from "crypto";
|
|
25037
|
-
import { constants as
|
|
25038
|
-
import { open as
|
|
25039
|
-
import { basename as basename3, dirname as
|
|
25480
|
+
import { constants as fsConstants3, lstatSync as lstatSync2 } from "fs";
|
|
25481
|
+
import { mkdir as mkdir4, open as open5, readdir as readdir5, rename as rename3, unlink as unlink2, lstat as lstat5 } from "fs/promises";
|
|
25482
|
+
import { basename as basename3, dirname as dirname6, join as join10, resolve as resolve3 } from "path";
|
|
25040
25483
|
|
|
25041
25484
|
// src/lib/replication-report.ts
|
|
25042
|
-
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.";
|
|
25043
25485
|
function percentile(values, fraction) {
|
|
25044
25486
|
if (values.length === 0) return null;
|
|
25045
25487
|
values.sort((left, right) => left - right);
|
|
25046
25488
|
return values[Math.ceil(fraction * values.length) - 1] ?? values[values.length - 1];
|
|
25047
25489
|
}
|
|
25048
|
-
function createReplicationReport(since, events,
|
|
25490
|
+
function createReplicationReport(since, events, partitions) {
|
|
25049
25491
|
const reads = events.filter((event) => event.type === "replication.read");
|
|
25050
25492
|
const replicaReads = reads.filter((event) => event.source === "replica");
|
|
25051
25493
|
const sourceLatency = /* @__PURE__ */ new Map();
|
|
@@ -25088,9 +25530,9 @@ function createReplicationReport(since, events, replicas, partitions, warning) {
|
|
|
25088
25530
|
syncs,
|
|
25089
25531
|
writes,
|
|
25090
25532
|
divergences,
|
|
25091
|
-
replicas,
|
|
25533
|
+
replicas: [],
|
|
25092
25534
|
partitions,
|
|
25093
|
-
|
|
25535
|
+
replicasUnavailable: true
|
|
25094
25536
|
};
|
|
25095
25537
|
}
|
|
25096
25538
|
function renderReplicationReport(report) {
|
|
@@ -25103,34 +25545,30 @@ function renderReplicationReport(report) {
|
|
|
25103
25545
|
`Writes: ${Object.entries(report.writes).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
|
|
25104
25546
|
`Read source/reason: ${Object.entries(report.readsBySourceReason).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
|
|
25105
25547
|
`Divergences: ${report.totals.divergences}; showing ${report.divergences.length} of ${report.totals.divergences}`,
|
|
25106
|
-
|
|
25548
|
+
"Replicas: unavailable (read-only report)"
|
|
25107
25549
|
];
|
|
25108
25550
|
for (const divergence of report.divergences) {
|
|
25109
25551
|
if (divergence.type === "replication.divergence") {
|
|
25110
25552
|
lines.push(` ${divergence.space} ${divergence.key}: ${divergence.kind} stalenessMs=${divergence.stalenessMs}`);
|
|
25111
25553
|
}
|
|
25112
25554
|
}
|
|
25113
|
-
for (const replica of report.replicas) {
|
|
25114
|
-
const grant = replica.grant ? ` grant=${replica.grant.cid} parent=${replica.grant.parentCid ?? "\u2014"} expires=${replica.grant.expiresAt ?? "\u2014"}` : " grant=missing";
|
|
25115
|
-
lines.push(` ${replica.prefix}: ${replica.state}; pending in_flight=${replica.pending.inFlight} committed=${replica.pending.committed} ambiguous=${replica.pending.ambiguous}${grant}`);
|
|
25116
|
-
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}`);
|
|
25117
|
-
}
|
|
25118
25555
|
for (const partition of report.partitions) {
|
|
25119
25556
|
lines.push(`Partition ${partition.idHash}: host=${partition.host ?? "unknown"} space=${partition.space ?? "unknown"} pinned=${partition.pinned}`);
|
|
25120
25557
|
}
|
|
25121
25558
|
if (report.partitions.some((partition) => partition.pinned > 100)) lines.push("Warning: a partition holds more than 100 pinned records.");
|
|
25122
|
-
if (report.warning) lines.push(`Warning: ${report.warning}`);
|
|
25123
25559
|
return lines.join("\n");
|
|
25124
25560
|
}
|
|
25125
25561
|
|
|
25126
25562
|
// src/commands/replica.ts
|
|
25127
25563
|
init_dist3();
|
|
25128
25564
|
import {
|
|
25565
|
+
ProfileSymlinkError as ProfileSymlinkError3,
|
|
25129
25566
|
ProfileDeletedError as ProfileDeletedError3,
|
|
25130
|
-
profilePath as
|
|
25567
|
+
profilePath as profilePath5,
|
|
25131
25568
|
readAdditionalDelegations as readAdditionalDelegations2,
|
|
25132
25569
|
readSession as readSession2,
|
|
25133
|
-
withProfileLock as withProfileLock5
|
|
25570
|
+
withProfileLock as withProfileLock5,
|
|
25571
|
+
withPinnedProfileDirectory as withPinnedProfileDirectory3
|
|
25134
25572
|
} from "@tinycloud/operations/state";
|
|
25135
25573
|
import { ownerSessionLacksProof as ownerSessionLacksProof4 } from "@tinycloud/operations/profile";
|
|
25136
25574
|
init_profiles();
|
|
@@ -25186,9 +25624,9 @@ function toCliError(error) {
|
|
|
25186
25624
|
const hint = error.code === ReplicaErrorCode.GRANT_MISSING && typeof error.detail?.hint === "string" ? error.detail.hint : void 0;
|
|
25187
25625
|
return new CLIError(error.code, error.message, EXIT_BY_CODE[error.code], hint === void 0 ? void 0 : { hint });
|
|
25188
25626
|
}
|
|
25189
|
-
async function run(action) {
|
|
25627
|
+
async function run(action, needsSqlite = true) {
|
|
25190
25628
|
try {
|
|
25191
|
-
await loadSqlite();
|
|
25629
|
+
if (needsSqlite) await loadSqlite();
|
|
25192
25630
|
await action();
|
|
25193
25631
|
} catch (error) {
|
|
25194
25632
|
return handleError(toCliError(error));
|
|
@@ -25201,20 +25639,25 @@ async function profileName(cmd) {
|
|
|
25201
25639
|
const config = await ProfileManager.getConfig().catch(() => void 0);
|
|
25202
25640
|
return config?.defaultProfile ?? DEFAULT_PROFILE;
|
|
25203
25641
|
}
|
|
25204
|
-
function replicasRoot(profile) {
|
|
25205
|
-
return join10(profilePath4(profile), "replicas");
|
|
25206
|
-
}
|
|
25207
25642
|
async function replicaNames(profile) {
|
|
25208
|
-
|
|
25209
|
-
|
|
25210
|
-
|
|
25211
|
-
|
|
25212
|
-
()
|
|
25213
|
-
() =>
|
|
25214
|
-
|
|
25215
|
-
|
|
25216
|
-
|
|
25217
|
-
|
|
25643
|
+
return await withReplicaRoot(profile, async (root) => {
|
|
25644
|
+
const names = [];
|
|
25645
|
+
for (const entry of await readdir5(root, { withFileTypes: true })) {
|
|
25646
|
+
if (entry.isSymbolicLink()) throw localPathChanged(profile);
|
|
25647
|
+
if (!NAME.test(entry.name) || !entry.isDirectory()) continue;
|
|
25648
|
+
const present = await withPinnedLocalDirectory(profile, root, entry.name, async (directory) => {
|
|
25649
|
+
const database = await lstat5(join10(directory, "replica.db"), { bigint: true }).catch((error) => {
|
|
25650
|
+
if (error.code === "ENOENT") return null;
|
|
25651
|
+
throw error;
|
|
25652
|
+
});
|
|
25653
|
+
if (database && (database.isSymbolicLink() || !database.isFile())) throw localPathChanged(profile);
|
|
25654
|
+
return database !== null;
|
|
25655
|
+
});
|
|
25656
|
+
if (present === null) throw localPathChanged(profile);
|
|
25657
|
+
if (present) names.push(entry.name);
|
|
25658
|
+
}
|
|
25659
|
+
return names.sort();
|
|
25660
|
+
}) ?? [];
|
|
25218
25661
|
}
|
|
25219
25662
|
function assertName(name) {
|
|
25220
25663
|
if (!NAME.test(name)) {
|
|
@@ -25222,6 +25665,67 @@ function assertName(name) {
|
|
|
25222
25665
|
}
|
|
25223
25666
|
return name;
|
|
25224
25667
|
}
|
|
25668
|
+
async function withReplicaRoot(profile, action, create4 = false) {
|
|
25669
|
+
const profileDirectory = profilePath5(profile);
|
|
25670
|
+
const readRoot = async (directory, recheckProfile) => {
|
|
25671
|
+
if (create4) {
|
|
25672
|
+
if (recheckProfile) await recheckProfile();
|
|
25673
|
+
await mkdir4(join10(directory, "replicas"), { mode: 448 }).catch((error) => {
|
|
25674
|
+
if (error.code !== "EEXIST") throw error;
|
|
25675
|
+
});
|
|
25676
|
+
}
|
|
25677
|
+
return withPinnedLocalDirectory(
|
|
25678
|
+
profile,
|
|
25679
|
+
directory,
|
|
25680
|
+
"replicas",
|
|
25681
|
+
(root, _named, _identity, recheckRoot) => action(root, recheckRoot, recheckProfile)
|
|
25682
|
+
);
|
|
25683
|
+
};
|
|
25684
|
+
if (process.platform === "linux") return readRoot(profileDirectory);
|
|
25685
|
+
const checked = await withPinnedLocalDirectory(
|
|
25686
|
+
profile,
|
|
25687
|
+
dirname6(profileDirectory),
|
|
25688
|
+
basename3(profileDirectory),
|
|
25689
|
+
(directory, _named, _identity, recheckProfile) => readRoot(directory, recheckProfile)
|
|
25690
|
+
);
|
|
25691
|
+
if (checked === null) throw localPathChanged(profile);
|
|
25692
|
+
return checked;
|
|
25693
|
+
}
|
|
25694
|
+
async function withReplicaDirectory(profile, name, action, create4 = false) {
|
|
25695
|
+
assertName(name);
|
|
25696
|
+
const result = await withReplicaRoot(profile, async (root, recheckRoot, recheckProfile) => {
|
|
25697
|
+
if (create4) {
|
|
25698
|
+
await recheckRoot();
|
|
25699
|
+
await mkdir4(join10(root, name), { mode: 448 }).catch((error) => {
|
|
25700
|
+
if (error.code !== "EEXIST") throw error;
|
|
25701
|
+
});
|
|
25702
|
+
}
|
|
25703
|
+
const opened = await withPinnedLocalDirectory(
|
|
25704
|
+
profile,
|
|
25705
|
+
root,
|
|
25706
|
+
name,
|
|
25707
|
+
async (_directory, path, expectedDirectory, recheckChild) => {
|
|
25708
|
+
await recheckRoot();
|
|
25709
|
+
await recheckChild();
|
|
25710
|
+
const database = await lstat5(join10(_directory, "replica.db"), { bigint: true }).catch((error) => {
|
|
25711
|
+
if (error.code === "ENOENT") return null;
|
|
25712
|
+
throw error;
|
|
25713
|
+
});
|
|
25714
|
+
if (database && (database.isSymbolicLink() || !database.isFile())) throw localPathChanged(profile);
|
|
25715
|
+
if (recheckProfile) {
|
|
25716
|
+
await recheckProfile();
|
|
25717
|
+
await recheckRoot();
|
|
25718
|
+
await recheckChild();
|
|
25719
|
+
}
|
|
25720
|
+
return action(path, expectedDirectory);
|
|
25721
|
+
}
|
|
25722
|
+
);
|
|
25723
|
+
if (opened === null) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Replica "${name}" was not found.`);
|
|
25724
|
+
return opened;
|
|
25725
|
+
}, create4);
|
|
25726
|
+
if (result === null) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas.`);
|
|
25727
|
+
return result;
|
|
25728
|
+
}
|
|
25225
25729
|
async function existingReplicaName(profile, option) {
|
|
25226
25730
|
if (option !== void 0) return assertName(option);
|
|
25227
25731
|
const names = await replicaNames(profile);
|
|
@@ -25243,17 +25747,37 @@ function profileGuard(profile) {
|
|
|
25243
25747
|
}
|
|
25244
25748
|
};
|
|
25245
25749
|
}
|
|
25246
|
-
async function
|
|
25247
|
-
return
|
|
25248
|
-
}
|
|
25249
|
-
|
|
25250
|
-
|
|
25251
|
-
|
|
25252
|
-
|
|
25253
|
-
|
|
25254
|
-
return store;
|
|
25750
|
+
async function withOpenReplica(profile, name, action, guard = profileGuard(profile)) {
|
|
25751
|
+
return withReplicaDirectory(profile, name, async (path, expectedDirectory) => {
|
|
25752
|
+
const store = await SqliteReplicaStore.open(path, { create: false, guard, expectedDirectory });
|
|
25753
|
+
try {
|
|
25754
|
+
return await action(store);
|
|
25755
|
+
} finally {
|
|
25756
|
+
await store.close();
|
|
25757
|
+
}
|
|
25255
25758
|
});
|
|
25256
25759
|
}
|
|
25760
|
+
function assertLegacyLogoutClear(profile) {
|
|
25761
|
+
try {
|
|
25762
|
+
lstatSync2(join10(profilePath5(profile), "replica-logout-pending.json"));
|
|
25763
|
+
} catch (error) {
|
|
25764
|
+
if (error?.code === "ENOENT") return;
|
|
25765
|
+
}
|
|
25766
|
+
throw new CLIError(
|
|
25767
|
+
"REPLICA_GATED",
|
|
25768
|
+
"A logout is pending or its state cannot be checked for this profile.",
|
|
25769
|
+
ExitCode.PERMISSION_DENIED
|
|
25770
|
+
);
|
|
25771
|
+
}
|
|
25772
|
+
async function withLegacyReplicaRead(profile, action) {
|
|
25773
|
+
await withProfileLock5(profile, async () => {
|
|
25774
|
+
const checkDelivery = async () => {
|
|
25775
|
+
assertLegacyLogoutClear(profile);
|
|
25776
|
+
};
|
|
25777
|
+
await checkDelivery();
|
|
25778
|
+
await action(checkDelivery);
|
|
25779
|
+
}, { requireProfile: true });
|
|
25780
|
+
}
|
|
25257
25781
|
async function storedGrants(profile) {
|
|
25258
25782
|
const authorizations = [];
|
|
25259
25783
|
const session = await readSession2(profile).catch(() => null);
|
|
@@ -25383,9 +25907,9 @@ function describeStatus(status) {
|
|
|
25383
25907
|
].join("\n");
|
|
25384
25908
|
}
|
|
25385
25909
|
async function writeFileAtomic(path, bytes) {
|
|
25386
|
-
const target =
|
|
25387
|
-
const temp = join10(
|
|
25388
|
-
const handle = await
|
|
25910
|
+
const target = resolve3(path);
|
|
25911
|
+
const temp = join10(dirname6(target), `.${basename3(target)}.${randomBytes6(6).toString("hex")}.tmp`);
|
|
25912
|
+
const handle = await open5(temp, fsConstants3.O_CREAT | fsConstants3.O_EXCL | fsConstants3.O_WRONLY, 384);
|
|
25389
25913
|
try {
|
|
25390
25914
|
await handle.writeFile(bytes);
|
|
25391
25915
|
await handle.sync();
|
|
@@ -25410,14 +25934,86 @@ function replicaSecretsWarning(space, prefix, secretsAllowed) {
|
|
|
25410
25934
|
if (!secretsAllowed || !requiresSecretsOptIn(space, prefix)) return void 0;
|
|
25411
25935
|
return `This replica stores the ciphertext of every secret under "${prefix}". A tinycloud.encryption/decrypt grant covers the whole encryption network, not one secret (TC-755), so whoever holds decrypt can open every replicated secret.`;
|
|
25412
25936
|
}
|
|
25413
|
-
|
|
25414
|
-
|
|
25937
|
+
function localFileId(value) {
|
|
25938
|
+
if (typeof value === "bigint") return value >= 0n ? value : null;
|
|
25939
|
+
return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
|
|
25940
|
+
}
|
|
25941
|
+
function sameLocalEntry(left, right) {
|
|
25942
|
+
const dev = localFileId(left.dev);
|
|
25943
|
+
const ino = localFileId(left.ino);
|
|
25944
|
+
return dev !== null && ino !== null && dev === localFileId(right.dev) && ino === localFileId(right.ino);
|
|
25945
|
+
}
|
|
25946
|
+
function localPathChanged(profile) {
|
|
25947
|
+
return new ProfileSymlinkError3(profile, "its local replica files contain a symlink or changed while reading");
|
|
25948
|
+
}
|
|
25949
|
+
async function assertNamedLocalEntry(profile, path, expected, kind) {
|
|
25950
|
+
const current = await lstat5(path, { bigint: true }).catch(() => null);
|
|
25951
|
+
if (!current || current.isSymbolicLink() || (kind === "directory" ? !current.isDirectory() : !current.isFile()) || !sameLocalEntry(expected, current)) throw localPathChanged(profile);
|
|
25952
|
+
}
|
|
25953
|
+
async function withPinnedLocalDirectory(profile, parent, name, action) {
|
|
25954
|
+
const path = join10(parent, name);
|
|
25955
|
+
const before = await lstat5(path, { bigint: true }).catch((error) => {
|
|
25956
|
+
if (error.code === "ENOENT") return null;
|
|
25957
|
+
throw error;
|
|
25958
|
+
});
|
|
25959
|
+
if (!before) return null;
|
|
25960
|
+
if (before.isSymbolicLink() || !before.isDirectory()) throw localPathChanged(profile);
|
|
25961
|
+
const handle = await open5(path, fsConstants3.O_RDONLY | fsConstants3.O_DIRECTORY | fsConstants3.O_NOFOLLOW).catch((error) => {
|
|
25962
|
+
if (error.code === "ELOOP" || error.code === "ENOTDIR" || error.code === "ENOENT") throw localPathChanged(profile);
|
|
25963
|
+
throw error;
|
|
25964
|
+
});
|
|
25965
|
+
try {
|
|
25966
|
+
const opened = await handle.stat({ bigint: true });
|
|
25967
|
+
if (!opened.isDirectory() || !sameLocalEntry(before, opened)) throw localPathChanged(profile);
|
|
25968
|
+
const dev = localFileId(opened.dev);
|
|
25969
|
+
const ino = localFileId(opened.ino);
|
|
25970
|
+
if (dev === null || ino === null || dev > BigInt(Number.MAX_SAFE_INTEGER) || ino > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
25971
|
+
throw localPathChanged(profile);
|
|
25972
|
+
}
|
|
25973
|
+
const pinned = process.platform === "linux" ? `/proc/self/fd/${handle.fd}` : path;
|
|
25974
|
+
const recheck = () => assertNamedLocalEntry(profile, path, opened, "directory");
|
|
25975
|
+
const result = await action(pinned, path, { dev: Number(dev), ino: Number(ino) }, recheck);
|
|
25976
|
+
await recheck();
|
|
25977
|
+
return result;
|
|
25978
|
+
} finally {
|
|
25979
|
+
await handle.close();
|
|
25980
|
+
}
|
|
25981
|
+
}
|
|
25982
|
+
async function readReportFile(profile, parent, name) {
|
|
25983
|
+
const path = join10(parent, name);
|
|
25984
|
+
const before = await lstat5(path, { bigint: true }).catch((error) => {
|
|
25985
|
+
if (error.code === "ENOENT") return null;
|
|
25986
|
+
throw error;
|
|
25987
|
+
});
|
|
25988
|
+
if (!before) return null;
|
|
25989
|
+
if (before.isSymbolicLink() || !before.isFile()) throw localPathChanged(profile);
|
|
25990
|
+
const handle = await open5(path, fsConstants3.O_RDONLY | fsConstants3.O_NOFOLLOW).catch((error) => {
|
|
25991
|
+
if (error.code === "ELOOP" || error.code === "ENOTDIR" || error.code === "ENOENT") throw localPathChanged(profile);
|
|
25992
|
+
throw error;
|
|
25993
|
+
});
|
|
25994
|
+
try {
|
|
25995
|
+
const opened = await handle.stat({ bigint: true });
|
|
25996
|
+
if (!opened.isFile() || !sameLocalEntry(before, opened)) throw localPathChanged(profile);
|
|
25997
|
+
const data = await handle.readFile("utf8");
|
|
25998
|
+
await assertNamedLocalEntry(profile, path, opened, "file");
|
|
25999
|
+
return data;
|
|
26000
|
+
} finally {
|
|
26001
|
+
await handle.close();
|
|
26002
|
+
}
|
|
26003
|
+
}
|
|
26004
|
+
async function readReportJson(profile, parent, name) {
|
|
26005
|
+
const data = await readReportFile(profile, parent, name);
|
|
26006
|
+
if (data === null) return null;
|
|
26007
|
+
try {
|
|
26008
|
+
return JSON.parse(data);
|
|
26009
|
+
} catch {
|
|
26010
|
+
return null;
|
|
26011
|
+
}
|
|
26012
|
+
}
|
|
26013
|
+
async function readReplicationEvents(profile, root, sinceMs) {
|
|
25415
26014
|
const events = [];
|
|
25416
26015
|
for (const name of ["events.jsonl.1", "events.jsonl"]) {
|
|
25417
|
-
const data = await
|
|
25418
|
-
if (error.code === "ENOENT") return "";
|
|
25419
|
-
throw error;
|
|
25420
|
-
});
|
|
26016
|
+
const data = await readReportFile(profile, root, name) ?? "";
|
|
25421
26017
|
for (const line of data.split("\n")) {
|
|
25422
26018
|
if (!line) continue;
|
|
25423
26019
|
try {
|
|
@@ -25429,27 +26025,26 @@ async function readReplicationEvents(profile, sinceMs) {
|
|
|
25429
26025
|
}
|
|
25430
26026
|
return events;
|
|
25431
26027
|
}
|
|
25432
|
-
async function replicationPartitions(profile) {
|
|
25433
|
-
const
|
|
25434
|
-
const entries = await readdir5(root, { withFileTypes: true }).catch((error) => {
|
|
25435
|
-
if (error.code === "ENOENT") return [];
|
|
25436
|
-
throw error;
|
|
25437
|
-
});
|
|
26028
|
+
async function replicationPartitions(profile, root) {
|
|
26029
|
+
const entries = await readdir5(root, { withFileTypes: true });
|
|
25438
26030
|
const partitions = [];
|
|
25439
26031
|
for (const entry of entries) {
|
|
25440
|
-
if (
|
|
25441
|
-
|
|
25442
|
-
|
|
25443
|
-
|
|
25444
|
-
|
|
25445
|
-
|
|
25446
|
-
|
|
25447
|
-
|
|
25448
|
-
|
|
25449
|
-
|
|
25450
|
-
|
|
25451
|
-
|
|
26032
|
+
if (!/^[a-z2-7]{26}$/.test(entry.name)) continue;
|
|
26033
|
+
if (entry.isSymbolicLink()) throw localPathChanged(profile);
|
|
26034
|
+
if (!entry.isDirectory()) continue;
|
|
26035
|
+
const partition = await withPinnedLocalDirectory(profile, root, entry.name, async (pinned) => {
|
|
26036
|
+
const identity = await readReportJson(profile, pinned, "identity.json");
|
|
26037
|
+
const pending = await readReportJson(profile, pinned, "pending.json");
|
|
26038
|
+
const pinnedCount = pending?.records?.filter((record2) => record2.state === "in_flight" || record2.state === "ambiguous").length ?? 0;
|
|
26039
|
+
return {
|
|
26040
|
+
idHash: entry.name,
|
|
26041
|
+
host: typeof identity?.host === "string" ? identity.host : null,
|
|
26042
|
+
space: typeof identity?.space === "string" ? identity.space : null,
|
|
26043
|
+
pinned: pinnedCount
|
|
26044
|
+
};
|
|
25452
26045
|
});
|
|
26046
|
+
if (!partition) throw localPathChanged(profile);
|
|
26047
|
+
partitions.push(partition);
|
|
25453
26048
|
}
|
|
25454
26049
|
return partitions.sort((left, right) => left.idHash.localeCompare(right.idHash));
|
|
25455
26050
|
}
|
|
@@ -25468,52 +26063,41 @@ SECRETS_OPT_IN_REQUIRED; 3 grant missing; 4 key absent, deleted, content missing
|
|
|
25468
26063
|
or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
|
|
25469
26064
|
7 node, protocol or integrity error; 10 storage full.`
|
|
25470
26065
|
);
|
|
25471
|
-
replica.command("report").description("
|
|
26066
|
+
replica.command("report").description("Read-only report of persisted replication events and identity partitions").option("--since <duration>", "Include events from the last duration (default: 24h)", "24h").option("--purge", "Refused: named replica reset --replica <name> --purge only; flag-owned purge unavailable (TC-1309)").option("--clear-pending", "Refused: no equivalent pending-clear command yet (TC-1309)").action(
|
|
25472
26067
|
(options, cmd) => run(async () => {
|
|
25473
|
-
const globals = cmd.optsWithGlobals();
|
|
25474
|
-
const context = await ProfileManager.resolveContext(globals);
|
|
25475
|
-
const node = context.replication ? await ensureAuthenticated(context) : void 0;
|
|
25476
26068
|
const profile = await profileName(cmd);
|
|
26069
|
+
if (options.purge) {
|
|
26070
|
+
throw new CLIError("REPORT_READ_ONLY", `Replica report is read-only. To remove a named local replica, run ${CLI_NAME22} replica reset --replica <name> --purge; flag-owned purge has no equivalent yet (TC-1309).`, ExitCode.USAGE_ERROR);
|
|
26071
|
+
}
|
|
26072
|
+
if (options.clearPending) {
|
|
26073
|
+
throw new CLIError("REPORT_READ_ONLY", "Clearing pending replica writes has no equivalent yet; see TC-1309 for a safe replacement command.", ExitCode.USAGE_ERROR);
|
|
26074
|
+
}
|
|
25477
26075
|
let durationMs;
|
|
25478
26076
|
try {
|
|
25479
26077
|
durationMs = parseDuration(options.since);
|
|
25480
26078
|
} catch (error) {
|
|
25481
26079
|
throw new CLIError("USAGE_ERROR", error instanceof Error ? error.message : String(error), ExitCode.USAGE_ERROR);
|
|
25482
26080
|
}
|
|
25483
|
-
const
|
|
25484
|
-
|
|
25485
|
-
|
|
25486
|
-
|
|
25487
|
-
|
|
25488
|
-
|
|
25489
|
-
|
|
25490
|
-
|
|
25491
|
-
|
|
25492
|
-
|
|
25493
|
-
|
|
25494
|
-
|
|
25495
|
-
|
|
25496
|
-
|
|
25497
|
-
|
|
25498
|
-
|
|
25499
|
-
|
|
25500
|
-
|
|
25501
|
-
const report = createReplicationReport(options.since, events, replicas, partitions, warning);
|
|
25502
|
-
if (shouldOutputJson()) {
|
|
25503
|
-
outputJson({
|
|
25504
|
-
...report,
|
|
25505
|
-
...cleared === void 0 ? {} : { cleared },
|
|
25506
|
-
...purged === void 0 ? {} : { purged }
|
|
25507
|
-
});
|
|
25508
|
-
} else {
|
|
25509
|
-
process.stdout.write(`${renderReplicationReport(report)}
|
|
25510
|
-
`);
|
|
25511
|
-
if (cleared !== void 0) process.stdout.write(`Cleared ${cleared} pending write record(s).
|
|
25512
|
-
`);
|
|
25513
|
-
if (purged !== void 0) process.stdout.write(`Purge: ${JSON.stringify(purged)}
|
|
26081
|
+
const report = await withPinnedProfileDirectory3(profile, async (directory) => {
|
|
26082
|
+
if (directory === null) throw new CLIError("PROFILE_NOT_FOUND", `Profile "${profile}" does not exist.`, ExitCode.ERROR);
|
|
26083
|
+
const readPersistedReport = async (root) => {
|
|
26084
|
+
await replicaNames(profile);
|
|
26085
|
+
const sinceMs = Date.now() - durationMs;
|
|
26086
|
+
const persisted = await withPinnedLocalDirectory(profile, root, "replication", async (directory2) => ({
|
|
26087
|
+
events: await readReplicationEvents(profile, directory2, sinceMs),
|
|
26088
|
+
partitions: await replicationPartitions(profile, directory2)
|
|
26089
|
+
}));
|
|
26090
|
+
return createReplicationReport(options.since, persisted?.events ?? [], persisted?.partitions ?? []);
|
|
26091
|
+
};
|
|
26092
|
+
if (process.platform === "linux") return readPersistedReport(directory);
|
|
26093
|
+
const checked = await withPinnedLocalDirectory(profile, dirname6(directory), basename3(directory), readPersistedReport);
|
|
26094
|
+
if (checked === null) throw localPathChanged(profile);
|
|
26095
|
+
return checked;
|
|
26096
|
+
});
|
|
26097
|
+
if (shouldOutputJson()) outputJson(report);
|
|
26098
|
+
else process.stdout.write(`${renderReplicationReport(report)}
|
|
25514
26099
|
`);
|
|
25515
|
-
|
|
25516
|
-
})
|
|
26100
|
+
}, false)
|
|
25517
26101
|
);
|
|
25518
26102
|
replica.command("sync").description("Create or update a replica from its source host").option("--replica <name>", "Replica name (default: the profile's only replica, or one named after the prefix)").option("--space <id|name>", "Space to replicate (first sync; default: the space of the sync grant)").option("--prefix <prefix>", "KV prefix to replicate, e.g. notes/ (first sync)").option("--retention-grant <cid>", "CID of a tinycloud.kv/retain grant: keep local reads after the sync grant expires").option("--limit <n>", "Feed page size (1-1000)", (value) => Number.parseInt(value, 10)).option("--allow-secrets", "Opt in to copying ciphertext from a secrets space, vault prefix, or whole space").action(
|
|
25519
26103
|
(options, cmd) => run(async () => {
|
|
@@ -25525,87 +26109,98 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
|
|
|
25525
26109
|
throw new CLIError("INVALID_REPLICATION_PREFIX", error instanceof Error ? error.message : String(error), ExitCode.USAGE_ERROR);
|
|
25526
26110
|
}
|
|
25527
26111
|
}
|
|
25528
|
-
|
|
25529
|
-
|
|
25530
|
-
|
|
25531
|
-
|
|
25532
|
-
|
|
25533
|
-
|
|
25534
|
-
|
|
25535
|
-
|
|
25536
|
-
|
|
25537
|
-
|
|
25538
|
-
|
|
25539
|
-
|
|
25540
|
-
|
|
25541
|
-
|
|
25542
|
-
|
|
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
|
-
|
|
26112
|
+
await withPinnedProfileDirectory3(profile, async (directory) => {
|
|
26113
|
+
if (directory === null) throw new CLIError("PROFILE_NOT_FOUND", `Profile "${profile}" does not exist.`, ExitCode.ERROR);
|
|
26114
|
+
const globals = cmd.optsWithGlobals();
|
|
26115
|
+
const config = await ProfileManager.getProfile(profile);
|
|
26116
|
+
const deviceDid = config.sessionDid ?? config.did;
|
|
26117
|
+
const jwk = await ProfileManager.getKey(profile);
|
|
26118
|
+
if (jwk === null) {
|
|
26119
|
+
throw new CLIError("AUTH_REQUIRED", `Profile "${profile}" has no device key.`, ExitCode.AUTH_REQUIRED);
|
|
26120
|
+
}
|
|
26121
|
+
if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
|
|
26122
|
+
throw new CLIError("USAGE_ERROR", "--limit must be an integer from 1 through 1000.", ExitCode.USAGE_ERROR);
|
|
26123
|
+
}
|
|
26124
|
+
const existing = await replicaNames(profile);
|
|
26125
|
+
const name = options.replica !== void 0 ? assertName(options.replica) : options.prefix !== void 0 ? assertName(options.prefix.replace(/\/+$/, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[^A-Za-z0-9]+/, "") || "default") : await existingReplicaName(profile, void 0);
|
|
26126
|
+
const grants = await storedGrants(profile);
|
|
26127
|
+
const initialConfig = existing.includes(name) ? null : (() => {
|
|
26128
|
+
const prefix = options.prefix;
|
|
26129
|
+
if (prefix === void 0) {
|
|
26130
|
+
throw new CLIError("USAGE_ERROR", "The first sync of a replica needs --prefix (and a device grant covering it).", ExitCode.USAGE_ERROR);
|
|
26131
|
+
}
|
|
26132
|
+
const space = resolveSpace(options.space, prefix, grants);
|
|
26133
|
+
if (requiresSecretsOptIn(space, prefix) && options.allowSecrets !== true) {
|
|
26134
|
+
throw new ReplicaError(
|
|
26135
|
+
ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
|
|
26136
|
+
`Replicating ${space}/kv/${prefix} copies encrypted secret material to this disk; pass --allow-secrets to opt in.`
|
|
26137
|
+
);
|
|
26138
|
+
}
|
|
26139
|
+
const host = (globals.host ?? process.env.TC_HOST ?? config.host)?.replace(/\/+$/, "");
|
|
26140
|
+
if (!host) throw new CLIError("USAGE_ERROR", "Pass --host: the profile has no host to pin.", ExitCode.USAGE_ERROR);
|
|
26141
|
+
chooseGrant(grants, deviceDid, space, prefix);
|
|
26142
|
+
return {
|
|
26143
|
+
name,
|
|
26144
|
+
replicaId: randomBytes6(16).toString("hex"),
|
|
26145
|
+
host,
|
|
26146
|
+
space,
|
|
26147
|
+
prefix,
|
|
26148
|
+
deviceDid,
|
|
26149
|
+
allowSecrets: options.allowSecrets === true,
|
|
26150
|
+
localReadPolicy: "whileGrantValid",
|
|
26151
|
+
retentionGrantCid: null
|
|
26152
|
+
};
|
|
26153
|
+
})();
|
|
26154
|
+
const { replica: replica2, report, status, warning } = await withReplicaDirectory(profile, name, async (path, expectedDirectory) => {
|
|
26155
|
+
const store = await SqliteReplicaStore.open(path, {
|
|
26156
|
+
create: initialConfig !== null,
|
|
26157
|
+
guard: profileGuard(profile),
|
|
26158
|
+
expectedDirectory
|
|
26159
|
+
});
|
|
26160
|
+
try {
|
|
26161
|
+
if (initialConfig !== null && await store.open() === null) await store.init(initialConfig);
|
|
26162
|
+
const state2 = await store.open();
|
|
26163
|
+
if (initialConfig === null) {
|
|
26164
|
+
const differs = syncOptionsDiffer(state2.config, {
|
|
26165
|
+
...options.space === void 0 ? {} : { space: options.space },
|
|
26166
|
+
...options.prefix === void 0 ? {} : { prefix: options.prefix },
|
|
26167
|
+
...globals.host === void 0 ? {} : { host: globals.host }
|
|
26168
|
+
});
|
|
26169
|
+
if (differs !== void 0) {
|
|
26170
|
+
throw new ReplicaError(
|
|
26171
|
+
ReplicaErrorCode.CONFIG_MISMATCH,
|
|
26172
|
+
`Replica ${name} already follows ${differs}. Use another --replica name, or ${CLI_NAME22} replica reset --replica ${name} --purge first.`
|
|
26173
|
+
);
|
|
26174
|
+
}
|
|
26175
|
+
}
|
|
26176
|
+
const { space, prefix, host } = state2.config;
|
|
26177
|
+
if (requiresSecretsOptIn(space, prefix) && !state2.config.allowSecrets && options.allowSecrets !== true) {
|
|
26178
|
+
throw new ReplicaError(ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED, `Replicating ${space}/kv/${prefix} needs --allow-secrets.`);
|
|
26179
|
+
}
|
|
26180
|
+
let grant;
|
|
26181
|
+
try {
|
|
26182
|
+
grant = chooseGrant(grants, state2.config.deviceDid, space, prefix);
|
|
26183
|
+
} catch (error) {
|
|
26184
|
+
if (!isReplicaError(error, ReplicaErrorCode.GRANT_MISSING) || (state2.grant ?? state2.pendingGrant) === null) throw error;
|
|
26185
|
+
}
|
|
26186
|
+
if (grant !== void 0) await store.installGrant(grant);
|
|
26187
|
+
if (options.retentionGrant !== void 0 && options.retentionGrant !== state2.config.retentionGrantCid) {
|
|
26188
|
+
await store.setRetentionGrant(options.retentionGrant === "" ? null : options.retentionGrant);
|
|
26189
|
+
}
|
|
26190
|
+
const current = await store.open();
|
|
26191
|
+
const transportFor = await grantTransports({ host, space, deviceDid: current.config.deviceDid, jwk });
|
|
26192
|
+
const warning2 = replicaSecretsWarning(space, prefix, options.allowSecrets === true || state2.config.allowSecrets);
|
|
26193
|
+
const report2 = await new Replica({ store, transportFor }).sync(options.limit === void 0 ? {} : { limit: options.limit });
|
|
26194
|
+
const status2 = await store.status();
|
|
26195
|
+
return { replica: current.config.name, report: report2, status: status2, warning: warning2 };
|
|
26196
|
+
} finally {
|
|
26197
|
+
await store.close();
|
|
26198
|
+
}
|
|
26199
|
+
}, initialConfig !== null);
|
|
25603
26200
|
if (warning !== void 0) process.stderr.write(`Warning: ${warning}
|
|
25604
26201
|
`);
|
|
25605
|
-
const report = await new Replica({ store, transportFor }).sync(options.limit === void 0 ? {} : { limit: options.limit });
|
|
25606
|
-
const status = await store.status();
|
|
25607
26202
|
if (shouldOutputJson()) {
|
|
25608
|
-
outputJson({ replica:
|
|
26203
|
+
outputJson({ replica: replica2, sync: report, status, ...warning === void 0 ? {} : { warning } });
|
|
25609
26204
|
} else {
|
|
25610
26205
|
process.stdout.write(
|
|
25611
26206
|
`${theme.success("\u2713")} Synced ${report.changes} change(s) in ${report.pages} page(s), fetched ${report.fetched} value(s).
|
|
@@ -25613,40 +26208,39 @@ ${describeStatus(status)}
|
|
|
25613
26208
|
`
|
|
25614
26209
|
);
|
|
25615
26210
|
}
|
|
25616
|
-
}
|
|
25617
|
-
await store.close();
|
|
25618
|
-
}
|
|
26211
|
+
});
|
|
25619
26212
|
})
|
|
25620
26213
|
);
|
|
25621
26214
|
replica.command("get <key>").description("Read a key from the local replica (never touches the network)").option("--replica <name>", "Replica name").option("--raw", "Write the value's bytes to stdout").option("-o, --output <file>", "Write the value to a file (atomically, mode 0600)").option("--no-verify", "Skip re-hashing the stored content").action(
|
|
25622
26215
|
(key, options, cmd) => run(async () => {
|
|
25623
26216
|
const profile = await profileName(cmd);
|
|
25624
|
-
|
|
25625
|
-
|
|
25626
|
-
|
|
25627
|
-
|
|
25628
|
-
|
|
25629
|
-
|
|
25630
|
-
|
|
25631
|
-
|
|
25632
|
-
|
|
25633
|
-
|
|
25634
|
-
|
|
25635
|
-
|
|
25636
|
-
|
|
25637
|
-
|
|
25638
|
-
|
|
25639
|
-
|
|
25640
|
-
|
|
26217
|
+
await withLegacyReplicaRead(profile, async (checkDelivery) => {
|
|
26218
|
+
const result = await withOpenReplica(
|
|
26219
|
+
profile,
|
|
26220
|
+
await existingReplicaName(profile, options.replica),
|
|
26221
|
+
(store) => new Replica({ store }).get(key, { verify: options.verify !== false })
|
|
26222
|
+
);
|
|
26223
|
+
await checkDelivery();
|
|
26224
|
+
if (result.status !== "present") {
|
|
26225
|
+
const failure = READ_FAILURE[result.status];
|
|
26226
|
+
throw new CLIError(failure.code, `Key ${JSON.stringify(key)} ${failure.what}.`, failure.exit);
|
|
26227
|
+
}
|
|
26228
|
+
if (options.output) {
|
|
26229
|
+
await writeFileAtomic(options.output, result.value);
|
|
26230
|
+
outputJson({ key, written: options.output, etag: result.etag, meta: result.meta });
|
|
26231
|
+
return;
|
|
26232
|
+
}
|
|
26233
|
+
if (options.raw) {
|
|
26234
|
+
process.stdout.write(result.value);
|
|
26235
|
+
return;
|
|
26236
|
+
}
|
|
26237
|
+
if (shouldOutputJson()) {
|
|
26238
|
+
outputJson({ key, status: result.status, ...jsonValue(result.value), etag: result.etag, metadata: result.metadata, meta: result.meta });
|
|
26239
|
+
return;
|
|
26240
|
+
}
|
|
25641
26241
|
process.stdout.write(result.value);
|
|
25642
|
-
|
|
25643
|
-
}
|
|
25644
|
-
if (shouldOutputJson()) {
|
|
25645
|
-
outputJson({ key, status: result.status, ...jsonValue(result.value), etag: result.etag, metadata: result.metadata, meta: result.meta });
|
|
25646
|
-
return;
|
|
25647
|
-
}
|
|
25648
|
-
process.stdout.write(result.value);
|
|
25649
|
-
process.stdout.write("\n");
|
|
26242
|
+
process.stdout.write("\n");
|
|
26243
|
+
});
|
|
25650
26244
|
})
|
|
25651
26245
|
);
|
|
25652
26246
|
replica.command("list [prefix]").description("List live keys in the local replica (never touches the network)").option("--replica <name>", "Replica name").option("--after <key>", "Start after this key").option("--limit <n>", "Maximum keys", (value) => Number.parseInt(value, 10)).action(
|
|
@@ -25655,13 +26249,17 @@ ${describeStatus(status)}
|
|
|
25655
26249
|
throw new CLIError("USAGE_ERROR", "--limit must be a positive integer.", ExitCode.USAGE_ERROR);
|
|
25656
26250
|
}
|
|
25657
26251
|
const profile = await profileName(cmd);
|
|
25658
|
-
|
|
25659
|
-
|
|
25660
|
-
|
|
25661
|
-
|
|
25662
|
-
|
|
25663
|
-
|
|
25664
|
-
|
|
26252
|
+
await withLegacyReplicaRead(profile, async (checkDelivery) => {
|
|
26253
|
+
const { entries, meta } = await withOpenReplica(
|
|
26254
|
+
profile,
|
|
26255
|
+
await existingReplicaName(profile, options.replica),
|
|
26256
|
+
(store) => new Replica({ store }).list({
|
|
26257
|
+
...prefix === void 0 ? {} : { prefix },
|
|
26258
|
+
...options.after === void 0 ? {} : { after: options.after },
|
|
26259
|
+
...options.limit === void 0 ? {} : { limit: options.limit }
|
|
26260
|
+
})
|
|
26261
|
+
);
|
|
26262
|
+
await checkDelivery();
|
|
25665
26263
|
if (shouldOutputJson()) {
|
|
25666
26264
|
outputJson({ keys: entries, count: entries.length, prefix: prefix ?? null, meta });
|
|
25667
26265
|
} else if (entries.length === 0) {
|
|
@@ -25674,50 +26272,57 @@ ${describeStatus(status)}
|
|
|
25674
26272
|
) + "\n"
|
|
25675
26273
|
);
|
|
25676
26274
|
}
|
|
25677
|
-
}
|
|
25678
|
-
await store.close();
|
|
25679
|
-
}
|
|
26275
|
+
});
|
|
25680
26276
|
})
|
|
25681
26277
|
);
|
|
25682
26278
|
replica.command("status").description("Show one replica's sync state, authority and counts, or all of them").option("--replica <name>", "Replica name").action(
|
|
25683
26279
|
(options, cmd) => run(async () => {
|
|
25684
26280
|
const profile = await profileName(cmd);
|
|
25685
|
-
|
|
25686
|
-
|
|
25687
|
-
|
|
25688
|
-
const
|
|
25689
|
-
|
|
25690
|
-
statuses.push(await new Replica({ store }).status());
|
|
25691
|
-
} finally {
|
|
25692
|
-
await store.close();
|
|
26281
|
+
await withLegacyReplicaRead(profile, async (checkDelivery) => {
|
|
26282
|
+
const names = options.replica === void 0 ? await replicaNames(profile) : [assertName(options.replica)];
|
|
26283
|
+
const statuses = [];
|
|
26284
|
+
for (const name of names) {
|
|
26285
|
+
statuses.push(await withOpenReplica(profile, name, (store) => new Replica({ store }).status()));
|
|
25693
26286
|
}
|
|
25694
|
-
|
|
25695
|
-
|
|
25696
|
-
|
|
25697
|
-
|
|
25698
|
-
|
|
25699
|
-
|
|
25700
|
-
|
|
25701
|
-
|
|
26287
|
+
await checkDelivery();
|
|
26288
|
+
if (shouldOutputJson()) {
|
|
26289
|
+
outputJson(options.replica === void 0 ? { replicas: statuses } : statuses[0]);
|
|
26290
|
+
} else if (statuses.length === 0) {
|
|
26291
|
+
process.stdout.write(theme.muted(`Profile "${profile}" has no replicas.`) + "\n");
|
|
26292
|
+
} else {
|
|
26293
|
+
process.stdout.write(statuses.map(describeStatus).join("\n\n") + "\n");
|
|
26294
|
+
}
|
|
26295
|
+
});
|
|
25702
26296
|
})
|
|
25703
26297
|
);
|
|
25704
26298
|
replica.command("reset").description("Clear a replica's entries, content and cursor (keeps its configuration and grant)").option("--replica <name>", "Replica name").option("--purge", "Remove the replica entirely").action(
|
|
25705
26299
|
(options, cmd) => run(async () => {
|
|
25706
26300
|
const profile = await profileName(cmd);
|
|
25707
|
-
|
|
25708
|
-
|
|
25709
|
-
try {
|
|
26301
|
+
await withProfileLock5(profile, async () => {
|
|
26302
|
+
const name = await existingReplicaName(profile, options.replica);
|
|
25710
26303
|
if (options.purge) {
|
|
25711
|
-
|
|
25712
|
-
|
|
25713
|
-
|
|
26304
|
+
await withPinnedReplicaForReset(profile, name, async (path, recheck, expectedDirectory) => {
|
|
26305
|
+
const profileLock = profileGuard(profile);
|
|
26306
|
+
const safeGuard = (section) => profileLock(async () => {
|
|
26307
|
+
await recheck();
|
|
26308
|
+
return section();
|
|
26309
|
+
});
|
|
26310
|
+
const store = await SqliteReplicaStore.open(path, { create: false, guard: safeGuard, expectedDirectory });
|
|
26311
|
+
try {
|
|
26312
|
+
await recheck();
|
|
26313
|
+
const lease = await store.acquireSyncLease(6e4);
|
|
26314
|
+
if (lease === null) throw new ReplicaError(ReplicaErrorCode.BUSY, "Another process is syncing this replica.");
|
|
26315
|
+
await recheck();
|
|
26316
|
+
await store.destroy(lease);
|
|
26317
|
+
} finally {
|
|
26318
|
+
await store.close();
|
|
26319
|
+
}
|
|
26320
|
+
});
|
|
25714
26321
|
} else {
|
|
25715
|
-
await new Replica({ store }).reset("manual");
|
|
26322
|
+
await withOpenReplica(profile, name, (store) => new Replica({ store }).reset("manual"));
|
|
25716
26323
|
}
|
|
25717
|
-
|
|
25718
|
-
|
|
25719
|
-
}
|
|
25720
|
-
outputJson({ replica: name, reset: true, purged: options.purge === true });
|
|
26324
|
+
outputJson({ replica: name, reset: true, purged: options.purge === true });
|
|
26325
|
+
}, { requireProfile: true });
|
|
25721
26326
|
})
|
|
25722
26327
|
);
|
|
25723
26328
|
}
|
|
@@ -25730,7 +26335,7 @@ init_errors();
|
|
|
25730
26335
|
init_warnings();
|
|
25731
26336
|
init_constants();
|
|
25732
26337
|
import { CLI_NAME as CLI_NAME24 } from "@tinycloud/operations/cli-name";
|
|
25733
|
-
import { readFile as
|
|
26338
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
25734
26339
|
import { join as join11 } from "path";
|
|
25735
26340
|
import { homedir, hostname, userInfo } from "os";
|
|
25736
26341
|
import { appSecretScope } from "@tinycloud/sdk-core";
|
|
@@ -25759,24 +26364,24 @@ init_space();
|
|
|
25759
26364
|
// src/lib/browser.ts
|
|
25760
26365
|
init_formatter();
|
|
25761
26366
|
init_theme();
|
|
25762
|
-
import
|
|
26367
|
+
import open6 from "open";
|
|
25763
26368
|
var LAUNCH_CHECK_MS = 1e3;
|
|
25764
26369
|
async function tryOpenBrowser(url) {
|
|
25765
26370
|
let launcher;
|
|
25766
26371
|
try {
|
|
25767
|
-
launcher = await
|
|
26372
|
+
launcher = await open6(url);
|
|
25768
26373
|
} catch {
|
|
25769
26374
|
return false;
|
|
25770
26375
|
}
|
|
25771
|
-
return new Promise((
|
|
25772
|
-
const timer = setTimeout(() =>
|
|
26376
|
+
return new Promise((resolve6) => {
|
|
26377
|
+
const timer = setTimeout(() => resolve6(true), LAUNCH_CHECK_MS);
|
|
25773
26378
|
launcher.once("error", () => {
|
|
25774
26379
|
clearTimeout(timer);
|
|
25775
|
-
|
|
26380
|
+
resolve6(false);
|
|
25776
26381
|
});
|
|
25777
26382
|
launcher.once("exit", (code3) => {
|
|
25778
26383
|
clearTimeout(timer);
|
|
25779
|
-
|
|
26384
|
+
resolve6(code3 === 0);
|
|
25780
26385
|
});
|
|
25781
26386
|
});
|
|
25782
26387
|
}
|
|
@@ -25820,9 +26425,9 @@ function runProbeChild(request, options = {}) {
|
|
|
25820
26425
|
const killGraceMs = options.killGraceMs ?? KILL_GRACE_MS;
|
|
25821
26426
|
const spawn2 = options.spawn ?? nodeSpawn;
|
|
25822
26427
|
const signal = options.signal;
|
|
25823
|
-
return new Promise((
|
|
26428
|
+
return new Promise((resolve6) => {
|
|
25824
26429
|
if (signal?.aborted) {
|
|
25825
|
-
|
|
26430
|
+
resolve6({ kind: "cancelled" });
|
|
25826
26431
|
return;
|
|
25827
26432
|
}
|
|
25828
26433
|
const child = spawn2(command, args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -25858,7 +26463,7 @@ function runProbeChild(request, options = {}) {
|
|
|
25858
26463
|
const envelope = stdout.length > 0 && stdout.length <= MAX_PROBE_LINE_BYTES ? parseStatusEnvelope(stdout.toString("utf8")) : void 0;
|
|
25859
26464
|
outcome = envelope !== void 0 ? { kind: "ok", envelope } : probeFailed(stderr.toString("utf8"));
|
|
25860
26465
|
}
|
|
25861
|
-
|
|
26466
|
+
resolve6(outcome);
|
|
25862
26467
|
};
|
|
25863
26468
|
if (child.pid !== void 0) options.onSpawn?.(child.pid);
|
|
25864
26469
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -25950,15 +26555,15 @@ var TIMEOUT_RE = /^(0|\d+s|\d+m)$/;
|
|
|
25950
26555
|
var RETRYABLE_STATUS_ERRORS = { NODE_UNREACHABLE: true, NODE_ERROR: true };
|
|
25951
26556
|
var EXPLICIT_KEY_HINT = `${CLI_NAME23} secrets wait never signs in, and an explicit private key has no stored session. Use a local profile signed in once with ${CLI_NAME23} --profile <profile> auth login --method local, without TC_PRIVATE_KEY.`;
|
|
25952
26557
|
function abortableSleep(ms2, signal) {
|
|
25953
|
-
return new Promise((
|
|
26558
|
+
return new Promise((resolve6) => {
|
|
25954
26559
|
if (signal.aborted) {
|
|
25955
|
-
|
|
26560
|
+
resolve6();
|
|
25956
26561
|
return;
|
|
25957
26562
|
}
|
|
25958
26563
|
const done = () => {
|
|
25959
26564
|
clearTimeout(timer);
|
|
25960
26565
|
signal.removeEventListener("abort", done);
|
|
25961
|
-
|
|
26566
|
+
resolve6();
|
|
25962
26567
|
};
|
|
25963
26568
|
const timer = setTimeout(done, ms2);
|
|
25964
26569
|
signal.addEventListener("abort", done, { once: true });
|
|
@@ -26624,7 +27229,7 @@ function permissionsFromDelegation2(delegation) {
|
|
|
26624
27229
|
}
|
|
26625
27230
|
async function loadDelegationCandidates(source) {
|
|
26626
27231
|
try {
|
|
26627
|
-
const raw = JSON.parse(await
|
|
27232
|
+
const raw = JSON.parse(await readFile9(source, "utf8"));
|
|
26628
27233
|
return normalizeDelegationCandidates(raw, source);
|
|
26629
27234
|
} catch (error) {
|
|
26630
27235
|
if (error?.code !== "ENOENT") {
|
|
@@ -26644,7 +27249,7 @@ async function loadDelegationCandidates(source) {
|
|
|
26644
27249
|
}
|
|
26645
27250
|
try {
|
|
26646
27251
|
const importedPath = join11(resolveProfilesDir(), source, "additional-delegations.json");
|
|
26647
|
-
const raw = JSON.parse(await
|
|
27252
|
+
const raw = JSON.parse(await readFile9(importedPath, "utf8"));
|
|
26648
27253
|
return normalizeDelegationCandidates(raw, source);
|
|
26649
27254
|
} catch (error) {
|
|
26650
27255
|
if (error?.code === "ENOENT") {
|
|
@@ -27151,7 +27756,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
27151
27756
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
27152
27757
|
}
|
|
27153
27758
|
if (options.file) {
|
|
27154
|
-
secretValue = await
|
|
27759
|
+
secretValue = await readFile9(options.file, "utf-8");
|
|
27155
27760
|
} else if (options.stdin) {
|
|
27156
27761
|
secretValue = (await readStdin3()).toString("utf-8");
|
|
27157
27762
|
} else {
|
|
@@ -32738,7 +33343,7 @@ async function importAesKey(key32, usage) {
|
|
|
32738
33343
|
[usage]
|
|
32739
33344
|
);
|
|
32740
33345
|
}
|
|
32741
|
-
async function
|
|
33346
|
+
async function open7(blob, key32) {
|
|
32742
33347
|
if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
|
|
32743
33348
|
throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
|
|
32744
33349
|
}
|
|
@@ -33076,7 +33681,7 @@ async function resolvePolicyShare(link3, options) {
|
|
|
33076
33681
|
options.signal?.throwIfAborted();
|
|
33077
33682
|
let envelope;
|
|
33078
33683
|
try {
|
|
33079
|
-
const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await
|
|
33684
|
+
const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await open7(parsed.ciphertext, parsed.key32));
|
|
33080
33685
|
const value = JSON.parse(encoded);
|
|
33081
33686
|
if (canonicalize2(value) !== encoded) throw new Error("non-canonical envelope");
|
|
33082
33687
|
envelope = shareEnvelopeV3Schema.parse(value);
|
|
@@ -35668,9 +36273,9 @@ function receiveJson(result, path) {
|
|
|
35668
36273
|
|
|
35669
36274
|
// src/share/io.ts
|
|
35670
36275
|
import { constants } from "fs";
|
|
35671
|
-
import { lstat as
|
|
36276
|
+
import { lstat as lstat6, mkdir as mkdir5, mkdtemp, open as open8, readFile as readFile10, realpath as realpath2, stat as stat5, link as link2, rename as rename4, rm as rm6, unlink as unlink3 } from "fs/promises";
|
|
35672
36277
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
35673
|
-
import { basename as basename4, join as join12, resolve as
|
|
36278
|
+
import { basename as basename4, join as join12, resolve as resolve4, sep as sep2 } from "path";
|
|
35674
36279
|
init_errors();
|
|
35675
36280
|
var MAX_SHARE_STDIN_BYTES = 100 * 1024 * 1024;
|
|
35676
36281
|
var MAX_SHARE_URL_BYTES = 64 * 1024;
|
|
@@ -35707,26 +36312,26 @@ function shareFilename(value) {
|
|
|
35707
36312
|
}
|
|
35708
36313
|
}
|
|
35709
36314
|
function shareInputFilename(input, name) {
|
|
35710
|
-
return shareFilename(name ?? (input === "-" ? "stdin.md" : basename4(
|
|
36315
|
+
return shareFilename(name ?? (input === "-" ? "stdin.md" : basename4(resolve4(input))));
|
|
35711
36316
|
}
|
|
35712
36317
|
async function readShareInput(input, name, limit = MAX_SHARE_STDIN_BYTES) {
|
|
35713
36318
|
const filename = shareInputFilename(input, name);
|
|
35714
36319
|
if (input === "-") return { bytes: await readBoundedStdin(limit), filename };
|
|
35715
|
-
const path =
|
|
35716
|
-
const info = await
|
|
36320
|
+
const path = resolve4(input);
|
|
36321
|
+
const info = await stat5(path);
|
|
35717
36322
|
if (!info.isFile() || info.size > limit) throw new Error("MAX_BYTES_EXCEEDED");
|
|
35718
|
-
const bytes = new Uint8Array(await
|
|
36323
|
+
const bytes = new Uint8Array(await readFile10(path));
|
|
35719
36324
|
if (bytes.byteLength > limit) throw new Error("MAX_BYTES_EXCEEDED");
|
|
35720
36325
|
return { bytes, filename };
|
|
35721
36326
|
}
|
|
35722
36327
|
async function assertDirectory(path) {
|
|
35723
|
-
const absolute =
|
|
36328
|
+
const absolute = resolve4(path);
|
|
35724
36329
|
const segments = absolute.split(sep2).filter(Boolean);
|
|
35725
36330
|
let current = absolute.startsWith(sep2) ? sep2 : "";
|
|
35726
36331
|
for (const segment of segments) {
|
|
35727
36332
|
current = current === sep2 ? join12(current, segment) : join12(current, segment);
|
|
35728
36333
|
try {
|
|
35729
|
-
const info = await
|
|
36334
|
+
const info = await lstat6(current);
|
|
35730
36335
|
if (info.isSymbolicLink()) {
|
|
35731
36336
|
const canonical = await realpath2(current);
|
|
35732
36337
|
if (current !== "/tmp" && current !== "/var") throw new Error("OUTPUT_EXISTS");
|
|
@@ -35734,27 +36339,27 @@ async function assertDirectory(path) {
|
|
|
35734
36339
|
} else if (!info.isDirectory()) throw new Error("OUTPUT_EXISTS");
|
|
35735
36340
|
} catch (error) {
|
|
35736
36341
|
if (error.code !== "ENOENT") throw error;
|
|
35737
|
-
await
|
|
35738
|
-
const created = await
|
|
36342
|
+
await mkdir5(current, { mode: 448 });
|
|
36343
|
+
const created = await lstat6(current);
|
|
35739
36344
|
if (created.isSymbolicLink() || !created.isDirectory()) throw new Error("OUTPUT_EXISTS");
|
|
35740
36345
|
}
|
|
35741
36346
|
}
|
|
35742
36347
|
}
|
|
35743
36348
|
async function writeShareOutput(directory, filename, bytes, force) {
|
|
35744
|
-
const outputDirectory =
|
|
36349
|
+
const outputDirectory = resolve4(directory);
|
|
35745
36350
|
await assertDirectory(outputDirectory);
|
|
35746
36351
|
const safeName = safeFilename(filename);
|
|
35747
|
-
const directoryHandle = await
|
|
36352
|
+
const directoryHandle = await open8(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
|
|
35748
36353
|
const stableDirectory = await realpath2(outputDirectory);
|
|
35749
36354
|
const outputPath = join12(stableDirectory, safeName);
|
|
35750
36355
|
const directoryIdentity = await directoryHandle.stat();
|
|
35751
36356
|
const assertStableDirectory = async () => {
|
|
35752
|
-
const current = await
|
|
36357
|
+
const current = await stat5(stableDirectory);
|
|
35753
36358
|
if (current.dev !== directoryIdentity.dev || current.ino !== directoryIdentity.ino) throw new Error("OUTPUT_EXISTS");
|
|
35754
36359
|
};
|
|
35755
36360
|
await assertStableDirectory();
|
|
35756
36361
|
const stagingDirectory = await mkdtemp(join12(stableDirectory, ".tinycloud-share-stage-"));
|
|
35757
|
-
const stagingInfo = await
|
|
36362
|
+
const stagingInfo = await lstat6(stagingDirectory);
|
|
35758
36363
|
if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
|
|
35759
36364
|
const stagingPath = join12(stagingDirectory, `.tinycloud-share-${randomBytes7(16).toString("hex")}.tmp`);
|
|
35760
36365
|
let temporaryPath;
|
|
@@ -35762,13 +36367,13 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
35762
36367
|
try {
|
|
35763
36368
|
await assertStableDirectory();
|
|
35764
36369
|
try {
|
|
35765
|
-
const existing = await
|
|
36370
|
+
const existing = await lstat6(outputPath);
|
|
35766
36371
|
if (existing.isSymbolicLink()) throw new Error("UNSAFE_FILENAME");
|
|
35767
36372
|
} catch (error) {
|
|
35768
36373
|
if (error.code !== "ENOENT") throw error;
|
|
35769
36374
|
}
|
|
35770
36375
|
temporaryPath = stagingPath;
|
|
35771
|
-
handle = await
|
|
36376
|
+
handle = await open8(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
|
|
35772
36377
|
await handle.writeFile(bytes);
|
|
35773
36378
|
await handle.close();
|
|
35774
36379
|
handle = void 0;
|
|
@@ -36218,8 +36823,8 @@ init_theme();
|
|
|
36218
36823
|
import { CLI_NAME as CLI_NAME26 } from "@tinycloud/operations/cli-name";
|
|
36219
36824
|
import { ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE2 } from "@tinycloud/node-sdk";
|
|
36220
36825
|
import { randomBytes as randomBytes8 } from "crypto";
|
|
36221
|
-
import { mkdir as
|
|
36222
|
-
import { dirname as
|
|
36826
|
+
import { mkdir as mkdir6, writeFile as writeFile4 } from "fs/promises";
|
|
36827
|
+
import { dirname as dirname7 } from "path";
|
|
36223
36828
|
function didWithoutFragment2(did) {
|
|
36224
36829
|
const fragment = did.indexOf("#");
|
|
36225
36830
|
return fragment === -1 ? did : did.slice(0, fragment);
|
|
@@ -36375,7 +36980,7 @@ it directly with \`${CLI_NAME26} space host <name>\` (no request needed).
|
|
|
36375
36980
|
}
|
|
36376
36981
|
async function emitHostRequestArtifact(artifact, emitOption) {
|
|
36377
36982
|
if (typeof emitOption === "string" && emitOption.length > 0) {
|
|
36378
|
-
await
|
|
36983
|
+
await mkdir6(dirname7(emitOption), { recursive: true });
|
|
36379
36984
|
await writeFile4(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
36380
36985
|
outputJson({
|
|
36381
36986
|
emitted: true,
|
|
@@ -36401,7 +37006,7 @@ init_host();
|
|
|
36401
37006
|
init_theme();
|
|
36402
37007
|
import { CLI_NAME as CLI_NAME27 } from "@tinycloud/operations/cli-name";
|
|
36403
37008
|
import { writeFile as writeFile5 } from "fs/promises";
|
|
36404
|
-
import { resolve as
|
|
37009
|
+
import { resolve as resolve5 } from "path";
|
|
36405
37010
|
async function dbHandle(node, dbName, spaceInput, profileName2) {
|
|
36406
37011
|
const spaceUri = await resolveSpaceUri(spaceInput, profileName2);
|
|
36407
37012
|
const sql = spaceUri ? node.sqlForSpace(spaceUri) : node.sql;
|
|
@@ -36543,7 +37148,7 @@ Output:
|
|
|
36543
37148
|
}
|
|
36544
37149
|
const blob = result.data;
|
|
36545
37150
|
const buffer = Buffer.from(await blob.arrayBuffer());
|
|
36546
|
-
const outputPath =
|
|
37151
|
+
const outputPath = resolve5(options.output);
|
|
36547
37152
|
await writeFile5(outputPath, buffer);
|
|
36548
37153
|
outputJson({
|
|
36549
37154
|
file: outputPath,
|
|
@@ -37099,7 +37704,7 @@ function registerUpgradeCommand(program) {
|
|
|
37099
37704
|
}
|
|
37100
37705
|
|
|
37101
37706
|
// src/commands/vault.ts
|
|
37102
|
-
import { readFile as
|
|
37707
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
37103
37708
|
init_profiles();
|
|
37104
37709
|
init_formatter();
|
|
37105
37710
|
init_errors();
|
|
@@ -37161,7 +37766,7 @@ function registerVaultCommand(program) {
|
|
|
37161
37766
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
37162
37767
|
}
|
|
37163
37768
|
if (options.file) {
|
|
37164
|
-
putValue = new Uint8Array(await
|
|
37769
|
+
putValue = new Uint8Array(await readFile11(options.file));
|
|
37165
37770
|
} else if (options.stdin) {
|
|
37166
37771
|
putValue = new Uint8Array(await readStdin4());
|
|
37167
37772
|
} else {
|
|
@@ -37276,7 +37881,7 @@ function registerVaultCommand(program) {
|
|
|
37276
37881
|
}
|
|
37277
37882
|
|
|
37278
37883
|
// src/commands/vars.ts
|
|
37279
|
-
import { readFile as
|
|
37884
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
37280
37885
|
init_profiles();
|
|
37281
37886
|
init_formatter();
|
|
37282
37887
|
init_errors();
|
|
@@ -37381,7 +37986,7 @@ function registerVarsCommand(program) {
|
|
|
37381
37986
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
37382
37987
|
}
|
|
37383
37988
|
if (options.file) {
|
|
37384
|
-
varValue = await
|
|
37989
|
+
varValue = await readFile12(options.file, "utf-8");
|
|
37385
37990
|
} else if (options.stdin) {
|
|
37386
37991
|
varValue = (await readStdin5()).toString("utf-8");
|
|
37387
37992
|
} else {
|