@tinycloud/cli 1.5.0-beta.2 → 1.5.0-beta.21

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.
@@ -174,6 +174,35 @@ var init_storage = __esm({
174
174
  }
175
175
  });
176
176
 
177
+ // src/config/types.ts
178
+ function isCLIProfilePosture(value) {
179
+ return typeof value === "string" && CLI_PROFILE_POSTURES.includes(value);
180
+ }
181
+ function isCLIOperatorType(value) {
182
+ return typeof value === "string" && CLI_OPERATOR_TYPES.includes(value);
183
+ }
184
+ function resolveProfilePosture(profile) {
185
+ if (isCLIProfilePosture(profile.posture)) return profile.posture;
186
+ if (profile.authMethod === "local") return "local-owner-key";
187
+ return "owner-openkey";
188
+ }
189
+ function resolveProfileOperatorType(profile) {
190
+ if (isCLIOperatorType(profile.operatorType)) return profile.operatorType;
191
+ return "human";
192
+ }
193
+ var CLI_PROFILE_POSTURES, CLI_OPERATOR_TYPES;
194
+ var init_types = __esm({
195
+ "src/config/types.ts"() {
196
+ "use strict";
197
+ CLI_PROFILE_POSTURES = [
198
+ "owner-openkey",
199
+ "delegate-session",
200
+ "local-owner-key"
201
+ ];
202
+ CLI_OPERATOR_TYPES = ["human", "agent"];
203
+ }
204
+ });
205
+
177
206
  // src/lib/replication-registry.ts
178
207
  function stopForSignal(signal) {
179
208
  void closeReplication().finally(() => {
@@ -903,35 +932,6 @@ var init_errors = __esm({
903
932
  }
904
933
  });
905
934
 
906
- // src/config/types.ts
907
- function isCLIProfilePosture(value) {
908
- return typeof value === "string" && CLI_PROFILE_POSTURES.includes(value);
909
- }
910
- function isCLIOperatorType(value) {
911
- return typeof value === "string" && CLI_OPERATOR_TYPES.includes(value);
912
- }
913
- function resolveProfilePosture(profile) {
914
- if (isCLIProfilePosture(profile.posture)) return profile.posture;
915
- if (profile.authMethod === "local") return "local-owner-key";
916
- return "owner-openkey";
917
- }
918
- function resolveProfileOperatorType(profile) {
919
- if (isCLIOperatorType(profile.operatorType)) return profile.operatorType;
920
- return "human";
921
- }
922
- var CLI_PROFILE_POSTURES, CLI_OPERATOR_TYPES;
923
- var init_types = __esm({
924
- "src/config/types.ts"() {
925
- "use strict";
926
- CLI_PROFILE_POSTURES = [
927
- "owner-openkey",
928
- "delegate-session",
929
- "local-owner-key"
930
- ];
931
- CLI_OPERATOR_TYPES = ["human", "agent"];
932
- }
933
- });
934
-
935
935
  // src/lib/space.ts
936
936
  import { CLI_NAME as CLI_NAME3 } from "@tinycloud/operations/cli-name";
937
937
  function invalidSpaceError(input) {
@@ -1857,7 +1857,8 @@ var init_sha256 = __esm({
1857
1857
  });
1858
1858
 
1859
1859
  // ../sdk-core/src/manifest.ts
1860
- import { resolveSecretPath, SECRET_NAME_RE } from "@tinycloud/sdk-services";
1860
+ import { resolveSecretPath, SECRET_NAME_RE, parseNetworkId } from "@tinycloud/sdk-services";
1861
+ import { parseEncryptionResource } from "@tinycloud/sdk-services/encryption/resource";
1861
1862
  function parseExpiry(duration) {
1862
1863
  if (typeof duration !== "string" || duration.length === 0) {
1863
1864
  throw new ManifestValidationError(
@@ -1900,6 +1901,14 @@ function expandEncryptionPermissionEntry(entry) {
1900
1901
  `tinycloud.encryption entries require path to be a networkId URN (got ${JSON.stringify(entry.path)})`
1901
1902
  );
1902
1903
  }
1904
+ if (entry.path.includes("/")) {
1905
+ throw new ManifestValidationError("named keys go in manifest.keys");
1906
+ }
1907
+ try {
1908
+ parseNetworkId(entry.path);
1909
+ } catch {
1910
+ throw new ManifestValidationError("tinycloud.encryption entries require a valid networkId URN");
1911
+ }
1903
1912
  const normalizedActions = [];
1904
1913
  for (const action of entry.actions) {
1905
1914
  if (action === "decrypt" || action === "tinycloud.encryption/decrypt") {
@@ -2004,8 +2013,52 @@ function validateManifest(input) {
2004
2013
  if (m.secrets !== void 0) {
2005
2014
  validateManifestSecrets(m.secrets);
2006
2015
  }
2016
+ if (Object.prototype.hasOwnProperty.call(m, "keys_app_id") && (typeof m.keys_app_id !== "string" || !KEY_APP_ID.test(m.keys_app_id))) {
2017
+ throw new ManifestValidationError("manifest.keys_app_id is invalid");
2018
+ }
2019
+ validateManifestKeys(m.keys_app_id ?? m.app_id, m.keys);
2007
2020
  return m;
2008
2021
  }
2022
+ function validateManifestKeys(appId, keys) {
2023
+ if (keys === void 0) return;
2024
+ if (!Array.isArray(keys) || keys.length > 32) {
2025
+ throw new ManifestValidationError("manifest.keys must contain at most 32 entries");
2026
+ }
2027
+ if (keys.length === 0) return;
2028
+ if (!KEY_APP_ID.test(appId)) {
2029
+ throw new ManifestValidationError("manifest.keys requires a valid app_id");
2030
+ }
2031
+ const paths = /* @__PURE__ */ new Set();
2032
+ let prefixSeen = false;
2033
+ for (const [index, value] of keys.entries()) {
2034
+ const label = `manifest.keys[${index}]`;
2035
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
2036
+ throw new ManifestValidationError(`${label} must be an object`);
2037
+ }
2038
+ const entry = value;
2039
+ const fields = Object.keys(entry);
2040
+ const hasPath = Object.prototype.hasOwnProperty.call(entry, "path");
2041
+ const hasPrefix = Object.prototype.hasOwnProperty.call(entry, "prefix");
2042
+ if (hasPath === hasPrefix || fields.some((field) => !["path", "prefix", "abilities"].includes(field))) {
2043
+ throw new ManifestValidationError(`${label} must have one path or prefix and abilities`);
2044
+ }
2045
+ if (!Array.isArray(entry.abilities) || entry.abilities.length === 0 || new Set(entry.abilities).size !== entry.abilities.length || entry.abilities.some((ability) => typeof ability !== "string" || !KEY_ABILITIES.includes(ability))) {
2046
+ throw new ManifestValidationError(`${label}.abilities is invalid`);
2047
+ }
2048
+ if (hasPath) {
2049
+ const path = entry.path;
2050
+ if (typeof path !== "string" || path.length > 255 || !KEY_PATH.test(path) || !path.startsWith(`apps/${appId}/`) || paths.has(path)) {
2051
+ throw new ManifestValidationError(`${label}.path is invalid`);
2052
+ }
2053
+ paths.add(path);
2054
+ } else {
2055
+ if (entry.prefix !== `apps/${appId}/` || prefixSeen) {
2056
+ throw new ManifestValidationError(`${label}.prefix is invalid`);
2057
+ }
2058
+ prefixSeen = true;
2059
+ }
2060
+ }
2061
+ }
2009
2062
  function resolveManifestKnowledgeRoot(knowledge) {
2010
2063
  if (knowledge === void 0) {
2011
2064
  return void 0;
@@ -2405,7 +2458,7 @@ function withCapabilitiesReadForSpaces(resources) {
2405
2458
  ...[...spaces].map(capabilitiesReadPermission)
2406
2459
  ]);
2407
2460
  }
2408
- var import_ms, DEFAULT_KNOWLEDGE_ROOT, ManifestValidationError, DEFAULT_EXPIRY, DEFAULT_DEFAULTS, DEFAULT_MANIFEST_VERSION, DEFAULT_MANIFEST_SPACE, SECRETS_SPACE, VAULT_PERMISSION_SERVICE, SERVICE_SHORT_TO_LONG, ENCRYPTION_PERMISSION_SERVICE, ENCRYPTION_MANIFEST_SPACE, SERVICE_LONG_TO_SHORT2, DEFAULT_STANDARD_ENTRIES, DEFAULT_ADMIN_ENTRIES, DEFAULT_ALL_ENTRIES;
2461
+ var import_ms, DEFAULT_KNOWLEDGE_ROOT, ManifestValidationError, DEFAULT_EXPIRY, DEFAULT_DEFAULTS, DEFAULT_MANIFEST_VERSION, DEFAULT_MANIFEST_SPACE, SECRETS_SPACE, VAULT_PERMISSION_SERVICE, SERVICE_SHORT_TO_LONG, ENCRYPTION_PERMISSION_SERVICE, ENCRYPTION_MANIFEST_SPACE, SERVICE_LONG_TO_SHORT2, DEFAULT_STANDARD_ENTRIES, DEFAULT_ADMIN_ENTRIES, DEFAULT_ALL_ENTRIES, KEY_SEGMENT, KEY_APP_ID, KEY_PATH, KEY_ABILITIES;
2409
2462
  var init_manifest = __esm({
2410
2463
  "../sdk-core/src/manifest.ts"() {
2411
2464
  "use strict";
@@ -2488,6 +2541,10 @@ var init_manifest = __esm({
2488
2541
  actions: ["read", "write"]
2489
2542
  }
2490
2543
  ];
2544
+ KEY_SEGMENT = "[a-z0-9][a-z0-9._-]{0,63}";
2545
+ KEY_APP_ID = new RegExp(`^${KEY_SEGMENT}$`);
2546
+ KEY_PATH = new RegExp(`^${KEY_SEGMENT}(?:/${KEY_SEGMENT}){2,7}$`);
2547
+ KEY_ABILITIES = ["key.create", "decrypt", "key.block", "key.unblock"];
2491
2548
  }
2492
2549
  });
2493
2550
 
@@ -5203,6 +5260,9 @@ function assertReadable(authority, now) {
5203
5260
  function contentHash(bytes) {
5204
5261
  return bytesToHex(blake3(bytes));
5205
5262
  }
5263
+ function repairId(entry) {
5264
+ return `${entry.hash}\0${entry.key}`;
5265
+ }
5206
5266
  function sameEtag(a, b) {
5207
5267
  return a.replace(/^"|"$/g, "") === b.replace(/^"|"$/g, "");
5208
5268
  }
@@ -5518,8 +5578,8 @@ function fetched(key, result, request) {
5518
5578
  }
5519
5579
  return { bytes: result.data.data, etag: result.data.headers.etag ?? "" };
5520
5580
  }
5521
- function kvSyncTransport(kv, options = {}) {
5522
- const host = options.host ?? kv.host;
5581
+ function kvSyncTransport(kv2, options = {}) {
5582
+ const host = options.host ?? kv2.host;
5523
5583
  const fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
5524
5584
  return {
5525
5585
  async syncPage({ prefix, cursor, limit, retentionGrant, signal }) {
@@ -5538,7 +5598,7 @@ function kvSyncTransport(kv, options = {}) {
5538
5598
  );
5539
5599
  }
5540
5600
  }
5541
- const result = await kv.changes({
5601
+ const result = await kv2.changes({
5542
5602
  prefix,
5543
5603
  limit,
5544
5604
  ...cursor === void 0 ? {} : { cursor },
@@ -5553,7 +5613,7 @@ function kvSyncTransport(kv, options = {}) {
5553
5613
  if (keys.length === 0) return contents;
5554
5614
  const signal = options2?.signal;
5555
5615
  const ordered = [...keys].sort(compareKeyBytes);
5556
- const batch = await kv.batchGet(ordered, { binary: true, ...signal === void 0 ? {} : { signal } });
5616
+ const batch = await kv2.batchGet(ordered, { binary: true, ...signal === void 0 ? {} : { signal } });
5557
5617
  if (batch.ok) {
5558
5618
  for (const item of batch.data.results) {
5559
5619
  const result = item.result;
@@ -5566,13 +5626,13 @@ function kvSyncTransport(kv, options = {}) {
5566
5626
  for (const key of keys) {
5567
5627
  if (contents.has(key)) continue;
5568
5628
  const request = `Failed to get key ${JSON.stringify(key)}`;
5569
- contents.set(key, fetched(key, await kv.get(key, { binary: true, ...signal === void 0 ? {} : { signal } }), request));
5629
+ contents.set(key, fetched(key, await kv2.get(key, { binary: true, ...signal === void 0 ? {} : { signal } }), request));
5570
5630
  }
5571
5631
  return contents;
5572
5632
  }
5573
5633
  };
5574
5634
  }
5575
- var ReplicaErrorCode, REPLICA_ERROR, ReplicaError, RFC3339, DEFAULT_PAGE_LIMIT, DEFAULT_LEASE_TTL_MS, FETCH_CHUNK, REPAIR_BATCH, RETENTION_REVOKED, PENDING_REFUSALS, Replica, BASE58, BASE32, featureCache, REVOKED_CODES, REVOKED_BODY, encoder;
5635
+ var ReplicaErrorCode, REPLICA_ERROR, ReplicaError, RFC3339, DEFAULT_PAGE_LIMIT, DEFAULT_LEASE_TTL_MS, FETCH_CHUNK, REPAIR_BATCH, REPAIR_MAX_TRIES, REPAIR_BACKOFF_BASE_MS, RETENTION_REVOKED, PENDING_REFUSALS, Replica, BASE58, BASE32, featureCache, REVOKED_CODES, REVOKED_BODY, encoder;
5576
5636
  var init_dist = __esm({
5577
5637
  "../replica/dist/index.js"() {
5578
5638
  "use strict";
@@ -5660,6 +5720,8 @@ var init_dist = __esm({
5660
5720
  DEFAULT_LEASE_TTL_MS = 12e4;
5661
5721
  FETCH_CHUNK = 100;
5662
5722
  REPAIR_BATCH = 100;
5723
+ REPAIR_MAX_TRIES = 5;
5724
+ REPAIR_BACKOFF_BASE_MS = 6e4;
5663
5725
  RETENTION_REVOKED = /* @__PURE__ */ new Set(["retention-grant-revoked", "retention-grant-ancestor-revoked"]);
5664
5726
  PENDING_REFUSALS = /* @__PURE__ */ new Set([
5665
5727
  ReplicaErrorCode.GRANT_UNAUTHORIZED,
@@ -5672,6 +5734,9 @@ var init_dist = __esm({
5672
5734
  #now;
5673
5735
  #leaseTtlMs;
5674
5736
  #onCommit;
5737
+ #contentFetch;
5738
+ /** Failed repair fetches of this session, by key and ETag hash. Never persisted: a new `Replica` retries afresh. */
5739
+ #repairTries = /* @__PURE__ */ new Map();
5675
5740
  constructor(options) {
5676
5741
  this.#store = options.store;
5677
5742
  const transport = options.transport;
@@ -5679,6 +5744,7 @@ var init_dist = __esm({
5679
5744
  this.#now = options.now ?? Date.now;
5680
5745
  this.#leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
5681
5746
  this.#onCommit = options.onCommit;
5747
+ this.#contentFetch = options.contentFetch ?? "eager";
5682
5748
  }
5683
5749
  async #state() {
5684
5750
  const state2 = await this.#store.open();
@@ -6007,6 +6073,7 @@ var init_dist = __esm({
6007
6073
  changes[candidate.index].content = true;
6008
6074
  return false;
6009
6075
  });
6076
+ if (this.#contentFetch === "lazy") return { changes, blobs, fetched: 0 };
6010
6077
  let fetched2 = 0;
6011
6078
  for (let start = 0; start < needed.length; start += FETCH_CHUNK) {
6012
6079
  const chunk = needed.slice(start, start + FETCH_CHUNK);
@@ -6026,7 +6093,15 @@ var init_dist = __esm({
6026
6093
  return { changes, blobs, fetched: fetched2 };
6027
6094
  }
6028
6095
  async #repair(attempt, cursor, nodeDid, coverage, retentionGrantCid) {
6029
- const pending = await this.#store.pendingRepairs(Number.MAX_SAFE_INTEGER);
6096
+ if (this.#contentFetch === "lazy") return 0;
6097
+ const all = await this.#store.pendingRepairs(Number.MAX_SAFE_INTEGER);
6098
+ const now = this.#now();
6099
+ const pendingIds = new Set(all.map((entry) => repairId(entry)));
6100
+ for (const id of this.#repairTries.keys()) if (!pendingIds.has(id)) this.#repairTries.delete(id);
6101
+ const pending = all.filter((entry) => {
6102
+ const state2 = this.#repairTries.get(repairId(entry));
6103
+ return state2 === void 0 || state2.tries < REPAIR_MAX_TRIES && now >= state2.nextAttemptAt;
6104
+ });
6030
6105
  let repaired = 0;
6031
6106
  for (let start = 0; start < pending.length; start += REPAIR_BATCH) {
6032
6107
  const chunk = pending.slice(start, start + REPAIR_BATCH);
@@ -6038,7 +6113,14 @@ var init_dist = __esm({
6038
6113
  const changes = [];
6039
6114
  for (const entry of chunk) {
6040
6115
  const bytes = acceptContent(entry, contents.get(entry.key));
6041
- if (bytes === void 0) continue;
6116
+ const id = repairId(entry);
6117
+ if (bytes === void 0) {
6118
+ const tries = (this.#repairTries.get(id)?.tries ?? 0) + 1;
6119
+ const wait = tries === 1 ? 0 : REPAIR_BACKOFF_BASE_MS * 2 ** (tries - 2);
6120
+ this.#repairTries.set(id, { tries, nextAttemptAt: this.#now() + wait });
6121
+ continue;
6122
+ }
6123
+ this.#repairTries.delete(id);
6042
6124
  blobs.set(entry.hash, bytes);
6043
6125
  changes.push({ ...entry, content: true });
6044
6126
  }
@@ -9760,6 +9842,7 @@ var init_profiles = __esm({
9760
9842
  "use strict";
9761
9843
  init_constants();
9762
9844
  init_storage();
9845
+ init_types();
9763
9846
  init_errors();
9764
9847
  ProfileManager = class _ProfileManager {
9765
9848
  // ── Initialization ──────────────────────────────────────────────────
@@ -10041,6 +10124,7 @@ var init_profiles = __esm({
10041
10124
  quiet: options.quiet ?? false,
10042
10125
  replication,
10043
10126
  replicationDebug: options.replicationDebug === true || process.env.TC_REPLICATION_DEBUG === "1",
10127
+ ...profileConfig === void 0 ? {} : { posture: resolveProfilePosture(profileConfig) },
10044
10128
  ...localDiscovery === void 0 ? {} : { localDiscovery }
10045
10129
  };
10046
10130
  }
@@ -15794,7 +15878,7 @@ async function refusal(response) {
15794
15878
  async function prepareLinkRecord(input) {
15795
15879
  const apiOrigin2 = checkApiOrigin(input.apiOrigin);
15796
15880
  const nowMs = toEpochMs(input.now);
15797
- const expiresMs = input.expiresAt.getTime();
15881
+ const expiresMs = Math.floor(input.expiresAt.getTime() / 1e3) * 1e3;
15798
15882
  if (!Number.isFinite(nowMs) || !Number.isFinite(expiresMs)) throw new LinkError("invalid_argument", "now and expiresAt must be valid dates");
15799
15883
  let blob;
15800
15884
  let key;
@@ -29060,12 +29144,12 @@ async function verifyEnvelopeV3(envelope, options) {
29060
29144
  const nativeProjection2 = sortedCapabilities.map((capability) => capability.kind === "encryption" ? { service: "tinycloud.encryption", space: capability.resource, path: capability.resource, actions: [capability.action] } : { service: "tinycloud.kv", space: capability.resource.slice(0, capability.resource.indexOf("/kv/")), path: capability.resource.split("/kv/")[1], actions: [...capability.actions], caveat: { type: "xyz.tinycloud.resource/selector", kind: capability.selector, value: capability.resource } }).sort((left, right) => canonicalize2(left).localeCompare(canonicalize2(right)));
29061
29145
  const nativeProjectionHashHex = hex(sha2562(new TextEncoder().encode(`${NATIVE_PROJECTION_V1_DOMAIN}${canonicalize2(nativeProjection2)}`)));
29062
29146
  const expectedAttenuation = Object.fromEntries(sortedCapabilities.map((capability) => capability.kind === "encryption" ? [capability.resource, { [capability.action]: [{}] }] : [capability.resource, Object.fromEntries(capability.actions.map((action) => [action, [{ kind: capability.selector, type: "xyz.tinycloud.resource/selector", value: capability.resource }]]))]));
29063
- const kv = policy.capabilityCeiling.find((capability) => capability.kind === "kv");
29147
+ const kv2 = policy.capabilityCeiling.find((capability) => capability.kind === "kv");
29064
29148
  const expectedKvActions = parsed.actions.flatMap((action) => action === "read" ? ["tinycloud.kv/get", "tinycloud.kv/metadata"] : action === "list" ? ["tinycloud.kv/list"] : ["tinycloud.kv/put"]);
29065
29149
  const marker = parsed.contentSource.kvResource.indexOf("/kv/");
29066
29150
  const resourceSpace = marker < 1 ? "" : parsed.contentSource.kvResource.slice(0, marker);
29067
29151
  const resourcePath = marker < 1 ? "" : parsed.contentSource.kvResource.slice(marker + 4);
29068
- if (parsed.shareId !== parsed.contentSource.shareId || resourceSpace !== parsed.target.spaceId || resourcePath !== parsed.resource.path.replace(/\/$/, "") || parsed.resource.kind !== parsed.contentSource.selector || kv?.kind !== "kv" || expectedKvActions.some((action) => !kv.actions.includes(action)) || parsed.encryptionNetwork !== parsed.contentSource.encryptionNetwork || parsed.contentSource.keyVersion <= 0 || policy.expiresAt !== void 0 && Date.parse(parsed.expiry) > Date.parse(policy.expiresAt)) return false;
29152
+ if (parsed.shareId !== parsed.contentSource.shareId || resourceSpace !== parsed.target.spaceId || resourcePath !== parsed.resource.path.replace(/\/$/, "") || parsed.resource.kind !== parsed.contentSource.selector || kv2?.kind !== "kv" || expectedKvActions.some((action) => !kv2.actions.includes(action)) || parsed.encryptionNetwork !== parsed.contentSource.encryptionNetwork || parsed.contentSource.keyVersion <= 0 || policy.expiresAt !== void 0 && Date.parse(parsed.expiry) > Date.parse(policy.expiresAt)) return false;
29069
29153
  const binding = parsed.attestedEnforcerBinding;
29070
29154
  const { signature: bindingSignature, ...unsignedBinding } = binding;
29071
29155
  const expectedBindingDigestHex = hex(sha2562(new TextEncoder().encode(canonicalize2({ enforcerDid: binding.enforcerDid, nodeAudience: binding.nodeAudience }))));
@@ -32441,10 +32525,10 @@ function hasKvAction(permission, action) {
32441
32525
  return permission.actions.some((entry) => entry === action || entry === `tinycloud.kv/${action}`);
32442
32526
  }
32443
32527
  function readTarget(permissions) {
32444
- const kv = (permissions ?? []).filter((permission) => permission.service === "tinycloud.kv" || permission.service === "kv");
32445
- const list = kv.find((permission) => hasKvAction(permission, "list"));
32528
+ const kv2 = (permissions ?? []).filter((permission) => permission.service === "tinycloud.kv" || permission.service === "kv");
32529
+ const list = kv2.find((permission) => hasKvAction(permission, "list"));
32446
32530
  if (list) return { op: "list", prefix: list.path };
32447
- const get = kv.find((permission) => hasKvAction(permission, "get"));
32531
+ const get = kv2.find((permission) => hasKvAction(permission, "get"));
32448
32532
  if (!get) return void 0;
32449
32533
  return { op: "get", key: get.path === "" || get.path.endsWith("/") ? `${get.path}.tinycloud-space-probe` : get.path };
32450
32534
  }
@@ -32456,8 +32540,8 @@ function kvSpaceProbe(ctx, permissions, restore2 = restoreProfileSessionNode) {
32456
32540
  if (!target) return "unreadable";
32457
32541
  restored ??= await restore2(ctx);
32458
32542
  if ("unavailable" in restored) return "unreadable";
32459
- const kv = restored.node.kv;
32460
- const result = target.op === "list" ? await kv.list({ prefix: target.prefix, limit: 1 }) : await kv.get(target.key);
32543
+ const kv2 = restored.node.kv;
32544
+ const result = target.op === "list" ? await kv2.list({ prefix: target.prefix, limit: 1 }) : await kv2.get(target.key);
32461
32545
  if (result.ok) return "hosted";
32462
32546
  const status = result.error.meta?.status;
32463
32547
  if (status === 404 && UNHOSTED.test(result.error.message)) return "unhosted";
@@ -33002,7 +33086,8 @@ async function revokeDelegationOnNode(params) {
33002
33086
  throw new CLIError(
33003
33087
  "RAW_RECAP_RESOURCE_UNSUPPORTED",
33004
33088
  `Cannot revoke "${cid}": this CLI profile has no record of this grant and the node's record does not match the signed-in owner.`,
33005
- ExitCode.PERMISSION_DENIED
33089
+ ExitCode.PERMISSION_DENIED,
33090
+ profile.authMethod === "openkey" ? { hint: "This permission can't be revoked from the CLI yet. Revoke it at https://account.tinycloud.xyz/delegations." } : void 0
33006
33091
  );
33007
33092
  }
33008
33093
  fallbackSpaceId = fallback.space;
@@ -33827,7 +33912,7 @@ async function prepareProfileRemoteLogout(ctx, expectedSessionCid, discardLocalC
33827
33912
  "REVOCATION_AUTHORITY_UNAVAILABLE",
33828
33913
  "OpenKey owner CID revocation requires a verified signed revoke-control grant or wallet proof. No CID was revoked; session, key and replica files remain for retry, while local replica reads stay denied.",
33829
33914
  ExitCode.AUTH_REQUIRED,
33830
- { hint: "Obtain verified owner revoke authority before retrying `auth logout --revoke`, or use plain `auth logout` to end this local session without node revocation." }
33915
+ { hint: `This sign-in can't be revoked from the CLI yet. Revoke it at https://account.tinycloud.xyz/delegations. To sign out on this computer only, run \`${CLI_NAME18} auth logout\`.` }
33831
33916
  );
33832
33917
  }
33833
33918
  const signerDid = addressToDID(
@@ -35986,7 +36071,7 @@ async function refreshOpenKeySession(profileName2, host, options = {}) {
35986
36071
  // owner is verified below.
35987
36072
  permissions: permissions ?? ownerLoginPermissions(expectedOwner),
35988
36073
  expiry: openKeyExpiry,
35989
- ...permissions ? { reason: "Allow this TinyCloud CLI profile to use the permissions in the requested manifest." } : {}
36074
+ ...permissions ? { reason: options.reason ?? "Allow this TinyCloud CLI profile to use the permissions in the requested manifest." } : {}
35990
36075
  });
35991
36076
  let sanitizedSession;
35992
36077
  let verifiedOwner;
@@ -36254,6 +36339,52 @@ function listedDelegation(d) {
36254
36339
  resources
36255
36340
  };
36256
36341
  }
36342
+ var ISSUED_ORDER_NOTE = "Sorted by delegation ID, not by date.";
36343
+ function issuedFailure(error, host) {
36344
+ if (error.code === "UNSUPPORTED_OPERATION") {
36345
+ return new CLIError(
36346
+ "ISSUED_DELEGATIONS_UNAVAILABLE",
36347
+ `${host} can't list the delegations you issued yet.`,
36348
+ ExitCode.NODE_ERROR
36349
+ );
36350
+ }
36351
+ if (error.code === "CHAIN_TOO_DEEP") {
36352
+ return new CLIError(
36353
+ "CHAIN_TOO_DEEP",
36354
+ "The node can't check this delegation chain right now because it's too long. Try again later.",
36355
+ ExitCode.NODE_ERROR,
36356
+ { status: 503 }
36357
+ );
36358
+ }
36359
+ if (error.code === "INVALID_CURSOR") {
36360
+ return new CLIError(
36361
+ "INVALID_CURSOR",
36362
+ "The node couldn't continue the list. Run the command again.",
36363
+ ExitCode.NODE_ERROR,
36364
+ { status: 400 }
36365
+ );
36366
+ }
36367
+ return cliErrorFromService(error);
36368
+ }
36369
+ async function revokeIssuedDelegation(node, ctx, cid) {
36370
+ const result = await node.delegationManager.revokeIssued(cid);
36371
+ if (result.ok) return { cid: result.data.cid, revoked: true };
36372
+ if (result.error.code === "PERMISSION_DENIED") {
36373
+ throw new CLIError(
36374
+ "REVOKE_UNAUTHORIZED",
36375
+ `The node refused this revocation: ${result.error.message}.`,
36376
+ ExitCode.PERMISSION_DENIED,
36377
+ {
36378
+ status: result.error.meta?.status,
36379
+ hint: `A profile can revoke only the delegations its own key issued. See them with \`${CLI_NAME21} --profile ${ctx.profile} delegation list --granted\`.`
36380
+ }
36381
+ );
36382
+ }
36383
+ if (result.error.code === "NOT_FOUND") {
36384
+ throw new CLIError("TARGET_NOT_FOUND", `The node has no delegation "${cid}".`, ExitCode.NOT_FOUND);
36385
+ }
36386
+ throw issuedFailure(result.error, ctx.host);
36387
+ }
36257
36388
  async function confirmCurrentSessionRevoke(yes) {
36258
36389
  if (yes) return;
36259
36390
  const warning = "This is your current sign-in session; revoking it signs you out";
@@ -36342,6 +36473,16 @@ function registerDelegationCommand(program, configuration = {}) {
36342
36473
  if (options.granted && options.received) {
36343
36474
  throw new CLIError("USAGE_ERROR", "Use only one of --granted or --received.", ExitCode.USAGE_ERROR);
36344
36475
  }
36476
+ if (options.granted && ctx.posture === "delegate-session" && node.address === void 0) {
36477
+ const issued = await node.delegationManager.queryAllIssued({ space: options.space });
36478
+ if (!issued.ok) throw issuedFailure(issued.error, ctx.host);
36479
+ outputJson({
36480
+ delegations: issued.data.map(listedDelegation),
36481
+ count: issued.data.length,
36482
+ note: ISSUED_ORDER_NOTE
36483
+ });
36484
+ return;
36485
+ }
36345
36486
  const delegations = await queryDelegations(node, {
36346
36487
  direction: options.granted ? "granted" : options.received ? "received" : "all",
36347
36488
  space: options.space
@@ -36372,11 +36513,16 @@ function registerDelegationCommand(program, configuration = {}) {
36372
36513
  try {
36373
36514
  const globalOpts = cmd.optsWithGlobals();
36374
36515
  const ctx = await ProfileManager.resolveContext(globalOpts);
36516
+ const delegateCandidate = ctx.posture === "delegate-session" ? await ensureAuthenticated(ctx) : void 0;
36517
+ if (delegateCandidate !== void 0 && delegateCandidate.address === void 0) {
36518
+ outputJson(await revokeIssuedDelegation(delegateCandidate, ctx, cid));
36519
+ return;
36520
+ }
36375
36521
  const session = await ProfileManager.getSession(ctx.profile);
36376
36522
  const currentSession = session && "delegationCid" in session && session.delegationCid === cid;
36377
36523
  if (currentSession) await confirmCurrentSessionRevoke(options.yes === true);
36378
36524
  const prepared = currentSession ? await prepareProfileRemoteLogout(ctx, cid) : void 0;
36379
- const node = prepared?.node ?? await ensureAuthenticated(ctx);
36525
+ const node = prepared?.node ?? delegateCandidate ?? await ensureAuthenticated(ctx);
36380
36526
  const profile = prepared?.profile ?? await ProfileManager.getProfile(ctx.profile);
36381
36527
  const revoked = await revokeDelegationOnNode({
36382
36528
  ctx,
@@ -37008,7 +37154,7 @@ Examples:
37008
37154
  }
37009
37155
  });
37010
37156
  }
37011
- function appsInstallIo(kv, db) {
37157
+ function appsInstallIo(kv2, db) {
37012
37158
  return {
37013
37159
  async fetchBytes(url) {
37014
37160
  const response = await fetch(url, { redirect: "error" });
@@ -37016,7 +37162,7 @@ function appsInstallIo(kv, db) {
37016
37162
  return new Uint8Array(await response.arrayBuffer());
37017
37163
  },
37018
37164
  async kvGet(key) {
37019
- const result = await kv.get(key, { binary: true });
37165
+ const result = await kv2.get(key, { binary: true });
37020
37166
  if (result.ok) {
37021
37167
  const bytes = result.data.data;
37022
37168
  if (bytes instanceof Uint8Array) return bytes;
@@ -37026,7 +37172,7 @@ function appsInstallIo(kv, db) {
37026
37172
  throw new Error(result.error.code ?? "KV_GET_FAILED");
37027
37173
  },
37028
37174
  async kvCreate(key, bytes) {
37029
- const result = await kv.put(key, bytes, { ifNoneMatch: "*" });
37175
+ const result = await kv2.put(key, bytes, { ifNoneMatch: "*" });
37030
37176
  if (result.ok) return "created";
37031
37177
  if (result.error.code === "KV_PRECONDITION_FAILED") return "exists";
37032
37178
  throw new Error(result.error.code ?? "KV_PUT_FAILED");
@@ -37079,6 +37225,25 @@ init_formatter();
37079
37225
  init_errors();
37080
37226
  init_constants();
37081
37227
  import { CLI_NAME as CLI_NAME25 } from "@tinycloud/operations/cli-name";
37228
+ async function createKeyOnlyProfile(profileName2, host, defaultSpace) {
37229
+ await ProfileManager.ensureConfigDir();
37230
+ const { jwk, did } = await generateKey2();
37231
+ await ProfileManager.setKey(profileName2, jwk);
37232
+ await ProfileManager.setProfile(profileName2, {
37233
+ name: profileName2,
37234
+ host,
37235
+ chainId: DEFAULT_CHAIN_ID,
37236
+ spaceName: "default",
37237
+ did,
37238
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
37239
+ ...defaultSpace ? { defaultSpace } : {}
37240
+ });
37241
+ const config = await ProfileManager.getConfig();
37242
+ if (profileName2 === "default" || !await ProfileManager.profileExists(config.defaultProfile)) {
37243
+ await ProfileManager.setConfig({ ...config, defaultProfile: profileName2 });
37244
+ }
37245
+ return { did };
37246
+ }
37082
37247
  function registerInitCommand(program) {
37083
37248
  program.command("init").description("Initialize a new TinyCloud profile").option("--name <profile>", "Profile name", "default").option("--key-only", "Only generate key, skip authentication").option("--host <url>", "TinyCloud node URL").option("--paste", "Use manual paste mode for authentication").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--default-space <name>", "Default space used when --space is omitted (e.g. applications)").action(async (options, cmd) => {
37084
37249
  try {
@@ -37100,25 +37265,7 @@ function registerInitCommand(program) {
37100
37265
  ExitCode.ERROR
37101
37266
  );
37102
37267
  }
37103
- await ProfileManager.ensureConfigDir();
37104
- const { jwk, did } = await withSpinner("Generating key...", async () => {
37105
- return generateKey2();
37106
- });
37107
- await ProfileManager.setKey(profileName2, jwk);
37108
- const profileConfig = {
37109
- name: profileName2,
37110
- host,
37111
- chainId: DEFAULT_CHAIN_ID,
37112
- spaceName: "default",
37113
- did,
37114
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
37115
- ...defaultSpace ? { defaultSpace } : {}
37116
- };
37117
- await ProfileManager.setProfile(profileName2, profileConfig);
37118
- const config = await ProfileManager.getConfig();
37119
- if (profileName2 === "default" || !await ProfileManager.profileExists(config.defaultProfile)) {
37120
- await ProfileManager.setConfig({ ...config, defaultProfile: profileName2 });
37121
- }
37268
+ const { did } = await withSpinner("Generating key...", () => createKeyOnlyProfile(profileName2, host, defaultSpace));
37122
37269
  if (options.keyOnly) {
37123
37270
  outputJson({
37124
37271
  profile: profileName2,
@@ -37393,25 +37540,25 @@ async function readStdin2() {
37393
37540
  }
37394
37541
  async function kvHandle(node, spaceInput, profileName2) {
37395
37542
  const spaceUri = await resolveSpaceUri(spaceInput, profileName2);
37396
- const kv = spaceUri ? node.kvForSpace(spaceUri) : node.kv;
37397
- return { kv, spaceUri };
37543
+ const kv2 = spaceUri ? node.kvForSpace(spaceUri) : node.kv;
37544
+ return { kv: kv2, spaceUri };
37398
37545
  }
37399
37546
  function retryFields(retry) {
37400
37547
  return retry ? { attempts: retry.attempts, backoffMs: retry.backoffMs } : {};
37401
37548
  }
37402
37549
  function registerKvCommand(program) {
37403
- const kv = program.command("kv").description("Key-value store operations");
37404
- kv.command("get <key>").description("Get a value by key").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, options, cmd) => {
37550
+ const kv2 = program.command("kv").description("Key-value store operations");
37551
+ kv2.command("get <key>").description("Get a value by key").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, options, cmd) => {
37405
37552
  try {
37406
37553
  assertAddressableKey(key);
37407
37554
  const globalOpts = cmd.optsWithGlobals();
37408
37555
  const ctx = await ProfileManager.resolveContext(globalOpts);
37409
37556
  const node = await ensureAuthenticated(ctx);
37410
- const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
37557
+ const { kv: kv3, spaceUri } = await kvHandle(node, options.space, ctx.profile);
37411
37558
  const wantBytes = !!options.output || !!options.raw;
37412
37559
  const result = await withSpinner(
37413
37560
  `Getting ${key}...`,
37414
- () => kv2.get(key, wantBytes ? { binary: true } : void 0)
37561
+ () => kv3.get(key, wantBytes ? { binary: true } : void 0)
37415
37562
  );
37416
37563
  if (!result.ok) {
37417
37564
  const hosted = await unhostedSpaceError(result.error, spaceUri, ctx.profile);
@@ -37447,7 +37594,7 @@ function registerKvCommand(program) {
37447
37594
  return handleError(error);
37448
37595
  }
37449
37596
  });
37450
- kv.command("put <key> [value]").description("Set a value").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").option("--if-none-match <value>", "`--if-none-match '*'`: Write only if the key doesn't exist yet; never overwrite").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, value, options, cmd) => {
37597
+ kv2.command("put <key> [value]").description("Set a value").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").option("--if-none-match <value>", "`--if-none-match '*'`: Write only if the key doesn't exist yet; never overwrite").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, value, options, cmd) => {
37451
37598
  try {
37452
37599
  assertAddressableKey(key);
37453
37600
  if (options.ifNoneMatch !== void 0 && options.ifNoneMatch !== "*") {
@@ -37475,8 +37622,8 @@ function registerKvCommand(program) {
37475
37622
  putValue = value;
37476
37623
  }
37477
37624
  }
37478
- const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
37479
- const result = await withSpinner(`Writing ${key}...`, () => kv2.put(
37625
+ const { kv: kv3, spaceUri } = await kvHandle(node, options.space, ctx.profile);
37626
+ const result = await withSpinner(`Writing ${key}...`, () => kv3.put(
37480
37627
  key,
37481
37628
  putValue,
37482
37629
  options.ifNoneMatch === "*" ? { ifNoneMatch: "*" } : void 0
@@ -37492,14 +37639,14 @@ function registerKvCommand(program) {
37492
37639
  return handleError(error);
37493
37640
  }
37494
37641
  });
37495
- 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) => {
37642
+ kv2.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) => {
37496
37643
  try {
37497
37644
  assertAddressableKey(key);
37498
37645
  const globalOpts = cmd.optsWithGlobals();
37499
37646
  const ctx = await ProfileManager.resolveContext(globalOpts);
37500
37647
  const node = await ensureAuthenticated(ctx);
37501
- const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
37502
- const result = await withSpinner(`Deleting ${key}...`, () => kv2.delete(key));
37648
+ const { kv: kv3, spaceUri } = await kvHandle(node, options.space, ctx.profile);
37649
+ const result = await withSpinner(`Deleting ${key}...`, () => kv3.delete(key));
37503
37650
  if (!result.ok) {
37504
37651
  await throwKvError(result.error, spaceUri, ctx.profile);
37505
37652
  }
@@ -37508,7 +37655,7 @@ function registerKvCommand(program) {
37508
37655
  return handleError(error);
37509
37656
  }
37510
37657
  });
37511
- kv.command("list").description("List keys").option("--prefix <prefix>", "Filter by key prefix").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("--limit <n>", "Keys per page, 1-1000 (the node's default otherwise)").option("--cursor <cursor>", "Continue from a previous page's nextCursor").option("--all", "Follow nextCursor until the listing is complete").action(async (options, cmd) => {
37658
+ kv2.command("list").description("List keys").option("--prefix <prefix>", "Filter by key prefix").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("--limit <n>", "Keys per page, 1-1000 (the node's default otherwise)").option("--cursor <cursor>", "Continue from a previous page's nextCursor").option("--all", "Follow nextCursor until the listing is complete").action(async (options, cmd) => {
37512
37659
  try {
37513
37660
  if (options.prefix) assertAddressableKey(options.prefix);
37514
37661
  const limit = options.limit === void 0 ? void 0 : Number(options.limit);
@@ -37518,13 +37665,13 @@ function registerKvCommand(program) {
37518
37665
  const globalOpts = cmd.optsWithGlobals();
37519
37666
  const ctx = await ProfileManager.resolveContext(globalOpts);
37520
37667
  const node = await ensureAuthenticated(ctx);
37521
- const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
37668
+ const { kv: kv3, spaceUri } = await kvHandle(node, options.space, ctx.profile);
37522
37669
  const keyList = [];
37523
37670
  let cursor = options.cursor;
37524
37671
  let truncated;
37525
37672
  const seenCursors = /* @__PURE__ */ new Set();
37526
37673
  for (; ; ) {
37527
- const result = await withSpinner("Listing keys...", () => kv2.list({
37674
+ const result = await withSpinner("Listing keys...", () => kv3.list({
37528
37675
  ...options.prefix ? { prefix: options.prefix } : {},
37529
37676
  ...limit === void 0 ? {} : { limit },
37530
37677
  ...cursor === void 0 ? {} : { cursor }
@@ -37579,14 +37726,14 @@ function registerKvCommand(program) {
37579
37726
  return handleError(error);
37580
37727
  }
37581
37728
  });
37582
- kv.command("head <key>").description("Get metadata for a key (no body)").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, options, cmd) => {
37729
+ kv2.command("head <key>").description("Get metadata for a key (no body)").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, options, cmd) => {
37583
37730
  try {
37584
37731
  assertAddressableKey(key);
37585
37732
  const globalOpts = cmd.optsWithGlobals();
37586
37733
  const ctx = await ProfileManager.resolveContext(globalOpts);
37587
37734
  const node = await ensureAuthenticated(ctx);
37588
- const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
37589
- const result = await withSpinner(`Checking ${key}...`, () => kv2.head(key));
37735
+ const { kv: kv3, spaceUri } = await kvHandle(node, options.space, ctx.profile);
37736
+ const result = await withSpinner(`Checking ${key}...`, () => kv3.head(key));
37590
37737
  if (!result.ok) {
37591
37738
  const hosted = await unhostedSpaceError(result.error, spaceUri, ctx.profile);
37592
37739
  if (hosted) throw hosted;
@@ -37988,12 +38135,129 @@ function registerNodeCommand(program) {
37988
38135
  });
37989
38136
  }
37990
38137
 
38138
+ // src/commands/onboard.ts
38139
+ import { CLI_NAME as CLI_NAME29 } from "@tinycloud/operations/cli-name";
38140
+ init_constants();
38141
+ init_profiles();
38142
+ init_types();
38143
+ init_permissions();
38144
+ init_sdk();
38145
+ init_errors();
38146
+ init_formatter();
38147
+ var ONBOARD_EXPIRY = "30d";
38148
+ var kv = (actions) => actions.map((action) => `tinycloud.kv/${action}`);
38149
+ var ONBOARD_STEPS = [
38150
+ {
38151
+ space: "applications",
38152
+ asks: "Read and change your apps' data for 30 days.",
38153
+ permissions: [{ service: "tinycloud.kv", space: "applications", path: "", actions: kv(["get", "list", "put", "del", "metadata"]) }]
38154
+ },
38155
+ {
38156
+ space: "account",
38157
+ asks: "See which spaces and apps you have, read-only, for 30 days.",
38158
+ permissions: [
38159
+ { service: "tinycloud.kv", space: "account", path: "spaces/", actions: kv(["get", "list"]) },
38160
+ { service: "tinycloud.kv", space: "account", path: "applications/", actions: kv(["get", "list"]) }
38161
+ ]
38162
+ }
38163
+ ];
38164
+ var ApprovalNeeded = class extends Error {
38165
+ constructor(approvalUrl) {
38166
+ super("approval needed");
38167
+ this.approvalUrl = approvalUrl;
38168
+ }
38169
+ };
38170
+ function registerOnboardCommand(program) {
38171
+ program.command("onboard").description(`Set up this computer to act for its owner: a profile, the owner's OpenKey sign-in (or new account), and a starting grant for 30 days (read and change the applications space; read which spaces and apps exist). Each run does the next approval: it prints the approval link and exits ${ExitCode.PENDING}; run it again with the code OpenKey shows. Exits 0 when onboarding is complete.`).option("--code <code>", "The code OpenKey showed after the owner approved the last link").action(async (options, cmd) => {
38172
+ try {
38173
+ const globalOpts = cmd.optsWithGlobals();
38174
+ const ctx = await ProfileManager.resolveContext(globalOpts, { storedSession: "unused" });
38175
+ if (!await ProfileManager.profileExists(ctx.profile)) await createKeyOnlyProfile(ctx.profile, ctx.host);
38176
+ let profile = await ProfileManager.getProfile(ctx.profile);
38177
+ if (resolveProfilePosture(profile) !== "owner-openkey") {
38178
+ throw new CLIError(
38179
+ "PROFILE_NOT_ONBOARDABLE",
38180
+ `Profile "${ctx.profile}" doesn't sign in with OpenKey, so onboarding can't use it. Run \`${CLI_NAME29} --profile <new name> onboard\` instead.`,
38181
+ ExitCode.USAGE_ERROR
38182
+ );
38183
+ }
38184
+ let code5 = typeof options.code === "string" && options.code.trim() !== "" ? options.code.trim() : void 0;
38185
+ const acquire = async (did, flow = {}) => {
38186
+ if (code5 === void 0) throw new ApprovalNeeded(buildAuthUrl(did, flow));
38187
+ const pasted = code5;
38188
+ code5 = void 0;
38189
+ return delegationFromInput(pasted, did, flow);
38190
+ };
38191
+ for (const [index, step] of ONBOARD_STEPS.entries()) {
38192
+ const reason = `TinyCloud onboarding, step ${index + 1} of ${ONBOARD_STEPS.length}: ${step.asks}`;
38193
+ try {
38194
+ const session2 = await ProfileManager.getSession(ctx.profile);
38195
+ if (!sessionStateIsUsable(storedSessionState(session2, profile).state)) {
38196
+ const { delegationData } = await refreshOpenKeySession(ctx.profile, ctx.host, {
38197
+ permissions: step.permissions,
38198
+ expiry: ONBOARD_EXPIRY,
38199
+ reason,
38200
+ persistHost: globalOpts.host !== void 0,
38201
+ openKeyAcquisition: acquire
38202
+ });
38203
+ await ensureSpaceSetup({ profile: ctx.profile, host: ctx.host }, delegationData, delegationData.permissions);
38204
+ profile = await ProfileManager.getProfile(ctx.profile);
38205
+ continue;
38206
+ }
38207
+ await ensureDelegationAuthority({
38208
+ ctx,
38209
+ profile,
38210
+ node: await ensureAuthenticated(ctx),
38211
+ requested: await resolvePermissionSpaces(step.permissions, ctx.profile),
38212
+ expiryOption: ONBOARD_EXPIRY,
38213
+ reason,
38214
+ yes: true,
38215
+ openKeyAcquisition: acquire
38216
+ });
38217
+ } catch (error) {
38218
+ if (!(error instanceof ApprovalNeeded)) throw error;
38219
+ const next = `${CLI_NAME29} --profile ${ctx.profile} onboard --code <code>`;
38220
+ output({
38221
+ onboarded: false,
38222
+ profile: ctx.profile,
38223
+ step: index + 1,
38224
+ steps: ONBOARD_STEPS.length,
38225
+ asks: step.asks,
38226
+ approvalUrl: error.approvalUrl,
38227
+ next
38228
+ }, () => [
38229
+ `Step ${index + 1} of ${ONBOARD_STEPS.length}: ${step.asks}`,
38230
+ `Open this link, sign in or create an account, and approve:`,
38231
+ ` ${error.approvalUrl}`,
38232
+ `Then run: ${next}`
38233
+ ].join("\n"));
38234
+ process.exitCode = ExitCode.PENDING;
38235
+ return;
38236
+ }
38237
+ }
38238
+ profile = await ProfileManager.getProfile(ctx.profile);
38239
+ const session = await ProfileManager.getSession(ctx.profile);
38240
+ output({
38241
+ onboarded: true,
38242
+ profile: ctx.profile,
38243
+ ownerDid: profile.ownerDid ?? null,
38244
+ host: ctx.host,
38245
+ granted: ONBOARD_STEPS.map(({ space, asks }) => ({ space, asks })),
38246
+ expiresAt: storedSessionState(session, profile).expiresAt
38247
+ }, () => `Onboarded: profile ${ctx.profile} acts for ${profile.ownerDid ?? "the owner"}.
38248
+ ${ONBOARD_STEPS.map(({ asks }) => ` ${asks}`).join("\n")}`);
38249
+ } catch (error) {
38250
+ return handleError(error);
38251
+ }
38252
+ });
38253
+ }
38254
+
37991
38255
  // src/commands/profile.ts
37992
38256
  init_profiles();
37993
38257
  init_formatter();
37994
38258
  init_errors();
37995
38259
  init_constants();
37996
- import { CLI_NAME as CLI_NAME29 } from "@tinycloud/operations/cli-name";
38260
+ import { CLI_NAME as CLI_NAME30 } from "@tinycloud/operations/cli-name";
37997
38261
  import { createInterface as createInterface5 } from "readline";
37998
38262
  init_theme();
37999
38263
  init_types();
@@ -38139,9 +38403,9 @@ profile at command time. Precedence for every kv/sql command:
38139
38403
  explicit --space flag > profile defaultSpace > primary space.
38140
38404
 
38141
38405
  Examples:
38142
- $ ${CLI_NAME29} profile set-default-space applications
38143
- $ ${CLI_NAME29} profile set-default-space applications --profile cli-test
38144
- $ ${CLI_NAME29} profile set-default-space --unset
38406
+ $ ${CLI_NAME30} profile set-default-space applications
38407
+ $ ${CLI_NAME30} profile set-default-space applications --profile cli-test
38408
+ $ ${CLI_NAME30} profile set-default-space --unset
38145
38409
  `).action(async (name, options, cmd) => {
38146
38410
  try {
38147
38411
  const globalOpts = cmd.optsWithGlobals();
@@ -38153,7 +38417,7 @@ Examples:
38153
38417
  if (!options.unset && (name === void 0 || name === "")) {
38154
38418
  throw new CLIError(
38155
38419
  "USAGE_ERROR",
38156
- `Provide a space name (e.g. \`${CLI_NAME29} profile set-default-space applications\`) or pass --unset.`,
38420
+ `Provide a space name (e.g. \`${CLI_NAME30} profile set-default-space applications\`) or pass --unset.`,
38157
38421
  ExitCode.USAGE_ERROR
38158
38422
  );
38159
38423
  }
@@ -38212,7 +38476,7 @@ function parseOperatorType(raw) {
38212
38476
 
38213
38477
  // src/commands/replica.ts
38214
38478
  init_profile_replicas();
38215
- import { CLI_NAME as CLI_NAME30 } from "@tinycloud/operations/cli-name";
38479
+ import { CLI_NAME as CLI_NAME31 } from "@tinycloud/operations/cli-name";
38216
38480
  import { randomBytes as randomBytes7 } from "crypto";
38217
38481
  import { constants as fsConstants5 } from "fs";
38218
38482
  import { mkdir as mkdir5, open as open11, readdir as readdir6, rename as rename4, unlink as unlink5, lstat as lstat9 } from "fs/promises";
@@ -38353,11 +38617,11 @@ var EXIT_BY_CODE = {
38353
38617
  var READ_FAILURE = {
38354
38618
  absent: { code: "KEY_ABSENT", exit: ExitCode.NOT_FOUND, what: "is not in the replica (the replica covers its whole prefix)" },
38355
38619
  deleted: { code: "KEY_DELETED", exit: ExitCode.NOT_FOUND, what: "was deleted at the source" },
38356
- content_missing: { code: "CONTENT_MISSING", exit: ExitCode.NOT_FOUND, what: `is known but its content has not been fetched yet; run ${CLI_NAME30} replica sync` },
38620
+ content_missing: { code: "CONTENT_MISSING", exit: ExitCode.NOT_FOUND, what: `is known but its content has not been fetched yet; run ${CLI_NAME31} replica sync` },
38357
38621
  coverage_incomplete: {
38358
38622
  code: "COVERAGE_INCOMPLETE",
38359
38623
  exit: ExitCode.NOT_FOUND,
38360
- what: `is not in the replica, which has not finished its first sync; run ${CLI_NAME30} replica sync`
38624
+ what: `is not in the replica, which has not finished its first sync; run ${CLI_NAME31} replica sync`
38361
38625
  },
38362
38626
  not_covered: { code: "NOT_COVERED", exit: ExitCode.USAGE_ERROR, what: "is outside the replica's prefix" }
38363
38627
  };
@@ -38475,7 +38739,7 @@ async function existingReplicaName(profile, option) {
38475
38739
  const names = await replicaNames(profile);
38476
38740
  if (names.length === 1) return names[0];
38477
38741
  if (names.length === 0) {
38478
- throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas. Create one with: ${CLI_NAME30} replica sync --prefix <prefix>`);
38742
+ throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas. Create one with: ${CLI_NAME31} replica sync --prefix <prefix>`);
38479
38743
  }
38480
38744
  throw new CLIError("USAGE_ERROR", `Profile "${profile}" has several replicas (${names.join(", ")}); pass --replica <name>.`, ExitCode.USAGE_ERROR);
38481
38745
  }
@@ -38583,9 +38847,9 @@ async function storedGrants(profile) {
38583
38847
  function grantHint(space, prefix) {
38584
38848
  return [
38585
38849
  "A replica needs a device grant with get and sync on its prefix:",
38586
- ` device: ${CLI_NAME30} auth request --cap tinycloud.kv:${space}:${prefix}:get,list,metadata,sync --expiry 30d --emit req.json`,
38587
- ` owner: ${CLI_NAME30} auth grant req.json > grant.json`,
38588
- ` device: ${CLI_NAME30} auth import grant.json`
38850
+ ` device: ${CLI_NAME31} auth request --cap tinycloud.kv:${space}:${prefix}:get,list,metadata,sync --expiry 30d --emit req.json`,
38851
+ ` owner: ${CLI_NAME31} auth grant req.json > grant.json`,
38852
+ ` device: ${CLI_NAME31} auth import grant.json`
38589
38853
  ].join("\n");
38590
38854
  }
38591
38855
  function resolveSpace(input, prefix, grants) {
@@ -38641,9 +38905,9 @@ async function grantTransports(input) {
38641
38905
  fetch: globalThis.fetch.bind(globalThis),
38642
38906
  hosts: [input.host]
38643
38907
  });
38644
- const kv = new KVService({});
38645
- kv.initialize(context);
38646
- context.registerService("kv", kv);
38908
+ const kv2 = new KVService({});
38909
+ kv2.initialize(context);
38910
+ context.registerService("kv", kv2);
38647
38911
  context.setSession({
38648
38912
  delegationHeader: { Authorization: new TextDecoder().decode(grant.bytes) },
38649
38913
  delegationCid: grant.cid,
@@ -38651,7 +38915,7 @@ async function grantTransports(input) {
38651
38915
  verificationMethod: input.deviceDid,
38652
38916
  jwk: input.jwk
38653
38917
  });
38654
- const client = kv;
38918
+ const client = kv2;
38655
38919
  return kvSyncTransport(client);
38656
38920
  };
38657
38921
  }
@@ -38676,7 +38940,7 @@ function describeStatus(status) {
38676
38940
  formatField("Size", formatBytes(status.bytes)),
38677
38941
  formatField("Grant", status.device.delegationCid),
38678
38942
  formatField("Last error", status.lastError ? `${status.lastError.code}: ${status.lastError.message}` : null),
38679
- formatField("Purge", status.purgePending ? `incomplete: content files remain; the next ${CLI_NAME30} replica command retries` : null)
38943
+ formatField("Purge", status.purgePending ? `incomplete: content files remain; the next ${CLI_NAME31} replica command retries` : null)
38680
38944
  ].join("\n");
38681
38945
  }
38682
38946
  async function writeFileAtomic(path, bytes) {
@@ -38840,7 +39104,7 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
38840
39104
  (options, cmd) => run(async () => {
38841
39105
  const profile = await profileName(cmd);
38842
39106
  if (options.purge) {
38843
- throw new CLIError("REPORT_READ_ONLY", `Replica report is read-only. To remove a named local replica, run ${CLI_NAME30} replica reset --replica <name> --purge; flag-owned purge has no equivalent yet (TC-1309).`, ExitCode.USAGE_ERROR);
39107
+ throw new CLIError("REPORT_READ_ONLY", `Replica report is read-only. To remove a named local replica, run ${CLI_NAME31} replica reset --replica <name> --purge; flag-owned purge has no equivalent yet (TC-1309).`, ExitCode.USAGE_ERROR);
38844
39108
  }
38845
39109
  if (options.clearPending) {
38846
39110
  throw new CLIError("REPORT_READ_ONLY", "Clearing pending replica writes has no equivalent yet; see TC-1309 for a safe replacement command.", ExitCode.USAGE_ERROR);
@@ -38942,7 +39206,7 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
38942
39206
  if (differs !== void 0) {
38943
39207
  throw new ReplicaError(
38944
39208
  ReplicaErrorCode.CONFIG_MISMATCH,
38945
- `Replica ${name} already follows ${differs}. Use another --replica name, or ${CLI_NAME30} replica reset --replica ${name} --purge first.`
39209
+ `Replica ${name} already follows ${differs}. Use another --replica name, or ${CLI_NAME31} replica reset --replica ${name} --purge first.`
38946
39210
  );
38947
39211
  }
38948
39212
  }
@@ -39107,10 +39371,10 @@ init_theme();
39107
39371
  init_errors();
39108
39372
  init_warnings();
39109
39373
  init_constants();
39110
- import { CLI_NAME as CLI_NAME32 } from "@tinycloud/operations/cli-name";
39374
+ import { CLI_NAME as CLI_NAME33 } from "@tinycloud/operations/cli-name";
39111
39375
  import { readFile as readFile14 } from "fs/promises";
39112
39376
  import { join as join17 } from "path";
39113
- import { homedir, hostname, userInfo } from "os";
39377
+ import { constants as osConstants, homedir, hostname, userInfo } from "os";
39114
39378
  import { appSecretScope } from "@tinycloud/sdk-core";
39115
39379
  import { invokeOperation as invokeOperation3 } from "@tinycloud/operations";
39116
39380
  import { isLocalOwnerProfile as isLocalOwnerProfile4, ownerSessionLacksProof as ownerSessionLacksProof5 } from "@tinycloud/operations/profile";
@@ -39131,6 +39395,7 @@ import {
39131
39395
  } from "@tinycloud/operations/secret-capabilities";
39132
39396
  import { invokeSecretsGetWithLocalAuthorityRetry } from "@tinycloud/operations/cli-runtime";
39133
39397
  init_sdk();
39398
+ init_replication_registry();
39134
39399
  init_session_expired();
39135
39400
  init_space();
39136
39401
 
@@ -39196,14 +39461,14 @@ function runProbeChild(request, options = {}) {
39196
39461
  const args = options.argv ?? probeChildArgv(request);
39197
39462
  const budgetMs = options.budgetMs ?? ATTEMPT_BUDGET_MS;
39198
39463
  const killGraceMs = options.killGraceMs ?? KILL_GRACE_MS;
39199
- const spawn2 = options.spawn ?? nodeSpawn;
39464
+ const spawn3 = options.spawn ?? nodeSpawn;
39200
39465
  const signal = options.signal;
39201
39466
  return new Promise((resolve9) => {
39202
39467
  if (signal?.aborted) {
39203
39468
  resolve9({ kind: "cancelled" });
39204
39469
  return;
39205
39470
  }
39206
- const child = spawn2(command, args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
39471
+ const child = spawn3(command, args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
39207
39472
  let outcome;
39208
39473
  let stdout = Buffer.alloc(0);
39209
39474
  let stderr = Buffer.alloc(0);
@@ -39315,10 +39580,79 @@ function parseStatusEnvelope(line) {
39315
39580
  return void 0;
39316
39581
  }
39317
39582
 
39583
+ // src/lib/run-command.ts
39584
+ import { execFileSync, spawn as spawn2 } from "child_process";
39585
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
39586
+ var FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
39587
+ var TERMINAL_SIGNALS = { SIGINT: true, SIGQUIT: true, SIGHUP: true };
39588
+ function psGroupArgs(pid) {
39589
+ return ["-o", "pgid=", "-o", "tpgid=", "-p", String(pid)];
39590
+ }
39591
+ function parsePsGroups(output2) {
39592
+ const columns = output2.trim().split(/\s+/);
39593
+ if (columns.length !== 2) return void 0;
39594
+ const group = Number(columns[0]);
39595
+ const foreground = Number(columns[1]);
39596
+ return Number.isSafeInteger(group) && Number.isSafeInteger(foreground) ? { group, foreground } : void 0;
39597
+ }
39598
+ function processGroups() {
39599
+ try {
39600
+ const stat8 = readFileSync2("/proc/self/stat", "utf8");
39601
+ const fields = stat8.slice(stat8.lastIndexOf(")") + 2).split(" ");
39602
+ return { group: Number(fields[2]), foreground: Number(fields[5]) };
39603
+ } catch {
39604
+ }
39605
+ try {
39606
+ const ps = existsSync("/bin/ps") ? "/bin/ps" : "/usr/bin/ps";
39607
+ return parsePsGroups(execFileSync(ps, psGroupArgs(process.pid), {
39608
+ env: {},
39609
+ encoding: "utf8",
39610
+ stdio: ["ignore", "pipe", "ignore"],
39611
+ timeout: 2e3
39612
+ }));
39613
+ } catch {
39614
+ return void 0;
39615
+ }
39616
+ }
39617
+ function inTerminalForeground() {
39618
+ const groups = processGroups();
39619
+ return groups !== void 0 && groups.foreground > 0 && groups.group === groups.foreground;
39620
+ }
39621
+ function runCommand(argv, env) {
39622
+ return new Promise((resolve9) => {
39623
+ let child;
39624
+ try {
39625
+ child = spawn2(argv[0], argv.slice(1), { stdio: "inherit", env, shell: false });
39626
+ } catch (error) {
39627
+ const code5 = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
39628
+ resolve9({ kind: "not_started", ...code5 === void 0 ? {} : { errno: code5 } });
39629
+ return;
39630
+ }
39631
+ const forward = (signal) => {
39632
+ if (TERMINAL_SIGNALS[signal] && inTerminalForeground()) return;
39633
+ child.kill(signal);
39634
+ };
39635
+ for (const signal of FORWARDED_SIGNALS) process.on(signal, forward);
39636
+ const settle = (outcome) => {
39637
+ for (const signal of FORWARDED_SIGNALS) process.off(signal, forward);
39638
+ resolve9(outcome);
39639
+ };
39640
+ child.on("error", (error) => {
39641
+ if (child.pid === void 0) settle({ kind: "not_started", ...error.code === void 0 ? {} : { errno: error.code } });
39642
+ });
39643
+ child.once("exit", (code5, signal) => {
39644
+ settle(signal !== null ? { kind: "signaled", signal } : { kind: "exited", code: code5 ?? 1 });
39645
+ });
39646
+ });
39647
+ }
39648
+
39649
+ // src/commands/secrets.ts
39650
+ init_safe_text();
39651
+
39318
39652
  // src/lib/secrets-wait.ts
39319
39653
  init_constants();
39320
39654
  init_errors();
39321
- import { CLI_NAME as CLI_NAME31 } from "@tinycloud/operations/cli-name";
39655
+ import { CLI_NAME as CLI_NAME32 } from "@tinycloud/operations/cli-name";
39322
39656
  var WAIT_BACKOFF_MS = [3e3, 4500, 6750, 10125];
39323
39657
  var WAIT_STEADY_INTERVAL_MS = 15e3;
39324
39658
  var DEFAULT_WAIT_TIMEOUT = "5m";
@@ -39326,7 +39660,7 @@ var MAX_WAIT_TIMEOUT_MS = 9 * 6e4;
39326
39660
  var MAX_CONSECUTIVE_FAILURES = 5;
39327
39661
  var TIMEOUT_RE = /^(0|\d+s|\d+m)$/;
39328
39662
  var RETRYABLE_STATUS_ERRORS = { NODE_UNREACHABLE: true, NODE_ERROR: true };
39329
- var EXPLICIT_KEY_HINT = `${CLI_NAME31} secrets wait never signs in, and an explicit private key has no stored session. Use a local profile signed in once with ${CLI_NAME31} --profile <profile> auth login --method local, without TC_PRIVATE_KEY.`;
39663
+ var EXPLICIT_KEY_HINT = `${CLI_NAME32} secrets wait never signs in, and an explicit private key has no stored session. Use a local profile signed in once with ${CLI_NAME32} --profile <profile> auth login --method local, without TC_PRIVATE_KEY.`;
39330
39664
  function abortableSleep(ms2, signal) {
39331
39665
  return new Promise((resolve9) => {
39332
39666
  if (signal.aborted) {
@@ -39362,7 +39696,7 @@ async function runSecretsWait(request, deps) {
39362
39696
  let step = 0;
39363
39697
  const cancelled = () => new CLIError(
39364
39698
  "CANCELLED",
39365
- `Interrupted: ${CLI_NAME31} secrets wait stopped before the secrets were ready.`,
39699
+ `Interrupted: ${CLI_NAME32} secrets wait stopped before the secrets were ready.`,
39366
39700
  ExitCode.CANCELLED,
39367
39701
  progress === void 0 ? {} : { progress }
39368
39702
  );
@@ -39506,13 +39840,13 @@ function statusError(error, profile, warnings) {
39506
39840
  if (error.code === "AUTH_REQUIRED" || error.code === "SESSION_NOT_FOUND") {
39507
39841
  const recovery = recoveryOf(error, "sign_in");
39508
39842
  if (recovery !== void 0) {
39509
- const hint = recovery.command !== void 0 ? `Sign in once with: ${recovery.command}, then retry.` : `Sign in to profile "${profile}" once with ${CLI_NAME31} auth login, then retry.`;
39843
+ const hint = recovery.command !== void 0 ? `Sign in once with: ${recovery.command}, then retry.` : `Sign in to profile "${profile}" once with ${CLI_NAME32} auth login, then retry.`;
39510
39844
  return new CLIError("AUTH_REQUIRED", error.message, ExitCode.AUTH_REQUIRED, { hint, recovery, ...extra });
39511
39845
  }
39512
39846
  }
39513
39847
  if (error.code === "PROFILE_MIGRATION_REQUIRED") {
39514
39848
  const recovery = recoveryOf(error, "complete_migration");
39515
- const hint = recovery?.command !== void 0 ? `Run ${recovery.command} once to finish this profile's one-time upgrade, then retry wait.` : `Run ${CLI_NAME31} auth caps once with profile "${profile}" to finish its one-time upgrade, then retry wait.`;
39849
+ const hint = recovery?.command !== void 0 ? `Run ${recovery.command} once to finish this profile's one-time upgrade, then retry wait.` : `Run ${CLI_NAME32} auth caps once with profile "${profile}" to finish its one-time upgrade, then retry wait.`;
39516
39850
  return new CLIError("PROFILE_MIGRATION_REQUIRED", error.message, ExitCode.ERROR, {
39517
39851
  hint,
39518
39852
  ...recovery === void 0 ? {} : { recovery },
@@ -39704,11 +40038,11 @@ async function runSecretOperation(params) {
39704
40038
  }
39705
40039
  function secretPermissionReason(action, name) {
39706
40040
  const target = name ? ` secret "${name}"` : " secrets";
39707
- return `Allow \`${CLI_NAME32} secrets ${action}${name ? ` ${name}` : ""}\` to access${target} with the required TinyCloud permissions.`;
40041
+ return `Allow \`${CLI_NAME33} secrets ${action}${name ? ` ${name}` : ""}\` to access${target} with the required TinyCloud permissions.`;
39708
40042
  }
39709
40043
  function assertOwnerApprovalPossible(profileName2, profile, action, name, missing = []) {
39710
40044
  if (profile.authMethod !== "openkey" || process.stdin.isTTY || process.stderr.isTTY) return;
39711
- const command = `${CLI_NAME32} secrets ${action === "del" ? "delete" : action}${name ? ` ${name}` : ""}`;
40045
+ const command = `${CLI_NAME33} secrets ${action === "del" ? "delete" : action}${name ? ` ${name}` : ""}`;
39712
40046
  throw new CLIError(
39713
40047
  "PERMISSION_DENIED",
39714
40048
  action === "get" ? missing.length > 0 && missing.every((permission) => permission.service === "tinycloud.encryption" && Array.isArray(permission.actions) && permission.actions.length > 0 && permission.actions.every((capability) => capability === SECRET_DECRYPT_CAPABILITY)) ? `Profile "${profileName2}" lacks the scoped decrypt authority (${SECRET_DECRYPT_CAPABILITY}) needed to read secret "${name}", and requesting it needs an interactive browser approval.` : `Profile "${profileName2}" lacks a scoped read or decrypt grant needed to read secret "${name}", and requesting it needs an interactive browser approval.` : `Profile "${profileName2}" holds no grant for \`${command}\`, and requesting one needs an interactive browser approval.`,
@@ -39719,7 +40053,7 @@ function assertOwnerApprovalPossible(profileName2, profile, action, name, missin
39719
40053
  );
39720
40054
  }
39721
40055
  function scopedSecretLoginHint(profileName2) {
39722
- return `Have the owner approve a scoped login whose manifest names the secret: ${CLI_NAME32} --profile ${profileName2} auth login --method openkey --paste --manifest <manifest.json>. Pass the owner's code on stdin, newline-terminated.`;
40056
+ return `Have the owner approve a scoped login whose manifest names the secret: ${CLI_NAME33} --profile ${profileName2} auth login --method openkey --paste --manifest <manifest.json>. Pass the owner's code on stdin, newline-terminated.`;
39723
40057
  }
39724
40058
  async function runSecretOperationAttempt(label, operation) {
39725
40059
  try {
@@ -39853,7 +40187,7 @@ ${message}`,
39853
40187
  "AUTH_REQUIRED",
39854
40188
  "Not signed in to TinyCloud.",
39855
40189
  ExitCode.AUTH_REQUIRED,
39856
- { hint: `Sign in with: ${CLI_NAME32} --profile ${result.context.profile} auth login` }
40190
+ { hint: `Sign in with: ${CLI_NAME33} --profile ${result.context.profile} auth login` }
39857
40191
  );
39858
40192
  }
39859
40193
  if (result.error.code === "NODE_UNREACHABLE") {
@@ -40269,6 +40603,153 @@ function outputSecretDoctor(result) {
40269
40603
  process.stdout.write(theme.warn(`${failed} secrets check${failed > 1 ? "s" : ""} need attention.`) + "\n");
40270
40604
  }
40271
40605
  }
40606
+ async function resolveSecretReadTarget(ctx, name, options) {
40607
+ const scopeOptions = resolveSecretScope(options);
40608
+ const legacySpaceUri = await resolveSecretSpace(options.space, ctx.profile);
40609
+ const resolved = resolveSecretPath2(name, scopeOptions);
40610
+ return { name, scopeOptions, scope: resolved.scope, legacySpaceUri, secretPath: resolved.permissionPaths.vault };
40611
+ }
40612
+ async function readSecret(params) {
40613
+ const { ctx, target, options } = params;
40614
+ const { name } = target;
40615
+ if (options.delegation) {
40616
+ const delegated = await resolveDelegatedSecretSource(
40617
+ options.delegation,
40618
+ target.secretPath,
40619
+ target.legacySpaceUri ?? SECRETS_SPACE2
40620
+ );
40621
+ const effectiveHost = params.host ?? delegated.delegation.host ?? ctx.host;
40622
+ const delegatedCtx = { ...ctx, host: effectiveHost };
40623
+ const local = effectiveLocal(await ProfileManager.getProfile(ctx.profile).catch(() => null), options);
40624
+ const node = await ensureSecretsNode(delegatedCtx, options);
40625
+ const value = await withSpinner(
40626
+ `Getting secret ${name}...`,
40627
+ () => readDelegatedSecretValue({
40628
+ node,
40629
+ delegation: delegated.delegation,
40630
+ delegationCid: delegated.delegation.cid,
40631
+ permissions: delegated.permissions,
40632
+ secretPath: target.secretPath,
40633
+ space: target.legacySpaceUri ?? SECRETS_SPACE2,
40634
+ name,
40635
+ ...target.scope === void 0 ? {} : { scope: target.scope },
40636
+ local
40637
+ })
40638
+ );
40639
+ return { value };
40640
+ }
40641
+ const privateKey = authOptions(options)?.privateKey;
40642
+ const spaceUri = privateKey !== void 0 && options.space !== void 0 && !options.space.startsWith("tinycloud:") ? options.space : target.legacySpaceUri;
40643
+ const result = await invokeCanonicalSecretGet({
40644
+ ctx,
40645
+ name,
40646
+ ...target.scopeOptions?.scope === void 0 ? {} : { scope: target.scopeOptions.scope },
40647
+ ...spaceUri === void 0 ? {} : { space: spaceUri },
40648
+ options,
40649
+ label: `Getting secret ${name}...`,
40650
+ openKeyAcquisition: params.openKeyAcquisition
40651
+ });
40652
+ if (result.status !== "ok") {
40653
+ try {
40654
+ throwCanonicalSecretGetError(result, name);
40655
+ } catch (error) {
40656
+ throw withOperationWarnings(error, result.warnings);
40657
+ }
40658
+ }
40659
+ return { value: result.output.value, warnings: result.warnings };
40660
+ }
40661
+ var ENV_VAR_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
40662
+ function argvAfterSeparator(cmd) {
40663
+ let root = cmd;
40664
+ while (root.parent) root = root.parent;
40665
+ const raw = "rawArgs" in root ? root.rawArgs : void 0;
40666
+ if (!Array.isArray(raw)) return void 0;
40667
+ const args = raw.map(String);
40668
+ const withValue = (flag) => root.options.some((option) => option.required && (option.long === flag || option.short === flag));
40669
+ for (let index = 0; index < args.length; index += 1) {
40670
+ const arg = args[index];
40671
+ if (arg === "--") return args.slice(index + 1);
40672
+ if (arg.startsWith("--")) {
40673
+ if (withValue(arg)) index += 1;
40674
+ continue;
40675
+ }
40676
+ if (arg.length < 2 || !arg.startsWith("-")) continue;
40677
+ for (let char = 1; char < arg.length; char += 1) {
40678
+ const option = root.options.find((candidate) => candidate.short === `-${arg[char]}`);
40679
+ if (option === void 0) break;
40680
+ if (option.required) {
40681
+ if (char === arg.length - 1) index += 1;
40682
+ break;
40683
+ }
40684
+ }
40685
+ }
40686
+ return void 0;
40687
+ }
40688
+ function secretRunPlan(cmd, operands, maps) {
40689
+ const usage3 = `${CLI_NAME33} secrets run NAME... [--map NAME=ENV_VAR] -- <command> [args...]`;
40690
+ const after = argvAfterSeparator(cmd);
40691
+ if (after === void 0 || after.length > operands.length) {
40692
+ throw new CLIError("USAGE_ERROR", `Put the command after \`--\`: ${usage3}`, ExitCode.USAGE_ERROR);
40693
+ }
40694
+ const [program, ...args] = after;
40695
+ if (program === void 0) {
40696
+ throw new CLIError("USAGE_ERROR", `Add the command to run after \`--\`: ${usage3}`, ExitCode.USAGE_ERROR);
40697
+ }
40698
+ const exports = [];
40699
+ for (const name of operands.slice(0, operands.length - after.length)) {
40700
+ if (!ENV_VAR_RE.test(name)) {
40701
+ throw new CLIError(
40702
+ "USAGE_ERROR",
40703
+ `${JSON.stringify(name)} can't be used as an environment variable name. Use \`--map ${name}=ENV_VAR\` to choose one.`,
40704
+ ExitCode.USAGE_ERROR
40705
+ );
40706
+ }
40707
+ exports.push({ name, variable: name });
40708
+ }
40709
+ for (const map of maps) {
40710
+ const equals7 = map.indexOf("=");
40711
+ const variable = map.slice(equals7 + 1);
40712
+ if (equals7 <= 0 || !ENV_VAR_RE.test(variable)) {
40713
+ throw new CLIError(
40714
+ "USAGE_ERROR",
40715
+ `Invalid \`--map ${JSON.stringify(map)}\`: use NAME=ENV_VAR, where ENV_VAR starts with a letter or _ and uses only letters, digits and _.`,
40716
+ ExitCode.USAGE_ERROR
40717
+ );
40718
+ }
40719
+ exports.push({ name: map.slice(0, equals7), variable });
40720
+ }
40721
+ if (exports.length === 0) {
40722
+ throw new CLIError("USAGE_ERROR", `Name at least one secret: ${usage3}`, ExitCode.USAGE_ERROR);
40723
+ }
40724
+ const variables = /* @__PURE__ */ new Set();
40725
+ for (const { variable } of exports) {
40726
+ if (variables.has(variable)) {
40727
+ throw new CLIError("USAGE_ERROR", `Two secrets are mapped to ${variable}. Give each its own variable.`, ExitCode.USAGE_ERROR);
40728
+ }
40729
+ variables.add(variable);
40730
+ }
40731
+ return { command: [program, ...args], exports };
40732
+ }
40733
+ var RERAISED_SIGNALS = {
40734
+ SIGINT: true,
40735
+ SIGTERM: true,
40736
+ SIGHUP: true,
40737
+ SIGKILL: true,
40738
+ SIGALRM: true,
40739
+ SIGUSR2: true
40740
+ };
40741
+ var TC_CREDENTIAL_VARIABLES = ["TC_PRIVATE_KEY"];
40742
+ function exitAsCommand(outcome, program) {
40743
+ if (outcome.kind === "exited") return process.exit(outcome.code);
40744
+ if (outcome.kind === "signaled") {
40745
+ if (RERAISED_SIGNALS[outcome.signal]) process.kill(process.pid, outcome.signal);
40746
+ return process.exit(128 + (osConstants.signals[outcome.signal] ?? 0));
40747
+ }
40748
+ const reason = outcome.errno === "ENOENT" ? "command not found" : outcome.errno === "EACCES" ? "permission denied" : `cannot run (${outcome.errno ?? "unknown error"})`;
40749
+ process.stderr.write(`${CLI_NAME33} secrets run: ${safeInline(program)}: ${reason}
40750
+ `);
40751
+ return process.exit(outcome.errno === "ENOENT" ? 127 : 126);
40752
+ }
40272
40753
  function registerSecretsCommand(program, openKeyAcquisition) {
40273
40754
  const secrets = program.command("secrets").description("Encrypted secrets management");
40274
40755
  const network = secrets.command("network").description("Manage the default secrets encryption network");
@@ -40330,7 +40811,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
40330
40811
  name: "Encryption network",
40331
40812
  ok: false,
40332
40813
  detail: `${networkName} not found`,
40333
- hint: `${CLI_NAME32} secrets network init ${networkName}`
40814
+ hint: `${CLI_NAME33} secrets network init ${networkName}`
40334
40815
  }
40335
40816
  ];
40336
40817
  let secret;
@@ -40376,7 +40857,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
40376
40857
  name: "Secret access",
40377
40858
  ok: false,
40378
40859
  detail: notFound ? `${resolved.permissionPaths.vault} not found` : result.error.message,
40379
- hint: notFound ? local ? `${CLI_NAME32} secrets put ${resolved.name}${formatSecretScopeFlag(scopeOptions)} --stdin` : `${CLI_NAME32} secrets request ${resolved.name}${formatSecretScopeFlag(scopeOptions)}` : `Ask the owner profile to grant ${secretCapabilityAction("get")} and ${SECRET_DECRYPT_CAPABILITY}.`
40860
+ hint: notFound ? local ? `${CLI_NAME33} secrets put ${resolved.name}${formatSecretScopeFlag(scopeOptions)} --stdin` : `${CLI_NAME33} secrets request ${resolved.name}${formatSecretScopeFlag(scopeOptions)}` : `Ask the owner profile to grant ${secretCapabilityAction("get")} and ${SECRET_DECRYPT_CAPABILITY}.`
40380
40861
  });
40381
40862
  }
40382
40863
  } else {
@@ -40436,66 +40917,23 @@ function registerSecretsCommand(program, openKeyAcquisition) {
40436
40917
  try {
40437
40918
  const globalOpts = cmd.optsWithGlobals();
40438
40919
  const ctx = await ProfileManager.resolveContext(globalOpts);
40439
- const scopeOptions = resolveSecretScope(options);
40440
- const legacySpaceUri = await resolveSecretSpace(options.space, ctx.profile);
40441
- const secretPath = resolveSecretPath2(name, scopeOptions).permissionPaths.vault;
40920
+ const target = await resolveSecretReadTarget(ctx, name, options);
40442
40921
  if (options.output) await validatePrivateOutput(options.output, "Secret output");
40922
+ const secret = await readSecret({ ctx, host: globalOpts.host, target, options, openKeyAcquisition });
40923
+ const value = secret.value;
40443
40924
  if (options.delegation) {
40444
- const delegated = await resolveDelegatedSecretSource(
40445
- options.delegation,
40446
- secretPath,
40447
- legacySpaceUri ?? SECRETS_SPACE2
40448
- );
40449
- const effectiveHost = globalOpts.host ?? delegated.delegation.host ?? ctx.host;
40450
- const delegatedCtx = { ...ctx, host: effectiveHost };
40451
- const local = effectiveLocal(await ProfileManager.getProfile(ctx.profile).catch(() => null), options);
40452
- const scope = resolveSecretPath2(name, scopeOptions).scope;
40453
- const node = await ensureSecretsNode(delegatedCtx, options);
40454
- const value2 = await withSpinner(
40455
- `Getting secret ${name}...`,
40456
- () => readDelegatedSecretValue({
40457
- node,
40458
- delegation: delegated.delegation,
40459
- delegationCid: delegated.delegation.cid,
40460
- permissions: delegated.permissions,
40461
- secretPath,
40462
- space: legacySpaceUri ?? SECRETS_SPACE2,
40463
- name,
40464
- ...scope === void 0 ? {} : { scope },
40465
- local
40466
- })
40467
- );
40468
40925
  if (options.output) {
40469
- await writePrivateOutput(options.output, value2, "Secret output");
40926
+ await writePrivateOutput(options.output, value, "Secret output");
40470
40927
  outputJson({ name, written: options.output });
40471
40928
  return;
40472
40929
  }
40473
40930
  if (options.raw) {
40474
- process.stdout.write(value2);
40931
+ process.stdout.write(value);
40475
40932
  return;
40476
40933
  }
40477
- outputJson({ name, value: value2 });
40934
+ outputJson({ name, value });
40478
40935
  return;
40479
40936
  }
40480
- const privateKey = authOptions(options)?.privateKey;
40481
- const spaceUri = privateKey !== void 0 && options.space !== void 0 && !options.space.startsWith("tinycloud:") ? options.space : legacySpaceUri;
40482
- const result = await invokeCanonicalSecretGet({
40483
- ctx,
40484
- name,
40485
- ...scopeOptions?.scope === void 0 ? {} : { scope: scopeOptions.scope },
40486
- ...spaceUri === void 0 ? {} : { space: spaceUri },
40487
- options,
40488
- label: `Getting secret ${name}...`,
40489
- openKeyAcquisition
40490
- });
40491
- if (result.status !== "ok") {
40492
- try {
40493
- throwCanonicalSecretGetError(result, name);
40494
- } catch (error) {
40495
- throw withOperationWarnings(error, result.warnings);
40496
- }
40497
- }
40498
- const value = result.output.value;
40499
40937
  try {
40500
40938
  if (options.output) {
40501
40939
  await writePrivateOutput(options.output, value, "Secret output");
@@ -40506,9 +40944,47 @@ function registerSecretsCommand(program, openKeyAcquisition) {
40506
40944
  outputJson({ name, value });
40507
40945
  }
40508
40946
  } catch (error) {
40509
- throw withOperationWarnings(error, result.warnings);
40947
+ throw withOperationWarnings(error, secret.warnings);
40510
40948
  }
40511
- outputWarnings(operationWarnings(result.warnings));
40949
+ outputWarnings(operationWarnings(secret.warnings));
40950
+ } catch (error) {
40951
+ return handleError(error);
40952
+ }
40953
+ });
40954
+ secrets.command("run [names...]").description("Run a command with secrets in its environment").usage("[options] [names...] -- <command> [args...]").option(
40955
+ "--map <NAME=ENV_VAR>",
40956
+ "Give secret NAME to the command as environment variable ENV_VAR (repeatable)",
40957
+ (value, previous) => [...previous ?? [], value]
40958
+ ).option("--scope <scope>", "Logical secret scope").option("--space <name|uri>", "Target a non-default secrets space (short name or full URI)").option("--delegation <source>", "Delegation file path or imported profile name").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (operands, options, cmd) => {
40959
+ try {
40960
+ const plan = secretRunPlan(cmd, operands, options.map ?? []);
40961
+ const globalOpts = cmd.optsWithGlobals();
40962
+ const ctx = await ProfileManager.resolveContext(globalOpts);
40963
+ const targets = [];
40964
+ for (const name of new Set(plan.exports.map((entry) => entry.name))) {
40965
+ targets.push(await resolveSecretReadTarget(ctx, name, options));
40966
+ }
40967
+ const values = /* @__PURE__ */ new Map();
40968
+ let warnings = [];
40969
+ for (const target of targets) {
40970
+ const secret = await readSecret({ ctx, host: globalOpts.host, target, options, openKeyAcquisition });
40971
+ if (secret.value.includes("\0")) {
40972
+ throw new CLIError(
40973
+ "SECRET_NOT_EXPORTABLE",
40974
+ `Secret "${target.name}" contains a NUL character, so it can't be put in an environment variable. The command wasn't started.`,
40975
+ ExitCode.ERROR
40976
+ );
40977
+ }
40978
+ values.set(target.name, secret.value);
40979
+ warnings = mergeWarnings(warnings, secret.warnings ?? []);
40980
+ }
40981
+ outputWarnings(operationWarnings(warnings));
40982
+ await closeReplication();
40983
+ const env = { ...process.env };
40984
+ for (const variable of TC_CREDENTIAL_VARIABLES) delete env[variable];
40985
+ for (const entry of plan.exports) env[entry.variable] = values.get(entry.name);
40986
+ values.clear();
40987
+ exitAsCommand(await runCommand(plan.command, env), plan.command[0]);
40512
40988
  } catch (error) {
40513
40989
  return handleError(error);
40514
40990
  }
@@ -40620,7 +41096,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
40620
41096
  if (effectiveLocal(profile, options)) {
40621
41097
  throw new CLIError(
40622
41098
  "LOCAL_OWNER_PROFILE",
40623
- `Profile "${ctx.profile}" uses a local key, which owns its secrets: store them with ${CLI_NAME32} secrets put NAME --stdin instead of requesting them.`,
41099
+ `Profile "${ctx.profile}" uses a local key, which owns its secrets: store them with ${CLI_NAME33} secrets put NAME --stdin instead of requesting them.`,
40624
41100
  ExitCode.USAGE_ERROR
40625
41101
  );
40626
41102
  }
@@ -40743,7 +41219,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
40743
41219
 
40744
41220
  // src/commands/share.ts
40745
41221
  import { Option } from "commander";
40746
- import { CLI_NAME as CLI_NAME33 } from "@tinycloud/operations/cli-name";
41222
+ import { CLI_NAME as CLI_NAME34 } from "@tinycloud/operations/cli-name";
40747
41223
  import { ProfileLockTimeoutError as ProfileLockTimeoutError3 } from "@tinycloud/operations/state";
40748
41224
  init_errors();
40749
41225
 
@@ -40970,7 +41446,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
40970
41446
  init_profiles();
40971
41447
 
40972
41448
  // src/share/html-folder.ts
40973
- import { constants as constants2, existsSync } from "fs";
41449
+ import { constants as constants2, existsSync as existsSync2 } from "fs";
40974
41450
  import { lstat as lstat11, open as open14, readdir as readdir7, realpath as realpath3, stat as stat7 } from "fs/promises";
40975
41451
  import { basename as basename5, dirname as dirname9, isAbsolute, join as join19, relative as relativePath, resolve as resolve7, sep as sep4 } from "path";
40976
41452
 
@@ -41908,7 +42384,7 @@ function isRaceError(error) {
41908
42384
  }
41909
42385
  var PROC_FD = "/proc/self/fd";
41910
42386
  async function linuxDescriptorPath(fd) {
41911
- if (process.platform !== "linux" || !existsSync(PROC_FD)) return void 0;
42387
+ if (process.platform !== "linux" || !existsSync2(PROC_FD)) return void 0;
41912
42388
  return realpath3(`${PROC_FD}/${fd}`);
41913
42389
  }
41914
42390
  async function parentChainIsRealDirectories(root, path) {
@@ -42204,7 +42680,7 @@ function shareCliError(error, operation = "publish") {
42204
42680
  "SHARE_HISTORY_UNREADABLE",
42205
42681
  "sender history cannot be read with the current session key",
42206
42682
  1,
42207
- { hint: `Run \`${CLI_NAME33} share history recover\` to move the unreadable file aside. The old file is kept, not deleted.` }
42683
+ { hint: `Run \`${CLI_NAME34} share history recover\` to move the unreadable file aside. The old file is kept, not deleted.` }
42208
42684
  );
42209
42685
  }
42210
42686
  if (error instanceof SharePublishAuthorityError) {
@@ -42212,12 +42688,12 @@ function shareCliError(error, operation = "publish") {
42212
42688
  const profileName2 = "profileName" in failure ? failure.profileName : void 0;
42213
42689
  const localKey = "localKey" in failure && failure.localKey === true;
42214
42690
  const profileHint = profileName2 === void 0 ? "" : `--profile ${profileName2} `;
42215
- const loginHint = localKey ? `\`${CLI_NAME33} ${profileHint}auth login --method local\`` : `\`${CLI_NAME33} ${profileHint}auth login --device --manifest builtin:share-publishing\` (or \`${CLI_NAME33} ${profileHint}enable share\`)`;
42691
+ const loginHint = localKey ? `\`${CLI_NAME34} ${profileHint}auth login --method local\`` : `\`${CLI_NAME34} ${profileHint}auth login --device --manifest builtin:share-publishing\` (or \`${CLI_NAME34} ${profileHint}enable share\`)`;
42216
42692
  if (failure.kind === "caveated-session") {
42217
42693
  const holder = profileName2 === void 0 ? "this session" : `profile ${profileName2}'s session`;
42218
42694
  return new CLIError(
42219
42695
  "PERMISSION_DENIED",
42220
- `${holder} carries signed restrictions (caveats) on the authority an anyone-with-link share needs, so it cannot create the share link; nothing was shared. Approve Share publishing without restrictions on a new, dedicated profile (any unused name): \`${CLI_NAME33} init --name publisher --key-only && ${CLI_NAME33} --profile publisher enable share\``,
42696
+ `${holder} carries signed restrictions (caveats) on the authority an anyone-with-link share needs, so it cannot create the share link; nothing was shared. Approve Share publishing without restrictions on a new, dedicated profile (any unused name): \`${CLI_NAME34} init --name publisher --key-only && ${CLI_NAME34} --profile publisher enable share\``,
42221
42697
  5
42222
42698
  );
42223
42699
  }
@@ -42318,7 +42794,7 @@ function shareCliError(error, operation = "publish") {
42318
42794
  if (nodeCode === "ENOENT") return new CLIError("INVALID_ARGUMENT", "share input was not found", 2);
42319
42795
  if (nodeCode === "EISDIR") return new CLIError("INVALID_ARGUMENT", "share input must be a Markdown file", 2);
42320
42796
  const mapped = typeof nodeCode === "string" && known[nodeCode] !== void 0 ? known[nodeCode] : known[message];
42321
- const classified = mapped === void 0 ? classifyShareFailure(error, `\`${CLI_NAME33} auth login\``) : void 0;
42797
+ const classified = mapped === void 0 ? classifyShareFailure(error, `\`${CLI_NAME34} auth login\``) : void 0;
42322
42798
  if (classified !== void 0) return Object.assign(new CLIError(classified.code, classified.reason, classified.exitCode), { cause: error });
42323
42799
  if (error instanceof TypeError) return new CLIError("INVALID_ARGUMENT", "share input is invalid", 2);
42324
42800
  return new CLIError(mapped?.code ?? "INVALID_ARGUMENT", mapped ? mapped.code : "share operation failed", mapped?.exit ?? 2);
@@ -42728,7 +43204,7 @@ init_constants();
42728
43204
  init_sdk();
42729
43205
  init_host();
42730
43206
  init_theme();
42731
- import { CLI_NAME as CLI_NAME34 } from "@tinycloud/operations/cli-name";
43207
+ import { CLI_NAME as CLI_NAME35 } from "@tinycloud/operations/cli-name";
42732
43208
  import { ACCOUNT_REGISTRY_SPACE } from "@tinycloud/node-sdk";
42733
43209
  import { randomBytes as randomBytes9 } from "crypto";
42734
43210
  import { mkdir as mkdir7, writeFile as writeFile4 } from "fs/promises";
@@ -42775,7 +43251,7 @@ function registerSpaceCommand(program) {
42775
43251
  } else {
42776
43252
  const profile = await ProfileManager.getProfile(ctx.profile);
42777
43253
  const cap = "tinycloud.kv:account:spaces/:get,list";
42778
- const hint = resolveProfilePosture(profile) === "delegate-session" ? `Ask the space owner to grant it. Create the request with: ${CLI_NAME34} --profile ${ctx.profile} auth request --cap ${cap} --emit` : `Grant it to this session with: ${CLI_NAME34} --profile ${ctx.profile} auth request --grant --cap ${cap}. The default OpenKey owner sign-in covers only the default space, so it includes neither.`;
43254
+ const hint = resolveProfilePosture(profile) === "delegate-session" ? `Ask the space owner to grant it. Create the request with: ${CLI_NAME35} --profile ${ctx.profile} auth request --cap ${cap} --emit` : `Grant it to this session with: ${CLI_NAME35} --profile ${ctx.profile} auth request --grant --cap ${cap}. The default OpenKey owner sign-in covers only the default space, so it includes neither.`;
42779
43255
  throw new CLIError(
42780
43256
  "PERMISSION_DENIED",
42781
43257
  `Listing spaces needs tinycloud.kv/get and tinycloud.kv/list on the account space's spaces/ registry (${accountSpaceId ?? ACCOUNT_REGISTRY_SPACE}), or tinycloud.space/list. This session holds neither.`,
@@ -42812,10 +43288,10 @@ function registerSpaceCommand(program) {
42812
43288
 
42813
43289
  A delegate cannot host a space \u2014 only its owner (root authority) can. This
42814
43290
  emits a tinycloud.host.request artifact naming the space and its owner so you
42815
- can hand it to the owner; they run \`${CLI_NAME34} space host <name>\` and confirm.
43291
+ can hand it to the owner; they run \`${CLI_NAME35} space host <name>\` and confirm.
42816
43292
 
42817
43293
  If you ARE the owner of the resolved space, this refuses and tells you to host
42818
- it directly with \`${CLI_NAME34} space host <name>\` (no request needed).
43294
+ it directly with \`${CLI_NAME35} space host <name>\` (no request needed).
42819
43295
  `).action(async (name, options, cmd) => {
42820
43296
  try {
42821
43297
  const globalOpts = cmd.optsWithGlobals();
@@ -42826,7 +43302,7 @@ it directly with \`${CLI_NAME34} space host <name>\` (no request needed).
42826
43302
  if (await isRootAuthority(spaceId, ctx.profile)) {
42827
43303
  throw new CLIError(
42828
43304
  "ALREADY_ROOT_AUTHORITY",
42829
- `You are the owner of ${spaceId}. Host it directly: ${CLI_NAME34} space host ${spaceName}`,
43305
+ `You are the owner of ${spaceId}. Host it directly: ${CLI_NAME35} space host ${spaceName}`,
42830
43306
  ExitCode.USAGE_ERROR
42831
43307
  );
42832
43308
  }
@@ -42912,7 +43388,7 @@ init_sdk();
42912
43388
  init_space();
42913
43389
  init_host();
42914
43390
  init_theme();
42915
- import { CLI_NAME as CLI_NAME35 } from "@tinycloud/operations/cli-name";
43391
+ import { CLI_NAME as CLI_NAME36 } from "@tinycloud/operations/cli-name";
42916
43392
  import { writeFile as writeFile5 } from "fs/promises";
42917
43393
  import { resolve as resolve8 } from "path";
42918
43394
  async function dbHandle(node, dbName, spaceInput, profileName2) {
@@ -42936,12 +43412,12 @@ database for simple apps, or pass --db to target a named database. Pass
42936
43412
  --space to target a non-primary space (e.g. the manifest "applications" space).
42937
43413
 
42938
43414
  Common workflows:
42939
- $ ${CLI_NAME35} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
42940
- $ ${CLI_NAME35} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["ship docs"]'
42941
- $ ${CLI_NAME35} sql query "SELECT id, body FROM notes ORDER BY id"
42942
- $ ${CLI_NAME35} sql query "SELECT * FROM events WHERE type = ?" --db analytics --params '["signup"]'
42943
- $ ${CLI_NAME35} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
42944
- $ ${CLI_NAME35} sql export --db analytics --output analytics.db
43415
+ $ ${CLI_NAME36} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
43416
+ $ ${CLI_NAME36} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["ship docs"]'
43417
+ $ ${CLI_NAME36} sql query "SELECT id, body FROM notes ORDER BY id"
43418
+ $ ${CLI_NAME36} sql query "SELECT * FROM events WHERE type = ?" --db analytics --params '["signup"]'
43419
+ $ ${CLI_NAME36} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
43420
+ $ ${CLI_NAME36} sql export --db analytics --output analytics.db
42945
43421
 
42946
43422
  Commands:
42947
43423
  query Read rows with SELECT statements
@@ -42958,10 +43434,10 @@ Tips:
42958
43434
  sql.command("query <sql>").description("Run a read-only SELECT query").option("--db <name>", "SQLite database name within the current space", "default").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("--params <json>", "Bind parameters as a JSON array for ? placeholders").addHelpText("after", `
42959
43435
 
42960
43436
  Examples:
42961
- $ ${CLI_NAME35} sql query "SELECT * FROM notes ORDER BY id"
42962
- $ ${CLI_NAME35} sql query "SELECT * FROM notes WHERE id = ?" --params '[42]'
42963
- $ ${CLI_NAME35} sql query "SELECT count(*) AS total FROM events" --db analytics --json
42964
- $ ${CLI_NAME35} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
43437
+ $ ${CLI_NAME36} sql query "SELECT * FROM notes ORDER BY id"
43438
+ $ ${CLI_NAME36} sql query "SELECT * FROM notes WHERE id = ?" --params '[42]'
43439
+ $ ${CLI_NAME36} sql query "SELECT count(*) AS total FROM events" --db analytics --json
43440
+ $ ${CLI_NAME36} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
42965
43441
 
42966
43442
  Output:
42967
43443
  Human output is formatted as a table. Piped output or --json returns
@@ -43002,11 +43478,11 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
43002
43478
  sql.command("execute <sql>").description("Run a write or schema statement").option("--db <name>", "SQLite database name within the current space", "default").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("--params <json>", "Bind parameters as a JSON array for ? placeholders").addHelpText("after", `
43003
43479
 
43004
43480
  Examples:
43005
- $ ${CLI_NAME35} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
43006
- $ ${CLI_NAME35} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["first note"]'
43007
- $ ${CLI_NAME35} sql execute "UPDATE notes SET body = ? WHERE id = ?" --params '["edited", 1]'
43008
- $ ${CLI_NAME35} sql execute "DROP TABLE old_notes" --db archive
43009
- $ ${CLI_NAME35} sql execute "DELETE FROM conversation WHERE id = ?" --space applications --db xyz.tinycloud.listen/conversations --params '["abc"]'
43481
+ $ ${CLI_NAME36} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
43482
+ $ ${CLI_NAME36} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["first note"]'
43483
+ $ ${CLI_NAME36} sql execute "UPDATE notes SET body = ? WHERE id = ?" --params '["edited", 1]'
43484
+ $ ${CLI_NAME36} sql execute "DROP TABLE old_notes" --db archive
43485
+ $ ${CLI_NAME36} sql execute "DELETE FROM conversation WHERE id = ?" --space applications --db xyz.tinycloud.listen/conversations --params '["abc"]'
43010
43486
 
43011
43487
  Output:
43012
43488
  Returns JSON with the changed row count and last inserted row id when available.
@@ -43035,9 +43511,9 @@ Output:
43035
43511
  sql.command("export").description("Export a SQLite database as a binary .db file").option("--db <name>", "SQLite database name within the current space", "default").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("-o, --output <file>", "Output file path", "export.db").addHelpText("after", `
43036
43512
 
43037
43513
  Examples:
43038
- $ ${CLI_NAME35} sql export
43039
- $ ${CLI_NAME35} sql export --db analytics --output analytics.db
43040
- $ ${CLI_NAME35} sql export --space applications --db xyz.tinycloud.listen/conversations --output listen.db
43514
+ $ ${CLI_NAME36} sql export
43515
+ $ ${CLI_NAME36} sql export --db analytics --output analytics.db
43516
+ $ ${CLI_NAME36} sql export --space applications --db xyz.tinycloud.listen/conversations --output listen.db
43041
43517
 
43042
43518
  Output:
43043
43519
  Writes the database file locally and returns JSON with the path and size.
@@ -43070,19 +43546,19 @@ Output:
43070
43546
  sql.command("copy").description("Copy rows between SQL databases (optionally across spaces)").requiredOption("--from-db <name>", "Source database name").requiredOption("--to-db <name>", "Destination database name").option("--from-space <name|uri>", "Source space (defaults to primary)").option("--to-space <name|uri>", "Destination space (defaults to primary)").option("--table <name...>", "Restrict copy to specific tables (repeat or comma-separated)").option("--dry-run", "Print the plan without writing", false).addHelpText("after", `
43071
43547
 
43072
43548
  Examples:
43073
- $ ${CLI_NAME35} sql copy --from-db com.tinycloud.conversation-sync/conversations \\
43549
+ $ ${CLI_NAME36} sql copy --from-db com.tinycloud.conversation-sync/conversations \\
43074
43550
  --to-db xyz.tinycloud.listen/conversations \\
43075
43551
  --space applications --dry-run
43076
- $ ${CLI_NAME35} sql copy --from-space applications --from-db com.foo/data \\
43552
+ $ ${CLI_NAME36} sql copy --from-space applications --from-db com.foo/data \\
43077
43553
  --to-space applications --to-db com.bar/data \\
43078
43554
  --table conversation --table participant
43079
43555
 
43080
43556
  Notes:
43081
43557
  - Refuses to run when (resolved space, db) is identical for source and destination.
43082
- - Does NOT create destination tables. Run the target app once (or use \`${CLI_NAME35} sql execute\`)
43558
+ - Does NOT create destination tables. Run the target app once (or use \`${CLI_NAME36} sql execute\`)
43083
43559
  to materialize the schema before copying.
43084
43560
  - One row at a time; suitable for small/medium datasets. Large copies should
43085
- use \`${CLI_NAME35} sql export\` + bulk import.
43561
+ use \`${CLI_NAME36} sql export\` + bulk import.
43086
43562
  - Authorization: the active session/delegation must cover sql/read on source
43087
43563
  AND sql/write on destination. Otherwise the relevant operation will fail.
43088
43564
  `).action(async (options, cmd) => {
@@ -43182,7 +43658,7 @@ init_errors();
43182
43658
  init_formatter();
43183
43659
  init_theme();
43184
43660
  init_permissions();
43185
- import { CLI_NAME as CLI_NAME36 } from "@tinycloud/operations/cli-name";
43661
+ import { CLI_NAME as CLI_NAME37 } from "@tinycloud/operations/cli-name";
43186
43662
  import {
43187
43663
  NodeWasmBindings as NodeWasmBindings2
43188
43664
  } from "@tinycloud/node-sdk";
@@ -43456,7 +43932,7 @@ function formatStatus(summary) {
43456
43932
  lines.push(`Default profile: ${theme.value(summary.defaultProfile)}`);
43457
43933
  lines.push("");
43458
43934
  if (summary.profiles.length === 0) {
43459
- lines.push(theme.muted(`No profiles configured. Run: ${CLI_NAME36} init`));
43935
+ lines.push(theme.muted(`No profiles configured. Run: ${CLI_NAME37} init`));
43460
43936
  return `${lines.join("\n")}
43461
43937
  `;
43462
43938
  }
@@ -43545,11 +44021,11 @@ function plural(count, label) {
43545
44021
  init_theme();
43546
44022
  init_errors();
43547
44023
  import { execSync } from "child_process";
43548
- import { readFileSync as readFileSync2 } from "fs";
44024
+ import { readFileSync as readFileSync3 } from "fs";
43549
44025
  var PACKAGE_NAME = "@tinycloud/cli";
43550
44026
  function getCurrentVersion() {
43551
44027
  const pkg = JSON.parse(
43552
- readFileSync2(new URL("../package.json", import.meta.url), "utf-8")
44028
+ readFileSync3(new URL("../package.json", import.meta.url), "utf-8")
43553
44029
  );
43554
44030
  return pkg.version;
43555
44031
  }
@@ -43935,6 +44411,7 @@ function registerVarsCommand(program) {
43935
44411
  // src/command-registry.ts
43936
44412
  function registerTinyCloudCommands(program) {
43937
44413
  registerInitCommand(program);
44414
+ registerOnboardCommand(program);
43938
44415
  registerContextCommand(program);
43939
44416
  registerAuthCommand(program);
43940
44417
  registerEnableCommand(program);