@tinycloud/cli 0.7.0-beta.1 → 0.7.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
@@ -322,7 +322,8 @@ function wrapError(error) {
322
322
  }
323
323
  function handleError(error) {
324
324
  const cliError = wrapError(error);
325
- const hint = buildAuthHint(cliError) ?? (cliError.code === "NETWORK_ERROR" ? buildNetworkHint() : void 0);
325
+ const prebuilt = typeof cliError.metadata?.hint === "string" ? cliError.metadata.hint : void 0;
326
+ const hint = prebuilt ?? buildAuthHint(cliError) ?? (cliError.code === "NETWORK_ERROR" ? buildNetworkHint() : void 0);
326
327
  outputError(cliError.code, cliError.message, hint);
327
328
  process.exit(cliError.exitCode);
328
329
  }
@@ -815,12 +816,19 @@ function buildAuthUrl(did, options = {}) {
815
816
  if (options.host) {
816
817
  params.set("host", options.host);
817
818
  }
819
+ const reason = typeof options.reason === "string" ? options.reason.trim() : "";
818
820
  if (options.permissions?.length) {
819
821
  params.set(
820
822
  "permissions",
821
- Buffer.from(JSON.stringify({ permissions: options.permissions })).toString("base64url")
823
+ Buffer.from(JSON.stringify({
824
+ permissions: options.permissions,
825
+ ...reason ? { reason } : {}
826
+ })).toString("base64url")
822
827
  );
823
828
  }
829
+ if (reason) {
830
+ params.set("reason", reason);
831
+ }
824
832
  if (options.expiry !== void 0) {
825
833
  params.set("expiry", String(options.expiry));
826
834
  }
@@ -911,8 +919,8 @@ async function callbackFlow(did, options = {}) {
911
919
  }
912
920
  if (openBrowser) {
913
921
  try {
914
- const open = (await import("open")).default;
915
- await open(authUrl);
922
+ const open2 = (await import("open")).default;
923
+ await open2(authUrl);
916
924
  } catch {
917
925
  server.close();
918
926
  throw new Error("Failed to open browser");
@@ -973,11 +981,19 @@ Open this URL in a browser to authenticate:
973
981
 
974
982
  // src/commands/init.ts
975
983
  function registerInitCommand(program2) {
976
- 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) => {
984
+ 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").option("--default-space <name>", "Default space used when --space is omitted (e.g. applications)").action(async (options, cmd) => {
977
985
  try {
978
986
  const globalOpts = cmd.optsWithGlobals();
979
987
  const profileName = options.name;
980
988
  const host = options.host ?? globalOpts.host ?? DEFAULT_HOST;
989
+ const defaultSpace = options.defaultSpace;
990
+ if (defaultSpace !== void 0 && !/^[A-Za-z0-9_-]+$/.test(defaultSpace)) {
991
+ throw new CLIError(
992
+ "INVALID_SPACE",
993
+ `Invalid --default-space "${defaultSpace}". Use a short name ([A-Za-z0-9_-]).`,
994
+ ExitCode.USAGE_ERROR
995
+ );
996
+ }
981
997
  if (await ProfileManager.profileExists(profileName)) {
982
998
  throw new CLIError(
983
999
  "PROFILE_EXISTS",
@@ -996,7 +1012,8 @@ function registerInitCommand(program2) {
996
1012
  chainId: DEFAULT_CHAIN_ID,
997
1013
  spaceName: "default",
998
1014
  did,
999
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
1015
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1016
+ ...defaultSpace ? { defaultSpace } : {}
1000
1017
  };
1001
1018
  await ProfileManager.setProfile(profileName, profileConfig);
1002
1019
  const config = await ProfileManager.getConfig();
@@ -1044,7 +1061,7 @@ import { spawn } from "child_process";
1044
1061
  import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1045
1062
  import { dirname as dirname2 } from "path";
1046
1063
  import { createInterface as createInterface2 } from "readline";
1047
- import { principalDidEquals } from "@tinycloud/node-sdk";
1064
+ import { grantAuthRequest, principalDidEquals } from "@tinycloud/node-sdk";
1048
1065
 
1049
1066
  // src/config/types.ts
1050
1067
  var CLI_PROFILE_POSTURES = [
@@ -5731,6 +5748,31 @@ var KVService = class extends BaseService {
5731
5748
  }
5732
5749
  return void 0;
5733
5750
  }
5751
+ /**
5752
+ * Classify a KV 404 by reading the response body once.
5753
+ *
5754
+ * The server returns 404 both for a genuinely missing key AND for an
5755
+ * un-hosted space (body "Space not found"). Previously get/head/delete
5756
+ * collapsed every 404 to KV_NOT_FOUND before reading the body, so an
5757
+ * un-hosted-space read was indistinguishable from a missing key. We now
5758
+ * preserve status + the "Space not found" body for the un-hosted case (so the
5759
+ * CLI/SDK can normalize it to SPACE_NOT_HOSTED, matching put/list/sql), and
5760
+ * fall through to KV_NOT_FOUND for a real missing key.
5761
+ */
5762
+ async classifyNotFound(response, key) {
5763
+ const errorText = await response.text();
5764
+ if (/space not found/i.test(errorText)) {
5765
+ return err(
5766
+ serviceError(
5767
+ ErrorCodes.KV_NOT_FOUND,
5768
+ `KV ${response.status} - ${errorText}`,
5769
+ "kv",
5770
+ { meta: { status: response.status, statusText: response.statusText } }
5771
+ )
5772
+ );
5773
+ }
5774
+ return err(serviceError(ErrorCodes.KV_NOT_FOUND, `Key not found: ${key}`, "kv"));
5775
+ }
5734
5776
  /**
5735
5777
  * Get the full path with optional prefix.
5736
5778
  *
@@ -5781,6 +5823,38 @@ var KVService = class extends BaseService {
5781
5823
  signal: this.combineSignals(signal)
5782
5824
  });
5783
5825
  }
5826
+ /**
5827
+ * Serialize a single put value into a fetch body.
5828
+ *
5829
+ * Binary values (Blob/ArrayBuffer/typed-array, incl. Node Buffer) are sent as
5830
+ * raw bytes (as a Blob) so they round-trip byte-identically — without this a
5831
+ * Buffer would be JSON.stringify'd into `{"type":"Buffer","data":[...]}`.
5832
+ * Strings are returned unchanged (preserving prior behavior); other values are
5833
+ * JSON-encoded. `contentType` overrides the inferred type for binary values.
5834
+ */
5835
+ serializePutValue(value, contentType) {
5836
+ if (value instanceof Blob) {
5837
+ if (!contentType || value.type === contentType) {
5838
+ return value;
5839
+ }
5840
+ return new Blob([value], { type: contentType });
5841
+ }
5842
+ if (value instanceof ArrayBuffer) {
5843
+ return new Blob([value], {
5844
+ type: contentType ?? "application/octet-stream"
5845
+ });
5846
+ }
5847
+ if (ArrayBuffer.isView(value)) {
5848
+ const view = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
5849
+ return new Blob([view], {
5850
+ type: contentType ?? "application/octet-stream"
5851
+ });
5852
+ }
5853
+ if (typeof value === "string") {
5854
+ return contentType ? new Blob([value], { type: contentType }) : value;
5855
+ }
5856
+ return JSON.stringify(value);
5857
+ }
5784
5858
  serializeBatchPutValue(item) {
5785
5859
  const contentType = item.contentType;
5786
5860
  if (item.value instanceof Blob) {
@@ -5850,10 +5924,13 @@ var KVService = class extends BaseService {
5850
5924
  * @param raw - Whether to return raw text
5851
5925
  * @returns Parsed data
5852
5926
  */
5853
- async parseResponse(response, raw = false) {
5927
+ async parseResponse(response, raw = false, binary = false) {
5854
5928
  if (!response.ok) {
5855
5929
  return void 0;
5856
5930
  }
5931
+ if (binary) {
5932
+ return new Uint8Array(await response.arrayBuffer());
5933
+ }
5857
5934
  if (raw) {
5858
5935
  return await response.text();
5859
5936
  }
@@ -5942,13 +6019,7 @@ var KVService = class extends BaseService {
5942
6019
  }));
5943
6020
  }
5944
6021
  if (response.status === 404) {
5945
- return err(
5946
- serviceError(
5947
- ErrorCodes.KV_NOT_FOUND,
5948
- `Key not found: ${key}`,
5949
- "kv"
5950
- )
5951
- );
6022
+ return this.classifyNotFound(response, key);
5952
6023
  }
5953
6024
  const errorText = await response.text();
5954
6025
  return err(
@@ -5960,7 +6031,11 @@ var KVService = class extends BaseService {
5960
6031
  )
5961
6032
  );
5962
6033
  }
5963
- const data = await this.parseResponse(response, options?.raw);
6034
+ const data = await this.parseResponse(
6035
+ response,
6036
+ options?.raw,
6037
+ options?.binary
6038
+ );
5964
6039
  return ok({
5965
6040
  data,
5966
6041
  headers: this.createResponseHeaders(response.headers)
@@ -5979,12 +6054,7 @@ var KVService = class extends BaseService {
5979
6054
  return err(authRequiredError("kv"));
5980
6055
  }
5981
6056
  const path = this.getFullPath(key, options?.prefix);
5982
- let body;
5983
- if (typeof value === "string") {
5984
- body = value;
5985
- } else {
5986
- body = JSON.stringify(value);
5987
- }
6057
+ const body = this.serializePutValue(value, options?.contentType);
5988
6058
  try {
5989
6059
  const response = await this.invokeOperation(
5990
6060
  path,
@@ -6210,13 +6280,7 @@ var KVService = class extends BaseService {
6210
6280
  }));
6211
6281
  }
6212
6282
  if (response.status === 404) {
6213
- return err(
6214
- serviceError(
6215
- ErrorCodes.KV_NOT_FOUND,
6216
- `Key not found: ${key}`,
6217
- "kv"
6218
- )
6219
- );
6283
+ return this.classifyNotFound(response, key);
6220
6284
  }
6221
6285
  const errorText = await response.text();
6222
6286
  return err(
@@ -6261,13 +6325,7 @@ var KVService = class extends BaseService {
6261
6325
  }));
6262
6326
  }
6263
6327
  if (response.status === 404) {
6264
- return err(
6265
- serviceError(
6266
- ErrorCodes.KV_NOT_FOUND,
6267
- `Key not found: ${key}`,
6268
- "kv"
6269
- )
6270
- );
6328
+ return this.classifyNotFound(response, key);
6271
6329
  }
6272
6330
  const errorText = await response.text();
6273
6331
  return err(
@@ -6414,6 +6472,7 @@ var DatabaseHandle = class {
6414
6472
  var SQLAction = {
6415
6473
  READ: "tinycloud.sql/read",
6416
6474
  WRITE: "tinycloud.sql/write",
6475
+ DDL: "tinycloud.sql/ddl",
6417
6476
  ADMIN: "tinycloud.sql/admin",
6418
6477
  SELECT: "tinycloud.sql/select",
6419
6478
  INSERT: "tinycloud.sql/insert",
@@ -6423,6 +6482,7 @@ var SQLAction = {
6423
6482
  EXPORT: "tinycloud.sql/export",
6424
6483
  ALL: "tinycloud.sql/*"
6425
6484
  };
6485
+ var DDL_TOKENS = /* @__PURE__ */ new Set(["alter", "create", "drop"]);
6426
6486
  var SQLService = class extends BaseService {
6427
6487
  constructor(config = {}) {
6428
6488
  super();
@@ -6498,9 +6558,15 @@ var SQLService = class extends BaseService {
6498
6558
  if (options?.schema) {
6499
6559
  body.schema = options.schema;
6500
6560
  }
6561
+ const actions = [
6562
+ this.actionForSql(sql, SQLAction.WRITE),
6563
+ ...(options?.schema ?? []).map(
6564
+ (statement) => this.actionForSql(statement, SQLAction.DDL)
6565
+ )
6566
+ ];
6501
6567
  const response = await this.invokeSQL(
6502
6568
  dbName,
6503
- this.actionForSql(sql, SQLAction.WRITE),
6569
+ this.dedupeActions(actions),
6504
6570
  body,
6505
6571
  options?.signal
6506
6572
  );
@@ -6522,7 +6588,7 @@ var SQLService = class extends BaseService {
6522
6588
  try {
6523
6589
  const response = await this.invokeSQL(
6524
6590
  dbName,
6525
- this.actionForSqlBatch(statements),
6591
+ this.actionsForSqlBatch(statements),
6526
6592
  { action: "batch", statements },
6527
6593
  options?.signal
6528
6594
  );
@@ -6586,9 +6652,10 @@ var SQLService = class extends BaseService {
6586
6652
  });
6587
6653
  }
6588
6654
  // === Private helpers ===
6589
- async invokeSQL(dbName, action, body, signal) {
6655
+ async invokeSQL(dbName, actions, body, signal) {
6590
6656
  const session = this.context.session;
6591
- const headers = this.context.invoke(session, "sql", dbName, action);
6657
+ const actionList = Array.isArray(actions) ? actions : [actions];
6658
+ const headers = actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
6592
6659
  return this.context.fetch(`${this.host}/invoke`, {
6593
6660
  method: "POST",
6594
6661
  headers: {
@@ -6600,12 +6667,34 @@ var SQLService = class extends BaseService {
6600
6667
  });
6601
6668
  }
6602
6669
  actionForSql(sql, fallback) {
6603
- return firstSqlToken(sql) === "pragma" ? SQLAction.ADMIN : fallback;
6670
+ const token = firstSqlToken(sql);
6671
+ if (token === "pragma") return SQLAction.ADMIN;
6672
+ if (token !== void 0 && DDL_TOKENS.has(token)) return SQLAction.DDL;
6673
+ return fallback;
6674
+ }
6675
+ actionsForSqlBatch(statements) {
6676
+ return this.dedupeActions(
6677
+ statements.map((statement) => this.actionForSql(statement.sql, SQLAction.WRITE))
6678
+ );
6604
6679
  }
6605
- actionForSqlBatch(statements) {
6606
- return statements.some(
6607
- (statement) => this.actionForSql(statement.sql, SQLAction.WRITE) === SQLAction.ADMIN
6608
- ) ? SQLAction.ADMIN : SQLAction.WRITE;
6680
+ dedupeActions(actions) {
6681
+ return [...new Set(actions)];
6682
+ }
6683
+ invokeSQLAny(session, dbName, actions) {
6684
+ if (!this.context.invokeAny) {
6685
+ throw new Error(
6686
+ `SQL operation requires multiple permissions (${actions.join(", ")}) but this SDK runtime does not support multi-resource invocations`
6687
+ );
6688
+ }
6689
+ return this.context.invokeAny(
6690
+ session,
6691
+ actions.map((action) => ({
6692
+ spaceId: session.spaceId,
6693
+ service: "sql",
6694
+ path: dbName,
6695
+ action
6696
+ }))
6697
+ );
6609
6698
  }
6610
6699
  async handleErrorResponse(response, operation) {
6611
6700
  const errorText = await response.text();
@@ -10733,30 +10822,31 @@ function resolveChainId(profile, session) {
10733
10822
  return profile.chainId;
10734
10823
  }
10735
10824
  async function resolveSpaceUri(input, profileName) {
10736
- if (!input) return void 0;
10737
- if (input.startsWith("tinycloud:")) {
10738
- const parsed = parseSpaceUri(input);
10825
+ const profile = await ProfileManager.getProfile(profileName);
10826
+ const effective = input || profile.defaultSpace;
10827
+ if (!effective) return void 0;
10828
+ if (effective.startsWith("tinycloud:")) {
10829
+ const parsed = parseSpaceUri(effective);
10739
10830
  if (!parsed) {
10740
10831
  throw new CLIError(
10741
10832
  "INVALID_SPACE",
10742
- `Invalid --space "${input}". Use a short name ([A-Za-z0-9_-]) or a full tinycloud:... URI.`,
10833
+ `Invalid space "${effective}". Use a short name ([A-Za-z0-9_-]) or a full tinycloud:... URI.`,
10743
10834
  ExitCode.USAGE_ERROR
10744
10835
  );
10745
10836
  }
10746
10837
  return buildSpaceUri(parsed.owner, parsed.name);
10747
10838
  }
10748
- if (!/^[A-Za-z0-9_-]+$/.test(input)) {
10839
+ if (!/^[A-Za-z0-9_-]+$/.test(effective)) {
10749
10840
  throw new CLIError(
10750
10841
  "INVALID_SPACE",
10751
- `Invalid --space "${input}". Use a short name ([A-Za-z0-9_-]) or a full tinycloud:... URI.`,
10842
+ `Invalid space "${effective}". Use a short name ([A-Za-z0-9_-]) or a full tinycloud:... URI.`,
10752
10843
  ExitCode.USAGE_ERROR
10753
10844
  );
10754
10845
  }
10755
- const profile = await ProfileManager.getProfile(profileName);
10756
10846
  const session = await ProfileManager.getSession(profileName);
10757
10847
  const address = resolveAddress(profile, session);
10758
10848
  const chainId = resolveChainId(profile, session);
10759
- return makePkhSpaceId(address, chainId, input);
10849
+ return makePkhSpaceId(address, chainId, effective);
10760
10850
  }
10761
10851
 
10762
10852
  // src/lib/permissions.ts
@@ -10938,6 +11028,7 @@ async function loadManifestPermissions(source, profile) {
10938
11028
  )
10939
11029
  };
10940
11030
  });
11031
+ permissions.push(...await secretPermissionsFromAppManifest(manifest, profile));
10941
11032
  return resolvePermissionSpaces(permissions, profile);
10942
11033
  }
10943
11034
  throw new CLIError(
@@ -10946,6 +11037,46 @@ async function loadManifestPermissions(source, profile) {
10946
11037
  ExitCode.USAGE_ERROR
10947
11038
  );
10948
11039
  }
11040
+ async function secretPermissionsFromAppManifest(manifest, profile) {
11041
+ if (manifest.secrets === void 0) {
11042
+ return [];
11043
+ }
11044
+ const resolved = resolveManifest({
11045
+ app_id: String(manifest.app_id),
11046
+ name: typeof manifest.name === "string" ? manifest.name : String(manifest.app_id),
11047
+ defaults: false,
11048
+ prefix: "",
11049
+ secrets: manifest.secrets
11050
+ });
11051
+ const permissions = resolved.resources.filter(
11052
+ (resource) => resource.service === "tinycloud.kv" && resource.space === "secrets" && resource.path.startsWith("vault/secrets/")
11053
+ );
11054
+ const needsDecrypt = permissions.some(
11055
+ (permission) => permission.actions.includes("tinycloud.kv/get")
11056
+ );
11057
+ if (needsDecrypt) {
11058
+ permissions.push({
11059
+ service: ENCRYPTION_PERMISSION_SERVICE,
11060
+ space: ENCRYPTION_MANIFEST_SPACE,
11061
+ path: await defaultSecretsNetworkId(profile),
11062
+ actions: ["tinycloud.encryption/decrypt"],
11063
+ skipPrefix: true
11064
+ });
11065
+ }
11066
+ return permissions;
11067
+ }
11068
+ async function defaultSecretsNetworkId(profileName) {
11069
+ const profile = await ProfileManager.getProfile(profileName);
11070
+ const ownerDid = (profile.ownerDid ?? profile.did)?.split("#")[0];
11071
+ if (!ownerDid) {
11072
+ throw new CLIError(
11073
+ "OWNER_DID_UNKNOWN",
11074
+ `Cannot determine owner DID for profile "${profileName}". Run \`tc auth login\` first.`,
11075
+ ExitCode.AUTH_REQUIRED
11076
+ );
11077
+ }
11078
+ return `urn:tinycloud:encryption:${ownerDid}:default`;
11079
+ }
10949
11080
  function permissionsFromDelegation(delegation) {
10950
11081
  if (delegation.resources?.length) {
10951
11082
  return delegation.resources.map((resource) => ({
@@ -11298,6 +11429,10 @@ function registerAuthCommand(program2) {
11298
11429
  jwk: key,
11299
11430
  host: ctx.host,
11300
11431
  permissions: group,
11432
+ reason: permissionGrantReason(
11433
+ "Grant requested TinyCloud permissions from `tc auth request --grant`.",
11434
+ group
11435
+ ),
11301
11436
  openkeyHost,
11302
11437
  expiry: expiryOption,
11303
11438
  noPopup: options.popup === false
@@ -11442,23 +11577,11 @@ function registerAuthCommand(program2) {
11442
11577
  node,
11443
11578
  requested: parsed.requested,
11444
11579
  expiryOption: parsed.requestedExpiry,
11580
+ reason: "Grant permissions requested by a TinyCloud auth request artifact.",
11445
11581
  yes: options.yes === true
11446
11582
  });
11447
- const result = await node.delegateTo(
11448
- parsed.sessionDid,
11449
- parsed.requested,
11450
- parsed.requestedExpiry !== void 0 ? { expiry: parsed.requestedExpiry } : void 0
11451
- );
11452
- outputJson({
11453
- kind: "tinycloud.auth.delegation",
11454
- version: 1,
11455
- requestId: parsed.requestId,
11456
- delegationCid: result.delegation.cid,
11457
- delegation: result.delegation,
11458
- permissions: parsed.requested,
11459
- expiry: result.delegation.expiry.toISOString(),
11460
- prompted: result.prompted
11461
- });
11583
+ const grant = await grantAuthRequest(node, parsed);
11584
+ outputJson(grant);
11462
11585
  } catch (error) {
11463
11586
  handleError(error);
11464
11587
  }
@@ -11738,6 +11861,7 @@ async function ensureDelegationAuthority(params) {
11738
11861
  jwk: key,
11739
11862
  host: params.ctx.host,
11740
11863
  permissions: group,
11864
+ reason: permissionGrantReason(params.reason, group),
11741
11865
  openkeyHost,
11742
11866
  expiry: params.expiryOption
11743
11867
  });
@@ -11785,6 +11909,12 @@ async function ensureDelegationAuthority(params) {
11785
11909
  });
11786
11910
  }
11787
11911
  }
11912
+ function permissionGrantReason(context, permissions) {
11913
+ const first = permissions[0];
11914
+ const summary = first ? compactPermission(first) : "no permissions";
11915
+ const more = permissions.length > 1 ? ` and ${permissions.length - 1} more permission${permissions.length === 2 ? "" : "s"}` : "";
11916
+ return `${context} Requested: ${summary}${more}.`;
11917
+ }
11788
11918
  function execCapturedCommand(command) {
11789
11919
  return new Promise((resolve3, reject) => {
11790
11920
  const child = spawn(process.execPath, [process.argv[1], ...command.argv], {
@@ -11874,9 +12004,10 @@ function groupPermissionsBySpace(permissions) {
11874
12004
  rawEntries.push(permission);
11875
12005
  continue;
11876
12006
  }
11877
- const group = groups.get(permission.space) ?? [];
12007
+ const key = normalizeSpaceForCompare(permission.space);
12008
+ const group = groups.get(key) ?? [];
11878
12009
  group.push(permission);
11879
- groups.set(permission.space, group);
12010
+ groups.set(key, group);
11880
12011
  }
11881
12012
  const grouped = Array.from(groups.values());
11882
12013
  if (grouped.length === 0) {
@@ -11888,8 +12019,16 @@ function groupPermissionsBySpace(permissions) {
11888
12019
  function isRawPermission(permission) {
11889
12020
  return permission.service === "tinycloud.encryption" && permission.path.startsWith("urn:tinycloud:encryption:");
11890
12021
  }
12022
+ function normalizeSpaceForCompare(space) {
12023
+ return space.replace(
12024
+ /(eip155:\d+:)(0x[0-9a-fA-F]{40})/,
12025
+ (_match, prefix, addr) => prefix + addr.toLowerCase()
12026
+ );
12027
+ }
11891
12028
  function returnedSpaceMatchesExpected(returnedSpace, expectedSpace) {
11892
- if (returnedSpace === expectedSpace) return true;
12029
+ if (normalizeSpaceForCompare(returnedSpace) === normalizeSpaceForCompare(expectedSpace)) {
12030
+ return true;
12031
+ }
11893
12032
  if (!returnedSpace.startsWith("tinycloud:")) return false;
11894
12033
  const returnedName = returnedSpace.slice(returnedSpace.lastIndexOf(":") + 1);
11895
12034
  return returnedName === expectedSpace;
@@ -11898,7 +12037,7 @@ function portableFromOpenKeyDelegation(data, permissions, host) {
11898
12037
  const primary = permissions.find((permission) => !isRawPermission(permission)) ?? permissions[0];
11899
12038
  const returnedSpace = String(data.spaceId ?? primary.space ?? "encryption");
11900
12039
  const expectedSpaces = new Set(
11901
- permissions.filter((permission) => !isRawPermission(permission)).map((permission) => permission.space)
12040
+ permissions.filter((permission) => !isRawPermission(permission)).map((permission) => normalizeSpaceForCompare(permission.space))
11902
12041
  );
11903
12042
  const matchesExpectedSpace = expectedSpaces.size === 1 && returnedSpaceMatchesExpected(returnedSpace, Array.from(expectedSpaces)[0]);
11904
12043
  if (expectedSpaces.size > 0 && !matchesExpectedSpace) {
@@ -12125,6 +12264,79 @@ async function refreshOpenKeySession(profileName, host, options = {}) {
12125
12264
  // src/commands/kv.ts
12126
12265
  import { readFile as readFile4 } from "fs/promises";
12127
12266
  import { writeFile as writeFile3 } from "fs/promises";
12267
+
12268
+ // src/lib/host.ts
12269
+ function canonicalizeAddress2(address) {
12270
+ const trimmed = address.trim();
12271
+ return trimmed.startsWith("0x") ? `0x${trimmed.slice(2).toLowerCase()}` : trimmed.toLowerCase();
12272
+ }
12273
+ async function resolveLocalAddress(profile, profileName) {
12274
+ const session = await ProfileManager.getSession(profileName);
12275
+ const sessAddr = session?.address;
12276
+ if (typeof sessAddr === "string" && sessAddr.length > 0) {
12277
+ return canonicalizeAddress2(sessAddr);
12278
+ }
12279
+ if (profile.address) return canonicalizeAddress2(profile.address);
12280
+ if (profile.ownerDid) {
12281
+ const match = profile.ownerDid.match(/^did:pkh:eip155:\d+:(0x[a-fA-F0-9]{40})$/);
12282
+ if (match) return canonicalizeAddress2(match[1]);
12283
+ }
12284
+ return null;
12285
+ }
12286
+ function ownerAddressFromSpaceUri(spaceUri) {
12287
+ const match = spaceUri.match(/^tinycloud:pkh:eip155:\d+:(0x[a-fA-F0-9]{40}):/);
12288
+ return match ? canonicalizeAddress2(match[1]) : null;
12289
+ }
12290
+ function ownerDidFromSpaceUri(spaceUri) {
12291
+ const match = spaceUri.match(/^tinycloud:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40}):/);
12292
+ if (!match) return null;
12293
+ return `did:pkh:eip155:${match[1]}:${canonicalizeAddress2(match[2])}`;
12294
+ }
12295
+ async function isRootAuthority(spaceUri, profileName) {
12296
+ const profile = await ProfileManager.getProfile(profileName);
12297
+ if (resolveProfilePosture(profile) === "delegate-session") return false;
12298
+ const ownerAddr = ownerAddressFromSpaceUri(spaceUri);
12299
+ if (!ownerAddr) return false;
12300
+ const selfAddr = await resolveLocalAddress(profile, profileName);
12301
+ return selfAddr !== null && selfAddr === ownerAddr;
12302
+ }
12303
+ function spaceNameFromUri(spaceUri) {
12304
+ return spaceUri.slice(spaceUri.lastIndexOf(":") + 1);
12305
+ }
12306
+ async function unhostedSpaceError(error, spaceUri, profileName) {
12307
+ if (!spaceUri) return null;
12308
+ const status = error.meta?.status;
12309
+ const isUnhosted = status === 404 && /space not found/i.test(error.message);
12310
+ if (!isUnhosted) return null;
12311
+ const spaceName = spaceNameFromUri(spaceUri);
12312
+ const owner = await isRootAuthority(spaceUri, profileName);
12313
+ const hint = owner ? [
12314
+ "You are the owner. Host it once:",
12315
+ ` tc space host ${spaceName}`,
12316
+ "Then retry."
12317
+ ].join("\n") : [
12318
+ "You are a delegate and CANNOT host this space \u2014 only its owner can.",
12319
+ "Emit a host request:",
12320
+ ` tc space host-request ${spaceName} --emit ./host-request.json`,
12321
+ "Send it to the owner; they run `tc space host` and confirm. Then retry."
12322
+ ].join("\n");
12323
+ const message = owner ? `Space '${spaceName}' (${spaceUri}) is not hosted.` : `Space '${spaceName}' (owner ${ownerDidFromSpaceUri(spaceUri) ?? spaceUri}) is not hosted.`;
12324
+ return new CLIError("SPACE_NOT_HOSTED", message, ExitCode.ERROR, { hint });
12325
+ }
12326
+ async function resolveHostSpace(name, profileName) {
12327
+ const resolved = await resolveSpaceUri(name, profileName);
12328
+ if (!resolved) {
12329
+ throw new Error(`Could not resolve a space for "${name}".`);
12330
+ }
12331
+ return resolved;
12332
+ }
12333
+
12334
+ // src/commands/kv.ts
12335
+ async function throwKvError(error, spaceUri, profileName) {
12336
+ const hosted = await unhostedSpaceError(error, spaceUri, profileName);
12337
+ if (hosted) throw hosted;
12338
+ throw new CLIError(error.code, error.message, ExitCode.ERROR);
12339
+ }
12128
12340
  async function readStdin2() {
12129
12341
  const chunks = [];
12130
12342
  for await (const chunk of process.stdin) {
@@ -12134,7 +12346,8 @@ async function readStdin2() {
12134
12346
  }
12135
12347
  async function kvHandle(node, spaceInput, profileName) {
12136
12348
  const spaceUri = await resolveSpaceUri(spaceInput, profileName);
12137
- return spaceUri ? node.kvForSpace(spaceUri) : node.kv;
12349
+ const kv = spaceUri ? node.kvForSpace(spaceUri) : node.kv;
12350
+ return { kv, spaceUri };
12138
12351
  }
12139
12352
  function registerKvCommand(program2) {
12140
12353
  const kv = program2.command("kv").description("Key-value store operations");
@@ -12143,25 +12356,29 @@ function registerKvCommand(program2) {
12143
12356
  const globalOpts = cmd.optsWithGlobals();
12144
12357
  const ctx = await ProfileManager.resolveContext(globalOpts);
12145
12358
  const node = await ensureAuthenticated(ctx);
12146
- const kv2 = await kvHandle(node, options.space, ctx.profile);
12147
- const result = await withSpinner(`Getting ${key}...`, () => kv2.get(key));
12359
+ const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
12360
+ const wantBytes = !!options.output || !!options.raw;
12361
+ const result = await withSpinner(
12362
+ `Getting ${key}...`,
12363
+ () => kv2.get(key, wantBytes ? { binary: true } : void 0)
12364
+ );
12148
12365
  if (!result.ok) {
12366
+ const hosted = await unhostedSpaceError(result.error, spaceUri, ctx.profile);
12367
+ if (hosted) throw hosted;
12149
12368
  if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
12150
12369
  throw new CLIError("NOT_FOUND", `Key "${key}" not found`, ExitCode.NOT_FOUND);
12151
12370
  }
12152
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
12371
+ await throwKvError(result.error, spaceUri, ctx.profile);
12153
12372
  }
12154
12373
  const data = result.data.data;
12155
12374
  const metadata = result.data.headers ?? {};
12156
12375
  if (options.output) {
12157
- const content = typeof data === "string" ? data : JSON.stringify(data);
12158
- await writeFile3(options.output, content);
12376
+ await writeFile3(options.output, data);
12159
12377
  outputJson({ key, written: options.output });
12160
12378
  return;
12161
12379
  }
12162
12380
  if (options.raw) {
12163
- const content = typeof data === "string" ? data : JSON.stringify(data);
12164
- process.stdout.write(content);
12381
+ process.stdout.write(data);
12165
12382
  return;
12166
12383
  }
12167
12384
  if (shouldOutputJson()) {
@@ -12178,7 +12395,7 @@ function registerKvCommand(program2) {
12178
12395
  handleError(error);
12179
12396
  }
12180
12397
  });
12181
- kv.command("put <key> [value]").description("Set a value").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").action(async (key, value, options, cmd) => {
12398
+ kv.command("put <key> [value]").description("Set a value").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, value, options, cmd) => {
12182
12399
  try {
12183
12400
  const globalOpts = cmd.optsWithGlobals();
12184
12401
  const ctx = await ProfileManager.resolveContext(globalOpts);
@@ -12202,23 +12419,25 @@ function registerKvCommand(program2) {
12202
12419
  putValue = value;
12203
12420
  }
12204
12421
  }
12205
- const result = await withSpinner(`Writing ${key}...`, () => node.kv.put(key, putValue));
12422
+ const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
12423
+ const result = await withSpinner(`Writing ${key}...`, () => kv2.put(key, putValue));
12206
12424
  if (!result.ok) {
12207
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
12425
+ await throwKvError(result.error, spaceUri, ctx.profile);
12208
12426
  }
12209
12427
  outputJson({ key, written: true });
12210
12428
  } catch (error) {
12211
12429
  handleError(error);
12212
12430
  }
12213
12431
  });
12214
- kv.command("delete <key>").description("Delete a key").action(async (key, _options, cmd) => {
12432
+ kv.command("delete <key>").description("Delete a key").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, options, cmd) => {
12215
12433
  try {
12216
12434
  const globalOpts = cmd.optsWithGlobals();
12217
12435
  const ctx = await ProfileManager.resolveContext(globalOpts);
12218
12436
  const node = await ensureAuthenticated(ctx);
12219
- const result = await withSpinner(`Deleting ${key}...`, () => node.kv.delete(key));
12437
+ const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
12438
+ const result = await withSpinner(`Deleting ${key}...`, () => kv2.delete(key));
12220
12439
  if (!result.ok) {
12221
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
12440
+ await throwKvError(result.error, spaceUri, ctx.profile);
12222
12441
  }
12223
12442
  outputJson({ key, deleted: true });
12224
12443
  } catch (error) {
@@ -12230,11 +12449,11 @@ function registerKvCommand(program2) {
12230
12449
  const globalOpts = cmd.optsWithGlobals();
12231
12450
  const ctx = await ProfileManager.resolveContext(globalOpts);
12232
12451
  const node = await ensureAuthenticated(ctx);
12233
- const kv2 = await kvHandle(node, options.space, ctx.profile);
12452
+ const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
12234
12453
  const listOptions = options.prefix ? { prefix: options.prefix } : void 0;
12235
12454
  const result = await withSpinner("Listing keys...", () => kv2.list(listOptions));
12236
12455
  if (!result.ok) {
12237
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
12456
+ await throwKvError(result.error, spaceUri, ctx.profile);
12238
12457
  }
12239
12458
  const rawData = result.data.data ?? result.data;
12240
12459
  const keyList = Array.isArray(rawData) ? rawData : rawData?.keys ?? [];
@@ -12265,14 +12484,16 @@ function registerKvCommand(program2) {
12265
12484
  const globalOpts = cmd.optsWithGlobals();
12266
12485
  const ctx = await ProfileManager.resolveContext(globalOpts);
12267
12486
  const node = await ensureAuthenticated(ctx);
12268
- const kv2 = await kvHandle(node, options.space, ctx.profile);
12487
+ const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
12269
12488
  const result = await withSpinner(`Checking ${key}...`, () => kv2.head(key));
12270
12489
  if (!result.ok) {
12490
+ const hosted = await unhostedSpaceError(result.error, spaceUri, ctx.profile);
12491
+ if (hosted) throw hosted;
12271
12492
  if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
12272
12493
  outputJson({ key, exists: false, metadata: {} });
12273
12494
  return;
12274
12495
  }
12275
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
12496
+ await throwKvError(result.error, spaceUri, ctx.profile);
12276
12497
  }
12277
12498
  outputJson({
12278
12499
  key,
@@ -12286,6 +12507,13 @@ function registerKvCommand(program2) {
12286
12507
  }
12287
12508
 
12288
12509
  // src/commands/space.ts
12510
+ import { randomBytes as randomBytes3 } from "crypto";
12511
+ import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
12512
+ import { dirname as dirname3 } from "path";
12513
+ function didWithoutFragment2(did) {
12514
+ const fragment = did.indexOf("#");
12515
+ return fragment === -1 ? did : did.slice(0, fragment);
12516
+ }
12289
12517
  function registerSpaceCommand(program2) {
12290
12518
  const space = program2.command("space").description("Space management");
12291
12519
  space.command("list").description("List spaces").action(async (_options, cmd) => {
@@ -12315,16 +12543,60 @@ function registerSpaceCommand(program2) {
12315
12543
  handleError(error);
12316
12544
  }
12317
12545
  });
12318
- space.command("create <name>").description("Create a new space").action(async (name, _options, cmd) => {
12546
+ space.command("create <name>").alias("host").description("Create (host) one of your owned spaces by name").action(async (name, _options, cmd) => {
12319
12547
  try {
12320
12548
  const globalOpts = cmd.optsWithGlobals();
12321
12549
  const ctx = await ProfileManager.resolveContext(globalOpts);
12322
12550
  const node = await ensureAuthenticated(ctx);
12323
- const result = await node.spaces.create(name);
12324
- if (!result.ok) {
12325
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
12551
+ const spaceId = await node.hostOwnedSpace(name);
12552
+ outputJson({ spaceId, name, hosted: true });
12553
+ } catch (error) {
12554
+ handleError(error);
12555
+ }
12556
+ });
12557
+ space.command("host-request <name>").description("Emit a request asking the space owner to host it (delegate-only)").option("--emit [file]", "Write the request artifact to file (or stdout when no path)").addHelpText("after", `
12558
+
12559
+ A delegate cannot host a space \u2014 only its owner (root authority) can. This
12560
+ emits a tinycloud.host.request artifact naming the space and its owner so you
12561
+ can hand it to the owner; they run \`tc space host <name>\` and confirm.
12562
+
12563
+ If you ARE the owner of the resolved space, this refuses and tells you to host
12564
+ it directly with \`tc space host <name>\` (no request needed).
12565
+ `).action(async (name, options, cmd) => {
12566
+ try {
12567
+ const globalOpts = cmd.optsWithGlobals();
12568
+ const ctx = await ProfileManager.resolveContext(globalOpts);
12569
+ const profile = await ProfileManager.getProfile(ctx.profile);
12570
+ const spaceId = await resolveHostSpace(name, ctx.profile);
12571
+ const spaceName = spaceNameFromUri(spaceId);
12572
+ if (await isRootAuthority(spaceId, ctx.profile)) {
12573
+ throw new CLIError(
12574
+ "ALREADY_ROOT_AUTHORITY",
12575
+ `You are the owner of ${spaceId}. Host it directly: tc space host ${spaceName}`,
12576
+ ExitCode.USAGE_ERROR
12577
+ );
12578
+ }
12579
+ const requesterDid = didWithoutFragment2(profile.sessionDid ?? profile.did);
12580
+ const ownerDid = ownerDidFromSpaceUri(spaceId);
12581
+ if (!ownerDid) {
12582
+ throw new CLIError(
12583
+ "UNRESOLVABLE_OWNER",
12584
+ `Cannot determine the owner of ${spaceId}; host-request needs a pkh space URI.`,
12585
+ ExitCode.USAGE_ERROR
12586
+ );
12326
12587
  }
12327
- outputJson({ spaceId: result.data.id, name });
12588
+ const artifact = {
12589
+ kind: "tinycloud.host.request",
12590
+ version: 1,
12591
+ requestId: `hostreq_${Date.now().toString(36)}_${randomBytes3(4).toString("hex")}`,
12592
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
12593
+ spaceName,
12594
+ spaceId,
12595
+ ownerDid,
12596
+ requesterDid,
12597
+ host: ctx.host
12598
+ };
12599
+ await emitHostRequestArtifact(artifact, options.emit);
12328
12600
  } catch (error) {
12329
12601
  handleError(error);
12330
12602
  }
@@ -12361,6 +12633,22 @@ function registerSpaceCommand(program2) {
12361
12633
  }
12362
12634
  });
12363
12635
  }
12636
+ async function emitHostRequestArtifact(artifact, emitOption) {
12637
+ if (typeof emitOption === "string" && emitOption.length > 0) {
12638
+ await mkdir3(dirname3(emitOption), { recursive: true });
12639
+ await writeFile4(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
12640
+ outputJson({
12641
+ emitted: true,
12642
+ path: emitOption,
12643
+ requestId: artifact.requestId,
12644
+ spaceName: artifact.spaceName,
12645
+ spaceId: artifact.spaceId,
12646
+ ownerDid: artifact.ownerDid
12647
+ });
12648
+ return;
12649
+ }
12650
+ outputJson(artifact);
12651
+ }
12364
12652
 
12365
12653
  // src/lib/duration.ts
12366
12654
  function parseDuration(input) {
@@ -12776,6 +13064,7 @@ function registerProfileCommand(program2) {
12776
13064
  process.stdout.write(formatField("Posture", posture) + "\n");
12777
13065
  process.stdout.write(formatField("Operator", operatorType) + "\n");
12778
13066
  process.stdout.write(formatField("Space", p.spaceId || null) + "\n");
13067
+ process.stdout.write(formatField("Default Space", p.defaultSpace || null) + "\n");
12779
13068
  process.stdout.write(formatField("Key", hasKey) + "\n");
12780
13069
  process.stdout.write(formatField("Session", hasSession) + "\n");
12781
13070
  process.stdout.write(formatField("Created", p.createdAt) + "\n");
@@ -12796,6 +13085,46 @@ function registerProfileCommand(program2) {
12796
13085
  handleError(error);
12797
13086
  }
12798
13087
  });
13088
+ profile.command("set-default-space [name]").description("Set (or clear) the default space used when --space is omitted").option("--profile <name>", "Profile to modify (defaults to the active profile)").option("--unset", "Clear the default space so commands fall back to the primary space").addHelpText("after", `
13089
+
13090
+ The default space is a short space NAME (e.g. "applications"), resolved per
13091
+ profile at command time. Precedence for every kv/sql command:
13092
+ explicit --space flag > profile defaultSpace > primary space.
13093
+
13094
+ Examples:
13095
+ $ tc profile set-default-space applications
13096
+ $ tc profile set-default-space applications --profile cli-test
13097
+ $ tc profile set-default-space --unset
13098
+ `).action(async (name, options, cmd) => {
13099
+ try {
13100
+ const globalOpts = cmd.optsWithGlobals();
13101
+ const ctx = await ProfileManager.resolveContext({
13102
+ ...globalOpts,
13103
+ profile: options.profile ?? globalOpts.profile
13104
+ });
13105
+ const profileName = ctx.profile;
13106
+ if (!options.unset && (name === void 0 || name === "")) {
13107
+ throw new CLIError(
13108
+ "USAGE_ERROR",
13109
+ "Provide a space name (e.g. `tc profile set-default-space applications`) or pass --unset.",
13110
+ ExitCode.USAGE_ERROR
13111
+ );
13112
+ }
13113
+ if (!options.unset && !/^[A-Za-z0-9_-]+$/.test(name)) {
13114
+ throw new CLIError(
13115
+ "INVALID_SPACE",
13116
+ `Invalid space name "${name}". Use a short name ([A-Za-z0-9_-]).`,
13117
+ ExitCode.USAGE_ERROR
13118
+ );
13119
+ }
13120
+ const p = await ProfileManager.getProfile(profileName);
13121
+ const defaultSpace = options.unset ? void 0 : name;
13122
+ await ProfileManager.setProfile(profileName, { ...p, defaultSpace });
13123
+ outputJson({ profile: profileName, defaultSpace: defaultSpace ?? null, updated: true });
13124
+ } catch (error) {
13125
+ handleError(error);
13126
+ }
13127
+ });
12799
13128
  profile.command("delete <name>").description("Delete a profile").action(async (name, _options, cmd) => {
12800
13129
  try {
12801
13130
  if (isInteractive()) {
@@ -12967,7 +13296,7 @@ complete -c tc -l quiet -s q -d "Suppress non-essential output"
12967
13296
 
12968
13297
  // src/commands/vault.ts
12969
13298
  import { readFile as readFile5 } from "fs/promises";
12970
- import { writeFile as writeFile4 } from "fs/promises";
13299
+ import { writeFile as writeFile5 } from "fs/promises";
12971
13300
  import { PrivateKeySigner as PrivateKeySigner2 } from "@tinycloud/node-sdk";
12972
13301
  async function readStdin3() {
12973
13302
  const chunks = [];
@@ -13056,7 +13385,7 @@ function registerVaultCommand(program2) {
13056
13385
  const data = result.data.data ?? result.data;
13057
13386
  if (options.output) {
13058
13387
  const content = data instanceof Uint8Array ? Buffer.from(data) : typeof data === "string" ? data : JSON.stringify(data);
13059
- await writeFile4(options.output, content);
13388
+ await writeFile5(options.output, content);
13060
13389
  outputJson({ key, written: options.output });
13061
13390
  return;
13062
13391
  }
@@ -13140,7 +13469,7 @@ function registerVaultCommand(program2) {
13140
13469
 
13141
13470
  // src/commands/secrets.ts
13142
13471
  import { readFile as readFile6 } from "fs/promises";
13143
- import { writeFile as writeFile5 } from "fs/promises";
13472
+ import { writeFile as writeFile6 } from "fs/promises";
13144
13473
  import { join as join5 } from "path";
13145
13474
  import { homedir as homedir2 } from "os";
13146
13475
  var SECRETS_SPACE2 = "secrets";
@@ -13262,12 +13591,17 @@ async function runSecretOperation(params) {
13262
13591
  node: params.node,
13263
13592
  requested,
13264
13593
  expiryOption: void 0,
13594
+ reason: secretPermissionReason(params.action, params.name),
13265
13595
  yes: true,
13266
13596
  force: true
13267
13597
  })
13268
13598
  );
13269
13599
  return runSecretOperationAttempt(params.label, params.operation);
13270
13600
  }
13601
+ function secretPermissionReason(action, name) {
13602
+ const target = name ? ` secret "${name}"` : " secrets";
13603
+ return `Allow \`tc secrets ${action}${name ? ` ${name}` : ""}\` to access${target} with the required TinyCloud permissions.`;
13604
+ }
13271
13605
  async function runSecretOperationAttempt(label, operation) {
13272
13606
  try {
13273
13607
  return await withSpinner(label, operation);
@@ -13639,6 +13973,30 @@ function secretPermissionEntries(params) {
13639
13973
  }
13640
13974
  return permissions;
13641
13975
  }
13976
+ function formatSecretScopeFlag(options) {
13977
+ return options?.scope ? ` --scope ${JSON.stringify(options.scope)}` : "";
13978
+ }
13979
+ function outputSecretDoctor(result) {
13980
+ if (shouldOutputJson()) {
13981
+ outputJson(result);
13982
+ return;
13983
+ }
13984
+ process.stderr.write(formatSection("Secrets") + "\n");
13985
+ for (const check of result.checks) {
13986
+ process.stdout.write(formatCheck(check.ok, check.name, check.detail) + "\n");
13987
+ if (check.hint) {
13988
+ process.stdout.write(` ${theme.hint(check.hint)}
13989
+ `);
13990
+ }
13991
+ }
13992
+ process.stdout.write("\n");
13993
+ if (result.healthy) {
13994
+ process.stdout.write(theme.success("Secrets checks passed.") + "\n");
13995
+ } else {
13996
+ const failed = result.checks.filter((check) => check.ok === false).length;
13997
+ process.stdout.write(theme.warn(`${failed} secrets check${failed > 1 ? "s" : ""} need attention.`) + "\n");
13998
+ }
13999
+ }
13642
14000
  function registerSecretsCommand(program2) {
13643
14001
  const secrets = program2.command("secrets").description("Encrypted secrets management");
13644
14002
  const network = secrets.command("network").description("Manage the default secrets encryption network");
@@ -13680,6 +14038,93 @@ function registerSecretsCommand(program2) {
13680
14038
  handleError(error);
13681
14039
  }
13682
14040
  });
14041
+ secrets.command("doctor [name]").description("Check secrets setup and optional secret access").option("--scope <scope>", "Logical secret scope").option("--space <scope>", "Deprecated alias for --scope").option("--network <name>", "Encryption network name", "default").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
14042
+ try {
14043
+ const globalOpts = cmd.optsWithGlobals();
14044
+ const ctx = await ProfileManager.resolveContext(globalOpts);
14045
+ const node = await ensureSecretsNode(ctx, options);
14046
+ const networkName = options.network ?? "default";
14047
+ const networkId = networkName.startsWith("urn:tinycloud:encryption:") ? networkName : node.getDefaultEncryptionNetworkId(networkName);
14048
+ const descriptor = await withSpinner(
14049
+ "Checking secrets encryption network...",
14050
+ () => node.getEncryptionNetwork(networkName)
14051
+ );
14052
+ const checks = [
14053
+ descriptor ? {
14054
+ name: "Encryption network",
14055
+ ok: descriptor.state === "active" ? true : "warn",
14056
+ detail: `${networkName} (${descriptor.state})`
14057
+ } : {
14058
+ name: "Encryption network",
14059
+ ok: false,
14060
+ detail: `${networkName} not found`,
14061
+ hint: `tc secrets network init ${networkName}`
14062
+ }
14063
+ ];
14064
+ let secret;
14065
+ if (name) {
14066
+ const scopeOptions = resolveSecretScope(options);
14067
+ const resolved = resolveSecretPath2(name, scopeOptions);
14068
+ const result = await runSecretOperation({
14069
+ ctx,
14070
+ node,
14071
+ action: "get",
14072
+ name,
14073
+ scopeOptions,
14074
+ label: `Checking secret ${name}...`,
14075
+ operation: () => node.secrets.get(name, scopeOptions)
14076
+ });
14077
+ if (result.ok) {
14078
+ secret = {
14079
+ name: resolved.name,
14080
+ path: resolved.permissionPaths.vault,
14081
+ ...resolved.scope ? { scope: resolved.scope } : {},
14082
+ exists: true,
14083
+ readable: true
14084
+ };
14085
+ checks.push({
14086
+ name: "Secret access",
14087
+ ok: true,
14088
+ detail: `${resolved.permissionPaths.vault} readable`
14089
+ });
14090
+ } else {
14091
+ const notFound = result.error.code === "NOT_FOUND" || result.error.code === "KEY_NOT_FOUND";
14092
+ secret = {
14093
+ name: resolved.name,
14094
+ path: resolved.permissionPaths.vault,
14095
+ ...resolved.scope ? { scope: resolved.scope } : {},
14096
+ exists: !notFound,
14097
+ readable: false
14098
+ };
14099
+ checks.push({
14100
+ name: "Secret access",
14101
+ ok: false,
14102
+ detail: notFound ? `${resolved.permissionPaths.vault} not found` : result.error.message,
14103
+ hint: notFound ? `tc secrets put ${resolved.name}${formatSecretScopeFlag(scopeOptions)} <value>` : "Ask the owner profile to grant tinycloud.kv/get and tinycloud.encryption/decrypt."
14104
+ });
14105
+ }
14106
+ } else {
14107
+ checks.push({
14108
+ name: "Secret access",
14109
+ ok: "warn",
14110
+ detail: "skipped; pass a secret name to verify read access"
14111
+ });
14112
+ }
14113
+ outputSecretDoctor({
14114
+ healthy: checks.every((check) => check.ok !== false),
14115
+ network: {
14116
+ name: networkName,
14117
+ networkId,
14118
+ exists: descriptor !== null,
14119
+ ...descriptor?.state ? { state: descriptor.state } : {}
14120
+ },
14121
+ ...secret ? { secret } : {},
14122
+ checks
14123
+ });
14124
+ } catch (error) {
14125
+ handleError(error);
14126
+ }
14127
+ });
13683
14128
  secrets.command("list").description("List secrets").option("--scope <scope>", "Logical secret scope").option("--space <scope>", "Deprecated alias for --scope").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (options, cmd) => {
13684
14129
  try {
13685
14130
  const globalOpts = cmd.optsWithGlobals();
@@ -13731,7 +14176,7 @@ function registerSecretsCommand(program2) {
13731
14176
  })
13732
14177
  );
13733
14178
  if (options.output) {
13734
- await writeFile5(options.output, value2);
14179
+ await writeFile6(options.output, value2);
13735
14180
  outputJson({ name, written: options.output });
13736
14181
  return;
13737
14182
  }
@@ -13760,7 +14205,7 @@ function registerSecretsCommand(program2) {
13760
14205
  }
13761
14206
  const value = String(result.data);
13762
14207
  if (options.output) {
13763
- await writeFile5(options.output, value);
14208
+ await writeFile6(options.output, value);
13764
14209
  outputJson({ name, written: options.output });
13765
14210
  return;
13766
14211
  }
@@ -13867,8 +14312,8 @@ function registerSecretsCommand(program2) {
13867
14312
  });
13868
14313
  secrets.command("manage").description("Open the TinyCloud Secrets Manager in your browser").action(async () => {
13869
14314
  try {
13870
- const open = (await import("open")).default;
13871
- await open("https://secrets.tinycloud.xyz");
14315
+ const open2 = (await import("open")).default;
14316
+ await open2("https://secrets.tinycloud.xyz");
13872
14317
  outputJson({ opened: "https://secrets.tinycloud.xyz" });
13873
14318
  } catch (error) {
13874
14319
  handleError(error);
@@ -13878,7 +14323,7 @@ function registerSecretsCommand(program2) {
13878
14323
 
13879
14324
  // src/commands/vars.ts
13880
14325
  import { readFile as readFile7 } from "fs/promises";
13881
- import { writeFile as writeFile6 } from "fs/promises";
14326
+ import { writeFile as writeFile7 } from "fs/promises";
13882
14327
  var VARIABLES_PREFIX = "variables/";
13883
14328
  async function readStdin5() {
13884
14329
  const chunks = [];
@@ -13950,7 +14395,7 @@ function registerVarsCommand(program2) {
13950
14395
  value = typeof data === "string" ? data : JSON.stringify(data);
13951
14396
  }
13952
14397
  if (options.output) {
13953
- await writeFile6(options.output, value);
14398
+ await writeFile7(options.output, value);
13954
14399
  outputJson({ name, written: options.output });
13955
14400
  return;
13956
14401
  }
@@ -14124,12 +14569,18 @@ function registerDoctorCommand(program2) {
14124
14569
  }
14125
14570
 
14126
14571
  // src/commands/sql.ts
14127
- import { writeFile as writeFile7 } from "fs/promises";
14572
+ import { writeFile as writeFile8 } from "fs/promises";
14128
14573
  import { resolve } from "path";
14129
14574
  async function dbHandle(node, dbName, spaceInput, profileName) {
14130
14575
  const spaceUri = await resolveSpaceUri(spaceInput, profileName);
14131
14576
  const sql = spaceUri ? node.sqlForSpace(spaceUri) : node.sql;
14132
- return sql.db(dbName);
14577
+ return { handle: sql.db(dbName), spaceUri };
14578
+ }
14579
+ async function throwSqlError(error, spaceUri, profileName, prefix) {
14580
+ const hosted = await unhostedSpaceError(error, spaceUri, profileName);
14581
+ if (hosted) throw hosted;
14582
+ const message = prefix ? `${prefix}${error.message}` : error.message;
14583
+ throw new CLIError(error.code, message, ExitCode.ERROR, error.meta);
14133
14584
  }
14134
14585
  function registerSqlCommand(program2) {
14135
14586
  const sql = program2.command("sql").description("SQLite database operations for your TinyCloud space").addHelpText("after", `
@@ -14175,13 +14626,13 @@ Output:
14175
14626
  const ctx = await ProfileManager.resolveContext(globalOpts);
14176
14627
  const node = await ensureAuthenticated(ctx);
14177
14628
  const params = options.params ? JSON.parse(options.params) : void 0;
14178
- const handle = await dbHandle(node, options.db, options.space, ctx.profile);
14629
+ const { handle, spaceUri } = await dbHandle(node, options.db, options.space, ctx.profile);
14179
14630
  const result = await withSpinner(
14180
14631
  "Running query...",
14181
14632
  () => handle.query(sqlStr, params)
14182
14633
  );
14183
14634
  if (!result.ok) {
14184
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR, result.error.meta);
14635
+ await throwSqlError(result.error, spaceUri, ctx.profile);
14185
14636
  }
14186
14637
  const { columns, rows, rowCount } = result.data;
14187
14638
  if (shouldOutputJson()) {
@@ -14219,13 +14670,13 @@ Output:
14219
14670
  const ctx = await ProfileManager.resolveContext(globalOpts);
14220
14671
  const node = await ensureAuthenticated(ctx);
14221
14672
  const params = options.params ? JSON.parse(options.params) : void 0;
14222
- const handle = await dbHandle(node, options.db, options.space, ctx.profile);
14673
+ const { handle, spaceUri } = await dbHandle(node, options.db, options.space, ctx.profile);
14223
14674
  const result = await withSpinner(
14224
14675
  "Executing statement...",
14225
14676
  () => handle.execute(sqlStr, params)
14226
14677
  );
14227
14678
  if (!result.ok) {
14228
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR, result.error.meta);
14679
+ await throwSqlError(result.error, spaceUri, ctx.profile);
14229
14680
  }
14230
14681
  outputJson({
14231
14682
  changes: result.data.changes,
@@ -14249,18 +14700,18 @@ Output:
14249
14700
  const globalOpts = cmd.optsWithGlobals();
14250
14701
  const ctx = await ProfileManager.resolveContext(globalOpts);
14251
14702
  const node = await ensureAuthenticated(ctx);
14252
- const handle = await dbHandle(node, options.db, options.space, ctx.profile);
14703
+ const { handle, spaceUri } = await dbHandle(node, options.db, options.space, ctx.profile);
14253
14704
  const result = await withSpinner(
14254
14705
  "Exporting database...",
14255
14706
  () => handle.export()
14256
14707
  );
14257
14708
  if (!result.ok) {
14258
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR, result.error.meta);
14709
+ await throwSqlError(result.error, spaceUri, ctx.profile);
14259
14710
  }
14260
14711
  const blob = result.data;
14261
14712
  const buffer = Buffer.from(await blob.arrayBuffer());
14262
14713
  const outputPath = resolve(options.output);
14263
- await writeFile7(outputPath, buffer);
14714
+ await writeFile8(outputPath, buffer);
14264
14715
  outputJson({
14265
14716
  file: outputPath,
14266
14717
  size: blob.size,
@@ -14304,8 +14755,8 @@ Notes:
14304
14755
  ExitCode.USAGE_ERROR
14305
14756
  );
14306
14757
  }
14307
- const fromHandle = await dbHandle(node, options.fromDb, fromSpaceInput, ctx.profile);
14308
- const toHandle = await dbHandle(node, options.toDb, toSpaceInput, ctx.profile);
14758
+ const { handle: fromHandle, spaceUri: fromSpaceUriResolved } = await dbHandle(node, options.fromDb, fromSpaceInput, ctx.profile);
14759
+ const { handle: toHandle, spaceUri: toSpaceUriResolved } = await dbHandle(node, options.toDb, toSpaceInput, ctx.profile);
14309
14760
  let tables;
14310
14761
  if (options.table && options.table.length > 0) {
14311
14762
  tables = options.table.flatMap((t) => t.split(",").map((s) => s.trim()).filter(Boolean));
@@ -14314,7 +14765,7 @@ Notes:
14314
14765
  "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
14315
14766
  );
14316
14767
  if (!listing.ok) {
14317
- throw new CLIError(listing.error.code, `Cannot list source tables: ${listing.error.message}`, ExitCode.ERROR, listing.error.meta);
14768
+ await throwSqlError(listing.error, fromSpaceUriResolved, ctx.profile, "Cannot list source tables: ");
14318
14769
  }
14319
14770
  tables = listing.data.rows.map((r) => String(r[0]));
14320
14771
  }
@@ -14330,12 +14781,7 @@ Notes:
14330
14781
  const safe = quoteIdent(table);
14331
14782
  const countResult = await fromHandle.query(`SELECT count(*) AS n FROM ${safe}`);
14332
14783
  if (!countResult.ok) {
14333
- throw new CLIError(
14334
- countResult.error.code,
14335
- `Cannot count rows in source table "${table}": ${countResult.error.message}`,
14336
- ExitCode.ERROR,
14337
- countResult.error.meta
14338
- );
14784
+ await throwSqlError(countResult.error, fromSpaceUriResolved, ctx.profile, `Cannot count rows in source table "${table}": `);
14339
14785
  }
14340
14786
  const rows = Number(countResult.data.rows[0]?.[0] ?? 0);
14341
14787
  plan.push({ table, rows, copied: 0, skipped: 0 });
@@ -14353,7 +14799,7 @@ Notes:
14353
14799
  const safe = quoteIdent(entry.table);
14354
14800
  const fetched = await fromHandle.query(`SELECT * FROM ${safe}`);
14355
14801
  if (!fetched.ok) {
14356
- throw new CLIError(fetched.error.code, `Failed to read "${entry.table}": ${fetched.error.message}`, ExitCode.ERROR, fetched.error.meta);
14802
+ await throwSqlError(fetched.error, fromSpaceUriResolved, ctx.profile, `Failed to read "${entry.table}": `);
14357
14803
  }
14358
14804
  const columns = fetched.data.columns;
14359
14805
  const rows = fetched.data.rows;
@@ -14364,12 +14810,7 @@ Notes:
14364
14810
  for (const row of rows) {
14365
14811
  const writeResult = await toHandle.execute(insertSql, row);
14366
14812
  if (!writeResult.ok) {
14367
- throw new CLIError(
14368
- writeResult.error.code,
14369
- `Insert into "${entry.table}" failed after ${entry.copied} row(s): ${writeResult.error.message}`,
14370
- ExitCode.ERROR,
14371
- writeResult.error.meta
14372
- );
14813
+ await throwSqlError(writeResult.error, toSpaceUriResolved, ctx.profile, `Insert into "${entry.table}" failed after ${entry.copied} row(s): `);
14373
14814
  }
14374
14815
  entry.copied += writeResult.data.changes ?? 1;
14375
14816
  }
@@ -14389,7 +14830,7 @@ function quoteIdent(name) {
14389
14830
  }
14390
14831
 
14391
14832
  // src/commands/duckdb.ts
14392
- import { readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
14833
+ import { readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
14393
14834
  import { resolve as resolve2 } from "path";
14394
14835
  function registerDuckdbCommand(program2) {
14395
14836
  const duckdb = program2.command("duckdb").description("DuckDB database operations");
@@ -14503,7 +14944,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
14503
14944
  const blob = result.data;
14504
14945
  const buffer = Buffer.from(await blob.arrayBuffer());
14505
14946
  const outputPath = resolve2(options.output);
14506
- await writeFile8(outputPath, buffer);
14947
+ await writeFile9(outputPath, buffer);
14507
14948
  outputJson({
14508
14949
  file: outputPath,
14509
14950
  size: blob.size,
@@ -15088,6 +15529,310 @@ function plural(count, label) {
15088
15529
  return `${count} ${label}${count === 1 ? "" : "s"}`;
15089
15530
  }
15090
15531
 
15532
+ // src/commands/account.ts
15533
+ import open from "open";
15534
+ import { readFile as readFile10 } from "fs/promises";
15535
+ var ACCOUNT_BILLING_URL = "https://account.tinycloud.xyz/billing";
15536
+ function registerAccountCommand(program2) {
15537
+ const account = program2.command("account").description("Account applications, spaces, delegations, and billing");
15538
+ account.command("status").description("Show account status").action(async (_options, cmd) => {
15539
+ try {
15540
+ const node = await authenticatedNode(cmd);
15541
+ const status = await node.account.status();
15542
+ assertOk(status);
15543
+ outputJson(status.data);
15544
+ } catch (error) {
15545
+ handleError(error);
15546
+ }
15547
+ });
15548
+ const apps = account.command("apps").description("Manage account application registry");
15549
+ apps.command("list").description("List applications registered under account/applications").option("--live", "Read canonical account KV records instead of the SQLite index").action(async (_options, cmd) => {
15550
+ try {
15551
+ const options = _options;
15552
+ const node = await authenticatedNode(cmd);
15553
+ const result = options.live ? await node.account.applications.list() : await node.account.index.applications.list();
15554
+ assertOk(result);
15555
+ const payload = { applications: result.data, count: result.data.length };
15556
+ if (shouldOutputJson()) {
15557
+ outputJson(payload);
15558
+ return;
15559
+ }
15560
+ if (result.data.length === 0) {
15561
+ process.stdout.write(theme.muted("No account applications registered.") + "\n");
15562
+ return;
15563
+ }
15564
+ process.stdout.write(
15565
+ formatTable(
15566
+ ["App ID", "Name", "Manifests", "Updated"],
15567
+ result.data.map((app) => [
15568
+ app.appId,
15569
+ app.name ?? "\u2014",
15570
+ String(app.manifests.length),
15571
+ app.updatedAt ?? "\u2014"
15572
+ ])
15573
+ ) + "\n"
15574
+ );
15575
+ } catch (error) {
15576
+ handleError(error);
15577
+ }
15578
+ });
15579
+ const spaces = account.command("spaces").description("Manage account space registry");
15580
+ spaces.command("list").description("List spaces registered under account/spaces").option("--live", "Read canonical account KV records instead of the SQLite index").action(async (options, cmd) => {
15581
+ try {
15582
+ const node = await authenticatedNode(cmd);
15583
+ const result = options.live ? await node.account.spaces.list() : await node.account.index.spaces.list();
15584
+ assertOk(result);
15585
+ const payload = { spaces: result.data.map(formatSpace), count: result.data.length };
15586
+ if (shouldOutputJson()) {
15587
+ outputJson(payload);
15588
+ return;
15589
+ }
15590
+ if (result.data.length === 0) {
15591
+ process.stdout.write(theme.muted("No account spaces registered.") + "\n");
15592
+ return;
15593
+ }
15594
+ process.stdout.write(
15595
+ formatTable(
15596
+ ["Space", "Type", "Owner", "Status", "Updated"],
15597
+ result.data.map((space) => [
15598
+ space.name,
15599
+ space.type,
15600
+ space.ownerDid,
15601
+ space.status,
15602
+ space.updatedAt ?? "\u2014"
15603
+ ])
15604
+ ) + "\n"
15605
+ );
15606
+ } catch (error) {
15607
+ handleError(error);
15608
+ }
15609
+ });
15610
+ spaces.command("info <space-id>").description("Show a registered account space").action(async (spaceId, _options, cmd) => {
15611
+ try {
15612
+ const node = await authenticatedNode(cmd);
15613
+ const result = await node.account.spaces.get(spaceId);
15614
+ assertOk(result);
15615
+ outputJson(formatSpace(result.data));
15616
+ } catch (error) {
15617
+ handleError(error);
15618
+ }
15619
+ });
15620
+ spaces.command("register <space-id>").description("Register a space in account/spaces").requiredOption("--name <name>", "Display name for the space").requiredOption("--owner <did>", "Owner DID for the space").option("--type <type>", "Space type: owned, delegated, or discovered", "discovered").option("--permission <permission...>", "Permission strings for the space").action(async (spaceId, options, cmd) => {
15621
+ try {
15622
+ const node = await authenticatedNode(cmd);
15623
+ const result = await node.account.spaces.register({
15624
+ spaceId,
15625
+ name: options.name,
15626
+ ownerDid: options.owner,
15627
+ type: options.type,
15628
+ permissions: options.permission ?? [],
15629
+ status: "active"
15630
+ });
15631
+ assertOk(result);
15632
+ outputJson({ space: formatSpace(result.data), registered: true });
15633
+ } catch (error) {
15634
+ handleError(error);
15635
+ }
15636
+ });
15637
+ spaces.command("sync").description("Register currently accessible spaces into account/spaces").action(async (_options, cmd) => {
15638
+ try {
15639
+ const node = await authenticatedNode(cmd);
15640
+ const result = await node.account.spaces.syncAccessible();
15641
+ assertOk(result);
15642
+ outputJson({ spaces: result.data.map(formatSpace), count: result.data.length, synced: true });
15643
+ } catch (error) {
15644
+ handleError(error);
15645
+ }
15646
+ });
15647
+ spaces.command("remove <space-id>").alias("delete").description("Remove a space registry entry").action(async (spaceId, _options, cmd) => {
15648
+ try {
15649
+ const node = await authenticatedNode(cmd);
15650
+ const result = await node.account.spaces.remove(spaceId);
15651
+ assertOk(result);
15652
+ outputJson({ spaceId, removed: true });
15653
+ } catch (error) {
15654
+ handleError(error);
15655
+ }
15656
+ });
15657
+ apps.command("info <app-id>").description("Show a registered account application").action(async (appId, _options, cmd) => {
15658
+ try {
15659
+ const node = await authenticatedNode(cmd);
15660
+ const result = await node.account.applications.get(appId);
15661
+ assertOk(result);
15662
+ outputJson(result.data);
15663
+ } catch (error) {
15664
+ handleError(error);
15665
+ }
15666
+ });
15667
+ apps.command("register <manifest>").description("Register an app manifest in account/applications").action(async (manifestSource, _options, cmd) => {
15668
+ try {
15669
+ const node = await authenticatedNode(cmd);
15670
+ const manifest = await loadManifestSource2(manifestSource);
15671
+ const result = await node.account.applications.register(manifest);
15672
+ assertOk(result);
15673
+ outputJson({ application: result.data, registered: true });
15674
+ } catch (error) {
15675
+ handleError(error);
15676
+ }
15677
+ });
15678
+ apps.command("remove <app-id>").alias("delete").description("Remove an application registry entry").action(async (appId, _options, cmd) => {
15679
+ try {
15680
+ const node = await authenticatedNode(cmd);
15681
+ const result = await node.account.applications.remove(appId);
15682
+ assertOk(result);
15683
+ outputJson({ appId, removed: true });
15684
+ } catch (error) {
15685
+ handleError(error);
15686
+ }
15687
+ });
15688
+ const delegations = account.command("delegations").description("View and revoke account delegations");
15689
+ delegations.command("list").description("List delegations granted by or to this account").option("--granted", "Show only delegations granted by this account").option("--received", "Show only delegations granted to this account").option("--space <space>", "Filter by space name or ID").option("--live", "Read live delegation services instead of the SQLite index").action(async (options, cmd) => {
15690
+ try {
15691
+ if (options.granted && options.received) {
15692
+ throw new CLIError("USAGE_ERROR", "Use only one of --granted or --received.", ExitCode.USAGE_ERROR);
15693
+ }
15694
+ const node = await authenticatedNode(cmd);
15695
+ const direction = options.granted ? "granted" : options.received ? "received" : "all";
15696
+ const result = options.live ? await node.account.delegations.list({ direction, space: options.space }) : await node.account.index.delegations.list({ direction, space: options.space });
15697
+ assertOk(result);
15698
+ const payload = { delegations: result.data.map(formatDelegation2), count: result.data.length };
15699
+ if (shouldOutputJson()) {
15700
+ outputJson(payload);
15701
+ return;
15702
+ }
15703
+ if (result.data.length === 0) {
15704
+ process.stdout.write(theme.muted("No delegations found.") + "\n");
15705
+ return;
15706
+ }
15707
+ process.stdout.write(
15708
+ formatTable(
15709
+ ["CID", "Direction", "Space", "Counterparty", "Status", "Expiry"],
15710
+ result.data.map((delegation) => [
15711
+ delegation.cid,
15712
+ delegation.direction,
15713
+ delegation.spaceName ?? delegation.spaceId,
15714
+ delegation.counterpartyDid,
15715
+ delegation.status,
15716
+ delegation.expiry.toISOString()
15717
+ ])
15718
+ ) + "\n"
15719
+ );
15720
+ } catch (error) {
15721
+ handleError(error);
15722
+ }
15723
+ });
15724
+ delegations.command("revoke <cid>").description("Revoke an active delegation granted by this account").requiredOption("--space <space>", "Space name or ID containing the delegation").action(async (cid, options, cmd) => {
15725
+ try {
15726
+ const node = await authenticatedNode(cmd);
15727
+ const result = await node.account.delegations.revoke({ cid, space: options.space });
15728
+ assertOk(result);
15729
+ outputJson({ cid, space: options.space, revoked: true });
15730
+ } catch (error) {
15731
+ handleError(error);
15732
+ }
15733
+ });
15734
+ const index = account.command("index").description("Manage the materialized account SQLite index");
15735
+ index.command("status").description("Show account SQLite index sync status").action(async (_options, cmd) => {
15736
+ try {
15737
+ const node = await authenticatedNode(cmd);
15738
+ const result = await node.account.index.status();
15739
+ assertOk(result);
15740
+ outputJson(result.data);
15741
+ } catch (error) {
15742
+ handleError(error);
15743
+ }
15744
+ });
15745
+ index.command("rebuild").description("Rebuild account SQLite index from canonical account data").action(async (_options, cmd) => {
15746
+ try {
15747
+ const node = await authenticatedNode(cmd);
15748
+ const result = await node.account.index.rebuild();
15749
+ assertOk(result);
15750
+ outputJson(result.data);
15751
+ } catch (error) {
15752
+ handleError(error);
15753
+ }
15754
+ });
15755
+ index.command("query <sql>").description("Query the materialized account SQLite index").option("--params <json>", "Bind parameters as a JSON array for ? placeholders").action(async (sql, options, cmd) => {
15756
+ try {
15757
+ const node = await authenticatedNode(cmd);
15758
+ const params = parseParams(options.params);
15759
+ const result = await node.account.index.query(sql, params);
15760
+ assertOk(result);
15761
+ outputJson(result.data);
15762
+ } catch (error) {
15763
+ handleError(error);
15764
+ }
15765
+ });
15766
+ const billing = account.command("billing").description("Open account billing");
15767
+ for (const name of ["status", "checkout", "portal"]) {
15768
+ billing.command(name).description(`${name === "status" ? "Show" : "Open"} account billing page`).option("--open", "Open account.tinycloud.xyz in your browser").action(async (options) => {
15769
+ try {
15770
+ if (options.open) {
15771
+ await open(ACCOUNT_BILLING_URL);
15772
+ }
15773
+ outputJson({ url: ACCOUNT_BILLING_URL, opened: Boolean(options.open) });
15774
+ } catch (error) {
15775
+ handleError(error);
15776
+ }
15777
+ });
15778
+ }
15779
+ }
15780
+ async function authenticatedNode(cmd) {
15781
+ const globalOpts = cmd.optsWithGlobals();
15782
+ const ctx = await ProfileManager.resolveContext(globalOpts);
15783
+ return ensureAuthenticated(ctx);
15784
+ }
15785
+ function assertOk(result) {
15786
+ if (!result.ok) {
15787
+ throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
15788
+ }
15789
+ }
15790
+ async function loadManifestSource2(source) {
15791
+ const raw = /^https?:\/\//i.test(source) ? await fetchManifest(source) : await readFile10(source, "utf8");
15792
+ return JSON.parse(raw);
15793
+ }
15794
+ async function fetchManifest(source) {
15795
+ const response = await fetch(source);
15796
+ if (!response.ok) {
15797
+ throw new CLIError(
15798
+ "MANIFEST_FETCH_FAILED",
15799
+ `Failed to fetch manifest from ${source}: ${response.status} ${response.statusText}`,
15800
+ ExitCode.NETWORK_ERROR
15801
+ );
15802
+ }
15803
+ return response.text();
15804
+ }
15805
+ function parseParams(input) {
15806
+ if (!input) return void 0;
15807
+ const parsed = JSON.parse(input);
15808
+ if (!Array.isArray(parsed)) {
15809
+ throw new CLIError("INVALID_PARAMS", "--params must be a JSON array.", ExitCode.USAGE_ERROR);
15810
+ }
15811
+ return parsed;
15812
+ }
15813
+ function formatDelegation2(delegation) {
15814
+ return {
15815
+ cid: delegation.cid,
15816
+ direction: delegation.direction,
15817
+ spaceId: delegation.spaceId,
15818
+ spaceName: delegation.spaceName,
15819
+ counterpartyDid: delegation.counterpartyDid,
15820
+ delegateDid: delegation.delegateDid,
15821
+ delegatorDid: delegation.delegatorDid,
15822
+ path: delegation.path,
15823
+ actions: delegation.actions,
15824
+ expiry: delegation.expiry.toISOString(),
15825
+ status: delegation.status,
15826
+ createdAt: delegation.createdAt?.toISOString()
15827
+ };
15828
+ }
15829
+ function formatSpace(space) {
15830
+ return {
15831
+ ...space,
15832
+ expiresAt: space.expiresAt?.toISOString()
15833
+ };
15834
+ }
15835
+
15091
15836
  // src/index.ts
15092
15837
  var { version } = JSON.parse(
15093
15838
  readFileSync3(new URL("../package.json", import.meta.url), "utf-8")
@@ -15138,6 +15883,7 @@ registerDuckdbCommand(program);
15138
15883
  registerManifestCommand(program);
15139
15884
  registerUpgradeCommand(program);
15140
15885
  registerStatusCommand(program);
15886
+ registerAccountCommand(program);
15141
15887
  program.addHelpText("before", () => `${theme.label("Version:")} ${theme.value(version)}
15142
15888
  `);
15143
15889
  program.addHelpText("afterAll", () => {
@@ -15149,6 +15895,7 @@ ${theme.heading("Examples:")}
15149
15895
  ${theme.command('tc kv put greeting "Hello"')} ${theme.muted("Store a value")}
15150
15896
  ${theme.command("tc kv list")} ${theme.muted("List all keys")}
15151
15897
  ${theme.command("tc secrets network init")} ${theme.muted("Create the default secrets network")}
15898
+ ${theme.command("tc account apps list")} ${theme.muted("List registered account apps")}
15152
15899
  ${theme.command("tc delegation create --to did:pkh:...")} ${theme.muted("Grant access to another user")}
15153
15900
  ${theme.command("tc space list")} ${theme.muted("Show your spaces")}
15154
15901