@tinycloud/cli 0.5.1-beta.0 → 0.6.0-beta.10

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 CHANGED
@@ -548,8 +548,8 @@ var ProfileManager = class _ProfileManager {
548
548
  };
549
549
 
550
550
  // src/auth/local-key.ts
551
- import { TCWSessionManager, initPanicHook } from "@tinycloud/node-sdk-wasm";
552
- import { PrivateKeySigner } from "@tinycloud/node-sdk";
551
+ import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
552
+ import { PrivateKeySigner, pkhDid } from "@tinycloud/node-sdk";
553
553
  import { randomBytes } from "crypto";
554
554
  var wasmInitialized = false;
555
555
  function ensureWasm() {
@@ -568,6 +568,12 @@ function generateKey() {
568
568
  const did = mgr.getDID(keyId);
569
569
  return { jwk, did };
570
570
  }
571
+ function keyToDID(jwk) {
572
+ ensureWasm();
573
+ const mgr = new TCWSessionManager();
574
+ const keyId = importKey(mgr, JSON.stringify(jwk), "imported");
575
+ return mgr.getDID(keyId);
576
+ }
571
577
  function generateEthereumPrivateKey() {
572
578
  const keyBytes = randomBytes(32);
573
579
  return "0x" + keyBytes.toString("hex");
@@ -577,7 +583,7 @@ async function deriveAddress(privateKey) {
577
583
  return signer.getAddress();
578
584
  }
579
585
  function addressToDID(address, chainId = 1) {
580
- return `did:pkh:eip155:${chainId}:${address}`;
586
+ return pkhDid(address, chainId);
581
587
  }
582
588
  async function generateLocalIdentity(chainId = 1) {
583
589
  const privateKey = generateEthereumPrivateKey();
@@ -594,16 +600,45 @@ async function localKeySignIn(options) {
594
600
  });
595
601
  await node.signIn();
596
602
  const address = await new PrivateKeySigner(options.privateKey).getAddress();
603
+ const session = node.session;
604
+ if (!session) {
605
+ throw new Error("Local key sign-in did not produce a TinyCloud session");
606
+ }
597
607
  return {
598
- spaceId: node.spaceId ?? "",
608
+ spaceId: session.spaceId,
599
609
  address,
600
- chainId: 1
610
+ chainId: 1,
611
+ delegationHeader: session.delegationHeader,
612
+ delegationCid: session.delegationCid,
613
+ jwk: session.jwk,
614
+ verificationMethod: session.verificationMethod,
615
+ siwe: session.siwe,
616
+ signature: session.signature
601
617
  };
602
618
  }
603
619
 
604
620
  // src/auth/browser-auth.ts
605
621
  import { createServer } from "http";
606
622
  import { createInterface } from "readline";
623
+ var PRIVATE_JWK_FIELDS = /* @__PURE__ */ new Set([
624
+ "d",
625
+ "p",
626
+ "q",
627
+ "dp",
628
+ "dq",
629
+ "qi",
630
+ "oth",
631
+ "k"
632
+ ]);
633
+ function publicJwkForDelegation(jwk) {
634
+ const publicJwk = {};
635
+ for (const [key, value] of Object.entries(jwk)) {
636
+ if (!PRIVATE_JWK_FIELDS.has(key)) {
637
+ publicJwk[key] = value;
638
+ }
639
+ }
640
+ return publicJwk;
641
+ }
607
642
  async function startAuthFlow(did, options = {}) {
608
643
  if (options.paste) {
609
644
  return pasteFlow(did, options);
@@ -625,7 +660,9 @@ function buildAuthUrl(did, options = {}) {
625
660
  params.set("callback", options.callback);
626
661
  }
627
662
  if (options.jwk) {
628
- const jwkB64 = Buffer.from(JSON.stringify(options.jwk)).toString("base64url");
663
+ const jwkB64 = Buffer.from(
664
+ JSON.stringify(publicJwkForDelegation(options.jwk))
665
+ ).toString("base64url");
629
666
  params.set("jwk", jwkB64);
630
667
  }
631
668
  if (options.host) {
@@ -827,7 +864,7 @@ function registerInitCommand(program2) {
827
864
  await ProfileManager.setProfile(profileName, {
828
865
  ...profileConfig,
829
866
  spaceId: delegationData.spaceId,
830
- primaryDid: delegationData.primaryDid
867
+ ownerDid: delegationData.ownerDid
831
868
  });
832
869
  outputJson({
833
870
  profile: profileName,
@@ -843,12 +880,41 @@ function registerInitCommand(program2) {
843
880
  }
844
881
 
845
882
  // src/commands/auth.ts
883
+ import { get as httpGet } from "http";
884
+ import { get as httpsGet } from "https";
885
+ import { spawn } from "child_process";
886
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
887
+ import { dirname as dirname2 } from "path";
846
888
  import { createInterface as createInterface2 } from "readline";
847
889
 
890
+ // src/config/types.ts
891
+ var CLI_PROFILE_POSTURES = [
892
+ "owner-openkey",
893
+ "delegate-session",
894
+ "local-owner-key"
895
+ ];
896
+ var CLI_OPERATOR_TYPES = ["human", "agent"];
897
+ function isCLIProfilePosture(value) {
898
+ return typeof value === "string" && CLI_PROFILE_POSTURES.includes(value);
899
+ }
900
+ function isCLIOperatorType(value) {
901
+ return typeof value === "string" && CLI_OPERATOR_TYPES.includes(value);
902
+ }
903
+ function resolveProfilePosture(profile) {
904
+ if (isCLIProfilePosture(profile.posture)) return profile.posture;
905
+ if (profile.authMethod === "local") return "local-owner-key";
906
+ return "owner-openkey";
907
+ }
908
+ function resolveProfileOperatorType(profile) {
909
+ if (isCLIOperatorType(profile.operatorType)) return profile.operatorType;
910
+ return "human";
911
+ }
912
+
848
913
  // src/lib/sdk.ts
849
914
  import { TinyCloudNode } from "@tinycloud/node-sdk";
850
915
 
851
916
  // src/lib/permissions.ts
917
+ import { randomBytes as randomBytes2 } from "crypto";
852
918
  import { appendFile, readFile as readFile2 } from "fs/promises";
853
919
  import { join as join4 } from "path";
854
920
  import {
@@ -858,13 +924,22 @@ import {
858
924
  } from "@tinycloud/node-sdk";
859
925
 
860
926
  // src/lib/space.ts
927
+ import {
928
+ buildSpaceUri,
929
+ canonicalizeAddress,
930
+ makePkhSpaceId,
931
+ parsePkhDid,
932
+ parseSpaceUri
933
+ } from "@tinycloud/node-sdk";
861
934
  function resolveAddress(profile, session) {
862
935
  const sessAddr = session?.address;
863
- if (typeof sessAddr === "string" && sessAddr.length > 0) return sessAddr;
864
- if (profile.address) return profile.address;
865
- if (profile.primaryDid) {
866
- const match = profile.primaryDid.match(/^did:pkh:eip155:\d+:(0x[a-fA-F0-9]{40})$/);
867
- if (match) return match[1];
936
+ if (typeof sessAddr === "string" && sessAddr.length > 0) {
937
+ return canonicalizeAddress(sessAddr);
938
+ }
939
+ if (profile.address) return canonicalizeAddress(profile.address);
940
+ if (profile.ownerDid) {
941
+ const pkh = parsePkhDid(profile.ownerDid);
942
+ if (pkh) return pkh.address;
868
943
  }
869
944
  throw new CLIError(
870
945
  "ADDRESS_UNKNOWN",
@@ -879,7 +954,17 @@ function resolveChainId(profile, session) {
879
954
  }
880
955
  async function resolveSpaceUri(input, profileName) {
881
956
  if (!input) return void 0;
882
- if (input.startsWith("tinycloud:")) return input;
957
+ if (input.startsWith("tinycloud:")) {
958
+ const parsed = parseSpaceUri(input);
959
+ if (!parsed) {
960
+ throw new CLIError(
961
+ "INVALID_SPACE",
962
+ `Invalid --space "${input}". Use a short name ([A-Za-z0-9_-]) or a full tinycloud:... URI.`,
963
+ ExitCode.USAGE_ERROR
964
+ );
965
+ }
966
+ return buildSpaceUri(parsed.owner, parsed.name);
967
+ }
883
968
  if (!/^[A-Za-z0-9_-]+$/.test(input)) {
884
969
  throw new CLIError(
885
970
  "INVALID_SPACE",
@@ -891,16 +976,44 @@ async function resolveSpaceUri(input, profileName) {
891
976
  const session = await ProfileManager.getSession(profileName);
892
977
  const address = resolveAddress(profile, session);
893
978
  const chainId = resolveChainId(profile, session);
894
- return `tinycloud:pkh:eip155:${chainId}:${address}:${input}`;
979
+ return makePkhSpaceId(address, chainId, input);
895
980
  }
896
981
 
897
982
  // src/lib/permissions.ts
898
983
  function additionalDelegationsPath(profile) {
899
984
  return join4(PROFILES_DIR, profile, "additional-delegations.json");
900
985
  }
986
+ function permissionRequestsPath(profile) {
987
+ return join4(PROFILES_DIR, profile, "auth-requests.json");
988
+ }
901
989
  function grantHistoryPath(profile) {
902
990
  return join4(PROFILES_DIR, profile, "auth-grants.jsonl");
903
991
  }
992
+ function createPermissionRequestArtifact(params) {
993
+ return {
994
+ kind: "tinycloud.auth.request",
995
+ version: 1,
996
+ requestId: `req_${Date.now().toString(36)}_${randomBytes2(4).toString("hex")}`,
997
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
998
+ profile: params.profileName,
999
+ posture: resolveProfilePosture(params.profile),
1000
+ operatorType: resolveProfileOperatorType(params.profile),
1001
+ host: params.host,
1002
+ sessionDid: didWithoutFragment(params.profile.sessionDid ?? params.profile.did),
1003
+ ownerDid: params.profile.ownerDid,
1004
+ spaceId: params.profile.spaceId,
1005
+ requestedExpiry: params.requestedExpiry,
1006
+ requested: params.requested,
1007
+ command: {
1008
+ argv: params.argv ?? process.argv.slice(2),
1009
+ cwd: params.cwd ?? process.cwd()
1010
+ }
1011
+ };
1012
+ }
1013
+ function didWithoutFragment(did) {
1014
+ const fragment = did.indexOf("#");
1015
+ return fragment === -1 ? did : did.slice(0, fragment);
1016
+ }
904
1017
  async function loadAdditionalDelegations(profile) {
905
1018
  const raw = await readJson(
906
1019
  additionalDelegationsPath(profile)
@@ -918,6 +1031,41 @@ async function appendAdditionalDelegation(profile, entry) {
918
1031
  next.push(entry);
919
1032
  await saveAdditionalDelegations(profile, next);
920
1033
  }
1034
+ async function loadPermissionRequestArtifacts(profile) {
1035
+ const raw = await readJson(
1036
+ permissionRequestsPath(profile)
1037
+ );
1038
+ return Array.isArray(raw) ? raw.filter(isPermissionRequestArtifact) : [];
1039
+ }
1040
+ async function savePermissionRequestArtifacts(profile, entries) {
1041
+ const profileDir = join4(PROFILES_DIR, profile);
1042
+ await ensureDir(profileDir);
1043
+ await writeJson(permissionRequestsPath(profile), entries);
1044
+ }
1045
+ async function appendPermissionRequestArtifact(profile, artifact) {
1046
+ const existing = await loadPermissionRequestArtifacts(profile);
1047
+ const next = existing.filter((item) => item.requestId !== artifact.requestId);
1048
+ next.push(artifact);
1049
+ await savePermissionRequestArtifacts(profile, next);
1050
+ }
1051
+ async function getPermissionRequestArtifact(profile, requestId) {
1052
+ const existing = await loadPermissionRequestArtifacts(profile);
1053
+ return existing.find((item) => item.requestId === requestId) ?? null;
1054
+ }
1055
+ async function getLastPermissionRequestArtifact(profile) {
1056
+ const existing = await loadPermissionRequestArtifacts(profile);
1057
+ return existing.at(-1) ?? null;
1058
+ }
1059
+ function isPermissionRequestArtifact(value) {
1060
+ if (value === null || typeof value !== "object") return false;
1061
+ const candidate = value;
1062
+ return candidate.kind === "tinycloud.auth.request" && candidate.version === 1 && typeof candidate.requestId === "string" && Array.isArray(candidate.requested);
1063
+ }
1064
+ function isDelegationImportArtifact(value) {
1065
+ if (value === null || typeof value !== "object") return false;
1066
+ const candidate = value;
1067
+ return candidate.kind === "tinycloud.auth.delegation" && candidate.version === 1 && candidate.delegation !== void 0 && typeof candidate.delegation === "object";
1068
+ }
921
1069
  async function replayAdditionalDelegations(node, profile) {
922
1070
  const entries = await loadAdditionalDelegations(profile);
923
1071
  for (const entry of entries) {
@@ -1130,7 +1278,21 @@ async function createSDKInstance(ctx, options) {
1130
1278
  host: ctx.host,
1131
1279
  privateKey: effectivePrivateKey
1132
1280
  });
1133
- await node2.signIn();
1281
+ if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
1282
+ await node2.restoreSession({
1283
+ delegationHeader: session.delegationHeader,
1284
+ delegationCid: session.delegationCid,
1285
+ spaceId: session.spaceId,
1286
+ jwk: session.jwk ?? key,
1287
+ verificationMethod: session.verificationMethod ?? profile.sessionDid ?? profile.did,
1288
+ address: session.address,
1289
+ chainId: session.chainId,
1290
+ siwe: session.siwe,
1291
+ signature: session.signature
1292
+ });
1293
+ } else {
1294
+ await node2.signIn();
1295
+ }
1134
1296
  await replayAdditionalDelegations(node2, ctx.profile);
1135
1297
  return node2;
1136
1298
  }
@@ -1240,6 +1402,15 @@ function registerAuthCommand(program2) {
1240
1402
  handleError(error);
1241
1403
  }
1242
1404
  });
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) => {
1406
+ try {
1407
+ const globalOpts = cmd.optsWithGlobals();
1408
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1409
+ await rotateAuthKey(ctx.profile, ctx.host, { paste: options.paste });
1410
+ } catch (error) {
1411
+ handleError(error);
1412
+ }
1413
+ });
1243
1414
  auth.command("status").description("Show current authentication state").action(async (_options, cmd) => {
1244
1415
  try {
1245
1416
  const globalOpts = cmd.optsWithGlobals();
@@ -1252,17 +1423,22 @@ function registerAuthCommand(program2) {
1252
1423
  } catch {
1253
1424
  profile = null;
1254
1425
  }
1426
+ const posture = profile ? resolveProfilePosture(profile) : null;
1427
+ const operatorType = profile ? resolveProfileOperatorType(profile) : null;
1255
1428
  const authenticated = session !== null;
1256
1429
  if (shouldOutputJson()) {
1257
1430
  outputJson({
1258
1431
  authenticated,
1259
1432
  did: profile?.did ?? null,
1260
- primaryDid: profile?.primaryDid ?? null,
1433
+ sessionDid: profile?.sessionDid ?? null,
1434
+ ownerDid: profile?.ownerDid ?? null,
1261
1435
  spaceId: profile?.spaceId ?? null,
1262
1436
  host: ctx.host,
1263
1437
  profile: ctx.profile,
1264
1438
  hasKey: hasKey !== null,
1265
1439
  authMethod: profile?.authMethod ?? null,
1440
+ posture,
1441
+ operatorType,
1266
1442
  address: profile?.address ?? null
1267
1443
  });
1268
1444
  } else {
@@ -1270,9 +1446,12 @@ function registerAuthCommand(program2) {
1270
1446
  process.stdout.write(formatField("Profile", ctx.profile) + "\n");
1271
1447
  process.stdout.write(formatField("Authenticated", authenticated) + "\n");
1272
1448
  process.stdout.write(formatField("Auth Method", profile?.authMethod ?? null) + "\n");
1449
+ process.stdout.write(formatField("Posture", posture) + "\n");
1450
+ process.stdout.write(formatField("Operator", operatorType) + "\n");
1273
1451
  process.stdout.write(formatField("Host", ctx.host) + "\n");
1274
1452
  process.stdout.write(formatField("DID", profile?.did ?? null) + "\n");
1275
- process.stdout.write(formatField("Primary DID", profile?.primaryDid ?? null) + "\n");
1453
+ process.stdout.write(formatField("Session DID", profile?.sessionDid ?? null) + "\n");
1454
+ process.stdout.write(formatField("Owner DID", profile?.ownerDid ?? null) + "\n");
1276
1455
  process.stdout.write(formatField("Address", profile?.address ?? null) + "\n");
1277
1456
  process.stdout.write(formatField("Space ID", profile?.spaceId ?? null) + "\n");
1278
1457
  process.stdout.write(formatField("Has Key", hasKey !== null) + "\n");
@@ -1281,7 +1460,7 @@ function registerAuthCommand(program2) {
1281
1460
  handleError(error);
1282
1461
  }
1283
1462
  });
1284
- auth.command("request").description("Request additional TinyCloud permissions for the active session").option(
1463
+ auth.command("request").description("Create a TinyCloud permission request artifact").option(
1285
1464
  "--cap <spec>",
1286
1465
  "Capability spec: tinycloud.<service>:<space>:<path>:<actions-csv> (repeatable)",
1287
1466
  (value, previous) => [...previous, value],
@@ -1289,13 +1468,13 @@ function registerAuthCommand(program2) {
1289
1468
  ).option("--permission <file>", 'JSON permission request: { "permissions": PermissionEntry[] }').option("--manifest <fileOrBase64>", "Manifest file, base64:<json>, or raw base64 JSON").option(
1290
1469
  "--expiry <duration>",
1291
1470
  `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.`
1292
- ).option("--yes", "Skip local-key TTY confirmation", false).action(async (options, cmd) => {
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) => {
1293
1472
  try {
1294
1473
  const globalOpts = cmd.optsWithGlobals();
1295
1474
  const ctx = await ProfileManager.resolveContext(globalOpts);
1296
1475
  const profile = await ProfileManager.getProfile(ctx.profile);
1297
- const session = await ProfileManager.getSession(ctx.profile);
1298
1476
  const requested = await collectRequestedPermissions(options, ctx.profile);
1477
+ const expiryOption = parseExpiryOption(options.expiry);
1299
1478
  if (requested.length === 0) {
1300
1479
  throw new CLIError(
1301
1480
  "NO_CAPS_REQUESTED",
@@ -1303,6 +1482,18 @@ function registerAuthCommand(program2) {
1303
1482
  ExitCode.USAGE_ERROR
1304
1483
  );
1305
1484
  }
1485
+ if (!options.grant) {
1486
+ const artifact = createPermissionRequestArtifact({
1487
+ profileName: ctx.profile,
1488
+ profile,
1489
+ host: ctx.host,
1490
+ requested,
1491
+ requestedExpiry: expiryOption
1492
+ });
1493
+ await appendPermissionRequestArtifact(ctx.profile, artifact);
1494
+ await emitPermissionRequestArtifact(artifact, options.emit);
1495
+ return;
1496
+ }
1306
1497
  const node = await ensureAuthenticated(ctx);
1307
1498
  if (node.hasRuntimePermissions(requested)) {
1308
1499
  outputJson({ changed: false, missing: [], added: [] });
@@ -1316,14 +1507,13 @@ function registerAuthCommand(program2) {
1316
1507
  const delegationCids2 = [];
1317
1508
  let expiry2;
1318
1509
  const openkeyHost = resolveOpenKeyHost(profile);
1319
- const expiryOption2 = parseExpiryOption(options.expiry);
1320
1510
  for (const group of groupPermissionsBySpace(requested)) {
1321
1511
  const delegationData = await startAuthFlow(profile.did, {
1322
1512
  jwk: key,
1323
1513
  host: ctx.host,
1324
1514
  permissions: group,
1325
1515
  openkeyHost,
1326
- expiry: expiryOption2
1516
+ expiry: expiryOption
1327
1517
  });
1328
1518
  const delegation = portableFromOpenKeyDelegation(delegationData, group, ctx.host);
1329
1519
  const stored = storedAdditionalDelegation(delegation, group);
@@ -1358,8 +1548,6 @@ function registerAuthCommand(program2) {
1358
1548
  ExitCode.USAGE_ERROR
1359
1549
  );
1360
1550
  }
1361
- void session;
1362
- const expiryOption = parseExpiryOption(options.expiry);
1363
1551
  const delegations = await node.grantRuntimePermissions(
1364
1552
  requested,
1365
1553
  expiryOption !== void 0 ? { expiry: expiryOption } : void 0
@@ -1394,6 +1582,136 @@ function registerAuthCommand(program2) {
1394
1582
  handleError(error);
1395
1583
  }
1396
1584
  });
1585
+ auth.command("import [source]").description("Import a TinyCloud delegation or permission request artifact").option("--stdin", "Read the JSON artifact from stdin").option("--paste", "Read the JSON artifact from stdin").action(async (source, options, cmd) => {
1586
+ try {
1587
+ const globalOpts = cmd.optsWithGlobals();
1588
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1589
+ const raw = await readAuthArtifactSource(source, {
1590
+ stdin: options.stdin === true || options.paste === true
1591
+ });
1592
+ const parsed = JSON.parse(raw);
1593
+ if (isPermissionRequestArtifact(parsed)) {
1594
+ await appendPermissionRequestArtifact(ctx.profile, parsed);
1595
+ outputJson({
1596
+ imported: true,
1597
+ kind: parsed.kind,
1598
+ requestId: parsed.requestId,
1599
+ requested: parsed.requested,
1600
+ next: `tc auth retry ${parsed.requestId}`
1601
+ });
1602
+ return;
1603
+ }
1604
+ const imported = normalizeDelegationImport(parsed);
1605
+ const node = await ensureAuthenticated(ctx);
1606
+ await appendAdditionalDelegation(ctx.profile, storedAdditionalDelegation(
1607
+ imported.delegation,
1608
+ imported.permissions
1609
+ ));
1610
+ await node.useRuntimeDelegation(imported.delegation);
1611
+ await appendGrantHistory(ctx.profile, {
1612
+ addedCaps: imported.permissions,
1613
+ source: "cli",
1614
+ delegationCid: imported.delegation.cid,
1615
+ expiry: imported.delegation.expiry.toISOString()
1616
+ });
1617
+ outputJson({
1618
+ imported: true,
1619
+ kind: "tinycloud.auth.delegation",
1620
+ requestId: imported.requestId ?? null,
1621
+ delegationCid: imported.delegation.cid,
1622
+ permissions: imported.permissions,
1623
+ expiry: imported.delegation.expiry.toISOString()
1624
+ });
1625
+ } catch (error) {
1626
+ handleError(error);
1627
+ }
1628
+ });
1629
+ auth.command("grant [request]").description("Grant a TinyCloud permission request artifact to its requester").option("--stdin", "Read the JSON request artifact from stdin").option("--paste", "Read the JSON request artifact from stdin").option("--yes", "Skip local-key TTY confirmation", false).action(async (source, options, cmd) => {
1630
+ try {
1631
+ const globalOpts = cmd.optsWithGlobals();
1632
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1633
+ const profile = await ProfileManager.getProfile(ctx.profile);
1634
+ const raw = await readAuthArtifactSource(source, {
1635
+ stdin: options.stdin === true || options.paste === true
1636
+ });
1637
+ const parsed = JSON.parse(raw);
1638
+ if (!isPermissionRequestArtifact(parsed)) {
1639
+ throw new CLIError(
1640
+ "INVALID_AUTH_REQUEST",
1641
+ "Auth grant requires a tinycloud.auth.request artifact.",
1642
+ ExitCode.USAGE_ERROR
1643
+ );
1644
+ }
1645
+ const node = await ensureAuthenticated(ctx);
1646
+ await ensureDelegationAuthority({
1647
+ ctx,
1648
+ profile,
1649
+ node,
1650
+ requested: parsed.requested,
1651
+ expiryOption: parsed.requestedExpiry,
1652
+ yes: options.yes === true
1653
+ });
1654
+ const result = await node.delegateTo(
1655
+ parsed.sessionDid,
1656
+ parsed.requested,
1657
+ parsed.requestedExpiry !== void 0 ? { expiry: parsed.requestedExpiry } : void 0
1658
+ );
1659
+ outputJson({
1660
+ kind: "tinycloud.auth.delegation",
1661
+ version: 1,
1662
+ requestId: parsed.requestId,
1663
+ delegationCid: result.delegation.cid,
1664
+ delegation: result.delegation,
1665
+ permissions: parsed.requested,
1666
+ expiry: result.delegation.expiry.toISOString(),
1667
+ prompted: result.prompted
1668
+ });
1669
+ } catch (error) {
1670
+ handleError(error);
1671
+ }
1672
+ });
1673
+ auth.command("retry [requestId]").description("Check whether a stored permission request is now satisfied").option("--last", "Use the latest stored permission request for this profile").option("--exec", "Run the captured command when the request is covered").action(async (requestId, options, cmd) => {
1674
+ try {
1675
+ const globalOpts = cmd.optsWithGlobals();
1676
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1677
+ const artifact = options.last ? await getLastPermissionRequestArtifact(ctx.profile) : requestId ? await getPermissionRequestArtifact(ctx.profile, requestId) : null;
1678
+ if (!artifact) {
1679
+ throw new CLIError(
1680
+ "REQUEST_NOT_FOUND",
1681
+ options.last ? `No stored permission requests exist for profile "${ctx.profile}".` : "Provide a requestId or use --last.",
1682
+ ExitCode.NOT_FOUND
1683
+ );
1684
+ }
1685
+ const node = await ensureAuthenticated(ctx);
1686
+ const covered = node.hasRuntimePermissions(artifact.requested);
1687
+ if (options.exec) {
1688
+ if (!covered) {
1689
+ throw new CLIError(
1690
+ "PERMISSIONS_MISSING",
1691
+ `Request ${artifact.requestId} is not covered yet. Import a delegation, then retry with --exec.`,
1692
+ ExitCode.PERMISSION_DENIED
1693
+ );
1694
+ }
1695
+ if (!artifact.command?.argv?.length) {
1696
+ throw new CLIError(
1697
+ "COMMAND_NOT_CAPTURED",
1698
+ `Request ${artifact.requestId} does not include a captured command.`,
1699
+ ExitCode.USAGE_ERROR
1700
+ );
1701
+ }
1702
+ await execCapturedCommand(artifact.command);
1703
+ return;
1704
+ }
1705
+ outputJson({
1706
+ requestId: artifact.requestId,
1707
+ covered,
1708
+ missing: covered ? [] : artifact.requested,
1709
+ command: artifact.command ?? null
1710
+ });
1711
+ } catch (error) {
1712
+ handleError(error);
1713
+ }
1714
+ });
1397
1715
  auth.command("caps").description("Show granted capabilities for the active session").option("--diff <spec>", "Show missing capabilities for a spec").option("--history", "Show recent permission grants").action(async (options, cmd) => {
1398
1716
  try {
1399
1717
  const globalOpts = cmd.optsWithGlobals();
@@ -1459,23 +1777,31 @@ function registerAuthCommand(program2) {
1459
1777
  const profile = await ProfileManager.getProfile(ctx.profile);
1460
1778
  const session = await ProfileManager.getSession(ctx.profile);
1461
1779
  const authenticated = session !== null;
1780
+ const posture = resolveProfilePosture(profile);
1781
+ const operatorType = resolveProfileOperatorType(profile);
1462
1782
  if (shouldOutputJson()) {
1463
1783
  outputJson({
1464
1784
  profile: ctx.profile,
1465
1785
  did: profile.did,
1466
- primaryDid: profile.primaryDid ?? null,
1786
+ sessionDid: profile.sessionDid ?? null,
1787
+ ownerDid: profile.ownerDid ?? null,
1467
1788
  spaceId: profile.spaceId ?? null,
1468
1789
  host: profile.host,
1469
1790
  authenticated,
1470
1791
  authMethod: profile.authMethod ?? null,
1792
+ posture,
1793
+ operatorType,
1471
1794
  address: profile.address ?? null
1472
1795
  });
1473
1796
  } else {
1474
1797
  process.stdout.write(theme.heading("Identity") + "\n");
1475
1798
  process.stdout.write(formatField("Profile", ctx.profile) + "\n");
1476
1799
  process.stdout.write(formatField("DID", profile.did) + "\n");
1477
- process.stdout.write(formatField("Primary DID", profile.primaryDid ?? null) + "\n");
1800
+ process.stdout.write(formatField("Session DID", profile.sessionDid ?? null) + "\n");
1801
+ process.stdout.write(formatField("Owner DID", profile.ownerDid ?? null) + "\n");
1478
1802
  process.stdout.write(formatField("Auth Method", profile.authMethod ?? null) + "\n");
1803
+ process.stdout.write(formatField("Posture", posture) + "\n");
1804
+ process.stdout.write(formatField("Operator", operatorType) + "\n");
1479
1805
  process.stdout.write(formatField("Address", profile.address ?? null) + "\n");
1480
1806
  process.stdout.write(formatField("Space ID", profile.spaceId ?? null) + "\n");
1481
1807
  process.stdout.write(formatField("Host", profile.host) + "\n");
@@ -1486,6 +1812,210 @@ function registerAuthCommand(program2) {
1486
1812
  }
1487
1813
  });
1488
1814
  }
1815
+ async function emitPermissionRequestArtifact(artifact, emitOption) {
1816
+ if (typeof emitOption === "string" && emitOption.length > 0) {
1817
+ await mkdir2(dirname2(emitOption), { recursive: true });
1818
+ await writeFile2(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
1819
+ outputJson({
1820
+ emitted: true,
1821
+ path: emitOption,
1822
+ requestId: artifact.requestId,
1823
+ requested: artifact.requested
1824
+ });
1825
+ return;
1826
+ }
1827
+ outputJson(artifact);
1828
+ }
1829
+ async function readAuthArtifactSource(source, options) {
1830
+ if (options.stdin || source === "-" || !source && !isInteractive()) {
1831
+ return readStdin();
1832
+ }
1833
+ if (!source) {
1834
+ throw new CLIError(
1835
+ "IMPORT_SOURCE_REQUIRED",
1836
+ "Provide an artifact file, URL, or use --stdin.",
1837
+ ExitCode.USAGE_ERROR
1838
+ );
1839
+ }
1840
+ if (source.startsWith("http://") || source.startsWith("https://")) {
1841
+ return readUrl(source);
1842
+ }
1843
+ return readFile3(source, "utf8");
1844
+ }
1845
+ async function readStdin() {
1846
+ const chunks = [];
1847
+ for await (const chunk of process.stdin) {
1848
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
1849
+ }
1850
+ return Buffer.concat(chunks).toString("utf8");
1851
+ }
1852
+ function readUrl(source) {
1853
+ return new Promise((resolve3, reject) => {
1854
+ const getter = source.startsWith("https://") ? httpsGet : httpGet;
1855
+ const request = getter(source, (response) => {
1856
+ const status = response.statusCode ?? 0;
1857
+ if (status >= 300 && status < 400 && response.headers.location) {
1858
+ response.resume();
1859
+ readUrl(new URL(response.headers.location, source).toString()).then(resolve3, reject);
1860
+ return;
1861
+ }
1862
+ if (status < 200 || status >= 300) {
1863
+ response.resume();
1864
+ reject(new CLIError(
1865
+ "IMPORT_FETCH_FAILED",
1866
+ `Failed to fetch ${source}: HTTP ${status}.`,
1867
+ ExitCode.ERROR
1868
+ ));
1869
+ return;
1870
+ }
1871
+ const chunks = [];
1872
+ response.on("data", (chunk) => {
1873
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1874
+ });
1875
+ response.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
1876
+ });
1877
+ request.on("error", reject);
1878
+ });
1879
+ }
1880
+ function normalizeDelegationImport(value) {
1881
+ if (isDelegationImportArtifact(value)) {
1882
+ const delegation = normalizePortableDelegation(value.delegation);
1883
+ return {
1884
+ requestId: value.requestId,
1885
+ delegation,
1886
+ permissions: Array.isArray(value.permissions) && value.permissions.length > 0 ? value.permissions : permissionsFromDelegation(delegation)
1887
+ };
1888
+ }
1889
+ if (isStoredDelegationLike(value)) {
1890
+ const delegation = normalizePortableDelegation(value.delegation);
1891
+ return {
1892
+ delegation,
1893
+ permissions: Array.isArray(value.permissions) && value.permissions.length > 0 ? value.permissions : permissionsFromDelegation(delegation)
1894
+ };
1895
+ }
1896
+ if (isPortableDelegationLike(value)) {
1897
+ const delegation = normalizePortableDelegation(value);
1898
+ return {
1899
+ delegation,
1900
+ permissions: permissionsFromDelegation(delegation)
1901
+ };
1902
+ }
1903
+ throw new CLIError(
1904
+ "INVALID_AUTH_IMPORT",
1905
+ "Auth import must be a tinycloud.auth.delegation artifact, a portable delegation, or a tinycloud.auth.request artifact.",
1906
+ ExitCode.USAGE_ERROR
1907
+ );
1908
+ }
1909
+ function isStoredDelegationLike(value) {
1910
+ if (value === null || typeof value !== "object") return false;
1911
+ const candidate = value;
1912
+ return isPortableDelegationLike(candidate.delegation);
1913
+ }
1914
+ function isPortableDelegationLike(value) {
1915
+ if (value === null || typeof value !== "object") return false;
1916
+ const candidate = value;
1917
+ return typeof candidate.cid === "string" && typeof candidate.spaceId === "string" && typeof candidate.path === "string" && Array.isArray(candidate.actions) && candidate.delegationHeader !== void 0 && typeof candidate.delegationHeader === "object";
1918
+ }
1919
+ function normalizePortableDelegation(delegation) {
1920
+ const rawExpiry = delegation.expiry;
1921
+ const expiry = rawExpiry instanceof Date ? rawExpiry : new Date(String(rawExpiry));
1922
+ if (Number.isNaN(expiry.getTime())) {
1923
+ throw new CLIError(
1924
+ "INVALID_AUTH_IMPORT",
1925
+ "Imported delegation must include a valid expiry.",
1926
+ ExitCode.USAGE_ERROR
1927
+ );
1928
+ }
1929
+ return { ...delegation, expiry };
1930
+ }
1931
+ async function ensureDelegationAuthority(params) {
1932
+ if (!params.force && params.node.hasRuntimePermissions(params.requested)) return;
1933
+ if (params.profile.authMethod === "openkey") {
1934
+ const key = await ProfileManager.getKey(params.ctx.profile);
1935
+ if (!key) {
1936
+ throw new CLIError(
1937
+ "NO_KEY",
1938
+ `No key found for profile "${params.ctx.profile}". Run \`tc init\` first.`,
1939
+ ExitCode.AUTH_REQUIRED
1940
+ );
1941
+ }
1942
+ const openkeyHost = resolveOpenKeyHost(params.profile);
1943
+ for (const group of groupPermissionsBySpace(params.requested)) {
1944
+ const delegationData = await startAuthFlow(params.profile.did, {
1945
+ jwk: key,
1946
+ host: params.ctx.host,
1947
+ permissions: group,
1948
+ openkeyHost,
1949
+ expiry: params.expiryOption
1950
+ });
1951
+ const delegation = portableFromOpenKeyDelegation(delegationData, group, params.ctx.host);
1952
+ await appendAdditionalDelegation(
1953
+ params.ctx.profile,
1954
+ storedAdditionalDelegation(delegation, group)
1955
+ );
1956
+ await params.node.useRuntimeDelegation(delegation);
1957
+ await appendGrantHistory(params.ctx.profile, {
1958
+ addedCaps: group,
1959
+ source: "cli",
1960
+ delegationCid: delegation.cid,
1961
+ expiry: delegation.expiry.toISOString()
1962
+ });
1963
+ }
1964
+ return;
1965
+ }
1966
+ if (isInteractive()) {
1967
+ if (!params.yes) {
1968
+ await confirmPermissionRequest(params.requested);
1969
+ }
1970
+ } else if (!params.yes) {
1971
+ throw new CLIError(
1972
+ "CONFIRMATION_REQUIRED",
1973
+ "Local-key auth grants in non-interactive mode require --yes.",
1974
+ ExitCode.USAGE_ERROR
1975
+ );
1976
+ }
1977
+ const delegations = await params.node.grantRuntimePermissions(
1978
+ params.requested,
1979
+ params.expiryOption !== void 0 ? { expiry: params.expiryOption } : void 0
1980
+ );
1981
+ for (const delegation of delegations) {
1982
+ const covering = permissionsFromDelegation(delegation);
1983
+ await appendAdditionalDelegation(
1984
+ params.ctx.profile,
1985
+ storedAdditionalDelegation(delegation, covering)
1986
+ );
1987
+ await appendGrantHistory(params.ctx.profile, {
1988
+ addedCaps: covering,
1989
+ source: "cli",
1990
+ delegationCid: delegation.cid,
1991
+ expiry: delegation.expiry.toISOString()
1992
+ });
1993
+ }
1994
+ }
1995
+ function execCapturedCommand(command) {
1996
+ return new Promise((resolve3, reject) => {
1997
+ const child = spawn(process.execPath, [process.argv[1], ...command.argv], {
1998
+ cwd: command.cwd,
1999
+ env: process.env,
2000
+ stdio: "inherit"
2001
+ });
2002
+ child.on("error", reject);
2003
+ child.on("exit", (code, signal) => {
2004
+ if (signal) {
2005
+ reject(new CLIError(
2006
+ "COMMAND_SIGNAL",
2007
+ `Captured command exited from signal ${signal}.`,
2008
+ ExitCode.ERROR
2009
+ ));
2010
+ return;
2011
+ }
2012
+ if (code && code !== 0) {
2013
+ process.exitCode = code;
2014
+ }
2015
+ resolve3();
2016
+ });
2017
+ });
2018
+ }
1489
2019
  async function collectRequestedPermissions(options, profile) {
1490
2020
  const permissions = [];
1491
2021
  for (const spec of options.cap ?? []) {
@@ -1545,18 +2075,40 @@ function parseExpiryOption(raw) {
1545
2075
  }
1546
2076
  function groupPermissionsBySpace(permissions) {
1547
2077
  const groups = /* @__PURE__ */ new Map();
2078
+ const rawEntries = [];
1548
2079
  for (const permission of permissions) {
2080
+ if (isRawPermission(permission)) {
2081
+ rawEntries.push(permission);
2082
+ continue;
2083
+ }
1549
2084
  const group = groups.get(permission.space) ?? [];
1550
2085
  group.push(permission);
1551
2086
  groups.set(permission.space, group);
1552
2087
  }
1553
- return Array.from(groups.values());
2088
+ const grouped = Array.from(groups.values());
2089
+ if (grouped.length === 0) {
2090
+ return rawEntries.length > 0 ? [rawEntries] : [];
2091
+ }
2092
+ grouped[0].push(...rawEntries);
2093
+ return grouped;
2094
+ }
2095
+ function isRawPermission(permission) {
2096
+ return permission.service === "tinycloud.encryption" && permission.path.startsWith("urn:tinycloud:encryption:");
2097
+ }
2098
+ function returnedSpaceMatchesExpected(returnedSpace, expectedSpace) {
2099
+ if (returnedSpace === expectedSpace) return true;
2100
+ if (!returnedSpace.startsWith("tinycloud:")) return false;
2101
+ const returnedName = returnedSpace.slice(returnedSpace.lastIndexOf(":") + 1);
2102
+ return returnedName === expectedSpace;
1554
2103
  }
1555
2104
  function portableFromOpenKeyDelegation(data, permissions, host) {
1556
- const primary = permissions[0];
1557
- const returnedSpace = String(data.spaceId ?? primary.space);
1558
- const expectedSpaces = new Set(permissions.map((permission) => permission.space));
1559
- if (expectedSpaces.size !== 1 || !expectedSpaces.has(returnedSpace)) {
2105
+ const primary = permissions.find((permission) => !isRawPermission(permission)) ?? permissions[0];
2106
+ const returnedSpace = String(data.spaceId ?? primary.space ?? "encryption");
2107
+ const expectedSpaces = new Set(
2108
+ permissions.filter((permission) => !isRawPermission(permission)).map((permission) => permission.space)
2109
+ );
2110
+ const matchesExpectedSpace = expectedSpaces.size === 1 && returnedSpaceMatchesExpected(returnedSpace, Array.from(expectedSpaces)[0]);
2111
+ if (expectedSpaces.size > 0 && !matchesExpectedSpace) {
1560
2112
  throw new CLIError(
1561
2113
  "OPENKEY_SCOPE_MISMATCH",
1562
2114
  `OpenKey returned delegation for ${returnedSpace}, expected ${Array.from(expectedSpaces).join(", ")}.`,
@@ -1591,15 +2143,74 @@ function inferDelegationExpiry(data) {
1591
2143
  }
1592
2144
  return new Date(Date.now() + 60 * 60 * 1e3);
1593
2145
  }
1594
- async function handleLocalAuth(profileName, host) {
2146
+ async function rotateAuthKey(profileName, host, options = {}) {
2147
+ const profile = await ProfileManager.getProfile(profileName);
2148
+ const posture = resolveProfilePosture(profile);
2149
+ const oldDid = profile.sessionDid ?? profile.did;
2150
+ if (posture === "delegate-session") {
2151
+ throw new CLIError(
2152
+ "ROTATE_DELEGATE_SESSION_UNSUPPORTED",
2153
+ `Profile "${profileName}" is a delegated session. Request or import a new owner delegation instead of rotating it locally.`,
2154
+ ExitCode.PERMISSION_DENIED
2155
+ );
2156
+ }
2157
+ if (profile.authMethod === "local" || posture === "local-owner-key") {
2158
+ if (!profile.privateKey) {
2159
+ throw new CLIError(
2160
+ "LOCAL_OWNER_KEY_REQUIRED",
2161
+ `Profile "${profileName}" does not have a local owner private key. Run \`tc auth login --method local\` first.`,
2162
+ ExitCode.AUTH_REQUIRED
2163
+ );
2164
+ }
2165
+ await ProfileManager.clearSession(profileName);
2166
+ const result2 = await handleLocalAuth(profileName, host, {
2167
+ emitOutput: false,
2168
+ forceSessionKey: true
2169
+ });
2170
+ outputRotationResult(result2.profile, profileName, oldDid, "local");
2171
+ return;
2172
+ }
2173
+ const { jwk, did } = await withSpinner("Generating session key...", async () => {
2174
+ return generateKey();
2175
+ });
2176
+ await ProfileManager.setKey(profileName, jwk);
2177
+ await ProfileManager.clearSession(profileName);
2178
+ await ProfileManager.setProfile(profileName, {
2179
+ ...profile,
2180
+ host,
2181
+ did,
2182
+ sessionDid: did,
2183
+ posture: profile.posture ?? "owner-openkey",
2184
+ operatorType: profile.operatorType ?? "human",
2185
+ authMethod: "openkey"
2186
+ });
2187
+ const result = await refreshOpenKeySession(profileName, host, {
2188
+ paste: options.paste
2189
+ });
2190
+ outputRotationResult(result.profile, profileName, oldDid, "openkey");
2191
+ }
2192
+ function outputRotationResult(profile, profileName, oldDid, authMethod) {
2193
+ outputJson({
2194
+ rotated: true,
2195
+ profile: profileName,
2196
+ oldDid,
2197
+ did: profile.did,
2198
+ sessionDid: profile.sessionDid ?? null,
2199
+ authMethod,
2200
+ spaceId: profile.spaceId ?? null
2201
+ });
2202
+ }
2203
+ async function handleLocalAuth(profileName, host, options = {}) {
1595
2204
  const profile = await ProfileManager.getProfile(profileName).catch(() => null);
2205
+ const posture = profile ? resolveProfilePosture(profile) : null;
1596
2206
  let privateKey;
1597
2207
  let address;
1598
2208
  let did;
1599
- if (profile?.authMethod === "local" && profile.privateKey && profile.address) {
2209
+ let sessionDid = profile?.sessionDid;
2210
+ if ((profile?.authMethod === "local" || posture === "local-owner-key") && profile.privateKey) {
1600
2211
  privateKey = profile.privateKey;
1601
- address = profile.address;
1602
- did = profile.did;
2212
+ address = profile.address ?? await deriveAddress(privateKey);
2213
+ did = profile.did.startsWith("did:pkh:") ? profile.did : addressToDID(address, profile.chainId ?? DEFAULT_CHAIN_ID);
1603
2214
  if (isInteractive()) {
1604
2215
  process.stderr.write(theme.muted("Using existing local key") + "\n");
1605
2216
  process.stderr.write(formatField("Address", address) + "\n");
@@ -1618,11 +2229,14 @@ async function handleLocalAuth(profileName, host) {
1618
2229
  }
1619
2230
  }
1620
2231
  const hasKey = await ProfileManager.getKey(profileName);
1621
- if (!hasKey) {
1622
- const { jwk } = await withSpinner("Generating session key...", async () => {
2232
+ if (options.forceSessionKey || !hasKey) {
2233
+ const { jwk, did: generatedSessionDid } = await withSpinner("Generating session key...", async () => {
1623
2234
  return generateKey();
1624
2235
  });
1625
2236
  await ProfileManager.setKey(profileName, jwk);
2237
+ sessionDid = generatedSessionDid;
2238
+ } else if (!sessionDid) {
2239
+ sessionDid = keyToDID(hasKey);
1626
2240
  }
1627
2241
  const sessionResult = await withSpinner("Signing in...", async () => {
1628
2242
  return localKeySignIn({ privateKey, host });
@@ -1631,31 +2245,57 @@ async function handleLocalAuth(profileName, host) {
1631
2245
  authMethod: "local",
1632
2246
  address,
1633
2247
  chainId: DEFAULT_CHAIN_ID,
1634
- spaceId: sessionResult.spaceId
2248
+ spaceId: sessionResult.spaceId,
2249
+ delegationHeader: sessionResult.delegationHeader,
2250
+ delegationCid: sessionResult.delegationCid,
2251
+ jwk: sessionResult.jwk,
2252
+ verificationMethod: sessionResult.verificationMethod,
2253
+ siwe: sessionResult.siwe,
2254
+ signature: sessionResult.signature
1635
2255
  });
1636
- await ProfileManager.setProfile(profileName, {
2256
+ sessionDid = sessionResult.verificationMethod;
2257
+ const updatedProfile = {
2258
+ ...profile,
1637
2259
  name: profileName,
1638
2260
  host,
1639
2261
  chainId: DEFAULT_CHAIN_ID,
1640
2262
  spaceName: "default",
1641
2263
  did,
1642
- primaryDid: did,
2264
+ sessionDid,
2265
+ ownerDid: did,
1643
2266
  spaceId: sessionResult.spaceId,
1644
2267
  createdAt: profile?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2268
+ posture: profile?.posture ?? "local-owner-key",
2269
+ operatorType: profile?.operatorType ?? "human",
1645
2270
  authMethod: "local",
1646
2271
  privateKey,
1647
2272
  address
1648
- });
2273
+ };
2274
+ await ProfileManager.setProfile(profileName, updatedProfile);
2275
+ if (options.emitOutput ?? true) {
2276
+ outputJson({
2277
+ authenticated: true,
2278
+ profile: profileName,
2279
+ did,
2280
+ sessionDid,
2281
+ address,
2282
+ spaceId: sessionResult.spaceId,
2283
+ authMethod: "local"
2284
+ });
2285
+ }
2286
+ return { profile: updatedProfile, sessionResult };
2287
+ }
2288
+ async function handleOpenKeyAuth(profileName, host, paste) {
2289
+ const { profile, delegationData } = await refreshOpenKeySession(profileName, host, { paste });
1649
2290
  outputJson({
1650
2291
  authenticated: true,
1651
2292
  profile: profileName,
1652
- did,
1653
- address,
1654
- spaceId: sessionResult.spaceId,
1655
- authMethod: "local"
2293
+ did: profile.did,
2294
+ spaceId: delegationData.spaceId,
2295
+ authMethod: "openkey"
1656
2296
  });
1657
2297
  }
1658
- async function handleOpenKeyAuth(profileName, host, paste) {
2298
+ async function refreshOpenKeySession(profileName, host, options = {}) {
1659
2299
  const key = await ProfileManager.getKey(profileName);
1660
2300
  if (!key) {
1661
2301
  throw new CLIError(
@@ -1666,7 +2306,7 @@ async function handleOpenKeyAuth(profileName, host, paste) {
1666
2306
  }
1667
2307
  const profile = await ProfileManager.getProfile(profileName);
1668
2308
  const delegationData = await startAuthFlow(profile.did, {
1669
- paste,
2309
+ paste: options.paste,
1670
2310
  jwk: key,
1671
2311
  host,
1672
2312
  openkeyHost: resolveOpenKeyHost(profile)
@@ -1674,26 +2314,23 @@ async function handleOpenKeyAuth(profileName, host, paste) {
1674
2314
  await ProfileManager.setSession(profileName, delegationData);
1675
2315
  const updatedProfile = {
1676
2316
  ...profile,
2317
+ sessionDid: profile.sessionDid ?? profile.did,
2318
+ posture: profile.posture ?? "owner-openkey",
2319
+ operatorType: profile.operatorType ?? "human",
1677
2320
  authMethod: "openkey"
1678
2321
  };
1679
2322
  if (delegationData.spaceId) {
1680
2323
  updatedProfile.spaceId = delegationData.spaceId;
1681
- updatedProfile.primaryDid = delegationData.primaryDid;
2324
+ updatedProfile.ownerDid = delegationData.ownerDid;
1682
2325
  }
1683
2326
  await ProfileManager.setProfile(profileName, updatedProfile);
1684
- outputJson({
1685
- authenticated: true,
1686
- profile: profileName,
1687
- did: profile.did,
1688
- spaceId: delegationData.spaceId,
1689
- authMethod: "openkey"
1690
- });
2327
+ return { profile: updatedProfile, delegationData };
1691
2328
  }
1692
2329
 
1693
2330
  // src/commands/kv.ts
1694
- import { readFile as readFile3 } from "fs/promises";
1695
- import { writeFile as writeFile2 } from "fs/promises";
1696
- async function readStdin() {
2331
+ import { readFile as readFile4 } from "fs/promises";
2332
+ import { writeFile as writeFile3 } from "fs/promises";
2333
+ async function readStdin2() {
1697
2334
  const chunks = [];
1698
2335
  for await (const chunk of process.stdin) {
1699
2336
  chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
@@ -1718,7 +2355,7 @@ function registerKvCommand(program2) {
1718
2355
  const metadata = result.data.headers ?? {};
1719
2356
  if (options.output) {
1720
2357
  const content = typeof data === "string" ? data : JSON.stringify(data);
1721
- await writeFile2(options.output, content);
2358
+ await writeFile3(options.output, content);
1722
2359
  outputJson({ key, written: options.output });
1723
2360
  return;
1724
2361
  }
@@ -1755,9 +2392,9 @@ function registerKvCommand(program2) {
1755
2392
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
1756
2393
  }
1757
2394
  if (options.file) {
1758
- putValue = await readFile3(options.file);
2395
+ putValue = await readFile4(options.file);
1759
2396
  } else if (options.stdin) {
1760
- putValue = await readStdin();
2397
+ putValue = await readStdin2();
1761
2398
  } else {
1762
2399
  try {
1763
2400
  putValue = JSON.parse(value);
@@ -1952,6 +2589,15 @@ function parseExpiry(input) {
1952
2589
  }
1953
2590
 
1954
2591
  // src/commands/delegation.ts
2592
+ import { principalDidEquals } from "@tinycloud/node-sdk";
2593
+ function didMatches(actual, expected) {
2594
+ if (!actual) return false;
2595
+ try {
2596
+ return principalDidEquals(actual, expected);
2597
+ } catch {
2598
+ return actual === expected;
2599
+ }
2600
+ }
1955
2601
  function registerDelegationCommand(program2) {
1956
2602
  const delegation = program2.command("delegation").description("Manage delegations");
1957
2603
  delegation.command("create").description("Create a delegation").requiredOption("--to <did>", "Recipient DID").requiredOption("--path <path>", "KV path scope").requiredOption("--actions <actions>", "Comma-separated actions (e.g., kv/get,kv/list)").option("--expiry <duration>", "Expiry duration (e.g., 1h, 7d, ISO date)", "1h").action(async (options, cmd) => {
@@ -1996,10 +2642,10 @@ function registerDelegationCommand(program2) {
1996
2642
  let delegations = result.data;
1997
2643
  if (options.granted) {
1998
2644
  const myDid = node.did;
1999
- delegations = delegations.filter((d) => d.delegatorDID === myDid);
2645
+ delegations = delegations.filter((d) => didMatches(d.delegatorDID, myDid));
2000
2646
  } else if (options.received) {
2001
2647
  const myDid = node.did;
2002
- delegations = delegations.filter((d) => d.delegateDID === myDid || d.delegateDID?.includes(myDid));
2648
+ delegations = delegations.filter((d) => didMatches(d.delegateDID, myDid));
2003
2649
  }
2004
2650
  outputJson({
2005
2651
  delegations: delegations.map((d) => ({
@@ -2225,10 +2871,19 @@ function registerProfileCommand(program2) {
2225
2871
  name: p.name,
2226
2872
  host: p.host,
2227
2873
  did: p.did,
2874
+ posture: resolveProfilePosture(p),
2875
+ operatorType: resolveProfileOperatorType(p),
2228
2876
  active: name === config.defaultProfile
2229
2877
  };
2230
2878
  } catch {
2231
- return { name, host: null, did: null, active: name === config.defaultProfile };
2879
+ return {
2880
+ name,
2881
+ host: null,
2882
+ did: null,
2883
+ posture: null,
2884
+ operatorType: null,
2885
+ active: name === config.defaultProfile
2886
+ };
2232
2887
  }
2233
2888
  })
2234
2889
  );
@@ -2242,7 +2897,8 @@ function registerProfileCommand(program2) {
2242
2897
  const marker = p.active ? theme.success("\u25CF ") : " ";
2243
2898
  const name = p.active ? theme.brand(p.name) : p.name;
2244
2899
  const host = theme.muted(p.host || "no host");
2245
- process.stdout.write(`${marker}${name} ${host}
2900
+ const posture = p.posture ? theme.muted(String(p.posture)) : theme.muted("no posture");
2901
+ process.stdout.write(`${marker}${name} ${host} ${posture}
2246
2902
  `);
2247
2903
  }
2248
2904
  }
@@ -2250,10 +2906,18 @@ function registerProfileCommand(program2) {
2250
2906
  handleError(error);
2251
2907
  }
2252
2908
  });
2253
- profile.command("create <name>").description("Create a new profile").option("--host <url>", "TinyCloud node URL").action(async (name, options, cmd) => {
2909
+ profile.command("create <name>").description("Create a new profile").option("--host <url>", "TinyCloud node URL").option(
2910
+ "--posture <posture>",
2911
+ `Profile posture: ${CLI_PROFILE_POSTURES.join(", ")}. Defaults to owner-openkey.`
2912
+ ).option(
2913
+ "--operator <type>",
2914
+ `Operator type: ${CLI_OPERATOR_TYPES.join(", ")}. Defaults to human.`
2915
+ ).action(async (name, options, cmd) => {
2254
2916
  try {
2255
2917
  const globalOpts = cmd.optsWithGlobals();
2256
2918
  const host = options.host ?? globalOpts.host ?? "https://node.tinycloud.xyz";
2919
+ const posture = parseProfilePosture(options.posture);
2920
+ const operatorType = parseOperatorType(options.operator);
2257
2921
  if (await ProfileManager.profileExists(name)) {
2258
2922
  throw new CLIError("PROFILE_EXISTS", `Profile "${name}" already exists`, ExitCode.ERROR);
2259
2923
  }
@@ -2266,9 +2930,12 @@ function registerProfileCommand(program2) {
2266
2930
  chainId: 1,
2267
2931
  spaceName: "default",
2268
2932
  did,
2269
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
2933
+ sessionDid: did,
2934
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2935
+ posture,
2936
+ operatorType
2270
2937
  });
2271
- outputJson({ profile: name, did, host, created: true });
2938
+ outputJson({ profile: name, did, host, posture, operatorType, created: true });
2272
2939
  } catch (error) {
2273
2940
  handleError(error);
2274
2941
  }
@@ -2283,9 +2950,13 @@ function registerProfileCommand(program2) {
2283
2950
  const hasSession = await ProfileManager.getSession(profileName) !== null;
2284
2951
  const config = await ProfileManager.getConfig();
2285
2952
  const isDefault = profileName === config.defaultProfile;
2953
+ const posture = resolveProfilePosture(p);
2954
+ const operatorType = resolveProfileOperatorType(p);
2286
2955
  if (shouldOutputJson()) {
2287
2956
  outputJson({
2288
2957
  ...p,
2958
+ posture,
2959
+ operatorType,
2289
2960
  hasKey,
2290
2961
  hasSession,
2291
2962
  isDefault
@@ -2295,6 +2966,9 @@ function registerProfileCommand(program2) {
2295
2966
  `);
2296
2967
  process.stdout.write(formatField("Host", p.host) + "\n");
2297
2968
  process.stdout.write(formatField("DID", p.did) + "\n");
2969
+ process.stdout.write(formatField("Session DID", p.sessionDid ?? null) + "\n");
2970
+ process.stdout.write(formatField("Posture", posture) + "\n");
2971
+ process.stdout.write(formatField("Operator", operatorType) + "\n");
2298
2972
  process.stdout.write(formatField("Space", p.spaceId || null) + "\n");
2299
2973
  process.stdout.write(formatField("Key", hasKey) + "\n");
2300
2974
  process.stdout.write(formatField("Session", hasSession) + "\n");
@@ -2336,6 +3010,24 @@ function registerProfileCommand(program2) {
2336
3010
  }
2337
3011
  });
2338
3012
  }
3013
+ function parseProfilePosture(raw) {
3014
+ if (raw === void 0 || raw === null || raw === "") return "owner-openkey";
3015
+ if (isCLIProfilePosture(raw)) return raw;
3016
+ throw new CLIError(
3017
+ "INVALID_POSTURE",
3018
+ `Invalid posture "${String(raw)}". Use one of: ${CLI_PROFILE_POSTURES.join(", ")}.`,
3019
+ ExitCode.USAGE_ERROR
3020
+ );
3021
+ }
3022
+ function parseOperatorType(raw) {
3023
+ if (raw === void 0 || raw === null || raw === "") return "human";
3024
+ if (isCLIOperatorType(raw)) return raw;
3025
+ throw new CLIError(
3026
+ "INVALID_OPERATOR",
3027
+ `Invalid operator "${String(raw)}". Use one of: ${CLI_OPERATOR_TYPES.join(", ")}.`,
3028
+ ExitCode.USAGE_ERROR
3029
+ );
3030
+ }
2339
3031
 
2340
3032
  // src/commands/completion.ts
2341
3033
  function registerCompletionCommand(program2) {
@@ -2364,7 +3056,7 @@ _tc_completions() {
2364
3056
  commands="init auth kv space delegation share node profile completion"
2365
3057
 
2366
3058
  case "\${COMP_WORDS[1]}" in
2367
- auth) subcommands="login logout status whoami" ;;
3059
+ auth) subcommands="login logout rotate status whoami" ;;
2368
3060
  kv) subcommands="get put delete list head" ;;
2369
3061
  space) subcommands="list create info switch" ;;
2370
3062
  delegation) subcommands="create list info revoke" ;;
@@ -2414,7 +3106,7 @@ _tc() {
2414
3106
  ;;
2415
3107
  args)
2416
3108
  case $words[1] in
2417
- auth) _values 'subcommand' login logout status whoami ;;
3109
+ auth) _values 'subcommand' login logout rotate status whoami ;;
2418
3110
  kv) _values 'subcommand' get put delete list head ;;
2419
3111
  space) _values 'subcommand' list create info switch ;;
2420
3112
  delegation) _values 'subcommand' create list info revoke ;;
@@ -2449,7 +3141,7 @@ complete -c tc -n "not __fish_seen_subcommand_from $commands" -a profile -d "Pro
2449
3141
  complete -c tc -n "not __fish_seen_subcommand_from $commands" -a completion -d "Generate shell completions"
2450
3142
 
2451
3143
  # Subcommands
2452
- complete -c tc -n "__fish_seen_subcommand_from auth" -a "login logout status whoami"
3144
+ complete -c tc -n "__fish_seen_subcommand_from auth" -a "login logout rotate status whoami"
2453
3145
  complete -c tc -n "__fish_seen_subcommand_from kv" -a "get put delete list head"
2454
3146
  complete -c tc -n "__fish_seen_subcommand_from space" -a "list create info switch"
2455
3147
  complete -c tc -n "__fish_seen_subcommand_from delegation" -a "create list info revoke"
@@ -2468,10 +3160,10 @@ complete -c tc -l quiet -s q -d "Suppress non-essential output"
2468
3160
  }
2469
3161
 
2470
3162
  // src/commands/vault.ts
2471
- import { readFile as readFile4 } from "fs/promises";
2472
- import { writeFile as writeFile3 } from "fs/promises";
3163
+ import { readFile as readFile5 } from "fs/promises";
3164
+ import { writeFile as writeFile4 } from "fs/promises";
2473
3165
  import { PrivateKeySigner as PrivateKeySigner2 } from "@tinycloud/node-sdk";
2474
- async function readStdin2() {
3166
+ async function readStdin3() {
2475
3167
  const chunks = [];
2476
3168
  for await (const chunk of process.stdin) {
2477
3169
  chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
@@ -2526,9 +3218,9 @@ function registerVaultCommand(program2) {
2526
3218
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
2527
3219
  }
2528
3220
  if (options.file) {
2529
- putValue = new Uint8Array(await readFile4(options.file));
3221
+ putValue = new Uint8Array(await readFile5(options.file));
2530
3222
  } else if (options.stdin) {
2531
- putValue = new Uint8Array(await readStdin2());
3223
+ putValue = new Uint8Array(await readStdin3());
2532
3224
  } else {
2533
3225
  putValue = value;
2534
3226
  }
@@ -2558,7 +3250,7 @@ function registerVaultCommand(program2) {
2558
3250
  const data = result.data.data ?? result.data;
2559
3251
  if (options.output) {
2560
3252
  const content = data instanceof Uint8Array ? Buffer.from(data) : typeof data === "string" ? data : JSON.stringify(data);
2561
- await writeFile3(options.output, content);
3253
+ await writeFile4(options.output, content);
2562
3254
  outputJson({ key, written: options.output });
2563
3255
  return;
2564
3256
  }
@@ -2641,9 +3333,20 @@ function registerVaultCommand(program2) {
2641
3333
  }
2642
3334
 
2643
3335
  // src/commands/secrets.ts
2644
- import { readFile as readFile5 } from "fs/promises";
2645
- import { writeFile as writeFile4 } from "fs/promises";
2646
- async function readStdin3() {
3336
+ import { readFile as readFile6 } from "fs/promises";
3337
+ import { writeFile as writeFile5 } from "fs/promises";
3338
+ import {
3339
+ resolveSecretListPrefix,
3340
+ resolveSecretPath
3341
+ } from "@tinycloud/node-sdk";
3342
+ var SECRETS_SPACE = "secrets";
3343
+ var SECRET_KV_ABILITIES = {
3344
+ get: "tinycloud.kv/get",
3345
+ put: "tinycloud.kv/put",
3346
+ del: "tinycloud.kv/del",
3347
+ list: "tinycloud.kv/list"
3348
+ };
3349
+ async function readStdin4() {
2647
3350
  const chunks = [];
2648
3351
  for await (const chunk of process.stdin) {
2649
3352
  chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
@@ -2658,6 +3361,123 @@ function resolveSecretScope(options) {
2658
3361
  const scope = options.scope ?? options.space;
2659
3362
  return scope ? { scope } : void 0;
2660
3363
  }
3364
+ async function ensureSecretsNode(ctx, options) {
3365
+ const auth = authOptions(options);
3366
+ if (auth?.privateKey) {
3367
+ return ensureAuthenticated(ctx, auth);
3368
+ }
3369
+ const profile = await ProfileManager.getProfile(ctx.profile).catch(() => null);
3370
+ if (profile?.authMethod === "openkey" && canRequestOwnerPermissions(profile)) {
3371
+ const session = await ProfileManager.getSession(ctx.profile);
3372
+ if (!session || isStoredSessionExpired(session)) {
3373
+ await withSpinner(
3374
+ session ? "Refreshing TinyCloud session..." : "Creating TinyCloud session...",
3375
+ () => refreshOpenKeySession(ctx.profile, ctx.host)
3376
+ );
3377
+ }
3378
+ }
3379
+ return ensureAuthenticated(ctx, auth);
3380
+ }
3381
+ async function runSecretOperation(params) {
3382
+ const first = await runSecretOperationAttempt(params.label, params.operation);
3383
+ if (first.ok || !shouldRequestSecretPermissions(first.error)) {
3384
+ return first;
3385
+ }
3386
+ const profile = await ProfileManager.getProfile(params.ctx.profile);
3387
+ if (!canRequestOwnerPermissions(profile)) {
3388
+ return first;
3389
+ }
3390
+ const requested = secretPermissionEntries({
3391
+ action: params.action,
3392
+ name: params.name,
3393
+ options: params.scopeOptions,
3394
+ node: params.node
3395
+ });
3396
+ await withSpinner(
3397
+ "Requesting secret permissions...",
3398
+ () => ensureDelegationAuthority({
3399
+ ctx: params.ctx,
3400
+ profile,
3401
+ node: params.node,
3402
+ requested,
3403
+ expiryOption: void 0,
3404
+ yes: true,
3405
+ force: true
3406
+ })
3407
+ );
3408
+ return runSecretOperationAttempt(params.label, params.operation);
3409
+ }
3410
+ async function runSecretOperationAttempt(label, operation) {
3411
+ try {
3412
+ return await withSpinner(label, operation);
3413
+ } catch (error) {
3414
+ const permissionError = thrownPermissionError(error);
3415
+ if (permissionError) return permissionError;
3416
+ throw error;
3417
+ }
3418
+ }
3419
+ function canRequestOwnerPermissions(profile) {
3420
+ const posture = resolveProfilePosture(profile);
3421
+ return posture === "owner-openkey" || posture === "local-owner-key";
3422
+ }
3423
+ function shouldRequestSecretPermissions(error) {
3424
+ if (error.code !== "PERMISSION_DENIED") return false;
3425
+ return /permission|session expired|autosign|capabilit/i.test(error.message);
3426
+ }
3427
+ function thrownPermissionError(error) {
3428
+ const record = error;
3429
+ const message = typeof record?.message === "string" ? record.message : String(error);
3430
+ const code = typeof record?.code === "string" ? record.code : "PERMISSION_DENIED";
3431
+ if (code !== "PERMISSION_DENIED" && !/permission|session expired|autosign|capabilit/i.test(message)) {
3432
+ return null;
3433
+ }
3434
+ return {
3435
+ ok: false,
3436
+ error: {
3437
+ code: "PERMISSION_DENIED",
3438
+ message
3439
+ }
3440
+ };
3441
+ }
3442
+ function isStoredSessionExpired(session) {
3443
+ const record = session;
3444
+ const direct = parseDate(record.expiresAt ?? record.expiry ?? record.expirationTime);
3445
+ if (direct) return direct.getTime() <= Date.now();
3446
+ if (typeof record.siwe !== "string") return false;
3447
+ const match = record.siwe.match(/^Expiration Time:\s*(.+)$/im);
3448
+ const expiry = match ? parseDate(match[1].trim()) : null;
3449
+ return expiry !== null && expiry.getTime() <= Date.now();
3450
+ }
3451
+ function parseDate(value) {
3452
+ if (value instanceof Date) {
3453
+ return Number.isNaN(value.getTime()) ? null : value;
3454
+ }
3455
+ if (typeof value !== "string" || value.trim() === "") return null;
3456
+ const date = new Date(value);
3457
+ return Number.isNaN(date.getTime()) ? null : date;
3458
+ }
3459
+ function secretKvAbility(action) {
3460
+ return SECRET_KV_ABILITIES[action];
3461
+ }
3462
+ function secretPermissionEntries(params) {
3463
+ const path = params.action === "list" ? resolveSecretListPrefix(params.options) : resolveSecretPath(params.name ?? "", params.options).permissionPaths.vault;
3464
+ const permissions = [{
3465
+ service: "tinycloud.kv",
3466
+ space: SECRETS_SPACE,
3467
+ path,
3468
+ actions: [secretKvAbility(params.action)],
3469
+ skipPrefix: true
3470
+ }];
3471
+ if (params.action === "get") {
3472
+ permissions.push({
3473
+ service: "tinycloud.encryption",
3474
+ path: params.node.getDefaultEncryptionNetworkId(),
3475
+ actions: ["tinycloud.encryption/decrypt"],
3476
+ skipPrefix: true
3477
+ });
3478
+ }
3479
+ return permissions;
3480
+ }
2661
3481
  function registerSecretsCommand(program2) {
2662
3482
  const secrets = program2.command("secrets").description("Encrypted secrets management");
2663
3483
  const network = secrets.command("network").description("Manage the default secrets encryption network");
@@ -2703,12 +3523,16 @@ function registerSecretsCommand(program2) {
2703
3523
  try {
2704
3524
  const globalOpts = cmd.optsWithGlobals();
2705
3525
  const ctx = await ProfileManager.resolveContext(globalOpts);
2706
- const node = await ensureAuthenticated(ctx, authOptions(options));
3526
+ const node = await ensureSecretsNode(ctx, options);
2707
3527
  const scopeOptions = resolveSecretScope(options);
2708
- const result = await withSpinner(
2709
- "Listing secrets...",
2710
- () => node.secrets.list(scopeOptions)
2711
- );
3528
+ const result = await runSecretOperation({
3529
+ ctx,
3530
+ node,
3531
+ action: "list",
3532
+ scopeOptions,
3533
+ label: "Listing secrets...",
3534
+ operation: () => node.secrets.list(scopeOptions)
3535
+ });
2712
3536
  if (!result.ok) {
2713
3537
  throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2714
3538
  }
@@ -2727,12 +3551,17 @@ function registerSecretsCommand(program2) {
2727
3551
  try {
2728
3552
  const globalOpts = cmd.optsWithGlobals();
2729
3553
  const ctx = await ProfileManager.resolveContext(globalOpts);
2730
- const node = await ensureAuthenticated(ctx, authOptions(options));
3554
+ const node = await ensureSecretsNode(ctx, options);
2731
3555
  const scopeOptions = resolveSecretScope(options);
2732
- const result = await withSpinner(
2733
- `Getting secret ${name}...`,
2734
- () => node.secrets.get(name, scopeOptions)
2735
- );
3556
+ const result = await runSecretOperation({
3557
+ ctx,
3558
+ node,
3559
+ action: "get",
3560
+ name,
3561
+ scopeOptions,
3562
+ label: `Getting secret ${name}...`,
3563
+ operation: () => node.secrets.get(name, scopeOptions)
3564
+ });
2736
3565
  if (!result.ok) {
2737
3566
  if (result.error.code === "NOT_FOUND" || result.error.code === "KEY_NOT_FOUND") {
2738
3567
  throw new CLIError("NOT_FOUND", `Secret "${name}" not found`, ExitCode.NOT_FOUND);
@@ -2741,7 +3570,7 @@ function registerSecretsCommand(program2) {
2741
3570
  }
2742
3571
  const value = String(result.data);
2743
3572
  if (options.output) {
2744
- await writeFile4(options.output, value);
3573
+ await writeFile5(options.output, value);
2745
3574
  outputJson({ name, written: options.output });
2746
3575
  return;
2747
3576
  }
@@ -2758,7 +3587,7 @@ function registerSecretsCommand(program2) {
2758
3587
  try {
2759
3588
  const globalOpts = cmd.optsWithGlobals();
2760
3589
  const ctx = await ProfileManager.resolveContext(globalOpts);
2761
- const node = await ensureAuthenticated(ctx, authOptions(options));
3590
+ const node = await ensureSecretsNode(ctx, options);
2762
3591
  let secretValue;
2763
3592
  const sources = [value !== void 0, !!options.file, !!options.stdin].filter(Boolean);
2764
3593
  if (sources.length === 0) {
@@ -2768,17 +3597,22 @@ function registerSecretsCommand(program2) {
2768
3597
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
2769
3598
  }
2770
3599
  if (options.file) {
2771
- secretValue = await readFile5(options.file, "utf-8");
3600
+ secretValue = await readFile6(options.file, "utf-8");
2772
3601
  } else if (options.stdin) {
2773
- secretValue = (await readStdin3()).toString("utf-8");
3602
+ secretValue = (await readStdin4()).toString("utf-8");
2774
3603
  } else {
2775
3604
  secretValue = value;
2776
3605
  }
2777
3606
  const scopeOptions = resolveSecretScope(options);
2778
- const result = await withSpinner(
2779
- `Storing secret ${name}...`,
2780
- () => node.secrets.put(name, secretValue, scopeOptions)
2781
- );
3607
+ const result = await runSecretOperation({
3608
+ ctx,
3609
+ node,
3610
+ action: "put",
3611
+ name,
3612
+ scopeOptions,
3613
+ label: `Storing secret ${name}...`,
3614
+ operation: () => node.secrets.put(name, secretValue, scopeOptions)
3615
+ });
2782
3616
  if (!result.ok) {
2783
3617
  throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2784
3618
  }
@@ -2791,12 +3625,17 @@ function registerSecretsCommand(program2) {
2791
3625
  try {
2792
3626
  const globalOpts = cmd.optsWithGlobals();
2793
3627
  const ctx = await ProfileManager.resolveContext(globalOpts);
2794
- const node = await ensureAuthenticated(ctx, authOptions(options));
3628
+ const node = await ensureSecretsNode(ctx, options);
2795
3629
  const scopeOptions = resolveSecretScope(options);
2796
- const result = await withSpinner(
2797
- `Deleting secret ${name}...`,
2798
- () => node.secrets.delete(name, scopeOptions)
2799
- );
3630
+ const result = await runSecretOperation({
3631
+ ctx,
3632
+ node,
3633
+ action: "del",
3634
+ name,
3635
+ scopeOptions,
3636
+ label: `Deleting secret ${name}...`,
3637
+ operation: () => node.secrets.delete(name, scopeOptions)
3638
+ });
2800
3639
  if (!result.ok) {
2801
3640
  throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2802
3641
  }
@@ -2848,10 +3687,10 @@ function registerSecretsCommand(program2) {
2848
3687
  }
2849
3688
 
2850
3689
  // src/commands/vars.ts
2851
- import { readFile as readFile6 } from "fs/promises";
2852
- import { writeFile as writeFile5 } from "fs/promises";
3690
+ import { readFile as readFile7 } from "fs/promises";
3691
+ import { writeFile as writeFile6 } from "fs/promises";
2853
3692
  var VARIABLES_PREFIX = "variables/";
2854
- async function readStdin4() {
3693
+ async function readStdin5() {
2855
3694
  const chunks = [];
2856
3695
  for await (const chunk of process.stdin) {
2857
3696
  chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
@@ -2921,7 +3760,7 @@ function registerVarsCommand(program2) {
2921
3760
  value = typeof data === "string" ? data : JSON.stringify(data);
2922
3761
  }
2923
3762
  if (options.output) {
2924
- await writeFile5(options.output, value);
3763
+ await writeFile6(options.output, value);
2925
3764
  outputJson({ name, written: options.output });
2926
3765
  return;
2927
3766
  }
@@ -2949,9 +3788,9 @@ function registerVarsCommand(program2) {
2949
3788
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
2950
3789
  }
2951
3790
  if (options.file) {
2952
- varValue = await readFile6(options.file, "utf-8");
3791
+ varValue = await readFile7(options.file, "utf-8");
2953
3792
  } else if (options.stdin) {
2954
- varValue = (await readStdin4()).toString("utf-8");
3793
+ varValue = (await readStdin5()).toString("utf-8");
2955
3794
  } else {
2956
3795
  varValue = value;
2957
3796
  }
@@ -3095,7 +3934,7 @@ function registerDoctorCommand(program2) {
3095
3934
  }
3096
3935
 
3097
3936
  // src/commands/sql.ts
3098
- import { writeFile as writeFile6 } from "fs/promises";
3937
+ import { writeFile as writeFile7 } from "fs/promises";
3099
3938
  import { resolve } from "path";
3100
3939
  async function dbHandle(node, dbName, spaceInput, profileName) {
3101
3940
  const spaceUri = await resolveSpaceUri(spaceInput, profileName);
@@ -3231,7 +4070,7 @@ Output:
3231
4070
  const blob = result.data;
3232
4071
  const buffer = Buffer.from(await blob.arrayBuffer());
3233
4072
  const outputPath = resolve(options.output);
3234
- await writeFile6(outputPath, buffer);
4073
+ await writeFile7(outputPath, buffer);
3235
4074
  outputJson({
3236
4075
  file: outputPath,
3237
4076
  size: blob.size,
@@ -3360,7 +4199,7 @@ function quoteIdent(name) {
3360
4199
  }
3361
4200
 
3362
4201
  // src/commands/duckdb.ts
3363
- import { readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
4202
+ import { readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
3364
4203
  import { resolve as resolve2 } from "path";
3365
4204
  function registerDuckdbCommand(program2) {
3366
4205
  const duckdb = program2.command("duckdb").description("DuckDB database operations");
@@ -3474,7 +4313,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
3474
4313
  const blob = result.data;
3475
4314
  const buffer = Buffer.from(await blob.arrayBuffer());
3476
4315
  const outputPath = resolve2(options.output);
3477
- await writeFile7(outputPath, buffer);
4316
+ await writeFile8(outputPath, buffer);
3478
4317
  outputJson({
3479
4318
  file: outputPath,
3480
4319
  size: blob.size,
@@ -3490,7 +4329,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
3490
4329
  const ctx = await ProfileManager.resolveContext(globalOpts);
3491
4330
  const node = await ensureAuthenticated(ctx);
3492
4331
  const filePath = resolve2(file);
3493
- const bytes = new Uint8Array(await readFile7(filePath));
4332
+ const bytes = new Uint8Array(await readFile8(filePath));
3494
4333
  const result = await withSpinner(
3495
4334
  "Importing database...",
3496
4335
  () => node.duckdb.db(options.db).import(bytes)
@@ -3511,7 +4350,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
3511
4350
  }
3512
4351
 
3513
4352
  // src/commands/manifest.ts
3514
- import { readFile as readFile8 } from "fs/promises";
4353
+ import { readFile as readFile9 } from "fs/promises";
3515
4354
  var DEFAULT_APP_SPACE = "applications";
3516
4355
  function registerManifestCommand(program2) {
3517
4356
  const manifest = program2.command("manifest").description("Inspect TinyCloud app manifests");
@@ -3619,7 +4458,7 @@ async function loadManifestSource(source) {
3619
4458
  }
3620
4459
  return response.text();
3621
4460
  }
3622
- return readFile8(source, "utf8");
4461
+ return readFile9(source, "utf8");
3623
4462
  }
3624
4463
  function prefixWithAppId(path, appId) {
3625
4464
  const slash = path.indexOf("/");
@@ -3707,6 +4546,358 @@ function registerUpgradeCommand(program2) {
3707
4546
  });
3708
4547
  }
3709
4548
 
4549
+ // src/commands/status.ts
4550
+ import {
4551
+ NodeWasmBindings
4552
+ } from "@tinycloud/node-sdk";
4553
+ var wasmBindings = null;
4554
+ function registerStatusCommand(program2) {
4555
+ program2.command("status").description("Show local TinyCloud profile, session, delegation, and permission state").action(async (_options, cmd) => {
4556
+ try {
4557
+ const globalOpts = cmd.optsWithGlobals();
4558
+ const ctx = await ProfileManager.resolveContext(globalOpts);
4559
+ const config = await ProfileManager.getConfig();
4560
+ const names = (await ProfileManager.listProfiles()).sort(
4561
+ (a, b) => a.localeCompare(b)
4562
+ );
4563
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
4564
+ const profiles = await Promise.all(
4565
+ names.map(
4566
+ (name) => inspectProfile({
4567
+ name,
4568
+ activeProfile: ctx.profile,
4569
+ defaultProfile: config.defaultProfile
4570
+ })
4571
+ )
4572
+ );
4573
+ const summary = {
4574
+ generatedAt,
4575
+ activeProfile: ctx.profile,
4576
+ defaultProfile: config.defaultProfile,
4577
+ profileCount: profiles.length,
4578
+ authenticatedProfileCount: profiles.filter((p) => p.authenticated).length,
4579
+ activeDelegationCount: profiles.reduce(
4580
+ (sum, profile) => sum + profile.activeDelegationCount,
4581
+ 0
4582
+ ),
4583
+ profiles
4584
+ };
4585
+ if (shouldOutputJson()) {
4586
+ outputJson(summary);
4587
+ return;
4588
+ }
4589
+ process.stdout.write(formatStatus(summary));
4590
+ } catch (error) {
4591
+ handleError(error);
4592
+ }
4593
+ });
4594
+ }
4595
+ async function inspectProfile(params) {
4596
+ const issues = [];
4597
+ const profile = await readProfile(params.name, issues);
4598
+ const session = await readSession(params.name, issues);
4599
+ const hasKey = await readHasKey(params.name, issues);
4600
+ const storedDelegations = await readDelegations(params.name, issues);
4601
+ const sessionPermissions = session ? sessionPermissionsFromRecap(session) : [];
4602
+ const sessionExpiry = session ? extractSessionExpiry(session) : null;
4603
+ const sessionExpired = sessionExpiry === null ? null : sessionExpiry.getTime() <= Date.now();
4604
+ const statusSession = {
4605
+ present: session !== null,
4606
+ expired: session === null ? null : sessionExpired,
4607
+ expiresAt: sessionExpiry?.toISOString() ?? null,
4608
+ permissions: sessionPermissions,
4609
+ permissionsCompact: compactPermissions(sessionPermissions)
4610
+ };
4611
+ const delegations = storedDelegations.map(inspectDelegation);
4612
+ const activeDelegationPermissions = delegations.filter((delegation) => delegation.active).flatMap((delegation) => delegation.permissions);
4613
+ const permissions = uniquePermissions([
4614
+ ...sessionPermissions,
4615
+ ...activeDelegationPermissions
4616
+ ]);
4617
+ const hasPrivateKey = typeof profile?.privateKey === "string" && profile.privateKey.length > 0;
4618
+ const localKeyAuthenticated = profile?.authMethod === "local" && hasPrivateKey;
4619
+ const sessionAuthenticated = session !== null && sessionExpired !== true;
4620
+ const authenticated = localKeyAuthenticated || sessionAuthenticated;
4621
+ const status = resolveStatus({
4622
+ exists: profile !== null,
4623
+ authenticated,
4624
+ localKeyAuthenticated,
4625
+ sessionExpired
4626
+ });
4627
+ return {
4628
+ name: params.name,
4629
+ active: params.name === params.activeProfile,
4630
+ default: params.name === params.defaultProfile,
4631
+ exists: profile !== null,
4632
+ status,
4633
+ host: profile?.host ?? null,
4634
+ did: profile?.did ?? null,
4635
+ sessionDid: profile?.sessionDid ?? null,
4636
+ ownerDid: profile?.ownerDid ?? null,
4637
+ address: profile?.address ?? null,
4638
+ spaceId: profile?.spaceId ?? null,
4639
+ authMethod: profile?.authMethod ?? null,
4640
+ posture: profile ? resolveProfilePosture(profile) : null,
4641
+ operatorType: profile ? resolveProfileOperatorType(profile) : null,
4642
+ hasKey,
4643
+ hasPrivateKey,
4644
+ authenticated,
4645
+ session: statusSession,
4646
+ delegations,
4647
+ permissions,
4648
+ permissionsCompact: compactPermissions(permissions),
4649
+ permissionCount: permissions.length,
4650
+ activeDelegationCount: delegations.filter((delegation) => delegation.active).length,
4651
+ delegationCount: delegations.length,
4652
+ issues
4653
+ };
4654
+ }
4655
+ async function readProfile(name, issues) {
4656
+ try {
4657
+ return await ProfileManager.getProfile(name);
4658
+ } catch (error) {
4659
+ issues.push(`profile: ${messageFromError(error)}`);
4660
+ return null;
4661
+ }
4662
+ }
4663
+ async function readSession(name, issues) {
4664
+ try {
4665
+ return asRecord(await ProfileManager.getSession(name));
4666
+ } catch (error) {
4667
+ issues.push(`session: ${messageFromError(error)}`);
4668
+ return null;
4669
+ }
4670
+ }
4671
+ async function readHasKey(name, issues) {
4672
+ try {
4673
+ return await ProfileManager.getKey(name) !== null;
4674
+ } catch (error) {
4675
+ issues.push(`key: ${messageFromError(error)}`);
4676
+ return false;
4677
+ }
4678
+ }
4679
+ async function readDelegations(name, issues) {
4680
+ try {
4681
+ return await loadAdditionalDelegations(name);
4682
+ } catch (error) {
4683
+ issues.push(`delegations: ${messageFromError(error)}`);
4684
+ return [];
4685
+ }
4686
+ }
4687
+ function inspectDelegation(entry) {
4688
+ const expiry = parseDate2(entry.delegation.expiry);
4689
+ const expired = expiry === null ? null : expiry.getTime() <= Date.now();
4690
+ const permissions = normalizePermissions(
4691
+ Array.isArray(entry.permissions) && entry.permissions.length > 0 ? entry.permissions : permissionsFromDelegation(entry.delegation)
4692
+ );
4693
+ return {
4694
+ cid: entry.delegation.cid,
4695
+ active: expired !== true,
4696
+ expired,
4697
+ expiresAt: expiry?.toISOString() ?? null,
4698
+ permissions,
4699
+ permissionsCompact: compactPermissions(permissions)
4700
+ };
4701
+ }
4702
+ function resolveStatus(params) {
4703
+ if (!params.exists) return "missing";
4704
+ if (params.localKeyAuthenticated) return "local-key";
4705
+ if (params.authenticated) return "logged-in";
4706
+ if (params.sessionExpired === true) return "expired";
4707
+ return "signed-out";
4708
+ }
4709
+ function sessionPermissionsFromRecap(session) {
4710
+ if (typeof session.siwe !== "string" || session.siwe.length === 0) return [];
4711
+ try {
4712
+ const rawEntries = getWasmBindings().parseRecapFromSiwe(session.siwe);
4713
+ if (!Array.isArray(rawEntries)) return [];
4714
+ return normalizePermissions(rawEntries.map(permissionFromRawRecap));
4715
+ } catch {
4716
+ return [];
4717
+ }
4718
+ }
4719
+ function permissionFromRawRecap(value) {
4720
+ const record = asRecord(value);
4721
+ if (!record) return null;
4722
+ const service = stringValue(record.service);
4723
+ const space = stringValue(record.space);
4724
+ const path = stringValue(record.path);
4725
+ const actions = Array.isArray(record.actions) ? record.actions.map(String).filter(Boolean) : [];
4726
+ if (!service || !space || path === null || actions.length === 0) return null;
4727
+ return {
4728
+ service: normalizeService2(service),
4729
+ space,
4730
+ path,
4731
+ actions
4732
+ };
4733
+ }
4734
+ function normalizePermissions(entries) {
4735
+ const permissions = [];
4736
+ for (const entry of entries) {
4737
+ const permission = permissionFromUnknown(entry);
4738
+ if (permission) permissions.push(permission);
4739
+ }
4740
+ return uniquePermissions(permissions);
4741
+ }
4742
+ function permissionFromUnknown(value) {
4743
+ const record = asRecord(value);
4744
+ if (!record) return null;
4745
+ const service = stringValue(record.service);
4746
+ const space = stringValue(record.space);
4747
+ const path = stringValue(record.path);
4748
+ const actions = Array.isArray(record.actions) ? record.actions.map(String).filter(Boolean) : [];
4749
+ if (!service || !space || path === null || actions.length === 0) return null;
4750
+ return {
4751
+ service: normalizeService2(service),
4752
+ space,
4753
+ path,
4754
+ actions
4755
+ };
4756
+ }
4757
+ function uniquePermissions(entries) {
4758
+ const seen = /* @__PURE__ */ new Set();
4759
+ const unique2 = [];
4760
+ for (const entry of entries) {
4761
+ const key = compactPermission(entry);
4762
+ if (seen.has(key)) continue;
4763
+ seen.add(key);
4764
+ unique2.push(entry);
4765
+ }
4766
+ return unique2;
4767
+ }
4768
+ function compactPermissions(entries) {
4769
+ return entries.map(compactPermission);
4770
+ }
4771
+ function extractSessionExpiry(session) {
4772
+ for (const key of ["expiresAt", "expiry", "expirationTime"]) {
4773
+ const parsed = parseDate2(session[key]);
4774
+ if (parsed) return parsed;
4775
+ }
4776
+ if (typeof session.siwe !== "string") return null;
4777
+ const match = session.siwe.match(/^Expiration Time:\s*(.+)$/im);
4778
+ return match ? parseDate2(match[1].trim()) : null;
4779
+ }
4780
+ function parseDate2(value) {
4781
+ if (value instanceof Date) {
4782
+ return Number.isNaN(value.getTime()) ? null : value;
4783
+ }
4784
+ if (typeof value === "number" && Number.isFinite(value)) {
4785
+ const millis = value > 0 && value < 1e12 ? value * 1e3 : value;
4786
+ const date2 = new Date(millis);
4787
+ return Number.isNaN(date2.getTime()) ? null : date2;
4788
+ }
4789
+ if (typeof value !== "string" || value.trim() === "") return null;
4790
+ const date = new Date(value);
4791
+ return Number.isNaN(date.getTime()) ? null : date;
4792
+ }
4793
+ function getWasmBindings() {
4794
+ wasmBindings ??= new NodeWasmBindings();
4795
+ return wasmBindings;
4796
+ }
4797
+ function normalizeService2(service) {
4798
+ return service.startsWith("tinycloud.") ? service : `tinycloud.${service}`;
4799
+ }
4800
+ function asRecord(value) {
4801
+ return value !== null && typeof value === "object" ? value : null;
4802
+ }
4803
+ function stringValue(value) {
4804
+ return typeof value === "string" ? value : null;
4805
+ }
4806
+ function messageFromError(error) {
4807
+ return error instanceof Error ? error.message : String(error);
4808
+ }
4809
+ function formatStatus(summary) {
4810
+ const lines = [];
4811
+ lines.push(theme.heading("TinyCloud Status"));
4812
+ lines.push(`Active profile: ${theme.value(summary.activeProfile)}`);
4813
+ lines.push(`Default profile: ${theme.value(summary.defaultProfile)}`);
4814
+ lines.push("");
4815
+ if (summary.profiles.length === 0) {
4816
+ lines.push(theme.muted("No profiles configured. Run: tc init"));
4817
+ return `${lines.join("\n")}
4818
+ `;
4819
+ }
4820
+ lines.push(theme.label("Profiles"));
4821
+ for (const profile of summary.profiles) {
4822
+ lines.push(formatProfile(profile));
4823
+ }
4824
+ return `${lines.join("\n")}
4825
+ `;
4826
+ }
4827
+ function formatProfile(profile) {
4828
+ const marker = profile.active ? theme.success("*") : " ";
4829
+ const name = profile.default ? `${profile.name} (default)` : profile.name;
4830
+ const host = profile.host ? theme.muted(profile.host) : theme.muted("no host");
4831
+ const summary = [
4832
+ `${marker} ${profile.active ? theme.brand(name) : name}`,
4833
+ formatProfileStatus(profile.status),
4834
+ profile.posture ?? "no posture",
4835
+ plural(profile.permissionCount, "permission"),
4836
+ `${profile.activeDelegationCount}/${profile.delegationCount} delegations`,
4837
+ host
4838
+ ].join(" ");
4839
+ const lines = [summary];
4840
+ lines.push(` session: ${formatSession(profile.session)}`);
4841
+ if (profile.permissionsCompact.length > 0) {
4842
+ lines.push(" permissions:");
4843
+ for (const permission of profile.permissionsCompact) {
4844
+ lines.push(` ${permission}`);
4845
+ }
4846
+ }
4847
+ if (profile.delegations.length > 0) {
4848
+ lines.push(" delegations:");
4849
+ for (const delegation of profile.delegations) {
4850
+ lines.push(` ${formatDelegation(delegation)}`);
4851
+ }
4852
+ }
4853
+ if (profile.issues.length > 0) {
4854
+ lines.push(" issues:");
4855
+ for (const issue of profile.issues) {
4856
+ lines.push(` ${theme.warn(issue)}`);
4857
+ }
4858
+ }
4859
+ return lines.join("\n");
4860
+ }
4861
+ function formatProfileStatus(status) {
4862
+ switch (status) {
4863
+ case "logged-in":
4864
+ return theme.success("logged in");
4865
+ case "local-key":
4866
+ return theme.success("local key");
4867
+ case "expired":
4868
+ return theme.warn("expired");
4869
+ case "missing":
4870
+ return theme.warn("missing");
4871
+ case "signed-out":
4872
+ return theme.muted("signed out");
4873
+ }
4874
+ }
4875
+ function formatSession(session) {
4876
+ if (!session.present) return theme.muted("none");
4877
+ if (session.expired === true) {
4878
+ return `${theme.warn("expired")}${formatExpiresAt(session.expiresAt)}`;
4879
+ }
4880
+ if (session.expired === false) {
4881
+ return `${theme.success("active")}${formatExpiresAt(session.expiresAt)}`;
4882
+ }
4883
+ return `${theme.success("present")}${formatExpiresAt(session.expiresAt)}`;
4884
+ }
4885
+ function formatDelegation(delegation) {
4886
+ const state = delegation.expired === true ? theme.warn("expired") : theme.success("active");
4887
+ return [
4888
+ delegation.cid,
4889
+ state,
4890
+ formatExpiresAt(delegation.expiresAt).trim(),
4891
+ plural(delegation.permissions.length, "permission")
4892
+ ].filter(Boolean).join(" ");
4893
+ }
4894
+ function formatExpiresAt(expiresAt) {
4895
+ return expiresAt ? ` until ${expiresAt}` : "";
4896
+ }
4897
+ function plural(count, label) {
4898
+ return `${count} ${label}${count === 1 ? "" : "s"}`;
4899
+ }
4900
+
3710
4901
  // src/index.ts
3711
4902
  var { version } = JSON.parse(
3712
4903
  readFileSync3(new URL("../package.json", import.meta.url), "utf-8")
@@ -3721,7 +4912,7 @@ program.hook("preAction", async (thisCommand) => {
3721
4912
  const commandName = thisCommand.name();
3722
4913
  const parentName = thisCommand.parent?.name();
3723
4914
  const fullCommand = parentName && parentName !== "tc" ? `${parentName} ${commandName}` : commandName;
3724
- const skipGuard = ["tc", "init", "doctor", "completion", "help", "upgrade"].includes(commandName) || fullCommand === "profile create";
4915
+ const skipGuard = ["tc", "init", "doctor", "completion", "help", "upgrade", "status"].includes(commandName) || fullCommand === "profile create";
3725
4916
  if (!skipGuard && !opts.quiet && isInteractive()) {
3726
4917
  try {
3727
4918
  const config = await ProfileManager.getConfig();
@@ -3756,6 +4947,9 @@ registerSqlCommand(program);
3756
4947
  registerDuckdbCommand(program);
3757
4948
  registerManifestCommand(program);
3758
4949
  registerUpgradeCommand(program);
4950
+ registerStatusCommand(program);
4951
+ program.addHelpText("before", () => `${theme.label("Version:")} ${theme.value(version)}
4952
+ `);
3759
4953
  program.addHelpText("afterAll", () => {
3760
4954
  if (!process.stdout.isTTY) return "";
3761
4955
  return `