@tinycloud/cli 1.2.0-beta.1 → 1.2.0-beta.3
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 +33 -2
- package/dist/index.js +400 -57
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +2459 -695
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +5 -5
- package/skills/tc-cli/AUTH.md +19 -0
- package/skills/tc-cli/REFERENCE.md +80 -5
package/dist/index.js
CHANGED
|
@@ -8,6 +8,114 @@ var __export = (target, all) => {
|
|
|
8
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// src/lib/replication-registry.ts
|
|
12
|
+
function stopForSignal(signal) {
|
|
13
|
+
void closeReplication().finally(() => {
|
|
14
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function installSignalHandlers() {
|
|
18
|
+
if (state.installedSignalHandlers) return;
|
|
19
|
+
state.installedSignalHandlers = true;
|
|
20
|
+
state.sigintHandler = () => stopForSignal("SIGINT");
|
|
21
|
+
state.sigtermHandler = () => stopForSignal("SIGTERM");
|
|
22
|
+
process.once("SIGINT", state.sigintHandler);
|
|
23
|
+
process.once("SIGTERM", state.sigtermHandler);
|
|
24
|
+
}
|
|
25
|
+
function removeSignalHandlers() {
|
|
26
|
+
if (!state.installedSignalHandlers) return;
|
|
27
|
+
state.installedSignalHandlers = false;
|
|
28
|
+
if (state.sigintHandler) process.removeListener("SIGINT", state.sigintHandler);
|
|
29
|
+
if (state.sigtermHandler) process.removeListener("SIGTERM", state.sigtermHandler);
|
|
30
|
+
state.sigintHandler = void 0;
|
|
31
|
+
state.sigtermHandler = void 0;
|
|
32
|
+
}
|
|
33
|
+
function registerReplication(profile, control) {
|
|
34
|
+
state.controls.set(profile, control);
|
|
35
|
+
state.allControls.add(control);
|
|
36
|
+
state.generation += 1;
|
|
37
|
+
installSignalHandlers();
|
|
38
|
+
}
|
|
39
|
+
function replicationForProfile(profile) {
|
|
40
|
+
return state.controls.get(profile);
|
|
41
|
+
}
|
|
42
|
+
function registeredReplications() {
|
|
43
|
+
return state.controls;
|
|
44
|
+
}
|
|
45
|
+
function closeReplication(schedule = scheduleTimeout) {
|
|
46
|
+
if (state.closing) return state.closing;
|
|
47
|
+
removeSignalHandlers();
|
|
48
|
+
let resolveClosing;
|
|
49
|
+
let rejectClosing;
|
|
50
|
+
const closing = new Promise((resolve2, reject) => {
|
|
51
|
+
resolveClosing = resolve2;
|
|
52
|
+
rejectClosing = reject;
|
|
53
|
+
});
|
|
54
|
+
state.closing = closing;
|
|
55
|
+
void (async () => {
|
|
56
|
+
let timeout;
|
|
57
|
+
const deadline = new Promise((resolve2) => {
|
|
58
|
+
timeout = schedule(() => resolve2(true), CLOSE_TIMEOUT_MS);
|
|
59
|
+
});
|
|
60
|
+
let timedOut = false;
|
|
61
|
+
const abandonQueuedControls = () => {
|
|
62
|
+
for (const control of state.allControls) {
|
|
63
|
+
void Promise.resolve().then(() => control.close()).catch(() => void 0);
|
|
64
|
+
}
|
|
65
|
+
state.controls.clear();
|
|
66
|
+
state.allControls.clear();
|
|
67
|
+
};
|
|
68
|
+
try {
|
|
69
|
+
while (!timedOut) {
|
|
70
|
+
while (state.allControls.size > 0) {
|
|
71
|
+
const generation = state.generation;
|
|
72
|
+
const pending = [...state.allControls];
|
|
73
|
+
state.controls.clear();
|
|
74
|
+
state.allControls.clear();
|
|
75
|
+
const drained = Promise.allSettled(pending.map((control) => Promise.resolve().then(() => control.close()))).then(() => false);
|
|
76
|
+
timedOut ||= await Promise.race([drained, deadline]);
|
|
77
|
+
if (timedOut) {
|
|
78
|
+
abandonQueuedControls();
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (state.generation === generation && state.allControls.size === 0) break;
|
|
82
|
+
}
|
|
83
|
+
if (timedOut) break;
|
|
84
|
+
const turn = new Promise((resolve2) => setImmediate(() => resolve2(false)));
|
|
85
|
+
timedOut ||= await Promise.race([turn, deadline]);
|
|
86
|
+
if (timedOut) abandonQueuedControls();
|
|
87
|
+
else if (state.allControls.size === 0) break;
|
|
88
|
+
}
|
|
89
|
+
state.closing = void 0;
|
|
90
|
+
if (state.allControls.size === 0) removeSignalHandlers();
|
|
91
|
+
resolveClosing(timedOut);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
state.closing = void 0;
|
|
94
|
+
if (state.allControls.size === 0) removeSignalHandlers();
|
|
95
|
+
rejectClosing(error);
|
|
96
|
+
} finally {
|
|
97
|
+
clearTimeout(timeout);
|
|
98
|
+
}
|
|
99
|
+
})();
|
|
100
|
+
return closing;
|
|
101
|
+
}
|
|
102
|
+
var registryKey, globalState, state, CLOSE_TIMEOUT_MS, scheduleTimeout;
|
|
103
|
+
var init_replication_registry = __esm({
|
|
104
|
+
"src/lib/replication-registry.ts"() {
|
|
105
|
+
"use strict";
|
|
106
|
+
registryKey = /* @__PURE__ */ Symbol.for("@tinycloud/cli/replication-registry");
|
|
107
|
+
globalState = globalThis;
|
|
108
|
+
state = globalState[registryKey] ??= {
|
|
109
|
+
controls: /* @__PURE__ */ new Map(),
|
|
110
|
+
allControls: /* @__PURE__ */ new Set(),
|
|
111
|
+
installedSignalHandlers: false,
|
|
112
|
+
generation: 0
|
|
113
|
+
};
|
|
114
|
+
CLOSE_TIMEOUT_MS = 3e3;
|
|
115
|
+
scheduleTimeout = (callback, delayMs) => setTimeout(callback, delayMs);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
11
119
|
// src/config/constants.ts
|
|
12
120
|
import {
|
|
13
121
|
profilesPath,
|
|
@@ -33,8 +141,12 @@ var init_constants = __esm({
|
|
|
33
141
|
PERMISSION_DENIED: 5,
|
|
34
142
|
NETWORK_ERROR: 6,
|
|
35
143
|
NODE_ERROR: 7,
|
|
144
|
+
/** `tc secrets wait` only: the deadline passed while a secret was still missing. */
|
|
145
|
+
PENDING: 8,
|
|
36
146
|
/** A write was refused because the owner's TinyCloud storage is full. */
|
|
37
|
-
STORAGE_FULL: 10
|
|
147
|
+
STORAGE_FULL: 10,
|
|
148
|
+
/** Interrupted by SIGINT/SIGTERM (`CANCELLED`). */
|
|
149
|
+
CANCELLED: 130
|
|
38
150
|
};
|
|
39
151
|
}
|
|
40
152
|
});
|
|
@@ -119,7 +231,7 @@ function warningLine(warnings) {
|
|
|
119
231
|
return `${subject} not used (${which}). Approve access again when a command needs it; remove the old grant with tc auth grants remove <cid>.`;
|
|
120
232
|
}
|
|
121
233
|
function outputError(code3, message, hint, details = {}) {
|
|
122
|
-
const { meta, warnings = [] } = details;
|
|
234
|
+
const { meta, warnings = [], setup, recovery, progress } = details;
|
|
123
235
|
if (!shouldOutputJson()) {
|
|
124
236
|
process.stderr.write(
|
|
125
237
|
`${theme.error("\u2717")} ${theme.label(code3)}: ${message}
|
|
@@ -138,6 +250,9 @@ function outputError(code3, message, hint, details = {}) {
|
|
|
138
250
|
error: { code: code3, message }
|
|
139
251
|
};
|
|
140
252
|
if (hint) payload.error.hint = hint;
|
|
253
|
+
if (recovery) payload.error.recovery = recovery;
|
|
254
|
+
if (setup) payload.error.setup = setup;
|
|
255
|
+
if (progress) payload.error.progress = progress;
|
|
141
256
|
if (meta) payload.error.meta = meta;
|
|
142
257
|
if (warnings.length > 0) payload.error.warnings = warnings;
|
|
143
258
|
process.stderr.write(JSON.stringify(payload, null, 2) + "\n");
|
|
@@ -267,6 +382,13 @@ var init_storage = __esm({
|
|
|
267
382
|
import { readFileSync, readdirSync } from "fs";
|
|
268
383
|
import { join } from "path";
|
|
269
384
|
import { ProfileDeletedError, ProfileLockTimeoutError } from "@tinycloud/operations/state";
|
|
385
|
+
import { isSafeRecovery } from "@tinycloud/operations/recovery";
|
|
386
|
+
import {
|
|
387
|
+
buildSecretRequestUrl,
|
|
388
|
+
isSafeRequestAccessRecovery,
|
|
389
|
+
isValidSecretName,
|
|
390
|
+
SECRET_REQUEST_MAX_NAMES
|
|
391
|
+
} from "@tinycloud/operations/secret-request";
|
|
270
392
|
import { authorizationVerdictOf, parseCapabilityResource, SERVICE_LONG_TO_SHORT, validatedCapabilityOf } from "@tinycloud/sdk-core";
|
|
271
393
|
function setActiveProfileName(name) {
|
|
272
394
|
activeProfileName = name;
|
|
@@ -342,11 +464,64 @@ function handleError(error) {
|
|
|
342
464
|
meta.resource = capability.resource;
|
|
343
465
|
meta.requiredAction = capability.requiredAction;
|
|
344
466
|
}
|
|
467
|
+
const deniedNames = cliError.metadata?.deniedNames;
|
|
468
|
+
const recovery = safeCliRecovery(cliError.metadata?.recovery, {
|
|
469
|
+
deniedNames: Array.isArray(deniedNames) && deniedNames.every((name) => typeof name === "string") ? deniedNames : []
|
|
470
|
+
});
|
|
471
|
+
const setup = cliError.code === "NOT_FOUND" || cliError.code === "PERMISSION_DENIED" ? safeCliSetup(cliError.metadata?.setup) : void 0;
|
|
472
|
+
const progress = cliError.code === "NETWORK_ERROR" || cliError.code === "CANCELLED" ? safeCliProgress(cliError.metadata?.progress) : void 0;
|
|
345
473
|
outputError(cliError.code, cliError.message, hint, {
|
|
474
|
+
...recovery ? { recovery } : {},
|
|
475
|
+
...setup ? { setup } : {},
|
|
476
|
+
...progress ? { progress } : {},
|
|
346
477
|
...Object.keys(meta).length ? { meta } : {},
|
|
347
478
|
warnings: mergeWarnings(drainWarnings(), operationWarnings(cliError.metadata?.warnings))
|
|
348
479
|
});
|
|
349
|
-
process.exit(cliError.exitCode);
|
|
480
|
+
if (registeredReplications().size === 0) return process.exit(cliError.exitCode);
|
|
481
|
+
return closeReplication().then(() => process.exit(cliError.exitCode));
|
|
482
|
+
}
|
|
483
|
+
function safeCliRecovery(value, context) {
|
|
484
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || !("action" in value)) return void 0;
|
|
485
|
+
const action = value.action;
|
|
486
|
+
switch (action) {
|
|
487
|
+
case "sign_in":
|
|
488
|
+
case "complete_migration":
|
|
489
|
+
return isSafeRecovery(value, action) ? { ...value } : void 0;
|
|
490
|
+
case "request_access":
|
|
491
|
+
return isSafeRequestAccessRecovery(value, context.deniedNames) ? { ...value, names: [...value.names] } : void 0;
|
|
492
|
+
default:
|
|
493
|
+
return void 0;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function safeCliSetup(value) {
|
|
497
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
498
|
+
const setup = { ...value };
|
|
499
|
+
if (!Object.keys(setup).every((key) => SETUP_KEYS[key] === true)) return void 0;
|
|
500
|
+
const { url, names, scope, owner, action } = setup;
|
|
501
|
+
if (action !== "add_secret" || typeof url !== "string" || typeof owner !== "string") return void 0;
|
|
502
|
+
if (!Array.isArray(names) || !names.every((name) => typeof name === "string")) return void 0;
|
|
503
|
+
if (scope !== void 0 && typeof scope !== "string") return void 0;
|
|
504
|
+
try {
|
|
505
|
+
const link2 = buildSecretRequestUrl({ owner, names, ...scope !== void 0 ? { scope } : {} });
|
|
506
|
+
if (link2.url !== url || link2.owner !== owner || link2.names.length !== names.length) return void 0;
|
|
507
|
+
} catch {
|
|
508
|
+
return void 0;
|
|
509
|
+
}
|
|
510
|
+
return { url, names: [...names], ...scope !== void 0 ? { scope } : {}, owner, action };
|
|
511
|
+
}
|
|
512
|
+
function safeNameList(value) {
|
|
513
|
+
if (!Array.isArray(value) || value.length > SECRET_REQUEST_MAX_NAMES) return void 0;
|
|
514
|
+
if (!value.every((name) => typeof name === "string" && isValidSecretName(name))) return void 0;
|
|
515
|
+
if (new Set(value).size !== value.length) return void 0;
|
|
516
|
+
return [...value];
|
|
517
|
+
}
|
|
518
|
+
function safeCliProgress(value) {
|
|
519
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
520
|
+
if (!("present" in value) || !("pending" in value) || Object.keys(value).length !== 2) return void 0;
|
|
521
|
+
const present = safeNameList(value.present);
|
|
522
|
+
const pending = safeNameList(value.pending);
|
|
523
|
+
if (!present || !pending || present.some((name) => pending.includes(name))) return void 0;
|
|
524
|
+
return { present, pending };
|
|
350
525
|
}
|
|
351
526
|
function buildAuthHint(error) {
|
|
352
527
|
const capability = validatedCapabilityOf({ meta: error.metadata });
|
|
@@ -409,10 +584,11 @@ function buildNetworkHint() {
|
|
|
409
584
|
lines.push(`Or override per-command with --host or TC_HOST.`);
|
|
410
585
|
return lines.join("\n");
|
|
411
586
|
}
|
|
412
|
-
var activeProfileName, CLIError;
|
|
587
|
+
var activeProfileName, CLIError, SETUP_KEYS;
|
|
413
588
|
var init_errors = __esm({
|
|
414
589
|
"src/output/errors.ts"() {
|
|
415
590
|
"use strict";
|
|
591
|
+
init_replication_registry();
|
|
416
592
|
init_constants();
|
|
417
593
|
init_formatter();
|
|
418
594
|
init_storage();
|
|
@@ -428,6 +604,7 @@ var init_errors = __esm({
|
|
|
428
604
|
}
|
|
429
605
|
status;
|
|
430
606
|
};
|
|
607
|
+
SETUP_KEYS = { url: true, names: true, scope: true, owner: true, action: true };
|
|
431
608
|
}
|
|
432
609
|
});
|
|
433
610
|
|
|
@@ -900,11 +1077,11 @@ var init_md = __esm({
|
|
|
900
1077
|
if (len % 4)
|
|
901
1078
|
throw new Error("_sha2: outputLen should be aligned to 32bit");
|
|
902
1079
|
const outLen = len / 4;
|
|
903
|
-
const
|
|
904
|
-
if (outLen >
|
|
1080
|
+
const state2 = this.get();
|
|
1081
|
+
if (outLen > state2.length)
|
|
905
1082
|
throw new Error("_sha2: outputLen bigger than state");
|
|
906
1083
|
for (let i = 0; i < outLen; i++)
|
|
907
|
-
oview.setUint32(4 * i,
|
|
1084
|
+
oview.setUint32(4 * i, state2[i], isLE2);
|
|
908
1085
|
}
|
|
909
1086
|
digest() {
|
|
910
1087
|
const { buffer, outputLen } = this;
|
|
@@ -5464,6 +5641,8 @@ import {
|
|
|
5464
5641
|
buildPermissionRequestArtifact,
|
|
5465
5642
|
isPermissionRequestArtifact
|
|
5466
5643
|
} from "@tinycloud/operations/artifacts";
|
|
5644
|
+
import { isLocalOwnerProfile } from "@tinycloud/operations/profile";
|
|
5645
|
+
import { resolveSecretsOwner } from "@tinycloud/operations/secret-request";
|
|
5467
5646
|
import {
|
|
5468
5647
|
additionalDelegationsPath as sharedAdditionalDelegationsPath,
|
|
5469
5648
|
authRequestsPath as sharedAuthRequestsPath,
|
|
@@ -5501,14 +5680,17 @@ var init_permissions = __esm({
|
|
|
5501
5680
|
});
|
|
5502
5681
|
|
|
5503
5682
|
// src/auth/session-expired.ts
|
|
5683
|
+
import { signInRecovery } from "@tinycloud/operations/recovery";
|
|
5504
5684
|
function signInAgainHint(profileName, posture) {
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5685
|
+
const delegate = posture === "delegate-session";
|
|
5686
|
+
const { command } = signInRecovery(
|
|
5687
|
+
profileName,
|
|
5688
|
+
delegate ? "delegate-session" : posture === "local-owner-key" ? "local-owner-key" : "owner-openkey"
|
|
5689
|
+
);
|
|
5690
|
+
if (command === void 0) {
|
|
5691
|
+
return delegate ? `Have the owner approve a new scoped login for profile ${JSON.stringify(profileName)} with tc auth login --method openkey --paste --manifest <manifest.json>.` : `Sign in again to profile ${JSON.stringify(profileName)} with tc auth login.`;
|
|
5510
5692
|
}
|
|
5511
|
-
return `
|
|
5693
|
+
return delegate ? `Have the owner approve a new scoped login: ${command}. Pass the owner's code on stdin, newline-terminated.` : `Sign in again with: ${command}`;
|
|
5512
5694
|
}
|
|
5513
5695
|
function sessionExpiredError(profileName, posture) {
|
|
5514
5696
|
return new CLIError(
|
|
@@ -5543,13 +5725,127 @@ var init_delegation_expiry = __esm({
|
|
|
5543
5725
|
}
|
|
5544
5726
|
});
|
|
5545
5727
|
|
|
5728
|
+
// src/lib/replication-log.ts
|
|
5729
|
+
import { appendFileSync, chmodSync, mkdirSync, renameSync, statSync, writeFileSync } from "fs";
|
|
5730
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
5731
|
+
function appendReplicationEvent(root, event) {
|
|
5732
|
+
mkdirSync(root, { recursive: true, mode: DIR_MODE });
|
|
5733
|
+
chmodSync(root, DIR_MODE);
|
|
5734
|
+
const path = join4(root, "events.jsonl");
|
|
5735
|
+
let size = 0;
|
|
5736
|
+
try {
|
|
5737
|
+
size = statSync(path).size;
|
|
5738
|
+
} catch (error) {
|
|
5739
|
+
if (error.code !== "ENOENT") throw error;
|
|
5740
|
+
}
|
|
5741
|
+
const line = `${JSON.stringify(event)}
|
|
5742
|
+
`;
|
|
5743
|
+
if (size + Buffer.byteLength(line) > MAX_LOG_BYTES) {
|
|
5744
|
+
const rotated = join4(root, "events.jsonl.1");
|
|
5745
|
+
try {
|
|
5746
|
+
renameSync(path, rotated);
|
|
5747
|
+
} catch (error) {
|
|
5748
|
+
if (error.code !== "ENOENT") throw error;
|
|
5749
|
+
}
|
|
5750
|
+
writeFileSync(path, line, { mode: LOG_MODE });
|
|
5751
|
+
} else {
|
|
5752
|
+
appendFileSync(path, line, { mode: LOG_MODE });
|
|
5753
|
+
}
|
|
5754
|
+
chmodSync(path, LOG_MODE);
|
|
5755
|
+
}
|
|
5756
|
+
function replicationLogPath(profileRoot) {
|
|
5757
|
+
return join4(profileRoot, "replication", "events.jsonl");
|
|
5758
|
+
}
|
|
5759
|
+
function createReplicationEventSink(profileRoot, options) {
|
|
5760
|
+
const pinnedNotices = /* @__PURE__ */ new Set();
|
|
5761
|
+
const missingGrantWarnings = /* @__PURE__ */ new Set();
|
|
5762
|
+
return (event) => {
|
|
5763
|
+
appendProfileReplicationEvent(profileRoot, event);
|
|
5764
|
+
if (event.type === "replication.state" && event.state === "grant_missing") {
|
|
5765
|
+
const prefix = event.replica ?? event.space ?? "configured prefix";
|
|
5766
|
+
const key = `${event.space ?? ""}\0${event.replica ?? ""}`;
|
|
5767
|
+
if (!missingGrantWarnings.has(key)) {
|
|
5768
|
+
missingGrantWarnings.add(key);
|
|
5769
|
+
process.stderr.write(`[replication] Warning: no usable kv/get+sync grant for ${prefix}; reads use the network (${event.code ?? "grant_missing"}).
|
|
5770
|
+
`);
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
5773
|
+
if (options.debug) {
|
|
5774
|
+
if (event.type === "replication.read") {
|
|
5775
|
+
const detail = event.source === "replica" ? "replica hit" : `${event.source} ${event.reason}`;
|
|
5776
|
+
const syncStatus = event.syncedBeforeRead === void 0 ? "" : ` syncedBeforeRead:${event.syncedBeforeRead}`;
|
|
5777
|
+
const syncError = event.syncError === void 0 ? "" : ` syncError:${event.syncError}`;
|
|
5778
|
+
process.stderr.write(`[replication] ${event.op} ${event.key} \u2190 ${detail} ${event.latencyMs}ms${syncStatus}${syncError}${event.stalenessMs === null ? "" : ` (synced ${Math.round(event.stalenessMs / 1e3)}s ago)`}
|
|
5779
|
+
`);
|
|
5780
|
+
} else if (event.type === "replication.write") {
|
|
5781
|
+
process.stderr.write(`[replication] ${event.op} ${event.keys.join(",")} ${event.outcome}${event.code ? ` ${event.code}` : ""}
|
|
5782
|
+
`);
|
|
5783
|
+
} else if (event.type === "replication.sync") {
|
|
5784
|
+
const pending = event.pendingCleared === void 0 ? "" : ` pendingCleared:${event.pendingCleared}`;
|
|
5785
|
+
process.stderr.write(`[replication] sync ${event.replica} ${event.outcome}${event.code ? ` ${event.code}` : ""}${pending}
|
|
5786
|
+
`);
|
|
5787
|
+
}
|
|
5788
|
+
}
|
|
5789
|
+
if (event.type === "replication.read" && (event.pendingState === "in_flight" || event.pendingState === "ambiguous") && !options.quiet && !pinnedNotices.has(event.key)) {
|
|
5790
|
+
pinnedNotices.add(event.key);
|
|
5791
|
+
process.stderr.write(`[replication] ${event.key} reads from the network: an earlier write's outcome is unknown (${event.pendingState}${event.code ? `, ${event.code}` : ""}). Review: tc replica report
|
|
5792
|
+
`);
|
|
5793
|
+
}
|
|
5794
|
+
};
|
|
5795
|
+
}
|
|
5796
|
+
function appendProfileReplicationEvent(profileRoot, event) {
|
|
5797
|
+
appendReplicationEvent(dirname2(replicationLogPath(profileRoot)), event);
|
|
5798
|
+
}
|
|
5799
|
+
var MAX_LOG_BYTES, LOG_MODE, DIR_MODE;
|
|
5800
|
+
var init_replication_log = __esm({
|
|
5801
|
+
"src/lib/replication-log.ts"() {
|
|
5802
|
+
"use strict";
|
|
5803
|
+
MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
5804
|
+
LOG_MODE = 384;
|
|
5805
|
+
DIR_MODE = 448;
|
|
5806
|
+
}
|
|
5807
|
+
});
|
|
5808
|
+
|
|
5809
|
+
// src/lib/replication-config.ts
|
|
5810
|
+
import { join as join5 } from "path";
|
|
5811
|
+
import { profilePath, withProfileLock as withProfileLock2 } from "@tinycloud/operations/state";
|
|
5812
|
+
import { sqliteReplicaStorage } from "@tinycloud/node-sdk";
|
|
5813
|
+
function createCliReplicationConfig(profile, settings, output, enabled) {
|
|
5814
|
+
if (!enabled || !settings || settings.prefixes.length === 0) return void 0;
|
|
5815
|
+
const root = profilePath(profile);
|
|
5816
|
+
return {
|
|
5817
|
+
enabled: true,
|
|
5818
|
+
prefixes: settings.prefixes,
|
|
5819
|
+
allowSecrets: settings.allowSecrets === true,
|
|
5820
|
+
mode: "foreground",
|
|
5821
|
+
verify: process.env.TC_REPLICATION_VERIFY === "1",
|
|
5822
|
+
maxStalenessMs: Number(process.env.TC_REPLICATION_MAX_STALENESS_MS ?? 6e4),
|
|
5823
|
+
staleSyncTimeoutMs: Number(process.env.TC_REPLICATION_SYNC_TIMEOUT_MS ?? 1e4),
|
|
5824
|
+
storage: sqliteReplicaStorage({
|
|
5825
|
+
dir: join5(root, "replication"),
|
|
5826
|
+
guard: (section) => withProfileLock2(profile, section, { requireProfile: true, timeoutMs: 35e3 })
|
|
5827
|
+
}),
|
|
5828
|
+
onEvent: createReplicationEventSink(root, output)
|
|
5829
|
+
};
|
|
5830
|
+
}
|
|
5831
|
+
var init_replication_config = __esm({
|
|
5832
|
+
"src/lib/replication-config.ts"() {
|
|
5833
|
+
"use strict";
|
|
5834
|
+
init_replication_log();
|
|
5835
|
+
}
|
|
5836
|
+
});
|
|
5837
|
+
|
|
5546
5838
|
// src/lib/sdk.ts
|
|
5547
5839
|
var sdk_exports = {};
|
|
5548
5840
|
__export(sdk_exports, {
|
|
5549
5841
|
bootstrapDelegatedSession: () => bootstrapDelegatedSession,
|
|
5842
|
+
closeReplication: () => closeReplication,
|
|
5550
5843
|
createSDKInstance: () => createSDKInstance,
|
|
5551
5844
|
ensureAuthenticated: () => ensureAuthenticated,
|
|
5552
5845
|
jwkHasPrivateParameter: () => jwkHasPrivateParameter,
|
|
5846
|
+
registerReplication: () => registerReplication,
|
|
5847
|
+
registeredReplications: () => registeredReplications,
|
|
5848
|
+
replicationForProfile: () => replicationForProfile,
|
|
5553
5849
|
restoreProfileSessionNode: () => restoreProfileSessionNode,
|
|
5554
5850
|
selectSignerJwk: () => selectSignerJwk,
|
|
5555
5851
|
storedSessionVerifiesLocally: () => storedSessionVerifiesLocally
|
|
@@ -5664,10 +5960,21 @@ async function createSDKInstance(ctx, options) {
|
|
|
5664
5960
|
);
|
|
5665
5961
|
}
|
|
5666
5962
|
if (profile?.authMethod === "local" && effectivePrivateKey) {
|
|
5963
|
+
const replication2 = createCliReplicationConfig(
|
|
5964
|
+
ctx.profile,
|
|
5965
|
+
profile?.replication,
|
|
5966
|
+
{ debug: ctx.replicationDebug, quiet: ctx.quiet },
|
|
5967
|
+
ctx.replication
|
|
5968
|
+
);
|
|
5969
|
+
if (ctx.replication && !replication2) {
|
|
5970
|
+
process.stderr.write("[replication] Enabled, but this profile has no configured prefixes; reads use the network.\n");
|
|
5971
|
+
}
|
|
5667
5972
|
const node2 = new TinyCloudNode({
|
|
5668
5973
|
host: ctx.host,
|
|
5669
|
-
privateKey: effectivePrivateKey
|
|
5974
|
+
privateKey: effectivePrivateKey,
|
|
5975
|
+
...replication2 ? { replication: replication2 } : {}
|
|
5670
5976
|
});
|
|
5977
|
+
if (node2.replication) registerReplication(ctx.profile, node2.replication);
|
|
5671
5978
|
const restoredOwnSession = Boolean(session && session.delegationHeader && session.delegationCid && session.spaceId);
|
|
5672
5979
|
if (restoredOwnSession) {
|
|
5673
5980
|
await restoreProfileSession(node2, ctx.profile, profile, persistedSessionData(ctx.profile, profile, session, key));
|
|
@@ -5683,10 +5990,21 @@ async function createSDKInstance(ctx, options) {
|
|
|
5683
5990
|
}
|
|
5684
5991
|
return node2;
|
|
5685
5992
|
}
|
|
5993
|
+
const replication = createCliReplicationConfig(
|
|
5994
|
+
ctx.profile,
|
|
5995
|
+
profile?.replication,
|
|
5996
|
+
{ debug: ctx.replicationDebug, quiet: ctx.quiet },
|
|
5997
|
+
ctx.replication
|
|
5998
|
+
);
|
|
5999
|
+
if (ctx.replication && !replication) {
|
|
6000
|
+
process.stderr.write("[replication] Enabled, but this profile has no configured prefixes; reads use the network.\n");
|
|
6001
|
+
}
|
|
5686
6002
|
const node = new TinyCloudNode({
|
|
5687
6003
|
host: ctx.host,
|
|
5688
|
-
privateKey: options?.privateKey
|
|
6004
|
+
privateKey: options?.privateKey,
|
|
6005
|
+
...replication ? { replication } : {}
|
|
5689
6006
|
});
|
|
6007
|
+
if (node.replication) registerReplication(ctx.profile, node.replication);
|
|
5690
6008
|
if (options?.privateKey) {
|
|
5691
6009
|
await node.signIn();
|
|
5692
6010
|
} else if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
|
|
@@ -5828,6 +6146,9 @@ var init_sdk = __esm({
|
|
|
5828
6146
|
init_session_expired();
|
|
5829
6147
|
init_warnings();
|
|
5830
6148
|
init_delegation_expiry();
|
|
6149
|
+
init_replication_config();
|
|
6150
|
+
init_replication_registry();
|
|
6151
|
+
init_replication_registry();
|
|
5831
6152
|
BOOTSTRAP_PROFILE_FIELDS = ["sessionDid", "spaceId"];
|
|
5832
6153
|
}
|
|
5833
6154
|
});
|
|
@@ -5968,14 +6289,14 @@ var init_host = __esm({
|
|
|
5968
6289
|
|
|
5969
6290
|
// src/config/profiles.ts
|
|
5970
6291
|
import { chmod as chmod2, lstat, readdir as readdir2, rm as rm2, rmdir } from "fs/promises";
|
|
5971
|
-
import { join as
|
|
6292
|
+
import { join as join6 } from "path";
|
|
5972
6293
|
import {
|
|
5973
|
-
profilePath,
|
|
6294
|
+
profilePath as profilePath2,
|
|
5974
6295
|
readSession,
|
|
5975
6296
|
recordProfileDeletion,
|
|
5976
6297
|
refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
|
|
5977
6298
|
removeSession,
|
|
5978
|
-
withProfileLock as
|
|
6299
|
+
withProfileLock as withProfileLock3,
|
|
5979
6300
|
writeSession
|
|
5980
6301
|
} from "@tinycloud/operations/state";
|
|
5981
6302
|
var ProfileManager;
|
|
@@ -5993,7 +6314,7 @@ var init_profiles = __esm({
|
|
|
5993
6314
|
* it around a whole read-modify-write, not just each write.
|
|
5994
6315
|
*/
|
|
5995
6316
|
static async withLock(name, action, options) {
|
|
5996
|
-
return
|
|
6317
|
+
return withProfileLock3(name, action, options);
|
|
5997
6318
|
}
|
|
5998
6319
|
/**
|
|
5999
6320
|
* Read-modify-write of profile.json under the profile lock, so concurrent
|
|
@@ -6019,7 +6340,7 @@ var init_profiles = __esm({
|
|
|
6019
6340
|
/** Owner-only profile directory (0700), created or tightened before any write into it. */
|
|
6020
6341
|
static async ensureProfileDir(name) {
|
|
6021
6342
|
await _ProfileManager.ensureConfigDir();
|
|
6022
|
-
const profileDir =
|
|
6343
|
+
const profileDir = join6(PROFILES_DIR, name);
|
|
6023
6344
|
await ensureDir(profileDir);
|
|
6024
6345
|
await chmod2(profileDir, PRIVATE_DIR_MODE);
|
|
6025
6346
|
return profileDir;
|
|
@@ -6048,8 +6369,8 @@ var init_profiles = __esm({
|
|
|
6048
6369
|
* Throws CLIError if the profile doesn't exist.
|
|
6049
6370
|
*/
|
|
6050
6371
|
static async getProfile(name) {
|
|
6051
|
-
const
|
|
6052
|
-
const profile = await readJson(
|
|
6372
|
+
const profilePath3 = join6(PROFILES_DIR, name, "profile.json");
|
|
6373
|
+
const profile = await readJson(profilePath3);
|
|
6053
6374
|
if (!profile) {
|
|
6054
6375
|
throw new CLIError(
|
|
6055
6376
|
"PROFILE_NOT_FOUND",
|
|
@@ -6064,18 +6385,18 @@ var init_profiles = __esm({
|
|
|
6064
6385
|
*/
|
|
6065
6386
|
static async setProfile(name, data) {
|
|
6066
6387
|
await _ProfileManager.withLock(name, async () => {
|
|
6067
|
-
await writeJson(
|
|
6388
|
+
await writeJson(join6(await _ProfileManager.ensureProfileDir(name), "profile.json"), data);
|
|
6068
6389
|
});
|
|
6069
6390
|
}
|
|
6070
6391
|
/** Removes profile.json under the profile lock (rollback of a profile a failed login created). */
|
|
6071
6392
|
static async removeProfileConfig(name) {
|
|
6072
|
-
await _ProfileManager.withLock(name, () => rm2(
|
|
6393
|
+
await _ProfileManager.withLock(name, () => rm2(join6(PROFILES_DIR, name, "profile.json"), { force: true }));
|
|
6073
6394
|
}
|
|
6074
6395
|
/**
|
|
6075
6396
|
* Returns true if a profile directory exists.
|
|
6076
6397
|
*/
|
|
6077
6398
|
static async profileExists(name) {
|
|
6078
|
-
return fileExists(
|
|
6399
|
+
return fileExists(join6(PROFILES_DIR, name, "profile.json"));
|
|
6079
6400
|
}
|
|
6080
6401
|
/**
|
|
6081
6402
|
* Returns an array of profile directory names.
|
|
@@ -6099,7 +6420,7 @@ var init_profiles = __esm({
|
|
|
6099
6420
|
static async deleteProfile(name) {
|
|
6100
6421
|
let profileDir;
|
|
6101
6422
|
try {
|
|
6102
|
-
profileDir =
|
|
6423
|
+
profileDir = profilePath2(name);
|
|
6103
6424
|
} catch {
|
|
6104
6425
|
throw new CLIError(
|
|
6105
6426
|
"INVALID_PROFILE_NAME",
|
|
@@ -6124,11 +6445,11 @@ var init_profiles = __esm({
|
|
|
6124
6445
|
return;
|
|
6125
6446
|
}
|
|
6126
6447
|
await _ProfileManager.withLock(name, async () => {
|
|
6127
|
-
for (const file of ["session.json", "key.json"]) await rm2(
|
|
6448
|
+
for (const file of ["session.json", "key.json"]) await rm2(join6(profileDir, file), { force: true });
|
|
6128
6449
|
for (const entry of await readdir2(profileDir)) {
|
|
6129
|
-
if (entry !== ".lock" && entry !== "profile.json") await rm2(
|
|
6450
|
+
if (entry !== ".lock" && entry !== "profile.json") await rm2(join6(profileDir, entry), { recursive: true, force: true });
|
|
6130
6451
|
}
|
|
6131
|
-
await rm2(
|
|
6452
|
+
await rm2(join6(profileDir, "profile.json"), { force: true });
|
|
6132
6453
|
await recordProfileDeletion(name);
|
|
6133
6454
|
});
|
|
6134
6455
|
await rmdir(profileDir).catch((error) => {
|
|
@@ -6140,7 +6461,7 @@ var init_profiles = __esm({
|
|
|
6140
6461
|
* Returns the parsed JWK for a profile, or null if no key exists.
|
|
6141
6462
|
*/
|
|
6142
6463
|
static async getKey(name) {
|
|
6143
|
-
return readJson(
|
|
6464
|
+
return readJson(join6(PROFILES_DIR, name, "key.json"));
|
|
6144
6465
|
}
|
|
6145
6466
|
/**
|
|
6146
6467
|
* Saves a JWK key under the profile lock (0600, in an owner-only profile
|
|
@@ -6150,12 +6471,12 @@ var init_profiles = __esm({
|
|
|
6150
6471
|
static async setKey(name, jwk) {
|
|
6151
6472
|
await _ProfileManager.withLock(name, async () => {
|
|
6152
6473
|
await refuseWriteToDeletedProfile2(name);
|
|
6153
|
-
await writeJson(
|
|
6474
|
+
await writeJson(join6(await _ProfileManager.ensureProfileDir(name), "key.json"), jwk);
|
|
6154
6475
|
});
|
|
6155
6476
|
}
|
|
6156
6477
|
/** Removes key.json under the profile lock (rollback of a key a failed login created). */
|
|
6157
6478
|
static async removeKey(name) {
|
|
6158
|
-
await _ProfileManager.withLock(name, () => rm2(
|
|
6479
|
+
await _ProfileManager.withLock(name, () => rm2(join6(PROFILES_DIR, name, "key.json"), { force: true }));
|
|
6159
6480
|
}
|
|
6160
6481
|
// ── Session management ──────────────────────────────────────────────
|
|
6161
6482
|
/**
|
|
@@ -6182,7 +6503,7 @@ var init_profiles = __esm({
|
|
|
6182
6503
|
* created or tightened to 0700.
|
|
6183
6504
|
*/
|
|
6184
6505
|
static async getCacheDir(name) {
|
|
6185
|
-
const cacheDir =
|
|
6506
|
+
const cacheDir = join6(await _ProfileManager.ensureProfileDir(name), "cache");
|
|
6186
6507
|
await ensureDir(cacheDir);
|
|
6187
6508
|
await chmod2(cacheDir, PRIVATE_DIR_MODE);
|
|
6188
6509
|
return cacheDir;
|
|
@@ -6205,11 +6526,20 @@ var init_profiles = __esm({
|
|
|
6205
6526
|
const config = await _ProfileManager.getConfig();
|
|
6206
6527
|
const profile = options.profile ?? process.env.TC_PROFILE ?? config.defaultProfile ?? DEFAULT_PROFILE;
|
|
6207
6528
|
let profileHost;
|
|
6529
|
+
let profileConfig;
|
|
6208
6530
|
try {
|
|
6209
|
-
|
|
6531
|
+
profileConfig = await _ProfileManager.getProfile(profile);
|
|
6210
6532
|
profileHost = profileConfig.host;
|
|
6211
6533
|
} catch {
|
|
6212
6534
|
}
|
|
6535
|
+
const envReplication = process.env.TC_REPLICATION?.toLowerCase();
|
|
6536
|
+
const envEnabled = envReplication === "1" || envReplication === "true" || envReplication === "on";
|
|
6537
|
+
const explicitEnvOff = envReplication === "0" || envReplication === "false" || envReplication === "off";
|
|
6538
|
+
if (envReplication !== void 0 && !envEnabled && !explicitEnvOff) {
|
|
6539
|
+
throw new CLIError("INVALID_ARGUMENT", "TC_REPLICATION must be one of 1, true, on, 0, false or off.", ExitCode.USAGE_ERROR);
|
|
6540
|
+
}
|
|
6541
|
+
const explicitOff = options.noReplication === true || options.replication === false;
|
|
6542
|
+
const replication = options.replication === true ? true : explicitOff ? false : envEnabled;
|
|
6213
6543
|
const explicitHost = options.host ?? process.env.TC_HOST;
|
|
6214
6544
|
let host = explicitHost ?? profileHost ?? DEFAULT_HOST;
|
|
6215
6545
|
let localDiscovery;
|
|
@@ -6229,6 +6559,8 @@ var init_profiles = __esm({
|
|
|
6229
6559
|
verbose: options.verbose ?? false,
|
|
6230
6560
|
noCache: options.noCache ?? false,
|
|
6231
6561
|
quiet: options.quiet ?? false,
|
|
6562
|
+
replication,
|
|
6563
|
+
replicationDebug: options.replicationDebug === true || process.env.TC_REPLICATION_DEBUG === "1",
|
|
6232
6564
|
...localDiscovery === void 0 ? {} : { localDiscovery }
|
|
6233
6565
|
};
|
|
6234
6566
|
}
|
|
@@ -6337,6 +6669,7 @@ function emitBanner(version2) {
|
|
|
6337
6669
|
}
|
|
6338
6670
|
|
|
6339
6671
|
// src/index.ts
|
|
6672
|
+
init_replication_registry();
|
|
6340
6673
|
init_theme();
|
|
6341
6674
|
init_formatter();
|
|
6342
6675
|
init_profiles();
|
|
@@ -8059,8 +8392,8 @@ var BLAKE3 = class _BLAKE3 extends BLAKE2 {
|
|
|
8059
8392
|
}
|
|
8060
8393
|
_cloneInto(to) {
|
|
8061
8394
|
to = super._cloneInto(to);
|
|
8062
|
-
const { IV, flags, state, chunkPos, posOut, chunkOut, stack, chunksDone } = this;
|
|
8063
|
-
to.state.set(
|
|
8395
|
+
const { IV, flags, state: state2, chunkPos, posOut, chunkOut, stack, chunksDone } = this;
|
|
8396
|
+
to.state.set(state2.slice());
|
|
8064
8397
|
to.stack = stack.map((i) => Uint32Array.from(i));
|
|
8065
8398
|
to.IV.set(IV);
|
|
8066
8399
|
to.flags = flags;
|
|
@@ -15280,12 +15613,12 @@ async function notifyShare(input) {
|
|
|
15280
15613
|
if (input.signal?.aborted) throw new ShareNotifyError("share delivery was cancelled");
|
|
15281
15614
|
attempts += 1;
|
|
15282
15615
|
try {
|
|
15283
|
-
const
|
|
15616
|
+
const state2 = await input.adapter.deliver({ shareId: input.shareId, recipient, idempotencyKey, ...input.record === void 0 ? {} : { record: input.record }, ...input.signal === void 0 ? {} : { signal: input.signal } });
|
|
15284
15617
|
return {
|
|
15285
15618
|
protocol: "tinycloud-share",
|
|
15286
15619
|
version: 1,
|
|
15287
15620
|
shareId: input.shareId,
|
|
15288
|
-
state:
|
|
15621
|
+
state: state2 === "delivered" && input.record?.deliveredRecipients?.includes(recipient) ? "already-delivered" : state2,
|
|
15289
15622
|
idempotencyKey,
|
|
15290
15623
|
attempts
|
|
15291
15624
|
};
|
|
@@ -16982,7 +17315,7 @@ function receiveJson(result, path) {
|
|
|
16982
17315
|
import { constants } from "fs";
|
|
16983
17316
|
import { lstat as lstat2, mkdir as mkdir2, mkdtemp, open as open2, readFile as readFile3, realpath, stat as stat2, link, rename as rename2, rm as rm3, unlink } from "fs/promises";
|
|
16984
17317
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
16985
|
-
import { basename as basename2, join as
|
|
17318
|
+
import { basename as basename2, join as join7, resolve, sep } from "path";
|
|
16986
17319
|
init_errors();
|
|
16987
17320
|
var MAX_SHARE_STDIN_BYTES = 100 * 1024 * 1024;
|
|
16988
17321
|
var MAX_SHARE_URL_BYTES = 64 * 1024;
|
|
@@ -17036,7 +17369,7 @@ async function assertDirectory(path) {
|
|
|
17036
17369
|
const segments = absolute.split(sep).filter(Boolean);
|
|
17037
17370
|
let current = absolute.startsWith(sep) ? sep : "";
|
|
17038
17371
|
for (const segment of segments) {
|
|
17039
|
-
current = current === sep ?
|
|
17372
|
+
current = current === sep ? join7(current, segment) : join7(current, segment);
|
|
17040
17373
|
try {
|
|
17041
17374
|
const info = await lstat2(current);
|
|
17042
17375
|
if (info.isSymbolicLink()) {
|
|
@@ -17058,17 +17391,17 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
17058
17391
|
const safeName = safeFilename(filename);
|
|
17059
17392
|
const directoryHandle = await open2(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
|
|
17060
17393
|
const stableDirectory = await realpath(outputDirectory);
|
|
17061
|
-
const outputPath =
|
|
17394
|
+
const outputPath = join7(stableDirectory, safeName);
|
|
17062
17395
|
const directoryIdentity = await directoryHandle.stat();
|
|
17063
17396
|
const assertStableDirectory = async () => {
|
|
17064
17397
|
const current = await stat2(stableDirectory);
|
|
17065
17398
|
if (current.dev !== directoryIdentity.dev || current.ino !== directoryIdentity.ino) throw new Error("OUTPUT_EXISTS");
|
|
17066
17399
|
};
|
|
17067
17400
|
await assertStableDirectory();
|
|
17068
|
-
const stagingDirectory = await mkdtemp(
|
|
17401
|
+
const stagingDirectory = await mkdtemp(join7(stableDirectory, ".tinycloud-share-stage-"));
|
|
17069
17402
|
const stagingInfo = await lstat2(stagingDirectory);
|
|
17070
17403
|
if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
|
|
17071
|
-
const stagingPath =
|
|
17404
|
+
const stagingPath = join7(stagingDirectory, `.tinycloud-share-${randomBytes2(16).toString("hex")}.tmp`);
|
|
17072
17405
|
let temporaryPath;
|
|
17073
17406
|
let handle;
|
|
17074
17407
|
try {
|
|
@@ -17105,7 +17438,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
17105
17438
|
await rm3(stagingDirectory, { recursive: true, force: true });
|
|
17106
17439
|
await directoryHandle.close();
|
|
17107
17440
|
}
|
|
17108
|
-
return
|
|
17441
|
+
return join7(outputDirectory, safeName);
|
|
17109
17442
|
}
|
|
17110
17443
|
|
|
17111
17444
|
// src/share/errors.ts
|
|
@@ -17391,7 +17724,7 @@ function registerShareCommand(program2) {
|
|
|
17391
17724
|
if (json) writeJson2({ ...redactPublishedShare(result), expiryClamped: result.metadata.expiryClamped === true, ...notification === void 0 ? {} : { notification } });
|
|
17392
17725
|
else publishHuman(result);
|
|
17393
17726
|
} catch (error) {
|
|
17394
|
-
handleError(shareCliError(error));
|
|
17727
|
+
return handleError(shareCliError(error));
|
|
17395
17728
|
}
|
|
17396
17729
|
});
|
|
17397
17730
|
share.command("inspect [url]").description("Verify a share link and print safe metadata").option("--stdin", "Read the complete URL from stdin").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Require this canonical Share origin", SHARE_ORIGIN).action(async (url, options, command) => {
|
|
@@ -17401,7 +17734,7 @@ function registerShareCommand(program2) {
|
|
|
17401
17734
|
if (json) writeJson2(result);
|
|
17402
17735
|
else inspectHuman(result);
|
|
17403
17736
|
} catch (error) {
|
|
17404
|
-
handleError(shareCliError(error));
|
|
17737
|
+
return handleError(shareCliError(error));
|
|
17405
17738
|
}
|
|
17406
17739
|
});
|
|
17407
17740
|
share.command("receive [url]").description("Verify and receive a share link").option("--stdin", "Read the complete URL from stdin").option("--output <directory>", "Create the file in this directory").option("--stdout", "Write verified plaintext bytes to stdout").option("--force", "Allow replacing an existing non-symlink output").option("--max-bytes <bytes>", "Bound received content bytes").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Require this canonical Share origin", SHARE_ORIGIN).action(async (url, options, command) => {
|
|
@@ -17449,7 +17782,7 @@ function registerShareCommand(program2) {
|
|
|
17449
17782
|
if (json) receiveJson(result, output);
|
|
17450
17783
|
else receiveHuman(output);
|
|
17451
17784
|
} catch (error) {
|
|
17452
|
-
handleError(shareCliError(error));
|
|
17785
|
+
return handleError(shareCliError(error));
|
|
17453
17786
|
}
|
|
17454
17787
|
});
|
|
17455
17788
|
share.command("list").description("List encrypted sender history without complete bearer URLs").option("--json", "Print versioned redacted JSON").action(async (options, command) => {
|
|
@@ -17460,7 +17793,7 @@ function registerShareCommand(program2) {
|
|
|
17460
17793
|
if (json) writeJson2({ protocol: "tinycloud-share", version: 1, shares: result });
|
|
17461
17794
|
else process.stdout.write(result.map((item) => `${item.shareId} ${item.target} ${item.expiresAt}`).join("\n") + (result.length ? "\n" : ""));
|
|
17462
17795
|
} catch (error) {
|
|
17463
|
-
handleError(shareCliError(error));
|
|
17796
|
+
return handleError(shareCliError(error));
|
|
17464
17797
|
}
|
|
17465
17798
|
});
|
|
17466
17799
|
share.command("show <id>").description("Show one redacted sender-history record").option("--reveal-link", "Explicitly include the complete link").option("--json", "Print versioned redacted JSON").action(async (id, options, command) => {
|
|
@@ -17472,7 +17805,7 @@ function registerShareCommand(program2) {
|
|
|
17472
17805
|
if (json) writeJson2({ protocol: "tinycloud-share", version: 1, share: result });
|
|
17473
17806
|
else writeJson2(result);
|
|
17474
17807
|
} catch (error) {
|
|
17475
|
-
handleError(shareCliError(error));
|
|
17808
|
+
return handleError(shareCliError(error));
|
|
17476
17809
|
}
|
|
17477
17810
|
});
|
|
17478
17811
|
share.command("notify <id>").description("Retry idempotent delivery within 5 minutes of publication; after that publish a new share").requiredOption("--to <address>", "Recipient email").option("--json", "Print versioned JSON").action(async (id, options, command) => {
|
|
@@ -17499,7 +17832,7 @@ function registerShareCommand(program2) {
|
|
|
17499
17832
|
if (result.reason === "delivery-window-expired") process.stderr.write(NOTIFY_WINDOW_MESSAGE);
|
|
17500
17833
|
if (result.state === "partial-failure") process.exitCode = 9;
|
|
17501
17834
|
} catch (error) {
|
|
17502
|
-
handleError(shareCliError(error, "notify"));
|
|
17835
|
+
return handleError(shareCliError(error, "notify"));
|
|
17503
17836
|
}
|
|
17504
17837
|
});
|
|
17505
17838
|
share.command("revoke <id>").description("Revoke addressed shares; report bearer retention honestly").option("--ancestor", "Revoke the owner delegation ancestry").option("--json", "Print versioned JSON").action(async (id, options, command) => {
|
|
@@ -17516,7 +17849,7 @@ function registerShareCommand(program2) {
|
|
|
17516
17849
|
else process.stdout.write(`${result.state}
|
|
17517
17850
|
`);
|
|
17518
17851
|
} catch (error) {
|
|
17519
|
-
handleError(shareCliError(error));
|
|
17852
|
+
return handleError(shareCliError(error));
|
|
17520
17853
|
}
|
|
17521
17854
|
});
|
|
17522
17855
|
}
|
|
@@ -17527,7 +17860,7 @@ init_profiles();
|
|
|
17527
17860
|
init_constants();
|
|
17528
17861
|
init_session_expired();
|
|
17529
17862
|
import { readFile as readFile4 } from "fs/promises";
|
|
17530
|
-
import { join as
|
|
17863
|
+
import { join as join8 } from "path";
|
|
17531
17864
|
import { createHash } from "crypto";
|
|
17532
17865
|
import { writeJsonAtomic as writeJsonAtomic2 } from "@tinycloud/operations/state";
|
|
17533
17866
|
import { LocationRecordValidationError, LocationRegistryHttpError, revokePolicyRootV3 } from "@tinycloud/sdk-core";
|
|
@@ -17632,8 +17965,8 @@ function createEncryptedProfileHistory(profileName, sessionSigner) {
|
|
|
17632
17965
|
const observedProfiles = /* @__PURE__ */ new Set();
|
|
17633
17966
|
let preparedKeys;
|
|
17634
17967
|
let preparedKey;
|
|
17635
|
-
const path = async (profile) =>
|
|
17636
|
-
const legacyPath = async (profile) =>
|
|
17968
|
+
const path = async (profile) => join8(await ProfileManager.getCacheDir(profile), "share-history-v2.json");
|
|
17969
|
+
const legacyPath = async (profile) => join8(await ProfileManager.getCacheDir(profile), "share-history-v1.bin");
|
|
17637
17970
|
const b64 = (value) => Buffer.from(value).toString("base64url");
|
|
17638
17971
|
const unb64 = (value) => {
|
|
17639
17972
|
if (typeof value !== "string" || !/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("share history is unavailable");
|
|
@@ -18197,7 +18530,7 @@ function selectedShareProfile() {
|
|
|
18197
18530
|
}
|
|
18198
18531
|
return process.env.TC_PROFILE;
|
|
18199
18532
|
}
|
|
18200
|
-
program.name("tc").description("TinyCloud CLI \u2014 self-sovereign storage from the terminal").version(version).option("-p, --profile <name>", "Profile to use").option("-H, --host <url>", "TinyCloud node URL").option("-v, --verbose", "Enable verbose output").option("--no-cache", "Disable caching").option("-q, --quiet", "Suppress non-essential output").option("--json", "Force JSON output");
|
|
18533
|
+
program.name("tc").description("TinyCloud CLI \u2014 self-sovereign storage from the terminal").version(version).option("-p, --profile <name>", "Profile to use").option("-H, --host <url>", "TinyCloud node URL").option("-v, --verbose", "Enable verbose output").option("--no-cache", "Disable caching").option("-q, --quiet", "Suppress non-essential output").option("--json", "Force JSON output").option("--replication", "Enable read-through replication for this invocation").option("--no-replication", "Disable read-through replication for this invocation").option("--replication-debug", "Write replication diagnostics to stderr");
|
|
18201
18534
|
program.hook("preAction", async (thisCommand) => {
|
|
18202
18535
|
const opts = thisCommand.optsWithGlobals();
|
|
18203
18536
|
setJsonOutputRequested(opts.json === true);
|
|
@@ -18248,7 +18581,7 @@ function firstCommandToken(values) {
|
|
|
18248
18581
|
continue;
|
|
18249
18582
|
}
|
|
18250
18583
|
if (value.startsWith("--profile=") || value.startsWith("--host=")) continue;
|
|
18251
|
-
if (value === "--verbose" || value === "--no-cache" || value === "-q" || value === "--quiet" || value === "--json") continue;
|
|
18584
|
+
if (value === "--verbose" || value === "--no-cache" || value === "-q" || value === "--quiet" || value === "--json" || value === "--replication" || value === "--no-replication" || value === "--replication-debug") continue;
|
|
18252
18585
|
if (value.startsWith("-")) continue;
|
|
18253
18586
|
return value;
|
|
18254
18587
|
}
|
|
@@ -18283,8 +18616,18 @@ ${theme.muted("Repo:")} ${theme.accent("https://github.com/tinycloudlabs/web-sdk
|
|
|
18283
18616
|
});
|
|
18284
18617
|
try {
|
|
18285
18618
|
await program.parseAsync(process.argv);
|
|
18619
|
+
if (await closeReplication()) {
|
|
18620
|
+
process.exitCode = 0;
|
|
18621
|
+
const stdoutFlushed = Promise.withResolvers();
|
|
18622
|
+
process.stdout.write("", () => stdoutFlushed.resolve());
|
|
18623
|
+
await stdoutFlushed.promise;
|
|
18624
|
+
const stderrFlushed = Promise.withResolvers();
|
|
18625
|
+
process.stderr.write("", () => stderrFlushed.resolve());
|
|
18626
|
+
await stderrFlushed.promise;
|
|
18627
|
+
process.exit();
|
|
18628
|
+
}
|
|
18286
18629
|
} catch (error) {
|
|
18287
|
-
handleError(error);
|
|
18630
|
+
await handleError(error);
|
|
18288
18631
|
}
|
|
18289
18632
|
/*! Bundled license information:
|
|
18290
18633
|
|