@tinycloud/cli 0.6.0-beta.9 → 0.6.1-beta.0
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/dist/index.js +90 -31
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -680,6 +680,11 @@ function buildAuthUrl(did, options = {}) {
|
|
|
680
680
|
const base = options.openkeyHost ?? DEFAULT_OPENKEY_HOST;
|
|
681
681
|
return `${base}/delegate?${params.toString()}`;
|
|
682
682
|
}
|
|
683
|
+
function shouldOpenBrowser(options) {
|
|
684
|
+
if (options.noPopup) return false;
|
|
685
|
+
const env = process.env.TC_AUTH_NO_POPUP ?? process.env.TC_NO_POPUP;
|
|
686
|
+
return env !== "1" && env !== "true";
|
|
687
|
+
}
|
|
683
688
|
async function callbackFlow(did, options = {}) {
|
|
684
689
|
return new Promise((resolve3, reject) => {
|
|
685
690
|
let timeout;
|
|
@@ -750,16 +755,21 @@ async function callbackFlow(did, options = {}) {
|
|
|
750
755
|
const port = addr.port;
|
|
751
756
|
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
752
757
|
const authUrl = buildAuthUrl(did, { ...options, callback: callbackUrl });
|
|
753
|
-
|
|
758
|
+
const openBrowser = shouldOpenBrowser(options);
|
|
759
|
+
if (openBrowser && isInteractive()) {
|
|
754
760
|
console.error(`Opening browser for authentication...`);
|
|
755
761
|
console.error(`If the browser doesn't open, visit: ${authUrl}`);
|
|
762
|
+
} else if (!openBrowser || isInteractive()) {
|
|
763
|
+
console.error(`Open this URL in a browser to authenticate: ${authUrl}`);
|
|
756
764
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
765
|
+
if (openBrowser) {
|
|
766
|
+
try {
|
|
767
|
+
const open = (await import("open")).default;
|
|
768
|
+
await open(authUrl);
|
|
769
|
+
} catch {
|
|
770
|
+
server.close();
|
|
771
|
+
throw new Error("Failed to open browser");
|
|
772
|
+
}
|
|
763
773
|
}
|
|
764
774
|
if (isInteractive()) {
|
|
765
775
|
console.error(`
|
|
@@ -816,7 +826,7 @@ Open this URL in a browser to authenticate:
|
|
|
816
826
|
|
|
817
827
|
// src/commands/init.ts
|
|
818
828
|
function registerInitCommand(program2) {
|
|
819
|
-
program2.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").action(async (options, cmd) => {
|
|
829
|
+
program2.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").action(async (options, cmd) => {
|
|
820
830
|
try {
|
|
821
831
|
const globalOpts = cmd.optsWithGlobals();
|
|
822
832
|
const profileName = options.name;
|
|
@@ -857,6 +867,7 @@ function registerInitCommand(program2) {
|
|
|
857
867
|
}
|
|
858
868
|
const delegationData = await startAuthFlow(did, {
|
|
859
869
|
paste: options.paste,
|
|
870
|
+
noPopup: options.popup === false,
|
|
860
871
|
jwk,
|
|
861
872
|
host
|
|
862
873
|
});
|
|
@@ -1366,7 +1377,7 @@ async function promptAuthMethod() {
|
|
|
1366
1377
|
}
|
|
1367
1378
|
function registerAuthCommand(program2) {
|
|
1368
1379
|
const auth = program2.command("auth").description("Authentication management");
|
|
1369
|
-
auth.command("login").description("Authenticate with TinyCloud").option("--paste", "Use manual paste mode instead of browser callback").option("--method <method>", "Authentication method: local or openkey").action(async (options, cmd) => {
|
|
1380
|
+
auth.command("login").description("Authenticate with TinyCloud").option("--paste", "Use manual paste mode instead of browser callback").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--method <method>", "Authentication method: local or openkey").action(async (options, cmd) => {
|
|
1370
1381
|
try {
|
|
1371
1382
|
const globalOpts = cmd.optsWithGlobals();
|
|
1372
1383
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
@@ -1386,7 +1397,10 @@ function registerAuthCommand(program2) {
|
|
|
1386
1397
|
if (method === "local") {
|
|
1387
1398
|
await handleLocalAuth(ctx.profile, ctx.host);
|
|
1388
1399
|
} else {
|
|
1389
|
-
await handleOpenKeyAuth(ctx.profile, ctx.host,
|
|
1400
|
+
await handleOpenKeyAuth(ctx.profile, ctx.host, {
|
|
1401
|
+
paste: options.paste,
|
|
1402
|
+
noPopup: options.popup === false
|
|
1403
|
+
});
|
|
1390
1404
|
}
|
|
1391
1405
|
} catch (error) {
|
|
1392
1406
|
handleError(error);
|
|
@@ -1402,11 +1416,14 @@ function registerAuthCommand(program2) {
|
|
|
1402
1416
|
handleError(error);
|
|
1403
1417
|
}
|
|
1404
1418
|
});
|
|
1405
|
-
auth.command("rotate").description("Rotate the active profile session key").option("--paste", "Use manual paste mode instead of browser callback").action(async (options, cmd) => {
|
|
1419
|
+
auth.command("rotate").description("Rotate the active profile session key").option("--paste", "Use manual paste mode instead of browser callback").option("--no-popup", "Print the OpenKey URL without opening a browser").action(async (options, cmd) => {
|
|
1406
1420
|
try {
|
|
1407
1421
|
const globalOpts = cmd.optsWithGlobals();
|
|
1408
1422
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
1409
|
-
await rotateAuthKey(ctx.profile, ctx.host, {
|
|
1423
|
+
await rotateAuthKey(ctx.profile, ctx.host, {
|
|
1424
|
+
paste: options.paste,
|
|
1425
|
+
noPopup: options.popup === false
|
|
1426
|
+
});
|
|
1410
1427
|
} catch (error) {
|
|
1411
1428
|
handleError(error);
|
|
1412
1429
|
}
|
|
@@ -1468,7 +1485,7 @@ function registerAuthCommand(program2) {
|
|
|
1468
1485
|
).option("--permission <file>", 'JSON permission request: { "permissions": PermissionEntry[] }').option("--manifest <fileOrBase64>", "Manifest file, base64:<json>, or raw base64 JSON").option(
|
|
1469
1486
|
"--expiry <duration>",
|
|
1470
1487
|
`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.`
|
|
1471
|
-
).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).action(async (options, cmd) => {
|
|
1488
|
+
).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").action(async (options, cmd) => {
|
|
1472
1489
|
try {
|
|
1473
1490
|
const globalOpts = cmd.optsWithGlobals();
|
|
1474
1491
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
@@ -1513,7 +1530,8 @@ function registerAuthCommand(program2) {
|
|
|
1513
1530
|
host: ctx.host,
|
|
1514
1531
|
permissions: group,
|
|
1515
1532
|
openkeyHost,
|
|
1516
|
-
expiry: expiryOption
|
|
1533
|
+
expiry: expiryOption,
|
|
1534
|
+
noPopup: options.popup === false
|
|
1517
1535
|
});
|
|
1518
1536
|
const delegation = portableFromOpenKeyDelegation(delegationData, group, ctx.host);
|
|
1519
1537
|
const stored = storedAdditionalDelegation(delegation, group);
|
|
@@ -1929,7 +1947,7 @@ function normalizePortableDelegation(delegation) {
|
|
|
1929
1947
|
return { ...delegation, expiry };
|
|
1930
1948
|
}
|
|
1931
1949
|
async function ensureDelegationAuthority(params) {
|
|
1932
|
-
if (params.node.hasRuntimePermissions(params.requested)) return;
|
|
1950
|
+
if (!params.force && params.node.hasRuntimePermissions(params.requested)) return;
|
|
1933
1951
|
if (params.profile.authMethod === "openkey") {
|
|
1934
1952
|
const key = await ProfileManager.getKey(params.ctx.profile);
|
|
1935
1953
|
if (!key) {
|
|
@@ -2095,13 +2113,20 @@ function groupPermissionsBySpace(permissions) {
|
|
|
2095
2113
|
function isRawPermission(permission) {
|
|
2096
2114
|
return permission.service === "tinycloud.encryption" && permission.path.startsWith("urn:tinycloud:encryption:");
|
|
2097
2115
|
}
|
|
2116
|
+
function returnedSpaceMatchesExpected(returnedSpace, expectedSpace) {
|
|
2117
|
+
if (returnedSpace === expectedSpace) return true;
|
|
2118
|
+
if (!returnedSpace.startsWith("tinycloud:")) return false;
|
|
2119
|
+
const returnedName = returnedSpace.slice(returnedSpace.lastIndexOf(":") + 1);
|
|
2120
|
+
return returnedName === expectedSpace;
|
|
2121
|
+
}
|
|
2098
2122
|
function portableFromOpenKeyDelegation(data, permissions, host) {
|
|
2099
2123
|
const primary = permissions.find((permission) => !isRawPermission(permission)) ?? permissions[0];
|
|
2100
2124
|
const returnedSpace = String(data.spaceId ?? primary.space ?? "encryption");
|
|
2101
2125
|
const expectedSpaces = new Set(
|
|
2102
2126
|
permissions.filter((permission) => !isRawPermission(permission)).map((permission) => permission.space)
|
|
2103
2127
|
);
|
|
2104
|
-
|
|
2128
|
+
const matchesExpectedSpace = expectedSpaces.size === 1 && returnedSpaceMatchesExpected(returnedSpace, Array.from(expectedSpaces)[0]);
|
|
2129
|
+
if (expectedSpaces.size > 0 && !matchesExpectedSpace) {
|
|
2105
2130
|
throw new CLIError(
|
|
2106
2131
|
"OPENKEY_SCOPE_MISMATCH",
|
|
2107
2132
|
`OpenKey returned delegation for ${returnedSpace}, expected ${Array.from(expectedSpaces).join(", ")}.`,
|
|
@@ -2117,7 +2142,7 @@ function portableFromOpenKeyDelegation(data, permissions, host) {
|
|
|
2117
2142
|
actions: primary.actions,
|
|
2118
2143
|
resources: permissions.map((permission) => ({
|
|
2119
2144
|
service: permission.service.startsWith("tinycloud.") ? permission.service.slice("tinycloud.".length) : permission.service,
|
|
2120
|
-
space: permission.space,
|
|
2145
|
+
space: isRawPermission(permission) ? permission.space : returnedSpace,
|
|
2121
2146
|
path: permission.path,
|
|
2122
2147
|
actions: [...permission.actions]
|
|
2123
2148
|
})),
|
|
@@ -2178,7 +2203,8 @@ async function rotateAuthKey(profileName, host, options = {}) {
|
|
|
2178
2203
|
authMethod: "openkey"
|
|
2179
2204
|
});
|
|
2180
2205
|
const result = await refreshOpenKeySession(profileName, host, {
|
|
2181
|
-
paste: options.paste
|
|
2206
|
+
paste: options.paste,
|
|
2207
|
+
noPopup: options.noPopup
|
|
2182
2208
|
});
|
|
2183
2209
|
outputRotationResult(result.profile, profileName, oldDid, "openkey");
|
|
2184
2210
|
}
|
|
@@ -2278,8 +2304,8 @@ async function handleLocalAuth(profileName, host, options = {}) {
|
|
|
2278
2304
|
}
|
|
2279
2305
|
return { profile: updatedProfile, sessionResult };
|
|
2280
2306
|
}
|
|
2281
|
-
async function handleOpenKeyAuth(profileName, host,
|
|
2282
|
-
const { profile, delegationData } = await refreshOpenKeySession(profileName, host,
|
|
2307
|
+
async function handleOpenKeyAuth(profileName, host, options = {}) {
|
|
2308
|
+
const { profile, delegationData } = await refreshOpenKeySession(profileName, host, options);
|
|
2283
2309
|
outputJson({
|
|
2284
2310
|
authenticated: true,
|
|
2285
2311
|
profile: profileName,
|
|
@@ -2300,6 +2326,7 @@ async function refreshOpenKeySession(profileName, host, options = {}) {
|
|
|
2300
2326
|
const profile = await ProfileManager.getProfile(profileName);
|
|
2301
2327
|
const delegationData = await startAuthFlow(profile.did, {
|
|
2302
2328
|
paste: options.paste,
|
|
2329
|
+
noPopup: options.noPopup,
|
|
2303
2330
|
jwk: key,
|
|
2304
2331
|
host,
|
|
2305
2332
|
openkeyHost: resolveOpenKeyHost(profile)
|
|
@@ -2330,14 +2357,19 @@ async function readStdin2() {
|
|
|
2330
2357
|
}
|
|
2331
2358
|
return Buffer.concat(chunks);
|
|
2332
2359
|
}
|
|
2360
|
+
async function kvHandle(node, spaceInput, profileName) {
|
|
2361
|
+
const spaceUri = await resolveSpaceUri(spaceInput, profileName);
|
|
2362
|
+
return spaceUri ? node.kvForSpace(spaceUri) : node.kv;
|
|
2363
|
+
}
|
|
2333
2364
|
function registerKvCommand(program2) {
|
|
2334
2365
|
const kv = program2.command("kv").description("Key-value store operations");
|
|
2335
|
-
kv.command("get <key>").description("Get a value by key").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").action(async (key, options, cmd) => {
|
|
2366
|
+
kv.command("get <key>").description("Get a value by key").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, options, cmd) => {
|
|
2336
2367
|
try {
|
|
2337
2368
|
const globalOpts = cmd.optsWithGlobals();
|
|
2338
2369
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
2339
2370
|
const node = await ensureAuthenticated(ctx);
|
|
2340
|
-
const
|
|
2371
|
+
const kv2 = await kvHandle(node, options.space, ctx.profile);
|
|
2372
|
+
const result = await withSpinner(`Getting ${key}...`, () => kv2.get(key));
|
|
2341
2373
|
if (!result.ok) {
|
|
2342
2374
|
if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
|
|
2343
2375
|
throw new CLIError("NOT_FOUND", `Key "${key}" not found`, ExitCode.NOT_FOUND);
|
|
@@ -2418,13 +2450,14 @@ function registerKvCommand(program2) {
|
|
|
2418
2450
|
handleError(error);
|
|
2419
2451
|
}
|
|
2420
2452
|
});
|
|
2421
|
-
kv.command("list").description("List keys").option("--prefix <prefix>", "Filter by key prefix").action(async (options, cmd) => {
|
|
2453
|
+
kv.command("list").description("List keys").option("--prefix <prefix>", "Filter by key prefix").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (options, cmd) => {
|
|
2422
2454
|
try {
|
|
2423
2455
|
const globalOpts = cmd.optsWithGlobals();
|
|
2424
2456
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
2425
2457
|
const node = await ensureAuthenticated(ctx);
|
|
2458
|
+
const kv2 = await kvHandle(node, options.space, ctx.profile);
|
|
2426
2459
|
const listOptions = options.prefix ? { prefix: options.prefix } : void 0;
|
|
2427
|
-
const result = await withSpinner("Listing keys...", () =>
|
|
2460
|
+
const result = await withSpinner("Listing keys...", () => kv2.list(listOptions));
|
|
2428
2461
|
if (!result.ok) {
|
|
2429
2462
|
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
2430
2463
|
}
|
|
@@ -2452,12 +2485,13 @@ function registerKvCommand(program2) {
|
|
|
2452
2485
|
handleError(error);
|
|
2453
2486
|
}
|
|
2454
2487
|
});
|
|
2455
|
-
kv.command("head <key>").description("Get metadata for a key (no body)").action(async (key,
|
|
2488
|
+
kv.command("head <key>").description("Get metadata for a key (no body)").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, options, cmd) => {
|
|
2456
2489
|
try {
|
|
2457
2490
|
const globalOpts = cmd.optsWithGlobals();
|
|
2458
2491
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
2459
2492
|
const node = await ensureAuthenticated(ctx);
|
|
2460
|
-
const
|
|
2493
|
+
const kv2 = await kvHandle(node, options.space, ctx.profile);
|
|
2494
|
+
const result = await withSpinner(`Checking ${key}...`, () => kv2.head(key));
|
|
2461
2495
|
if (!result.ok) {
|
|
2462
2496
|
if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
|
|
2463
2497
|
outputJson({ key, exists: false, metadata: {} });
|
|
@@ -3372,7 +3406,7 @@ async function ensureSecretsNode(ctx, options) {
|
|
|
3372
3406
|
return ensureAuthenticated(ctx, auth);
|
|
3373
3407
|
}
|
|
3374
3408
|
async function runSecretOperation(params) {
|
|
3375
|
-
const first = await
|
|
3409
|
+
const first = await runSecretOperationAttempt(params.label, params.operation);
|
|
3376
3410
|
if (first.ok || !shouldRequestSecretPermissions(first.error)) {
|
|
3377
3411
|
return first;
|
|
3378
3412
|
}
|
|
@@ -3394,10 +3428,20 @@ async function runSecretOperation(params) {
|
|
|
3394
3428
|
node: params.node,
|
|
3395
3429
|
requested,
|
|
3396
3430
|
expiryOption: void 0,
|
|
3397
|
-
yes: true
|
|
3431
|
+
yes: true,
|
|
3432
|
+
force: true
|
|
3398
3433
|
})
|
|
3399
3434
|
);
|
|
3400
|
-
return
|
|
3435
|
+
return runSecretOperationAttempt(params.label, params.operation);
|
|
3436
|
+
}
|
|
3437
|
+
async function runSecretOperationAttempt(label, operation) {
|
|
3438
|
+
try {
|
|
3439
|
+
return await withSpinner(label, operation);
|
|
3440
|
+
} catch (error) {
|
|
3441
|
+
const permissionError = thrownPermissionError(error);
|
|
3442
|
+
if (permissionError) return permissionError;
|
|
3443
|
+
throw error;
|
|
3444
|
+
}
|
|
3401
3445
|
}
|
|
3402
3446
|
function canRequestOwnerPermissions(profile) {
|
|
3403
3447
|
const posture = resolveProfilePosture(profile);
|
|
@@ -3407,6 +3451,21 @@ function shouldRequestSecretPermissions(error) {
|
|
|
3407
3451
|
if (error.code !== "PERMISSION_DENIED") return false;
|
|
3408
3452
|
return /permission|session expired|autosign|capabilit/i.test(error.message);
|
|
3409
3453
|
}
|
|
3454
|
+
function thrownPermissionError(error) {
|
|
3455
|
+
const record = error;
|
|
3456
|
+
const message = typeof record?.message === "string" ? record.message : String(error);
|
|
3457
|
+
const code = typeof record?.code === "string" ? record.code : "PERMISSION_DENIED";
|
|
3458
|
+
if (code !== "PERMISSION_DENIED" && !/permission|session expired|autosign|capabilit/i.test(message)) {
|
|
3459
|
+
return null;
|
|
3460
|
+
}
|
|
3461
|
+
return {
|
|
3462
|
+
ok: false,
|
|
3463
|
+
error: {
|
|
3464
|
+
code: "PERMISSION_DENIED",
|
|
3465
|
+
message
|
|
3466
|
+
}
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
3410
3469
|
function isStoredSessionExpired(session) {
|
|
3411
3470
|
const record = session;
|
|
3412
3471
|
const direct = parseDate(record.expiresAt ?? record.expiry ?? record.expirationTime);
|
|
@@ -3515,7 +3574,7 @@ function registerSecretsCommand(program2) {
|
|
|
3515
3574
|
handleError(error);
|
|
3516
3575
|
}
|
|
3517
3576
|
});
|
|
3518
|
-
secrets.command("get <name>").description("Get a secret value").option("--scope <scope>", "Logical secret scope").option("--space <scope>", "Deprecated alias for --scope").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
|
|
3577
|
+
secrets.command("get <name>").description("Get a secret value").option("--scope <scope>", "Logical secret scope").option("--space <scope>", "Deprecated alias for --scope").option("--raw", "Output raw value (no JSON wrapping)").option("--value-only", "Output only the secret value (alias for --raw)").option("-o, --output <file>", "Write value to file").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
|
|
3519
3578
|
try {
|
|
3520
3579
|
const globalOpts = cmd.optsWithGlobals();
|
|
3521
3580
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
@@ -3542,7 +3601,7 @@ function registerSecretsCommand(program2) {
|
|
|
3542
3601
|
outputJson({ name, written: options.output });
|
|
3543
3602
|
return;
|
|
3544
3603
|
}
|
|
3545
|
-
if (options.raw) {
|
|
3604
|
+
if (options.raw || options.valueOnly) {
|
|
3546
3605
|
process.stdout.write(value);
|
|
3547
3606
|
return;
|
|
3548
3607
|
}
|