@tinycloud/cli 1.3.0-beta.36 → 1.3.0-beta.38
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 +3 -0
- package/dist/courier-entry.js +1261 -79
- package/dist/courier-entry.js.map +1 -1
- package/dist/index.js +57 -22
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +842 -166
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +4 -4
- package/skills/tc-cli/AUTH.md +2 -0
- package/skills/tc-cli/REFERENCE.md +3 -1
- package/skills/tc-cli/SKILL.md +30 -0
package/dist/legacy-entry.js
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
tinycloudConfigPath,
|
|
38
38
|
tinycloudHomePath
|
|
39
39
|
} from "@tinycloud/operations/state";
|
|
40
|
-
var CONFIG_DIR, PROFILES_DIR, CONFIG_FILE, DEFAULT_HOST, DEFAULT_OPENKEY_HOST, DEFAULT_OPENKEY_DEVICE_API_HOST, DEFAULT_SHARE_ORIGIN, DEFAULT_PROFILE, DEFAULT_CHAIN_ID, PROFILE_COMMIT_LOCK_TIMEOUT_MS, ExitCode;
|
|
40
|
+
var CONFIG_DIR, PROFILES_DIR, CONFIG_FILE, DEFAULT_HOST, DEFAULT_OPENKEY_HOST, DEFAULT_OPENKEY_DEVICE_API_HOST, DEFAULT_SHARE_ORIGIN, DEFAULT_PROFILE, DEFAULT_CHAIN_ID, PROFILE_COMMIT_LOCK_TIMEOUT_MS, ExitCode, OPENKEY_SPACE_SETUP_RETRY_HINT;
|
|
41
41
|
var init_constants = __esm({
|
|
42
42
|
"src/config/constants.ts"() {
|
|
43
43
|
"use strict";
|
|
@@ -67,6 +67,7 @@ var init_constants = __esm({
|
|
|
67
67
|
/** Interrupted by SIGINT/SIGTERM (`CANCELLED`). */
|
|
68
68
|
CANCELLED: 130
|
|
69
69
|
};
|
|
70
|
+
OPENKEY_SPACE_SETUP_RETRY_HINT = "Wait a minute and retry; approving again in OpenKey re-runs the setup.";
|
|
70
71
|
}
|
|
71
72
|
});
|
|
72
73
|
|
|
@@ -281,6 +282,40 @@ var init_replication_registry = __esm({
|
|
|
281
282
|
}
|
|
282
283
|
});
|
|
283
284
|
|
|
285
|
+
// src/output/safe-text.ts
|
|
286
|
+
function escape(match) {
|
|
287
|
+
return `\\u{${match.codePointAt(0).toString(16)}}`;
|
|
288
|
+
}
|
|
289
|
+
function safeBlock(text) {
|
|
290
|
+
return text.replace(UNSAFE_BLOCK, escape);
|
|
291
|
+
}
|
|
292
|
+
function safeInline(text) {
|
|
293
|
+
return text.replace(UNSAFE_INLINE, escape);
|
|
294
|
+
}
|
|
295
|
+
function safeClipped(text, max = ECHO_FIELD_MAX) {
|
|
296
|
+
const escaped = Array.from(safeInline(text));
|
|
297
|
+
return escaped.length <= max ? escaped.join("") : `${escaped.slice(0, max).join("")}\u2026`;
|
|
298
|
+
}
|
|
299
|
+
function safeClippedList(items) {
|
|
300
|
+
const shown = items.slice(0, ECHO_LIST_MAX).map((item) => `"${safeClipped(item)}"`).join(", ");
|
|
301
|
+
return items.length > ECHO_LIST_MAX ? `${shown} \u2026 and ${items.length - ECHO_LIST_MAX} more` : shown;
|
|
302
|
+
}
|
|
303
|
+
function safeJsonStringify(value, space) {
|
|
304
|
+
const text = JSON.stringify(value, null, space);
|
|
305
|
+
return text === void 0 ? text : text.replace(JSON_UNSAFE, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
306
|
+
}
|
|
307
|
+
var UNSAFE_BLOCK, UNSAFE_INLINE, ECHO_FIELD_MAX, ECHO_LIST_MAX, JSON_UNSAFE;
|
|
308
|
+
var init_safe_text = __esm({
|
|
309
|
+
"src/output/safe-text.ts"() {
|
|
310
|
+
"use strict";
|
|
311
|
+
UNSAFE_BLOCK = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u061c\u200b-\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069\ufeff]/g;
|
|
312
|
+
UNSAFE_INLINE = /[\u0000-\u001f\u007f-\u009f\u061c\u200b-\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069\ufeff]/g;
|
|
313
|
+
ECHO_FIELD_MAX = 80;
|
|
314
|
+
ECHO_LIST_MAX = 5;
|
|
315
|
+
JSON_UNSAFE = /[\u007f-\u009f\u061c\u200b-\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069\ufeff]/g;
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
284
319
|
// src/output/theme.ts
|
|
285
320
|
import chalk from "chalk";
|
|
286
321
|
var TC_PALETTE, theme;
|
|
@@ -363,7 +398,7 @@ var init_warnings = __esm({
|
|
|
363
398
|
import ora from "ora";
|
|
364
399
|
import { CLI_NAME } from "@tinycloud/operations/cli-name";
|
|
365
400
|
function outputJson(data) {
|
|
366
|
-
process.stdout.write(
|
|
401
|
+
process.stdout.write(safeJsonStringify(data, 2) + "\n");
|
|
367
402
|
}
|
|
368
403
|
function warningLine(warnings) {
|
|
369
404
|
const which = warnings.map((warning) => `${warning.grantCid ?? "unidentified"}: ${warning.reason}`).join(", ");
|
|
@@ -374,12 +409,12 @@ function outputError(code4, message, hint, details = {}) {
|
|
|
374
409
|
const { meta, warnings = [], setup, recovery, progress } = details;
|
|
375
410
|
if (!shouldOutputJson()) {
|
|
376
411
|
process.stderr.write(
|
|
377
|
-
`${theme.error("\u2717")} ${theme.label(code4)}: ${message}
|
|
412
|
+
`${theme.error("\u2717")} ${theme.label(safeInline(code4))}: ${safeBlock(message)}
|
|
378
413
|
`
|
|
379
414
|
);
|
|
380
415
|
if (hint) {
|
|
381
416
|
for (const line of hint.split("\n")) {
|
|
382
|
-
process.stderr.write(` ${theme.hint(line)}
|
|
417
|
+
process.stderr.write(` ${theme.hint(safeBlock(line))}
|
|
383
418
|
`);
|
|
384
419
|
}
|
|
385
420
|
}
|
|
@@ -395,13 +430,13 @@ function outputError(code4, message, hint, details = {}) {
|
|
|
395
430
|
if (progress) payload.error.progress = progress;
|
|
396
431
|
if (meta) payload.error.meta = meta;
|
|
397
432
|
if (warnings.length > 0) payload.error.warnings = warnings;
|
|
398
|
-
process.stderr.write(
|
|
433
|
+
process.stderr.write(safeJsonStringify(payload, 2) + "\n");
|
|
399
434
|
}
|
|
400
435
|
}
|
|
401
436
|
function outputWarnings(warnings) {
|
|
402
437
|
const combined = mergeWarnings(drainWarnings(), operationWarnings(warnings));
|
|
403
438
|
if (combined.length === 0) return;
|
|
404
|
-
process.stderr.write(shouldOutputJson() ? `${
|
|
439
|
+
process.stderr.write(shouldOutputJson() ? `${safeJsonStringify({ warnings: combined })}
|
|
405
440
|
` : `${theme.warn("!")} ${warningLine(combined)}
|
|
406
441
|
`);
|
|
407
442
|
}
|
|
@@ -479,6 +514,7 @@ var WARNING_CODE, WARNING_REASON, DELEGATION_CID, JSON_REQUESTED;
|
|
|
479
514
|
var init_formatter = __esm({
|
|
480
515
|
"src/output/formatter.ts"() {
|
|
481
516
|
"use strict";
|
|
517
|
+
init_safe_text();
|
|
482
518
|
init_theme();
|
|
483
519
|
init_warnings();
|
|
484
520
|
WARNING_CODE = /^[A-Z][A-Z_]{0,63}$/;
|
|
@@ -881,6 +917,13 @@ var init_types = __esm({
|
|
|
881
917
|
|
|
882
918
|
// src/lib/space.ts
|
|
883
919
|
import { CLI_NAME as CLI_NAME3 } from "@tinycloud/operations/cli-name";
|
|
920
|
+
function invalidSpaceError(input) {
|
|
921
|
+
return new CLIError(
|
|
922
|
+
"INVALID_SPACE",
|
|
923
|
+
`Invalid space "${safeClipped(input)}". Use a short name ([A-Za-z0-9_-]) or a full tinycloud:... URI.`,
|
|
924
|
+
ExitCode.USAGE_ERROR
|
|
925
|
+
);
|
|
926
|
+
}
|
|
884
927
|
function canonicalizeAddress(address) {
|
|
885
928
|
const trimmed = address.trim();
|
|
886
929
|
return trimmed.startsWith("0x") ? `0x${trimmed.slice(2).toLowerCase()}` : trimmed.toLowerCase();
|
|
@@ -944,22 +987,10 @@ async function resolveSpaceUri(input, profileName2, options = {}) {
|
|
|
944
987
|
if (!effective) return void 0;
|
|
945
988
|
if (effective.startsWith("tinycloud:")) {
|
|
946
989
|
const parsed = parseSpaceUri(effective);
|
|
947
|
-
if (!parsed)
|
|
948
|
-
throw new CLIError(
|
|
949
|
-
"INVALID_SPACE",
|
|
950
|
-
`Invalid space "${effective}". Use a short name ([A-Za-z0-9_-]) or a full tinycloud:... URI.`,
|
|
951
|
-
ExitCode.USAGE_ERROR
|
|
952
|
-
);
|
|
953
|
-
}
|
|
990
|
+
if (!parsed) throw invalidSpaceError(effective);
|
|
954
991
|
return buildSpaceUri(parsed.owner, parsed.name);
|
|
955
992
|
}
|
|
956
|
-
if (!/^[A-Za-z0-9_-]+$/.test(effective))
|
|
957
|
-
throw new CLIError(
|
|
958
|
-
"INVALID_SPACE",
|
|
959
|
-
`Invalid space "${effective}". Use a short name ([A-Za-z0-9_-]) or a full tinycloud:... URI.`,
|
|
960
|
-
ExitCode.USAGE_ERROR
|
|
961
|
-
);
|
|
962
|
-
}
|
|
993
|
+
if (!/^[A-Za-z0-9_-]+$/.test(effective)) throw invalidSpaceError(effective);
|
|
963
994
|
const session = await ProfileManager.getSession(profileName2);
|
|
964
995
|
const address = resolveAddress(profile, session);
|
|
965
996
|
const chainId = resolveChainId(profile, session);
|
|
@@ -970,6 +1001,7 @@ var init_space = __esm({
|
|
|
970
1001
|
"use strict";
|
|
971
1002
|
init_errors();
|
|
972
1003
|
init_constants();
|
|
1004
|
+
init_safe_text();
|
|
973
1005
|
init_profiles();
|
|
974
1006
|
}
|
|
975
1007
|
});
|
|
@@ -9604,17 +9636,25 @@ async function isRootAuthority(spaceUri, profileName2) {
|
|
|
9604
9636
|
const selfAddr = await resolveLocalAddress(profile, profileName2);
|
|
9605
9637
|
return selfAddr !== null && selfAddr === ownerAddr;
|
|
9606
9638
|
}
|
|
9639
|
+
async function primarySpaceUri(profileName2) {
|
|
9640
|
+
const session = await ProfileManager.getSession(profileName2);
|
|
9641
|
+
if (typeof session?.spaceId === "string" && session.spaceId !== "") return session.spaceId;
|
|
9642
|
+
const profile = await ProfileManager.getProfile(profileName2);
|
|
9643
|
+
return typeof profile.spaceId === "string" && profile.spaceId !== "" ? profile.spaceId : void 0;
|
|
9644
|
+
}
|
|
9607
9645
|
function spaceNameFromUri(spaceUri) {
|
|
9608
9646
|
return spaceUri.slice(spaceUri.lastIndexOf(":") + 1);
|
|
9609
9647
|
}
|
|
9610
|
-
async function unhostedSpaceError(error,
|
|
9611
|
-
if (!spaceUri) return null;
|
|
9648
|
+
async function unhostedSpaceError(error, requestedSpaceUri, profileName2) {
|
|
9612
9649
|
const status = error.meta?.status;
|
|
9613
9650
|
const isUnhosted = status === 404 && /space not found/i.test(error.message);
|
|
9614
9651
|
if (!isUnhosted) return null;
|
|
9652
|
+
const spaceUri = requestedSpaceUri ?? await primarySpaceUri(profileName2);
|
|
9653
|
+
if (!spaceUri) return null;
|
|
9615
9654
|
const spaceName = spaceNameFromUri(spaceUri);
|
|
9616
9655
|
const owner = await isRootAuthority(spaceUri, profileName2);
|
|
9617
|
-
const
|
|
9656
|
+
const openKeyOwner = owner && resolveProfilePosture(await ProfileManager.getProfile(profileName2)) === "owner-openkey";
|
|
9657
|
+
const hint = openKeyOwner ? `OpenKey is still setting up your space. ${OPENKEY_SPACE_SETUP_RETRY_HINT}` : owner ? [
|
|
9618
9658
|
"You are the owner. Host it once:",
|
|
9619
9659
|
` ${CLI_NAME8} space host ${spaceName}`,
|
|
9620
9660
|
"Then retry."
|
|
@@ -10353,7 +10393,7 @@ function formatSpace(space) {
|
|
|
10353
10393
|
init_profile_gate();
|
|
10354
10394
|
init_dist();
|
|
10355
10395
|
init_profiles();
|
|
10356
|
-
import { CLI_NAME as
|
|
10396
|
+
import { CLI_NAME as CLI_NAME16 } from "@tinycloud/operations/cli-name";
|
|
10357
10397
|
import { get as httpGet } from "http";
|
|
10358
10398
|
import { get as httpsGet } from "https";
|
|
10359
10399
|
import { spawn } from "child_process";
|
|
@@ -11886,6 +11926,132 @@ async function loginWithDeviceAuthorization(input) {
|
|
|
11886
11926
|
return { profile, result };
|
|
11887
11927
|
}
|
|
11888
11928
|
|
|
11929
|
+
// src/auth/space-setup.ts
|
|
11930
|
+
init_constants();
|
|
11931
|
+
init_sdk();
|
|
11932
|
+
init_errors();
|
|
11933
|
+
var SPACE_SETUP_TIMEOUT_MS = 15e4;
|
|
11934
|
+
var POLL_DELAYS_MS = [2e3, 3e3, 5e3, 8e3, 13e3, 15e3];
|
|
11935
|
+
var PROGRESS_INTERVAL_MS = 15e3;
|
|
11936
|
+
var SPACE_ERROR_MAX_LENGTH = 500;
|
|
11937
|
+
function spaceSetupReport(delegation) {
|
|
11938
|
+
const rawError = typeof delegation.spaceError === "string" ? delegation.spaceError : void 0;
|
|
11939
|
+
const error = rawError?.replace(/[\u0000-\u001f\u007f-\u009f\s]+/g, " ").trim().slice(0, SPACE_ERROR_MAX_LENGTH);
|
|
11940
|
+
const ready = delegation.spaceReady;
|
|
11941
|
+
if (ready === true) return void 0;
|
|
11942
|
+
const pending = delegation.spacePending === true || ready === null;
|
|
11943
|
+
if (!pending && !error && ready !== false) return void 0;
|
|
11944
|
+
return {
|
|
11945
|
+
ready: ready === false || ready === null ? ready : void 0,
|
|
11946
|
+
pending,
|
|
11947
|
+
...error ? { error } : {}
|
|
11948
|
+
};
|
|
11949
|
+
}
|
|
11950
|
+
async function awaitSpaceSetup(report, deps) {
|
|
11951
|
+
const write = deps.write ?? ((line) => {
|
|
11952
|
+
process.stderr.write(`${line}
|
|
11953
|
+
`);
|
|
11954
|
+
});
|
|
11955
|
+
const sleep = deps.sleep ?? ((ms2) => new Promise((resolve8) => setTimeout(resolve8, ms2)));
|
|
11956
|
+
const now = deps.now ?? Date.now;
|
|
11957
|
+
const timeoutMs = deps.timeoutMs ?? SPACE_SETUP_TIMEOUT_MS;
|
|
11958
|
+
const started = now();
|
|
11959
|
+
const deadline = started + timeoutMs;
|
|
11960
|
+
const probe = async () => deps.probe().catch(() => "transient");
|
|
11961
|
+
const unreadable = () => write(
|
|
11962
|
+
`Warning: this login cannot read your TinyCloud space to check it${report.error ? `; OpenKey reported: ${report.error}` : report.pending ? "; OpenKey is still setting it up" : ""}. If the first storage command fails with "Space not found", OpenKey is still setting up your space. ${OPENKEY_SPACE_SETUP_RETRY_HINT}`
|
|
11963
|
+
);
|
|
11964
|
+
let result = await probe();
|
|
11965
|
+
if (result === "hosted") {
|
|
11966
|
+
if (!report.pending && report.error) write(`Warning: OpenKey reported a problem finishing your account setup: ${report.error}`);
|
|
11967
|
+
return;
|
|
11968
|
+
}
|
|
11969
|
+
if (result === "unreadable") {
|
|
11970
|
+
unreadable();
|
|
11971
|
+
return;
|
|
11972
|
+
}
|
|
11973
|
+
if (!report.pending) {
|
|
11974
|
+
if (result === "transient") {
|
|
11975
|
+
write(`Warning: could not check whether your TinyCloud space is hosted${report.error ? `; OpenKey reported: ${report.error}` : ""}. ${OPENKEY_SPACE_SETUP_RETRY_HINT}`);
|
|
11976
|
+
return;
|
|
11977
|
+
}
|
|
11978
|
+
if (report.error) {
|
|
11979
|
+
throw new CLIError(
|
|
11980
|
+
"SPACE_SETUP_FAILED",
|
|
11981
|
+
`OpenKey could not finish setting up your space: ${report.error} Your login is saved, but storage commands will fail with "Space not found" until the space is hosted.`,
|
|
11982
|
+
ExitCode.ERROR,
|
|
11983
|
+
{ hint: "Approve again in OpenKey to re-run the setup, then retry." }
|
|
11984
|
+
);
|
|
11985
|
+
}
|
|
11986
|
+
write(
|
|
11987
|
+
'Warning: OpenKey did not set up your TinyCloud space (spaceReady: false), so storage commands will fail with "Space not found". Turn on Auto-Sign in OpenKey and approve again, or host the space from a wallet profile.'
|
|
11988
|
+
);
|
|
11989
|
+
return;
|
|
11990
|
+
}
|
|
11991
|
+
write(
|
|
11992
|
+
"Setting up your space\u2026 (OpenKey is hosting it; this can take a minute or two)" + (report.error ? ` OpenKey reported: ${report.error} It is still retrying.` : "")
|
|
11993
|
+
);
|
|
11994
|
+
let lastProgress = started;
|
|
11995
|
+
for (let attempt = 0; ; attempt++) {
|
|
11996
|
+
const current = now();
|
|
11997
|
+
if (current >= deadline) break;
|
|
11998
|
+
await sleep(Math.min(POLL_DELAYS_MS[Math.min(attempt, POLL_DELAYS_MS.length - 1)], deadline - current));
|
|
11999
|
+
result = await probe();
|
|
12000
|
+
if (result === "hosted") {
|
|
12001
|
+
write("Your space is ready.");
|
|
12002
|
+
return;
|
|
12003
|
+
}
|
|
12004
|
+
if (result === "unreadable") {
|
|
12005
|
+
unreadable();
|
|
12006
|
+
return;
|
|
12007
|
+
}
|
|
12008
|
+
const elapsed = now();
|
|
12009
|
+
if (elapsed < deadline && elapsed - lastProgress >= PROGRESS_INTERVAL_MS) {
|
|
12010
|
+
lastProgress = elapsed;
|
|
12011
|
+
write(`Still setting up your space\u2026 (${Math.round((elapsed - started) / 1e3)}s)`);
|
|
12012
|
+
}
|
|
12013
|
+
}
|
|
12014
|
+
throw new CLIError(
|
|
12015
|
+
"SPACE_SETUP_PENDING",
|
|
12016
|
+
`OpenKey is still setting up your space after ${Math.round(timeoutMs / 1e3)} seconds.${report.error ? ` OpenKey's last report: ${report.error}` : ""} Your login is saved, but storage commands will fail with "Space not found" until the space is hosted.`,
|
|
12017
|
+
ExitCode.ERROR,
|
|
12018
|
+
{ hint: OPENKEY_SPACE_SETUP_RETRY_HINT }
|
|
12019
|
+
);
|
|
12020
|
+
}
|
|
12021
|
+
function hasKvAction(permission, action) {
|
|
12022
|
+
return permission.actions.some((entry) => entry === action || entry === `tinycloud.kv/${action}`);
|
|
12023
|
+
}
|
|
12024
|
+
function readTarget(permissions) {
|
|
12025
|
+
const kv = (permissions ?? []).filter((permission) => permission.service === "tinycloud.kv" || permission.service === "kv");
|
|
12026
|
+
const list = kv.find((permission) => hasKvAction(permission, "list"));
|
|
12027
|
+
if (list) return { op: "list", prefix: list.path };
|
|
12028
|
+
const get = kv.find((permission) => hasKvAction(permission, "get"));
|
|
12029
|
+
if (!get) return void 0;
|
|
12030
|
+
return { op: "get", key: get.path === "" || get.path.endsWith("/") ? `${get.path}.tinycloud-space-probe` : get.path };
|
|
12031
|
+
}
|
|
12032
|
+
var UNHOSTED = /space not found/i;
|
|
12033
|
+
function kvSpaceProbe(ctx, permissions, restore2 = restoreProfileSessionNode) {
|
|
12034
|
+
const target = readTarget(permissions);
|
|
12035
|
+
let restored;
|
|
12036
|
+
return async () => {
|
|
12037
|
+
if (!target) return "unreadable";
|
|
12038
|
+
restored ??= await restore2(ctx);
|
|
12039
|
+
if ("unavailable" in restored) return "unreadable";
|
|
12040
|
+
const kv = restored.node.kv;
|
|
12041
|
+
const result = target.op === "list" ? await kv.list({ prefix: target.prefix, limit: 1 }) : await kv.get(target.key);
|
|
12042
|
+
if (result.ok) return "hosted";
|
|
12043
|
+
const status = result.error.meta?.status;
|
|
12044
|
+
if (status === 404 && UNHOSTED.test(result.error.message)) return "unhosted";
|
|
12045
|
+
if (status === 401 || status === 403) return "unreadable";
|
|
12046
|
+
return result.error.code === "KV_NOT_FOUND" || status === 404 ? "hosted" : "transient";
|
|
12047
|
+
};
|
|
12048
|
+
}
|
|
12049
|
+
async function ensureSpaceSetup(ctx, delegation, permissions) {
|
|
12050
|
+
const report = spaceSetupReport(delegation);
|
|
12051
|
+
if (!report) return;
|
|
12052
|
+
await awaitSpaceSetup(report, { probe: kvSpaceProbe(ctx, permissions) });
|
|
12053
|
+
}
|
|
12054
|
+
|
|
11889
12055
|
// src/commands/auth.ts
|
|
11890
12056
|
init_raw_encryption();
|
|
11891
12057
|
init_owner_did();
|
|
@@ -12212,7 +12378,7 @@ async function cidBoundOwnerSpace(record3, ownerDid, node, host) {
|
|
|
12212
12378
|
try {
|
|
12213
12379
|
const signed = parseSignedCompactUcanAttenuation2(withoutBearer2(authorization), delegation.cid);
|
|
12214
12380
|
for (const resource of Object.keys(signed.payload.att)) {
|
|
12215
|
-
const space = /^(tinycloud:[^/]+)\/[^/]
|
|
12381
|
+
const space = /^(tinycloud:[^/]+)\/[^/]+(?:\/.*)?$/.exec(resource)?.[1];
|
|
12216
12382
|
if (space && ownerDidFromSpace(space) && normalizeDid(ownerDidFromSpace(space)) === normalizeDid(ownerDid)) {
|
|
12217
12383
|
return space;
|
|
12218
12384
|
}
|
|
@@ -12615,6 +12781,290 @@ function approvalNotices(groups) {
|
|
|
12615
12781
|
};
|
|
12616
12782
|
}
|
|
12617
12783
|
|
|
12784
|
+
// src/lib/apps-agent-preset.ts
|
|
12785
|
+
init_constants();
|
|
12786
|
+
init_errors();
|
|
12787
|
+
init_safe_text();
|
|
12788
|
+
init_raw_encryption();
|
|
12789
|
+
init_space();
|
|
12790
|
+
import { DEFAULT_EXPIRY as DEFAULT_EXPIRY2, parseExpiry as parseExpiry3, SERVICE_LONG_TO_SHORT as SERVICE_LONG_TO_SHORT3, VAULT_PERMISSION_SERVICE as VAULT_PERMISSION_SERVICE2 } from "@tinycloud/sdk-core";
|
|
12791
|
+
import { CLI_NAME as CLI_NAME15 } from "@tinycloud/operations/cli-name";
|
|
12792
|
+
var APPS_AGENT_PRESET = "apps-agent";
|
|
12793
|
+
var APPS_AGENT_SPACE = "applications";
|
|
12794
|
+
var APPS_AGENT_EXPIRY_DAYS = 7;
|
|
12795
|
+
var APPS_AGENT_EXPIRY = `${APPS_AGENT_EXPIRY_DAYS}d`;
|
|
12796
|
+
var APPS_AGENT_MAX_APPS = 5;
|
|
12797
|
+
var DAY_MS2 = 864e5;
|
|
12798
|
+
var APPS_AGENT_VARIANTS = {
|
|
12799
|
+
read: {
|
|
12800
|
+
grants: [
|
|
12801
|
+
{ service: "tinycloud.kv", actions: ["tinycloud.kv/get", "tinycloud.kv/list"] },
|
|
12802
|
+
{ service: "tinycloud.sql", actions: ["tinycloud.sql/read"] }
|
|
12803
|
+
],
|
|
12804
|
+
perAppOnly: false,
|
|
12805
|
+
changesStructure: false
|
|
12806
|
+
},
|
|
12807
|
+
write: {
|
|
12808
|
+
grants: [
|
|
12809
|
+
{ service: "tinycloud.kv", actions: ["tinycloud.kv/get", "tinycloud.kv/list", "tinycloud.kv/put"] },
|
|
12810
|
+
{ service: "tinycloud.sql", actions: ["tinycloud.sql/read", "tinycloud.sql/write"] }
|
|
12811
|
+
],
|
|
12812
|
+
perAppOnly: true,
|
|
12813
|
+
changesStructure: true
|
|
12814
|
+
}
|
|
12815
|
+
};
|
|
12816
|
+
var ACTION_WORDS = {
|
|
12817
|
+
"tinycloud.kv/get": "read files",
|
|
12818
|
+
"tinycloud.kv/list": "list files",
|
|
12819
|
+
"tinycloud.kv/put": "create and overwrite files",
|
|
12820
|
+
"tinycloud.kv/del": "delete files",
|
|
12821
|
+
"tinycloud.kv/metadata": "read file metadata",
|
|
12822
|
+
"tinycloud.sql/read": "read table rows",
|
|
12823
|
+
"tinycloud.sql/write": "add, change and delete table rows",
|
|
12824
|
+
"tinycloud.sql/schema": "create, change and drop tables",
|
|
12825
|
+
"tinycloud.sql/admin": "do anything in the database",
|
|
12826
|
+
"tinycloud.delegation/create": "can give other people or agents access on your behalf",
|
|
12827
|
+
"tinycloud.delegation/revoke": "can cancel access you gave",
|
|
12828
|
+
"tinycloud.delegation/*": "can do everything the delegation permissions allow, including giving access on to others",
|
|
12829
|
+
"tinycloud.kv/*": "do anything with files (read, write, delete)",
|
|
12830
|
+
"tinycloud.sql/*": "do anything in the database",
|
|
12831
|
+
"tinycloud.capabilities/read": "see which permissions this key holds"
|
|
12832
|
+
};
|
|
12833
|
+
var STRUCTURE_ACTIONS = /* @__PURE__ */ new Set([
|
|
12834
|
+
"tinycloud.sql/write",
|
|
12835
|
+
"tinycloud.sql/schema",
|
|
12836
|
+
"tinycloud.sql/admin",
|
|
12837
|
+
"tinycloud.sql/*"
|
|
12838
|
+
]);
|
|
12839
|
+
var APP_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
12840
|
+
var APP_ID_RULE = "an app id looks like xyz.example.app: letters, digits, dots, hyphens and underscores, no slashes or spaces";
|
|
12841
|
+
function isAppId(value) {
|
|
12842
|
+
return value.length <= 200 && APP_ID.test(value);
|
|
12843
|
+
}
|
|
12844
|
+
function appsAgentPermissions(mode, appIds = []) {
|
|
12845
|
+
const paths = appIds.length === 0 ? [""] : appIds.map((appId) => `${appId}/`);
|
|
12846
|
+
return APPS_AGENT_VARIANTS[mode].grants.flatMap(({ service, actions }) => paths.map((path) => ({ service, space: APPS_AGENT_SPACE, path, actions: [...actions] })));
|
|
12847
|
+
}
|
|
12848
|
+
function quoteApps(appIds) {
|
|
12849
|
+
return appIds.map((appId) => `\`${appId}\``).join(", ");
|
|
12850
|
+
}
|
|
12851
|
+
function lifetimePhrase(ms2) {
|
|
12852
|
+
if (ms2 >= DAY_MS2) {
|
|
12853
|
+
const days = Math.ceil(ms2 / DAY_MS2);
|
|
12854
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
12855
|
+
}
|
|
12856
|
+
const hours = Math.max(1, Math.ceil(ms2 / 36e5));
|
|
12857
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
12858
|
+
}
|
|
12859
|
+
function revokeSentence(lifetimeMs2) {
|
|
12860
|
+
return `It lasts up to ${lifetimePhrase(lifetimeMs2)}; revoke it any time with \`${CLI_NAME15} delegation revoke <id>\`.`;
|
|
12861
|
+
}
|
|
12862
|
+
function appsAgentScopeNotice(mode, appIds = []) {
|
|
12863
|
+
const variant = APPS_AGENT_VARIANTS[mode];
|
|
12864
|
+
const reads = variant.grants.some(({ actions }) => actions.includes("tinycloud.kv/put")) ? "read it and add and change its files and table rows" : "list and read its files and read its table rows";
|
|
12865
|
+
const scope = appIds.length === 0 ? `ALL your installed apps' data in the \`${APPS_AGENT_SPACE}\` space` : `the data of ${quoteApps(appIds)} (everything under ${appIds.map((appId) => `\`${appId}/\``).join(", ")}) in the \`${APPS_AGENT_SPACE}\` space`;
|
|
12866
|
+
const lines = [
|
|
12867
|
+
variant.changesStructure ? `This gives the agent access to ${scope}: it can ${reads}.` : `This gives the agent READ-ONLY access to ${scope}: it can ${reads}.`
|
|
12868
|
+
];
|
|
12869
|
+
if (variant.changesStructure) {
|
|
12870
|
+
lines.push(
|
|
12871
|
+
`Because of how TinyCloud permissions work today, it can also change or delete ${appIds.length === 1 ? "this app's" : "these apps'"} tables (structure) and data.`,
|
|
12872
|
+
"It cannot touch other apps, other spaces or secrets."
|
|
12873
|
+
);
|
|
12874
|
+
} else {
|
|
12875
|
+
lines.push("It cannot create, change or delete anything, and it cannot touch other spaces or secrets.");
|
|
12876
|
+
}
|
|
12877
|
+
lines.push(revokeSentence(APPS_AGENT_EXPIRY_DAYS * DAY_MS2));
|
|
12878
|
+
return lines.join("\n");
|
|
12879
|
+
}
|
|
12880
|
+
function usage(message) {
|
|
12881
|
+
return new CLIError("INVALID_ARGUMENT", message, ExitCode.USAGE_ERROR);
|
|
12882
|
+
}
|
|
12883
|
+
function resolveAppsAgentRequest(options) {
|
|
12884
|
+
const apps = options.app ?? [];
|
|
12885
|
+
if (options.preset === void 0) {
|
|
12886
|
+
if (apps.length > 0 || options.write === true) {
|
|
12887
|
+
throw usage(`--app and --write require --preset ${APPS_AGENT_PRESET}.`);
|
|
12888
|
+
}
|
|
12889
|
+
return void 0;
|
|
12890
|
+
}
|
|
12891
|
+
if (options.preset !== APPS_AGENT_PRESET) {
|
|
12892
|
+
throw usage(`Unknown --preset "${safeClipped(options.preset)}". Available: ${APPS_AGENT_PRESET}.`);
|
|
12893
|
+
}
|
|
12894
|
+
if ((options.cap?.length ?? 0) > 0 || options.permission !== void 0 || options.manifest !== void 0 || options.expiry !== void 0) {
|
|
12895
|
+
throw usage(
|
|
12896
|
+
`--preset ${APPS_AGENT_PRESET} is the whole request (${APPS_AGENT_EXPIRY}); it cannot be combined with --cap, --permission, --manifest or --expiry.`
|
|
12897
|
+
);
|
|
12898
|
+
}
|
|
12899
|
+
if (options.device === true) {
|
|
12900
|
+
throw usage(`--preset ${APPS_AGENT_PRESET} includes SQL, which device approval cannot grant. Use browser or --paste approval.`);
|
|
12901
|
+
}
|
|
12902
|
+
const mode = options.write === true ? "write" : "read";
|
|
12903
|
+
const appIds = [...new Set(apps)];
|
|
12904
|
+
for (const appId of appIds) {
|
|
12905
|
+
if (!isAppId(appId)) throw usage(`Invalid --app "${safeClipped(appId)}": ${APP_ID_RULE}.`);
|
|
12906
|
+
}
|
|
12907
|
+
if (appIds.length > APPS_AGENT_MAX_APPS) {
|
|
12908
|
+
throw usage(`--app can be repeated at most ${APPS_AGENT_MAX_APPS} times; for more apps, ask for read-only access to the whole space (leave --app off).`);
|
|
12909
|
+
}
|
|
12910
|
+
if (APPS_AGENT_VARIANTS[mode].perAppOnly && appIds.length === 0) {
|
|
12911
|
+
throw usage("--write needs --app <app_id>: write access is only given for named apps, never for the whole space.");
|
|
12912
|
+
}
|
|
12913
|
+
return { mode, appIds };
|
|
12914
|
+
}
|
|
12915
|
+
function lifetimeMs(value) {
|
|
12916
|
+
try {
|
|
12917
|
+
if (value === void 0) return parseExpiry3(DEFAULT_EXPIRY2);
|
|
12918
|
+
if (typeof value === "number") return Number.isFinite(value) && value > 0 ? value : void 0;
|
|
12919
|
+
return /^\d+$/.test(value.trim()) ? Number(value.trim()) : parseExpiry3(value);
|
|
12920
|
+
} catch {
|
|
12921
|
+
return void 0;
|
|
12922
|
+
}
|
|
12923
|
+
}
|
|
12924
|
+
function inApplicationsSpace(entry, applicationsSpace) {
|
|
12925
|
+
return normalizePkhIdentifier(entry.space ?? "") === normalizePkhIdentifier(applicationsSpace);
|
|
12926
|
+
}
|
|
12927
|
+
function sameActions(have, want) {
|
|
12928
|
+
return have.length === want.length && want.every((action) => have.includes(action));
|
|
12929
|
+
}
|
|
12930
|
+
function matchAppsAgentRequest(requested, requestedExpiry, applicationsSpace) {
|
|
12931
|
+
if (lifetimeMs(requestedExpiry) !== APPS_AGENT_EXPIRY_DAYS * DAY_MS2) return void 0;
|
|
12932
|
+
if (requested.some((entry) => entry.expiry !== void 0 || entry.caveats !== void 0)) return void 0;
|
|
12933
|
+
const kvPaths = requested.filter((entry) => entry.service === "tinycloud.kv").map((entry) => entry.path);
|
|
12934
|
+
const appIds = [];
|
|
12935
|
+
for (const path of new Set(kvPaths)) {
|
|
12936
|
+
if (path === "") continue;
|
|
12937
|
+
if (!path.endsWith("/") || !isAppId(path.slice(0, -1))) return void 0;
|
|
12938
|
+
appIds.push(path.slice(0, -1));
|
|
12939
|
+
}
|
|
12940
|
+
if (appIds.length > APPS_AGENT_MAX_APPS || appIds.length > 0 && kvPaths.includes("")) return void 0;
|
|
12941
|
+
for (const mode of ["read", "write"]) {
|
|
12942
|
+
if (APPS_AGENT_VARIANTS[mode].perAppOnly && appIds.length === 0) continue;
|
|
12943
|
+
const expected = appsAgentPermissions(mode, appIds);
|
|
12944
|
+
const exact = requested.length === expected.length && expected.every((want) => requested.some((have) => have.service === want.service && inApplicationsSpace(have, applicationsSpace) && have.path === want.path && sameActions(have.actions, want.actions)));
|
|
12945
|
+
if (exact) return { mode, appIds };
|
|
12946
|
+
}
|
|
12947
|
+
return void 0;
|
|
12948
|
+
}
|
|
12949
|
+
var RAW_ENCRYPTION_SPACE = "encryption";
|
|
12950
|
+
function badRequest(message) {
|
|
12951
|
+
return new CLIError("INVALID_AUTH_REQUEST", `${message} Nothing was signed.`, ExitCode.USAGE_ERROR);
|
|
12952
|
+
}
|
|
12953
|
+
var DID_SHAPE = /^did:[a-z0-9]+:[A-Za-z0-9._:%#-]+$/;
|
|
12954
|
+
function assertRequestFields(request) {
|
|
12955
|
+
if (typeof request.sessionDid !== "string" || request.sessionDid.length > 300 || !DID_SHAPE.test(request.sessionDid)) {
|
|
12956
|
+
throw badRequest("The request's sessionDid is not a valid DID.");
|
|
12957
|
+
}
|
|
12958
|
+
if (request.requestedExpiry !== void 0 && (typeof request.requestedExpiry !== "string" && typeof request.requestedExpiry !== "number" || lifetimeMs(request.requestedExpiry) === void 0)) {
|
|
12959
|
+
throw badRequest("The request's requestedExpiry is not a duration such as 7d or 12h.");
|
|
12960
|
+
}
|
|
12961
|
+
}
|
|
12962
|
+
function entriesForSigning(requested) {
|
|
12963
|
+
return requested.map((entry, index) => {
|
|
12964
|
+
const label = `Entry ${index + 1} of the request`;
|
|
12965
|
+
if (typeof entry !== "object" || entry === null || typeof entry.service !== "string" || entry.service === "" || typeof entry.path !== "string" || !Array.isArray(entry.actions) || entry.actions.length === 0 || entry.actions.some((action) => typeof action !== "string" || action === "")) {
|
|
12966
|
+
throw badRequest(`${label} is malformed: it needs a service, a path and at least one action.`);
|
|
12967
|
+
}
|
|
12968
|
+
if (entry.service === VAULT_PERMISSION_SERVICE2 || entry.service === "vault") {
|
|
12969
|
+
throw badRequest(`${label} asks for the vault (\`tinycloud.vault\`). This command does not sign vault access from a request file: the signer turns it into KV access on \`vault/\` for the vault actions you listed, so the printed list would not show what is signed.`);
|
|
12970
|
+
}
|
|
12971
|
+
if (entry.space !== void 0 && (typeof entry.space !== "string" || entry.space === "")) {
|
|
12972
|
+
throw badRequest(`${label} has an empty space: name the space, or leave it out to mean your \`${APPS_AGENT_SPACE}\` space.`);
|
|
12973
|
+
}
|
|
12974
|
+
return entry.space === void 0 && !isRawEncryptionPermission(entry) ? { ...entry, space: APPS_AGENT_SPACE } : entry;
|
|
12975
|
+
});
|
|
12976
|
+
}
|
|
12977
|
+
function finalizeSigningEntries(resolved) {
|
|
12978
|
+
const merged = /* @__PURE__ */ new Map();
|
|
12979
|
+
const spaces = /* @__PURE__ */ new Set();
|
|
12980
|
+
const spellings = /* @__PURE__ */ new Map();
|
|
12981
|
+
resolved.forEach((entry, index) => {
|
|
12982
|
+
const label = `Entry ${index + 1} of the request`;
|
|
12983
|
+
if (SERVICE_LONG_TO_SHORT3[entry.service] === void 0) {
|
|
12984
|
+
throw badRequest(`${label} names the service "${safeClipped(entry.service)}", which the signer cannot sign.`);
|
|
12985
|
+
}
|
|
12986
|
+
const space = entry.space;
|
|
12987
|
+
const raw = isRawEncryptionPermission(entry);
|
|
12988
|
+
const concrete = typeof space === "string" && (space.startsWith("tinycloud:") || space === RAW_ENCRYPTION_SPACE && raw);
|
|
12989
|
+
if (!concrete) {
|
|
12990
|
+
throw badRequest(`${label} does not resolve to a concrete space (got "${safeClipped(String(space))}").`);
|
|
12991
|
+
}
|
|
12992
|
+
const normalized = normalizePkhIdentifier(space);
|
|
12993
|
+
if (!raw) spaces.add(normalized);
|
|
12994
|
+
const spelled = spellings.get(normalized) ?? space;
|
|
12995
|
+
spellings.set(normalized, spelled);
|
|
12996
|
+
const unified = { ...entry, space: spelled };
|
|
12997
|
+
const key = JSON.stringify([entry.service, normalized, entry.path, entry.expiry ?? null, entry.caveats ?? null]);
|
|
12998
|
+
const existing = merged.get(key);
|
|
12999
|
+
merged.set(key, existing === void 0 ? unified : { ...existing, actions: [...existing.actions, ...entry.actions] });
|
|
13000
|
+
});
|
|
13001
|
+
if (spaces.size > 1) {
|
|
13002
|
+
throw badRequest(`The request names ${spaces.size} different spaces (${safeClippedList([...spaces])}); a grant covers one space, so make one request per space.`);
|
|
13003
|
+
}
|
|
13004
|
+
return [...merged.values()].map((entry) => ({ ...entry, actions: [...new Set(entry.actions)].sort() }));
|
|
13005
|
+
}
|
|
13006
|
+
function describeAction(action) {
|
|
13007
|
+
if (Object.prototype.hasOwnProperty.call(ACTION_WORDS, action)) return ACTION_WORDS[action];
|
|
13008
|
+
return action.endsWith("/*") ? `ALL abilities of ${safeInline(action.slice(0, -2))} (unrecognised: grants ${safeInline(action)})` : `unrecognised: grants ${safeInline(action)}`;
|
|
13009
|
+
}
|
|
13010
|
+
function describeEntry(entry, applicationsSpace) {
|
|
13011
|
+
const inApplications = inApplicationsSpace(entry, applicationsSpace);
|
|
13012
|
+
const space = inApplications ? "your `applications` space" : `space \`${safeInline(entry.space ?? "")}\``;
|
|
13013
|
+
const wholeSpace = entry.path === "" || entry.path === "/";
|
|
13014
|
+
const where = wholeSpace ? inApplications ? `ALL apps in ${space}` : `everything in ${space}` : `\`${safeInline(entry.path)}\` in ${space}`;
|
|
13015
|
+
const known = entry.service === "tinycloud.kv" || entry.service === "tinycloud.sql" || entry.service === "tinycloud.delegation" || entry.service === "tinycloud.capabilities";
|
|
13016
|
+
const service = known ? safeInline(entry.service) : `unrecognised service ${safeInline(entry.service)}`;
|
|
13017
|
+
return `${entry.actions.map(describeAction).join(", ")} on ${where} (${service})`;
|
|
13018
|
+
}
|
|
13019
|
+
function withinPresetSet(entry, applicationsSpace) {
|
|
13020
|
+
if (!inApplicationsSpace(entry, applicationsSpace) || entry.caveats !== void 0) return false;
|
|
13021
|
+
const reads = /* @__PURE__ */ new Set();
|
|
13022
|
+
const writes = /* @__PURE__ */ new Set();
|
|
13023
|
+
for (const { grants } of Object.values(APPS_AGENT_VARIANTS)) {
|
|
13024
|
+
for (const grant of grants) {
|
|
13025
|
+
if (grant.service !== entry.service) continue;
|
|
13026
|
+
for (const action of grant.actions) (action.endsWith("/put") || action.endsWith("/write") ? writes : reads).add(action);
|
|
13027
|
+
}
|
|
13028
|
+
}
|
|
13029
|
+
const firstSegment = entry.path.split("/")[0] ?? "";
|
|
13030
|
+
const named = entry.path.includes("/") && isAppId(firstSegment);
|
|
13031
|
+
return entry.actions.length > 0 && entry.actions.every((action) => reads.has(action) || writes.has(action) && named);
|
|
13032
|
+
}
|
|
13033
|
+
function describeGrantScope(requested, requestedExpiry, applicationsSpace) {
|
|
13034
|
+
const preset = matchAppsAgentRequest(requested, requestedExpiry, applicationsSpace);
|
|
13035
|
+
if (preset !== void 0) {
|
|
13036
|
+
return { text: appsAgentScopeNotice(preset.mode, preset.appIds), entries: requested, extras: [] };
|
|
13037
|
+
}
|
|
13038
|
+
const touchesApplications = requested.some((entry) => (entry.space ?? "").split(":").at(-1) === APPS_AGENT_SPACE);
|
|
13039
|
+
const extras = touchesApplications ? requested.filter((entry) => !withinPresetSet(entry, applicationsSpace)) : [];
|
|
13040
|
+
const others = requested.filter((entry) => !extras.includes(entry));
|
|
13041
|
+
const lines = [
|
|
13042
|
+
extras.length > 0 ? "This request asks for MORE than the apps-agent preset. Entries marked ! go beyond it:" : touchesApplications ? "This request is not the standard apps-agent grant (other paths or lifetime). It asks for:" : "This request asks for:",
|
|
13043
|
+
...extras.map((entry) => `! ${describeEntry(entry, applicationsSpace)}`),
|
|
13044
|
+
...others.map((entry) => `- ${describeEntry(entry, applicationsSpace)}`)
|
|
13045
|
+
];
|
|
13046
|
+
if (requested.some((entry) => entry.service === "tinycloud.sql" && entry.actions.some((action) => STRUCTURE_ACTIONS.has(action)))) {
|
|
13047
|
+
lines.push("Because of how TinyCloud permissions work today (TC-1581), SQL write access can also change or delete tables (structure).");
|
|
13048
|
+
}
|
|
13049
|
+
const entryLifetimes = requested.filter((entry) => entry.expiry !== void 0).map((entry) => lifetimeMs(entry.expiry));
|
|
13050
|
+
const durations = [lifetimeMs(requestedExpiry), ...entryLifetimes].filter((ms2) => ms2 !== void 0);
|
|
13051
|
+
const longest = Math.max(...durations);
|
|
13052
|
+
lines.push(
|
|
13053
|
+
durations.length === 0 || !Number.isFinite(longest) ? "Its lifetime could not be read from the request." : `It lasts up to ${lifetimePhrase(longest)}${requestedExpiry === void 0 ? " (no lifetime was requested; this is the default)" : ""}; revoke it any time with \`${CLI_NAME15} delegation revoke <id>\`.`
|
|
13054
|
+
);
|
|
13055
|
+
return { text: lines.join("\n"), entries: requested, extras };
|
|
13056
|
+
}
|
|
13057
|
+
|
|
13058
|
+
// src/lib/consent-text.ts
|
|
13059
|
+
init_safe_text();
|
|
13060
|
+
init_permissions();
|
|
13061
|
+
function permissionConsentLine(permission) {
|
|
13062
|
+
return safeInline(compactPermission(permission));
|
|
13063
|
+
}
|
|
13064
|
+
function signExtrasQuestion(count) {
|
|
13065
|
+
return `Sign the ${count} ${count === 1 ? "entry" : "entries"} marked ! above, and the rest of the request? [y/N] `;
|
|
13066
|
+
}
|
|
13067
|
+
|
|
12618
13068
|
// src/auth/replication-login.ts
|
|
12619
13069
|
init_dist();
|
|
12620
13070
|
init_errors();
|
|
@@ -12908,7 +13358,7 @@ async function revokeForLogout(ctx, discardLocalChanges) {
|
|
|
12908
13358
|
"REVOCATION_FAILED",
|
|
12909
13359
|
`Revocation failed for ${cid}: ${result.error.message}. Session, key, and replica files are retained for retry; local reads stay denied.`,
|
|
12910
13360
|
ExitCode.ERROR,
|
|
12911
|
-
{ cid, hint: `Retry \`${
|
|
13361
|
+
{ cid, hint: `Retry \`${CLI_NAME16} auth logout --revoke\` when the node is available.` }
|
|
12912
13362
|
);
|
|
12913
13363
|
}
|
|
12914
13364
|
if (result.data.cid !== cid || result.data.revoked !== true) {
|
|
@@ -12927,7 +13377,7 @@ async function revokeForLogout(ctx, discardLocalChanges) {
|
|
|
12927
13377
|
"REVOCATION_FAILED",
|
|
12928
13378
|
`Revocation failed before local logout: ${error instanceof Error ? error.message : String(error)}. Session, key, and replica files are retained for retry; local reads stay denied.`,
|
|
12929
13379
|
ExitCode.ERROR,
|
|
12930
|
-
{ hint: `Retry \`${
|
|
13380
|
+
{ hint: `Retry \`${CLI_NAME16} auth logout --revoke\` when online authority is available.` }
|
|
12931
13381
|
);
|
|
12932
13382
|
}
|
|
12933
13383
|
}
|
|
@@ -13126,7 +13576,7 @@ async function finishProfileLogout(ctx, options = {}) {
|
|
|
13126
13576
|
function registerAuthCommand(program, options) {
|
|
13127
13577
|
const auth = program.command("auth").description("Authentication management");
|
|
13128
13578
|
registerAuthGrantsCommand(auth, { openKeyAcquisition: options?.openKeyAcquisition });
|
|
13129
|
-
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", "Wait up to 5 minutes for a signed code from OpenKey on stdin instead of a browser callback (Safari may require paste)").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 \`${
|
|
13579
|
+
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", "Wait up to 5 minutes for a signed code from OpenKey on stdin instead of a browser callback (Safari may require paste)").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 \`${CLI_NAME16} 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 a folder prefix ending in /, or root (repeatable)", (prefix, values = []) => [...values, prefix]).option("--replication-allow-secrets", "Allow replication of secret material (network-wide decrypt warning)").action(async (options2, cmd) => {
|
|
13130
13580
|
try {
|
|
13131
13581
|
if (options2.device && options2.paste) {
|
|
13132
13582
|
throw new CLIError("INVALID_ARGUMENT", "--device and --paste are mutually exclusive.", ExitCode.USAGE_ERROR);
|
|
@@ -13176,6 +13626,7 @@ function registerAuthCommand(program, options) {
|
|
|
13176
13626
|
await persistReplicationLogin(ctx.profile, replicationOptions);
|
|
13177
13627
|
if (replicationOptions.allowSecrets) writeReplicationSecretsWarning();
|
|
13178
13628
|
reportDeclined(result.declined);
|
|
13629
|
+
await ensureSpaceSetup({ profile: ctx.profile, host: ctx.host }, result.session, result.session.permissions);
|
|
13179
13630
|
outputJson({
|
|
13180
13631
|
authenticated: true,
|
|
13181
13632
|
profile: ctx.profile,
|
|
@@ -13275,7 +13726,7 @@ function registerAuthCommand(program, options) {
|
|
|
13275
13726
|
authenticated: false,
|
|
13276
13727
|
replicasRemoved,
|
|
13277
13728
|
replicasKept: options2.keepReplicas === true,
|
|
13278
|
-
warning: online ? "The node confirmed revocation of the listed CIDs before local logout." : options2.keepReplicas ? "Legacy replica files remain on disk; this local logout clears the session but does not revoke node grants. Purge the files before a future sign-in if they must not be reused." : `Logout is local; delegations and grants on the node stay valid until they expire. Use \`${
|
|
13729
|
+
warning: online ? "The node confirmed revocation of the listed CIDs before local logout." : options2.keepReplicas ? "Legacy replica files remain on disk; this local logout clears the session but does not revoke node grants. Purge the files before a future sign-in if they must not be reused." : `Logout is local; delegations and grants on the node stay valid until they expire. Use \`${CLI_NAME16} delegation revoke <cid>\` to end access now.`,
|
|
13279
13730
|
...online === void 0 ? {} : {
|
|
13280
13731
|
revokedCids: online.plan.cids,
|
|
13281
13732
|
revocationExcluded: online.plan.excluded
|
|
@@ -13365,7 +13816,15 @@ ${signInAgainHint(ctx.profile, posture ?? void 0)}
|
|
|
13365
13816
|
).option("--permission <file>", 'JSON permission request: { "permissions": PermissionEntry[] }').option("--manifest <fileOrBase64>", "Manifest file, base64:<json>, or raw base64 JSON").option(
|
|
13366
13817
|
"--expiry <duration>",
|
|
13367
13818
|
`Lifetime of the granted delegation. ms-format string (e.g. "7d", "30m") or raw milliseconds. Defaults to 7d, capped by the active session's expiry.`
|
|
13368
|
-
).option("--emit [file]", "Emit the request artifact to stdout, or write it to file when provided").option("--grant", "Grant the requested permissions immediately with this owner profile").option("--yes", "Skip local-key TTY confirmation", false).option("--no-popup", "Print the OpenKey URL without opening a browser when granting with OpenKey").option("--device", "With --grant: approve on another device (e.g. a phone) through OpenKey device authorization (KV-scoped; SQL needs browser login)").option(
|
|
13819
|
+
).option("--emit [file]", "Emit the request artifact to stdout, or write it to file when provided").option("--grant", "Grant the requested permissions immediately with this owner profile").option("--yes", "Skip local-key TTY confirmation", false).option("--no-popup", "Print the OpenKey URL without opening a browser when granting with OpenKey").option("--device", "With --grant: approve on another device (e.g. a phone) through OpenKey device authorization (KV-scoped; SQL needs browser login)").option(
|
|
13820
|
+
"--preset <name>",
|
|
13821
|
+
`Request a named permission set instead of --cap: ${APPS_AGENT_PRESET} is READ-ONLY on the applications space (kv get/list + sql read, ${APPS_AGENT_EXPIRY_DAYS} days)`
|
|
13822
|
+
).option(
|
|
13823
|
+
"--app <app_id>",
|
|
13824
|
+
`With --preset ${APPS_AGENT_PRESET}: limit the grant to this app's <app_id>/ prefix (recommended); repeat for up to ${APPS_AGENT_MAX_APPS} apps`,
|
|
13825
|
+
(value, previous) => [...previous, value],
|
|
13826
|
+
[]
|
|
13827
|
+
).option("--write", `Don't request \`--write\` during the tiny-apps beta. It currently also allows changes to the app's own code and manifest; the narrowed write ships in TC-1620. With --preset ${APPS_AGENT_PRESET} it also allows writes (kv put + sql write) in the named --app apps; SQL write can also change or drop those apps' tables`).option("--paste", "With --grant on an OpenKey profile: wait up to 5 minutes for a signed code from OpenKey on stdin instead of a browser callback (use when the browser cannot reach this CLI)").action(async (options2, cmd) => {
|
|
13369
13828
|
try {
|
|
13370
13829
|
const globalOpts = cmd.optsWithGlobals();
|
|
13371
13830
|
const ctx = await ProfileManager.resolveContext(globalOpts, options2.grant ? {} : { storedSession: "unused" });
|
|
@@ -13376,8 +13835,13 @@ ${signInAgainHint(ctx.profile, posture ?? void 0)}
|
|
|
13376
13835
|
if (options2.device && (!options2.grant || resolveProfilePosture(profile) !== "owner-openkey")) {
|
|
13377
13836
|
throw new CLIError("INVALID_ARGUMENT", "--device requires --grant on an OpenKey owner profile.", ExitCode.USAGE_ERROR);
|
|
13378
13837
|
}
|
|
13379
|
-
const
|
|
13380
|
-
const
|
|
13838
|
+
const preset = resolveAppsAgentRequest(options2);
|
|
13839
|
+
const requested = preset === void 0 ? await collectRequestedPermissions(options2, ctx.profile) : await resolvePermissionSpaces(appsAgentPermissions(preset.mode, preset.appIds), ctx.profile);
|
|
13840
|
+
const expiryOption = preset === void 0 ? parseExpiryOption(options2.expiry) : APPS_AGENT_EXPIRY;
|
|
13841
|
+
if (preset !== void 0) process.stderr.write(`
|
|
13842
|
+
${appsAgentScopeNotice(preset.mode, preset.appIds)}
|
|
13843
|
+
|
|
13844
|
+
`);
|
|
13381
13845
|
if (requested.length === 0) {
|
|
13382
13846
|
throw new CLIError(
|
|
13383
13847
|
"NO_CAPS_REQUESTED",
|
|
@@ -13405,7 +13869,7 @@ ${signInAgainHint(ctx.profile, posture ?? void 0)}
|
|
|
13405
13869
|
if (profile.authMethod === "openkey" || options2.device) {
|
|
13406
13870
|
const key = await ProfileManager.getKey(ctx.profile);
|
|
13407
13871
|
if (!key) {
|
|
13408
|
-
throw new CLIError("NO_KEY", `No key found for profile "${ctx.profile}". Run \`${
|
|
13872
|
+
throw new CLIError("NO_KEY", `No key found for profile "${ctx.profile}". Run \`${CLI_NAME16} --profile ${ctx.profile} auth rotate\` to create a new key and sign in.`, ExitCode.AUTH_REQUIRED);
|
|
13409
13873
|
}
|
|
13410
13874
|
const openkeyHost = resolveOpenKeyHost(profile);
|
|
13411
13875
|
const grants = [];
|
|
@@ -13425,7 +13889,7 @@ ${signInAgainHint(ctx.profile, posture ?? void 0)}
|
|
|
13425
13889
|
if (notices) process.stderr.write(`${notices.prompts[index]}
|
|
13426
13890
|
`);
|
|
13427
13891
|
const reason = permissionGrantReason(
|
|
13428
|
-
`Grant requested TinyCloud permissions from \`${
|
|
13892
|
+
`Grant requested TinyCloud permissions from \`${CLI_NAME16} auth request --grant\`.`,
|
|
13429
13893
|
group
|
|
13430
13894
|
);
|
|
13431
13895
|
const request = grantRequestPermissions(group, profile.spaceId ?? profile.spaceName);
|
|
@@ -13535,7 +13999,7 @@ ${signInAgainHint(ctx.profile, posture ?? void 0)}
|
|
|
13535
13999
|
kind: parsed.kind,
|
|
13536
14000
|
requestId: parsed.requestId,
|
|
13537
14001
|
requested: parsed.requested,
|
|
13538
|
-
next: `${
|
|
14002
|
+
next: `${CLI_NAME16} auth retry ${parsed.requestId}`
|
|
13539
14003
|
});
|
|
13540
14004
|
return;
|
|
13541
14005
|
}
|
|
@@ -13669,7 +14133,12 @@ ${signInAgainHint(ctx.profile, posture ?? void 0)}
|
|
|
13669
14133
|
const raw = await readAuthArtifactSource(source, {
|
|
13670
14134
|
stdin: options2.stdin === true || options2.paste === true
|
|
13671
14135
|
});
|
|
13672
|
-
|
|
14136
|
+
let parsed;
|
|
14137
|
+
try {
|
|
14138
|
+
parsed = JSON.parse(raw);
|
|
14139
|
+
} catch {
|
|
14140
|
+
throw new CLIError("INVALID_AUTH_REQUEST", "The request file is not valid JSON.", ExitCode.USAGE_ERROR);
|
|
14141
|
+
}
|
|
13673
14142
|
if (!isCompatiblePermissionRequestArtifact(parsed)) {
|
|
13674
14143
|
throw new CLIError(
|
|
13675
14144
|
"INVALID_AUTH_REQUEST",
|
|
@@ -13677,7 +14146,31 @@ ${signInAgainHint(ctx.profile, posture ?? void 0)}
|
|
|
13677
14146
|
ExitCode.USAGE_ERROR
|
|
13678
14147
|
);
|
|
13679
14148
|
}
|
|
13680
|
-
|
|
14149
|
+
assertRequestFields(parsed);
|
|
14150
|
+
const requested = finalizeSigningEntries(
|
|
14151
|
+
await resolvePermissionSpaces(entriesForSigning(parsed.requested), ctx.profile)
|
|
14152
|
+
);
|
|
14153
|
+
const [applications] = await resolvePermissionSpaces(
|
|
14154
|
+
[{ service: "tinycloud.kv", space: APPS_AGENT_SPACE, path: "", actions: ["tinycloud.kv/get"] }],
|
|
14155
|
+
ctx.profile
|
|
14156
|
+
);
|
|
14157
|
+
const scope = describeGrantScope(requested, parsed.requestedExpiry, applications.space ?? APPS_AGENT_SPACE);
|
|
14158
|
+
process.stderr.write(`
|
|
14159
|
+
${scope.text}
|
|
14160
|
+
|
|
14161
|
+
`);
|
|
14162
|
+
let confirmedExtras = false;
|
|
14163
|
+
if (scope.extras.length > 0 && options2.yes !== true) {
|
|
14164
|
+
if (!isInteractive()) {
|
|
14165
|
+
throw new CLIError(
|
|
14166
|
+
"CONFIRMATION_REQUIRED",
|
|
14167
|
+
`This request asks for more than the apps-agent preset (${scope.extras.length} entr${scope.extras.length === 1 ? "y" : "ies"} marked ! in the list above). Nothing was signed. Read the list, then re-run with --yes to sign it.`,
|
|
14168
|
+
ExitCode.USAGE_ERROR
|
|
14169
|
+
);
|
|
14170
|
+
}
|
|
14171
|
+
await askYesNo(signExtrasQuestion(scope.extras.length), "Grant cancelled. Nothing was signed.");
|
|
14172
|
+
confirmedExtras = true;
|
|
14173
|
+
}
|
|
13681
14174
|
const resolvedRequest = { ...parsed, requested };
|
|
13682
14175
|
const node = await ensureAuthenticated(ctx);
|
|
13683
14176
|
await ensureDelegationAuthority({
|
|
@@ -13687,7 +14180,7 @@ ${signInAgainHint(ctx.profile, posture ?? void 0)}
|
|
|
13687
14180
|
requested,
|
|
13688
14181
|
expiryOption: parsed.requestedExpiry,
|
|
13689
14182
|
reason: "Grant permissions requested by a TinyCloud auth request artifact.",
|
|
13690
|
-
yes: options2.yes === true
|
|
14183
|
+
yes: options2.yes === true || confirmedExtras
|
|
13691
14184
|
});
|
|
13692
14185
|
const grant = await grantAuthRequest(node, resolvedRequest);
|
|
13693
14186
|
await appendLocalGrantArtifact(ctx.profile, grant.delegation);
|
|
@@ -14086,7 +14579,7 @@ async function ensureDelegationAuthority(params) {
|
|
|
14086
14579
|
if (!key) {
|
|
14087
14580
|
throw new CLIError(
|
|
14088
14581
|
"NO_KEY",
|
|
14089
|
-
`No key found for profile "${params.ctx.profile}". Run \`${
|
|
14582
|
+
`No key found for profile "${params.ctx.profile}". Run \`${CLI_NAME16} --profile ${params.ctx.profile} auth rotate\` to create a new key and sign in.`,
|
|
14090
14583
|
ExitCode.AUTH_REQUIRED
|
|
14091
14584
|
);
|
|
14092
14585
|
}
|
|
@@ -14198,26 +14691,29 @@ async function collectRequestedPermissions(options, profile) {
|
|
|
14198
14691
|
}
|
|
14199
14692
|
return permissions;
|
|
14200
14693
|
}
|
|
14201
|
-
async function
|
|
14202
|
-
process.stderr.write("\n" + theme.heading("Additional Permissions") + "\n");
|
|
14203
|
-
for (const permission of permissions) {
|
|
14204
|
-
const dangerous = isDangerousPermission(permission);
|
|
14205
|
-
const line = ` ${compactPermission(permission)}`;
|
|
14206
|
-
process.stderr.write((dangerous ? theme.warn(line) : theme.value(line)) + "\n");
|
|
14207
|
-
}
|
|
14208
|
-
process.stderr.write("\n");
|
|
14694
|
+
async function askYesNo(question, cancelMessage) {
|
|
14209
14695
|
const rl = createInterface3({
|
|
14210
14696
|
input: process.stdin,
|
|
14211
14697
|
output: process.stderr
|
|
14212
14698
|
});
|
|
14213
14699
|
const answer = await new Promise((resolve8) => {
|
|
14214
|
-
rl.question(
|
|
14700
|
+
rl.question(question, resolve8);
|
|
14215
14701
|
});
|
|
14216
14702
|
rl.close();
|
|
14217
14703
|
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
14218
|
-
throw new CLIError("REQUEST_CANCELLED",
|
|
14704
|
+
throw new CLIError("REQUEST_CANCELLED", cancelMessage, ExitCode.ERROR);
|
|
14219
14705
|
}
|
|
14220
14706
|
}
|
|
14707
|
+
async function confirmPermissionRequest(permissions) {
|
|
14708
|
+
process.stderr.write("\n" + theme.heading("Additional Permissions") + "\n");
|
|
14709
|
+
for (const permission of permissions) {
|
|
14710
|
+
const dangerous = isDangerousPermission(permission);
|
|
14711
|
+
const line = ` ${permissionConsentLine(permission)}`;
|
|
14712
|
+
process.stderr.write((dangerous ? theme.warn(line) : theme.value(line)) + "\n");
|
|
14713
|
+
}
|
|
14714
|
+
process.stderr.write("\n");
|
|
14715
|
+
await askYesNo("Approve local-key delegation? [y/N] ", "Permission request cancelled.");
|
|
14716
|
+
}
|
|
14221
14717
|
async function requirePermissionConsent(permissions, yes) {
|
|
14222
14718
|
if (isInteractive()) {
|
|
14223
14719
|
if (!yes) await confirmPermissionRequest(permissions);
|
|
@@ -14332,7 +14828,7 @@ async function rotateAuthKey(profileName2, host, options = {}) {
|
|
|
14332
14828
|
if (!profile.privateKey) {
|
|
14333
14829
|
throw new CLIError(
|
|
14334
14830
|
"LOCAL_OWNER_KEY_REQUIRED",
|
|
14335
|
-
`Profile "${profileName2}" does not have a local owner private key. Run \`${
|
|
14831
|
+
`Profile "${profileName2}" does not have a local owner private key. Run \`${CLI_NAME16} auth login --method local\` first.`,
|
|
14336
14832
|
ExitCode.AUTH_REQUIRED
|
|
14337
14833
|
);
|
|
14338
14834
|
}
|
|
@@ -14518,6 +15014,7 @@ async function handleLocalAuth(profileName2, host, options = {}) {
|
|
|
14518
15014
|
async function handleOpenKeyAuth(profileName2, host, options = {}) {
|
|
14519
15015
|
const { profile, delegationData, declined, legacyNested } = await refreshOpenKeySession(profileName2, host, options);
|
|
14520
15016
|
reportDeclined(declined, legacyNested);
|
|
15017
|
+
await ensureSpaceSetup({ profile: profileName2, host }, delegationData, delegationData.permissions);
|
|
14521
15018
|
outputJson({
|
|
14522
15019
|
authenticated: true,
|
|
14523
15020
|
profile: profileName2,
|
|
@@ -14772,7 +15269,7 @@ async function refreshOpenKeySession(profileName2, host, options = {}) {
|
|
|
14772
15269
|
}
|
|
14773
15270
|
|
|
14774
15271
|
// src/commands/completion.ts
|
|
14775
|
-
import { CLI_NAME as
|
|
15272
|
+
import { CLI_NAME as CLI_NAME17 } from "@tinycloud/operations/cli-name";
|
|
14776
15273
|
function registerCompletionCommand(program) {
|
|
14777
15274
|
const completion = program.command("completion").description("Generate shell completions");
|
|
14778
15275
|
completion.command("bash").description("Output bash completions").action(() => {
|
|
@@ -14789,7 +15286,7 @@ function registerCompletionCommand(program) {
|
|
|
14789
15286
|
});
|
|
14790
15287
|
}
|
|
14791
15288
|
function generateBashCompletion() {
|
|
14792
|
-
return `# ${
|
|
15289
|
+
return `# ${CLI_NAME17} bash completion
|
|
14793
15290
|
_tc_completions() {
|
|
14794
15291
|
local cur prev commands subcommands
|
|
14795
15292
|
COMPREPLY=()
|
|
@@ -14814,11 +15311,11 @@ _tc_completions() {
|
|
|
14814
15311
|
COMPREPLY=( $(compgen -W "\${subcommands}" -- "\${cur}") )
|
|
14815
15312
|
fi
|
|
14816
15313
|
}
|
|
14817
|
-
complete -F _tc_completions ${
|
|
15314
|
+
complete -F _tc_completions ${CLI_NAME17}
|
|
14818
15315
|
`;
|
|
14819
15316
|
}
|
|
14820
15317
|
function generateZshCompletion() {
|
|
14821
|
-
return `#compdef ${
|
|
15318
|
+
return `#compdef ${CLI_NAME17}
|
|
14822
15319
|
|
|
14823
15320
|
_tc() {
|
|
14824
15321
|
local -a commands
|
|
@@ -14869,50 +15366,50 @@ _tc
|
|
|
14869
15366
|
`;
|
|
14870
15367
|
}
|
|
14871
15368
|
function generateFishCompletion() {
|
|
14872
|
-
return `# ${
|
|
15369
|
+
return `# ${CLI_NAME17} fish completion
|
|
14873
15370
|
set -l commands init auth kv space delegation share node profile completion
|
|
14874
15371
|
|
|
14875
15372
|
# Disable file completion by default
|
|
14876
|
-
complete -c ${
|
|
15373
|
+
complete -c ${CLI_NAME17} -f
|
|
14877
15374
|
|
|
14878
15375
|
# Top-level commands
|
|
14879
|
-
complete -c ${
|
|
14880
|
-
complete -c ${
|
|
14881
|
-
complete -c ${
|
|
14882
|
-
complete -c ${
|
|
14883
|
-
complete -c ${
|
|
14884
|
-
complete -c ${
|
|
14885
|
-
complete -c ${
|
|
14886
|
-
complete -c ${
|
|
14887
|
-
complete -c ${
|
|
15376
|
+
complete -c ${CLI_NAME17} -n "not __fish_seen_subcommand_from $commands" -a init -d "Initialize a new TinyCloud profile"
|
|
15377
|
+
complete -c ${CLI_NAME17} -n "not __fish_seen_subcommand_from $commands" -a auth -d "Authentication management"
|
|
15378
|
+
complete -c ${CLI_NAME17} -n "not __fish_seen_subcommand_from $commands" -a kv -d "Key-value store operations"
|
|
15379
|
+
complete -c ${CLI_NAME17} -n "not __fish_seen_subcommand_from $commands" -a space -d "Space management"
|
|
15380
|
+
complete -c ${CLI_NAME17} -n "not __fish_seen_subcommand_from $commands" -a delegation -d "Manage delegations"
|
|
15381
|
+
complete -c ${CLI_NAME17} -n "not __fish_seen_subcommand_from $commands" -a share -d "Share data with others"
|
|
15382
|
+
complete -c ${CLI_NAME17} -n "not __fish_seen_subcommand_from $commands" -a node -d "Node health and info"
|
|
15383
|
+
complete -c ${CLI_NAME17} -n "not __fish_seen_subcommand_from $commands" -a profile -d "Profile management"
|
|
15384
|
+
complete -c ${CLI_NAME17} -n "not __fish_seen_subcommand_from $commands" -a completion -d "Generate shell completions"
|
|
14888
15385
|
|
|
14889
15386
|
# Subcommands
|
|
14890
|
-
complete -c ${
|
|
14891
|
-
complete -c ${
|
|
14892
|
-
complete -c ${
|
|
14893
|
-
complete -c ${
|
|
14894
|
-
complete -c ${
|
|
14895
|
-
complete -c ${
|
|
14896
|
-
complete -c ${
|
|
14897
|
-
complete -c ${
|
|
15387
|
+
complete -c ${CLI_NAME17} -n "__fish_seen_subcommand_from auth" -a "login logout rotate status whoami"
|
|
15388
|
+
complete -c ${CLI_NAME17} -n "__fish_seen_subcommand_from kv" -a "get put delete list head"
|
|
15389
|
+
complete -c ${CLI_NAME17} -n "__fish_seen_subcommand_from space" -a "list create info switch"
|
|
15390
|
+
complete -c ${CLI_NAME17} -n "__fish_seen_subcommand_from delegation" -a "create list info revoke"
|
|
15391
|
+
complete -c ${CLI_NAME17} -n "__fish_seen_subcommand_from share" -a "create receive list revoke"
|
|
15392
|
+
complete -c ${CLI_NAME17} -n "__fish_seen_subcommand_from node" -a "health version status"
|
|
15393
|
+
complete -c ${CLI_NAME17} -n "__fish_seen_subcommand_from profile" -a "list create show switch delete"
|
|
15394
|
+
complete -c ${CLI_NAME17} -n "__fish_seen_subcommand_from completion" -a "bash zsh fish"
|
|
14898
15395
|
|
|
14899
15396
|
# Global options
|
|
14900
|
-
complete -c ${
|
|
14901
|
-
complete -c ${
|
|
14902
|
-
complete -c ${
|
|
14903
|
-
complete -c ${
|
|
14904
|
-
complete -c ${
|
|
14905
|
-
complete -c ${
|
|
14906
|
-
complete -c ${
|
|
14907
|
-
complete -c ${
|
|
15397
|
+
complete -c ${CLI_NAME17} -l profile -s p -d "Profile to use"
|
|
15398
|
+
complete -c ${CLI_NAME17} -l host -s H -d "TinyCloud node URL"
|
|
15399
|
+
complete -c ${CLI_NAME17} -l verbose -s v -d "Enable verbose output"
|
|
15400
|
+
complete -c ${CLI_NAME17} -l no-cache -d "Disable caching"
|
|
15401
|
+
complete -c ${CLI_NAME17} -l quiet -s q -d "Suppress non-essential output"
|
|
15402
|
+
complete -c ${CLI_NAME17} -l replication -d "Enable read-through replication"
|
|
15403
|
+
complete -c ${CLI_NAME17} -l no-replication -d "Disable replication"
|
|
15404
|
+
complete -c ${CLI_NAME17} -l replication-debug -d "Write replication diagnostics to stderr"
|
|
14908
15405
|
`;
|
|
14909
15406
|
}
|
|
14910
15407
|
|
|
14911
15408
|
// src/commands/courier.ts
|
|
14912
|
-
import { CLI_NAME as
|
|
14913
|
-
var TC_OPTIONS_BEFORE_COURIER = `${
|
|
15409
|
+
import { CLI_NAME as CLI_NAME18 } from "@tinycloud/operations/cli-name";
|
|
15410
|
+
var TC_OPTIONS_BEFORE_COURIER = `${CLI_NAME18} courier takes no ${CLI_NAME18} options and does not use ${CLI_NAME18} profiles; Courier keeps its own profile in $COURIER_HOME (default ~/.courier). Put flags after "courier", e.g. ${CLI_NAME18} courier init --host <url>.`;
|
|
14914
15411
|
function registerCourierCommand(program) {
|
|
14915
|
-
const courier = program.command("courier").description(`Agent-to-agent threads over TinyCloud; run \`${
|
|
15412
|
+
const courier = program.command("courier").description(`Agent-to-agent threads over TinyCloud; run \`${CLI_NAME18} courier --help\` (beta)`).argument("[args...]").allowUnknownOption().helpOption(false);
|
|
14916
15413
|
courier.action(() => courier.error(TC_OPTIONS_BEFORE_COURIER, { exitCode: 2, code: "tc.courier.options" }));
|
|
14917
15414
|
}
|
|
14918
15415
|
|
|
@@ -14957,7 +15454,7 @@ init_formatter();
|
|
|
14957
15454
|
init_errors();
|
|
14958
15455
|
init_constants();
|
|
14959
15456
|
init_sdk();
|
|
14960
|
-
import { CLI_NAME as
|
|
15457
|
+
import { CLI_NAME as CLI_NAME19 } from "@tinycloud/operations/cli-name";
|
|
14961
15458
|
import { PermissionNotInManifestError } from "@tinycloud/node-sdk";
|
|
14962
15459
|
async function queryDelegations(node, options, until) {
|
|
14963
15460
|
const found = [];
|
|
@@ -14968,7 +15465,7 @@ async function queryDelegations(node, options, until) {
|
|
|
14968
15465
|
if (page.error.code === "PERMISSION_DENIED") {
|
|
14969
15466
|
throw new CLIError(
|
|
14970
15467
|
"PERMISSION_DENIED",
|
|
14971
|
-
`This session cannot read delegations: it lacks tinycloud.delegation/list. Run \`${
|
|
15468
|
+
`This session cannot read delegations: it lacks tinycloud.delegation/list. Run \`${CLI_NAME19} auth login\` again to grant it.`,
|
|
14972
15469
|
ExitCode.PERMISSION_DENIED
|
|
14973
15470
|
);
|
|
14974
15471
|
}
|
|
@@ -15143,7 +15640,7 @@ init_constants();
|
|
|
15143
15640
|
init_formatter();
|
|
15144
15641
|
init_theme();
|
|
15145
15642
|
init_errors();
|
|
15146
|
-
import { CLI_NAME as
|
|
15643
|
+
import { CLI_NAME as CLI_NAME20 } from "@tinycloud/operations/cli-name";
|
|
15147
15644
|
init_session_expired();
|
|
15148
15645
|
init_types();
|
|
15149
15646
|
function registerDoctorCommand(program) {
|
|
@@ -15177,7 +15674,7 @@ function registerDoctorCommand(program) {
|
|
|
15177
15674
|
const profile = await ProfileManager.getProfile(profileName2);
|
|
15178
15675
|
keyDetail = profile.did ? `${profile.did.slice(0, 20)}...` : "key found";
|
|
15179
15676
|
} else {
|
|
15180
|
-
keyDetail = `no key \u2014 run ${
|
|
15677
|
+
keyDetail = `no key \u2014 run ${CLI_NAME20} init`;
|
|
15181
15678
|
}
|
|
15182
15679
|
} catch {
|
|
15183
15680
|
keyDetail = "error reading key";
|
|
@@ -15199,7 +15696,7 @@ function registerDoctorCommand(program) {
|
|
|
15199
15696
|
} else if (state2 === "unverified") {
|
|
15200
15697
|
sessionDetail = `not usable (no signed OpenKey proof) \u2014 ${signInAgainHint(profileName2, resolveProfilePosture(profile))}`;
|
|
15201
15698
|
} else {
|
|
15202
|
-
sessionDetail = sessionOk ? "active" : `no session \u2014 run ${
|
|
15699
|
+
sessionDetail = sessionOk ? "active" : `no session \u2014 run ${CLI_NAME20} auth login`;
|
|
15203
15700
|
}
|
|
15204
15701
|
} catch {
|
|
15205
15702
|
sessionDetail = "error reading session";
|
|
@@ -15227,7 +15724,7 @@ function registerDoctorCommand(program) {
|
|
|
15227
15724
|
try {
|
|
15228
15725
|
const profile = await ProfileManager.getProfile(profileName2);
|
|
15229
15726
|
spaceOk = Boolean(profile.spaceId);
|
|
15230
|
-
spaceDetail = spaceOk ? `${profile.spaceId.slice(0, 16)}...` : `no space \u2014 run ${
|
|
15727
|
+
spaceDetail = spaceOk ? `${profile.spaceId.slice(0, 16)}...` : `no space \u2014 run ${CLI_NAME20} space create`;
|
|
15231
15728
|
} catch {
|
|
15232
15729
|
spaceDetail = "error checking space";
|
|
15233
15730
|
}
|
|
@@ -15417,7 +15914,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
15417
15914
|
}
|
|
15418
15915
|
|
|
15419
15916
|
// src/commands/enable.ts
|
|
15420
|
-
import { CLI_NAME as
|
|
15917
|
+
import { CLI_NAME as CLI_NAME21 } from "@tinycloud/operations/cli-name";
|
|
15421
15918
|
init_constants();
|
|
15422
15919
|
init_profiles();
|
|
15423
15920
|
init_errors();
|
|
@@ -15425,7 +15922,7 @@ init_formatter();
|
|
|
15425
15922
|
init_publishing_manifest();
|
|
15426
15923
|
function registerEnableCommand(program) {
|
|
15427
15924
|
const enable = program.command("enable").description("Enable a narrowly scoped TinyCloud service");
|
|
15428
|
-
enable.command("share").description(`Approve Share publishing scope through OpenKey device authorization (same as \`${
|
|
15925
|
+
enable.command("share").description(`Approve Share publishing scope through OpenKey device authorization (same as \`${CLI_NAME21} auth login --device --manifest builtin:share-publishing\`). Use a dedicated profile, e.g. \`${CLI_NAME21} init --name publisher --key-only && ${CLI_NAME21} --profile publisher enable share\`; keep existing profiles for their apps.`).option("--replace-session", "Replace this profile's live session even though the new scope would narrow, change or shorten it (prefer a new profile)").action(async (options, command) => {
|
|
15429
15926
|
try {
|
|
15430
15927
|
const globalOptions = command.optsWithGlobals();
|
|
15431
15928
|
const context = await ProfileManager.resolveContext(globalOptions, { storedSession: "unused" });
|
|
@@ -15461,7 +15958,7 @@ init_profiles();
|
|
|
15461
15958
|
init_formatter();
|
|
15462
15959
|
init_errors();
|
|
15463
15960
|
init_constants();
|
|
15464
|
-
import { CLI_NAME as
|
|
15961
|
+
import { CLI_NAME as CLI_NAME22 } from "@tinycloud/operations/cli-name";
|
|
15465
15962
|
function registerInitCommand(program) {
|
|
15466
15963
|
program.command("init").description("Initialize a new TinyCloud profile").option("--name <profile>", "Profile name", "default").option("--key-only", "Only generate key, skip authentication").option("--host <url>", "TinyCloud node URL").option("--paste", "Use manual paste mode for authentication").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--default-space <name>", "Default space used when --space is omitted (e.g. applications)").action(async (options, cmd) => {
|
|
15467
15964
|
try {
|
|
@@ -15479,7 +15976,7 @@ function registerInitCommand(program) {
|
|
|
15479
15976
|
if (await ProfileManager.profileExists(profileName2)) {
|
|
15480
15977
|
throw new CLIError(
|
|
15481
15978
|
"PROFILE_EXISTS",
|
|
15482
|
-
`Profile "${profileName2}" already exists. Use \`${
|
|
15979
|
+
`Profile "${profileName2}" already exists. Use \`${CLI_NAME22} profile delete ${profileName2}\` first or choose a different name.`,
|
|
15483
15980
|
ExitCode.ERROR
|
|
15484
15981
|
);
|
|
15485
15982
|
}
|
|
@@ -15516,6 +16013,7 @@ function registerInitCommand(program) {
|
|
|
15516
16013
|
noPopup: options.popup === false,
|
|
15517
16014
|
persistHost: true
|
|
15518
16015
|
});
|
|
16016
|
+
await ensureSpaceSetup({ profile: profileName2, host }, delegationData, delegationData.permissions);
|
|
15519
16017
|
outputJson({
|
|
15520
16018
|
profile: profileName2,
|
|
15521
16019
|
did,
|
|
@@ -15529,6 +16027,183 @@ function registerInitCommand(program) {
|
|
|
15529
16027
|
});
|
|
15530
16028
|
}
|
|
15531
16029
|
|
|
16030
|
+
// src/commands/knowledge.ts
|
|
16031
|
+
init_session_expired();
|
|
16032
|
+
init_constants();
|
|
16033
|
+
init_profiles();
|
|
16034
|
+
import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
|
|
16035
|
+
import { CLI_NAME as CLI_NAME23 } from "@tinycloud/operations/cli-name";
|
|
16036
|
+
|
|
16037
|
+
// src/lib/knowledge-input.ts
|
|
16038
|
+
init_constants();
|
|
16039
|
+
init_errors();
|
|
16040
|
+
init_safe_text();
|
|
16041
|
+
function usage2(message) {
|
|
16042
|
+
return new CLIError("INVALID_ARGUMENT", message, ExitCode.USAGE_ERROR);
|
|
16043
|
+
}
|
|
16044
|
+
function assertKnowledgeAppId(value) {
|
|
16045
|
+
if (!isAppId(value)) throw usage2(`Invalid app id "${safeClipped(value)}": ${APP_ID_RULE}.`);
|
|
16046
|
+
return value;
|
|
16047
|
+
}
|
|
16048
|
+
function assertKnowledgeConcept(value) {
|
|
16049
|
+
const reason = value.length > 300 ? "it is longer than 300 characters" : value.startsWith("/") ? "it starts with a slash" : /[*\\]/.test(value) ? "it contains a wildcard or backslash" : /%(?:2e|2f|5c)/i.test(value) ? "it contains an encoded dot, slash or backslash" : value.split("/").some((segment) => segment === "" || segment === "." || segment === "..") ? "it has an empty, `.` or `..` segment" : void 0;
|
|
16050
|
+
if (reason !== void 0) {
|
|
16051
|
+
throw usage2(
|
|
16052
|
+
`Invalid concept "${safeClipped(value)}": ${reason}. Use a path inside the app's knowledge folder, such as app or tables/meal.md.`
|
|
16053
|
+
);
|
|
16054
|
+
}
|
|
16055
|
+
return value;
|
|
16056
|
+
}
|
|
16057
|
+
|
|
16058
|
+
// src/commands/knowledge.ts
|
|
16059
|
+
init_permissions();
|
|
16060
|
+
init_errors();
|
|
16061
|
+
init_formatter();
|
|
16062
|
+
|
|
16063
|
+
// src/lib/knowledge-render.ts
|
|
16064
|
+
init_safe_text();
|
|
16065
|
+
function knowledgeListRows(apps) {
|
|
16066
|
+
return apps.map((app) => [
|
|
16067
|
+
safeInline(app.app_id),
|
|
16068
|
+
app.name === void 0 ? "\u2014" : safeInline(app.name),
|
|
16069
|
+
safeInline(app.has_index ? app.root : `${app.root} (no index)`),
|
|
16070
|
+
String(app.concepts.length)
|
|
16071
|
+
]);
|
|
16072
|
+
}
|
|
16073
|
+
function knowledgeReadParts(output2) {
|
|
16074
|
+
const body = safeBlock(output2.body);
|
|
16075
|
+
return {
|
|
16076
|
+
heading: safeInline(output2.key),
|
|
16077
|
+
...output2.frontmatter === null ? {} : { meta: safeInline(JSON.stringify(output2.frontmatter)) },
|
|
16078
|
+
body: body.endsWith("\n") ? body : `${body}
|
|
16079
|
+
`
|
|
16080
|
+
};
|
|
16081
|
+
}
|
|
16082
|
+
|
|
16083
|
+
// src/commands/knowledge.ts
|
|
16084
|
+
init_theme();
|
|
16085
|
+
function registerKnowledgeCommand(program) {
|
|
16086
|
+
const knowledge = program.command("knowledge").description("Read the knowledge bundles of apps installed in a user's TinyCloud");
|
|
16087
|
+
knowledge.command("list").description("List installed apps that carry a knowledge bundle (`<app_id>/knowledge/index.md` in the applications space)").option("--app <app_id>", "Only this app; works with a grant scoped to that app").action(async (options, cmd) => {
|
|
16088
|
+
try {
|
|
16089
|
+
const appId = options.app === void 0 ? void 0 : assertKnowledgeAppId(options.app);
|
|
16090
|
+
const ctx = await ProfileManager.resolveContext(cmd.optsWithGlobals());
|
|
16091
|
+
const result = await withSpinner("Listing installed apps...", () => invokeOperation2(
|
|
16092
|
+
"tinycloud.knowledge.list",
|
|
16093
|
+
1,
|
|
16094
|
+
{ profile: ctx.profile, host: ctx.host, allowOwnerProfile: true },
|
|
16095
|
+
appId === void 0 ? {} : { app_id: appId }
|
|
16096
|
+
));
|
|
16097
|
+
if (result.status !== "ok") throw knowledgeFailure(result, ctx.profile, appId);
|
|
16098
|
+
const output2 = result.output;
|
|
16099
|
+
if (shouldOutputJson()) {
|
|
16100
|
+
outputJson(result.output);
|
|
16101
|
+
return;
|
|
16102
|
+
}
|
|
16103
|
+
if (output2.apps.length === 0) {
|
|
16104
|
+
process.stdout.write(theme.muted("No installed apps with a knowledge bundle found.") + "\n");
|
|
16105
|
+
return;
|
|
16106
|
+
}
|
|
16107
|
+
process.stdout.write(
|
|
16108
|
+
formatTable(["App ID", "Name", "Root", "Concepts"], knowledgeListRows(output2.apps)) + "\n"
|
|
16109
|
+
);
|
|
16110
|
+
if (output2.truncated) {
|
|
16111
|
+
process.stderr.write(theme.warn("The applications space has more keys than one listing covers; some apps may be missing.") + "\n");
|
|
16112
|
+
}
|
|
16113
|
+
} catch (error) {
|
|
16114
|
+
return handleError(error);
|
|
16115
|
+
}
|
|
16116
|
+
});
|
|
16117
|
+
knowledge.command("read <app_id> [concept]").description("Read an app's knowledge index, or one concept (for example tables/meal.md), with frontmatter parsed").action(async (appIdArgument, conceptArgument, _options, cmd) => {
|
|
16118
|
+
try {
|
|
16119
|
+
const appId = assertKnowledgeAppId(appIdArgument);
|
|
16120
|
+
const concept = conceptArgument === void 0 ? void 0 : assertKnowledgeConcept(conceptArgument);
|
|
16121
|
+
const ctx = await ProfileManager.resolveContext(cmd.optsWithGlobals());
|
|
16122
|
+
const result = await withSpinner("Reading knowledge...", () => invokeOperation2(
|
|
16123
|
+
"tinycloud.knowledge.read",
|
|
16124
|
+
1,
|
|
16125
|
+
{ profile: ctx.profile, host: ctx.host, allowOwnerProfile: true },
|
|
16126
|
+
{ app_id: appId, ...concept === void 0 ? {} : { concept } }
|
|
16127
|
+
));
|
|
16128
|
+
if (result.status !== "ok") throw knowledgeFailure(result, ctx.profile, appId);
|
|
16129
|
+
const output2 = result.output;
|
|
16130
|
+
if (shouldOutputJson()) {
|
|
16131
|
+
outputJson(result.output);
|
|
16132
|
+
return;
|
|
16133
|
+
}
|
|
16134
|
+
const parts = knowledgeReadParts(output2);
|
|
16135
|
+
process.stdout.write(`${theme.heading(parts.heading)}
|
|
16136
|
+
`);
|
|
16137
|
+
if (parts.meta !== void 0) process.stdout.write(`${parts.meta}
|
|
16138
|
+
`);
|
|
16139
|
+
process.stdout.write(`
|
|
16140
|
+
${parts.body}`);
|
|
16141
|
+
} catch (error) {
|
|
16142
|
+
return handleError(error);
|
|
16143
|
+
}
|
|
16144
|
+
});
|
|
16145
|
+
}
|
|
16146
|
+
function knowledgeFailure(result, profile, appId) {
|
|
16147
|
+
switch (result.status) {
|
|
16148
|
+
case "authority_required": {
|
|
16149
|
+
return new CLIError(
|
|
16150
|
+
"PERMISSION_DENIED",
|
|
16151
|
+
`This profile has no grant for that. Missing: ${result.missing.map((entry) => compactPermission(entry)).join("; ")}`,
|
|
16152
|
+
ExitCode.PERMISSION_DENIED,
|
|
16153
|
+
{ hint: missingGrantHint(profile, appId) }
|
|
16154
|
+
);
|
|
16155
|
+
}
|
|
16156
|
+
case "setup_required":
|
|
16157
|
+
return new CLIError("SETUP_REQUIRED", "The operation needs setup before it can run.", ExitCode.ERROR);
|
|
16158
|
+
case "error": {
|
|
16159
|
+
if (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "PROFILE_POSTURE_NOT_ALLOWED" && result.context.posture === "unauthenticated") {
|
|
16160
|
+
return new CLIError("AUTH_REQUIRED", "Not signed in to TinyCloud.", ExitCode.AUTH_REQUIRED, {
|
|
16161
|
+
hint: `Sign in with: ${CLI_NAME23} --profile ${result.context.profile} auth login`
|
|
16162
|
+
});
|
|
16163
|
+
}
|
|
16164
|
+
if (result.error.code === "NODE_UNREACHABLE") {
|
|
16165
|
+
return new CLIError("NETWORK_ERROR", result.error.message, ExitCode.NETWORK_ERROR);
|
|
16166
|
+
}
|
|
16167
|
+
if (result.error.code === "KV_NOT_FOUND") {
|
|
16168
|
+
return new CLIError(
|
|
16169
|
+
"NOT_FOUND",
|
|
16170
|
+
"No such knowledge file. Run `knowledge list` for the installed apps, then `knowledge read <app_id>` for the index.",
|
|
16171
|
+
ExitCode.NOT_FOUND
|
|
16172
|
+
);
|
|
16173
|
+
}
|
|
16174
|
+
if (result.error.code === "PERMISSION_DENIED") {
|
|
16175
|
+
return new CLIError("PERMISSION_DENIED", result.error.message, ExitCode.PERMISSION_DENIED, {
|
|
16176
|
+
hint: missingGrantHint(profile, appId)
|
|
16177
|
+
});
|
|
16178
|
+
}
|
|
16179
|
+
if (result.error.code === "INPUT_INVALID") {
|
|
16180
|
+
return new CLIError(
|
|
16181
|
+
"INVALID_ARGUMENT",
|
|
16182
|
+
"The app id or concept is not valid. An app id looks like xyz.example.app; a concept is a path inside the app's knowledge folder, such as app or tables/meal.md.",
|
|
16183
|
+
ExitCode.USAGE_ERROR
|
|
16184
|
+
);
|
|
16185
|
+
}
|
|
16186
|
+
return withSignInHint(cliErrorFromService(result.error), result.context.profile, result.context.posture);
|
|
16187
|
+
}
|
|
16188
|
+
}
|
|
16189
|
+
}
|
|
16190
|
+
function missingGrantHint(profile, appId) {
|
|
16191
|
+
const request = `${CLI_NAME23} --profile ${profile} auth request`;
|
|
16192
|
+
const lines = appId === void 0 ? [
|
|
16193
|
+
` ${request} --preset ${APPS_AGENT_PRESET} --emit req.json (read-only, all installed apps)`,
|
|
16194
|
+
` ${request} --cap tinycloud.kv:${APPS_AGENT_SPACE}::get,list --emit req.json (all files, no table access)`,
|
|
16195
|
+
"To limit the grant to one app, add `--app` and that app's id to the first command (repeat `--app` for up to 5)."
|
|
16196
|
+
] : [
|
|
16197
|
+
` ${request} --preset ${APPS_AGENT_PRESET} --app ${appId} --emit req.json (read-only: files and table rows of ${appId})`,
|
|
16198
|
+
` ${request} --cap tinycloud.kv:${APPS_AGENT_SPACE}:${appId}/knowledge/:get,list --emit req.json (only ${appId}'s knowledge)`
|
|
16199
|
+
];
|
|
16200
|
+
return [
|
|
16201
|
+
"Ask the owner for a grant, narrowest first. Create the request:",
|
|
16202
|
+
...lines,
|
|
16203
|
+
`The owner runs \`${CLI_NAME23} auth grant req.json > grant.json\`; then import the result with \`${CLI_NAME23} --profile ${profile} auth import grant.json\`.`
|
|
16204
|
+
].join("\n");
|
|
16205
|
+
}
|
|
16206
|
+
|
|
15532
16207
|
// src/commands/kv.ts
|
|
15533
16208
|
import { readFile as readFile10 } from "fs/promises";
|
|
15534
16209
|
|
|
@@ -15833,7 +16508,7 @@ init_errors();
|
|
|
15833
16508
|
init_constants();
|
|
15834
16509
|
init_theme();
|
|
15835
16510
|
import { resolveManifestKnowledgeRoot as resolveManifestKnowledgeRoot2 } from "@tinycloud/sdk-core";
|
|
15836
|
-
import { CLI_NAME as
|
|
16511
|
+
import { CLI_NAME as CLI_NAME24 } from "@tinycloud/operations/cli-name";
|
|
15837
16512
|
import { readFile as readFile11 } from "fs/promises";
|
|
15838
16513
|
var DEFAULT_APP_SPACE = "applications";
|
|
15839
16514
|
function registerManifestCommand(program) {
|
|
@@ -15841,8 +16516,8 @@ function registerManifestCommand(program) {
|
|
|
15841
16516
|
manifest.command("resolve <source>").description("Resolve a manifest file or URL to its effective space, paths, and DB basenames").addHelpText("after", `
|
|
15842
16517
|
|
|
15843
16518
|
Examples:
|
|
15844
|
-
$ ${
|
|
15845
|
-
$ ${
|
|
16519
|
+
$ ${CLI_NAME24} manifest resolve ./manifest.json
|
|
16520
|
+
$ ${CLI_NAME24} manifest resolve https://app.example.com/manifest.json --json
|
|
15846
16521
|
|
|
15847
16522
|
What it shows:
|
|
15848
16523
|
- app_id, name, manifest_version
|
|
@@ -15852,7 +16527,7 @@ What it shows:
|
|
|
15852
16527
|
|
|
15853
16528
|
This command is read-only and does NOT contact the node \u2014 it just resolves
|
|
15854
16529
|
the manifest against the active profile's address/chain so you know which
|
|
15855
|
-
\`--space\` and \`--db\` values to pass to other ${
|
|
16530
|
+
\`--space\` and \`--db\` values to pass to other ${CLI_NAME24} commands.
|
|
15856
16531
|
`).action(async (source, _options, cmd) => {
|
|
15857
16532
|
try {
|
|
15858
16533
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -15921,7 +16596,7 @@ ${theme.heading("SQL databases")}
|
|
|
15921
16596
|
`);
|
|
15922
16597
|
}
|
|
15923
16598
|
process.stdout.write(theme.muted(`
|
|
15924
|
-
Use with: ${
|
|
16599
|
+
Use with: ${CLI_NAME24} sql query --space ${spaceName} --db <db> "..."
|
|
15925
16600
|
`));
|
|
15926
16601
|
}
|
|
15927
16602
|
if (permissions.length > 0) {
|
|
@@ -16041,7 +16716,7 @@ init_profiles();
|
|
|
16041
16716
|
init_formatter();
|
|
16042
16717
|
init_errors();
|
|
16043
16718
|
init_constants();
|
|
16044
|
-
import { CLI_NAME as
|
|
16719
|
+
import { CLI_NAME as CLI_NAME25 } from "@tinycloud/operations/cli-name";
|
|
16045
16720
|
import { createInterface as createInterface5 } from "readline";
|
|
16046
16721
|
init_theme();
|
|
16047
16722
|
init_types();
|
|
@@ -16187,9 +16862,9 @@ profile at command time. Precedence for every kv/sql command:
|
|
|
16187
16862
|
explicit --space flag > profile defaultSpace > primary space.
|
|
16188
16863
|
|
|
16189
16864
|
Examples:
|
|
16190
|
-
$ ${
|
|
16191
|
-
$ ${
|
|
16192
|
-
$ ${
|
|
16865
|
+
$ ${CLI_NAME25} profile set-default-space applications
|
|
16866
|
+
$ ${CLI_NAME25} profile set-default-space applications --profile cli-test
|
|
16867
|
+
$ ${CLI_NAME25} profile set-default-space --unset
|
|
16193
16868
|
`).action(async (name, options, cmd) => {
|
|
16194
16869
|
try {
|
|
16195
16870
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -16201,7 +16876,7 @@ Examples:
|
|
|
16201
16876
|
if (!options.unset && (name === void 0 || name === "")) {
|
|
16202
16877
|
throw new CLIError(
|
|
16203
16878
|
"USAGE_ERROR",
|
|
16204
|
-
`Provide a space name (e.g. \`${
|
|
16879
|
+
`Provide a space name (e.g. \`${CLI_NAME25} profile set-default-space applications\`) or pass --unset.`,
|
|
16205
16880
|
ExitCode.USAGE_ERROR
|
|
16206
16881
|
);
|
|
16207
16882
|
}
|
|
@@ -16260,7 +16935,7 @@ function parseOperatorType(raw) {
|
|
|
16260
16935
|
|
|
16261
16936
|
// src/commands/replica.ts
|
|
16262
16937
|
init_profile_replicas();
|
|
16263
|
-
import { CLI_NAME as
|
|
16938
|
+
import { CLI_NAME as CLI_NAME26 } from "@tinycloud/operations/cli-name";
|
|
16264
16939
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
16265
16940
|
import { constants as fsConstants5 } from "fs";
|
|
16266
16941
|
import { mkdir as mkdir5, open as open9, readdir as readdir6, rename as rename4, unlink as unlink4, lstat as lstat9 } from "fs/promises";
|
|
@@ -16401,11 +17076,11 @@ var EXIT_BY_CODE = {
|
|
|
16401
17076
|
var READ_FAILURE = {
|
|
16402
17077
|
absent: { code: "KEY_ABSENT", exit: ExitCode.NOT_FOUND, what: "is not in the replica (the replica covers its whole prefix)" },
|
|
16403
17078
|
deleted: { code: "KEY_DELETED", exit: ExitCode.NOT_FOUND, what: "was deleted at the source" },
|
|
16404
|
-
content_missing: { code: "CONTENT_MISSING", exit: ExitCode.NOT_FOUND, what: `is known but its content has not been fetched yet; run ${
|
|
17079
|
+
content_missing: { code: "CONTENT_MISSING", exit: ExitCode.NOT_FOUND, what: `is known but its content has not been fetched yet; run ${CLI_NAME26} replica sync` },
|
|
16405
17080
|
coverage_incomplete: {
|
|
16406
17081
|
code: "COVERAGE_INCOMPLETE",
|
|
16407
17082
|
exit: ExitCode.NOT_FOUND,
|
|
16408
|
-
what: `is not in the replica, which has not finished its first sync; run ${
|
|
17083
|
+
what: `is not in the replica, which has not finished its first sync; run ${CLI_NAME26} replica sync`
|
|
16409
17084
|
},
|
|
16410
17085
|
not_covered: { code: "NOT_COVERED", exit: ExitCode.USAGE_ERROR, what: "is outside the replica's prefix" }
|
|
16411
17086
|
};
|
|
@@ -16523,7 +17198,7 @@ async function existingReplicaName(profile, option) {
|
|
|
16523
17198
|
const names = await replicaNames(profile);
|
|
16524
17199
|
if (names.length === 1) return names[0];
|
|
16525
17200
|
if (names.length === 0) {
|
|
16526
|
-
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas. Create one with: ${
|
|
17201
|
+
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas. Create one with: ${CLI_NAME26} replica sync --prefix <prefix>`);
|
|
16527
17202
|
}
|
|
16528
17203
|
throw new CLIError("USAGE_ERROR", `Profile "${profile}" has several replicas (${names.join(", ")}); pass --replica <name>.`, ExitCode.USAGE_ERROR);
|
|
16529
17204
|
}
|
|
@@ -16631,9 +17306,9 @@ async function storedGrants(profile) {
|
|
|
16631
17306
|
function grantHint(space, prefix) {
|
|
16632
17307
|
return [
|
|
16633
17308
|
"A replica needs a device grant with get and sync on its prefix:",
|
|
16634
|
-
` device: ${
|
|
16635
|
-
` owner: ${
|
|
16636
|
-
` device: ${
|
|
17309
|
+
` device: ${CLI_NAME26} auth request --cap tinycloud.kv:${space}:${prefix}:get,list,metadata,sync --expiry 30d --emit req.json`,
|
|
17310
|
+
` owner: ${CLI_NAME26} auth grant req.json > grant.json`,
|
|
17311
|
+
` device: ${CLI_NAME26} auth import grant.json`
|
|
16637
17312
|
].join("\n");
|
|
16638
17313
|
}
|
|
16639
17314
|
function resolveSpace(input, prefix, grants) {
|
|
@@ -16724,7 +17399,7 @@ function describeStatus(status) {
|
|
|
16724
17399
|
formatField("Size", formatBytes(status.bytes)),
|
|
16725
17400
|
formatField("Grant", status.device.delegationCid),
|
|
16726
17401
|
formatField("Last error", status.lastError ? `${status.lastError.code}: ${status.lastError.message}` : null),
|
|
16727
|
-
formatField("Purge", status.purgePending ? `incomplete: content files remain; the next ${
|
|
17402
|
+
formatField("Purge", status.purgePending ? `incomplete: content files remain; the next ${CLI_NAME26} replica command retries` : null)
|
|
16728
17403
|
].join("\n");
|
|
16729
17404
|
}
|
|
16730
17405
|
async function writeFileAtomic(path, bytes) {
|
|
@@ -16888,7 +17563,7 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
|
|
|
16888
17563
|
(options, cmd) => run(async () => {
|
|
16889
17564
|
const profile = await profileName(cmd);
|
|
16890
17565
|
if (options.purge) {
|
|
16891
|
-
throw new CLIError("REPORT_READ_ONLY", `Replica report is read-only. To remove a named local replica, run ${
|
|
17566
|
+
throw new CLIError("REPORT_READ_ONLY", `Replica report is read-only. To remove a named local replica, run ${CLI_NAME26} replica reset --replica <name> --purge; flag-owned purge has no equivalent yet (TC-1309).`, ExitCode.USAGE_ERROR);
|
|
16892
17567
|
}
|
|
16893
17568
|
if (options.clearPending) {
|
|
16894
17569
|
throw new CLIError("REPORT_READ_ONLY", "Clearing pending replica writes has no equivalent yet; see TC-1309 for a safe replacement command.", ExitCode.USAGE_ERROR);
|
|
@@ -16990,7 +17665,7 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
|
|
|
16990
17665
|
if (differs !== void 0) {
|
|
16991
17666
|
throw new ReplicaError(
|
|
16992
17667
|
ReplicaErrorCode.CONFIG_MISMATCH,
|
|
16993
|
-
`Replica ${name} already follows ${differs}. Use another --replica name, or ${
|
|
17668
|
+
`Replica ${name} already follows ${differs}. Use another --replica name, or ${CLI_NAME26} replica reset --replica ${name} --purge first.`
|
|
16994
17669
|
);
|
|
16995
17670
|
}
|
|
16996
17671
|
}
|
|
@@ -17155,12 +17830,12 @@ init_theme();
|
|
|
17155
17830
|
init_errors();
|
|
17156
17831
|
init_warnings();
|
|
17157
17832
|
init_constants();
|
|
17158
|
-
import { CLI_NAME as
|
|
17833
|
+
import { CLI_NAME as CLI_NAME28 } from "@tinycloud/operations/cli-name";
|
|
17159
17834
|
import { readFile as readFile12 } from "fs/promises";
|
|
17160
17835
|
import { join as join16 } from "path";
|
|
17161
17836
|
import { homedir, hostname, userInfo } from "os";
|
|
17162
17837
|
import { appSecretScope } from "@tinycloud/sdk-core";
|
|
17163
|
-
import { invokeOperation as
|
|
17838
|
+
import { invokeOperation as invokeOperation3 } from "@tinycloud/operations";
|
|
17164
17839
|
import { isLocalOwnerProfile as isLocalOwnerProfile4, ownerSessionLacksProof as ownerSessionLacksProof5 } from "@tinycloud/operations/profile";
|
|
17165
17840
|
import {
|
|
17166
17841
|
buildSecretManagerUrl,
|
|
@@ -17366,7 +18041,7 @@ function parseStatusEnvelope(line) {
|
|
|
17366
18041
|
// src/lib/secrets-wait.ts
|
|
17367
18042
|
init_constants();
|
|
17368
18043
|
init_errors();
|
|
17369
|
-
import { CLI_NAME as
|
|
18044
|
+
import { CLI_NAME as CLI_NAME27 } from "@tinycloud/operations/cli-name";
|
|
17370
18045
|
var WAIT_BACKOFF_MS = [3e3, 4500, 6750, 10125];
|
|
17371
18046
|
var WAIT_STEADY_INTERVAL_MS = 15e3;
|
|
17372
18047
|
var DEFAULT_WAIT_TIMEOUT = "5m";
|
|
@@ -17374,7 +18049,7 @@ var MAX_WAIT_TIMEOUT_MS = 9 * 6e4;
|
|
|
17374
18049
|
var MAX_CONSECUTIVE_FAILURES = 5;
|
|
17375
18050
|
var TIMEOUT_RE = /^(0|\d+s|\d+m)$/;
|
|
17376
18051
|
var RETRYABLE_STATUS_ERRORS = { NODE_UNREACHABLE: true, NODE_ERROR: true };
|
|
17377
|
-
var EXPLICIT_KEY_HINT = `${
|
|
18052
|
+
var EXPLICIT_KEY_HINT = `${CLI_NAME27} secrets wait never signs in, and an explicit private key has no stored session. Use a local profile signed in once with ${CLI_NAME27} --profile <profile> auth login --method local, without TC_PRIVATE_KEY.`;
|
|
17378
18053
|
function abortableSleep(ms2, signal) {
|
|
17379
18054
|
return new Promise((resolve8) => {
|
|
17380
18055
|
if (signal.aborted) {
|
|
@@ -17410,7 +18085,7 @@ async function runSecretsWait(request, deps) {
|
|
|
17410
18085
|
let step = 0;
|
|
17411
18086
|
const cancelled = () => new CLIError(
|
|
17412
18087
|
"CANCELLED",
|
|
17413
|
-
`Interrupted: ${
|
|
18088
|
+
`Interrupted: ${CLI_NAME27} secrets wait stopped before the secrets were ready.`,
|
|
17414
18089
|
ExitCode.CANCELLED,
|
|
17415
18090
|
progress === void 0 ? {} : { progress }
|
|
17416
18091
|
);
|
|
@@ -17554,13 +18229,13 @@ function statusError(error, profile, warnings) {
|
|
|
17554
18229
|
if (error.code === "AUTH_REQUIRED" || error.code === "SESSION_NOT_FOUND") {
|
|
17555
18230
|
const recovery = recoveryOf(error, "sign_in");
|
|
17556
18231
|
if (recovery !== void 0) {
|
|
17557
|
-
const hint = recovery.command !== void 0 ? `Sign in once with: ${recovery.command}, then retry.` : `Sign in to profile "${profile}" once with ${
|
|
18232
|
+
const hint = recovery.command !== void 0 ? `Sign in once with: ${recovery.command}, then retry.` : `Sign in to profile "${profile}" once with ${CLI_NAME27} auth login, then retry.`;
|
|
17558
18233
|
return new CLIError("AUTH_REQUIRED", error.message, ExitCode.AUTH_REQUIRED, { hint, recovery, ...extra });
|
|
17559
18234
|
}
|
|
17560
18235
|
}
|
|
17561
18236
|
if (error.code === "PROFILE_MIGRATION_REQUIRED") {
|
|
17562
18237
|
const recovery = recoveryOf(error, "complete_migration");
|
|
17563
|
-
const hint = recovery?.command !== void 0 ? `Run ${recovery.command} once to finish this profile's one-time upgrade, then retry wait.` : `Run ${
|
|
18238
|
+
const hint = recovery?.command !== void 0 ? `Run ${recovery.command} once to finish this profile's one-time upgrade, then retry wait.` : `Run ${CLI_NAME27} auth caps once with profile "${profile}" to finish its one-time upgrade, then retry wait.`;
|
|
17564
18239
|
return new CLIError("PROFILE_MIGRATION_REQUIRED", error.message, ExitCode.ERROR, {
|
|
17565
18240
|
hint,
|
|
17566
18241
|
...recovery === void 0 ? {} : { recovery },
|
|
@@ -17752,11 +18427,11 @@ async function runSecretOperation(params) {
|
|
|
17752
18427
|
}
|
|
17753
18428
|
function secretPermissionReason(action, name) {
|
|
17754
18429
|
const target = name ? ` secret "${name}"` : " secrets";
|
|
17755
|
-
return `Allow \`${
|
|
18430
|
+
return `Allow \`${CLI_NAME28} secrets ${action}${name ? ` ${name}` : ""}\` to access${target} with the required TinyCloud permissions.`;
|
|
17756
18431
|
}
|
|
17757
18432
|
function assertOwnerApprovalPossible(profileName2, profile, action, name, missing = []) {
|
|
17758
18433
|
if (profile.authMethod !== "openkey" || process.stdin.isTTY || process.stderr.isTTY) return;
|
|
17759
|
-
const command = `${
|
|
18434
|
+
const command = `${CLI_NAME28} secrets ${action === "del" ? "delete" : action}${name ? ` ${name}` : ""}`;
|
|
17760
18435
|
throw new CLIError(
|
|
17761
18436
|
"PERMISSION_DENIED",
|
|
17762
18437
|
action === "get" ? missing.length > 0 && missing.every((permission) => permission.service === "tinycloud.encryption" && Array.isArray(permission.actions) && permission.actions.length > 0 && permission.actions.every((capability) => capability === SECRET_DECRYPT_CAPABILITY)) ? `Profile "${profileName2}" lacks the scoped decrypt authority (${SECRET_DECRYPT_CAPABILITY}) needed to read secret "${name}", and requesting it needs an interactive browser approval.` : `Profile "${profileName2}" lacks a scoped read or decrypt grant needed to read secret "${name}", and requesting it needs an interactive browser approval.` : `Profile "${profileName2}" holds no grant for \`${command}\`, and requesting one needs an interactive browser approval.`,
|
|
@@ -17767,7 +18442,7 @@ function assertOwnerApprovalPossible(profileName2, profile, action, name, missin
|
|
|
17767
18442
|
);
|
|
17768
18443
|
}
|
|
17769
18444
|
function scopedSecretLoginHint(profileName2) {
|
|
17770
|
-
return `Have the owner approve a scoped login whose manifest names the secret: ${
|
|
18445
|
+
return `Have the owner approve a scoped login whose manifest names the secret: ${CLI_NAME28} --profile ${profileName2} auth login --method openkey --paste --manifest <manifest.json>. Pass the owner's code on stdin, newline-terminated.`;
|
|
17771
18446
|
}
|
|
17772
18447
|
async function runSecretOperationAttempt(label, operation) {
|
|
17773
18448
|
try {
|
|
@@ -17828,7 +18503,7 @@ async function canonicalSecretGet(params, collect) {
|
|
|
17828
18503
|
};
|
|
17829
18504
|
const invoke = async () => collect(await withSpinner(
|
|
17830
18505
|
params.label,
|
|
17831
|
-
() => auth?.privateKey ? invokeSecretsGetWithLocalAuthorityRetry(target, input) :
|
|
18506
|
+
() => auth?.privateKey ? invokeSecretsGetWithLocalAuthorityRetry(target, input) : invokeOperation3("tinycloud.secrets.get", 1, target, input)
|
|
17832
18507
|
));
|
|
17833
18508
|
let first = await invoke();
|
|
17834
18509
|
if (first.status === "error" && first.error.code === "SESSION_NOT_FOUND" && auth?.privateKey === void 0) {
|
|
@@ -17901,7 +18576,7 @@ ${message}`,
|
|
|
17901
18576
|
"AUTH_REQUIRED",
|
|
17902
18577
|
"Not signed in to TinyCloud.",
|
|
17903
18578
|
ExitCode.AUTH_REQUIRED,
|
|
17904
|
-
{ hint: `Sign in with: ${
|
|
18579
|
+
{ hint: `Sign in with: ${CLI_NAME28} --profile ${result.context.profile} auth login` }
|
|
17905
18580
|
);
|
|
17906
18581
|
}
|
|
17907
18582
|
if (result.error.code === "NODE_UNREACHABLE") {
|
|
@@ -18378,7 +19053,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
18378
19053
|
name: "Encryption network",
|
|
18379
19054
|
ok: false,
|
|
18380
19055
|
detail: `${networkName} not found`,
|
|
18381
|
-
hint: `${
|
|
19056
|
+
hint: `${CLI_NAME28} secrets network init ${networkName}`
|
|
18382
19057
|
}
|
|
18383
19058
|
];
|
|
18384
19059
|
let secret;
|
|
@@ -18424,7 +19099,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
18424
19099
|
name: "Secret access",
|
|
18425
19100
|
ok: false,
|
|
18426
19101
|
detail: notFound ? `${resolved.permissionPaths.vault} not found` : result.error.message,
|
|
18427
|
-
hint: notFound ? local ? `${
|
|
19102
|
+
hint: notFound ? local ? `${CLI_NAME28} secrets put ${resolved.name}${formatSecretScopeFlag(scopeOptions)} --stdin` : `${CLI_NAME28} secrets request ${resolved.name}${formatSecretScopeFlag(scopeOptions)}` : `Ask the owner profile to grant ${secretCapabilityAction("get")} and ${SECRET_DECRYPT_CAPABILITY}.`
|
|
18428
19103
|
});
|
|
18429
19104
|
}
|
|
18430
19105
|
} else {
|
|
@@ -18668,7 +19343,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
18668
19343
|
if (effectiveLocal(profile, options)) {
|
|
18669
19344
|
throw new CLIError(
|
|
18670
19345
|
"LOCAL_OWNER_PROFILE",
|
|
18671
|
-
`Profile "${ctx.profile}" uses a local key, which owns its secrets: store them with ${
|
|
19346
|
+
`Profile "${ctx.profile}" uses a local key, which owns its secrets: store them with ${CLI_NAME28} secrets put NAME --stdin instead of requesting them.`,
|
|
18672
19347
|
ExitCode.USAGE_ERROR
|
|
18673
19348
|
);
|
|
18674
19349
|
}
|
|
@@ -18771,7 +19446,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
18771
19446
|
try {
|
|
18772
19447
|
const ctx = await ProfileManager.resolveContext(cmd.optsWithGlobals());
|
|
18773
19448
|
const privateKey = process.env.TC_PRIVATE_KEY;
|
|
18774
|
-
const result = await
|
|
19449
|
+
const result = await invokeOperation3("tinycloud.secrets.status", 1, {
|
|
18775
19450
|
profile: ctx.profile,
|
|
18776
19451
|
host: ctx.host,
|
|
18777
19452
|
allowOwnerProfile: true,
|
|
@@ -18791,7 +19466,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
18791
19466
|
|
|
18792
19467
|
// src/commands/share.ts
|
|
18793
19468
|
import { Option } from "commander";
|
|
18794
|
-
import { CLI_NAME as
|
|
19469
|
+
import { CLI_NAME as CLI_NAME29 } from "@tinycloud/operations/cli-name";
|
|
18795
19470
|
|
|
18796
19471
|
// ../share-sdk/dist/artifact.js
|
|
18797
19472
|
init_sha256();
|
|
@@ -29641,14 +30316,14 @@ function assertKey(key32) {
|
|
|
29641
30316
|
throw new TypeError(`key must be ${KEY_LENGTH} bytes, got ${key32.length}`);
|
|
29642
30317
|
}
|
|
29643
30318
|
}
|
|
29644
|
-
async function importAesKey(key32,
|
|
30319
|
+
async function importAesKey(key32, usage3) {
|
|
29645
30320
|
assertKey(key32);
|
|
29646
30321
|
return globalThis.crypto.subtle.importKey(
|
|
29647
30322
|
"raw",
|
|
29648
30323
|
key32,
|
|
29649
30324
|
"AES-GCM",
|
|
29650
30325
|
false,
|
|
29651
|
-
[
|
|
30326
|
+
[usage3]
|
|
29652
30327
|
);
|
|
29653
30328
|
}
|
|
29654
30329
|
async function open11(blob, key32) {
|
|
@@ -36976,12 +37651,12 @@ function shareCliError(error, operation = "publish") {
|
|
|
36976
37651
|
const profileName2 = "profileName" in failure ? failure.profileName : void 0;
|
|
36977
37652
|
const localKey = "localKey" in failure && failure.localKey === true;
|
|
36978
37653
|
const profileHint = profileName2 === void 0 ? "" : `--profile ${profileName2} `;
|
|
36979
|
-
const loginHint = localKey ? `\`${
|
|
37654
|
+
const loginHint = localKey ? `\`${CLI_NAME29} ${profileHint}auth login --method local\`` : `\`${CLI_NAME29} ${profileHint}auth login --device --manifest builtin:share-publishing\` (or \`${CLI_NAME29} ${profileHint}enable share\`)`;
|
|
36980
37655
|
if (failure.kind === "caveated-session") {
|
|
36981
37656
|
const holder = profileName2 === void 0 ? "this session" : `profile ${profileName2}'s session`;
|
|
36982
37657
|
return new CLIError(
|
|
36983
37658
|
"PERMISSION_DENIED",
|
|
36984
|
-
`${holder} carries signed restrictions (caveats) on the authority an anyone-with-link share needs, so it cannot create the share link; nothing was shared. Approve Share publishing without restrictions on a new, dedicated profile (any unused name): \`${
|
|
37659
|
+
`${holder} carries signed restrictions (caveats) on the authority an anyone-with-link share needs, so it cannot create the share link; nothing was shared. Approve Share publishing without restrictions on a new, dedicated profile (any unused name): \`${CLI_NAME29} init --name publisher --key-only && ${CLI_NAME29} --profile publisher enable share\``,
|
|
36985
37660
|
5
|
|
36986
37661
|
);
|
|
36987
37662
|
}
|
|
@@ -37067,7 +37742,7 @@ function shareCliError(error, operation = "publish") {
|
|
|
37067
37742
|
if (nodeCode === "ENOENT") return new CLIError("INVALID_ARGUMENT", "share input was not found", 2);
|
|
37068
37743
|
if (nodeCode === "EISDIR") return new CLIError("INVALID_ARGUMENT", "share input must be a Markdown file", 2);
|
|
37069
37744
|
const mapped = typeof nodeCode === "string" && known[nodeCode] !== void 0 ? known[nodeCode] : known[message];
|
|
37070
|
-
const classified = mapped === void 0 ? classifyShareFailure(error, `\`${
|
|
37745
|
+
const classified = mapped === void 0 ? classifyShareFailure(error, `\`${CLI_NAME29} auth login\``) : void 0;
|
|
37071
37746
|
if (classified !== void 0) return Object.assign(new CLIError(classified.code, classified.reason, classified.exitCode), { cause: error });
|
|
37072
37747
|
if (error instanceof TypeError) return new CLIError("INVALID_ARGUMENT", "share input is invalid", 2);
|
|
37073
37748
|
return new CLIError(mapped?.code ?? "INVALID_ARGUMENT", mapped ? mapped.code : "share operation failed", mapped?.exit ?? 2);
|
|
@@ -37400,7 +38075,7 @@ init_constants();
|
|
|
37400
38075
|
init_sdk();
|
|
37401
38076
|
init_host();
|
|
37402
38077
|
init_theme();
|
|
37403
|
-
import { CLI_NAME as
|
|
38078
|
+
import { CLI_NAME as CLI_NAME30 } from "@tinycloud/operations/cli-name";
|
|
37404
38079
|
import { ACCOUNT_REGISTRY_SPACE } from "@tinycloud/node-sdk";
|
|
37405
38080
|
import { randomBytes as randomBytes9 } from "crypto";
|
|
37406
38081
|
import { mkdir as mkdir7, writeFile as writeFile4 } from "fs/promises";
|
|
@@ -37447,7 +38122,7 @@ function registerSpaceCommand(program) {
|
|
|
37447
38122
|
} else {
|
|
37448
38123
|
const profile = await ProfileManager.getProfile(ctx.profile);
|
|
37449
38124
|
const cap = "tinycloud.kv:account:spaces/:get,list";
|
|
37450
|
-
const hint = resolveProfilePosture(profile) === "delegate-session" ? `Ask the space owner to grant it. Create the request with: ${
|
|
38125
|
+
const hint = resolveProfilePosture(profile) === "delegate-session" ? `Ask the space owner to grant it. Create the request with: ${CLI_NAME30} --profile ${ctx.profile} auth request --cap ${cap} --emit` : `Grant it to this session with: ${CLI_NAME30} --profile ${ctx.profile} auth request --grant --cap ${cap}. The default OpenKey owner sign-in covers only the default space, so it includes neither.`;
|
|
37451
38126
|
throw new CLIError(
|
|
37452
38127
|
"PERMISSION_DENIED",
|
|
37453
38128
|
`Listing spaces needs tinycloud.kv/get and tinycloud.kv/list on the account space's spaces/ registry (${accountSpaceId ?? ACCOUNT_REGISTRY_SPACE}), or tinycloud.space/list. This session holds neither.`,
|
|
@@ -37484,10 +38159,10 @@ function registerSpaceCommand(program) {
|
|
|
37484
38159
|
|
|
37485
38160
|
A delegate cannot host a space \u2014 only its owner (root authority) can. This
|
|
37486
38161
|
emits a tinycloud.host.request artifact naming the space and its owner so you
|
|
37487
|
-
can hand it to the owner; they run \`${
|
|
38162
|
+
can hand it to the owner; they run \`${CLI_NAME30} space host <name>\` and confirm.
|
|
37488
38163
|
|
|
37489
38164
|
If you ARE the owner of the resolved space, this refuses and tells you to host
|
|
37490
|
-
it directly with \`${
|
|
38165
|
+
it directly with \`${CLI_NAME30} space host <name>\` (no request needed).
|
|
37491
38166
|
`).action(async (name, options, cmd) => {
|
|
37492
38167
|
try {
|
|
37493
38168
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -37498,7 +38173,7 @@ it directly with \`${CLI_NAME28} space host <name>\` (no request needed).
|
|
|
37498
38173
|
if (await isRootAuthority(spaceId, ctx.profile)) {
|
|
37499
38174
|
throw new CLIError(
|
|
37500
38175
|
"ALREADY_ROOT_AUTHORITY",
|
|
37501
|
-
`You are the owner of ${spaceId}. Host it directly: ${
|
|
38176
|
+
`You are the owner of ${spaceId}. Host it directly: ${CLI_NAME30} space host ${spaceName}`,
|
|
37502
38177
|
ExitCode.USAGE_ERROR
|
|
37503
38178
|
);
|
|
37504
38179
|
}
|
|
@@ -37584,7 +38259,7 @@ init_sdk();
|
|
|
37584
38259
|
init_space();
|
|
37585
38260
|
init_host();
|
|
37586
38261
|
init_theme();
|
|
37587
|
-
import { CLI_NAME as
|
|
38262
|
+
import { CLI_NAME as CLI_NAME31 } from "@tinycloud/operations/cli-name";
|
|
37588
38263
|
import { writeFile as writeFile5 } from "fs/promises";
|
|
37589
38264
|
import { resolve as resolve7 } from "path";
|
|
37590
38265
|
async function dbHandle(node, dbName, spaceInput, profileName2) {
|
|
@@ -37608,12 +38283,12 @@ database for simple apps, or pass --db to target a named database. Pass
|
|
|
37608
38283
|
--space to target a non-primary space (e.g. the manifest "applications" space).
|
|
37609
38284
|
|
|
37610
38285
|
Common workflows:
|
|
37611
|
-
$ ${
|
|
37612
|
-
$ ${
|
|
37613
|
-
$ ${
|
|
37614
|
-
$ ${
|
|
37615
|
-
$ ${
|
|
37616
|
-
$ ${
|
|
38286
|
+
$ ${CLI_NAME31} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
|
|
38287
|
+
$ ${CLI_NAME31} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["ship docs"]'
|
|
38288
|
+
$ ${CLI_NAME31} sql query "SELECT id, body FROM notes ORDER BY id"
|
|
38289
|
+
$ ${CLI_NAME31} sql query "SELECT * FROM events WHERE type = ?" --db analytics --params '["signup"]'
|
|
38290
|
+
$ ${CLI_NAME31} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
|
|
38291
|
+
$ ${CLI_NAME31} sql export --db analytics --output analytics.db
|
|
37617
38292
|
|
|
37618
38293
|
Commands:
|
|
37619
38294
|
query Read rows with SELECT statements
|
|
@@ -37630,10 +38305,10 @@ Tips:
|
|
|
37630
38305
|
sql.command("query <sql>").description("Run a read-only SELECT query").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", `
|
|
37631
38306
|
|
|
37632
38307
|
Examples:
|
|
37633
|
-
$ ${
|
|
37634
|
-
$ ${
|
|
37635
|
-
$ ${
|
|
37636
|
-
$ ${
|
|
38308
|
+
$ ${CLI_NAME31} sql query "SELECT * FROM notes ORDER BY id"
|
|
38309
|
+
$ ${CLI_NAME31} sql query "SELECT * FROM notes WHERE id = ?" --params '[42]'
|
|
38310
|
+
$ ${CLI_NAME31} sql query "SELECT count(*) AS total FROM events" --db analytics --json
|
|
38311
|
+
$ ${CLI_NAME31} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
|
|
37637
38312
|
|
|
37638
38313
|
Output:
|
|
37639
38314
|
Human output is formatted as a table. Piped output or --json returns
|
|
@@ -37674,11 +38349,11 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
37674
38349
|
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", `
|
|
37675
38350
|
|
|
37676
38351
|
Examples:
|
|
37677
|
-
$ ${
|
|
37678
|
-
$ ${
|
|
37679
|
-
$ ${
|
|
37680
|
-
$ ${
|
|
37681
|
-
$ ${
|
|
38352
|
+
$ ${CLI_NAME31} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
|
|
38353
|
+
$ ${CLI_NAME31} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["first note"]'
|
|
38354
|
+
$ ${CLI_NAME31} sql execute "UPDATE notes SET body = ? WHERE id = ?" --params '["edited", 1]'
|
|
38355
|
+
$ ${CLI_NAME31} sql execute "DROP TABLE old_notes" --db archive
|
|
38356
|
+
$ ${CLI_NAME31} sql execute "DELETE FROM conversation WHERE id = ?" --space applications --db xyz.tinycloud.listen/conversations --params '["abc"]'
|
|
37682
38357
|
|
|
37683
38358
|
Output:
|
|
37684
38359
|
Returns JSON with the changed row count and last inserted row id when available.
|
|
@@ -37707,9 +38382,9 @@ Output:
|
|
|
37707
38382
|
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", `
|
|
37708
38383
|
|
|
37709
38384
|
Examples:
|
|
37710
|
-
$ ${
|
|
37711
|
-
$ ${
|
|
37712
|
-
$ ${
|
|
38385
|
+
$ ${CLI_NAME31} sql export
|
|
38386
|
+
$ ${CLI_NAME31} sql export --db analytics --output analytics.db
|
|
38387
|
+
$ ${CLI_NAME31} sql export --space applications --db xyz.tinycloud.listen/conversations --output listen.db
|
|
37713
38388
|
|
|
37714
38389
|
Output:
|
|
37715
38390
|
Writes the database file locally and returns JSON with the path and size.
|
|
@@ -37742,19 +38417,19 @@ Output:
|
|
|
37742
38417
|
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", `
|
|
37743
38418
|
|
|
37744
38419
|
Examples:
|
|
37745
|
-
$ ${
|
|
38420
|
+
$ ${CLI_NAME31} sql copy --from-db com.tinycloud.conversation-sync/conversations \\
|
|
37746
38421
|
--to-db xyz.tinycloud.listen/conversations \\
|
|
37747
38422
|
--space applications --dry-run
|
|
37748
|
-
$ ${
|
|
38423
|
+
$ ${CLI_NAME31} sql copy --from-space applications --from-db com.foo/data \\
|
|
37749
38424
|
--to-space applications --to-db com.bar/data \\
|
|
37750
38425
|
--table conversation --table participant
|
|
37751
38426
|
|
|
37752
38427
|
Notes:
|
|
37753
38428
|
- Refuses to run when (resolved space, db) is identical for source and destination.
|
|
37754
|
-
- Does NOT create destination tables. Run the target app once (or use \`${
|
|
38429
|
+
- Does NOT create destination tables. Run the target app once (or use \`${CLI_NAME31} sql execute\`)
|
|
37755
38430
|
to materialize the schema before copying.
|
|
37756
38431
|
- One row at a time; suitable for small/medium datasets. Large copies should
|
|
37757
|
-
use \`${
|
|
38432
|
+
use \`${CLI_NAME31} sql export\` + bulk import.
|
|
37758
38433
|
- Authorization: the active session/delegation must cover sql/read on source
|
|
37759
38434
|
AND sql/write on destination. Otherwise the relevant operation will fail.
|
|
37760
38435
|
`).action(async (options, cmd) => {
|
|
@@ -37854,7 +38529,7 @@ init_errors();
|
|
|
37854
38529
|
init_formatter();
|
|
37855
38530
|
init_theme();
|
|
37856
38531
|
init_permissions();
|
|
37857
|
-
import { CLI_NAME as
|
|
38532
|
+
import { CLI_NAME as CLI_NAME32 } from "@tinycloud/operations/cli-name";
|
|
37858
38533
|
import {
|
|
37859
38534
|
NodeWasmBindings as NodeWasmBindings2
|
|
37860
38535
|
} from "@tinycloud/node-sdk";
|
|
@@ -38128,7 +38803,7 @@ function formatStatus(summary) {
|
|
|
38128
38803
|
lines.push(`Default profile: ${theme.value(summary.defaultProfile)}`);
|
|
38129
38804
|
lines.push("");
|
|
38130
38805
|
if (summary.profiles.length === 0) {
|
|
38131
|
-
lines.push(theme.muted(`No profiles configured. Run: ${
|
|
38806
|
+
lines.push(theme.muted(`No profiles configured. Run: ${CLI_NAME32} init`));
|
|
38132
38807
|
return `${lines.join("\n")}
|
|
38133
38808
|
`;
|
|
38134
38809
|
}
|
|
@@ -38626,6 +39301,7 @@ function registerTinyCloudCommands(program) {
|
|
|
38626
39301
|
registerSqlCommand(program);
|
|
38627
39302
|
registerDuckdbCommand(program);
|
|
38628
39303
|
registerManifestCommand(program);
|
|
39304
|
+
registerKnowledgeCommand(program);
|
|
38629
39305
|
registerUpgradeCommand(program);
|
|
38630
39306
|
registerStatusCommand(program);
|
|
38631
39307
|
registerAccountCommand(program);
|