@tinycloud/cli 1.2.0-beta.2 → 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 +23 -1
- package/dist/index.js +319 -48
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +1700 -638
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +5 -5
- package/skills/tc-cli/REFERENCE.md +28 -1
package/dist/legacy-entry.js
CHANGED
|
@@ -131,6 +131,114 @@ var init_storage = __esm({
|
|
|
131
131
|
}
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
// src/lib/replication-registry.ts
|
|
135
|
+
function stopForSignal(signal) {
|
|
136
|
+
void closeReplication().finally(() => {
|
|
137
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function installSignalHandlers() {
|
|
141
|
+
if (state.installedSignalHandlers) return;
|
|
142
|
+
state.installedSignalHandlers = true;
|
|
143
|
+
state.sigintHandler = () => stopForSignal("SIGINT");
|
|
144
|
+
state.sigtermHandler = () => stopForSignal("SIGTERM");
|
|
145
|
+
process.once("SIGINT", state.sigintHandler);
|
|
146
|
+
process.once("SIGTERM", state.sigtermHandler);
|
|
147
|
+
}
|
|
148
|
+
function removeSignalHandlers() {
|
|
149
|
+
if (!state.installedSignalHandlers) return;
|
|
150
|
+
state.installedSignalHandlers = false;
|
|
151
|
+
if (state.sigintHandler) process.removeListener("SIGINT", state.sigintHandler);
|
|
152
|
+
if (state.sigtermHandler) process.removeListener("SIGTERM", state.sigtermHandler);
|
|
153
|
+
state.sigintHandler = void 0;
|
|
154
|
+
state.sigtermHandler = void 0;
|
|
155
|
+
}
|
|
156
|
+
function registerReplication(profile, control) {
|
|
157
|
+
state.controls.set(profile, control);
|
|
158
|
+
state.allControls.add(control);
|
|
159
|
+
state.generation += 1;
|
|
160
|
+
installSignalHandlers();
|
|
161
|
+
}
|
|
162
|
+
function replicationForProfile(profile) {
|
|
163
|
+
return state.controls.get(profile);
|
|
164
|
+
}
|
|
165
|
+
function registeredReplications() {
|
|
166
|
+
return state.controls;
|
|
167
|
+
}
|
|
168
|
+
function closeReplication(schedule = scheduleTimeout) {
|
|
169
|
+
if (state.closing) return state.closing;
|
|
170
|
+
removeSignalHandlers();
|
|
171
|
+
let resolveClosing;
|
|
172
|
+
let rejectClosing;
|
|
173
|
+
const closing = new Promise((resolve5, reject) => {
|
|
174
|
+
resolveClosing = resolve5;
|
|
175
|
+
rejectClosing = reject;
|
|
176
|
+
});
|
|
177
|
+
state.closing = closing;
|
|
178
|
+
void (async () => {
|
|
179
|
+
let timeout;
|
|
180
|
+
const deadline = new Promise((resolve5) => {
|
|
181
|
+
timeout = schedule(() => resolve5(true), CLOSE_TIMEOUT_MS);
|
|
182
|
+
});
|
|
183
|
+
let timedOut = false;
|
|
184
|
+
const abandonQueuedControls = () => {
|
|
185
|
+
for (const control of state.allControls) {
|
|
186
|
+
void Promise.resolve().then(() => control.close()).catch(() => void 0);
|
|
187
|
+
}
|
|
188
|
+
state.controls.clear();
|
|
189
|
+
state.allControls.clear();
|
|
190
|
+
};
|
|
191
|
+
try {
|
|
192
|
+
while (!timedOut) {
|
|
193
|
+
while (state.allControls.size > 0) {
|
|
194
|
+
const generation = state.generation;
|
|
195
|
+
const pending = [...state.allControls];
|
|
196
|
+
state.controls.clear();
|
|
197
|
+
state.allControls.clear();
|
|
198
|
+
const drained = Promise.allSettled(pending.map((control) => Promise.resolve().then(() => control.close()))).then(() => false);
|
|
199
|
+
timedOut ||= await Promise.race([drained, deadline]);
|
|
200
|
+
if (timedOut) {
|
|
201
|
+
abandonQueuedControls();
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
if (state.generation === generation && state.allControls.size === 0) break;
|
|
205
|
+
}
|
|
206
|
+
if (timedOut) break;
|
|
207
|
+
const turn = new Promise((resolve5) => setImmediate(() => resolve5(false)));
|
|
208
|
+
timedOut ||= await Promise.race([turn, deadline]);
|
|
209
|
+
if (timedOut) abandonQueuedControls();
|
|
210
|
+
else if (state.allControls.size === 0) break;
|
|
211
|
+
}
|
|
212
|
+
state.closing = void 0;
|
|
213
|
+
if (state.allControls.size === 0) removeSignalHandlers();
|
|
214
|
+
resolveClosing(timedOut);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
state.closing = void 0;
|
|
217
|
+
if (state.allControls.size === 0) removeSignalHandlers();
|
|
218
|
+
rejectClosing(error);
|
|
219
|
+
} finally {
|
|
220
|
+
clearTimeout(timeout);
|
|
221
|
+
}
|
|
222
|
+
})();
|
|
223
|
+
return closing;
|
|
224
|
+
}
|
|
225
|
+
var registryKey, globalState, state, CLOSE_TIMEOUT_MS, scheduleTimeout;
|
|
226
|
+
var init_replication_registry = __esm({
|
|
227
|
+
"src/lib/replication-registry.ts"() {
|
|
228
|
+
"use strict";
|
|
229
|
+
registryKey = /* @__PURE__ */ Symbol.for("@tinycloud/cli/replication-registry");
|
|
230
|
+
globalState = globalThis;
|
|
231
|
+
state = globalState[registryKey] ??= {
|
|
232
|
+
controls: /* @__PURE__ */ new Map(),
|
|
233
|
+
allControls: /* @__PURE__ */ new Set(),
|
|
234
|
+
installedSignalHandlers: false,
|
|
235
|
+
generation: 0
|
|
236
|
+
};
|
|
237
|
+
CLOSE_TIMEOUT_MS = 3e3;
|
|
238
|
+
scheduleTimeout = (callback, delayMs) => setTimeout(callback, delayMs);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
134
242
|
// src/output/theme.ts
|
|
135
243
|
import chalk from "chalk";
|
|
136
244
|
var TC_PALETTE, theme;
|
|
@@ -546,7 +654,8 @@ function handleError(error) {
|
|
|
546
654
|
...Object.keys(meta).length ? { meta } : {},
|
|
547
655
|
warnings: mergeWarnings(drainWarnings(), operationWarnings(cliError.metadata?.warnings))
|
|
548
656
|
});
|
|
549
|
-
process.exit(cliError.exitCode);
|
|
657
|
+
if (registeredReplications().size === 0) return process.exit(cliError.exitCode);
|
|
658
|
+
return closeReplication().then(() => process.exit(cliError.exitCode));
|
|
550
659
|
}
|
|
551
660
|
function safeCliRecovery(value, context) {
|
|
552
661
|
if (typeof value !== "object" || value === null || Array.isArray(value) || !("action" in value)) return void 0;
|
|
@@ -656,6 +765,7 @@ var activeProfileName, CLIError, SETUP_KEYS;
|
|
|
656
765
|
var init_errors = __esm({
|
|
657
766
|
"src/output/errors.ts"() {
|
|
658
767
|
"use strict";
|
|
768
|
+
init_replication_registry();
|
|
659
769
|
init_constants();
|
|
660
770
|
init_formatter();
|
|
661
771
|
init_storage2();
|
|
@@ -1214,11 +1324,11 @@ var init_md = __esm({
|
|
|
1214
1324
|
if (len % 4)
|
|
1215
1325
|
throw new Error("_sha2: outputLen should be aligned to 32bit");
|
|
1216
1326
|
const outLen = len / 4;
|
|
1217
|
-
const
|
|
1218
|
-
if (outLen >
|
|
1327
|
+
const state2 = this.get();
|
|
1328
|
+
if (outLen > state2.length)
|
|
1219
1329
|
throw new Error("_sha2: outputLen bigger than state");
|
|
1220
1330
|
for (let i = 0; i < outLen; i++)
|
|
1221
|
-
oview.setUint32(4 * i,
|
|
1331
|
+
oview.setUint32(4 * i, state2[i], isLE2);
|
|
1222
1332
|
}
|
|
1223
1333
|
digest() {
|
|
1224
1334
|
const { buffer, outputLen } = this;
|
|
@@ -6401,6 +6511,12 @@ function projectDiagnosticData(value) {
|
|
|
6401
6511
|
const boolean = read(value, key);
|
|
6402
6512
|
if (typeof boolean === "boolean") projected[key] = boolean;
|
|
6403
6513
|
}
|
|
6514
|
+
for (const [key, allowed] of Object.entries(SAFE_REPLICATION_FIELDS)) {
|
|
6515
|
+
const candidate = read(value, key);
|
|
6516
|
+
if (typeof candidate === "string" && allowed.includes(candidate)) {
|
|
6517
|
+
projected[key] = candidate;
|
|
6518
|
+
}
|
|
6519
|
+
}
|
|
6404
6520
|
const url = read(value, "url");
|
|
6405
6521
|
if (url !== void 0) projected.url = REDACTED;
|
|
6406
6522
|
const error = read(value, "error");
|
|
@@ -8322,7 +8438,7 @@ function encOk(data) {
|
|
|
8322
8438
|
function encErr(error) {
|
|
8323
8439
|
return { ok: false, error };
|
|
8324
8440
|
}
|
|
8325
|
-
var import_decrypt_transport_response_error, ErrorCodes, defaultRetryPolicy, TelemetryEvents, REDACTED, SAFE_NUMBER_FIELDS, SAFE_BOOLEAN_FIELDS, DEBUG_FLAG, MAX_EVENTS, TinyCloudDebugLogger, tinyCloudDebugLogger, ServiceErrorSchema, GenericResultSchema, KVResponseHeadersSchema, GenericKVResponseSchema, KVListResponseSchema, KVListResultSchema, ServiceRequestEventSchema, ServiceResponseEventSchema, ServiceErrorEventSchema, ServiceRetryEventSchema, TelemetrySpanEventSchema, RetryPolicySchema, ServiceSessionSchema, URN_PREFIX, NETWORK_NAME_RE, PKH_EIP155_DID_RE, NetworkIdError, RequestTimeoutError, grantableActions, capabilityServices, MAX_CAUSE_DEPTH2, STORAGE_FULL_MESSAGE, STORAGE_WRITE_TOO_LARGE_MESSAGE, MAX_TIMER_DELAY_MS, BaseService, PrefixedKVService, DEFAULT_SIGNED_READ_URL_EXPIRY_MS, KVAction, MAX_KV_BATCH_READ_ITEMS, pathByteEncoder, REVOKED_BODY, KVService, SQLMigrations, DatabaseHandle, SQLAction, DDL_TOKENS, MIGRATIONS_TABLE, MIGRATIONS_SCHEMA, MIGRATIONS_META_NAMESPACE, MIGRATIONS_META_ID, SQLService, DuckDbDatabaseHandle, DuckDbAction, DuckDbService, AsyncQueue, HOOK_STREAM_RETRY_BASE_DELAY_MS, HOOK_STREAM_RETRY_MAX_DELAY_MS, HOOK_STREAM_HEALTHY_MS, HooksService, SUPERSEDED, STREAM_ERROR_NAMES, STREAM_ERROR_CODES, VaultVersionConfig, CURRENT_VAULT_VERSION, VaultHeaders, DB_NAME, DB_VERSION, STORE_NAME, WRAP_KEY_ID, DataVaultService, SECRET_NAME_RE, SECRET_PREFIX, SCOPED_SECRET_PREFIX, RESERVED_SECRET_SCOPES, TC832_RESERVED_SCOPES, HEX, DEFAULT_ENCRYPTION_ALG, ENVELOPE_VERSION, DEFAULT_KEY_VERSION, DECRYPT_FACT_TYPE, DECRYPT_RESULT_TYPE, EncryptionService;
|
|
8441
|
+
var import_decrypt_transport_response_error, ErrorCodes, defaultRetryPolicy, TelemetryEvents, REDACTED, SAFE_NUMBER_FIELDS, SAFE_BOOLEAN_FIELDS, SAFE_REPLICATION_FIELDS, DEBUG_FLAG, MAX_EVENTS, TinyCloudDebugLogger, tinyCloudDebugLogger, ServiceErrorSchema, GenericResultSchema, KVResponseHeadersSchema, GenericKVResponseSchema, KVListResponseSchema, KVListResultSchema, ServiceRequestEventSchema, ServiceResponseEventSchema, ServiceErrorEventSchema, ServiceRetryEventSchema, TelemetrySpanEventSchema, RetryPolicySchema, ServiceSessionSchema, URN_PREFIX, NETWORK_NAME_RE, PKH_EIP155_DID_RE, NetworkIdError, RequestTimeoutError, grantableActions, capabilityServices, MAX_CAUSE_DEPTH2, STORAGE_FULL_MESSAGE, STORAGE_WRITE_TOO_LARGE_MESSAGE, MAX_TIMER_DELAY_MS, BaseService, PrefixedKVService, DEFAULT_SIGNED_READ_URL_EXPIRY_MS, KVAction, MAX_KV_BATCH_READ_ITEMS, pathByteEncoder, REVOKED_BODY, KVService, SQLMigrations, DatabaseHandle, SQLAction, DDL_TOKENS, MIGRATIONS_TABLE, MIGRATIONS_SCHEMA, MIGRATIONS_META_NAMESPACE, MIGRATIONS_META_ID, SQLService, DuckDbDatabaseHandle, DuckDbAction, DuckDbService, AsyncQueue, HOOK_STREAM_RETRY_BASE_DELAY_MS, HOOK_STREAM_RETRY_MAX_DELAY_MS, HOOK_STREAM_HEALTHY_MS, HooksService, SUPERSEDED, STREAM_ERROR_NAMES, STREAM_ERROR_CODES, VaultVersionConfig, CURRENT_VAULT_VERSION, VaultHeaders, DB_NAME, DB_VERSION, STORE_NAME, WRAP_KEY_ID, DataVaultService, SECRET_NAME_RE, SECRET_PREFIX, SCOPED_SECRET_PREFIX, RESERVED_SECRET_SCOPES, TC832_RESERVED_SCOPES, HEX, DEFAULT_ENCRYPTION_ALG, ENVELOPE_VERSION, DEFAULT_KEY_VERSION, DECRYPT_FACT_TYPE, DECRYPT_RESULT_TYPE, EncryptionService;
|
|
8326
8442
|
var init_dist2 = __esm({
|
|
8327
8443
|
"../sdk-services/dist/index.js"() {
|
|
8328
8444
|
"use strict";
|
|
@@ -8392,7 +8508,12 @@ var init_dist2 = __esm({
|
|
|
8392
8508
|
SERVICE_ERROR: "service.error",
|
|
8393
8509
|
SERVICE_RETRY: "service.retry",
|
|
8394
8510
|
SESSION_CHANGED: "session.changed",
|
|
8395
|
-
SESSION_EXPIRED: "session.expired"
|
|
8511
|
+
SESSION_EXPIRED: "session.expired",
|
|
8512
|
+
REPLICATION_READ: "replication.read",
|
|
8513
|
+
REPLICATION_WRITE: "replication.write",
|
|
8514
|
+
REPLICATION_SYNC: "replication.sync",
|
|
8515
|
+
REPLICATION_STATE: "replication.state",
|
|
8516
|
+
REPLICATION_DIVERGENCE: "replication.divergence"
|
|
8396
8517
|
};
|
|
8397
8518
|
REDACTED = "[REDACTED]";
|
|
8398
8519
|
SAFE_NUMBER_FIELDS = /* @__PURE__ */ new Set([
|
|
@@ -8401,9 +8522,34 @@ var init_dist2 = __esm({
|
|
|
8401
8522
|
"endedAt",
|
|
8402
8523
|
"startedAt",
|
|
8403
8524
|
"status",
|
|
8404
|
-
"timestamp"
|
|
8525
|
+
"timestamp",
|
|
8526
|
+
"latencyMs",
|
|
8527
|
+
"stalenessMs",
|
|
8528
|
+
"count",
|
|
8529
|
+
"changes",
|
|
8530
|
+
"deleted",
|
|
8531
|
+
"fetched",
|
|
8532
|
+
"pages",
|
|
8533
|
+
"lagMs",
|
|
8534
|
+
"pendingCleared"
|
|
8405
8535
|
]);
|
|
8406
8536
|
SAFE_BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["authenticated", "ok", "persisted"]);
|
|
8537
|
+
SAFE_REPLICATION_FIELDS = {
|
|
8538
|
+
source: ["replica", "network", "none"],
|
|
8539
|
+
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"],
|
|
8540
|
+
op: ["get", "list", "put", "delete", "batchPut"],
|
|
8541
|
+
trigger: ["start", "interval", "stale_read", "manual"],
|
|
8542
|
+
outcome: ["found", "not_found", "error", "committed", "failed", "ambiguous", "ok", "busy", "aborted"],
|
|
8543
|
+
class: ["offline", "authority", "storage", "node"],
|
|
8544
|
+
state: ["opened", "grant_installed", "grant_missing", "runtime_unsupported", "unavailable", "revoked", "recreated", "purged", "purge_failed", "pending_store_error", "pinned", "pending_cleared", "closed", "idle", "ready"],
|
|
8545
|
+
kind: ["value", "missing_local", "extra_local", "keys", "order"],
|
|
8546
|
+
coverage: ["empty", "bootstrapping", "complete"],
|
|
8547
|
+
authority: ["valid", "expired", "revoked", "not-yet-valid"],
|
|
8548
|
+
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"],
|
|
8549
|
+
pendingState: ["in_flight", "committed", "ambiguous"],
|
|
8550
|
+
strategy: ["session", "minted", "installed"],
|
|
8551
|
+
verify: ["match", "diverged", "error", "aborted"]
|
|
8552
|
+
};
|
|
8407
8553
|
DEBUG_FLAG = "TinyCloud_debug";
|
|
8408
8554
|
MAX_EVENTS = 1e3;
|
|
8409
8555
|
TinyCloudDebugLogger = class {
|
|
@@ -8854,6 +9000,7 @@ var init_dist2 = __esm({
|
|
|
8854
9000
|
const parents = [
|
|
8855
9001
|
this.abortController.signal,
|
|
8856
9002
|
this.context?.abortSignal,
|
|
9003
|
+
this.context?.operationAbortSignal,
|
|
8857
9004
|
signal
|
|
8858
9005
|
].filter((parent) => parent !== void 0);
|
|
8859
9006
|
for (const parent of parents) {
|
|
@@ -9068,14 +9215,28 @@ var init_dist2 = __esm({
|
|
|
9068
9215
|
*/
|
|
9069
9216
|
constructor(config = {}) {
|
|
9070
9217
|
super();
|
|
9218
|
+
this.readThrough = null;
|
|
9219
|
+
this.readThroughReady = null;
|
|
9071
9220
|
this._config = config;
|
|
9072
9221
|
}
|
|
9222
|
+
/** Delay KV routing until an asynchronously loaded read-through is attached. */
|
|
9223
|
+
setReadThroughReady(ready) {
|
|
9224
|
+
const settled = ready.then(() => void 0, () => void 0);
|
|
9225
|
+
this.readThroughReady = settled;
|
|
9226
|
+
void settled.then(() => {
|
|
9227
|
+
if (this.readThroughReady === settled) this.readThroughReady = null;
|
|
9228
|
+
});
|
|
9229
|
+
}
|
|
9073
9230
|
/**
|
|
9074
9231
|
* Get the service configuration.
|
|
9075
9232
|
*/
|
|
9076
9233
|
get config() {
|
|
9077
9234
|
return this._config;
|
|
9078
9235
|
}
|
|
9236
|
+
/** Install or remove the optional replication read-through policy. */
|
|
9237
|
+
setReadThrough(readThrough) {
|
|
9238
|
+
this.readThrough = readThrough;
|
|
9239
|
+
}
|
|
9079
9240
|
handleQuotaErrorResponse(response, errorText, key) {
|
|
9080
9241
|
if (response.status !== 402 && response.status !== 413) {
|
|
9081
9242
|
return void 0;
|
|
@@ -9329,6 +9490,7 @@ var init_dist2 = __esm({
|
|
|
9329
9490
|
return aligned;
|
|
9330
9491
|
}
|
|
9331
9492
|
async batchRead(keys, action, options) {
|
|
9493
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
9332
9494
|
if (!this.requireAuth()) return err(authRequiredError("kv"));
|
|
9333
9495
|
if (keys.length === 0) return ok({ results: [], count: 0 });
|
|
9334
9496
|
if (keys.length > MAX_KV_BATCH_READ_ITEMS) {
|
|
@@ -9579,6 +9741,7 @@ var init_dist2 = __esm({
|
|
|
9579
9741
|
*/
|
|
9580
9742
|
async get(key, options) {
|
|
9581
9743
|
return this.withTelemetry("get", key, async () => {
|
|
9744
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
9582
9745
|
if (!this.requireAuth()) {
|
|
9583
9746
|
return err(authRequiredError("kv"));
|
|
9584
9747
|
}
|
|
@@ -9592,64 +9755,38 @@ var init_dist2 = __esm({
|
|
|
9592
9755
|
const path = this.getFullPath(key, options?.prefix);
|
|
9593
9756
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
9594
9757
|
try {
|
|
9595
|
-
const
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
|
|
9609
|
-
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
|
|
9613
|
-
|
|
9614
|
-
|
|
9615
|
-
|
|
9616
|
-
|
|
9617
|
-
response,
|
|
9618
|
-
errorText2,
|
|
9619
|
-
[path],
|
|
9620
|
-
KVAction.GET,
|
|
9621
|
-
structuredCapability ? { permissionHint, ...structuredCapability } : {}
|
|
9622
|
-
);
|
|
9623
|
-
}
|
|
9624
|
-
if (response.status === 404) {
|
|
9625
|
-
return await this.classifyNotFound(response, key);
|
|
9626
|
-
}
|
|
9627
|
-
const errorText = await response.text();
|
|
9628
|
-
if (response.status === 413) {
|
|
9629
|
-
return err(serviceError(
|
|
9630
|
-
ErrorCodes.KV_RESPONSE_TOO_LARGE,
|
|
9631
|
-
`KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
|
|
9632
|
-
"kv",
|
|
9633
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9634
|
-
));
|
|
9758
|
+
const network = () => this.getFromNetwork(key, path, options, request.signal);
|
|
9759
|
+
if (options?.source === "network") {
|
|
9760
|
+
const observer = this.readThrough;
|
|
9761
|
+
if (observer === null) return await network();
|
|
9762
|
+
let outcome = "error";
|
|
9763
|
+
const startedAt = Date.now();
|
|
9764
|
+
try {
|
|
9765
|
+
const result = await network();
|
|
9766
|
+
outcome = result.ok ? "found" : result.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error";
|
|
9767
|
+
return result;
|
|
9768
|
+
} finally {
|
|
9769
|
+
try {
|
|
9770
|
+
observer.observeNetworkRequested({
|
|
9771
|
+
op: "get",
|
|
9772
|
+
space: this.context.session.spaceId,
|
|
9773
|
+
path,
|
|
9774
|
+
reason: "NETWORK_REQUESTED",
|
|
9775
|
+
outcome,
|
|
9776
|
+
latencyMs: Date.now() - startedAt
|
|
9777
|
+
});
|
|
9778
|
+
} catch {
|
|
9779
|
+
}
|
|
9635
9780
|
}
|
|
9636
|
-
return err(
|
|
9637
|
-
serviceError(
|
|
9638
|
-
ErrorCodes.NETWORK_ERROR,
|
|
9639
|
-
`Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
9640
|
-
"kv",
|
|
9641
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9642
|
-
)
|
|
9643
|
-
);
|
|
9644
9781
|
}
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
|
|
9652
|
-
|
|
9782
|
+
if (this.readThrough === null) return await network();
|
|
9783
|
+
return await this.readThrough.get({
|
|
9784
|
+
space: this.context.session.spaceId,
|
|
9785
|
+
key,
|
|
9786
|
+
path,
|
|
9787
|
+
options,
|
|
9788
|
+
signal: request.signal,
|
|
9789
|
+
network
|
|
9653
9790
|
});
|
|
9654
9791
|
} catch (error) {
|
|
9655
9792
|
return err(wrapError2("kv", error));
|
|
@@ -9658,6 +9795,56 @@ var init_dist2 = __esm({
|
|
|
9658
9795
|
}
|
|
9659
9796
|
});
|
|
9660
9797
|
}
|
|
9798
|
+
async getFromNetwork(key, path, options, signal) {
|
|
9799
|
+
const response = await this.invokeOperation(
|
|
9800
|
+
path,
|
|
9801
|
+
KVAction.GET,
|
|
9802
|
+
void 0,
|
|
9803
|
+
signal,
|
|
9804
|
+
options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
|
|
9805
|
+
);
|
|
9806
|
+
if (!response.ok) {
|
|
9807
|
+
if (response.status === 401 || response.status === 403) {
|
|
9808
|
+
const errorText2 = await this.readAuthorizationText(response);
|
|
9809
|
+
const permissionHint = parsePermissionHintFromErrorText(errorText2);
|
|
9810
|
+
const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf2({
|
|
9811
|
+
service: "kv",
|
|
9812
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
9813
|
+
meta: {
|
|
9814
|
+
status: response.status,
|
|
9815
|
+
resource: `${this.context.session.spaceId}/kv/${path}`,
|
|
9816
|
+
requiredAction: KVAction.GET
|
|
9817
|
+
}
|
|
9818
|
+
}) : void 0;
|
|
9819
|
+
return this.authorizationFailure(
|
|
9820
|
+
`Failed to get key ${JSON.stringify(key)}`,
|
|
9821
|
+
response,
|
|
9822
|
+
errorText2,
|
|
9823
|
+
[path],
|
|
9824
|
+
KVAction.GET,
|
|
9825
|
+
structuredCapability ? { permissionHint, ...structuredCapability } : {}
|
|
9826
|
+
);
|
|
9827
|
+
}
|
|
9828
|
+
if (response.status === 404) return this.classifyNotFound(response, key);
|
|
9829
|
+
const errorText = await response.text();
|
|
9830
|
+
if (response.status === 413) {
|
|
9831
|
+
return err(serviceError(
|
|
9832
|
+
ErrorCodes.KV_RESPONSE_TOO_LARGE,
|
|
9833
|
+
`KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
|
|
9834
|
+
"kv",
|
|
9835
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9836
|
+
));
|
|
9837
|
+
}
|
|
9838
|
+
return err(serviceError(
|
|
9839
|
+
ErrorCodes.NETWORK_ERROR,
|
|
9840
|
+
`Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
9841
|
+
"kv",
|
|
9842
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9843
|
+
));
|
|
9844
|
+
}
|
|
9845
|
+
const data = await this.parseResponse(response, options?.raw, options?.binary);
|
|
9846
|
+
return ok({ data, headers: this.createResponseHeaders(response.headers) });
|
|
9847
|
+
}
|
|
9661
9848
|
async batchGet(keys, options) {
|
|
9662
9849
|
return this.withTelemetry(
|
|
9663
9850
|
"batchGet",
|
|
@@ -9670,6 +9857,7 @@ var init_dist2 = __esm({
|
|
|
9670
9857
|
*/
|
|
9671
9858
|
async put(key, value, options) {
|
|
9672
9859
|
return this.withTelemetry("put", key, async () => {
|
|
9860
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
9673
9861
|
if (!this.requireAuth()) {
|
|
9674
9862
|
return err(authRequiredError("kv"));
|
|
9675
9863
|
}
|
|
@@ -9684,63 +9872,15 @@ var init_dist2 = __esm({
|
|
|
9684
9872
|
const body = this.serializePutValue(value, options?.contentType);
|
|
9685
9873
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
9686
9874
|
try {
|
|
9687
|
-
const
|
|
9688
|
-
|
|
9689
|
-
|
|
9690
|
-
|
|
9691
|
-
|
|
9692
|
-
{
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
);
|
|
9697
|
-
if (response.status === 401 || response.status === 403) {
|
|
9698
|
-
return this.authorizationFailure(
|
|
9699
|
-
`Failed to put key ${JSON.stringify(key)}`,
|
|
9700
|
-
response,
|
|
9701
|
-
await this.readAuthorizationText(response),
|
|
9702
|
-
[path],
|
|
9703
|
-
KVAction.PUT
|
|
9704
|
-
);
|
|
9705
|
-
}
|
|
9706
|
-
if (!response.ok) {
|
|
9707
|
-
const errorText = await response.text();
|
|
9708
|
-
if (response.status === 412) {
|
|
9709
|
-
return err(serviceError(
|
|
9710
|
-
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
9711
|
-
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
9712
|
-
"kv",
|
|
9713
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9714
|
-
));
|
|
9715
|
-
}
|
|
9716
|
-
if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
|
|
9717
|
-
return err(serviceError(
|
|
9718
|
-
ErrorCodes.KV_CONFLICT,
|
|
9719
|
-
`Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
|
|
9720
|
-
"kv",
|
|
9721
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9722
|
-
));
|
|
9723
|
-
}
|
|
9724
|
-
const quotaError = this.handleQuotaErrorResponse(
|
|
9725
|
-
response,
|
|
9726
|
-
errorText,
|
|
9727
|
-
key
|
|
9728
|
-
);
|
|
9729
|
-
if (quotaError) {
|
|
9730
|
-
return quotaError;
|
|
9731
|
-
}
|
|
9732
|
-
return err(
|
|
9733
|
-
serviceError(
|
|
9734
|
-
ErrorCodes.KV_WRITE_FAILED,
|
|
9735
|
-
`Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
9736
|
-
"kv",
|
|
9737
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9738
|
-
)
|
|
9739
|
-
);
|
|
9740
|
-
}
|
|
9741
|
-
return ok({
|
|
9742
|
-
data: void 0,
|
|
9743
|
-
headers: this.createResponseHeaders(response.headers)
|
|
9875
|
+
const network = () => this.putToNetwork(key, path, body, options, request.signal);
|
|
9876
|
+
if (this.readThrough === null) return await network();
|
|
9877
|
+
return await this.readThrough.write({
|
|
9878
|
+
op: "put",
|
|
9879
|
+
space: this.context.session.spaceId,
|
|
9880
|
+
entries: [{ path, body }],
|
|
9881
|
+
signal: request.signal,
|
|
9882
|
+
conditional: options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0,
|
|
9883
|
+
network
|
|
9744
9884
|
});
|
|
9745
9885
|
} catch (error) {
|
|
9746
9886
|
return err(wrapError2("kv", error));
|
|
@@ -9749,11 +9889,64 @@ var init_dist2 = __esm({
|
|
|
9749
9889
|
}
|
|
9750
9890
|
});
|
|
9751
9891
|
}
|
|
9892
|
+
async putToNetwork(key, path, body, options, signal) {
|
|
9893
|
+
const response = await this.invokeOperation(
|
|
9894
|
+
path,
|
|
9895
|
+
KVAction.PUT,
|
|
9896
|
+
body,
|
|
9897
|
+
signal,
|
|
9898
|
+
{
|
|
9899
|
+
...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
|
|
9900
|
+
...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
|
|
9901
|
+
}
|
|
9902
|
+
);
|
|
9903
|
+
if (response.status === 401 || response.status === 403) {
|
|
9904
|
+
return this.authorizationFailure(
|
|
9905
|
+
`Failed to put key ${JSON.stringify(key)}`,
|
|
9906
|
+
response,
|
|
9907
|
+
await this.readAuthorizationText(response),
|
|
9908
|
+
[path],
|
|
9909
|
+
KVAction.PUT
|
|
9910
|
+
);
|
|
9911
|
+
}
|
|
9912
|
+
if (!response.ok) {
|
|
9913
|
+
const errorText = await response.text();
|
|
9914
|
+
if (response.status === 412) {
|
|
9915
|
+
return err(serviceError(
|
|
9916
|
+
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
9917
|
+
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
9918
|
+
"kv",
|
|
9919
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9920
|
+
));
|
|
9921
|
+
}
|
|
9922
|
+
if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
|
|
9923
|
+
return err(serviceError(
|
|
9924
|
+
ErrorCodes.KV_CONFLICT,
|
|
9925
|
+
`Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
|
|
9926
|
+
"kv",
|
|
9927
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9928
|
+
));
|
|
9929
|
+
}
|
|
9930
|
+
const quotaError = this.handleQuotaErrorResponse(response, errorText, key);
|
|
9931
|
+
if (quotaError) return quotaError;
|
|
9932
|
+
return err(serviceError(
|
|
9933
|
+
ErrorCodes.KV_WRITE_FAILED,
|
|
9934
|
+
`Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
9935
|
+
"kv",
|
|
9936
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9937
|
+
));
|
|
9938
|
+
}
|
|
9939
|
+
return ok({
|
|
9940
|
+
data: void 0,
|
|
9941
|
+
headers: this.createResponseHeaders(response.headers)
|
|
9942
|
+
});
|
|
9943
|
+
}
|
|
9752
9944
|
/**
|
|
9753
9945
|
* Store multiple values in one TinyCloud KV invocation.
|
|
9754
9946
|
*/
|
|
9755
9947
|
async batchPut(items, options) {
|
|
9756
9948
|
return this.withTelemetry("batchPut", String(items.length), async () => {
|
|
9949
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
9757
9950
|
if (!this.requireAuth()) {
|
|
9758
9951
|
return err(authRequiredError("kv"));
|
|
9759
9952
|
}
|
|
@@ -9784,155 +9977,177 @@ var init_dist2 = __esm({
|
|
|
9784
9977
|
}
|
|
9785
9978
|
seen.add(path);
|
|
9786
9979
|
}
|
|
9787
|
-
let requestMayHaveDispatched = false;
|
|
9788
9980
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
9789
9981
|
try {
|
|
9790
|
-
const
|
|
9791
|
-
|
|
9792
|
-
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
session,
|
|
9799
|
-
|
|
9800
|
-
|
|
9801
|
-
|
|
9802
|
-
|
|
9803
|
-
|
|
9804
|
-
|
|
9982
|
+
const network = () => this.batchPutToNetwork(items, paths, request.signal);
|
|
9983
|
+
if (this.readThrough === null) return await network();
|
|
9984
|
+
const entries = items.map((item, index) => ({
|
|
9985
|
+
path: paths[index],
|
|
9986
|
+
body: this.serializeBatchPutValue(item)
|
|
9987
|
+
}));
|
|
9988
|
+
return await this.readThrough.write({
|
|
9989
|
+
op: "batchPut",
|
|
9990
|
+
space: session.spaceId,
|
|
9991
|
+
entries,
|
|
9992
|
+
signal: request.signal,
|
|
9993
|
+
network: () => this.batchPutToNetwork(items, paths, request.signal, entries)
|
|
9994
|
+
});
|
|
9995
|
+
} catch (error) {
|
|
9996
|
+
const wrapped = wrapError2("kv", error);
|
|
9997
|
+
return err({ ...wrapped, meta: { ...wrapped.meta, requestMayHaveDispatched: false } });
|
|
9998
|
+
} finally {
|
|
9999
|
+
request.dispose();
|
|
10000
|
+
}
|
|
10001
|
+
});
|
|
10002
|
+
}
|
|
10003
|
+
async batchPutToNetwork(items, paths, signal, entries) {
|
|
10004
|
+
const session = this.context.session;
|
|
10005
|
+
let requestMayHaveDispatched = false;
|
|
10006
|
+
try {
|
|
10007
|
+
const body = new FormData();
|
|
10008
|
+
for (let index = 0; index < items.length; index++) {
|
|
10009
|
+
body.append(
|
|
10010
|
+
encodeKvBatchPartName(paths[index]),
|
|
10011
|
+
entries?.[index]?.body ?? this.serializeBatchPutValue(items[index])
|
|
9805
10012
|
);
|
|
9806
|
-
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
9815
|
-
|
|
9816
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
9824
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
return this.authorizationFailure(
|
|
9833
|
-
`Failed to batch put ${items.length} key(s)`,
|
|
9834
|
-
response,
|
|
9835
|
-
errorText,
|
|
9836
|
-
paths,
|
|
9837
|
-
KVAction.PUT
|
|
9838
|
-
);
|
|
9839
|
-
}
|
|
9840
|
-
const quotaError = this.handleQuotaErrorResponse(
|
|
9841
|
-
response,
|
|
9842
|
-
errorText,
|
|
9843
|
-
"batch"
|
|
9844
|
-
);
|
|
9845
|
-
if (quotaError) {
|
|
9846
|
-
return quotaError;
|
|
9847
|
-
}
|
|
10013
|
+
}
|
|
10014
|
+
const headers = this.context.invokeAny(
|
|
10015
|
+
session,
|
|
10016
|
+
paths.map((path) => ({
|
|
10017
|
+
spaceId: session.spaceId,
|
|
10018
|
+
service: "kv",
|
|
10019
|
+
path,
|
|
10020
|
+
action: KVAction.PUT
|
|
10021
|
+
}))
|
|
10022
|
+
);
|
|
10023
|
+
const url = `${this.host}/invoke`;
|
|
10024
|
+
const init = {
|
|
10025
|
+
method: "POST",
|
|
10026
|
+
headers,
|
|
10027
|
+
body,
|
|
10028
|
+
signal
|
|
10029
|
+
};
|
|
10030
|
+
const fetchFn = this.context.fetch;
|
|
10031
|
+
requestMayHaveDispatched = true;
|
|
10032
|
+
const response = await fetchFn(url, init);
|
|
10033
|
+
if (!response.ok) {
|
|
10034
|
+
let errorText;
|
|
10035
|
+
try {
|
|
10036
|
+
errorText = await response.text();
|
|
10037
|
+
} catch (textError) {
|
|
10038
|
+
const cause = textError instanceof Error ? textError : new Error(String(textError));
|
|
9848
10039
|
return err(
|
|
9849
10040
|
serviceError(
|
|
9850
10041
|
ErrorCodes.KV_WRITE_FAILED,
|
|
9851
|
-
`Failed to batch put ${items.length} key(s): ${response.status} - ${
|
|
10042
|
+
`Failed to batch put ${items.length} key(s): ${response.status} - <response body could not be read: ${cause.message}>`,
|
|
9852
10043
|
"kv",
|
|
9853
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
10044
|
+
{ cause, meta: { status: response.status, statusText: response.statusText } }
|
|
9854
10045
|
)
|
|
9855
10046
|
);
|
|
9856
10047
|
}
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
serviceError(
|
|
9865
|
-
timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
|
|
9866
|
-
timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
|
|
9867
|
-
"kv",
|
|
9868
|
-
{
|
|
9869
|
-
cause,
|
|
9870
|
-
meta: {
|
|
9871
|
-
...timedOut ? { timeoutMs: cause.timeoutMs } : {},
|
|
9872
|
-
requestMayHaveDispatched: true,
|
|
9873
|
-
responseReceived: true,
|
|
9874
|
-
status: response.status,
|
|
9875
|
-
outcome: "batch-unconfirmed"
|
|
9876
|
-
}
|
|
9877
|
-
}
|
|
9878
|
-
)
|
|
10048
|
+
if (response.status === 401 || response.status === 403) {
|
|
10049
|
+
return this.authorizationFailure(
|
|
10050
|
+
`Failed to batch put ${items.length} key(s)`,
|
|
10051
|
+
response,
|
|
10052
|
+
errorText,
|
|
10053
|
+
paths,
|
|
10054
|
+
KVAction.PUT
|
|
9879
10055
|
);
|
|
9880
10056
|
}
|
|
9881
|
-
const
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
{
|
|
9889
|
-
meta: {
|
|
9890
|
-
requestMayHaveDispatched: true,
|
|
9891
|
-
responseReceived: true,
|
|
9892
|
-
status: response.status,
|
|
9893
|
-
outcome: "batch-unconfirmed"
|
|
9894
|
-
}
|
|
9895
|
-
}
|
|
9896
|
-
)
|
|
9897
|
-
);
|
|
10057
|
+
const quotaError = this.handleQuotaErrorResponse(
|
|
10058
|
+
response,
|
|
10059
|
+
errorText,
|
|
10060
|
+
"batch"
|
|
10061
|
+
);
|
|
10062
|
+
if (quotaError) {
|
|
10063
|
+
return quotaError;
|
|
9898
10064
|
}
|
|
9899
|
-
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
10065
|
+
return err(
|
|
10066
|
+
serviceError(
|
|
10067
|
+
ErrorCodes.KV_WRITE_FAILED,
|
|
10068
|
+
`Failed to batch put ${items.length} key(s): ${response.status} - ${errorText}`,
|
|
10069
|
+
"kv",
|
|
10070
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
10071
|
+
)
|
|
10072
|
+
);
|
|
10073
|
+
}
|
|
10074
|
+
let rawBody;
|
|
10075
|
+
try {
|
|
10076
|
+
rawBody = await response.json();
|
|
10077
|
+
} catch (jsonError) {
|
|
10078
|
+
const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
|
|
10079
|
+
const timedOut = isRequestTimeoutError(cause);
|
|
10080
|
+
return err(
|
|
10081
|
+
serviceError(
|
|
10082
|
+
timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
|
|
10083
|
+
timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
|
|
10084
|
+
"kv",
|
|
10085
|
+
{
|
|
10086
|
+
cause,
|
|
10087
|
+
meta: {
|
|
10088
|
+
...timedOut ? { timeoutMs: cause.timeoutMs } : {},
|
|
10089
|
+
requestMayHaveDispatched: true,
|
|
10090
|
+
responseReceived: true,
|
|
10091
|
+
status: response.status,
|
|
10092
|
+
outcome: "batch-unconfirmed"
|
|
9915
10093
|
}
|
|
9916
|
-
|
|
9917
|
-
)
|
|
9918
|
-
|
|
9919
|
-
return ok(batchResponse);
|
|
9920
|
-
} catch (error) {
|
|
9921
|
-
const wrapped = wrapError2("kv", error);
|
|
9922
|
-
return err({
|
|
9923
|
-
...wrapped,
|
|
9924
|
-
meta: { ...wrapped.meta, requestMayHaveDispatched }
|
|
9925
|
-
});
|
|
9926
|
-
} finally {
|
|
9927
|
-
request.dispose();
|
|
10094
|
+
}
|
|
10095
|
+
)
|
|
10096
|
+
);
|
|
9928
10097
|
}
|
|
9929
|
-
|
|
10098
|
+
const batchResponse = this.normalizeBatchPutResponse(rawBody);
|
|
10099
|
+
if (!batchResponse) {
|
|
10100
|
+
return err(
|
|
10101
|
+
serviceError(
|
|
10102
|
+
ErrorCodes.NETWORK_ERROR,
|
|
10103
|
+
"KV batchPut response did not include matching written keys and count",
|
|
10104
|
+
"kv",
|
|
10105
|
+
{
|
|
10106
|
+
meta: {
|
|
10107
|
+
requestMayHaveDispatched: true,
|
|
10108
|
+
responseReceived: true,
|
|
10109
|
+
status: response.status,
|
|
10110
|
+
outcome: "batch-unconfirmed"
|
|
10111
|
+
}
|
|
10112
|
+
}
|
|
10113
|
+
)
|
|
10114
|
+
);
|
|
10115
|
+
}
|
|
10116
|
+
const requestedPaths = new Set(paths);
|
|
10117
|
+
const writtenPaths = new Set(batchResponse.written);
|
|
10118
|
+
const matchesRequest = batchResponse.count === paths.length && batchResponse.written.length === paths.length && writtenPaths.size === paths.length && batchResponse.written.every((key) => requestedPaths.has(key));
|
|
10119
|
+
if (!matchesRequest) {
|
|
10120
|
+
return err(
|
|
10121
|
+
serviceError(
|
|
10122
|
+
ErrorCodes.NETWORK_ERROR,
|
|
10123
|
+
`KV batchPut response did not confirm all ${paths.length} requested key(s) were written`,
|
|
10124
|
+
"kv",
|
|
10125
|
+
{
|
|
10126
|
+
meta: {
|
|
10127
|
+
requestMayHaveDispatched: true,
|
|
10128
|
+
responseReceived: true,
|
|
10129
|
+
status: response.status,
|
|
10130
|
+
outcome: "batch-unconfirmed"
|
|
10131
|
+
}
|
|
10132
|
+
}
|
|
10133
|
+
)
|
|
10134
|
+
);
|
|
10135
|
+
}
|
|
10136
|
+
return ok(batchResponse);
|
|
10137
|
+
} catch (error) {
|
|
10138
|
+
const wrapped = wrapError2("kv", error);
|
|
10139
|
+
return err({
|
|
10140
|
+
...wrapped,
|
|
10141
|
+
meta: { ...wrapped.meta, requestMayHaveDispatched }
|
|
10142
|
+
});
|
|
10143
|
+
}
|
|
9930
10144
|
}
|
|
9931
10145
|
/**
|
|
9932
10146
|
* List keys with optional prefix filtering.
|
|
9933
10147
|
*/
|
|
9934
10148
|
async list(options) {
|
|
9935
10149
|
return this.withTelemetry("list", options?.prefix, async () => {
|
|
10150
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
9936
10151
|
if (!this.requireAuth()) {
|
|
9937
10152
|
return err(authRequiredError("kv"));
|
|
9938
10153
|
}
|
|
@@ -9949,49 +10164,44 @@ var init_dist2 = __esm({
|
|
|
9949
10164
|
}
|
|
9950
10165
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
9951
10166
|
try {
|
|
9952
|
-
const
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
|
|
10167
|
+
const network = () => this.listFromNetwork(listPath, options, request.signal);
|
|
10168
|
+
let page;
|
|
10169
|
+
if (options?.source === "network") {
|
|
10170
|
+
const observer = this.readThrough;
|
|
10171
|
+
if (observer === null) {
|
|
10172
|
+
page = await network();
|
|
10173
|
+
} else {
|
|
10174
|
+
let outcome = "error";
|
|
10175
|
+
const startedAt = Date.now();
|
|
10176
|
+
try {
|
|
10177
|
+
page = await network();
|
|
10178
|
+
outcome = page.ok ? "found" : "error";
|
|
10179
|
+
} finally {
|
|
10180
|
+
try {
|
|
10181
|
+
observer.observeNetworkRequested({
|
|
10182
|
+
op: "list",
|
|
10183
|
+
space: this.context.session.spaceId,
|
|
10184
|
+
path: listPath,
|
|
10185
|
+
reason: "NETWORK_REQUESTED",
|
|
10186
|
+
outcome,
|
|
10187
|
+
latencyMs: Date.now() - startedAt
|
|
10188
|
+
});
|
|
10189
|
+
} catch {
|
|
10190
|
+
}
|
|
10191
|
+
}
|
|
9971
10192
|
}
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
let keys = await this.parseResponse(response, options?.raw);
|
|
9983
|
-
keys = keys ?? [];
|
|
9984
|
-
if (options?.removePrefix && listPath) {
|
|
9985
|
-
const prefixWithSlash = listPath.endsWith("/") ? listPath : `${listPath}/`;
|
|
9986
|
-
keys = keys.map(
|
|
9987
|
-
(key) => key.startsWith(prefixWithSlash) ? key.slice(prefixWithSlash.length) : key
|
|
9988
|
-
);
|
|
10193
|
+
} else if (this.readThrough === null) {
|
|
10194
|
+
page = await network();
|
|
10195
|
+
} else {
|
|
10196
|
+
page = await this.readThrough.list({
|
|
10197
|
+
space: this.context.session.spaceId,
|
|
10198
|
+
listPath,
|
|
10199
|
+
options,
|
|
10200
|
+
signal: request.signal,
|
|
10201
|
+
network
|
|
10202
|
+
});
|
|
9989
10203
|
}
|
|
9990
|
-
return ok(
|
|
9991
|
-
keys,
|
|
9992
|
-
...response.headers.get("x-tinycloud-truncated") === null ? {} : { truncated: response.headers.get("x-tinycloud-truncated") === "true" },
|
|
9993
|
-
...response.headers.get("x-tinycloud-next-cursor") === null ? {} : { nextCursor: response.headers.get("x-tinycloud-next-cursor") ?? void 0 }
|
|
9994
|
-
});
|
|
10204
|
+
return page.ok ? this.finishList(page.data, listPath, options?.removePrefix) : page;
|
|
9995
10205
|
} catch (error) {
|
|
9996
10206
|
return err(wrapError2("kv", error));
|
|
9997
10207
|
} finally {
|
|
@@ -9999,6 +10209,56 @@ var init_dist2 = __esm({
|
|
|
9999
10209
|
}
|
|
10000
10210
|
});
|
|
10001
10211
|
}
|
|
10212
|
+
async listFromNetwork(listPath, options, signal) {
|
|
10213
|
+
const response = await this.invokeOperation(
|
|
10214
|
+
listPath,
|
|
10215
|
+
KVAction.LIST,
|
|
10216
|
+
void 0,
|
|
10217
|
+
signal,
|
|
10218
|
+
options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
|
|
10219
|
+
...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
|
|
10220
|
+
...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
|
|
10221
|
+
}
|
|
10222
|
+
);
|
|
10223
|
+
if (!response.ok) {
|
|
10224
|
+
if (response.status === 401 || response.status === 403) {
|
|
10225
|
+
return this.authorizationFailure(
|
|
10226
|
+
"Failed to list keys",
|
|
10227
|
+
response,
|
|
10228
|
+
await this.readAuthorizationText(response),
|
|
10229
|
+
[listPath],
|
|
10230
|
+
KVAction.LIST
|
|
10231
|
+
);
|
|
10232
|
+
}
|
|
10233
|
+
const errorText = await response.text();
|
|
10234
|
+
return err(serviceError(
|
|
10235
|
+
ErrorCodes.NETWORK_ERROR,
|
|
10236
|
+
`Failed to list keys: ${response.status} - ${errorText}`,
|
|
10237
|
+
"kv",
|
|
10238
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
10239
|
+
));
|
|
10240
|
+
}
|
|
10241
|
+
const keys = await this.parseResponse(response, options?.raw) ?? [];
|
|
10242
|
+
return ok({
|
|
10243
|
+
keys,
|
|
10244
|
+
...response.headers.get("x-tinycloud-truncated") === null ? {} : { truncated: response.headers.get("x-tinycloud-truncated") === "true" },
|
|
10245
|
+
...response.headers.get("x-tinycloud-next-cursor") === null ? {} : { nextCursor: response.headers.get("x-tinycloud-next-cursor") ?? void 0 }
|
|
10246
|
+
});
|
|
10247
|
+
}
|
|
10248
|
+
finishList(page, listPath, removePrefix) {
|
|
10249
|
+
let keys = page.keys;
|
|
10250
|
+
if (removePrefix && listPath) {
|
|
10251
|
+
const prefixWithSlash = listPath.endsWith("/") ? listPath : `${listPath}/`;
|
|
10252
|
+
keys = keys.map(
|
|
10253
|
+
(key) => key.startsWith(prefixWithSlash) ? key.slice(prefixWithSlash.length) : key
|
|
10254
|
+
);
|
|
10255
|
+
}
|
|
10256
|
+
return ok({
|
|
10257
|
+
keys,
|
|
10258
|
+
...page.truncated === void 0 ? {} : { truncated: page.truncated },
|
|
10259
|
+
...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor }
|
|
10260
|
+
});
|
|
10261
|
+
}
|
|
10002
10262
|
/**
|
|
10003
10263
|
* Read one page of the `tinycloud.kv/sync` change feed for a prefix.
|
|
10004
10264
|
*/
|
|
@@ -10167,61 +10427,22 @@ var init_dist2 = __esm({
|
|
|
10167
10427
|
*/
|
|
10168
10428
|
async delete(key, options) {
|
|
10169
10429
|
return this.withTelemetry("delete", key, async () => {
|
|
10430
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
10170
10431
|
if (!this.requireAuth()) {
|
|
10171
10432
|
return err(authRequiredError("kv"));
|
|
10172
10433
|
}
|
|
10173
10434
|
const path = this.getFullPath(key, options?.prefix);
|
|
10174
10435
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
10175
10436
|
try {
|
|
10176
|
-
const
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
return this.authorizationFailure(
|
|
10186
|
-
`Failed to delete key ${JSON.stringify(key)}`,
|
|
10187
|
-
response,
|
|
10188
|
-
await this.readAuthorizationText(response),
|
|
10189
|
-
[path],
|
|
10190
|
-
KVAction.DELETE
|
|
10191
|
-
);
|
|
10192
|
-
}
|
|
10193
|
-
if (response.status === 404) {
|
|
10194
|
-
return await this.classifyNotFound(response, key);
|
|
10195
|
-
}
|
|
10196
|
-
const errorText = await response.text();
|
|
10197
|
-
if (response.status === 412) {
|
|
10198
|
-
return err(serviceError(
|
|
10199
|
-
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
10200
|
-
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
10201
|
-
"kv",
|
|
10202
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
10203
|
-
));
|
|
10204
|
-
}
|
|
10205
|
-
if (response.status === 503 && options?.ifMatch !== void 0) {
|
|
10206
|
-
return err(serviceError(
|
|
10207
|
-
ErrorCodes.KV_CONFLICT,
|
|
10208
|
-
`Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
|
|
10209
|
-
"kv",
|
|
10210
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
10211
|
-
));
|
|
10212
|
-
}
|
|
10213
|
-
return err(
|
|
10214
|
-
serviceError(
|
|
10215
|
-
ErrorCodes.NETWORK_ERROR,
|
|
10216
|
-
`Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
10217
|
-
"kv",
|
|
10218
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
10219
|
-
)
|
|
10220
|
-
);
|
|
10221
|
-
}
|
|
10222
|
-
return ok({
|
|
10223
|
-
data: void 0,
|
|
10224
|
-
headers: this.createResponseHeaders(response.headers)
|
|
10437
|
+
const network = () => this.deleteFromNetwork(key, path, options, request.signal);
|
|
10438
|
+
if (this.readThrough === null) return await network();
|
|
10439
|
+
return await this.readThrough.write({
|
|
10440
|
+
op: "delete",
|
|
10441
|
+
space: this.context.session.spaceId,
|
|
10442
|
+
entries: [{ path }],
|
|
10443
|
+
signal: request.signal,
|
|
10444
|
+
conditional: options?.ifMatch !== void 0,
|
|
10445
|
+
network
|
|
10225
10446
|
});
|
|
10226
10447
|
} catch (error) {
|
|
10227
10448
|
return err(wrapError2("kv", error));
|
|
@@ -10230,6 +10451,54 @@ var init_dist2 = __esm({
|
|
|
10230
10451
|
}
|
|
10231
10452
|
});
|
|
10232
10453
|
}
|
|
10454
|
+
async deleteFromNetwork(key, path, options, signal) {
|
|
10455
|
+
const response = await this.invokeOperation(
|
|
10456
|
+
path,
|
|
10457
|
+
KVAction.DELETE,
|
|
10458
|
+
void 0,
|
|
10459
|
+
signal,
|
|
10460
|
+
options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
|
|
10461
|
+
);
|
|
10462
|
+
if (!response.ok) {
|
|
10463
|
+
if (response.status === 401 || response.status === 403) {
|
|
10464
|
+
return this.authorizationFailure(
|
|
10465
|
+
`Failed to delete key ${JSON.stringify(key)}`,
|
|
10466
|
+
response,
|
|
10467
|
+
await this.readAuthorizationText(response),
|
|
10468
|
+
[path],
|
|
10469
|
+
KVAction.DELETE
|
|
10470
|
+
);
|
|
10471
|
+
}
|
|
10472
|
+
if (response.status === 404) return this.classifyNotFound(response, key);
|
|
10473
|
+
const errorText = await response.text();
|
|
10474
|
+
if (response.status === 412) {
|
|
10475
|
+
return err(serviceError(
|
|
10476
|
+
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
10477
|
+
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
10478
|
+
"kv",
|
|
10479
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
10480
|
+
));
|
|
10481
|
+
}
|
|
10482
|
+
if (response.status === 503 && options?.ifMatch !== void 0) {
|
|
10483
|
+
return err(serviceError(
|
|
10484
|
+
ErrorCodes.KV_CONFLICT,
|
|
10485
|
+
`Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
|
|
10486
|
+
"kv",
|
|
10487
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
10488
|
+
));
|
|
10489
|
+
}
|
|
10490
|
+
return err(serviceError(
|
|
10491
|
+
ErrorCodes.NETWORK_ERROR,
|
|
10492
|
+
`Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
10493
|
+
"kv",
|
|
10494
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
10495
|
+
));
|
|
10496
|
+
}
|
|
10497
|
+
return ok({
|
|
10498
|
+
data: void 0,
|
|
10499
|
+
headers: this.createResponseHeaders(response.headers)
|
|
10500
|
+
});
|
|
10501
|
+
}
|
|
10233
10502
|
/**
|
|
10234
10503
|
* Get metadata for a key without retrieving the value.
|
|
10235
10504
|
*/
|
|
@@ -10525,7 +10794,7 @@ var init_dist2 = __esm({
|
|
|
10525
10794
|
request.signal
|
|
10526
10795
|
);
|
|
10527
10796
|
if (!response.ok) {
|
|
10528
|
-
return this.handleErrorResponse(response, "query", dbName, [action]);
|
|
10797
|
+
return await this.handleErrorResponse(response, "query", dbName, [action]);
|
|
10529
10798
|
}
|
|
10530
10799
|
const data = await response.json();
|
|
10531
10800
|
return ok(data);
|
|
@@ -10565,7 +10834,7 @@ var init_dist2 = __esm({
|
|
|
10565
10834
|
request.signal
|
|
10566
10835
|
);
|
|
10567
10836
|
if (!response.ok) {
|
|
10568
|
-
return this.handleErrorResponse(response, "execute", dbName, requestedActions2);
|
|
10837
|
+
return await this.handleErrorResponse(response, "execute", dbName, requestedActions2);
|
|
10569
10838
|
}
|
|
10570
10839
|
const data = await response.json();
|
|
10571
10840
|
return ok(data);
|
|
@@ -10591,7 +10860,7 @@ var init_dist2 = __esm({
|
|
|
10591
10860
|
request.signal
|
|
10592
10861
|
);
|
|
10593
10862
|
if (!response.ok) {
|
|
10594
|
-
return this.handleErrorResponse(response, "batch", dbName, requestedActions2);
|
|
10863
|
+
return await this.handleErrorResponse(response, "batch", dbName, requestedActions2);
|
|
10595
10864
|
}
|
|
10596
10865
|
const data = await response.json();
|
|
10597
10866
|
return ok(data);
|
|
@@ -10616,7 +10885,7 @@ var init_dist2 = __esm({
|
|
|
10616
10885
|
request.signal
|
|
10617
10886
|
);
|
|
10618
10887
|
if (!response.ok) {
|
|
10619
|
-
return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
|
|
10888
|
+
return await this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
|
|
10620
10889
|
}
|
|
10621
10890
|
const data = await response.json();
|
|
10622
10891
|
return ok(data);
|
|
@@ -10641,7 +10910,7 @@ var init_dist2 = __esm({
|
|
|
10641
10910
|
request.signal
|
|
10642
10911
|
);
|
|
10643
10912
|
if (!response.ok) {
|
|
10644
|
-
return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
|
|
10913
|
+
return await this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
|
|
10645
10914
|
}
|
|
10646
10915
|
const resp = response;
|
|
10647
10916
|
if (typeof resp.blob === "function") {
|
|
@@ -10928,7 +11197,7 @@ var init_dist2 = __esm({
|
|
|
10928
11197
|
request.signal
|
|
10929
11198
|
);
|
|
10930
11199
|
if (!response.ok) {
|
|
10931
|
-
return this.handleErrorResponse(response, "query");
|
|
11200
|
+
return await this.handleErrorResponse(response, "query");
|
|
10932
11201
|
}
|
|
10933
11202
|
const data = await response.json();
|
|
10934
11203
|
return ok(data);
|
|
@@ -10954,7 +11223,7 @@ var init_dist2 = __esm({
|
|
|
10954
11223
|
{ Accept: "application/vnd.apache.arrow.stream" }
|
|
10955
11224
|
);
|
|
10956
11225
|
if (!response.ok) {
|
|
10957
|
-
return this.handleErrorResponse(response, "queryArrow");
|
|
11226
|
+
return await this.handleErrorResponse(response, "queryArrow");
|
|
10958
11227
|
}
|
|
10959
11228
|
const buffer = await response.arrayBuffer();
|
|
10960
11229
|
return ok(buffer);
|
|
@@ -10987,7 +11256,7 @@ var init_dist2 = __esm({
|
|
|
10987
11256
|
request.signal
|
|
10988
11257
|
);
|
|
10989
11258
|
if (!response.ok) {
|
|
10990
|
-
return this.handleErrorResponse(response, "execute");
|
|
11259
|
+
return await this.handleErrorResponse(response, "execute");
|
|
10991
11260
|
}
|
|
10992
11261
|
const data = await response.json();
|
|
10993
11262
|
return ok(data);
|
|
@@ -11019,7 +11288,7 @@ var init_dist2 = __esm({
|
|
|
11019
11288
|
request.signal
|
|
11020
11289
|
);
|
|
11021
11290
|
if (!response.ok) {
|
|
11022
|
-
return this.handleErrorResponse(response, "batch");
|
|
11291
|
+
return await this.handleErrorResponse(response, "batch");
|
|
11023
11292
|
}
|
|
11024
11293
|
const data = await response.json();
|
|
11025
11294
|
return ok(data);
|
|
@@ -11044,7 +11313,7 @@ var init_dist2 = __esm({
|
|
|
11044
11313
|
request.signal
|
|
11045
11314
|
);
|
|
11046
11315
|
if (!response.ok) {
|
|
11047
|
-
return this.handleErrorResponse(response, "executeStatement");
|
|
11316
|
+
return await this.handleErrorResponse(response, "executeStatement");
|
|
11048
11317
|
}
|
|
11049
11318
|
const data = await response.json();
|
|
11050
11319
|
return ok(data);
|
|
@@ -11069,7 +11338,7 @@ var init_dist2 = __esm({
|
|
|
11069
11338
|
request.signal
|
|
11070
11339
|
);
|
|
11071
11340
|
if (!response.ok) {
|
|
11072
|
-
return this.handleErrorResponse(response, "describe");
|
|
11341
|
+
return await this.handleErrorResponse(response, "describe");
|
|
11073
11342
|
}
|
|
11074
11343
|
const data = await response.json();
|
|
11075
11344
|
return ok(data);
|
|
@@ -11094,7 +11363,7 @@ var init_dist2 = __esm({
|
|
|
11094
11363
|
request.signal
|
|
11095
11364
|
);
|
|
11096
11365
|
if (!response.ok) {
|
|
11097
|
-
return this.handleErrorResponse(response, "export");
|
|
11366
|
+
return await this.handleErrorResponse(response, "export");
|
|
11098
11367
|
}
|
|
11099
11368
|
const blob = await response.blob();
|
|
11100
11369
|
return ok(blob);
|
|
@@ -11129,7 +11398,7 @@ var init_dist2 = __esm({
|
|
|
11129
11398
|
signal: request.signal
|
|
11130
11399
|
});
|
|
11131
11400
|
if (!response.ok) {
|
|
11132
|
-
return this.handleErrorResponse(response, "import");
|
|
11401
|
+
return await this.handleErrorResponse(response, "import");
|
|
11133
11402
|
}
|
|
11134
11403
|
return ok(void 0);
|
|
11135
11404
|
} catch (error) {
|
|
@@ -12139,7 +12408,8 @@ var init_dist2 = __esm({
|
|
|
12139
12408
|
}
|
|
12140
12409
|
try {
|
|
12141
12410
|
const valueResult = await this.tc.kv.get(`vault/${key}`, {
|
|
12142
|
-
raw: true
|
|
12411
|
+
raw: true,
|
|
12412
|
+
...options?.source === void 0 ? {} : { source: options.source }
|
|
12143
12413
|
});
|
|
12144
12414
|
if (!valueResult.ok) {
|
|
12145
12415
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
@@ -12184,7 +12454,7 @@ var init_dist2 = __esm({
|
|
|
12184
12454
|
}
|
|
12185
12455
|
let valueResult;
|
|
12186
12456
|
try {
|
|
12187
|
-
valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true });
|
|
12457
|
+
valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true, source: "network" });
|
|
12188
12458
|
} catch {
|
|
12189
12459
|
return { status: "node_unreachable" };
|
|
12190
12460
|
}
|
|
@@ -12253,7 +12523,8 @@ var init_dist2 = __esm({
|
|
|
12253
12523
|
}
|
|
12254
12524
|
try {
|
|
12255
12525
|
const valueResult = await this.tc.kv.get(`vault/${key}`, {
|
|
12256
|
-
raw: true
|
|
12526
|
+
raw: true,
|
|
12527
|
+
source: "network"
|
|
12257
12528
|
});
|
|
12258
12529
|
if (!valueResult.ok) {
|
|
12259
12530
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
@@ -12387,7 +12658,8 @@ var init_dist2 = __esm({
|
|
|
12387
12658
|
}
|
|
12388
12659
|
try {
|
|
12389
12660
|
const keyResult = await this.tc.kv.get(`keys/${key}`, {
|
|
12390
|
-
raw: true
|
|
12661
|
+
raw: true,
|
|
12662
|
+
...options?.source === void 0 ? {} : { source: options.source }
|
|
12391
12663
|
});
|
|
12392
12664
|
if (!keyResult.ok) {
|
|
12393
12665
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
@@ -12396,7 +12668,8 @@ var init_dist2 = __esm({
|
|
|
12396
12668
|
const keyBlobBytes = base64Decode(keyEnvelope.key);
|
|
12397
12669
|
const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
|
|
12398
12670
|
const valueResult = await this.tc.kv.get(`vault/${key}`, {
|
|
12399
|
-
raw: true
|
|
12671
|
+
raw: true,
|
|
12672
|
+
...options?.source === void 0 ? {} : { source: options.source }
|
|
12400
12673
|
});
|
|
12401
12674
|
if (!valueResult.ok) {
|
|
12402
12675
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
@@ -12506,6 +12779,7 @@ var init_dist2 = __esm({
|
|
|
12506
12779
|
prefix: listPrefix,
|
|
12507
12780
|
removePrefix: true,
|
|
12508
12781
|
...options?.limit === void 0 ? {} : { limit: options.limit },
|
|
12782
|
+
...options?.source === void 0 ? {} : { source: options.source },
|
|
12509
12783
|
...options?.cursor === void 0 ? {} : { cursor: options.cursor }
|
|
12510
12784
|
});
|
|
12511
12785
|
if (!listResult.ok) {
|
|
@@ -14264,7 +14538,8 @@ async function loadManifestPermissions(source, profile, options = {}) {
|
|
|
14264
14538
|
service,
|
|
14265
14539
|
space: String(manifest.space ?? "applications"),
|
|
14266
14540
|
path: resolvedPath,
|
|
14267
|
-
actions
|
|
14541
|
+
actions,
|
|
14542
|
+
...Array.isArray(entry.caveats) ? { caveats: entry.caveats } : {}
|
|
14268
14543
|
};
|
|
14269
14544
|
});
|
|
14270
14545
|
permissions.push(...await secretPermissionsFromAppManifest(manifest, profile, options.ownerDid));
|
|
@@ -14511,13 +14786,127 @@ var init_delegation_expiry = __esm({
|
|
|
14511
14786
|
}
|
|
14512
14787
|
});
|
|
14513
14788
|
|
|
14789
|
+
// src/lib/replication-log.ts
|
|
14790
|
+
import { appendFileSync, chmodSync, mkdirSync, renameSync, statSync, writeFileSync } from "fs";
|
|
14791
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
14792
|
+
function appendReplicationEvent(root, event) {
|
|
14793
|
+
mkdirSync(root, { recursive: true, mode: DIR_MODE });
|
|
14794
|
+
chmodSync(root, DIR_MODE);
|
|
14795
|
+
const path = join4(root, "events.jsonl");
|
|
14796
|
+
let size = 0;
|
|
14797
|
+
try {
|
|
14798
|
+
size = statSync(path).size;
|
|
14799
|
+
} catch (error) {
|
|
14800
|
+
if (error.code !== "ENOENT") throw error;
|
|
14801
|
+
}
|
|
14802
|
+
const line = `${JSON.stringify(event)}
|
|
14803
|
+
`;
|
|
14804
|
+
if (size + Buffer.byteLength(line) > MAX_LOG_BYTES) {
|
|
14805
|
+
const rotated = join4(root, "events.jsonl.1");
|
|
14806
|
+
try {
|
|
14807
|
+
renameSync(path, rotated);
|
|
14808
|
+
} catch (error) {
|
|
14809
|
+
if (error.code !== "ENOENT") throw error;
|
|
14810
|
+
}
|
|
14811
|
+
writeFileSync(path, line, { mode: LOG_MODE });
|
|
14812
|
+
} else {
|
|
14813
|
+
appendFileSync(path, line, { mode: LOG_MODE });
|
|
14814
|
+
}
|
|
14815
|
+
chmodSync(path, LOG_MODE);
|
|
14816
|
+
}
|
|
14817
|
+
function replicationLogPath(profileRoot) {
|
|
14818
|
+
return join4(profileRoot, "replication", "events.jsonl");
|
|
14819
|
+
}
|
|
14820
|
+
function createReplicationEventSink(profileRoot, options) {
|
|
14821
|
+
const pinnedNotices = /* @__PURE__ */ new Set();
|
|
14822
|
+
const missingGrantWarnings = /* @__PURE__ */ new Set();
|
|
14823
|
+
return (event) => {
|
|
14824
|
+
appendProfileReplicationEvent(profileRoot, event);
|
|
14825
|
+
if (event.type === "replication.state" && event.state === "grant_missing") {
|
|
14826
|
+
const prefix = event.replica ?? event.space ?? "configured prefix";
|
|
14827
|
+
const key = `${event.space ?? ""}\0${event.replica ?? ""}`;
|
|
14828
|
+
if (!missingGrantWarnings.has(key)) {
|
|
14829
|
+
missingGrantWarnings.add(key);
|
|
14830
|
+
process.stderr.write(`[replication] Warning: no usable kv/get+sync grant for ${prefix}; reads use the network (${event.code ?? "grant_missing"}).
|
|
14831
|
+
`);
|
|
14832
|
+
}
|
|
14833
|
+
}
|
|
14834
|
+
if (options.debug) {
|
|
14835
|
+
if (event.type === "replication.read") {
|
|
14836
|
+
const detail = event.source === "replica" ? "replica hit" : `${event.source} ${event.reason}`;
|
|
14837
|
+
const syncStatus = event.syncedBeforeRead === void 0 ? "" : ` syncedBeforeRead:${event.syncedBeforeRead}`;
|
|
14838
|
+
const syncError = event.syncError === void 0 ? "" : ` syncError:${event.syncError}`;
|
|
14839
|
+
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)`}
|
|
14840
|
+
`);
|
|
14841
|
+
} else if (event.type === "replication.write") {
|
|
14842
|
+
process.stderr.write(`[replication] ${event.op} ${event.keys.join(",")} ${event.outcome}${event.code ? ` ${event.code}` : ""}
|
|
14843
|
+
`);
|
|
14844
|
+
} else if (event.type === "replication.sync") {
|
|
14845
|
+
const pending = event.pendingCleared === void 0 ? "" : ` pendingCleared:${event.pendingCleared}`;
|
|
14846
|
+
process.stderr.write(`[replication] sync ${event.replica} ${event.outcome}${event.code ? ` ${event.code}` : ""}${pending}
|
|
14847
|
+
`);
|
|
14848
|
+
}
|
|
14849
|
+
}
|
|
14850
|
+
if (event.type === "replication.read" && (event.pendingState === "in_flight" || event.pendingState === "ambiguous") && !options.quiet && !pinnedNotices.has(event.key)) {
|
|
14851
|
+
pinnedNotices.add(event.key);
|
|
14852
|
+
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
|
|
14853
|
+
`);
|
|
14854
|
+
}
|
|
14855
|
+
};
|
|
14856
|
+
}
|
|
14857
|
+
function appendProfileReplicationEvent(profileRoot, event) {
|
|
14858
|
+
appendReplicationEvent(dirname2(replicationLogPath(profileRoot)), event);
|
|
14859
|
+
}
|
|
14860
|
+
var MAX_LOG_BYTES, LOG_MODE, DIR_MODE;
|
|
14861
|
+
var init_replication_log = __esm({
|
|
14862
|
+
"src/lib/replication-log.ts"() {
|
|
14863
|
+
"use strict";
|
|
14864
|
+
MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
14865
|
+
LOG_MODE = 384;
|
|
14866
|
+
DIR_MODE = 448;
|
|
14867
|
+
}
|
|
14868
|
+
});
|
|
14869
|
+
|
|
14870
|
+
// src/lib/replication-config.ts
|
|
14871
|
+
import { join as join5 } from "path";
|
|
14872
|
+
import { profilePath, withProfileLock as withProfileLock2 } from "@tinycloud/operations/state";
|
|
14873
|
+
import { sqliteReplicaStorage } from "@tinycloud/node-sdk";
|
|
14874
|
+
function createCliReplicationConfig(profile, settings, output2, enabled) {
|
|
14875
|
+
if (!enabled || !settings || settings.prefixes.length === 0) return void 0;
|
|
14876
|
+
const root = profilePath(profile);
|
|
14877
|
+
return {
|
|
14878
|
+
enabled: true,
|
|
14879
|
+
prefixes: settings.prefixes,
|
|
14880
|
+
allowSecrets: settings.allowSecrets === true,
|
|
14881
|
+
mode: "foreground",
|
|
14882
|
+
verify: process.env.TC_REPLICATION_VERIFY === "1",
|
|
14883
|
+
maxStalenessMs: Number(process.env.TC_REPLICATION_MAX_STALENESS_MS ?? 6e4),
|
|
14884
|
+
staleSyncTimeoutMs: Number(process.env.TC_REPLICATION_SYNC_TIMEOUT_MS ?? 1e4),
|
|
14885
|
+
storage: sqliteReplicaStorage({
|
|
14886
|
+
dir: join5(root, "replication"),
|
|
14887
|
+
guard: (section) => withProfileLock2(profile, section, { requireProfile: true, timeoutMs: 35e3 })
|
|
14888
|
+
}),
|
|
14889
|
+
onEvent: createReplicationEventSink(root, output2)
|
|
14890
|
+
};
|
|
14891
|
+
}
|
|
14892
|
+
var init_replication_config = __esm({
|
|
14893
|
+
"src/lib/replication-config.ts"() {
|
|
14894
|
+
"use strict";
|
|
14895
|
+
init_replication_log();
|
|
14896
|
+
}
|
|
14897
|
+
});
|
|
14898
|
+
|
|
14514
14899
|
// src/lib/sdk.ts
|
|
14515
14900
|
var sdk_exports = {};
|
|
14516
14901
|
__export(sdk_exports, {
|
|
14517
14902
|
bootstrapDelegatedSession: () => bootstrapDelegatedSession,
|
|
14903
|
+
closeReplication: () => closeReplication,
|
|
14518
14904
|
createSDKInstance: () => createSDKInstance,
|
|
14519
14905
|
ensureAuthenticated: () => ensureAuthenticated,
|
|
14520
14906
|
jwkHasPrivateParameter: () => jwkHasPrivateParameter,
|
|
14907
|
+
registerReplication: () => registerReplication,
|
|
14908
|
+
registeredReplications: () => registeredReplications,
|
|
14909
|
+
replicationForProfile: () => replicationForProfile,
|
|
14521
14910
|
restoreProfileSessionNode: () => restoreProfileSessionNode,
|
|
14522
14911
|
selectSignerJwk: () => selectSignerJwk,
|
|
14523
14912
|
storedSessionVerifiesLocally: () => storedSessionVerifiesLocally
|
|
@@ -14632,10 +15021,21 @@ async function createSDKInstance(ctx, options) {
|
|
|
14632
15021
|
);
|
|
14633
15022
|
}
|
|
14634
15023
|
if (profile?.authMethod === "local" && effectivePrivateKey) {
|
|
15024
|
+
const replication2 = createCliReplicationConfig(
|
|
15025
|
+
ctx.profile,
|
|
15026
|
+
profile?.replication,
|
|
15027
|
+
{ debug: ctx.replicationDebug, quiet: ctx.quiet },
|
|
15028
|
+
ctx.replication
|
|
15029
|
+
);
|
|
15030
|
+
if (ctx.replication && !replication2) {
|
|
15031
|
+
process.stderr.write("[replication] Enabled, but this profile has no configured prefixes; reads use the network.\n");
|
|
15032
|
+
}
|
|
14635
15033
|
const node2 = new TinyCloudNode({
|
|
14636
15034
|
host: ctx.host,
|
|
14637
|
-
privateKey: effectivePrivateKey
|
|
15035
|
+
privateKey: effectivePrivateKey,
|
|
15036
|
+
...replication2 ? { replication: replication2 } : {}
|
|
14638
15037
|
});
|
|
15038
|
+
if (node2.replication) registerReplication(ctx.profile, node2.replication);
|
|
14639
15039
|
const restoredOwnSession = Boolean(session && session.delegationHeader && session.delegationCid && session.spaceId);
|
|
14640
15040
|
if (restoredOwnSession) {
|
|
14641
15041
|
await restoreProfileSession(node2, ctx.profile, profile, persistedSessionData(ctx.profile, profile, session, key));
|
|
@@ -14651,10 +15051,21 @@ async function createSDKInstance(ctx, options) {
|
|
|
14651
15051
|
}
|
|
14652
15052
|
return node2;
|
|
14653
15053
|
}
|
|
15054
|
+
const replication = createCliReplicationConfig(
|
|
15055
|
+
ctx.profile,
|
|
15056
|
+
profile?.replication,
|
|
15057
|
+
{ debug: ctx.replicationDebug, quiet: ctx.quiet },
|
|
15058
|
+
ctx.replication
|
|
15059
|
+
);
|
|
15060
|
+
if (ctx.replication && !replication) {
|
|
15061
|
+
process.stderr.write("[replication] Enabled, but this profile has no configured prefixes; reads use the network.\n");
|
|
15062
|
+
}
|
|
14654
15063
|
const node = new TinyCloudNode({
|
|
14655
15064
|
host: ctx.host,
|
|
14656
|
-
privateKey: options?.privateKey
|
|
15065
|
+
privateKey: options?.privateKey,
|
|
15066
|
+
...replication ? { replication } : {}
|
|
14657
15067
|
});
|
|
15068
|
+
if (node.replication) registerReplication(ctx.profile, node.replication);
|
|
14658
15069
|
if (options?.privateKey) {
|
|
14659
15070
|
await node.signIn();
|
|
14660
15071
|
} else if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
|
|
@@ -14796,6 +15207,9 @@ var init_sdk = __esm({
|
|
|
14796
15207
|
init_session_expired();
|
|
14797
15208
|
init_warnings();
|
|
14798
15209
|
init_delegation_expiry();
|
|
15210
|
+
init_replication_config();
|
|
15211
|
+
init_replication_registry();
|
|
15212
|
+
init_replication_registry();
|
|
14799
15213
|
BOOTSTRAP_PROFILE_FIELDS = ["sessionDid", "spaceId"];
|
|
14800
15214
|
}
|
|
14801
15215
|
});
|
|
@@ -14936,14 +15350,14 @@ var init_host = __esm({
|
|
|
14936
15350
|
|
|
14937
15351
|
// src/config/profiles.ts
|
|
14938
15352
|
import { chmod as chmod2, lstat, readdir as readdir2, rm as rm2, rmdir } from "fs/promises";
|
|
14939
|
-
import { join as
|
|
15353
|
+
import { join as join6 } from "path";
|
|
14940
15354
|
import {
|
|
14941
|
-
profilePath,
|
|
15355
|
+
profilePath as profilePath2,
|
|
14942
15356
|
readSession,
|
|
14943
15357
|
recordProfileDeletion,
|
|
14944
15358
|
refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
|
|
14945
15359
|
removeSession,
|
|
14946
|
-
withProfileLock as
|
|
15360
|
+
withProfileLock as withProfileLock3,
|
|
14947
15361
|
writeSession
|
|
14948
15362
|
} from "@tinycloud/operations/state";
|
|
14949
15363
|
var ProfileManager;
|
|
@@ -14961,7 +15375,7 @@ var init_profiles = __esm({
|
|
|
14961
15375
|
* it around a whole read-modify-write, not just each write.
|
|
14962
15376
|
*/
|
|
14963
15377
|
static async withLock(name, action, options) {
|
|
14964
|
-
return
|
|
15378
|
+
return withProfileLock3(name, action, options);
|
|
14965
15379
|
}
|
|
14966
15380
|
/**
|
|
14967
15381
|
* Read-modify-write of profile.json under the profile lock, so concurrent
|
|
@@ -14987,7 +15401,7 @@ var init_profiles = __esm({
|
|
|
14987
15401
|
/** Owner-only profile directory (0700), created or tightened before any write into it. */
|
|
14988
15402
|
static async ensureProfileDir(name) {
|
|
14989
15403
|
await _ProfileManager.ensureConfigDir();
|
|
14990
|
-
const profileDir =
|
|
15404
|
+
const profileDir = join6(PROFILES_DIR, name);
|
|
14991
15405
|
await ensureDir(profileDir);
|
|
14992
15406
|
await chmod2(profileDir, PRIVATE_DIR_MODE);
|
|
14993
15407
|
return profileDir;
|
|
@@ -15016,8 +15430,8 @@ var init_profiles = __esm({
|
|
|
15016
15430
|
* Throws CLIError if the profile doesn't exist.
|
|
15017
15431
|
*/
|
|
15018
15432
|
static async getProfile(name) {
|
|
15019
|
-
const
|
|
15020
|
-
const profile = await readJson(
|
|
15433
|
+
const profilePath5 = join6(PROFILES_DIR, name, "profile.json");
|
|
15434
|
+
const profile = await readJson(profilePath5);
|
|
15021
15435
|
if (!profile) {
|
|
15022
15436
|
throw new CLIError(
|
|
15023
15437
|
"PROFILE_NOT_FOUND",
|
|
@@ -15032,18 +15446,18 @@ var init_profiles = __esm({
|
|
|
15032
15446
|
*/
|
|
15033
15447
|
static async setProfile(name, data) {
|
|
15034
15448
|
await _ProfileManager.withLock(name, async () => {
|
|
15035
|
-
await writeJson(
|
|
15449
|
+
await writeJson(join6(await _ProfileManager.ensureProfileDir(name), "profile.json"), data);
|
|
15036
15450
|
});
|
|
15037
15451
|
}
|
|
15038
15452
|
/** Removes profile.json under the profile lock (rollback of a profile a failed login created). */
|
|
15039
15453
|
static async removeProfileConfig(name) {
|
|
15040
|
-
await _ProfileManager.withLock(name, () => rm2(
|
|
15454
|
+
await _ProfileManager.withLock(name, () => rm2(join6(PROFILES_DIR, name, "profile.json"), { force: true }));
|
|
15041
15455
|
}
|
|
15042
15456
|
/**
|
|
15043
15457
|
* Returns true if a profile directory exists.
|
|
15044
15458
|
*/
|
|
15045
15459
|
static async profileExists(name) {
|
|
15046
|
-
return fileExists(
|
|
15460
|
+
return fileExists(join6(PROFILES_DIR, name, "profile.json"));
|
|
15047
15461
|
}
|
|
15048
15462
|
/**
|
|
15049
15463
|
* Returns an array of profile directory names.
|
|
@@ -15067,7 +15481,7 @@ var init_profiles = __esm({
|
|
|
15067
15481
|
static async deleteProfile(name) {
|
|
15068
15482
|
let profileDir;
|
|
15069
15483
|
try {
|
|
15070
|
-
profileDir =
|
|
15484
|
+
profileDir = profilePath2(name);
|
|
15071
15485
|
} catch {
|
|
15072
15486
|
throw new CLIError(
|
|
15073
15487
|
"INVALID_PROFILE_NAME",
|
|
@@ -15092,11 +15506,11 @@ var init_profiles = __esm({
|
|
|
15092
15506
|
return;
|
|
15093
15507
|
}
|
|
15094
15508
|
await _ProfileManager.withLock(name, async () => {
|
|
15095
|
-
for (const file of ["session.json", "key.json"]) await rm2(
|
|
15509
|
+
for (const file of ["session.json", "key.json"]) await rm2(join6(profileDir, file), { force: true });
|
|
15096
15510
|
for (const entry of await readdir2(profileDir)) {
|
|
15097
|
-
if (entry !== ".lock" && entry !== "profile.json") await rm2(
|
|
15511
|
+
if (entry !== ".lock" && entry !== "profile.json") await rm2(join6(profileDir, entry), { recursive: true, force: true });
|
|
15098
15512
|
}
|
|
15099
|
-
await rm2(
|
|
15513
|
+
await rm2(join6(profileDir, "profile.json"), { force: true });
|
|
15100
15514
|
await recordProfileDeletion(name);
|
|
15101
15515
|
});
|
|
15102
15516
|
await rmdir(profileDir).catch((error) => {
|
|
@@ -15108,7 +15522,7 @@ var init_profiles = __esm({
|
|
|
15108
15522
|
* Returns the parsed JWK for a profile, or null if no key exists.
|
|
15109
15523
|
*/
|
|
15110
15524
|
static async getKey(name) {
|
|
15111
|
-
return readJson(
|
|
15525
|
+
return readJson(join6(PROFILES_DIR, name, "key.json"));
|
|
15112
15526
|
}
|
|
15113
15527
|
/**
|
|
15114
15528
|
* Saves a JWK key under the profile lock (0600, in an owner-only profile
|
|
@@ -15118,12 +15532,12 @@ var init_profiles = __esm({
|
|
|
15118
15532
|
static async setKey(name, jwk) {
|
|
15119
15533
|
await _ProfileManager.withLock(name, async () => {
|
|
15120
15534
|
await refuseWriteToDeletedProfile2(name);
|
|
15121
|
-
await writeJson(
|
|
15535
|
+
await writeJson(join6(await _ProfileManager.ensureProfileDir(name), "key.json"), jwk);
|
|
15122
15536
|
});
|
|
15123
15537
|
}
|
|
15124
15538
|
/** Removes key.json under the profile lock (rollback of a key a failed login created). */
|
|
15125
15539
|
static async removeKey(name) {
|
|
15126
|
-
await _ProfileManager.withLock(name, () => rm2(
|
|
15540
|
+
await _ProfileManager.withLock(name, () => rm2(join6(PROFILES_DIR, name, "key.json"), { force: true }));
|
|
15127
15541
|
}
|
|
15128
15542
|
// ── Session management ──────────────────────────────────────────────
|
|
15129
15543
|
/**
|
|
@@ -15150,7 +15564,7 @@ var init_profiles = __esm({
|
|
|
15150
15564
|
* created or tightened to 0700.
|
|
15151
15565
|
*/
|
|
15152
15566
|
static async getCacheDir(name) {
|
|
15153
|
-
const cacheDir =
|
|
15567
|
+
const cacheDir = join6(await _ProfileManager.ensureProfileDir(name), "cache");
|
|
15154
15568
|
await ensureDir(cacheDir);
|
|
15155
15569
|
await chmod2(cacheDir, PRIVATE_DIR_MODE);
|
|
15156
15570
|
return cacheDir;
|
|
@@ -15173,11 +15587,20 @@ var init_profiles = __esm({
|
|
|
15173
15587
|
const config = await _ProfileManager.getConfig();
|
|
15174
15588
|
const profile = options.profile ?? process.env.TC_PROFILE ?? config.defaultProfile ?? DEFAULT_PROFILE;
|
|
15175
15589
|
let profileHost;
|
|
15590
|
+
let profileConfig;
|
|
15176
15591
|
try {
|
|
15177
|
-
|
|
15592
|
+
profileConfig = await _ProfileManager.getProfile(profile);
|
|
15178
15593
|
profileHost = profileConfig.host;
|
|
15179
15594
|
} catch {
|
|
15180
15595
|
}
|
|
15596
|
+
const envReplication = process.env.TC_REPLICATION?.toLowerCase();
|
|
15597
|
+
const envEnabled = envReplication === "1" || envReplication === "true" || envReplication === "on";
|
|
15598
|
+
const explicitEnvOff = envReplication === "0" || envReplication === "false" || envReplication === "off";
|
|
15599
|
+
if (envReplication !== void 0 && !envEnabled && !explicitEnvOff) {
|
|
15600
|
+
throw new CLIError("INVALID_ARGUMENT", "TC_REPLICATION must be one of 1, true, on, 0, false or off.", ExitCode.USAGE_ERROR);
|
|
15601
|
+
}
|
|
15602
|
+
const explicitOff = options.noReplication === true || options.replication === false;
|
|
15603
|
+
const replication = options.replication === true ? true : explicitOff ? false : envEnabled;
|
|
15181
15604
|
const explicitHost = options.host ?? process.env.TC_HOST;
|
|
15182
15605
|
let host = explicitHost ?? profileHost ?? DEFAULT_HOST;
|
|
15183
15606
|
let localDiscovery;
|
|
@@ -15197,6 +15620,8 @@ var init_profiles = __esm({
|
|
|
15197
15620
|
verbose: options.verbose ?? false,
|
|
15198
15621
|
noCache: options.noCache ?? false,
|
|
15199
15622
|
quiet: options.quiet ?? false,
|
|
15623
|
+
replication,
|
|
15624
|
+
replicationDebug: options.replicationDebug === true || process.env.TC_REPLICATION_DEBUG === "1",
|
|
15200
15625
|
...localDiscovery === void 0 ? {} : { localDiscovery }
|
|
15201
15626
|
};
|
|
15202
15627
|
}
|
|
@@ -15223,7 +15648,7 @@ function registerAccountCommand(program) {
|
|
|
15223
15648
|
assertOk(status);
|
|
15224
15649
|
outputJson(status.data);
|
|
15225
15650
|
} catch (error) {
|
|
15226
|
-
handleError(error);
|
|
15651
|
+
return handleError(error);
|
|
15227
15652
|
}
|
|
15228
15653
|
});
|
|
15229
15654
|
const apps = account.command("apps").description("Manage account application registry");
|
|
@@ -15254,7 +15679,7 @@ function registerAccountCommand(program) {
|
|
|
15254
15679
|
) + "\n"
|
|
15255
15680
|
);
|
|
15256
15681
|
} catch (error) {
|
|
15257
|
-
handleError(error);
|
|
15682
|
+
return handleError(error);
|
|
15258
15683
|
}
|
|
15259
15684
|
});
|
|
15260
15685
|
const spaces = account.command("spaces").description("Manage account space registry");
|
|
@@ -15285,7 +15710,7 @@ function registerAccountCommand(program) {
|
|
|
15285
15710
|
) + "\n"
|
|
15286
15711
|
);
|
|
15287
15712
|
} catch (error) {
|
|
15288
|
-
handleError(error);
|
|
15713
|
+
return handleError(error);
|
|
15289
15714
|
}
|
|
15290
15715
|
});
|
|
15291
15716
|
spaces.command("info <space-id>").description("Show a registered account space").action(async (spaceId, _options, cmd) => {
|
|
@@ -15295,7 +15720,7 @@ function registerAccountCommand(program) {
|
|
|
15295
15720
|
assertOk(result);
|
|
15296
15721
|
outputJson(formatSpace(result.data));
|
|
15297
15722
|
} catch (error) {
|
|
15298
|
-
handleError(error);
|
|
15723
|
+
return handleError(error);
|
|
15299
15724
|
}
|
|
15300
15725
|
});
|
|
15301
15726
|
spaces.command("register <space-id>").description("Register a space in account/spaces").requiredOption("--name <name>", "Display name for the space").requiredOption("--owner <did>", "Owner DID for the space").option("--type <type>", "Space type: owned, delegated, or discovered", "discovered").option("--permission <permission...>", "Permission strings for the space").action(async (spaceId, options, cmd) => {
|
|
@@ -15312,7 +15737,7 @@ function registerAccountCommand(program) {
|
|
|
15312
15737
|
assertOk(result);
|
|
15313
15738
|
outputJson({ space: formatSpace(result.data), registered: true });
|
|
15314
15739
|
} catch (error) {
|
|
15315
|
-
handleError(error);
|
|
15740
|
+
return handleError(error);
|
|
15316
15741
|
}
|
|
15317
15742
|
});
|
|
15318
15743
|
spaces.command("sync").description("Register currently accessible spaces into account/spaces").action(async (_options, cmd) => {
|
|
@@ -15322,7 +15747,7 @@ function registerAccountCommand(program) {
|
|
|
15322
15747
|
assertOk(result);
|
|
15323
15748
|
outputJson({ spaces: result.data.map(formatSpace), count: result.data.length, synced: true });
|
|
15324
15749
|
} catch (error) {
|
|
15325
|
-
handleError(error);
|
|
15750
|
+
return handleError(error);
|
|
15326
15751
|
}
|
|
15327
15752
|
});
|
|
15328
15753
|
spaces.command("remove <space-id>").alias("delete").description("Remove a space registry entry").action(async (spaceId, _options, cmd) => {
|
|
@@ -15332,7 +15757,7 @@ function registerAccountCommand(program) {
|
|
|
15332
15757
|
assertOk(result);
|
|
15333
15758
|
outputJson({ spaceId, removed: true });
|
|
15334
15759
|
} catch (error) {
|
|
15335
|
-
handleError(error);
|
|
15760
|
+
return handleError(error);
|
|
15336
15761
|
}
|
|
15337
15762
|
});
|
|
15338
15763
|
apps.command("info <app-id>").description("Show a registered account application").action(async (appId, _options, cmd) => {
|
|
@@ -15342,7 +15767,7 @@ function registerAccountCommand(program) {
|
|
|
15342
15767
|
assertOk(result);
|
|
15343
15768
|
outputJson(result.data);
|
|
15344
15769
|
} catch (error) {
|
|
15345
|
-
handleError(error);
|
|
15770
|
+
return handleError(error);
|
|
15346
15771
|
}
|
|
15347
15772
|
});
|
|
15348
15773
|
apps.command("register <manifest>").description("Register an app manifest in account/applications").action(async (manifestSource, _options, cmd) => {
|
|
@@ -15353,7 +15778,7 @@ function registerAccountCommand(program) {
|
|
|
15353
15778
|
assertOk(result);
|
|
15354
15779
|
outputJson({ application: result.data, registered: true });
|
|
15355
15780
|
} catch (error) {
|
|
15356
|
-
handleError(error);
|
|
15781
|
+
return handleError(error);
|
|
15357
15782
|
}
|
|
15358
15783
|
});
|
|
15359
15784
|
apps.command("remove <app-id>").alias("delete").description("Remove an application registry entry").action(async (appId, _options, cmd) => {
|
|
@@ -15363,7 +15788,7 @@ function registerAccountCommand(program) {
|
|
|
15363
15788
|
assertOk(result);
|
|
15364
15789
|
outputJson({ appId, removed: true });
|
|
15365
15790
|
} catch (error) {
|
|
15366
|
-
handleError(error);
|
|
15791
|
+
return handleError(error);
|
|
15367
15792
|
}
|
|
15368
15793
|
});
|
|
15369
15794
|
const delegations = account.command("delegations").description("View and revoke account delegations");
|
|
@@ -15399,7 +15824,7 @@ function registerAccountCommand(program) {
|
|
|
15399
15824
|
) + "\n"
|
|
15400
15825
|
);
|
|
15401
15826
|
} catch (error) {
|
|
15402
|
-
handleError(error);
|
|
15827
|
+
return handleError(error);
|
|
15403
15828
|
}
|
|
15404
15829
|
});
|
|
15405
15830
|
delegations.command("revoke <cid>").description("Revoke an active delegation granted by this account").requiredOption("--space <space>", "Space name or ID containing the delegation").action(async (cid, options, cmd) => {
|
|
@@ -15409,7 +15834,7 @@ function registerAccountCommand(program) {
|
|
|
15409
15834
|
assertOk(result);
|
|
15410
15835
|
outputJson({ cid, space: options.space, revoked: true });
|
|
15411
15836
|
} catch (error) {
|
|
15412
|
-
handleError(error);
|
|
15837
|
+
return handleError(error);
|
|
15413
15838
|
}
|
|
15414
15839
|
});
|
|
15415
15840
|
const index = account.command("index").description("Manage the materialized account SQLite index");
|
|
@@ -15420,7 +15845,7 @@ function registerAccountCommand(program) {
|
|
|
15420
15845
|
assertOk(result);
|
|
15421
15846
|
outputJson({ ...result.data, ensured: true });
|
|
15422
15847
|
} catch (error) {
|
|
15423
|
-
handleError(error);
|
|
15848
|
+
return handleError(error);
|
|
15424
15849
|
}
|
|
15425
15850
|
});
|
|
15426
15851
|
index.command("status").description("Show account SQLite index sync status").action(async (_options, cmd) => {
|
|
@@ -15430,7 +15855,7 @@ function registerAccountCommand(program) {
|
|
|
15430
15855
|
assertOk(result);
|
|
15431
15856
|
outputJson(result.data);
|
|
15432
15857
|
} catch (error) {
|
|
15433
|
-
handleError(error);
|
|
15858
|
+
return handleError(error);
|
|
15434
15859
|
}
|
|
15435
15860
|
});
|
|
15436
15861
|
index.command("rebuild").description("Rebuild account SQLite index from canonical account data").action(async (_options, cmd) => {
|
|
@@ -15440,7 +15865,7 @@ function registerAccountCommand(program) {
|
|
|
15440
15865
|
assertOk(result);
|
|
15441
15866
|
outputJson(result.data);
|
|
15442
15867
|
} catch (error) {
|
|
15443
|
-
handleError(error);
|
|
15868
|
+
return handleError(error);
|
|
15444
15869
|
}
|
|
15445
15870
|
});
|
|
15446
15871
|
index.command("query <sql>").description("Query the materialized account SQLite index").option("--params <json>", "Bind parameters as a JSON array for ? placeholders").action(async (sql, options, cmd) => {
|
|
@@ -15451,7 +15876,7 @@ function registerAccountCommand(program) {
|
|
|
15451
15876
|
assertOk(result);
|
|
15452
15877
|
outputJson(result.data);
|
|
15453
15878
|
} catch (error) {
|
|
15454
|
-
handleError(error);
|
|
15879
|
+
return handleError(error);
|
|
15455
15880
|
}
|
|
15456
15881
|
});
|
|
15457
15882
|
const billing = account.command("billing").description("Open account billing");
|
|
@@ -15463,7 +15888,7 @@ function registerAccountCommand(program) {
|
|
|
15463
15888
|
}
|
|
15464
15889
|
outputJson({ url: ACCOUNT_BILLING_URL, opened: Boolean(options.open) });
|
|
15465
15890
|
} catch (error) {
|
|
15466
|
-
handleError(error);
|
|
15891
|
+
return handleError(error);
|
|
15467
15892
|
}
|
|
15468
15893
|
});
|
|
15469
15894
|
}
|
|
@@ -15529,15 +15954,15 @@ import { get as httpGet } from "http";
|
|
|
15529
15954
|
import { get as httpsGet } from "https";
|
|
15530
15955
|
import { spawn } from "child_process";
|
|
15531
15956
|
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
|
|
15532
|
-
import { dirname as
|
|
15957
|
+
import { dirname as dirname3 } from "path";
|
|
15533
15958
|
import { createInterface as createInterface2 } from "readline";
|
|
15534
15959
|
import { grantAuthRequest, principalDidEquals, signedCompactUcanExpiry as signedCompactUcanExpiry3 } from "@tinycloud/node-sdk";
|
|
15535
15960
|
import { invokeOperation } from "@tinycloud/operations";
|
|
15536
15961
|
|
|
15537
15962
|
// src/lib/profile-replicas.ts
|
|
15538
|
-
import { lstat as lstat2, readdir as readdir4, realpath } from "fs/promises";
|
|
15539
|
-
import { join as
|
|
15540
|
-
import { ProfileDeletedError as ProfileDeletedError2, profilePath as
|
|
15963
|
+
import { lstat as lstat2, readdir as readdir4, realpath, rm as rm4 } from "fs/promises";
|
|
15964
|
+
import { join as join8, relative, sep } from "path";
|
|
15965
|
+
import { ProfileDeletedError as ProfileDeletedError2, profilePath as profilePath3, withProfileLock as withProfileLock4 } from "@tinycloud/operations/state";
|
|
15541
15966
|
|
|
15542
15967
|
// ../../node_modules/@noble/hashes/esm/blake3.js
|
|
15543
15968
|
init_md();
|
|
@@ -15772,8 +16197,8 @@ var BLAKE3 = class _BLAKE3 extends BLAKE2 {
|
|
|
15772
16197
|
}
|
|
15773
16198
|
_cloneInto(to) {
|
|
15774
16199
|
to = super._cloneInto(to);
|
|
15775
|
-
const { IV, flags, state, chunkPos, posOut, chunkOut, stack, chunksDone } = this;
|
|
15776
|
-
to.state.set(
|
|
16200
|
+
const { IV, flags, state: state2, chunkPos, posOut, chunkOut, stack, chunksDone } = this;
|
|
16201
|
+
to.state.set(state2.slice());
|
|
15777
16202
|
to.stack = stack.map((i) => Uint32Array.from(i));
|
|
15778
16203
|
to.IV.set(IV);
|
|
15779
16204
|
to.flags = flags;
|
|
@@ -17435,6 +17860,11 @@ function kvPrefixCovers(prefix, key) {
|
|
|
17435
17860
|
if (prefix.endsWith("/")) return key.startsWith(prefix);
|
|
17436
17861
|
return key === prefix || key.startsWith(`${prefix}/`);
|
|
17437
17862
|
}
|
|
17863
|
+
function authorityPathCovers(grantPath, requestedPath) {
|
|
17864
|
+
if (grantPath === "" || grantPath === "/") return true;
|
|
17865
|
+
if (grantPath.endsWith("/")) return requestedPath.startsWith(grantPath);
|
|
17866
|
+
return requestedPath === grantPath;
|
|
17867
|
+
}
|
|
17438
17868
|
function requiresSecretsOptIn(space, prefix) {
|
|
17439
17869
|
if (space === "secrets" || space.endsWith(":secrets")) return true;
|
|
17440
17870
|
const firstSegment = prefix.split("/", 1)[0];
|
|
@@ -17464,11 +17894,11 @@ function effectiveAuthority(input) {
|
|
|
17464
17894
|
const notBefore = maxIso(input.window?.notBefore ?? null, input.grant?.notBefore ?? null);
|
|
17465
17895
|
const expiresAt = minIso(input.window?.expiresAt ?? null, input.grant?.expiresAt ?? null);
|
|
17466
17896
|
const retainUntil = input.policy === "retainAfterExpiry" ? input.window?.retainUntil ?? null : null;
|
|
17467
|
-
let
|
|
17468
|
-
if (input.revoked !== null)
|
|
17469
|
-
else if (notBefore !== null && input.now < Date.parse(notBefore))
|
|
17470
|
-
else if (expiresAt !== null && input.now >= Date.parse(expiresAt))
|
|
17471
|
-
return { state, notBefore, expiresAt, retainUntil, retentionRevoked: (input.retentionRevoked ?? null) !== null };
|
|
17897
|
+
let state2 = "valid";
|
|
17898
|
+
if (input.revoked !== null) state2 = "revoked";
|
|
17899
|
+
else if (notBefore !== null && input.now < Date.parse(notBefore)) state2 = "not-yet-valid";
|
|
17900
|
+
else if (expiresAt !== null && input.now >= Date.parse(expiresAt)) state2 = "expired";
|
|
17901
|
+
return { state: state2, notBefore, expiresAt, retainUntil, retentionRevoked: (input.retentionRevoked ?? null) !== null };
|
|
17472
17902
|
}
|
|
17473
17903
|
function assertReadable(authority, now) {
|
|
17474
17904
|
switch (authority.state) {
|
|
@@ -17533,29 +17963,29 @@ var Replica = class {
|
|
|
17533
17963
|
this.#onCommit = options.onCommit;
|
|
17534
17964
|
}
|
|
17535
17965
|
async #state() {
|
|
17536
|
-
const
|
|
17537
|
-
if (
|
|
17538
|
-
return
|
|
17966
|
+
const state2 = await this.#store.open();
|
|
17967
|
+
if (state2 === null) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica has not been created.");
|
|
17968
|
+
return state2;
|
|
17539
17969
|
}
|
|
17540
17970
|
/** Throws unless local reads are allowed now; returns the meta every read reports. */
|
|
17541
|
-
#readMeta(
|
|
17971
|
+
#readMeta(state2) {
|
|
17542
17972
|
const now = this.#now();
|
|
17543
17973
|
const authority = assertReadable(
|
|
17544
17974
|
effectiveAuthority({
|
|
17545
|
-
window:
|
|
17546
|
-
grant:
|
|
17547
|
-
revoked:
|
|
17548
|
-
retentionRevoked:
|
|
17549
|
-
policy:
|
|
17975
|
+
window: state2.authority,
|
|
17976
|
+
grant: state2.grant,
|
|
17977
|
+
revoked: state2.revoked,
|
|
17978
|
+
retentionRevoked: state2.retentionRevoked,
|
|
17979
|
+
policy: state2.config.localReadPolicy,
|
|
17550
17980
|
now
|
|
17551
17981
|
}),
|
|
17552
17982
|
now
|
|
17553
17983
|
);
|
|
17554
17984
|
return {
|
|
17555
17985
|
consistency: "observed",
|
|
17556
|
-
source: { host:
|
|
17557
|
-
asOf:
|
|
17558
|
-
coverage:
|
|
17986
|
+
source: { host: state2.config.host, nodeDid: state2.nodeDid, space: state2.config.space, prefix: state2.config.prefix },
|
|
17987
|
+
asOf: state2.lastSyncAt,
|
|
17988
|
+
coverage: state2.coverage,
|
|
17559
17989
|
authority
|
|
17560
17990
|
};
|
|
17561
17991
|
}
|
|
@@ -17568,13 +17998,13 @@ var Replica = class {
|
|
|
17568
17998
|
return this.#readMeta(await this.#state());
|
|
17569
17999
|
}
|
|
17570
18000
|
async get(key, options = {}) {
|
|
17571
|
-
const
|
|
17572
|
-
this.#readMeta(
|
|
17573
|
-
if (!kvPrefixCovers(
|
|
18001
|
+
const state2 = await this.#state();
|
|
18002
|
+
this.#readMeta(state2);
|
|
18003
|
+
if (!kvPrefixCovers(state2.config.prefix, key)) return { status: "not_covered", key, meta: await this.#confirmedMeta() };
|
|
17574
18004
|
for (let attempt = 0; ; attempt += 1) {
|
|
17575
18005
|
const entry = await this.#store.get(key);
|
|
17576
18006
|
if (entry === void 0) {
|
|
17577
|
-
const status =
|
|
18007
|
+
const status = state2.coverage === "complete" ? "absent" : "coverage_incomplete";
|
|
17578
18008
|
return { status, key, meta: await this.#confirmedMeta() };
|
|
17579
18009
|
}
|
|
17580
18010
|
if (entry.deleted) return { status: "deleted", key, meta: await this.#confirmedMeta() };
|
|
@@ -17597,9 +18027,9 @@ var Replica = class {
|
|
|
17597
18027
|
}
|
|
17598
18028
|
/** Live keys under `prefix` (a plain string prefix within the replica's scope). */
|
|
17599
18029
|
async list(options = {}) {
|
|
17600
|
-
const
|
|
17601
|
-
this.#readMeta(
|
|
17602
|
-
const scope =
|
|
18030
|
+
const state2 = await this.#state();
|
|
18031
|
+
this.#readMeta(state2);
|
|
18032
|
+
const scope = state2.config.prefix;
|
|
17603
18033
|
const prefix = options.prefix ?? "";
|
|
17604
18034
|
if (prefix !== "" && !scope.startsWith(prefix) && !kvPrefixCovers(scope, prefix)) {
|
|
17605
18035
|
throw new ReplicaError(
|
|
@@ -17706,9 +18136,9 @@ var Replica = class {
|
|
|
17706
18136
|
return { notBefore: authority.notBefore, expiresAt: authority.expiresAt };
|
|
17707
18137
|
}
|
|
17708
18138
|
async #syncUnder(attempt, limit) {
|
|
17709
|
-
const
|
|
17710
|
-
const { config } =
|
|
17711
|
-
this.#syncWindow(attempt.promote === null ?
|
|
18139
|
+
const state2 = await this.#state();
|
|
18140
|
+
const { config } = state2;
|
|
18141
|
+
this.#syncWindow(attempt.promote === null ? state2.authority : null, attempt.grant);
|
|
17712
18142
|
const report = {
|
|
17713
18143
|
pages: 0,
|
|
17714
18144
|
changes: 0,
|
|
@@ -17717,14 +18147,14 @@ var Replica = class {
|
|
|
17717
18147
|
contentMissing: 0,
|
|
17718
18148
|
repaired: 0,
|
|
17719
18149
|
resets: 0,
|
|
17720
|
-
coverage:
|
|
18150
|
+
coverage: state2.coverage,
|
|
17721
18151
|
cursorAdvanced: false,
|
|
17722
18152
|
promotedGrant: false,
|
|
17723
18153
|
blobsCollected: 0
|
|
17724
18154
|
};
|
|
17725
|
-
let cursor =
|
|
17726
|
-
let pinned =
|
|
17727
|
-
let coverage =
|
|
18155
|
+
let cursor = state2.cursor;
|
|
18156
|
+
let pinned = state2.nodeDid;
|
|
18157
|
+
let coverage = state2.coverage;
|
|
17728
18158
|
const retentionGrantCid = config.retentionGrantCid;
|
|
17729
18159
|
for (; ; ) {
|
|
17730
18160
|
let page;
|
|
@@ -17885,17 +18315,17 @@ var Replica = class {
|
|
|
17885
18315
|
}
|
|
17886
18316
|
await this.#store.renewLease(attempt.lease, this.#leaseTtlMs);
|
|
17887
18317
|
if (changes.length === 0) continue;
|
|
17888
|
-
const
|
|
18318
|
+
const state2 = await this.#state();
|
|
17889
18319
|
await this.#store.applyPage(attempt.lease, {
|
|
17890
18320
|
changes,
|
|
17891
18321
|
blobs,
|
|
17892
18322
|
cursor,
|
|
17893
|
-
source: { nodeDid: nodeDid ?? "", space:
|
|
18323
|
+
source: { nodeDid: nodeDid ?? "", space: state2.config.space, prefix: state2.config.prefix },
|
|
17894
18324
|
authority: null,
|
|
17895
18325
|
coverage,
|
|
17896
18326
|
at: new Date(this.#now()).toISOString(),
|
|
17897
18327
|
retentionGrantCid,
|
|
17898
|
-
window: this.#syncWindow(
|
|
18328
|
+
window: this.#syncWindow(state2.authority, attempt.grant),
|
|
17899
18329
|
complete: false,
|
|
17900
18330
|
promoteGrant: null
|
|
17901
18331
|
});
|
|
@@ -18062,7 +18492,7 @@ function kvPathIn(resource, space) {
|
|
|
18062
18492
|
function grantCovers(grant, space, prefix, ability) {
|
|
18063
18493
|
for (const [resource, abilities] of Object.entries(grant.att)) {
|
|
18064
18494
|
const path = kvPathIn(resource, space);
|
|
18065
|
-
if (path === void 0 || !
|
|
18495
|
+
if (path === void 0 || !authorityPathCovers(path, prefix)) continue;
|
|
18066
18496
|
if (ability in abilities) return true;
|
|
18067
18497
|
if (ability !== "tinycloud.kv/sync" && ability !== "tinycloud.kv/retain" && "tinycloud.kv/*" in abilities) return true;
|
|
18068
18498
|
}
|
|
@@ -18084,8 +18514,9 @@ function assertGrantInstallable(grant, input) {
|
|
|
18084
18514
|
`The grant was issued to ${grant.audience}, not this device (${input.deviceDid}).`
|
|
18085
18515
|
);
|
|
18086
18516
|
}
|
|
18517
|
+
const requiredPaths = input.prefix.endsWith("/") ? [input.prefix] : [input.prefix, `${input.prefix}/`];
|
|
18087
18518
|
for (const ability of ["tinycloud.kv/sync", "tinycloud.kv/get"]) {
|
|
18088
|
-
if (!grantCovers(grant, input.space,
|
|
18519
|
+
if (!requiredPaths.every((path) => grantCovers(grant, input.space, path, ability))) {
|
|
18089
18520
|
throw new ReplicaError(
|
|
18090
18521
|
ReplicaErrorCode.GRANT_NOT_COVERING,
|
|
18091
18522
|
`The grant does not carry ${ability} on ${input.space}/kv/${input.prefix}.`,
|
|
@@ -18238,7 +18669,7 @@ function kvSyncTransport(kv) {
|
|
|
18238
18669
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
18239
18670
|
import { constants as fsConstants } from "fs";
|
|
18240
18671
|
import { chmod as chmod3, link, mkdir as mkdir2, open as open2, readFile as readFile4, readdir as readdir3, rm as rm3, rmdir as rmdir2, stat as stat2, unlink } from "fs/promises";
|
|
18241
|
-
import { join as
|
|
18672
|
+
import { join as join7 } from "path";
|
|
18242
18673
|
var ReplicaErrorCode2 = {
|
|
18243
18674
|
/** Another process holds the sync lease. */
|
|
18244
18675
|
BUSY: "REPLICA_BUSY",
|
|
@@ -18325,11 +18756,11 @@ function effectiveAuthority2(input) {
|
|
|
18325
18756
|
const notBefore = maxIso2(input.window?.notBefore ?? null, input.grant?.notBefore ?? null);
|
|
18326
18757
|
const expiresAt = minIso2(input.window?.expiresAt ?? null, input.grant?.expiresAt ?? null);
|
|
18327
18758
|
const retainUntil = input.policy === "retainAfterExpiry" ? input.window?.retainUntil ?? null : null;
|
|
18328
|
-
let
|
|
18329
|
-
if (input.revoked !== null)
|
|
18330
|
-
else if (notBefore !== null && input.now < Date.parse(notBefore))
|
|
18331
|
-
else if (expiresAt !== null && input.now >= Date.parse(expiresAt))
|
|
18332
|
-
return { state, notBefore, expiresAt, retainUntil, retentionRevoked: (input.retentionRevoked ?? null) !== null };
|
|
18759
|
+
let state2 = "valid";
|
|
18760
|
+
if (input.revoked !== null) state2 = "revoked";
|
|
18761
|
+
else if (notBefore !== null && input.now < Date.parse(notBefore)) state2 = "not-yet-valid";
|
|
18762
|
+
else if (expiresAt !== null && input.now >= Date.parse(expiresAt)) state2 = "expired";
|
|
18763
|
+
return { state: state2, notBefore, expiresAt, retainUntil, retentionRevoked: (input.retentionRevoked ?? null) !== null };
|
|
18333
18764
|
}
|
|
18334
18765
|
function wrap(native) {
|
|
18335
18766
|
const statements = /* @__PURE__ */ new Map();
|
|
@@ -18348,8 +18779,20 @@ function wrap(native) {
|
|
|
18348
18779
|
get: (sql, ...params) => prepared(sql).get(...params) ?? void 0,
|
|
18349
18780
|
all: (sql, ...params) => prepared(sql).all(...params),
|
|
18350
18781
|
close: () => {
|
|
18351
|
-
|
|
18352
|
-
|
|
18782
|
+
let failure;
|
|
18783
|
+
try {
|
|
18784
|
+
for (const statement of statements.values()) {
|
|
18785
|
+
try {
|
|
18786
|
+
statement.finalize?.();
|
|
18787
|
+
} catch (error) {
|
|
18788
|
+
failure ??= error;
|
|
18789
|
+
}
|
|
18790
|
+
}
|
|
18791
|
+
} finally {
|
|
18792
|
+
statements.clear();
|
|
18793
|
+
native.close();
|
|
18794
|
+
}
|
|
18795
|
+
if (failure !== void 0) throw failure;
|
|
18353
18796
|
}
|
|
18354
18797
|
};
|
|
18355
18798
|
}
|
|
@@ -18363,7 +18806,7 @@ async function loadSqlite() {
|
|
|
18363
18806
|
if (runtime.Bun !== void 0) {
|
|
18364
18807
|
const module2 = await load(["bun", "sqlite"].join(":"));
|
|
18365
18808
|
const Database = module2.Database;
|
|
18366
|
-
return (path) => wrap(new Database(path, { create: true }));
|
|
18809
|
+
return (path, options) => wrap(new Database(path, { create: !options?.readonly, ...options?.readonly ? { readonly: true } : {} }));
|
|
18367
18810
|
}
|
|
18368
18811
|
const version = runtime.process?.versions?.node ?? "0.0.0";
|
|
18369
18812
|
if (!nodeSqliteSupported(version)) {
|
|
@@ -18388,7 +18831,7 @@ async function loadSqlite() {
|
|
|
18388
18831
|
process2.emitWarning = emitWarning;
|
|
18389
18832
|
}
|
|
18390
18833
|
const DatabaseSync = module.DatabaseSync;
|
|
18391
|
-
return (path) => wrap(new DatabaseSync(path));
|
|
18834
|
+
return (path, options) => wrap(options?.readonly ? new DatabaseSync(path, { readOnly: true }) : new DatabaseSync(path));
|
|
18392
18835
|
}
|
|
18393
18836
|
var SCHEMA_VERSION = 1;
|
|
18394
18837
|
var DURABILITY = "sqlite-wal-synchronous-full+fsynced-blobs";
|
|
@@ -18506,12 +18949,60 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
18506
18949
|
this.#faults = options[FAULTS] ?? {};
|
|
18507
18950
|
this.#guard = options.guard ?? ((section) => section());
|
|
18508
18951
|
}
|
|
18952
|
+
/**
|
|
18953
|
+
* Read-only status snapshot; does not create a replica, migrate, recover
|
|
18954
|
+
* revocation cleanup, or enter the mutation guard. An existing database is
|
|
18955
|
+
* opened normally in read-only WAL mode so committed WAL state is visible.
|
|
18956
|
+
*/
|
|
18957
|
+
static async inspect(dir, options = {}) {
|
|
18958
|
+
const checkAbort = () => {
|
|
18959
|
+
if (options.signal?.aborted) throw options.signal.reason ?? new ReplicaError2(ReplicaErrorCode2.CLOSED, "Replica status inspection was cancelled.");
|
|
18960
|
+
};
|
|
18961
|
+
checkAbort();
|
|
18962
|
+
const dbPath = join7(dir, "replica.db");
|
|
18963
|
+
let ino;
|
|
18964
|
+
try {
|
|
18965
|
+
ino = (await stat2(dbPath)).ino;
|
|
18966
|
+
} catch (error) {
|
|
18967
|
+
if (errnoOf(error) === "ENOENT") return null;
|
|
18968
|
+
throw storageError(error, "Inspecting the replica");
|
|
18969
|
+
}
|
|
18970
|
+
checkAbort();
|
|
18971
|
+
const opener = await loadSqlite();
|
|
18972
|
+
checkAbort();
|
|
18973
|
+
let db;
|
|
18974
|
+
try {
|
|
18975
|
+
db = opener(dbPath, { readonly: true });
|
|
18976
|
+
db.exec("PRAGMA busy_timeout = 0");
|
|
18977
|
+
checkAbort();
|
|
18978
|
+
const store = new _SqliteReplicaStore(dir, db, ino, { create: false, ...options.now ? { now: options.now } : {} });
|
|
18979
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
18980
|
+
const before = db.get("PRAGMA data_version").data_version;
|
|
18981
|
+
const state2 = await store.open();
|
|
18982
|
+
checkAbort();
|
|
18983
|
+
if (state2 === null) return null;
|
|
18984
|
+
const status = await store.status();
|
|
18985
|
+
await new Promise((resolve5) => setImmediate(resolve5));
|
|
18986
|
+
checkAbort();
|
|
18987
|
+
const after = db.get("PRAGMA data_version").data_version;
|
|
18988
|
+
if (before === after) return { state: state2, status };
|
|
18989
|
+
}
|
|
18990
|
+
throw new ReplicaError2(ReplicaErrorCode2.BUSY, "The replica changed during status inspection.");
|
|
18991
|
+
} catch (error) {
|
|
18992
|
+
if (/SQLITE_BUSY|database is locked/i.test(error instanceof Error ? error.message : String(error))) {
|
|
18993
|
+
throw new ReplicaError2(ReplicaErrorCode2.BUSY, "The replica is busy during status inspection.", void 0, { cause: error });
|
|
18994
|
+
}
|
|
18995
|
+
throw storageError(error, "Inspecting the replica");
|
|
18996
|
+
} finally {
|
|
18997
|
+
db?.close();
|
|
18998
|
+
}
|
|
18999
|
+
}
|
|
18509
19000
|
/**
|
|
18510
19001
|
* Open the replica store in `dir`. With `create: false` a missing replica is
|
|
18511
19002
|
* REPLICA_NOT_FOUND and nothing is written to disk.
|
|
18512
19003
|
*/
|
|
18513
19004
|
static async open(dir, options) {
|
|
18514
|
-
const dbPath =
|
|
19005
|
+
const dbPath = join7(dir, "replica.db");
|
|
18515
19006
|
if (!options.create) {
|
|
18516
19007
|
const exists = await stat2(dbPath).then(
|
|
18517
19008
|
() => true,
|
|
@@ -18523,8 +19014,8 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
18523
19014
|
let store;
|
|
18524
19015
|
try {
|
|
18525
19016
|
if (options.create) {
|
|
18526
|
-
await mkdir2(
|
|
18527
|
-
await Promise.all([chmod3(dir, 448), chmod3(
|
|
19017
|
+
await mkdir2(join7(dir, "blobs", ".tmp"), { recursive: true, mode: 448 });
|
|
19018
|
+
await Promise.all([chmod3(dir, 448), chmod3(join7(dir, "blobs"), 448), chmod3(join7(dir, "blobs", ".tmp"), 448)]);
|
|
18528
19019
|
const handle = await open2(dbPath, fsConstants.O_CREAT | fsConstants.O_RDWR, 384);
|
|
18529
19020
|
await handle.close();
|
|
18530
19021
|
await chmod3(dbPath, 384);
|
|
@@ -18565,7 +19056,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
18565
19056
|
}
|
|
18566
19057
|
/** Refuse to touch the filesystem for a replica that was deleted (or replaced) under us. */
|
|
18567
19058
|
async #assertPresent() {
|
|
18568
|
-
const ino = await stat2(
|
|
19059
|
+
const ino = await stat2(join7(this.dir, "replica.db")).then(
|
|
18569
19060
|
(stats) => stats.ino,
|
|
18570
19061
|
() => void 0
|
|
18571
19062
|
);
|
|
@@ -18797,7 +19288,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
18797
19288
|
return held;
|
|
18798
19289
|
}
|
|
18799
19290
|
#blobPath(hash) {
|
|
18800
|
-
return
|
|
19291
|
+
return join7(this.dir, "blobs", hash.slice(0, 2), hash);
|
|
18801
19292
|
}
|
|
18802
19293
|
/**
|
|
18803
19294
|
* Make every blob durable before any row references it: write to a fresh
|
|
@@ -18814,11 +19305,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
18814
19305
|
() => false
|
|
18815
19306
|
);
|
|
18816
19307
|
if (exists) continue;
|
|
18817
|
-
const shard =
|
|
19308
|
+
const shard = join7(this.dir, "blobs", hash.slice(0, 2));
|
|
18818
19309
|
await mkdir2(shard, { mode: 448 }).catch((error) => {
|
|
18819
19310
|
if (errnoOf(error) !== "EEXIST") throw error;
|
|
18820
19311
|
});
|
|
18821
|
-
const temp =
|
|
19312
|
+
const temp = join7(this.dir, "blobs", ".tmp", `${hash}.${randomBytes2(6).toString("hex")}`);
|
|
18822
19313
|
const handle = await open2(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 384);
|
|
18823
19314
|
try {
|
|
18824
19315
|
await handle.writeFile(bytes);
|
|
@@ -18834,7 +19325,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
18834
19325
|
await unlink(temp);
|
|
18835
19326
|
}
|
|
18836
19327
|
dirs.add(shard);
|
|
18837
|
-
dirs.add(
|
|
19328
|
+
dirs.add(join7(this.dir, "blobs"));
|
|
18838
19329
|
}
|
|
18839
19330
|
for (const dir of dirs) {
|
|
18840
19331
|
const handle = await open2(dir, fsConstants.O_RDONLY);
|
|
@@ -18942,12 +19433,12 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
18942
19433
|
/** Unlink every blob not in `keep`, and every temp file. Only a missing directory counts as empty. */
|
|
18943
19434
|
async #unlinkBlobs(keep) {
|
|
18944
19435
|
let removed = 0;
|
|
18945
|
-
const root =
|
|
19436
|
+
const root = join7(this.dir, "blobs");
|
|
18946
19437
|
for (const shard of await entriesOf(root)) {
|
|
18947
|
-
const shardPath =
|
|
19438
|
+
const shardPath = join7(root, shard);
|
|
18948
19439
|
if (shard === ".tmp") {
|
|
18949
19440
|
for (const name of await entriesOf(shardPath)) {
|
|
18950
|
-
await rm3(
|
|
19441
|
+
await rm3(join7(shardPath, name), { force: true });
|
|
18951
19442
|
removed += 1;
|
|
18952
19443
|
}
|
|
18953
19444
|
continue;
|
|
@@ -18958,7 +19449,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
18958
19449
|
kept += 1;
|
|
18959
19450
|
continue;
|
|
18960
19451
|
}
|
|
18961
|
-
await rm3(
|
|
19452
|
+
await rm3(join7(shardPath, name), { force: true });
|
|
18962
19453
|
removed += 1;
|
|
18963
19454
|
}
|
|
18964
19455
|
if (kept === 0) await rmdir2(shardPath).catch(() => void 0);
|
|
@@ -18967,9 +19458,9 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
18967
19458
|
}
|
|
18968
19459
|
/** Whether any content file (a blob or a temp file) is on disk. */
|
|
18969
19460
|
async #holdsContentFiles() {
|
|
18970
|
-
const root =
|
|
19461
|
+
const root = join7(this.dir, "blobs");
|
|
18971
19462
|
for (const shard of await entriesOf(root)) {
|
|
18972
|
-
if ((await entriesOf(
|
|
19463
|
+
if ((await entriesOf(join7(root, shard))).length > 0) return true;
|
|
18973
19464
|
}
|
|
18974
19465
|
return false;
|
|
18975
19466
|
}
|
|
@@ -19150,8 +19641,8 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19150
19641
|
}
|
|
19151
19642
|
}
|
|
19152
19643
|
async status(now = this.#now()) {
|
|
19153
|
-
const
|
|
19154
|
-
if (
|
|
19644
|
+
const state2 = await this.open();
|
|
19645
|
+
if (state2 === null) throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica has not been created.");
|
|
19155
19646
|
const row = this.#row();
|
|
19156
19647
|
const counts = this.#db.get(
|
|
19157
19648
|
`SELECT
|
|
@@ -19162,41 +19653,41 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
19162
19653
|
FROM entry`
|
|
19163
19654
|
);
|
|
19164
19655
|
const { retentionRevoked: _retentionRevoked, ...authority } = effectiveAuthority2({
|
|
19165
|
-
window:
|
|
19166
|
-
grant:
|
|
19167
|
-
revoked:
|
|
19168
|
-
retentionRevoked:
|
|
19169
|
-
policy:
|
|
19656
|
+
window: state2.authority,
|
|
19657
|
+
grant: state2.grant,
|
|
19658
|
+
revoked: state2.revoked,
|
|
19659
|
+
retentionRevoked: state2.retentionRevoked,
|
|
19660
|
+
policy: state2.config.localReadPolicy,
|
|
19170
19661
|
now
|
|
19171
19662
|
});
|
|
19172
19663
|
return {
|
|
19173
|
-
name:
|
|
19174
|
-
replicaId:
|
|
19175
|
-
source: { host:
|
|
19664
|
+
name: state2.config.name,
|
|
19665
|
+
replicaId: state2.config.replicaId,
|
|
19666
|
+
source: { host: state2.config.host, nodeDid: state2.nodeDid, space: state2.config.space, prefix: state2.config.prefix },
|
|
19176
19667
|
device: {
|
|
19177
|
-
did:
|
|
19178
|
-
delegationCid:
|
|
19179
|
-
pendingDelegationCid:
|
|
19180
|
-
pendingDelegationError:
|
|
19668
|
+
did: state2.config.deviceDid,
|
|
19669
|
+
delegationCid: state2.grant?.cid ?? null,
|
|
19670
|
+
pendingDelegationCid: state2.pendingGrant?.cid ?? null,
|
|
19671
|
+
pendingDelegationError: state2.pendingGrantError
|
|
19181
19672
|
},
|
|
19182
19673
|
authority: {
|
|
19183
19674
|
...authority,
|
|
19184
|
-
localReadPolicy:
|
|
19185
|
-
retentionGrantCid:
|
|
19186
|
-
revokedDetail:
|
|
19187
|
-
retentionRevokedDetail:
|
|
19675
|
+
localReadPolicy: state2.config.localReadPolicy,
|
|
19676
|
+
retentionGrantCid: state2.config.retentionGrantCid,
|
|
19677
|
+
revokedDetail: state2.revoked,
|
|
19678
|
+
retentionRevokedDetail: state2.retentionRevoked
|
|
19188
19679
|
},
|
|
19189
19680
|
consistency: "observed",
|
|
19190
|
-
coverage:
|
|
19191
|
-
lastSyncAt:
|
|
19192
|
-
lastCompleteAt:
|
|
19681
|
+
coverage: state2.coverage,
|
|
19682
|
+
lastSyncAt: state2.lastSyncAt,
|
|
19683
|
+
lastCompleteAt: state2.lastCompleteAt,
|
|
19193
19684
|
counts: { keys: Number(counts.keys), contentMissing: Number(counts.missing), tombstones: Number(counts.tombstones) },
|
|
19194
19685
|
bytes: Number(counts.bytes),
|
|
19195
19686
|
durability: DURABILITY,
|
|
19196
19687
|
syncing: row.lease_holder !== null && row.lease_expires_at !== null && row.lease_expires_at > now,
|
|
19197
19688
|
purgePending: row.purge_pending === 1,
|
|
19198
|
-
lastError:
|
|
19199
|
-
lastReset:
|
|
19689
|
+
lastError: state2.lastError,
|
|
19690
|
+
lastReset: state2.lastReset
|
|
19200
19691
|
};
|
|
19201
19692
|
}
|
|
19202
19693
|
async close() {
|
|
@@ -19229,14 +19720,14 @@ async function safeReplicaRoot(profile, root) {
|
|
|
19229
19720
|
if (!rootStats.isDirectory()) {
|
|
19230
19721
|
throw refuseUnsafeReplicaPath(profile, `the replicas path "${root}" is not a real directory`);
|
|
19231
19722
|
}
|
|
19232
|
-
const [profileRealPath, rootRealPath] = await Promise.all([realpath(
|
|
19723
|
+
const [profileRealPath, rootRealPath] = await Promise.all([realpath(profilePath3(profile)), realpath(root)]);
|
|
19233
19724
|
if (!rootRealPath.startsWith(profileRealPath + sep)) {
|
|
19234
19725
|
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" resolves outside the selected profile`);
|
|
19235
19726
|
}
|
|
19236
19727
|
return rootRealPath;
|
|
19237
19728
|
}
|
|
19238
19729
|
async function safeReplicaEntry(profile, root, entry) {
|
|
19239
|
-
const entryPath =
|
|
19730
|
+
const entryPath = join8(root, entry.name);
|
|
19240
19731
|
const rootRealPath = await safeReplicaRoot(profile, root);
|
|
19241
19732
|
if (rootRealPath === null) {
|
|
19242
19733
|
throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" disappeared`);
|
|
@@ -19254,9 +19745,9 @@ async function safeReplicaEntry(profile, root, entry) {
|
|
|
19254
19745
|
}
|
|
19255
19746
|
return entryPath;
|
|
19256
19747
|
}
|
|
19257
|
-
async function removeProfileReplicas(profile) {
|
|
19258
|
-
return
|
|
19259
|
-
const root =
|
|
19748
|
+
async function removeProfileReplicas(profile, heldLeases) {
|
|
19749
|
+
return withProfileLock4(profile, async () => {
|
|
19750
|
+
const root = join8(profilePath3(profile), "replicas");
|
|
19260
19751
|
if (await safeReplicaRoot(profile, root) === null) return [];
|
|
19261
19752
|
const replicas = (await readdir4(root, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
19262
19753
|
if (replicas.length === 0) return [];
|
|
@@ -19270,7 +19761,7 @@ async function removeProfileReplicas(profile) {
|
|
|
19270
19761
|
create: false,
|
|
19271
19762
|
guard: async (action) => {
|
|
19272
19763
|
try {
|
|
19273
|
-
return await
|
|
19764
|
+
return await withProfileLock4(profile, action, { requireProfile: true });
|
|
19274
19765
|
} catch (error) {
|
|
19275
19766
|
if (error instanceof ProfileDeletedError2) {
|
|
19276
19767
|
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" was deleted; its replicas are gone and nothing was written.`);
|
|
@@ -19283,7 +19774,7 @@ async function removeProfileReplicas(profile) {
|
|
|
19283
19774
|
if (await store.open() === null) {
|
|
19284
19775
|
throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
|
|
19285
19776
|
}
|
|
19286
|
-
const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
19777
|
+
const lease = heldLeases?.get(entryPath) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
19287
19778
|
if (lease === null) {
|
|
19288
19779
|
throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
19289
19780
|
}
|
|
@@ -19295,7 +19786,7 @@ async function removeProfileReplicas(profile) {
|
|
|
19295
19786
|
await replica.store.destroy(replica.lease);
|
|
19296
19787
|
removed.push(replica.name);
|
|
19297
19788
|
} catch (error) {
|
|
19298
|
-
const replicaStillExists = await lstat2(
|
|
19789
|
+
const replicaStillExists = await lstat2(join8(root, replica.name)).then(
|
|
19299
19790
|
() => true,
|
|
19300
19791
|
(statError) => statError.code !== "ENOENT"
|
|
19301
19792
|
);
|
|
@@ -19318,8 +19809,248 @@ async function removeProfileReplicas(profile) {
|
|
|
19318
19809
|
ExitCode.ERROR
|
|
19319
19810
|
);
|
|
19320
19811
|
} finally {
|
|
19812
|
+
await Promise.all(opened.map(async ({ name, store, lease }) => {
|
|
19813
|
+
if (lease !== null && heldLeases?.has(join8(root, name)) !== true) await store.releaseLease(lease).catch(() => void 0);
|
|
19814
|
+
await store.close().catch(() => void 0);
|
|
19815
|
+
}));
|
|
19816
|
+
}
|
|
19817
|
+
});
|
|
19818
|
+
}
|
|
19819
|
+
async function removeProfileReplication(profile, heldLeases) {
|
|
19820
|
+
return withProfileLock4(profile, async () => {
|
|
19821
|
+
const root = join8(profilePath3(profile), "replication");
|
|
19822
|
+
const rootReal = await safeReplicaRoot(profile, root);
|
|
19823
|
+
if (rootReal === null) return [];
|
|
19824
|
+
const profileReal = await realpath(profilePath3(profile));
|
|
19825
|
+
const partitions = await readdir4(root, { withFileTypes: true });
|
|
19826
|
+
const opened = [];
|
|
19827
|
+
const removed = [];
|
|
19828
|
+
let deleting = false;
|
|
19829
|
+
try {
|
|
19830
|
+
for (const partition of partitions) {
|
|
19831
|
+
const partitionPath = join8(root, partition.name);
|
|
19832
|
+
const partitionStats = await lstat2(partitionPath);
|
|
19833
|
+
if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
|
|
19834
|
+
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
|
|
19835
|
+
}
|
|
19836
|
+
if (partitionStats.isDirectory()) {
|
|
19837
|
+
const partitionReal = await realpath(partitionPath);
|
|
19838
|
+
if (!partitionReal.startsWith(rootReal + sep) || !partitionReal.startsWith(profileReal + sep)) {
|
|
19839
|
+
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the selected profile`);
|
|
19840
|
+
}
|
|
19841
|
+
const replicasRoot2 = join8(partitionPath, "replicas");
|
|
19842
|
+
const replicasStats = await lstat2(replicasRoot2).catch((error) => {
|
|
19843
|
+
if (error.code === "ENOENT") return null;
|
|
19844
|
+
throw error;
|
|
19845
|
+
});
|
|
19846
|
+
if (!replicasStats) continue;
|
|
19847
|
+
if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
|
|
19848
|
+
throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot2}" is not a real directory`);
|
|
19849
|
+
}
|
|
19850
|
+
const replicas = await readdir4(replicasRoot2, { withFileTypes: true });
|
|
19851
|
+
for (const replica of replicas) {
|
|
19852
|
+
const replicaPath = join8(replicasRoot2, replica.name);
|
|
19853
|
+
const replicaStats = await lstat2(replicaPath);
|
|
19854
|
+
if (replica.isSymbolicLink() || replicaStats.isSymbolicLink() || !replicaStats.isDirectory()) {
|
|
19855
|
+
throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
|
|
19856
|
+
}
|
|
19857
|
+
const replicaReal = await realpath(replicaPath);
|
|
19858
|
+
if (!replicaReal.startsWith(rootReal + sep)) {
|
|
19859
|
+
throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" resolves outside the replication root`);
|
|
19860
|
+
}
|
|
19861
|
+
const database = await lstat2(join8(replicaPath, "replica.db")).catch((error) => {
|
|
19862
|
+
if (error.code === "ENOENT") return null;
|
|
19863
|
+
throw error;
|
|
19864
|
+
});
|
|
19865
|
+
if (!database) continue;
|
|
19866
|
+
if (database.isSymbolicLink() || !database.isFile()) {
|
|
19867
|
+
throw refuseUnsafeReplicaPath(profile, `replication database "${replica.name}" is not a regular file`);
|
|
19868
|
+
}
|
|
19869
|
+
}
|
|
19870
|
+
} else if (!partitionStats.isFile()) {
|
|
19871
|
+
throw refuseUnsafeReplicaPath(profile, `replication path "${partition.name}" is not a regular file or directory`);
|
|
19872
|
+
}
|
|
19873
|
+
}
|
|
19874
|
+
const hasReplicaDatabase = await Promise.all(partitions.filter((entry) => entry.isDirectory()).map(async (partition) => {
|
|
19875
|
+
return Promise.all((await readdir4(join8(root, partition.name, "replicas"), { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map((entry) => lstat2(join8(root, partition.name, "replicas", entry.name, "replica.db")).then(() => true, () => false)));
|
|
19876
|
+
})).then((values) => values.some((partition) => partition.some(Boolean)));
|
|
19877
|
+
if (hasReplicaDatabase) await loadSqlite();
|
|
19878
|
+
for (const partition of partitions.filter((entry) => entry.isDirectory())) {
|
|
19879
|
+
const replicasRoot2 = join8(root, partition.name, "replicas");
|
|
19880
|
+
for (const entry of await readdir4(replicasRoot2, { withFileTypes: true }).catch(() => [])) {
|
|
19881
|
+
if (!entry.isDirectory()) continue;
|
|
19882
|
+
const database = await lstat2(join8(replicasRoot2, entry.name, "replica.db")).catch((error) => {
|
|
19883
|
+
if (error.code === "ENOENT") return null;
|
|
19884
|
+
throw error;
|
|
19885
|
+
});
|
|
19886
|
+
if (!database) continue;
|
|
19887
|
+
const name = `replication/${partition.name}/${entry.name}`;
|
|
19888
|
+
const store = await SqliteReplicaStore.open(join8(replicasRoot2, entry.name), { create: false, guard: async (action) => withProfileLock4(profile, action, { requireProfile: true }) });
|
|
19889
|
+
if (await store.open() === null) {
|
|
19890
|
+
await store.close();
|
|
19891
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "${name}" has no initialized store.`, ExitCode.ERROR);
|
|
19892
|
+
}
|
|
19893
|
+
const lease = heldLeases?.get(join8(replicasRoot2, entry.name)) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
19894
|
+
if (lease === null) {
|
|
19895
|
+
await store.close();
|
|
19896
|
+
throw new CLIError("REPLICA_BUSY", `Cannot log out: flag replica "${name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
19897
|
+
}
|
|
19898
|
+
opened.push({ name, path: join8(replicasRoot2, entry.name), store, lease });
|
|
19899
|
+
}
|
|
19900
|
+
}
|
|
19901
|
+
deleting = true;
|
|
19902
|
+
for (const item of opened) {
|
|
19903
|
+
await item.store.destroy(item.lease);
|
|
19904
|
+
removed.push(item.name);
|
|
19905
|
+
}
|
|
19906
|
+
await rm4(root, { recursive: true });
|
|
19907
|
+
return removed;
|
|
19908
|
+
} catch (error) {
|
|
19909
|
+
if (deleting) {
|
|
19910
|
+
const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
|
|
19911
|
+
throw new CLIError(
|
|
19912
|
+
"REPLICA_PURGE_FAILED",
|
|
19913
|
+
`Logout cleared the session, but replication cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
|
|
19914
|
+
ExitCode.ERROR,
|
|
19915
|
+
{ replicasRemoved: removed, replicasRemaining: remaining }
|
|
19916
|
+
);
|
|
19917
|
+
}
|
|
19918
|
+
if (error instanceof CLIError) throw error;
|
|
19919
|
+
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);
|
|
19920
|
+
} finally {
|
|
19921
|
+
await Promise.all(opened.map(async ({ path, store, lease }) => {
|
|
19922
|
+
if (heldLeases?.has(path) !== true) await store.releaseLease(lease).catch(() => void 0);
|
|
19923
|
+
await store.close().catch(() => void 0);
|
|
19924
|
+
}));
|
|
19925
|
+
}
|
|
19926
|
+
});
|
|
19927
|
+
}
|
|
19928
|
+
async function acquireLogoutLeases(profile) {
|
|
19929
|
+
return withProfileLock4(profile, async () => {
|
|
19930
|
+
let sqliteLoaded = false;
|
|
19931
|
+
const opened = [];
|
|
19932
|
+
try {
|
|
19933
|
+
const legacyRoot = join8(profilePath3(profile), "replicas");
|
|
19934
|
+
if (await safeReplicaRoot(profile, legacyRoot) !== null) {
|
|
19935
|
+
const entries = (await readdir4(legacyRoot, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
19936
|
+
for (const entry of entries) {
|
|
19937
|
+
const path = await safeReplicaEntry(profile, legacyRoot, entry);
|
|
19938
|
+
if (!sqliteLoaded) {
|
|
19939
|
+
await loadSqlite();
|
|
19940
|
+
sqliteLoaded = true;
|
|
19941
|
+
}
|
|
19942
|
+
const store = await SqliteReplicaStore.open(path, { create: false, guard: async (action) => withProfileLock4(profile, action, { requireProfile: true }) });
|
|
19943
|
+
if (await store.open() === null) {
|
|
19944
|
+
await store.close();
|
|
19945
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
|
|
19946
|
+
}
|
|
19947
|
+
const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
19948
|
+
if (lease === null) {
|
|
19949
|
+
await store.close();
|
|
19950
|
+
throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
|
|
19951
|
+
}
|
|
19952
|
+
opened.push({ path, store, lease });
|
|
19953
|
+
}
|
|
19954
|
+
}
|
|
19955
|
+
const replicationRoot = join8(profilePath3(profile), "replication");
|
|
19956
|
+
const replicationReal = await safeReplicaRoot(profile, replicationRoot);
|
|
19957
|
+
if (replicationReal !== null) {
|
|
19958
|
+
const partitions = await readdir4(replicationRoot, { withFileTypes: true });
|
|
19959
|
+
for (const partition of partitions) {
|
|
19960
|
+
const partitionPath = join8(replicationRoot, partition.name);
|
|
19961
|
+
const partitionStats = await lstat2(partitionPath);
|
|
19962
|
+
if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
|
|
19963
|
+
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
|
|
19964
|
+
}
|
|
19965
|
+
if (!partitionStats.isDirectory()) {
|
|
19966
|
+
if (!partitionStats.isFile()) throw refuseUnsafeReplicaPath(profile, `replication path "${partition.name}" is not a regular file or directory`);
|
|
19967
|
+
continue;
|
|
19968
|
+
}
|
|
19969
|
+
const partitionReal = await realpath(partitionPath);
|
|
19970
|
+
if (!partitionReal.startsWith(replicationReal + sep)) {
|
|
19971
|
+
throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the replication root`);
|
|
19972
|
+
}
|
|
19973
|
+
const replicasRoot2 = join8(partitionPath, "replicas");
|
|
19974
|
+
const replicasStats = await lstat2(replicasRoot2).catch((error) => {
|
|
19975
|
+
if (error.code === "ENOENT") return null;
|
|
19976
|
+
throw error;
|
|
19977
|
+
});
|
|
19978
|
+
if (!replicasStats) continue;
|
|
19979
|
+
if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
|
|
19980
|
+
throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot2}" is not a real directory`);
|
|
19981
|
+
}
|
|
19982
|
+
for (const replica of await readdir4(replicasRoot2, { withFileTypes: true })) {
|
|
19983
|
+
const path = join8(replicasRoot2, replica.name);
|
|
19984
|
+
const stats = await lstat2(path);
|
|
19985
|
+
if (!replica.isDirectory() || replica.isSymbolicLink() || stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
19986
|
+
throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
|
|
19987
|
+
}
|
|
19988
|
+
const database = await lstat2(join8(path, "replica.db")).catch((error) => {
|
|
19989
|
+
if (error.code === "ENOENT") return null;
|
|
19990
|
+
throw error;
|
|
19991
|
+
});
|
|
19992
|
+
if (!database) continue;
|
|
19993
|
+
if (database.isSymbolicLink() || !database.isFile()) {
|
|
19994
|
+
throw refuseUnsafeReplicaPath(profile, `replication database "${replica.name}" is not a regular file`);
|
|
19995
|
+
}
|
|
19996
|
+
const replicaReal = await realpath(path);
|
|
19997
|
+
if (!replicaReal.startsWith(replicationReal + sep)) {
|
|
19998
|
+
throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" resolves outside the replication root`);
|
|
19999
|
+
}
|
|
20000
|
+
if (!sqliteLoaded) {
|
|
20001
|
+
await loadSqlite();
|
|
20002
|
+
sqliteLoaded = true;
|
|
20003
|
+
}
|
|
20004
|
+
const store = await SqliteReplicaStore.open(path, { create: false, guard: async (action) => withProfileLock4(profile, action, { requireProfile: true }) });
|
|
20005
|
+
if (await store.open() === null) {
|
|
20006
|
+
await store.close();
|
|
20007
|
+
throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "replication/${partition.name}/${replica.name}" has no initialized store.`, ExitCode.ERROR);
|
|
20008
|
+
}
|
|
20009
|
+
const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
|
|
20010
|
+
if (lease === null) {
|
|
20011
|
+
await store.close();
|
|
20012
|
+
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);
|
|
20013
|
+
}
|
|
20014
|
+
opened.push({ path, store, lease });
|
|
20015
|
+
}
|
|
20016
|
+
}
|
|
20017
|
+
}
|
|
20018
|
+
return opened;
|
|
20019
|
+
} catch (error) {
|
|
19321
20020
|
await Promise.all(opened.map(async ({ store, lease }) => {
|
|
19322
|
-
|
|
20021
|
+
await store.releaseLease(lease).catch(() => void 0);
|
|
20022
|
+
await store.close().catch(() => void 0);
|
|
20023
|
+
}));
|
|
20024
|
+
throw error;
|
|
20025
|
+
}
|
|
20026
|
+
});
|
|
20027
|
+
}
|
|
20028
|
+
async function removeProfileReplicasAndReplication(profile) {
|
|
20029
|
+
return withProfileLock4(profile, async () => {
|
|
20030
|
+
const acquired = await acquireLogoutLeases(profile);
|
|
20031
|
+
const heldLeases = new Map(acquired.map(({ path, lease }) => [path, lease]));
|
|
20032
|
+
let legacyRemoved = [];
|
|
20033
|
+
try {
|
|
20034
|
+
legacyRemoved = await removeProfileReplicas(profile, heldLeases);
|
|
20035
|
+
const replication = await removeProfileReplication(profile, heldLeases);
|
|
20036
|
+
return [...legacyRemoved, ...replication];
|
|
20037
|
+
} catch (error) {
|
|
20038
|
+
const partialRemoved = error instanceof CLIError && Array.isArray(error.metadata?.replicasRemoved) ? error.metadata.replicasRemoved.filter((name) => typeof name === "string") : [];
|
|
20039
|
+
const removed = [.../* @__PURE__ */ new Set([...legacyRemoved, ...partialRemoved])];
|
|
20040
|
+
const removedSet = new Set(removed);
|
|
20041
|
+
const remaining = acquired.map(({ path }) => {
|
|
20042
|
+
const name = relative(profilePath3(profile), path).split(sep).join("/");
|
|
20043
|
+
return name.startsWith("replication/") ? name.replace("/replicas/", "/") : name.split("/").at(-1);
|
|
20044
|
+
}).filter((name) => !removedSet.has(name));
|
|
20045
|
+
throw new CLIError(
|
|
20046
|
+
error instanceof CLIError ? error.code : "REPLICA_PURGE_FAILED",
|
|
20047
|
+
`Logout cleared the session, but replica cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}.`,
|
|
20048
|
+
error instanceof CLIError ? error.exitCode : ExitCode.ERROR,
|
|
20049
|
+
{ replicasRemoved: removed, replicasRemaining: remaining }
|
|
20050
|
+
);
|
|
20051
|
+
} finally {
|
|
20052
|
+
await Promise.all(acquired.map(async ({ store, lease }) => {
|
|
20053
|
+
await store.releaseLease(lease).catch(() => void 0);
|
|
19323
20054
|
await store.close().catch(() => void 0);
|
|
19324
20055
|
}));
|
|
19325
20056
|
}
|
|
@@ -19648,6 +20379,7 @@ import {
|
|
|
19648
20379
|
} from "crypto";
|
|
19649
20380
|
|
|
19650
20381
|
// src/auth/local-key.ts
|
|
20382
|
+
init_replication_config();
|
|
19651
20383
|
import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
|
|
19652
20384
|
import { PrivateKeySigner } from "@tinycloud/node-sdk";
|
|
19653
20385
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
@@ -19693,10 +20425,18 @@ async function generateLocalIdentity(chainId = 1) {
|
|
|
19693
20425
|
}
|
|
19694
20426
|
async function localKeySignIn(options) {
|
|
19695
20427
|
const { TinyCloudNode: TinyCloudNode3 } = await import("@tinycloud/node-sdk");
|
|
20428
|
+
const profile = options.profile ?? "default";
|
|
20429
|
+
const replication = createCliReplicationConfig(
|
|
20430
|
+
profile,
|
|
20431
|
+
options.replication,
|
|
20432
|
+
{ debug: false, quiet: true },
|
|
20433
|
+
options.replication !== void 0
|
|
20434
|
+
);
|
|
19696
20435
|
const node = new TinyCloudNode3({
|
|
19697
20436
|
privateKey: options.privateKey,
|
|
19698
20437
|
host: options.host,
|
|
19699
|
-
autoCreateSpace: true
|
|
20438
|
+
autoCreateSpace: true,
|
|
20439
|
+
...replication ? { replication } : {}
|
|
19700
20440
|
});
|
|
19701
20441
|
await node.signIn();
|
|
19702
20442
|
const address = await new PrivateKeySigner(options.privateKey).getAddress();
|
|
@@ -19826,6 +20566,15 @@ function sessionExpiresAt(session) {
|
|
|
19826
20566
|
function ownerSpaceId(space, ownerDid) {
|
|
19827
20567
|
return space.startsWith("tinycloud:") ? space : `tinycloud:${ownerDid.slice("did:".length)}:${space}`;
|
|
19828
20568
|
}
|
|
20569
|
+
function sameLoginSpace(left, right, ownerDid) {
|
|
20570
|
+
const normalizedLeft = normalizePkhIdentifier(ownerDid === void 0 ? left : ownerSpaceId(left, ownerDid));
|
|
20571
|
+
const normalizedRight = normalizePkhIdentifier(ownerDid === void 0 ? right : ownerSpaceId(right, ownerDid));
|
|
20572
|
+
if (normalizedLeft === normalizedRight) return true;
|
|
20573
|
+
if (ownerDid !== void 0) return false;
|
|
20574
|
+
if (left.startsWith("tinycloud:") && !right.startsWith("tinycloud:")) return left.split(":").at(-1) === right;
|
|
20575
|
+
if (right.startsWith("tinycloud:") && !left.startsWith("tinycloud:")) return right.split(":").at(-1) === left;
|
|
20576
|
+
return false;
|
|
20577
|
+
}
|
|
19829
20578
|
function permissionTuples(permissions, ownerDid) {
|
|
19830
20579
|
return new Set(permissions.flatMap((permission) => actionTuples(permission, ownerDid)));
|
|
19831
20580
|
}
|
|
@@ -20091,11 +20840,11 @@ function assertSessionReplaceable(profileName2, snapshot, ownerDid, scope, newEx
|
|
|
20091
20840
|
ExitCode.USAGE_ERROR
|
|
20092
20841
|
);
|
|
20093
20842
|
}
|
|
20094
|
-
async function restore(profileName2,
|
|
20843
|
+
async function restore(profileName2, state2) {
|
|
20095
20844
|
const writes = [
|
|
20096
|
-
() =>
|
|
20097
|
-
() =>
|
|
20098
|
-
() =>
|
|
20845
|
+
() => state2.key === null ? ProfileManager.removeKey(profileName2) : ProfileManager.setKey(profileName2, state2.key),
|
|
20846
|
+
() => state2.session === null ? ProfileManager.clearSession(profileName2) : ProfileManager.setSession(profileName2, state2.session),
|
|
20847
|
+
() => state2.profile === null ? ProfileManager.removeProfileConfig(profileName2) : ProfileManager.setProfile(profileName2, state2.profile)
|
|
20099
20848
|
];
|
|
20100
20849
|
const failures = [];
|
|
20101
20850
|
for (const write of writes) {
|
|
@@ -21017,6 +21766,79 @@ init_session_expired();
|
|
|
21017
21766
|
init_delegation_expiry();
|
|
21018
21767
|
init_space();
|
|
21019
21768
|
init_permissions();
|
|
21769
|
+
|
|
21770
|
+
// src/auth/replication-login.ts
|
|
21771
|
+
import { grantPathsForSelector, isCapabilitySubset } from "@tinycloud/sdk-core";
|
|
21772
|
+
init_errors();
|
|
21773
|
+
init_constants();
|
|
21774
|
+
function unconstrained(caveats) {
|
|
21775
|
+
return (caveats ?? []).every((caveat) => Object.keys(caveat).length === 0);
|
|
21776
|
+
}
|
|
21777
|
+
function addReplicationLoginEntries(request, primarySpace, options) {
|
|
21778
|
+
const result = [...request];
|
|
21779
|
+
for (const prefix of options.prefixes) {
|
|
21780
|
+
if (requiresSecretsOptIn(primarySpace, prefix) && options.allowSecrets !== true) {
|
|
21781
|
+
throw new CLIError("SECRETS_OPT_IN_REQUIRED", `Replication prefix ${JSON.stringify(prefix)} in ${primarySpace} requires --replication-allow-secrets.`, ExitCode.USAGE_ERROR);
|
|
21782
|
+
}
|
|
21783
|
+
const requested = {
|
|
21784
|
+
service: "tinycloud.kv",
|
|
21785
|
+
space: primarySpace,
|
|
21786
|
+
path: prefix,
|
|
21787
|
+
actions: ["tinycloud.kv/get"]
|
|
21788
|
+
};
|
|
21789
|
+
const required = grantPathsForSelector(prefix).map((path) => ({ ...requested, path }));
|
|
21790
|
+
const matching = request.filter(
|
|
21791
|
+
(entry) => sameLoginSpace(entry.space ?? "", primarySpace, options.ownerDid)
|
|
21792
|
+
);
|
|
21793
|
+
const authority = matching.map((entry) => {
|
|
21794
|
+
const { caveats: _caveats, ...grant } = entry;
|
|
21795
|
+
return { ...grant, space: primarySpace };
|
|
21796
|
+
});
|
|
21797
|
+
if (!isCapabilitySubset(required, authority).subset) {
|
|
21798
|
+
const caveatedAtRequestedPath = matching.filter((entry) => !unconstrained(entry.caveats)).map((entry) => {
|
|
21799
|
+
const { caveats: _caveats, ...grant } = entry;
|
|
21800
|
+
return { ...grant, space: primarySpace };
|
|
21801
|
+
});
|
|
21802
|
+
if (isCapabilitySubset([requested], caveatedAtRequestedPath).subset) {
|
|
21803
|
+
throw new CLIError("REPLICATION_PREFIX_CAVEATED", `Replication prefix ${JSON.stringify(prefix)} is covered only by caveated get authority.`, ExitCode.USAGE_ERROR);
|
|
21804
|
+
}
|
|
21805
|
+
throw new CLIError("REPLICATION_PREFIX_OUTSIDE_SCOPE", `Replication prefix ${JSON.stringify(prefix)} has no covering get in the login request.`, ExitCode.USAGE_ERROR);
|
|
21806
|
+
}
|
|
21807
|
+
const unrestricted = matching.filter((entry) => unconstrained(entry.caveats)).map((entry) => {
|
|
21808
|
+
const { caveats: _caveats, ...grant } = entry;
|
|
21809
|
+
return { ...grant, space: primarySpace };
|
|
21810
|
+
});
|
|
21811
|
+
if (!isCapabilitySubset(required, unrestricted).subset) {
|
|
21812
|
+
throw new CLIError("REPLICATION_PREFIX_CAVEATED", `Replication prefix ${JSON.stringify(prefix)} is covered only by caveated get authority.`, ExitCode.USAGE_ERROR);
|
|
21813
|
+
}
|
|
21814
|
+
const service = "tinycloud.kv";
|
|
21815
|
+
const actions = [`${service}/get`, `${service}/sync`];
|
|
21816
|
+
for (const path of grantPathsForSelector(prefix)) {
|
|
21817
|
+
const existing = result.find((entry) => entry.service === service && entry.space === primarySpace && entry.path === path && actions.every((action) => entry.actions.includes(action)) && unconstrained(entry.caveats));
|
|
21818
|
+
if (!existing) result.push({ service, space: primarySpace, path, actions });
|
|
21819
|
+
}
|
|
21820
|
+
}
|
|
21821
|
+
return result;
|
|
21822
|
+
}
|
|
21823
|
+
function buildReplicationLoginRequest(request, options) {
|
|
21824
|
+
if (options.prefixes.length === 0) return request;
|
|
21825
|
+
if (request === void 0) {
|
|
21826
|
+
const space = options.ownerDid === void 0 ? "default" : ownerSpaceId("default", options.ownerDid);
|
|
21827
|
+
const defaults = options.ownerDid === void 0 ? [
|
|
21828
|
+
{ service: "tinycloud.kv", space, path: "", actions: ["tinycloud.kv/put", "tinycloud.kv/get", "tinycloud.kv/del", "tinycloud.kv/list", "tinycloud.kv/metadata"] },
|
|
21829
|
+
{ service: "tinycloud.sql", space, path: "", actions: ["tinycloud.sql/read", "tinycloud.sql/write", "tinycloud.sql/admin"] },
|
|
21830
|
+
{ service: "tinycloud.capabilities", space, path: "", actions: ["tinycloud.capabilities/read"] }
|
|
21831
|
+
] : ownerLoginPermissions(options.ownerDid);
|
|
21832
|
+
return addReplicationLoginEntries(defaults, space, options);
|
|
21833
|
+
}
|
|
21834
|
+
const primary = request.find((entry) => entry.service === "tinycloud.kv" && entry.space !== void 0)?.space ?? request.find((entry) => entry.space !== void 0)?.space;
|
|
21835
|
+
if (!primary) {
|
|
21836
|
+
throw new CLIError("INVALID_LOGIN_SCOPE", "Replication login needs a primary space in the requested manifest.", ExitCode.USAGE_ERROR);
|
|
21837
|
+
}
|
|
21838
|
+
return addReplicationLoginEntries(request, primary, options);
|
|
21839
|
+
}
|
|
21840
|
+
|
|
21841
|
+
// src/commands/auth.ts
|
|
21020
21842
|
function resolveOpenKeyHost(profile) {
|
|
21021
21843
|
return process.env.TC_OPENKEY_HOST ?? profile.openkeyHost ?? DEFAULT_OPENKEY_HOST;
|
|
21022
21844
|
}
|
|
@@ -21049,7 +21871,7 @@ async function promptAuthMethod() {
|
|
|
21049
21871
|
function registerAuthCommand(program, options) {
|
|
21050
21872
|
const auth = program.command("auth").description("Authentication management");
|
|
21051
21873
|
registerAuthGrantsCommand(auth, { openKeyAcquisition: options?.openKeyAcquisition });
|
|
21052
|
-
auth.command("login").description("Authenticate with TinyCloud").option("--device", "Approve on another device (e.g. a phone) through OpenKey device authorization; requires a KV-scoped --manifest (SQL and secret decrypt need browser or --paste login)").option("--paste", "Use manual paste mode instead of browser callback").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--method <method>", "Authentication method: local or openkey").option("--manifest <fileOrBase64>", `Request only this manifest's permissions (one space, plus raw encryption network entries such as a secrets decrypt grant); ${SHARE_PUBLISHING_MANIFEST_REF} covers \`tc share publish\``).option("--expiry <duration>", "OpenKey session lifetime, e.g. 1h or 7d (device login: at most 30d, default 30d)").option("--owner <did>", "Sign in as this owner (did:pkh:eip155:CHAIN:ADDRESS): OpenKey preselects that key, e.g. one that is not the account's primary key, and an approval by any other identity is refused. With --manifest, also names the secrets owner for a manifest's `secrets` on a profile with no recorded owner").option("--replace-session", "Scoped or device login: replace this profile's live session even though the new scope would narrow, change or shorten it (prefer a new profile)").action(async (options2, cmd) => {
|
|
21874
|
+
auth.command("login").description("Authenticate with TinyCloud").option("--device", "Approve on another device (e.g. a phone) through OpenKey device authorization; requires a KV-scoped --manifest (SQL and secret decrypt need browser or --paste login)").option("--paste", "Use manual paste mode instead of browser callback").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--method <method>", "Authentication method: local or openkey").option("--manifest <fileOrBase64>", `Request only this manifest's permissions (one space, plus raw encryption network entries such as a secrets decrypt grant); ${SHARE_PUBLISHING_MANIFEST_REF} covers \`tc share publish\``).option("--expiry <duration>", "OpenKey session lifetime, e.g. 1h or 7d (device login: at most 30d, default 30d)").option("--owner <did>", "Sign in as this owner (did:pkh:eip155:CHAIN:ADDRESS): OpenKey preselects that key, e.g. one that is not the account's primary key, and an approval by any other identity is refused. With --manifest, also names the secrets owner for a manifest's `secrets` on a profile with no recorded owner").option("--replace-session", "Scoped or device login: replace this profile's live session even though the new scope would narrow, change or shorten it (prefer a new profile)").option("--replication-prefix <prefix>", "Add replication get+sync authority for this prefix (repeatable)", (prefix, values = []) => [...values, prefix]).option("--replication-allow-secrets", "Allow replication of secret material (network-wide decrypt warning)").action(async (options2, cmd) => {
|
|
21053
21875
|
try {
|
|
21054
21876
|
if (options2.device && options2.paste) {
|
|
21055
21877
|
throw new CLIError("INVALID_ARGUMENT", "--device and --paste are mutually exclusive.", ExitCode.USAGE_ERROR);
|
|
@@ -21069,19 +21891,31 @@ function registerAuthCommand(program, options) {
|
|
|
21069
21891
|
const ctx = await ProfileManager.resolveContext(globalOpts, { storedSession: "unused" });
|
|
21070
21892
|
const owner = options2.owner === void 0 ? void 0 : canonicalOwnerDid(options2.owner);
|
|
21071
21893
|
const permissions = options2.manifest ? await loadManifestPermissions(options2.manifest, ctx.profile, { allowLogicalSpaces: true, ownerDid: owner, device: options2.device === true }) : void 0;
|
|
21894
|
+
const profileConfig = await ProfileManager.getProfile(ctx.profile).catch((error) => {
|
|
21895
|
+
if (error instanceof CLIError && error.code === "PROFILE_NOT_FOUND") return void 0;
|
|
21896
|
+
throw error;
|
|
21897
|
+
});
|
|
21898
|
+
const replicationOptions = {
|
|
21899
|
+
prefixes: options2.replicationPrefix ?? profileConfig?.replication?.prefixes ?? [],
|
|
21900
|
+
...options2.replicationAllowSecrets === true || profileConfig?.replication?.allowSecrets === true ? { allowSecrets: true } : {},
|
|
21901
|
+
...owner === void 0 ? {} : { ownerDid: owner }
|
|
21902
|
+
};
|
|
21903
|
+
const loginPermissions = permissions === void 0 ? void 0 : buildReplicationLoginRequest(permissions, replicationOptions);
|
|
21072
21904
|
const persistHost = globalOpts.host !== void 0;
|
|
21073
21905
|
if (options2.device) {
|
|
21074
21906
|
const { profile, result } = await loginWithDeviceAuthorization({
|
|
21075
21907
|
profileName: ctx.profile,
|
|
21076
21908
|
nodeOrigin: ctx.host,
|
|
21077
21909
|
shareOrigin: DEFAULT_SHARE_ORIGIN,
|
|
21078
|
-
permissions,
|
|
21910
|
+
permissions: loginPermissions,
|
|
21079
21911
|
...options2.expiry === void 0 ? {} : { expiry: parseRequestedExpiry(parseExpiryOption(options2.expiry)) },
|
|
21080
21912
|
reason: "Allow this TinyCloud CLI profile to use the permissions in the requested manifest.",
|
|
21081
21913
|
expectedOwner: owner,
|
|
21082
21914
|
replaceSession: options2.replaceSession === true,
|
|
21083
21915
|
persistHost
|
|
21084
21916
|
});
|
|
21917
|
+
await persistReplicationLogin(ctx.profile, replicationOptions);
|
|
21918
|
+
if (replicationOptions.allowSecrets) writeReplicationSecretsWarning();
|
|
21085
21919
|
reportDeclined(result.declined);
|
|
21086
21920
|
outputJson({
|
|
21087
21921
|
authenticated: true,
|
|
@@ -21120,20 +21954,26 @@ function registerAuthCommand(program, options) {
|
|
|
21120
21954
|
);
|
|
21121
21955
|
}
|
|
21122
21956
|
if (method === "local") {
|
|
21123
|
-
|
|
21957
|
+
buildReplicationLoginRequest(void 0, replicationOptions);
|
|
21958
|
+
await handleLocalAuth(ctx.profile, ctx.host, { replication: replicationOptions });
|
|
21959
|
+
await persistReplicationLogin(ctx.profile, replicationOptions);
|
|
21960
|
+
if (replicationOptions.allowSecrets) writeReplicationSecretsWarning();
|
|
21124
21961
|
} else {
|
|
21125
21962
|
await handleOpenKeyAuth(ctx.profile, ctx.host, {
|
|
21126
21963
|
paste: options2.paste,
|
|
21127
21964
|
noPopup: options2.popup === false,
|
|
21128
|
-
permissions,
|
|
21965
|
+
permissions: loginPermissions,
|
|
21966
|
+
replication: replicationOptions,
|
|
21129
21967
|
expiry: parseExpiryOption(options2.expiry),
|
|
21130
21968
|
expectedOwner: owner,
|
|
21131
21969
|
replaceSession: options2.replaceSession === true,
|
|
21132
21970
|
persistHost
|
|
21133
21971
|
});
|
|
21972
|
+
await persistReplicationLogin(ctx.profile, replicationOptions);
|
|
21973
|
+
if (replicationOptions.allowSecrets) writeReplicationSecretsWarning();
|
|
21134
21974
|
}
|
|
21135
21975
|
} catch (error) {
|
|
21136
|
-
handleError(error);
|
|
21976
|
+
return handleError(error);
|
|
21137
21977
|
}
|
|
21138
21978
|
});
|
|
21139
21979
|
auth.command("logout").description("Clear the local session and remove local replicas. Logout is local; delegations and grants on the node stay valid until they expire. Use `tc delegation revoke <cid>` to end access now.").option("--keep-replicas", "Keep this profile's local replicas").action(async (options2, cmd) => {
|
|
@@ -21142,7 +21982,7 @@ function registerAuthCommand(program, options) {
|
|
|
21142
21982
|
const ctx = await ProfileManager.resolveContext(globalOpts, { storedSession: "unused" });
|
|
21143
21983
|
await ProfileManager.getProfile(ctx.profile);
|
|
21144
21984
|
await ProfileManager.clearSession(ctx.profile);
|
|
21145
|
-
const replicasRemoved = options2.keepReplicas ? [] : await
|
|
21985
|
+
const replicasRemoved = options2.keepReplicas ? [] : await removeProfileReplicasAndReplication(ctx.profile);
|
|
21146
21986
|
outputJson({
|
|
21147
21987
|
profile: ctx.profile,
|
|
21148
21988
|
authenticated: false,
|
|
@@ -21151,7 +21991,7 @@ function registerAuthCommand(program, options) {
|
|
|
21151
21991
|
warning: "Logout is local; delegations and grants on the node stay valid until they expire. Use `tc delegation revoke <cid>` to end access now."
|
|
21152
21992
|
});
|
|
21153
21993
|
} catch (error) {
|
|
21154
|
-
handleError(error);
|
|
21994
|
+
return handleError(error);
|
|
21155
21995
|
}
|
|
21156
21996
|
});
|
|
21157
21997
|
auth.command("rotate").description("Rotate the active profile session key").option("--paste", "Use manual paste mode instead of browser callback").option("--no-popup", "Print the OpenKey URL without opening a browser").action(async (options2, cmd) => {
|
|
@@ -21163,7 +22003,7 @@ function registerAuthCommand(program, options) {
|
|
|
21163
22003
|
noPopup: options2.popup === false
|
|
21164
22004
|
});
|
|
21165
22005
|
} catch (error) {
|
|
21166
|
-
handleError(error);
|
|
22006
|
+
return handleError(error);
|
|
21167
22007
|
}
|
|
21168
22008
|
});
|
|
21169
22009
|
auth.command("status").description("Show current authentication state").action(async (_options, cmd) => {
|
|
@@ -21214,7 +22054,7 @@ function registerAuthCommand(program, options) {
|
|
|
21214
22054
|
process.stdout.write(formatField("Has Key", hasKey !== null) + "\n");
|
|
21215
22055
|
}
|
|
21216
22056
|
} catch (error) {
|
|
21217
|
-
handleError(error);
|
|
22057
|
+
return handleError(error);
|
|
21218
22058
|
}
|
|
21219
22059
|
});
|
|
21220
22060
|
auth.command("request").description("Create a TinyCloud permission request artifact").option(
|
|
@@ -21367,7 +22207,7 @@ function registerAuthCommand(program, options) {
|
|
|
21367
22207
|
expiry
|
|
21368
22208
|
});
|
|
21369
22209
|
} catch (error) {
|
|
21370
|
-
handleError(error);
|
|
22210
|
+
return handleError(error);
|
|
21371
22211
|
}
|
|
21372
22212
|
});
|
|
21373
22213
|
auth.command("import [source]").description("Import a TinyCloud delegation or permission request artifact").option("--stdin", "Read the JSON artifact from stdin").option("--paste", "Read the JSON artifact from stdin").action(async (source, options2, cmd) => {
|
|
@@ -21508,7 +22348,7 @@ function registerAuthCommand(program, options) {
|
|
|
21508
22348
|
expiry: importExpiry.toISOString()
|
|
21509
22349
|
});
|
|
21510
22350
|
} catch (error) {
|
|
21511
|
-
handleError(error);
|
|
22351
|
+
return handleError(error);
|
|
21512
22352
|
}
|
|
21513
22353
|
});
|
|
21514
22354
|
auth.command("grant [request]").description("Grant a TinyCloud permission request artifact to its requester").option("--stdin", "Read the JSON request artifact from stdin").option("--paste", "Read the JSON request artifact from stdin").option("--yes", "Skip local-key TTY confirmation", false).action(async (source, options2, cmd) => {
|
|
@@ -21549,7 +22389,7 @@ function registerAuthCommand(program, options) {
|
|
|
21549
22389
|
});
|
|
21550
22390
|
outputJson(grant);
|
|
21551
22391
|
} catch (error) {
|
|
21552
|
-
handleError(error);
|
|
22392
|
+
return handleError(error);
|
|
21553
22393
|
}
|
|
21554
22394
|
});
|
|
21555
22395
|
auth.command("retry [requestId]").description("Check whether a stored permission request is now satisfied").option("--last", "Use the latest stored permission request for this profile").option("--exec", "Run the captured command when the request is covered").action(async (requestId, options2, cmd) => {
|
|
@@ -21592,7 +22432,7 @@ function registerAuthCommand(program, options) {
|
|
|
21592
22432
|
command: isPermissionRequestArtifact(artifact) ? artifact.command ?? null : null
|
|
21593
22433
|
});
|
|
21594
22434
|
} catch (error) {
|
|
21595
|
-
handleError(error);
|
|
22435
|
+
return handleError(error);
|
|
21596
22436
|
}
|
|
21597
22437
|
});
|
|
21598
22438
|
auth.command("caps").description("Show granted capabilities for the active session").option("--diff <spec>", "Show missing capabilities for a spec").option("--history", "Show recent permission grants").action(async (options2, cmd) => {
|
|
@@ -21650,7 +22490,7 @@ function registerAuthCommand(program, options) {
|
|
|
21650
22490
|
) + "\n");
|
|
21651
22491
|
}
|
|
21652
22492
|
} catch (error) {
|
|
21653
|
-
handleError(error);
|
|
22493
|
+
return handleError(error);
|
|
21654
22494
|
}
|
|
21655
22495
|
});
|
|
21656
22496
|
auth.command("whoami").description("Show identity information").action(async (_options, cmd) => {
|
|
@@ -21691,13 +22531,13 @@ function registerAuthCommand(program, options) {
|
|
|
21691
22531
|
process.stdout.write(formatField("Authenticated", authenticated) + "\n");
|
|
21692
22532
|
}
|
|
21693
22533
|
} catch (error) {
|
|
21694
|
-
handleError(error);
|
|
22534
|
+
return handleError(error);
|
|
21695
22535
|
}
|
|
21696
22536
|
});
|
|
21697
22537
|
}
|
|
21698
22538
|
async function emitPermissionRequestArtifact(artifact, emitOption) {
|
|
21699
22539
|
if (typeof emitOption === "string" && emitOption.length > 0) {
|
|
21700
|
-
await mkdir3(
|
|
22540
|
+
await mkdir3(dirname3(emitOption), { recursive: true });
|
|
21701
22541
|
await writeFile2(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
21702
22542
|
outputJson({
|
|
21703
22543
|
emitted: true,
|
|
@@ -22178,8 +23018,13 @@ async function rotateAuthKey(profileName2, host, options = {}) {
|
|
|
22178
23018
|
const { jwk, did } = await withSpinner("Generating session key...", async () => {
|
|
22179
23019
|
return generateKey();
|
|
22180
23020
|
});
|
|
23021
|
+
let permissions;
|
|
22181
23022
|
await ProfileManager.withLock(profileName2, async () => {
|
|
22182
23023
|
const current = await ProfileManager.getProfile(profileName2);
|
|
23024
|
+
if (current.replication?.prefixes.length) {
|
|
23025
|
+
const previousSession = await ProfileManager.getSession(profileName2);
|
|
23026
|
+
if (Array.isArray(previousSession?.permissions)) permissions = previousSession.permissions;
|
|
23027
|
+
}
|
|
22183
23028
|
await ProfileManager.setKey(profileName2, jwk);
|
|
22184
23029
|
await ProfileManager.clearSession(profileName2);
|
|
22185
23030
|
await ProfileManager.setProfile(profileName2, {
|
|
@@ -22194,7 +23039,8 @@ async function rotateAuthKey(profileName2, host, options = {}) {
|
|
|
22194
23039
|
});
|
|
22195
23040
|
const result = await refreshOpenKeySession(profileName2, host, {
|
|
22196
23041
|
paste: options.paste,
|
|
22197
|
-
noPopup: options.noPopup
|
|
23042
|
+
noPopup: options.noPopup,
|
|
23043
|
+
permissions
|
|
22198
23044
|
});
|
|
22199
23045
|
outputRotationResult(result.profile, profileName2, oldDid, "openkey");
|
|
22200
23046
|
}
|
|
@@ -22241,7 +23087,7 @@ async function handleLocalAuth(profileName2, host, options = {}) {
|
|
|
22241
23087
|
let address;
|
|
22242
23088
|
let did;
|
|
22243
23089
|
let sessionDid = profile?.sessionDid;
|
|
22244
|
-
if ((profile
|
|
23090
|
+
if (profile && (profile.authMethod === "local" || posture === "local-owner-key") && profile.privateKey) {
|
|
22245
23091
|
privateKey = profile.privateKey;
|
|
22246
23092
|
address = profile.address ?? await deriveAddress(privateKey);
|
|
22247
23093
|
did = profile.did.startsWith("did:pkh:") ? profile.did : addressToDID(address, profile.chainId ?? DEFAULT_CHAIN_ID);
|
|
@@ -22275,7 +23121,12 @@ async function handleLocalAuth(profileName2, host, options = {}) {
|
|
|
22275
23121
|
sessionDid ??= keyToDID(hasKey);
|
|
22276
23122
|
}
|
|
22277
23123
|
const sessionResult = await withSpinner("Signing in...", async () => {
|
|
22278
|
-
|
|
23124
|
+
const replication = options.replication ?? profile?.replication;
|
|
23125
|
+
return localKeySignIn({
|
|
23126
|
+
privateKey,
|
|
23127
|
+
host,
|
|
23128
|
+
...replication?.prefixes.length ? { profile: profileName2, replication } : {}
|
|
23129
|
+
});
|
|
22279
23130
|
});
|
|
22280
23131
|
const session = {
|
|
22281
23132
|
authMethod: "local",
|
|
@@ -22353,6 +23204,22 @@ ${declined.map((permission) => ` ${compactPermission(permission)}`).join("\n")}
|
|
|
22353
23204
|
`);
|
|
22354
23205
|
}
|
|
22355
23206
|
}
|
|
23207
|
+
async function persistReplicationLogin(profileName2, settings) {
|
|
23208
|
+
await ProfileManager.updateProfile(profileName2, (profile) => {
|
|
23209
|
+
const { replication: _previous, ...rest } = profile;
|
|
23210
|
+
return settings.prefixes.length === 0 ? rest : {
|
|
23211
|
+
...rest,
|
|
23212
|
+
replication: {
|
|
23213
|
+
prefixes: [...settings.prefixes],
|
|
23214
|
+
...settings.allowSecrets === true ? { allowSecrets: true } : {}
|
|
23215
|
+
}
|
|
23216
|
+
};
|
|
23217
|
+
});
|
|
23218
|
+
}
|
|
23219
|
+
function writeReplicationSecretsWarning() {
|
|
23220
|
+
process.stderr.write(`${theme.warn("Warning:")} replication copies encrypted secret material to this device. The allow-secrets option does not grant decrypt.
|
|
23221
|
+
`);
|
|
23222
|
+
}
|
|
22356
23223
|
async function refreshOpenKeySession(profileName2, host, options = {}) {
|
|
22357
23224
|
const snapshot = await readProfileSnapshot(profileName2);
|
|
22358
23225
|
const profile = snapshot.profile ?? await ProfileManager.getProfile(profileName2);
|
|
@@ -22364,14 +23231,22 @@ async function refreshOpenKeySession(profileName2, host, options = {}) {
|
|
|
22364
23231
|
ExitCode.AUTH_REQUIRED
|
|
22365
23232
|
);
|
|
22366
23233
|
}
|
|
22367
|
-
if (options.permissions !== void 0) validateLoginPermissions(options.permissions);
|
|
22368
|
-
const permissions = options.permissions === void 0 ? void 0 : scopedLoginPermissions(options.permissions);
|
|
22369
|
-
if (permissions !== void 0) {
|
|
22370
|
-
assertNotLocalOwner(profileName2, profile, "Scoped browser login");
|
|
22371
|
-
}
|
|
22372
23234
|
const expiry = options.expiry === void 0 ? void 0 : parseRequestedExpiry(options.expiry);
|
|
22373
23235
|
const openKeyExpiry = expiry === void 0 ? void 0 : openKeyExpiryParam(expiry);
|
|
22374
23236
|
const expectedOwner = expectedOwnerFor(profileName2, profile, options.expectedOwner);
|
|
23237
|
+
const replication = options.replication ?? profile.replication ?? { prefixes: [] };
|
|
23238
|
+
const previousSession = options.replication === void 0 && profile.replication?.prefixes.length ? await ProfileManager.getSession(profileName2) : null;
|
|
23239
|
+
const rememberedPermissions = Array.isArray(previousSession?.permissions) ? previousSession.permissions : void 0;
|
|
23240
|
+
const baseRequest = options.permissions ?? rememberedPermissions;
|
|
23241
|
+
const requested = buildReplicationLoginRequest(baseRequest, {
|
|
23242
|
+
...replication,
|
|
23243
|
+
...expectedOwner === void 0 ? {} : { ownerDid: expectedOwner }
|
|
23244
|
+
});
|
|
23245
|
+
if (requested !== void 0) validateLoginPermissions(requested);
|
|
23246
|
+
const permissions = requested === void 0 ? void 0 : scopedLoginPermissions(requested);
|
|
23247
|
+
if (permissions !== void 0) {
|
|
23248
|
+
assertNotLocalOwner(profileName2, profile, "Scoped browser login");
|
|
23249
|
+
}
|
|
22375
23250
|
if (permissions !== void 0 && options.replaceSession !== true) {
|
|
22376
23251
|
const estimatedExpiry = expiry === void 0 ? void 0 : new Date(Date.now() + expiry.durationMs).toISOString();
|
|
22377
23252
|
assertSessionReplaceable(profileName2, snapshot, expectedOwner, permissions, estimatedExpiry);
|
|
@@ -22500,6 +23375,9 @@ _tc() {
|
|
|
22500
23375
|
'(-v --verbose)'{-v,--verbose}'[Enable verbose output]' \\
|
|
22501
23376
|
'--no-cache[Disable caching]' \\
|
|
22502
23377
|
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]' \\
|
|
23378
|
+
'--replication[Enable read-through replication]' \\
|
|
23379
|
+
'--no-replication[Disable replication]' \\
|
|
23380
|
+
'--replication-debug[Write replication diagnostics to stderr]' \\
|
|
22503
23381
|
'1:command:->cmd' \\
|
|
22504
23382
|
'*::arg:->args'
|
|
22505
23383
|
|
|
@@ -22559,6 +23437,9 @@ complete -c tc -l host -s H -d "TinyCloud node URL"
|
|
|
22559
23437
|
complete -c tc -l verbose -s v -d "Enable verbose output"
|
|
22560
23438
|
complete -c tc -l no-cache -d "Disable caching"
|
|
22561
23439
|
complete -c tc -l quiet -s q -d "Suppress non-essential output"
|
|
23440
|
+
complete -c tc -l replication -d "Enable read-through replication"
|
|
23441
|
+
complete -c tc -l no-replication -d "Disable replication"
|
|
23442
|
+
complete -c tc -l replication-debug -d "Write replication diagnostics to stderr"
|
|
22562
23443
|
`;
|
|
22563
23444
|
}
|
|
22564
23445
|
|
|
@@ -22593,7 +23474,7 @@ function registerContextCommand(program) {
|
|
|
22593
23474
|
access: "not-tested"
|
|
22594
23475
|
});
|
|
22595
23476
|
} catch (error) {
|
|
22596
|
-
handleError(error);
|
|
23477
|
+
return handleError(error);
|
|
22597
23478
|
}
|
|
22598
23479
|
});
|
|
22599
23480
|
}
|
|
@@ -22642,7 +23523,7 @@ function registerDelegationCommand(program, configuration = {}) {
|
|
|
22642
23523
|
expiry: expiry.toISOString()
|
|
22643
23524
|
});
|
|
22644
23525
|
} catch (error) {
|
|
22645
|
-
handleError(error);
|
|
23526
|
+
return handleError(error);
|
|
22646
23527
|
}
|
|
22647
23528
|
});
|
|
22648
23529
|
delegation.command("list").description("List delegations").option("--granted", "Show only delegations I've granted").option("--received", "Show only delegations I've received").action(async (options, cmd) => {
|
|
@@ -22674,7 +23555,7 @@ function registerDelegationCommand(program, configuration = {}) {
|
|
|
22674
23555
|
count: delegations.length
|
|
22675
23556
|
});
|
|
22676
23557
|
} catch (error) {
|
|
22677
|
-
handleError(error);
|
|
23558
|
+
return handleError(error);
|
|
22678
23559
|
}
|
|
22679
23560
|
});
|
|
22680
23561
|
delegation.command("info <cid>").description("Get delegation details").action(async (cid, _options, cmd) => {
|
|
@@ -22688,7 +23569,7 @@ function registerDelegationCommand(program, configuration = {}) {
|
|
|
22688
23569
|
}
|
|
22689
23570
|
outputJson(result.data);
|
|
22690
23571
|
} catch (error) {
|
|
22691
|
-
handleError(error);
|
|
23572
|
+
return handleError(error);
|
|
22692
23573
|
}
|
|
22693
23574
|
});
|
|
22694
23575
|
delegation.command("revoke <cid>").description("Revoke a delegation").option("--yes", "Skip local-key TTY confirmation", false).action(async (cid, options, cmd) => {
|
|
@@ -22706,7 +23587,7 @@ function registerDelegationCommand(program, configuration = {}) {
|
|
|
22706
23587
|
openKeyAcquisition: configuration.openKeyAcquisition
|
|
22707
23588
|
}));
|
|
22708
23589
|
} catch (error) {
|
|
22709
|
-
handleError(error);
|
|
23590
|
+
return handleError(error);
|
|
22710
23591
|
}
|
|
22711
23592
|
});
|
|
22712
23593
|
}
|
|
@@ -22818,7 +23699,7 @@ function registerDoctorCommand(program) {
|
|
|
22818
23699
|
}
|
|
22819
23700
|
}
|
|
22820
23701
|
} catch (error) {
|
|
22821
|
-
handleError(error);
|
|
23702
|
+
return handleError(error);
|
|
22822
23703
|
}
|
|
22823
23704
|
});
|
|
22824
23705
|
}
|
|
@@ -22862,7 +23743,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
22862
23743
|
}
|
|
22863
23744
|
}
|
|
22864
23745
|
} catch (error) {
|
|
22865
|
-
handleError(error);
|
|
23746
|
+
return handleError(error);
|
|
22866
23747
|
}
|
|
22867
23748
|
});
|
|
22868
23749
|
duckdb.command("execute <sql>").description("Run INSERT/UPDATE/DELETE/DDL statement").option("--db <name>", "Database name", "default").option("--params <json>", "Bind parameters as JSON array").action(async (sqlStr, options, cmd) => {
|
|
@@ -22880,7 +23761,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
22880
23761
|
}
|
|
22881
23762
|
outputJson({ changes: result.data.changes });
|
|
22882
23763
|
} catch (error) {
|
|
22883
|
-
handleError(error);
|
|
23764
|
+
return handleError(error);
|
|
22884
23765
|
}
|
|
22885
23766
|
});
|
|
22886
23767
|
duckdb.command("describe").description("Show database schema (tables, columns, views)").option("--db <name>", "Database name", "default").action(async (options, cmd) => {
|
|
@@ -22925,7 +23806,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
22925
23806
|
}
|
|
22926
23807
|
}
|
|
22927
23808
|
} catch (error) {
|
|
22928
|
-
handleError(error);
|
|
23809
|
+
return handleError(error);
|
|
22929
23810
|
}
|
|
22930
23811
|
});
|
|
22931
23812
|
duckdb.command("export").description("Export database as binary file").option("--db <name>", "Database name", "default").option("-o, --output <file>", "Output file path", "export.duckdb").action(async (options, cmd) => {
|
|
@@ -22950,7 +23831,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
22950
23831
|
sizeHuman: formatBytes(blob.size)
|
|
22951
23832
|
});
|
|
22952
23833
|
} catch (error) {
|
|
22953
|
-
handleError(error);
|
|
23834
|
+
return handleError(error);
|
|
22954
23835
|
}
|
|
22955
23836
|
});
|
|
22956
23837
|
duckdb.command("import <file>").description("Import a DuckDB database file").option("--db <name>", "Database name", "default").action(async (file, options, cmd) => {
|
|
@@ -22974,7 +23855,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
22974
23855
|
imported: true
|
|
22975
23856
|
});
|
|
22976
23857
|
} catch (error) {
|
|
22977
|
-
handleError(error);
|
|
23858
|
+
return handleError(error);
|
|
22978
23859
|
}
|
|
22979
23860
|
});
|
|
22980
23861
|
}
|
|
@@ -23013,7 +23894,7 @@ function registerEnableCommand(program) {
|
|
|
23013
23894
|
};
|
|
23014
23895
|
output(value, () => result.declined.length === 0 ? `Share enabled for profile ${context.profile}.` : `Share enabled for profile ${context.profile} with ${result.declined.length} capability group(s) declined; publishing may fail.`);
|
|
23015
23896
|
} catch (error) {
|
|
23016
|
-
handleError(error);
|
|
23897
|
+
return handleError(error);
|
|
23017
23898
|
}
|
|
23018
23899
|
});
|
|
23019
23900
|
}
|
|
@@ -23085,7 +23966,7 @@ function registerInitCommand(program) {
|
|
|
23085
23966
|
authenticated: true
|
|
23086
23967
|
});
|
|
23087
23968
|
} catch (error) {
|
|
23088
|
-
handleError(error);
|
|
23969
|
+
return handleError(error);
|
|
23089
23970
|
}
|
|
23090
23971
|
});
|
|
23091
23972
|
}
|
|
@@ -23098,8 +23979,8 @@ init_storage();
|
|
|
23098
23979
|
init_errors();
|
|
23099
23980
|
init_constants();
|
|
23100
23981
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
23101
|
-
import { lstat as lstat3, open as open3, rename as rename2, rm as
|
|
23102
|
-
import { basename as basename2, dirname as
|
|
23982
|
+
import { lstat as lstat3, open as open3, rename as rename2, rm as rm5 } from "fs/promises";
|
|
23983
|
+
import { basename as basename2, dirname as dirname4, join as join9 } from "path";
|
|
23103
23984
|
async function validatePrivateOutput(path, label = "output") {
|
|
23104
23985
|
try {
|
|
23105
23986
|
const destination = await lstat3(path);
|
|
@@ -23110,7 +23991,7 @@ async function validatePrivateOutput(path, label = "output") {
|
|
|
23110
23991
|
if (error?.code !== "ENOENT") throw error;
|
|
23111
23992
|
}
|
|
23112
23993
|
try {
|
|
23113
|
-
const parent = await lstat3(
|
|
23994
|
+
const parent = await lstat3(dirname4(path));
|
|
23114
23995
|
if (!parent.isDirectory()) {
|
|
23115
23996
|
throw new CLIError("INVALID_ARGUMENT", `${label} "${path}" requires an existing directory parent.`, ExitCode.USAGE_ERROR);
|
|
23116
23997
|
}
|
|
@@ -23121,8 +24002,8 @@ async function validatePrivateOutput(path, label = "output") {
|
|
|
23121
24002
|
}
|
|
23122
24003
|
async function writePrivateOutput(path, value, label = "output") {
|
|
23123
24004
|
await validatePrivateOutput(path, label);
|
|
23124
|
-
const parentPath =
|
|
23125
|
-
const temp =
|
|
24005
|
+
const parentPath = dirname4(path);
|
|
24006
|
+
const temp = join9(parentPath, `.${basename2(path)}.${randomUUID2()}.tmp`);
|
|
23126
24007
|
let created = false;
|
|
23127
24008
|
try {
|
|
23128
24009
|
const handle = await open3(temp, "wx", PRIVATE_FILE_MODE);
|
|
@@ -23135,7 +24016,7 @@ async function writePrivateOutput(path, value, label = "output") {
|
|
|
23135
24016
|
}
|
|
23136
24017
|
await rename2(temp, path);
|
|
23137
24018
|
} catch (error) {
|
|
23138
|
-
if (created) await
|
|
24019
|
+
if (created) await rm5(temp, { force: true }).catch(() => void 0);
|
|
23139
24020
|
const code3 = error instanceof Error && "code" in error && typeof error.code === "string" ? ` (${error.code})` : "";
|
|
23140
24021
|
throw new CLIError("ERROR", `Could not write ${label.toLowerCase()} "${path}"${code3}.`, ExitCode.ERROR);
|
|
23141
24022
|
}
|
|
@@ -23228,7 +24109,7 @@ function registerKvCommand(program) {
|
|
|
23228
24109
|
process.stdout.write(content + "\n");
|
|
23229
24110
|
}
|
|
23230
24111
|
} catch (error) {
|
|
23231
|
-
handleError(error);
|
|
24112
|
+
return handleError(error);
|
|
23232
24113
|
}
|
|
23233
24114
|
});
|
|
23234
24115
|
kv.command("put <key> [value]").description("Set a value").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, value, options, cmd) => {
|
|
@@ -23263,7 +24144,7 @@ function registerKvCommand(program) {
|
|
|
23263
24144
|
}
|
|
23264
24145
|
outputJson({ key, written: true });
|
|
23265
24146
|
} catch (error) {
|
|
23266
|
-
handleError(error);
|
|
24147
|
+
return handleError(error);
|
|
23267
24148
|
}
|
|
23268
24149
|
});
|
|
23269
24150
|
kv.command("delete <key>").description("Delete a key").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, options, cmd) => {
|
|
@@ -23279,7 +24160,7 @@ function registerKvCommand(program) {
|
|
|
23279
24160
|
}
|
|
23280
24161
|
outputJson({ key, deleted: true });
|
|
23281
24162
|
} catch (error) {
|
|
23282
|
-
handleError(error);
|
|
24163
|
+
return handleError(error);
|
|
23283
24164
|
}
|
|
23284
24165
|
});
|
|
23285
24166
|
kv.command("list").description("List keys").option("--prefix <prefix>", "Filter by key prefix").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (options, cmd) => {
|
|
@@ -23315,7 +24196,7 @@ function registerKvCommand(program) {
|
|
|
23315
24196
|
}
|
|
23316
24197
|
}
|
|
23317
24198
|
} catch (error) {
|
|
23318
|
-
handleError(error);
|
|
24199
|
+
return handleError(error);
|
|
23319
24200
|
}
|
|
23320
24201
|
});
|
|
23321
24202
|
kv.command("head <key>").description("Get metadata for a key (no body)").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, options, cmd) => {
|
|
@@ -23341,7 +24222,7 @@ function registerKvCommand(program) {
|
|
|
23341
24222
|
metadata: result.data.headers ?? {}
|
|
23342
24223
|
});
|
|
23343
24224
|
} catch (error) {
|
|
23344
|
-
handleError(error);
|
|
24225
|
+
return handleError(error);
|
|
23345
24226
|
}
|
|
23346
24227
|
});
|
|
23347
24228
|
}
|
|
@@ -23453,7 +24334,7 @@ ${theme.heading("Permissions")}
|
|
|
23453
24334
|
process.stdout.write(formatTable(["service", "path", "actions"], rows) + "\n");
|
|
23454
24335
|
}
|
|
23455
24336
|
} catch (error) {
|
|
23456
|
-
handleError(error);
|
|
24337
|
+
return handleError(error);
|
|
23457
24338
|
}
|
|
23458
24339
|
});
|
|
23459
24340
|
}
|
|
@@ -23512,7 +24393,7 @@ function registerNodeCommand(program) {
|
|
|
23512
24393
|
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
23513
24394
|
outputJson({ healthy: false, host: (await ProfileManager.resolveContext(cmd.optsWithGlobals(), { storedSession: "unused" })).host, error: "Connection refused" });
|
|
23514
24395
|
} else {
|
|
23515
|
-
handleError(error);
|
|
24396
|
+
return handleError(error);
|
|
23516
24397
|
}
|
|
23517
24398
|
}
|
|
23518
24399
|
});
|
|
@@ -23527,7 +24408,7 @@ function registerNodeCommand(program) {
|
|
|
23527
24408
|
const data = await response.json();
|
|
23528
24409
|
outputJson({ ...data, host: ctx.host });
|
|
23529
24410
|
} catch (error) {
|
|
23530
|
-
handleError(error);
|
|
24411
|
+
return handleError(error);
|
|
23531
24412
|
}
|
|
23532
24413
|
});
|
|
23533
24414
|
node.command("status").description("Combined health and version info").action(async (_options, cmd) => {
|
|
@@ -23552,7 +24433,7 @@ function registerNodeCommand(program) {
|
|
|
23552
24433
|
...versionData
|
|
23553
24434
|
});
|
|
23554
24435
|
} catch (error) {
|
|
23555
|
-
handleError(error);
|
|
24436
|
+
return handleError(error);
|
|
23556
24437
|
}
|
|
23557
24438
|
});
|
|
23558
24439
|
}
|
|
@@ -23612,7 +24493,7 @@ function registerProfileCommand(program) {
|
|
|
23612
24493
|
}
|
|
23613
24494
|
}
|
|
23614
24495
|
} catch (error) {
|
|
23615
|
-
handleError(error);
|
|
24496
|
+
return handleError(error);
|
|
23616
24497
|
}
|
|
23617
24498
|
});
|
|
23618
24499
|
profile.command("create <name>").description("Create a new profile").option("--host <url>", "TinyCloud node URL").option(
|
|
@@ -23646,7 +24527,7 @@ function registerProfileCommand(program) {
|
|
|
23646
24527
|
});
|
|
23647
24528
|
outputJson({ profile: name, did, host, posture, operatorType, created: true });
|
|
23648
24529
|
} catch (error) {
|
|
23649
|
-
handleError(error);
|
|
24530
|
+
return handleError(error);
|
|
23650
24531
|
}
|
|
23651
24532
|
});
|
|
23652
24533
|
profile.command("show [name]").description("Show profile details").action(async (name, _options, cmd) => {
|
|
@@ -23685,7 +24566,7 @@ function registerProfileCommand(program) {
|
|
|
23685
24566
|
process.stdout.write(formatField("Created", p.createdAt) + "\n");
|
|
23686
24567
|
}
|
|
23687
24568
|
} catch (error) {
|
|
23688
|
-
handleError(error);
|
|
24569
|
+
return handleError(error);
|
|
23689
24570
|
}
|
|
23690
24571
|
});
|
|
23691
24572
|
profile.command("switch <name>").description("Set default profile").action(async (name, _options, cmd) => {
|
|
@@ -23697,7 +24578,7 @@ function registerProfileCommand(program) {
|
|
|
23697
24578
|
await ProfileManager.setConfig({ ...config, defaultProfile: name });
|
|
23698
24579
|
outputJson({ defaultProfile: name, switched: true });
|
|
23699
24580
|
} catch (error) {
|
|
23700
|
-
handleError(error);
|
|
24581
|
+
return handleError(error);
|
|
23701
24582
|
}
|
|
23702
24583
|
});
|
|
23703
24584
|
profile.command("set-default-space [name]").description("Set (or clear) the default space used when --space is omitted").option("--profile <name>", "Profile to modify (defaults to the active profile)").option("--unset", "Clear the default space so commands fall back to the primary space").addHelpText("after", `
|
|
@@ -23736,7 +24617,7 @@ Examples:
|
|
|
23736
24617
|
await ProfileManager.updateProfile(profileName2, (p) => ({ ...p, defaultSpace }));
|
|
23737
24618
|
outputJson({ profile: profileName2, defaultSpace: defaultSpace ?? null, updated: true });
|
|
23738
24619
|
} catch (error) {
|
|
23739
|
-
handleError(error);
|
|
24620
|
+
return handleError(error);
|
|
23740
24621
|
}
|
|
23741
24622
|
});
|
|
23742
24623
|
profile.command("delete <name>").description("Delete a profile").action(async (name, _options, cmd) => {
|
|
@@ -23755,7 +24636,7 @@ Examples:
|
|
|
23755
24636
|
await ProfileManager.deleteProfile(name);
|
|
23756
24637
|
outputJson({ profile: name, deleted: true });
|
|
23757
24638
|
} catch (error) {
|
|
23758
|
-
handleError(error);
|
|
24639
|
+
return handleError(error);
|
|
23759
24640
|
}
|
|
23760
24641
|
});
|
|
23761
24642
|
}
|
|
@@ -23779,16 +24660,105 @@ function parseOperatorType(raw) {
|
|
|
23779
24660
|
}
|
|
23780
24661
|
|
|
23781
24662
|
// src/commands/replica.ts
|
|
24663
|
+
init_sdk();
|
|
24664
|
+
init_replication_registry();
|
|
23782
24665
|
import { randomBytes as randomBytes5 } from "crypto";
|
|
23783
24666
|
import { constants as fsConstants2 } from "fs";
|
|
23784
|
-
import { open as open4, readdir as readdir5, rename as rename3, stat as stat3, unlink as unlink2 } from "fs/promises";
|
|
23785
|
-
import { basename as basename3, dirname as
|
|
24667
|
+
import { open as open4, readdir as readdir5, rename as rename3, stat as stat3, unlink as unlink2, readFile as readFile9, lstat as lstat4 } from "fs/promises";
|
|
24668
|
+
import { basename as basename3, dirname as dirname5, join as join10, resolve as resolve2 } from "path";
|
|
24669
|
+
|
|
24670
|
+
// src/lib/replication-report.ts
|
|
24671
|
+
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.";
|
|
24672
|
+
function percentile(values, fraction) {
|
|
24673
|
+
if (values.length === 0) return null;
|
|
24674
|
+
values.sort((left, right) => left - right);
|
|
24675
|
+
return values[Math.ceil(fraction * values.length) - 1] ?? values[values.length - 1];
|
|
24676
|
+
}
|
|
24677
|
+
function createReplicationReport(since, events, replicas, partitions, warning) {
|
|
24678
|
+
const reads = events.filter((event) => event.type === "replication.read");
|
|
24679
|
+
const replicaReads = reads.filter((event) => event.source === "replica");
|
|
24680
|
+
const sourceLatency = /* @__PURE__ */ new Map();
|
|
24681
|
+
const readsBySourceReason = {};
|
|
24682
|
+
for (const event of reads) {
|
|
24683
|
+
const key = `${event.source}:${event.reason}`;
|
|
24684
|
+
readsBySourceReason[key] = (readsBySourceReason[key] ?? 0) + 1;
|
|
24685
|
+
const values = sourceLatency.get(event.source) ?? [];
|
|
24686
|
+
values.push(event.latencyMs);
|
|
24687
|
+
sourceLatency.set(event.source, values);
|
|
24688
|
+
}
|
|
24689
|
+
const latencyMs = Object.fromEntries([...sourceLatency].map(([source, values]) => [source, {
|
|
24690
|
+
p50: percentile([...values], 0.5),
|
|
24691
|
+
p95: percentile([...values], 0.95)
|
|
24692
|
+
}]));
|
|
24693
|
+
const staleness = reads.flatMap((event) => event.stalenessMs === null ? [] : [event.stalenessMs]);
|
|
24694
|
+
const syncs = {};
|
|
24695
|
+
const writes = {};
|
|
24696
|
+
const divergences = events.filter((event) => event.type === "replication.divergence").slice(-20);
|
|
24697
|
+
for (const event of events) {
|
|
24698
|
+
if (event.type === "replication.sync") {
|
|
24699
|
+
const key = `${event.class ?? "none"}:${event.code ?? event.outcome}`;
|
|
24700
|
+
syncs[key] = (syncs[key] ?? 0) + 1;
|
|
24701
|
+
} else if (event.type === "replication.write") {
|
|
24702
|
+
writes[event.outcome] = (writes[event.outcome] ?? 0) + 1;
|
|
24703
|
+
}
|
|
24704
|
+
}
|
|
24705
|
+
return {
|
|
24706
|
+
since,
|
|
24707
|
+
totals: {
|
|
24708
|
+
reads: reads.length,
|
|
24709
|
+
replicaReads: replicaReads.length,
|
|
24710
|
+
hitRatio: reads.length === 0 ? 0 : replicaReads.length / reads.length,
|
|
24711
|
+
writes: events.filter((event) => event.type === "replication.write").length,
|
|
24712
|
+
divergences: events.filter((event) => event.type === "replication.divergence").length
|
|
24713
|
+
},
|
|
24714
|
+
readsBySourceReason,
|
|
24715
|
+
latencyMs,
|
|
24716
|
+
stalenessMs: { p50: percentile([...staleness], 0.5), p95: percentile([...staleness], 0.95), max: staleness.length ? Math.max(...staleness) : null },
|
|
24717
|
+
syncs,
|
|
24718
|
+
writes,
|
|
24719
|
+
divergences,
|
|
24720
|
+
replicas,
|
|
24721
|
+
partitions,
|
|
24722
|
+
...warning === void 0 ? {} : { warning }
|
|
24723
|
+
};
|
|
24724
|
+
}
|
|
24725
|
+
function renderReplicationReport(report) {
|
|
24726
|
+
const lines = [
|
|
24727
|
+
`Replication report (since ${report.since})`,
|
|
24728
|
+
`Reads: ${report.totals.reads}; replica: ${report.totals.replicaReads}; hit ratio: ${(report.totals.hitRatio * 100).toFixed(1)}%`,
|
|
24729
|
+
`Latency ms: ${Object.entries(report.latencyMs).map(([source, value]) => `${source} p50=${value.p50 ?? "\u2014"} p95=${value.p95 ?? "\u2014"}`).join("; ") || "none"}`,
|
|
24730
|
+
`Staleness ms: p50=${report.stalenessMs.p50 ?? "\u2014"} p95=${report.stalenessMs.p95 ?? "\u2014"} max=${report.stalenessMs.max ?? "\u2014"}`,
|
|
24731
|
+
`Syncs: ${Object.entries(report.syncs).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
|
|
24732
|
+
`Writes: ${Object.entries(report.writes).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
|
|
24733
|
+
`Read source/reason: ${Object.entries(report.readsBySourceReason).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
|
|
24734
|
+
`Divergences: ${report.totals.divergences}; showing ${report.divergences.length} of ${report.totals.divergences}`,
|
|
24735
|
+
`Replicas: ${report.replicas.length}`
|
|
24736
|
+
];
|
|
24737
|
+
for (const divergence of report.divergences) {
|
|
24738
|
+
if (divergence.type === "replication.divergence") {
|
|
24739
|
+
lines.push(` ${divergence.space} ${divergence.key}: ${divergence.kind} stalenessMs=${divergence.stalenessMs}`);
|
|
24740
|
+
}
|
|
24741
|
+
}
|
|
24742
|
+
for (const replica of report.replicas) {
|
|
24743
|
+
const grant = replica.grant ? ` grant=${replica.grant.cid} parent=${replica.grant.parentCid ?? "\u2014"} expires=${replica.grant.expiresAt ?? "\u2014"}` : " grant=missing";
|
|
24744
|
+
lines.push(` ${replica.prefix}: ${replica.state}; pending in_flight=${replica.pending.inFlight} committed=${replica.pending.committed} ambiguous=${replica.pending.ambiguous}${grant}`);
|
|
24745
|
+
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}`);
|
|
24746
|
+
}
|
|
24747
|
+
for (const partition of report.partitions) {
|
|
24748
|
+
lines.push(`Partition ${partition.idHash}: host=${partition.host ?? "unknown"} space=${partition.space ?? "unknown"} pinned=${partition.pinned}`);
|
|
24749
|
+
}
|
|
24750
|
+
if (report.partitions.some((partition) => partition.pinned > 100)) lines.push("Warning: a partition holds more than 100 pinned records.");
|
|
24751
|
+
if (report.warning) lines.push(`Warning: ${report.warning}`);
|
|
24752
|
+
return lines.join("\n");
|
|
24753
|
+
}
|
|
24754
|
+
|
|
24755
|
+
// src/commands/replica.ts
|
|
23786
24756
|
import {
|
|
23787
24757
|
ProfileDeletedError as ProfileDeletedError3,
|
|
23788
|
-
profilePath as
|
|
24758
|
+
profilePath as profilePath4,
|
|
23789
24759
|
readAdditionalDelegations as readAdditionalDelegations2,
|
|
23790
24760
|
readSession as readSession2,
|
|
23791
|
-
withProfileLock as
|
|
24761
|
+
withProfileLock as withProfileLock5
|
|
23792
24762
|
} from "@tinycloud/operations/state";
|
|
23793
24763
|
init_profiles();
|
|
23794
24764
|
init_constants();
|
|
@@ -23796,6 +24766,8 @@ init_errors();
|
|
|
23796
24766
|
init_formatter();
|
|
23797
24767
|
init_theme();
|
|
23798
24768
|
var EXIT_BY_CODE = {
|
|
24769
|
+
[ReplicaErrorCode.INVALID_ARGUMENT]: ExitCode.USAGE_ERROR,
|
|
24770
|
+
[ReplicaErrorCode.CLOSED]: ExitCode.ERROR,
|
|
23799
24771
|
[ReplicaErrorCode.BUSY]: ExitCode.ERROR,
|
|
23800
24772
|
[ReplicaErrorCode.RUNTIME_UNSUPPORTED]: ExitCode.ERROR,
|
|
23801
24773
|
[ReplicaErrorCode.STORAGE_ERROR]: ExitCode.ERROR,
|
|
@@ -23845,7 +24817,7 @@ async function run(action) {
|
|
|
23845
24817
|
await loadSqlite();
|
|
23846
24818
|
await action();
|
|
23847
24819
|
} catch (error) {
|
|
23848
|
-
handleError(toCliError(error));
|
|
24820
|
+
return handleError(toCliError(error));
|
|
23849
24821
|
}
|
|
23850
24822
|
}
|
|
23851
24823
|
async function profileName(cmd) {
|
|
@@ -23856,13 +24828,13 @@ async function profileName(cmd) {
|
|
|
23856
24828
|
return config?.defaultProfile ?? DEFAULT_PROFILE;
|
|
23857
24829
|
}
|
|
23858
24830
|
function replicasRoot(profile) {
|
|
23859
|
-
return
|
|
24831
|
+
return join10(profilePath4(profile), "replicas");
|
|
23860
24832
|
}
|
|
23861
24833
|
async function replicaNames(profile) {
|
|
23862
24834
|
const names = [];
|
|
23863
24835
|
for (const name of await readdir5(replicasRoot(profile)).catch(() => [])) {
|
|
23864
24836
|
if (!NAME.test(name)) continue;
|
|
23865
|
-
const exists = await stat3(
|
|
24837
|
+
const exists = await stat3(join10(replicasRoot(profile), name, "replica.db")).then(
|
|
23866
24838
|
() => true,
|
|
23867
24839
|
() => false
|
|
23868
24840
|
);
|
|
@@ -23888,7 +24860,7 @@ async function existingReplicaName(profile, option) {
|
|
|
23888
24860
|
function profileGuard(profile) {
|
|
23889
24861
|
return async (section) => {
|
|
23890
24862
|
try {
|
|
23891
|
-
return await
|
|
24863
|
+
return await withProfileLock5(profile, section, { requireProfile: true });
|
|
23892
24864
|
} catch (error) {
|
|
23893
24865
|
if (error instanceof ProfileDeletedError3) {
|
|
23894
24866
|
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" was deleted; its replicas are gone and nothing was written.`);
|
|
@@ -23898,12 +24870,12 @@ function profileGuard(profile) {
|
|
|
23898
24870
|
};
|
|
23899
24871
|
}
|
|
23900
24872
|
async function openReplica(profile, name) {
|
|
23901
|
-
return SqliteReplicaStore.open(
|
|
24873
|
+
return SqliteReplicaStore.open(join10(replicasRoot(profile), name), { create: false, guard: profileGuard(profile) });
|
|
23902
24874
|
}
|
|
23903
24875
|
async function createReplica(profile, config) {
|
|
23904
24876
|
const guard = profileGuard(profile);
|
|
23905
24877
|
return guard(async () => {
|
|
23906
|
-
const store = await SqliteReplicaStore.open(
|
|
24878
|
+
const store = await SqliteReplicaStore.open(join10(replicasRoot(profile), config.name), { create: true, guard });
|
|
23907
24879
|
if (await store.open() === null) await store.init(config);
|
|
23908
24880
|
return store;
|
|
23909
24881
|
});
|
|
@@ -24035,7 +25007,7 @@ function describeStatus(status) {
|
|
|
24035
25007
|
}
|
|
24036
25008
|
async function writeFileAtomic(path, bytes) {
|
|
24037
25009
|
const target = resolve2(path);
|
|
24038
|
-
const temp =
|
|
25010
|
+
const temp = join10(dirname5(target), `.${basename3(target)}.${randomBytes5(6).toString("hex")}.tmp`);
|
|
24039
25011
|
const handle = await open4(temp, fsConstants2.O_CREAT | fsConstants2.O_EXCL | fsConstants2.O_WRONLY, 384);
|
|
24040
25012
|
try {
|
|
24041
25013
|
await handle.writeFile(bytes);
|
|
@@ -24061,6 +25033,49 @@ function replicaSecretsWarning(space, prefix, secretsAllowed) {
|
|
|
24061
25033
|
if (!secretsAllowed || !requiresSecretsOptIn(space, prefix)) return void 0;
|
|
24062
25034
|
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.`;
|
|
24063
25035
|
}
|
|
25036
|
+
async function readReplicationEvents(profile, sinceMs) {
|
|
25037
|
+
const root = join10(profilePath4(profile), "replication");
|
|
25038
|
+
const events = [];
|
|
25039
|
+
for (const name of ["events.jsonl.1", "events.jsonl"]) {
|
|
25040
|
+
const data = await readFile9(join10(root, name), "utf8").catch((error) => {
|
|
25041
|
+
if (error.code === "ENOENT") return "";
|
|
25042
|
+
throw error;
|
|
25043
|
+
});
|
|
25044
|
+
for (const line of data.split("\n")) {
|
|
25045
|
+
if (!line) continue;
|
|
25046
|
+
try {
|
|
25047
|
+
const event = JSON.parse(line);
|
|
25048
|
+
if (Number.isFinite(Date.parse(event.at)) && Date.parse(event.at) >= sinceMs) events.push(event);
|
|
25049
|
+
} catch {
|
|
25050
|
+
}
|
|
25051
|
+
}
|
|
25052
|
+
}
|
|
25053
|
+
return events;
|
|
25054
|
+
}
|
|
25055
|
+
async function replicationPartitions(profile) {
|
|
25056
|
+
const root = join10(profilePath4(profile), "replication");
|
|
25057
|
+
const entries = await readdir5(root, { withFileTypes: true }).catch((error) => {
|
|
25058
|
+
if (error.code === "ENOENT") return [];
|
|
25059
|
+
throw error;
|
|
25060
|
+
});
|
|
25061
|
+
const partitions = [];
|
|
25062
|
+
for (const entry of entries) {
|
|
25063
|
+
if (!entry.isDirectory() || !/^[a-z2-7]{26}$/.test(entry.name)) continue;
|
|
25064
|
+
const directory = join10(root, entry.name);
|
|
25065
|
+
const info = await lstat4(directory);
|
|
25066
|
+
if (info.isSymbolicLink() || !info.isDirectory()) continue;
|
|
25067
|
+
const identity = await readFile9(join10(directory, "identity.json"), "utf8").then((value) => JSON.parse(value)).catch(() => null);
|
|
25068
|
+
const pending = await readFile9(join10(directory, "pending.json"), "utf8").then((value) => JSON.parse(value)).catch(() => null);
|
|
25069
|
+
const pinned = pending?.records?.filter((record2) => record2.state === "in_flight" || record2.state === "ambiguous").length ?? 0;
|
|
25070
|
+
partitions.push({
|
|
25071
|
+
idHash: entry.name,
|
|
25072
|
+
host: typeof identity?.host === "string" ? identity.host : null,
|
|
25073
|
+
space: typeof identity?.space === "string" ? identity.space : null,
|
|
25074
|
+
pinned
|
|
25075
|
+
});
|
|
25076
|
+
}
|
|
25077
|
+
return partitions.sort((left, right) => left.idHash.localeCompare(right.idHash));
|
|
25078
|
+
}
|
|
24064
25079
|
function registerReplicaCommand(program) {
|
|
24065
25080
|
const replica = program.command("replica").description("Durable read-only local replicas of a KV prefix (sync online, read offline) [beta]").addHelpText(
|
|
24066
25081
|
"after",
|
|
@@ -24076,6 +25091,53 @@ SECRETS_OPT_IN_REQUIRED; 3 grant missing; 4 key absent, deleted, content missing
|
|
|
24076
25091
|
or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
|
|
24077
25092
|
7 node, protocol or integrity error; 10 storage full.`
|
|
24078
25093
|
);
|
|
25094
|
+
replica.command("report").description("Report replication reads, syncs, pending writes and local identity partitions").option("--since <duration>", "Include events from the last duration (default: 24h)", "24h").option("--purge", "Purge configured replicas for the current identity").option("--clear-pending", "Clear ambiguous and likely orphaned in-flight writes").action(
|
|
25095
|
+
(options, cmd) => run(async () => {
|
|
25096
|
+
const globals = cmd.optsWithGlobals();
|
|
25097
|
+
const context = await ProfileManager.resolveContext(globals);
|
|
25098
|
+
const node = context.replication ? await ensureAuthenticated(context) : void 0;
|
|
25099
|
+
const profile = await profileName(cmd);
|
|
25100
|
+
let durationMs;
|
|
25101
|
+
try {
|
|
25102
|
+
durationMs = parseDuration(options.since);
|
|
25103
|
+
} catch (error) {
|
|
25104
|
+
throw new CLIError("USAGE_ERROR", error instanceof Error ? error.message : String(error), ExitCode.USAGE_ERROR);
|
|
25105
|
+
}
|
|
25106
|
+
const control = node?.replication ?? replicationForProfile(profile);
|
|
25107
|
+
let cleared;
|
|
25108
|
+
let purged;
|
|
25109
|
+
let warning;
|
|
25110
|
+
if (options.clearPending || options.purge) {
|
|
25111
|
+
if (!control) throw new CLIError("REPLICATION_UNAVAILABLE", "No replication runtime is registered for this profile.", ExitCode.ERROR);
|
|
25112
|
+
warning = CLEAR_PENDING_WARNING;
|
|
25113
|
+
process.stderr.write(`Warning: ${warning}
|
|
25114
|
+
`);
|
|
25115
|
+
if (options.clearPending) cleared = await control.clearPending();
|
|
25116
|
+
if (options.purge) purged = await control.purge();
|
|
25117
|
+
}
|
|
25118
|
+
const sinceMs = Date.now() - durationMs;
|
|
25119
|
+
const [events, partitions, replicas] = await Promise.all([
|
|
25120
|
+
readReplicationEvents(profile, sinceMs),
|
|
25121
|
+
replicationPartitions(profile),
|
|
25122
|
+
node?.replication ? node.replication.status() : control ? control.status() : Promise.resolve([])
|
|
25123
|
+
]);
|
|
25124
|
+
const report = createReplicationReport(options.since, events, replicas, partitions, warning);
|
|
25125
|
+
if (shouldOutputJson()) {
|
|
25126
|
+
outputJson({
|
|
25127
|
+
...report,
|
|
25128
|
+
...cleared === void 0 ? {} : { cleared },
|
|
25129
|
+
...purged === void 0 ? {} : { purged }
|
|
25130
|
+
});
|
|
25131
|
+
} else {
|
|
25132
|
+
process.stdout.write(`${renderReplicationReport(report)}
|
|
25133
|
+
`);
|
|
25134
|
+
if (cleared !== void 0) process.stdout.write(`Cleared ${cleared} pending write record(s).
|
|
25135
|
+
`);
|
|
25136
|
+
if (purged !== void 0) process.stdout.write(`Purge: ${JSON.stringify(purged)}
|
|
25137
|
+
`);
|
|
25138
|
+
}
|
|
25139
|
+
})
|
|
25140
|
+
);
|
|
24079
25141
|
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 under a secrets prefix; replica grants do not include decrypt").action(
|
|
24080
25142
|
(options, cmd) => run(async () => {
|
|
24081
25143
|
const profile = await profileName(cmd);
|
|
@@ -24095,8 +25157,8 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
|
|
|
24095
25157
|
let store;
|
|
24096
25158
|
if (existing.includes(name)) {
|
|
24097
25159
|
store = await openReplica(profile, name);
|
|
24098
|
-
const
|
|
24099
|
-
const differs = syncOptionsDiffer(
|
|
25160
|
+
const state2 = await store.open();
|
|
25161
|
+
const differs = syncOptionsDiffer(state2.config, {
|
|
24100
25162
|
...options.space === void 0 ? {} : { space: options.space },
|
|
24101
25163
|
...options.prefix === void 0 ? {} : { prefix: options.prefix },
|
|
24102
25164
|
...globals.host === void 0 ? {} : { host: globals.host }
|
|
@@ -24136,24 +25198,24 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
|
|
|
24136
25198
|
});
|
|
24137
25199
|
}
|
|
24138
25200
|
try {
|
|
24139
|
-
const
|
|
24140
|
-
const { space, prefix, host } =
|
|
24141
|
-
if (requiresSecretsOptIn(space, prefix) && !
|
|
25201
|
+
const state2 = await store.open();
|
|
25202
|
+
const { space, prefix, host } = state2.config;
|
|
25203
|
+
if (requiresSecretsOptIn(space, prefix) && !state2.config.allowSecrets && options.allowSecrets !== true) {
|
|
24142
25204
|
throw new ReplicaError(ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED, `Replicating ${space}/kv/${prefix} needs --allow-secrets.`);
|
|
24143
25205
|
}
|
|
24144
25206
|
let grant;
|
|
24145
25207
|
try {
|
|
24146
|
-
grant = chooseGrant(grants,
|
|
25208
|
+
grant = chooseGrant(grants, state2.config.deviceDid, space, prefix);
|
|
24147
25209
|
} catch (error) {
|
|
24148
|
-
if (!isReplicaError(error, ReplicaErrorCode.GRANT_MISSING) || (
|
|
25210
|
+
if (!isReplicaError(error, ReplicaErrorCode.GRANT_MISSING) || (state2.grant ?? state2.pendingGrant) === null) throw error;
|
|
24149
25211
|
}
|
|
24150
25212
|
if (grant !== void 0) await store.installGrant(grant);
|
|
24151
|
-
if (options.retentionGrant !== void 0 && options.retentionGrant !==
|
|
25213
|
+
if (options.retentionGrant !== void 0 && options.retentionGrant !== state2.config.retentionGrantCid) {
|
|
24152
25214
|
await store.setRetentionGrant(options.retentionGrant === "" ? null : options.retentionGrant);
|
|
24153
25215
|
}
|
|
24154
25216
|
const current = await store.open();
|
|
24155
25217
|
const transportFor = await grantTransports({ host, space, deviceDid: current.config.deviceDid, jwk });
|
|
24156
|
-
const warning = replicaSecretsWarning(space, prefix, options.allowSecrets === true ||
|
|
25218
|
+
const warning = replicaSecretsWarning(space, prefix, options.allowSecrets === true || state2.config.allowSecrets);
|
|
24157
25219
|
if (warning !== void 0) process.stderr.write(`Warning: ${warning}
|
|
24158
25220
|
`);
|
|
24159
25221
|
const report = await new Replica({ store, transportFor }).sync(options.limit === void 0 ? {} : { limit: options.limit });
|
|
@@ -24283,8 +25345,8 @@ init_theme();
|
|
|
24283
25345
|
init_errors();
|
|
24284
25346
|
init_warnings();
|
|
24285
25347
|
init_constants();
|
|
24286
|
-
import { readFile as
|
|
24287
|
-
import { join as
|
|
25348
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
25349
|
+
import { join as join11 } from "path";
|
|
24288
25350
|
import { homedir, hostname, userInfo } from "os";
|
|
24289
25351
|
import { appSecretScope } from "@tinycloud/sdk-core";
|
|
24290
25352
|
import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
|
|
@@ -24813,7 +25875,7 @@ function resolveSecretListPrefix(options = {}) {
|
|
|
24813
25875
|
}
|
|
24814
25876
|
function resolveProfilesDir() {
|
|
24815
25877
|
const home = process.env.TC_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir();
|
|
24816
|
-
return
|
|
25878
|
+
return join11(home, ".tinycloud", "profiles");
|
|
24817
25879
|
}
|
|
24818
25880
|
async function ensureSecretsNode(ctx, options, openKeyAcquisition, selectedProfile) {
|
|
24819
25881
|
const auth = authOptions(options);
|
|
@@ -25176,7 +26238,7 @@ function permissionsFromDelegation2(delegation) {
|
|
|
25176
26238
|
}
|
|
25177
26239
|
async function loadDelegationCandidates(source) {
|
|
25178
26240
|
try {
|
|
25179
|
-
const raw = JSON.parse(await
|
|
26241
|
+
const raw = JSON.parse(await readFile10(source, "utf8"));
|
|
25180
26242
|
return normalizeDelegationCandidates(raw, source);
|
|
25181
26243
|
} catch (error) {
|
|
25182
26244
|
if (error?.code !== "ENOENT") {
|
|
@@ -25195,8 +26257,8 @@ async function loadDelegationCandidates(source) {
|
|
|
25195
26257
|
}
|
|
25196
26258
|
}
|
|
25197
26259
|
try {
|
|
25198
|
-
const importedPath =
|
|
25199
|
-
const raw = JSON.parse(await
|
|
26260
|
+
const importedPath = join11(resolveProfilesDir(), source, "additional-delegations.json");
|
|
26261
|
+
const raw = JSON.parse(await readFile10(importedPath, "utf8"));
|
|
25200
26262
|
return normalizeDelegationCandidates(raw, source);
|
|
25201
26263
|
} catch (error) {
|
|
25202
26264
|
if (error?.code === "ENOENT") {
|
|
@@ -25521,7 +26583,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
25521
26583
|
scopeOptions,
|
|
25522
26584
|
space: spaceUri,
|
|
25523
26585
|
label: `Checking secret ${name}...`,
|
|
25524
|
-
operation: () => secrets2.get(name, scopeOptions)
|
|
26586
|
+
operation: () => secrets2.get(name, { ...scopeOptions, source: "network" })
|
|
25525
26587
|
});
|
|
25526
26588
|
if (result.ok) {
|
|
25527
26589
|
secret = {
|
|
@@ -25590,7 +26652,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
25590
26652
|
scopeOptions,
|
|
25591
26653
|
space: spaceUri,
|
|
25592
26654
|
label: "Listing secrets...",
|
|
25593
|
-
operation: () => secrets2.list(scopeOptions)
|
|
26655
|
+
operation: () => secrets2.list({ ...scopeOptions, source: "network" })
|
|
25594
26656
|
});
|
|
25595
26657
|
if (!result.ok) {
|
|
25596
26658
|
throw cliErrorFromService(result.error);
|
|
@@ -25703,7 +26765,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
25703
26765
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
25704
26766
|
}
|
|
25705
26767
|
if (options.file) {
|
|
25706
|
-
secretValue = await
|
|
26768
|
+
secretValue = await readFile10(options.file, "utf-8");
|
|
25707
26769
|
} else if (options.stdin) {
|
|
25708
26770
|
secretValue = (await readStdin3()).toString("utf-8");
|
|
25709
26771
|
} else {
|
|
@@ -32735,12 +33797,12 @@ async function notifyShare(input) {
|
|
|
32735
33797
|
if (input.signal?.aborted) throw new ShareNotifyError("share delivery was cancelled");
|
|
32736
33798
|
attempts += 1;
|
|
32737
33799
|
try {
|
|
32738
|
-
const
|
|
33800
|
+
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 } });
|
|
32739
33801
|
return {
|
|
32740
33802
|
protocol: "tinycloud-share",
|
|
32741
33803
|
version: 1,
|
|
32742
33804
|
shareId: input.shareId,
|
|
32743
|
-
state:
|
|
33805
|
+
state: state2 === "delivered" && input.record?.deliveredRecipients?.includes(recipient) ? "already-delivered" : state2,
|
|
32744
33806
|
idempotencyKey,
|
|
32745
33807
|
attempts
|
|
32746
33808
|
};
|
|
@@ -34211,9 +35273,9 @@ function receiveJson(result, path) {
|
|
|
34211
35273
|
|
|
34212
35274
|
// src/share/io.ts
|
|
34213
35275
|
import { constants } from "fs";
|
|
34214
|
-
import { lstat as
|
|
35276
|
+
import { lstat as lstat5, mkdir as mkdir4, mkdtemp, open as open7, readFile as readFile11, realpath as realpath2, stat as stat4, link as link2, rename as rename4, rm as rm6, unlink as unlink3 } from "fs/promises";
|
|
34215
35277
|
import { randomBytes as randomBytes6 } from "crypto";
|
|
34216
|
-
import { basename as basename4, join as
|
|
35278
|
+
import { basename as basename4, join as join12, resolve as resolve3, sep as sep2 } from "path";
|
|
34217
35279
|
init_errors();
|
|
34218
35280
|
var MAX_SHARE_STDIN_BYTES = 100 * 1024 * 1024;
|
|
34219
35281
|
var MAX_SHARE_URL_BYTES = 64 * 1024;
|
|
@@ -34258,7 +35320,7 @@ async function readShareInput(input, name, limit = MAX_SHARE_STDIN_BYTES) {
|
|
|
34258
35320
|
const path = resolve3(input);
|
|
34259
35321
|
const info = await stat4(path);
|
|
34260
35322
|
if (!info.isFile() || info.size > limit) throw new Error("MAX_BYTES_EXCEEDED");
|
|
34261
|
-
const bytes = new Uint8Array(await
|
|
35323
|
+
const bytes = new Uint8Array(await readFile11(path));
|
|
34262
35324
|
if (bytes.byteLength > limit) throw new Error("MAX_BYTES_EXCEEDED");
|
|
34263
35325
|
return { bytes, filename };
|
|
34264
35326
|
}
|
|
@@ -34267,9 +35329,9 @@ async function assertDirectory(path) {
|
|
|
34267
35329
|
const segments = absolute.split(sep2).filter(Boolean);
|
|
34268
35330
|
let current = absolute.startsWith(sep2) ? sep2 : "";
|
|
34269
35331
|
for (const segment of segments) {
|
|
34270
|
-
current = current === sep2 ?
|
|
35332
|
+
current = current === sep2 ? join12(current, segment) : join12(current, segment);
|
|
34271
35333
|
try {
|
|
34272
|
-
const info = await
|
|
35334
|
+
const info = await lstat5(current);
|
|
34273
35335
|
if (info.isSymbolicLink()) {
|
|
34274
35336
|
const canonical = await realpath2(current);
|
|
34275
35337
|
if (current !== "/tmp" && current !== "/var") throw new Error("OUTPUT_EXISTS");
|
|
@@ -34278,7 +35340,7 @@ async function assertDirectory(path) {
|
|
|
34278
35340
|
} catch (error) {
|
|
34279
35341
|
if (error.code !== "ENOENT") throw error;
|
|
34280
35342
|
await mkdir4(current, { mode: 448 });
|
|
34281
|
-
const created = await
|
|
35343
|
+
const created = await lstat5(current);
|
|
34282
35344
|
if (created.isSymbolicLink() || !created.isDirectory()) throw new Error("OUTPUT_EXISTS");
|
|
34283
35345
|
}
|
|
34284
35346
|
}
|
|
@@ -34289,23 +35351,23 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
34289
35351
|
const safeName = safeFilename(filename);
|
|
34290
35352
|
const directoryHandle = await open7(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
|
|
34291
35353
|
const stableDirectory = await realpath2(outputDirectory);
|
|
34292
|
-
const outputPath =
|
|
35354
|
+
const outputPath = join12(stableDirectory, safeName);
|
|
34293
35355
|
const directoryIdentity = await directoryHandle.stat();
|
|
34294
35356
|
const assertStableDirectory = async () => {
|
|
34295
35357
|
const current = await stat4(stableDirectory);
|
|
34296
35358
|
if (current.dev !== directoryIdentity.dev || current.ino !== directoryIdentity.ino) throw new Error("OUTPUT_EXISTS");
|
|
34297
35359
|
};
|
|
34298
35360
|
await assertStableDirectory();
|
|
34299
|
-
const stagingDirectory = await mkdtemp(
|
|
34300
|
-
const stagingInfo = await
|
|
35361
|
+
const stagingDirectory = await mkdtemp(join12(stableDirectory, ".tinycloud-share-stage-"));
|
|
35362
|
+
const stagingInfo = await lstat5(stagingDirectory);
|
|
34301
35363
|
if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
|
|
34302
|
-
const stagingPath =
|
|
35364
|
+
const stagingPath = join12(stagingDirectory, `.tinycloud-share-${randomBytes6(16).toString("hex")}.tmp`);
|
|
34303
35365
|
let temporaryPath;
|
|
34304
35366
|
let handle;
|
|
34305
35367
|
try {
|
|
34306
35368
|
await assertStableDirectory();
|
|
34307
35369
|
try {
|
|
34308
|
-
const existing = await
|
|
35370
|
+
const existing = await lstat5(outputPath);
|
|
34309
35371
|
if (existing.isSymbolicLink()) throw new Error("UNSAFE_FILENAME");
|
|
34310
35372
|
} catch (error) {
|
|
34311
35373
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -34333,10 +35395,10 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
34333
35395
|
if (temporaryPath !== void 0) await unlink3(temporaryPath);
|
|
34334
35396
|
} catch {
|
|
34335
35397
|
}
|
|
34336
|
-
await
|
|
35398
|
+
await rm6(stagingDirectory, { recursive: true, force: true });
|
|
34337
35399
|
await directoryHandle.close();
|
|
34338
35400
|
}
|
|
34339
|
-
return
|
|
35401
|
+
return join12(outputDirectory, safeName);
|
|
34340
35402
|
}
|
|
34341
35403
|
|
|
34342
35404
|
// src/share/errors.ts
|
|
@@ -34619,7 +35681,7 @@ function registerShareCommand(program) {
|
|
|
34619
35681
|
if (json) writeJson2({ ...redactPublishedShare(result), expiryClamped: result.metadata.expiryClamped === true, ...notification === void 0 ? {} : { notification } });
|
|
34620
35682
|
else publishHuman(result);
|
|
34621
35683
|
} catch (error) {
|
|
34622
|
-
handleError(shareCliError(error));
|
|
35684
|
+
return handleError(shareCliError(error));
|
|
34623
35685
|
}
|
|
34624
35686
|
});
|
|
34625
35687
|
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) => {
|
|
@@ -34629,7 +35691,7 @@ function registerShareCommand(program) {
|
|
|
34629
35691
|
if (json) writeJson2(result);
|
|
34630
35692
|
else inspectHuman(result);
|
|
34631
35693
|
} catch (error) {
|
|
34632
|
-
handleError(shareCliError(error));
|
|
35694
|
+
return handleError(shareCliError(error));
|
|
34633
35695
|
}
|
|
34634
35696
|
});
|
|
34635
35697
|
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) => {
|
|
@@ -34677,7 +35739,7 @@ function registerShareCommand(program) {
|
|
|
34677
35739
|
if (json) receiveJson(result, output2);
|
|
34678
35740
|
else receiveHuman(output2);
|
|
34679
35741
|
} catch (error) {
|
|
34680
|
-
handleError(shareCliError(error));
|
|
35742
|
+
return handleError(shareCliError(error));
|
|
34681
35743
|
}
|
|
34682
35744
|
});
|
|
34683
35745
|
share.command("list").description("List encrypted sender history without complete bearer URLs").option("--json", "Print versioned redacted JSON").action(async (options, command) => {
|
|
@@ -34688,7 +35750,7 @@ function registerShareCommand(program) {
|
|
|
34688
35750
|
if (json) writeJson2({ protocol: "tinycloud-share", version: 1, shares: result });
|
|
34689
35751
|
else process.stdout.write(result.map((item) => `${item.shareId} ${item.target} ${item.expiresAt}`).join("\n") + (result.length ? "\n" : ""));
|
|
34690
35752
|
} catch (error) {
|
|
34691
|
-
handleError(shareCliError(error));
|
|
35753
|
+
return handleError(shareCliError(error));
|
|
34692
35754
|
}
|
|
34693
35755
|
});
|
|
34694
35756
|
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) => {
|
|
@@ -34700,7 +35762,7 @@ function registerShareCommand(program) {
|
|
|
34700
35762
|
if (json) writeJson2({ protocol: "tinycloud-share", version: 1, share: result });
|
|
34701
35763
|
else writeJson2(result);
|
|
34702
35764
|
} catch (error) {
|
|
34703
|
-
handleError(shareCliError(error));
|
|
35765
|
+
return handleError(shareCliError(error));
|
|
34704
35766
|
}
|
|
34705
35767
|
});
|
|
34706
35768
|
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) => {
|
|
@@ -34727,7 +35789,7 @@ function registerShareCommand(program) {
|
|
|
34727
35789
|
if (result.reason === "delivery-window-expired") process.stderr.write(NOTIFY_WINDOW_MESSAGE);
|
|
34728
35790
|
if (result.state === "partial-failure") process.exitCode = 9;
|
|
34729
35791
|
} catch (error) {
|
|
34730
|
-
handleError(shareCliError(error, "notify"));
|
|
35792
|
+
return handleError(shareCliError(error, "notify"));
|
|
34731
35793
|
}
|
|
34732
35794
|
});
|
|
34733
35795
|
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) => {
|
|
@@ -34744,7 +35806,7 @@ function registerShareCommand(program) {
|
|
|
34744
35806
|
else process.stdout.write(`${result.state}
|
|
34745
35807
|
`);
|
|
34746
35808
|
} catch (error) {
|
|
34747
|
-
handleError(shareCliError(error));
|
|
35809
|
+
return handleError(shareCliError(error));
|
|
34748
35810
|
}
|
|
34749
35811
|
});
|
|
34750
35812
|
}
|
|
@@ -34759,7 +35821,7 @@ init_host();
|
|
|
34759
35821
|
init_theme();
|
|
34760
35822
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
34761
35823
|
import { mkdir as mkdir5, writeFile as writeFile4 } from "fs/promises";
|
|
34762
|
-
import { dirname as
|
|
35824
|
+
import { dirname as dirname6 } from "path";
|
|
34763
35825
|
function didWithoutFragment2(did) {
|
|
34764
35826
|
const fragment = did.indexOf("#");
|
|
34765
35827
|
return fragment === -1 ? did : did.slice(0, fragment);
|
|
@@ -34790,7 +35852,7 @@ function registerSpaceCommand(program) {
|
|
|
34790
35852
|
}
|
|
34791
35853
|
}
|
|
34792
35854
|
} catch (error) {
|
|
34793
|
-
handleError(error);
|
|
35855
|
+
return handleError(error);
|
|
34794
35856
|
}
|
|
34795
35857
|
});
|
|
34796
35858
|
space.command("create <name>").alias("host").description("Create (host) one of your owned spaces by name").action(async (name, _options, cmd) => {
|
|
@@ -34801,7 +35863,7 @@ function registerSpaceCommand(program) {
|
|
|
34801
35863
|
const spaceId = await node.hostOwnedSpace(name);
|
|
34802
35864
|
outputJson({ spaceId, name, hosted: true });
|
|
34803
35865
|
} catch (error) {
|
|
34804
|
-
handleError(error);
|
|
35866
|
+
return handleError(error);
|
|
34805
35867
|
}
|
|
34806
35868
|
});
|
|
34807
35869
|
space.command("host-request <name>").description("Emit a request asking the space owner to host it (delegate-only)").option("--emit [file]", "Write the request artifact to file (or stdout when no path)").addHelpText("after", `
|
|
@@ -34848,7 +35910,7 @@ it directly with \`tc space host <name>\` (no request needed).
|
|
|
34848
35910
|
};
|
|
34849
35911
|
await emitHostRequestArtifact(artifact, options.emit);
|
|
34850
35912
|
} catch (error) {
|
|
34851
|
-
handleError(error);
|
|
35913
|
+
return handleError(error);
|
|
34852
35914
|
}
|
|
34853
35915
|
});
|
|
34854
35916
|
space.command("info [space-id]").description("Get space info").action(async (spaceId, _options, cmd) => {
|
|
@@ -34868,7 +35930,7 @@ it directly with \`tc space host <name>\` (no request needed).
|
|
|
34868
35930
|
host: ctx.host
|
|
34869
35931
|
});
|
|
34870
35932
|
} catch (error) {
|
|
34871
|
-
handleError(error);
|
|
35933
|
+
return handleError(error);
|
|
34872
35934
|
}
|
|
34873
35935
|
});
|
|
34874
35936
|
space.command("switch <name>").description("Switch active space").action(async (name, _options, cmd) => {
|
|
@@ -34878,13 +35940,13 @@ it directly with \`tc space host <name>\` (no request needed).
|
|
|
34878
35940
|
await ProfileManager.updateProfile(ctx.profile, (profile) => ({ ...profile, spaceName: name }));
|
|
34879
35941
|
outputJson({ profile: ctx.profile, spaceName: name, switched: true });
|
|
34880
35942
|
} catch (error) {
|
|
34881
|
-
handleError(error);
|
|
35943
|
+
return handleError(error);
|
|
34882
35944
|
}
|
|
34883
35945
|
});
|
|
34884
35946
|
}
|
|
34885
35947
|
async function emitHostRequestArtifact(artifact, emitOption) {
|
|
34886
35948
|
if (typeof emitOption === "string" && emitOption.length > 0) {
|
|
34887
|
-
await mkdir5(
|
|
35949
|
+
await mkdir5(dirname6(emitOption), { recursive: true });
|
|
34888
35950
|
await writeFile4(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
34889
35951
|
outputJson({
|
|
34890
35952
|
emitted: true,
|
|
@@ -34991,7 +36053,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
34991
36053
|
}
|
|
34992
36054
|
}
|
|
34993
36055
|
} catch (error) {
|
|
34994
|
-
handleError(error);
|
|
36056
|
+
return handleError(error);
|
|
34995
36057
|
}
|
|
34996
36058
|
});
|
|
34997
36059
|
sql.command("execute <sql>").description("Run a write or schema statement").option("--db <name>", "SQLite database name within the current space", "default").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("--params <json>", "Bind parameters as a JSON array for ? placeholders").addHelpText("after", `
|
|
@@ -35024,7 +36086,7 @@ Output:
|
|
|
35024
36086
|
lastInsertRowId: result.data.lastInsertRowId
|
|
35025
36087
|
});
|
|
35026
36088
|
} catch (error) {
|
|
35027
|
-
handleError(error);
|
|
36089
|
+
return handleError(error);
|
|
35028
36090
|
}
|
|
35029
36091
|
});
|
|
35030
36092
|
sql.command("export").description("Export a SQLite database as a binary .db file").option("--db <name>", "SQLite database name within the current space", "default").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("-o, --output <file>", "Output file path", "export.db").addHelpText("after", `
|
|
@@ -35059,7 +36121,7 @@ Output:
|
|
|
35059
36121
|
sizeHuman: formatBytes(blob.size)
|
|
35060
36122
|
});
|
|
35061
36123
|
} catch (error) {
|
|
35062
|
-
handleError(error);
|
|
36124
|
+
return handleError(error);
|
|
35063
36125
|
}
|
|
35064
36126
|
});
|
|
35065
36127
|
sql.command("copy").description("Copy rows between SQL databases (optionally across spaces)").requiredOption("--from-db <name>", "Source database name").requiredOption("--to-db <name>", "Destination database name").option("--from-space <name|uri>", "Source space (defaults to primary)").option("--to-space <name|uri>", "Destination space (defaults to primary)").option("--table <name...>", "Restrict copy to specific tables (repeat or comma-separated)").option("--dry-run", "Print the plan without writing", false).addHelpText("after", `
|
|
@@ -35162,7 +36224,7 @@ Notes:
|
|
|
35162
36224
|
tables: plan.map((p) => ({ table: p.table, rowsRead: p.rows, rowsWritten: p.copied }))
|
|
35163
36225
|
});
|
|
35164
36226
|
} catch (error) {
|
|
35165
|
-
handleError(error);
|
|
36227
|
+
return handleError(error);
|
|
35166
36228
|
}
|
|
35167
36229
|
});
|
|
35168
36230
|
}
|
|
@@ -35218,7 +36280,7 @@ function registerStatusCommand(program) {
|
|
|
35218
36280
|
}
|
|
35219
36281
|
process.stdout.write(formatStatus(summary));
|
|
35220
36282
|
} catch (error) {
|
|
35221
|
-
handleError(error);
|
|
36283
|
+
return handleError(error);
|
|
35222
36284
|
}
|
|
35223
36285
|
});
|
|
35224
36286
|
}
|
|
@@ -35519,10 +36581,10 @@ function formatSession(session) {
|
|
|
35519
36581
|
return `${theme.success("present")}${formatExpiresAt(session.expiresAt)}`;
|
|
35520
36582
|
}
|
|
35521
36583
|
function formatDelegation2(delegation) {
|
|
35522
|
-
const
|
|
36584
|
+
const state2 = delegation.expired === true ? theme.warn("expired") : theme.success("active");
|
|
35523
36585
|
return [
|
|
35524
36586
|
delegation.cid,
|
|
35525
|
-
|
|
36587
|
+
state2,
|
|
35526
36588
|
formatExpiresAt(delegation.expiresAt).trim(),
|
|
35527
36589
|
plural(delegation.permissions.length, "permission")
|
|
35528
36590
|
].filter(Boolean).join(" ");
|
|
@@ -35599,13 +36661,13 @@ function registerUpgradeCommand(program) {
|
|
|
35599
36661
|
`);
|
|
35600
36662
|
}
|
|
35601
36663
|
} catch (error) {
|
|
35602
|
-
handleError(error);
|
|
36664
|
+
return handleError(error);
|
|
35603
36665
|
}
|
|
35604
36666
|
});
|
|
35605
36667
|
}
|
|
35606
36668
|
|
|
35607
36669
|
// src/commands/vault.ts
|
|
35608
|
-
import { readFile as
|
|
36670
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
35609
36671
|
init_profiles();
|
|
35610
36672
|
init_formatter();
|
|
35611
36673
|
init_errors();
|
|
@@ -35648,7 +36710,7 @@ function registerVaultCommand(program) {
|
|
|
35648
36710
|
await withSpinner("Unlocking vault...", () => unlockVault(node, privateKey));
|
|
35649
36711
|
outputJson({ unlocked: true });
|
|
35650
36712
|
} catch (error) {
|
|
35651
|
-
handleError(error);
|
|
36713
|
+
return handleError(error);
|
|
35652
36714
|
}
|
|
35653
36715
|
});
|
|
35654
36716
|
vault.command("put <key> [value]").description("Encrypt and store a value").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (key, value, options, cmd) => {
|
|
@@ -35667,7 +36729,7 @@ function registerVaultCommand(program) {
|
|
|
35667
36729
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
35668
36730
|
}
|
|
35669
36731
|
if (options.file) {
|
|
35670
|
-
putValue = new Uint8Array(await
|
|
36732
|
+
putValue = new Uint8Array(await readFile12(options.file));
|
|
35671
36733
|
} else if (options.stdin) {
|
|
35672
36734
|
putValue = new Uint8Array(await readStdin4());
|
|
35673
36735
|
} else {
|
|
@@ -35679,7 +36741,7 @@ function registerVaultCommand(program) {
|
|
|
35679
36741
|
}
|
|
35680
36742
|
outputJson({ key, written: true });
|
|
35681
36743
|
} catch (error) {
|
|
35682
|
-
handleError(error);
|
|
36744
|
+
return handleError(error);
|
|
35683
36745
|
}
|
|
35684
36746
|
});
|
|
35685
36747
|
vault.command("get <key>").description("Decrypt and retrieve a value").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (key, options, cmd) => {
|
|
@@ -35689,7 +36751,7 @@ function registerVaultCommand(program) {
|
|
|
35689
36751
|
const privateKey = resolvePrivateKey(options);
|
|
35690
36752
|
const node = await ensureAuthenticated(ctx, { privateKey });
|
|
35691
36753
|
await withSpinner("Unlocking vault...", () => unlockVault(node, privateKey));
|
|
35692
|
-
const result = await withSpinner(`Getting ${key}...`, () => node.vault.get(key));
|
|
36754
|
+
const result = await withSpinner(`Getting ${key}...`, () => node.vault.get(key, { source: "network" }));
|
|
35693
36755
|
if (!result.ok) {
|
|
35694
36756
|
if (result.error.code === "NOT_FOUND") {
|
|
35695
36757
|
throw new CLIError("NOT_FOUND", `Key "${key}" not found`, ExitCode.NOT_FOUND);
|
|
@@ -35713,7 +36775,7 @@ function registerVaultCommand(program) {
|
|
|
35713
36775
|
data: data instanceof Uint8Array ? Buffer.from(data).toString("base64") : data
|
|
35714
36776
|
});
|
|
35715
36777
|
} catch (error) {
|
|
35716
|
-
handleError(error);
|
|
36778
|
+
return handleError(error);
|
|
35717
36779
|
}
|
|
35718
36780
|
});
|
|
35719
36781
|
vault.command("delete <key>").description("Delete an encrypted key").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (key, options, cmd) => {
|
|
@@ -35729,7 +36791,7 @@ function registerVaultCommand(program) {
|
|
|
35729
36791
|
}
|
|
35730
36792
|
outputJson({ key, deleted: true });
|
|
35731
36793
|
} catch (error) {
|
|
35732
|
-
handleError(error);
|
|
36794
|
+
return handleError(error);
|
|
35733
36795
|
}
|
|
35734
36796
|
});
|
|
35735
36797
|
vault.command("list").description("List vault keys").option("--prefix <prefix>", "Filter by key prefix").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (options, cmd) => {
|
|
@@ -35739,7 +36801,7 @@ function registerVaultCommand(program) {
|
|
|
35739
36801
|
const privateKey = resolvePrivateKey(options);
|
|
35740
36802
|
const node = await ensureAuthenticated(ctx, { privateKey });
|
|
35741
36803
|
await withSpinner("Unlocking vault...", () => unlockVault(node, privateKey));
|
|
35742
|
-
const listOptions = options.prefix ? { prefix: options.prefix } :
|
|
36804
|
+
const listOptions = options.prefix ? { prefix: options.prefix, source: "network" } : { source: "network" };
|
|
35743
36805
|
const result = await withSpinner("Listing vault keys...", () => node.vault.list(listOptions));
|
|
35744
36806
|
if (!result.ok) {
|
|
35745
36807
|
throw cliErrorFromService(result.error);
|
|
@@ -35752,7 +36814,7 @@ function registerVaultCommand(program) {
|
|
|
35752
36814
|
prefix: options.prefix ?? null
|
|
35753
36815
|
});
|
|
35754
36816
|
} catch (error) {
|
|
35755
|
-
handleError(error);
|
|
36817
|
+
return handleError(error);
|
|
35756
36818
|
}
|
|
35757
36819
|
});
|
|
35758
36820
|
vault.command("head <key>").description("Get metadata for a vault key").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (key, options, cmd) => {
|
|
@@ -35776,13 +36838,13 @@ function registerVaultCommand(program) {
|
|
|
35776
36838
|
metadata: result.data.headers ?? result.data
|
|
35777
36839
|
});
|
|
35778
36840
|
} catch (error) {
|
|
35779
|
-
handleError(error);
|
|
36841
|
+
return handleError(error);
|
|
35780
36842
|
}
|
|
35781
36843
|
});
|
|
35782
36844
|
}
|
|
35783
36845
|
|
|
35784
36846
|
// src/commands/vars.ts
|
|
35785
|
-
import { readFile as
|
|
36847
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
35786
36848
|
init_profiles();
|
|
35787
36849
|
init_formatter();
|
|
35788
36850
|
init_errors();
|
|
@@ -35827,7 +36889,7 @@ function registerVarsCommand(program) {
|
|
|
35827
36889
|
count: keyList.length
|
|
35828
36890
|
});
|
|
35829
36891
|
} catch (error) {
|
|
35830
|
-
handleError(error);
|
|
36892
|
+
return handleError(error);
|
|
35831
36893
|
}
|
|
35832
36894
|
});
|
|
35833
36895
|
vars.command("get <name>").description("Get a variable value").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
|
|
@@ -35869,7 +36931,7 @@ function registerVarsCommand(program) {
|
|
|
35869
36931
|
}
|
|
35870
36932
|
outputJson({ name, value });
|
|
35871
36933
|
} catch (error) {
|
|
35872
|
-
handleError(error);
|
|
36934
|
+
return handleError(error);
|
|
35873
36935
|
}
|
|
35874
36936
|
});
|
|
35875
36937
|
vars.command("put <name> [value]").description("Set a variable").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, value, options, cmd) => {
|
|
@@ -35887,7 +36949,7 @@ function registerVarsCommand(program) {
|
|
|
35887
36949
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
35888
36950
|
}
|
|
35889
36951
|
if (options.file) {
|
|
35890
|
-
varValue = await
|
|
36952
|
+
varValue = await readFile13(options.file, "utf-8");
|
|
35891
36953
|
} else if (options.stdin) {
|
|
35892
36954
|
varValue = (await readStdin5()).toString("utf-8");
|
|
35893
36955
|
} else {
|
|
@@ -35904,7 +36966,7 @@ function registerVarsCommand(program) {
|
|
|
35904
36966
|
}
|
|
35905
36967
|
outputJson({ name, written: true });
|
|
35906
36968
|
} catch (error) {
|
|
35907
|
-
handleError(error);
|
|
36969
|
+
return handleError(error);
|
|
35908
36970
|
}
|
|
35909
36971
|
});
|
|
35910
36972
|
vars.command("delete <name>").description("Delete a variable").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
|
|
@@ -35920,7 +36982,7 @@ function registerVarsCommand(program) {
|
|
|
35920
36982
|
}
|
|
35921
36983
|
outputJson({ name, deleted: true });
|
|
35922
36984
|
} catch (error) {
|
|
35923
|
-
handleError(error);
|
|
36985
|
+
return handleError(error);
|
|
35924
36986
|
}
|
|
35925
36987
|
});
|
|
35926
36988
|
}
|