@tinycloud/cli 1.4.0 → 1.5.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9049,7 +9049,8 @@ async function assertPendingRemoteLogoutSession(name, expected) {
9049
9049
  }
9050
9050
  async function clearProfileCredentials(name, syncDirectory = false, legacyCleanup = false) {
9051
9051
  const profile = await ProfileManager.getProfile(name);
9052
- await ProfileManager.setProfileDurable(name, { ...profile, authSignedOut: true, ...legacyCleanup ? { legacyReplicaCleanup: true } : {} });
9052
+ const { namedKeys: _namedKeys, ...signedOutProfile } = profile;
9053
+ await ProfileManager.setProfileDurable(name, { ...signedOutProfile, authSignedOut: true, ...legacyCleanup ? { legacyReplicaCleanup: true } : {} });
9053
9054
  let failed = false;
9054
9055
  let error;
9055
9056
  try {
@@ -9481,7 +9482,8 @@ async function bootstrapDelegatedSession(ctx, delegation) {
9481
9482
  jwk,
9482
9483
  verificationMethod: sessionDid
9483
9484
  };
9484
- const profile = { ...previousProfile, sessionDid, spaceId: delegation.spaceId };
9485
+ const { namedKeys: _namedKeys, ...previousFields } = previousProfile;
9486
+ const profile = { ...previousFields, sessionDid, spaceId: delegation.spaceId };
9485
9487
  try {
9486
9488
  await ProfileManager.setSession(ctx.profile, session);
9487
9489
  await ProfileManager.setProfile(ctx.profile, profile);
@@ -30611,7 +30613,7 @@ function createEncryptedProfileHistory(profileName2, sessionSigner) {
30611
30613
  throw new ShareHistoryReadError();
30612
30614
  }
30613
30615
  };
30614
- const parseEnvelope = (text) => {
30616
+ const parseEnvelope2 = (text) => {
30615
30617
  let envelope;
30616
30618
  try {
30617
30619
  envelope = JSON.parse(text);
@@ -30633,7 +30635,7 @@ function createEncryptedProfileHistory(profileName2, sessionSigner) {
30633
30635
  const preparedSalt = async (profile) => {
30634
30636
  const file = await readIfPresent(await path(profile));
30635
30637
  if (file === void 0) return crypto.getRandomValues(new Uint8Array(16));
30636
- const salt = unb64(parseEnvelope(file.toString("utf8")).kdfSalt);
30638
+ const salt = unb64(parseEnvelope2(file.toString("utf8")).kdfSalt);
30637
30639
  if (salt.length < 16) throw new ShareHistoryUnreadableError();
30638
30640
  return salt;
30639
30641
  };
@@ -30644,7 +30646,7 @@ function createEncryptedProfileHistory(profileName2, sessionSigner) {
30644
30646
  if (legacy === void 0 || legacy.length <= 12) return [];
30645
30647
  return decryptRecords(ready.legacyKey, new Uint8Array(legacy.subarray(0, 12)), new Uint8Array(legacy.subarray(12)));
30646
30648
  }
30647
- const envelope = parseEnvelope(file.toString("utf8"));
30649
+ const envelope = parseEnvelope2(file.toString("utf8"));
30648
30650
  if (envelope.kdfSalt !== b64(ready.salt)) throw saltChanged;
30649
30651
  const iv = unb64(envelope.iv);
30650
30652
  const ciphertext = unb64(envelope.ciphertext);
@@ -31281,9 +31283,16 @@ async function localKeySignIn(options) {
31281
31283
  privateKey: options.privateKey,
31282
31284
  host: options.host,
31283
31285
  autoCreateSpace: true,
31286
+ ...options.keyAbilities ? { additionalRawAbilities: options.keyAbilities } : {},
31284
31287
  ...replication ? { replication } : {}
31285
31288
  });
31286
31289
  await node.signIn();
31290
+ let networkReady;
31291
+ if (options.keyAbilities && !node.freshEncryptionNetworkCreated) {
31292
+ networkReady = await node.ensureEncryptionNetwork().then(() => true, () => false);
31293
+ } else if (options.keyAbilities) {
31294
+ networkReady = true;
31295
+ }
31287
31296
  const address = await new PrivateKeySigner(options.privateKey).getAddress();
31288
31297
  const session = node.session;
31289
31298
  if (!session) {
@@ -31299,7 +31308,8 @@ async function localKeySignIn(options) {
31299
31308
  verificationMethod: session.verificationMethod,
31300
31309
  siwe: session.siwe,
31301
31310
  signature: session.signature,
31302
- ...node.replicaStamp === void 0 ? {} : { replicaStamp: node.replicaStamp }
31311
+ ...node.replicaStamp === void 0 ? {} : { replicaStamp: node.replicaStamp },
31312
+ ...networkReady === void 0 ? {} : { networkReady }
31303
31313
  };
31304
31314
  }
31305
31315
 
@@ -31638,13 +31648,87 @@ function verifyScopedLogin(data, key, sessionDid, requested, expected = {}) {
31638
31648
  return { session, legacyNested };
31639
31649
  }
31640
31650
 
31651
+ // src/auth/login-commit.ts
31652
+ import { lstat as lstat6 } from "fs/promises";
31653
+
31654
+ // src/lib/named-keys.ts
31655
+ init_constants();
31656
+ init_errors();
31657
+ import { extractRecapAttenuations } from "@tinycloud/sdk-core";
31658
+ import {
31659
+ EncryptionResourceError,
31660
+ KEY_CREATE_ABILITY,
31661
+ formatPrefixResource,
31662
+ nodeSupportsNamedKeys
31663
+ } from "@tinycloud/sdk-services/encryption/resource";
31664
+ import { DECRYPT_ACTION } from "@tinycloud/sdk-services";
31665
+ var NAMED_KEYS_NETWORK = "default";
31666
+ function namedKeysNetworkId(ownerDid) {
31667
+ return `urn:tinycloud:encryption:${ownerDid}:${NAMED_KEYS_NETWORK}`;
31668
+ }
31669
+ function parseKeysPrefix(input, ownerDid) {
31670
+ const prefix = input.endsWith("/") ? input.slice(0, -1) : input;
31671
+ const invalid3 = (reason) => new CLIError("INVALID_KEY_PREFIX", `Invalid \`--keys\` folder "${input}": ${reason}`, ExitCode.USAGE_ERROR);
31672
+ if (prefix.length === 0) throw invalid3("the folder can't be empty.");
31673
+ if (prefix === "apps" || prefix.startsWith("apps/")) {
31674
+ throw invalid3("folders under `apps/` belong to apps and can't be used from the CLI.");
31675
+ }
31676
+ try {
31677
+ return { prefix, resource: formatPrefixResource(namedKeysNetworkId(ownerDid), prefix) };
31678
+ } catch (error) {
31679
+ if (error instanceof EncryptionResourceError) throw invalid3(error.message);
31680
+ throw error;
31681
+ }
31682
+ }
31683
+ function keyAbilitiesFor(keys) {
31684
+ return { [keys.resource]: [KEY_CREATE_ABILITY, DECRYPT_ACTION] };
31685
+ }
31686
+ function keyPathInPrefix(prefix, path) {
31687
+ return path.startsWith(`${prefix}/`) && path.length > prefix.length + 1;
31688
+ }
31689
+ async function assertNodeSupportsNamedKeys(host, fetchImpl = fetch) {
31690
+ let info = null;
31691
+ try {
31692
+ const response = await fetchImpl(`${host}/info`);
31693
+ if (response.ok) info = await response.json();
31694
+ } catch {
31695
+ info = null;
31696
+ }
31697
+ if (!nodeSupportsNamedKeys(info)) {
31698
+ throw new CLIError(
31699
+ "NAMED_KEYS_UNAVAILABLE",
31700
+ `${host} doesn't support named keys, so \`--keys\` can't be used there. Nothing was signed.`,
31701
+ ExitCode.NODE_ERROR
31702
+ );
31703
+ }
31704
+ }
31705
+ function namedKeysGrantIn(siwe, keys) {
31706
+ let recap = {};
31707
+ try {
31708
+ recap = siwe === void 0 ? {} : extractRecapAttenuations(siwe);
31709
+ } catch {
31710
+ return "none";
31711
+ }
31712
+ const row = recap[keys.resource] ?? {};
31713
+ const wanted = keyAbilitiesFor(keys)[keys.resource];
31714
+ const granted = wanted.filter((ability) => ability in row);
31715
+ if (granted.length === 0) return "none";
31716
+ return granted.length === wanted.length && Object.keys(row).length === wanted.length ? "both" : "partial";
31717
+ }
31718
+ function withVerifiedNamedKeys(profile, session) {
31719
+ if (profile.namedKeys === void 0) return profile;
31720
+ const siwe = typeof session === "object" && session !== null ? session.siwe : void 0;
31721
+ if (typeof siwe === "string" && namedKeysGrantIn(siwe, profile.namedKeys) === "both") return profile;
31722
+ const { namedKeys: _dropped, ...rest } = profile;
31723
+ return rest;
31724
+ }
31725
+
31641
31726
  // src/auth/login-commit.ts
31642
31727
  init_constants();
31643
31728
  init_profiles();
31644
31729
  init_storage();
31645
31730
  init_errors();
31646
31731
  init_space();
31647
- import { lstat as lstat6 } from "fs/promises";
31648
31732
  import { join as join13 } from "path";
31649
31733
  import { REPLICA_LOGOUT_PENDING_FILE as REPLICA_LOGOUT_PENDING_FILE3 } from "@tinycloud/node-sdk";
31650
31734
  import { CLI_NAME as CLI_NAME12 } from "@tinycloud/operations/cli-name";
@@ -31758,13 +31842,14 @@ async function commitLogin(profileName2, snapshot, commit) {
31758
31842
  assertSessionReplaceable(profileName2, current, commit.approved.ownerDid, commit.approved.scope, newExpiresAt);
31759
31843
  }
31760
31844
  try {
31845
+ const profile = withVerifiedNamedKeys(commit.profile, commit.session);
31761
31846
  await ProfileManager.setKey(profileName2, commit.key);
31762
31847
  await ProfileManager.setSession(profileName2, commit.session);
31763
31848
  if (current.profile?.authSignedOut === true || commit.durable === true) {
31764
31849
  await syncAuthCredentialFiles(profilePath7(profileName2));
31765
- await ProfileManager.setProfileDurable(profileName2, { ...commit.profile, authSignedOut: false });
31850
+ await ProfileManager.setProfileDurable(profileName2, { ...profile, authSignedOut: false });
31766
31851
  } else {
31767
- await ProfileManager.setProfile(profileName2, commit.profile);
31852
+ await ProfileManager.setProfile(profileName2, profile);
31768
31853
  }
31769
31854
  } catch (error) {
31770
31855
  const failures = await restore(profileName2, current);
@@ -32209,6 +32294,7 @@ async function loginWithDeviceAuthorization(input) {
32209
32294
  operatorType: existing?.operatorType ?? "human",
32210
32295
  authMethod: "openkey"
32211
32296
  }, result.primary);
32297
+ delete profile.namedKeys;
32212
32298
  await commitLogin(input.profileName, snapshot, {
32213
32299
  key,
32214
32300
  session: result.session,
@@ -33965,7 +34051,7 @@ async function finishProfileLogout(ctx, options = {}) {
33965
34051
  function registerAuthCommand(program, options) {
33966
34052
  const auth = program.command("auth").description("Authentication management");
33967
34053
  registerAuthGrantsCommand(auth, { openKeyAcquisition: options?.openKeyAcquisition });
33968
- auth.command("login").description("Authenticate with TinyCloud").option("--device", "Approve on another device (e.g. a phone) through OpenKey device authorization; requires a KV-scoped --manifest (SQL and secret decrypt need browser or --paste login)").option("--paste", "Wait up to 5 minutes for a signed code from OpenKey on stdin instead of a browser callback (Safari may require paste)").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--method <method>", "Authentication method: local or openkey").option("--manifest <fileOrBase64>", `Request only this manifest's permissions (one space, plus raw encryption network entries such as a secrets decrypt grant); ${SHARE_PUBLISHING_MANIFEST_REF} covers \`${CLI_NAME17} share publish\``).option("--expiry <duration>", "OpenKey session lifetime, e.g. 1h or 7d (device login: at most 30d, default 30d)").option("--owner <did>", "Sign in as this owner (did:pkh:eip155:CHAIN:ADDRESS): OpenKey preselects that key, e.g. one that is not the account's primary key, and an approval by any other identity is refused. With --manifest, also names the secrets owner for a manifest's `secrets` on a profile with no recorded owner").option("--replace-session", "Scoped or device login: replace this profile's live session even though the new scope would narrow, change or shorten it (prefer a new profile)").option("--force", "Replica-enabled OpenKey login: replace the stored session key even when recorded shares could not be checked or hold unexpired shares that need the current key").option("--replication-prefix <prefix>", "Add replication get+sync authority for a folder prefix ending in /, or root (repeatable)", (prefix, values = []) => [...values, prefix]).option("--replication-allow-secrets", "Allow replication of secret material (network-wide decrypt warning)").action(async (options2, cmd) => {
34054
+ auth.command("login").description("Authenticate with TinyCloud").option("--device", "Approve on another device (e.g. a phone) through OpenKey device authorization; requires a KV-scoped --manifest (SQL and secret decrypt need browser or --paste login)").option("--paste", "Wait up to 5 minutes for a signed code from OpenKey on stdin instead of a browser callback (Safari may require paste)").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--method <method>", "Authentication method: local or openkey").option("--manifest <fileOrBase64>", `Request only this manifest's permissions (one space, plus raw encryption network entries such as a secrets decrypt grant); ${SHARE_PUBLISHING_MANIFEST_REF} covers \`${CLI_NAME17} share publish\``).option("--expiry <duration>", "OpenKey session lifetime, e.g. 1h or 7d (device login: at most 30d, default 30d)").option("--owner <did>", "Sign in as this owner (did:pkh:eip155:CHAIN:ADDRESS): OpenKey preselects that key, e.g. one that is not the account's primary key, and an approval by any other identity is refused. With --manifest, also names the secrets owner for a manifest's `secrets` on a profile with no recorded owner").option("--replace-session", "Scoped or device login: replace this profile's live session even though the new scope would narrow, change or shorten it (prefer a new profile)").option("--force", "Replica-enabled OpenKey login: replace the stored session key even when recorded shares could not be checked or hold unexpired shares that need the current key").option("--keys <folder>", "Also let this sign-in create and use encryption keys in this folder (for example `qa`). The node must support named keys. With OpenKey, the first time on a profile also needs `--owner`.").option("--replication-prefix <prefix>", "Add replication get+sync authority for a folder prefix ending in /, or root (repeatable)", (prefix, values = []) => [...values, prefix]).option("--replication-allow-secrets", "Allow replication of secret material (network-wide decrypt warning)").action(async (options2, cmd) => {
33969
34055
  try {
33970
34056
  if (options2.device && options2.paste) {
33971
34057
  throw new CLIError("INVALID_ARGUMENT", "--device and --paste are mutually exclusive.", ExitCode.USAGE_ERROR);
@@ -33981,6 +34067,9 @@ function registerAuthCommand(program, options) {
33981
34067
  ExitCode.USAGE_ERROR
33982
34068
  );
33983
34069
  }
34070
+ if (options2.keys !== void 0 && (options2.device || options2.manifest)) {
34071
+ throw new CLIError("INVALID_ARGUMENT", "`--keys` can't be used with `--device` or `--manifest`.", ExitCode.USAGE_ERROR);
34072
+ }
33984
34073
  const globalOpts = cmd.optsWithGlobals();
33985
34074
  const ctx = await ProfileManager.resolveContext(globalOpts, { storedSession: "unused" });
33986
34075
  await recoverPendingLogout(ctx.profile);
@@ -34042,6 +34131,8 @@ function registerAuthCommand(program, options) {
34042
34131
  );
34043
34132
  }
34044
34133
  method = options2.method;
34134
+ } else if (options2.keys !== void 0) {
34135
+ method = profileConfig?.authMethod === "openkey" || options2.paste || options2.owner ? "openkey" : "local";
34045
34136
  } else {
34046
34137
  method = scoped ? "openkey" : await promptAuthMethod();
34047
34138
  }
@@ -34054,7 +34145,7 @@ function registerAuthCommand(program, options) {
34054
34145
  }
34055
34146
  if (method === "local") {
34056
34147
  buildReplicationLoginRequest(void 0, replicationOptions);
34057
- await handleLocalAuth(ctx.profile, ctx.host, { replication: replicationOptions });
34148
+ await handleLocalAuth(ctx.profile, ctx.host, { replication: replicationOptions, keys: options2.keys });
34058
34149
  await persistReplicationLogin(ctx.profile, replicationOptions);
34059
34150
  if (replicationOptions.allowSecrets) writeReplicationSecretsWarning();
34060
34151
  } else {
@@ -34067,7 +34158,8 @@ function registerAuthCommand(program, options) {
34067
34158
  expectedOwner: owner,
34068
34159
  replaceSession: options2.replaceSession === true,
34069
34160
  persistHost,
34070
- force: options2.force === true
34161
+ force: options2.force === true,
34162
+ keys: options2.keys
34071
34163
  });
34072
34164
  if (!preparedCommitted) await persistReplicationLogin(ctx.profile, replicationOptions);
34073
34165
  if (replicationOptions.allowSecrets) writeReplicationSecretsWarning();
@@ -35333,7 +35425,8 @@ async function rotateAuthKey(profileName2, host, options = {}) {
35333
35425
  );
35334
35426
  }
35335
35427
  const result2 = await handleLocalAuth(profileName2, host, {
35336
- emitOutput: false
35428
+ emitOutput: false,
35429
+ keys: profile.namedKeys?.prefix
35337
35430
  });
35338
35431
  outputRotationResult(result2.profile, profileName2, oldDid, "local");
35339
35432
  return;
@@ -35358,8 +35451,9 @@ async function rotateAuthKey(profileName2, host, options = {}) {
35358
35451
  }
35359
35452
  await ProfileManager.setKey(profileName2, jwk);
35360
35453
  await ProfileManager.clearSession(profileName2);
35454
+ const { namedKeys: _rotated, ...currentFields } = current;
35361
35455
  await ProfileManager.setProfile(profileName2, {
35362
- ...current,
35456
+ ...currentFields,
35363
35457
  host,
35364
35458
  did,
35365
35459
  sessionDid: did,
@@ -35371,7 +35465,8 @@ async function rotateAuthKey(profileName2, host, options = {}) {
35371
35465
  const result = await refreshOpenKeySession(profileName2, host, {
35372
35466
  paste: options.paste,
35373
35467
  noPopup: options.noPopup,
35374
- permissions
35468
+ permissions,
35469
+ ...profile.namedKeys === void 0 ? {} : { keys: profile.namedKeys.prefix, expectedOwner: profile.ownerDid }
35375
35470
  });
35376
35471
  outputRotationResult(result.profile, profileName2, oldDid, "openkey");
35377
35472
  }
@@ -35420,12 +35515,12 @@ async function persistCurrentLocalSession(profileName2, profile, session, replic
35420
35515
  signature: session.signature,
35421
35516
  ...replicaStamp === void 0 ? {} : { replicaStamp }
35422
35517
  });
35423
- if (profile.sessionDid !== session.verificationMethod || profile.spaceId !== session.spaceId) {
35424
- await ProfileManager.updateProfile(profileName2, (current) => ({
35518
+ if (profile.sessionDid !== session.verificationMethod || profile.spaceId !== session.spaceId || profile.namedKeys !== void 0) {
35519
+ await ProfileManager.updateProfile(profileName2, (current) => withVerifiedNamedKeys({
35425
35520
  ...current,
35426
35521
  sessionDid: session.verificationMethod,
35427
35522
  spaceId: session.spaceId
35428
- }));
35523
+ }, session));
35429
35524
  }
35430
35525
  });
35431
35526
  }
@@ -35457,11 +35552,14 @@ async function handleLocalAuth(profileName2, host, options = {}) {
35457
35552
  process.stderr.write(formatField("DID", did) + "\n\n");
35458
35553
  }
35459
35554
  }
35555
+ const namedKeys = options.keys === void 0 ? void 0 : parseKeysPrefix(options.keys, did);
35556
+ if (namedKeys !== void 0) await assertNodeSupportsNamedKeys(host);
35460
35557
  const sessionResult = await withSpinner("Signing in...", async () => {
35461
35558
  const replication = options.replication ?? profile?.replication;
35462
35559
  return localKeySignIn({
35463
35560
  privateKey,
35464
35561
  host,
35562
+ ...namedKeys === void 0 ? {} : { keyAbilities: keyAbilitiesFor(namedKeys) },
35465
35563
  ...replication?.prefixes.length ? { profile: profileName2, replication } : {}
35466
35564
  });
35467
35565
  });
@@ -35496,9 +35594,12 @@ async function handleLocalAuth(profileName2, host, options = {}) {
35496
35594
  operatorType: profile?.operatorType ?? "human",
35497
35595
  authMethod: "local",
35498
35596
  privateKey,
35499
- address
35597
+ address,
35598
+ ...namedKeys === void 0 ? {} : { namedKeys }
35500
35599
  };
35600
+ if (namedKeys === void 0) delete updatedProfile.namedKeys;
35501
35601
  await commitLogin(profileName2, snapshot, { key, session, profile: updatedProfile });
35602
+ if (namedKeys !== void 0 && sessionResult.networkReady === false) process.stderr.write(KEYS_FOLDER_NOT_READY);
35502
35603
  if (options.emitOutput ?? true) {
35503
35604
  outputJson({
35504
35605
  authenticated: true,
@@ -35660,6 +35761,104 @@ async function refreshPreparedOpenKeySession(params) {
35660
35761
  throw error;
35661
35762
  }
35662
35763
  }
35764
+ var KEYS_APPROVAL_NOTE = (folder) => `On the OpenKey page, the row that mentions tinycloud.encryption lets this profile create and use keys in the folder "${folder}/". Keep every row ticked. To sign in without keys, untick that whole row and nothing else; any other change makes OpenKey refuse the sign-in.
35765
+ `;
35766
+ var keysNotGrantedAfterLogin = (profileName2) => `You're signed in, but without named keys: the tinycloud.encryption permission wasn't approved. To add them, sign in again with \`${CLI_NAME17} --profile ${profileName2} auth login --keys <folder>\` and leave the row that mentions tinycloud.encryption ticked.
35767
+ `;
35768
+ var KEYS_FOLDER_NOT_READY = `You're signed in, but your encryption keys aren't set up yet. Try \`${CLI_NAME17} keys ensure <folder>/<name>\` again in a minute.
35769
+ `;
35770
+ async function readyNamedKeysNetwork(node) {
35771
+ try {
35772
+ await node.ensureEncryptionNetwork();
35773
+ } catch {
35774
+ process.stderr.write(KEYS_FOLDER_NOT_READY);
35775
+ }
35776
+ }
35777
+ function isNarrowedRecapMismatch(error) {
35778
+ const text = error instanceof Error ? `${error.code ?? ""} ${error.message}` : String(error);
35779
+ return text.includes("narrowed_recap_mismatch");
35780
+ }
35781
+ async function refreshPreparedKeysSession(params) {
35782
+ const { profileName: profileName2, host, profile, snapshot, options, expectedOwner, expiry } = params;
35783
+ const storedOwner = snapshot.session && snapshot.key && profile.sessionDid ? verifySignedSession(snapshot.session, snapshot.key, profile.sessionDid).ownerDid : void 0;
35784
+ const owner = options.expectedOwner ? canonicalOwnerDid(options.expectedOwner) : storedOwner ?? (profile.ownerDid ? canonicalOwnerDid(profile.ownerDid) : void 0);
35785
+ if (!owner) {
35786
+ throw new CLIError(
35787
+ "OWNER_REQUIRED",
35788
+ "The first OpenKey sign-in with `--keys` needs your account: add `--owner did:pkh:eip155:<chain>:<address>`. Nothing was started.",
35789
+ ExitCode.AUTH_REQUIRED
35790
+ );
35791
+ }
35792
+ if (expectedOwner && normalizePkhIdentifier(owner) !== normalizePkhIdentifier(expectedOwner)) {
35793
+ throw new CLIError("OPENKEY_OWNER_MISMATCH", "The verified owner differs from this profile. No approval was started.", ExitCode.PERMISSION_DENIED);
35794
+ }
35795
+ const namedKeys = parseKeysPrefix(options.keys, owner);
35796
+ await assertNodeSupportsNamedKeys(host);
35797
+ if (options.force !== true) await assertShareHistoryAllowsKeyDrop(profileName2, "login");
35798
+ const node = new TinyCloudNode3({
35799
+ host,
35800
+ signer: ownerIdentitySigner(owner),
35801
+ additionalRawAbilities: keyAbilitiesFor(namedKeys),
35802
+ includeAccountRegistryPermissions: false,
35803
+ autoBootstrapAccount: false,
35804
+ ...expiry ? { sessionExpirationMs: expiry.durationMs } : {}
35805
+ });
35806
+ let handoff;
35807
+ let preparedDid;
35808
+ try {
35809
+ process.stderr.write(KEYS_APPROVAL_NOTE(namedKeys.prefix));
35810
+ await node.signInWithOpenKey(async (prepared) => {
35811
+ preparedDid = prepared.verificationMethod;
35812
+ handoff = await startPreparedOpenKeyHandoff({
35813
+ prepared,
35814
+ openKeyHost: resolveOpenKeyHost(profile),
35815
+ noPopup: options.noPopup,
35816
+ paste: options.paste
35817
+ });
35818
+ return handoff.result;
35819
+ });
35820
+ const active = node.restorableSession;
35821
+ if (!active || !preparedDid || node.sessionDid !== preparedDid || active.verificationMethod !== preparedDid) {
35822
+ throw new CLIError("OPENKEY_SESSION_MISMATCH", "OpenKey signed a different SDK session key; no profile or session was saved.", ExitCode.PERMISSION_DENIED);
35823
+ }
35824
+ const key = active.jwk;
35825
+ const signed = verifySignedSession({ ...active }, key, preparedDid, { expectedOwner: owner, expiry });
35826
+ const grant = namedKeysGrantIn(active.siwe, namedKeys);
35827
+ if (grant === "partial") throw partialKeysGrantError();
35828
+ const session = { ...withVerifiedAuthority({ ...active, jwk: key }, signed), authMethod: "openkey" };
35829
+ const updatedProfile = {
35830
+ ...profile,
35831
+ host: options.persistHost === true || !profile.host ? host : profile.host,
35832
+ did: preparedDid,
35833
+ sessionDid: preparedDid,
35834
+ posture: profile.posture ?? "owner-openkey",
35835
+ operatorType: profile.operatorType ?? "human",
35836
+ authMethod: "openkey",
35837
+ ownerDid: signed.ownerDid,
35838
+ spaceId: active.spaceId
35839
+ };
35840
+ delete updatedProfile.namedKeys;
35841
+ if (grant === "both") updatedProfile.namedKeys = namedKeys;
35842
+ await commitLogin(profileName2, snapshot, { key, session, profile: updatedProfile });
35843
+ await handoff?.complete(true);
35844
+ if (grant === "both") await readyNamedKeysNetwork(node);
35845
+ if (grant === "none") {
35846
+ process.stderr.write(keysNotGrantedAfterLogin(profileName2));
35847
+ }
35848
+ return { profile: updatedProfile, delegationData: session, declined: [], legacyNested: [] };
35849
+ } catch (error) {
35850
+ await handoff?.complete(false);
35851
+ await node.replication?.close();
35852
+ throw isNarrowedRecapMismatch(error) ? partialKeysGrantError() : error;
35853
+ }
35854
+ }
35855
+ function partialKeysGrantError() {
35856
+ return new CLIError(
35857
+ "KEYS_PARTIAL_GRANT",
35858
+ "Named keys need both permissions (create and use), but only one was approved, so this sign-in wasn't saved. Sign in again and approve both.",
35859
+ ExitCode.PERMISSION_DENIED
35860
+ );
35861
+ }
35663
35862
  async function undoPreparedAttempt(profileName2, node, owner, gateWasActive) {
35664
35863
  if (!gateWasActive) {
35665
35864
  const undone = await ProfileManager.withLock(profileName2, async () => {
@@ -35698,6 +35897,16 @@ async function refreshOpenKeySession(profileName2, host, options = {}) {
35698
35897
  const estimatedExpiry = expiry === void 0 ? void 0 : new Date(Date.now() + expiry.durationMs).toISOString();
35699
35898
  assertSessionReplaceable(profileName2, snapshot, expectedOwner, permissions, estimatedExpiry);
35700
35899
  }
35900
+ if (options.keys !== void 0) {
35901
+ if (replication.prefixes.length > 0) {
35902
+ throw new CLIError(
35903
+ "INVALID_ARGUMENT",
35904
+ `With OpenKey sign-in, \`--keys\` can't be used on a profile that has replication prefixes (given with \`--replication-prefix\` or saved from an earlier sign-in). Nothing was started. Use a separate profile for named keys: \`${CLI_NAME17} --profile <name> auth login --owner did:pkh:eip155:<chain>:<address> --keys <folder>\`.`,
35905
+ ExitCode.USAGE_ERROR
35906
+ );
35907
+ }
35908
+ return refreshPreparedKeysSession({ profileName: profileName2, host, profile, snapshot, options, expectedOwner, expiry });
35909
+ }
35701
35910
  if (replication.prefixes.length > 0) {
35702
35911
  return refreshPreparedOpenKeySession({
35703
35912
  profileName: profileName2,
@@ -35760,6 +35969,7 @@ async function refreshOpenKeySession(profileName2, host, options = {}) {
35760
35969
  ...typeof sanitizedSession.spaceId === "string" ? { spaceId: sanitizedSession.spaceId } : {},
35761
35970
  ...verifiedOwner === void 0 ? {} : { ownerDid: verifiedOwner }
35762
35971
  }, ownerKeyPrimary);
35972
+ delete updatedProfile.namedKeys;
35763
35973
  await commitLogin(profileName2, snapshot, {
35764
35974
  key,
35765
35975
  session: sanitizedSession,
@@ -37337,6 +37547,175 @@ function registerKvCommand(program) {
37337
37547
  });
37338
37548
  }
37339
37549
 
37550
+ // src/commands/keys.ts
37551
+ init_profiles();
37552
+ init_constants();
37553
+ init_formatter();
37554
+ init_errors();
37555
+ init_sdk();
37556
+ import { readFile as readFile12 } from "fs/promises";
37557
+ import { NamedKeyError, createNamedKeyClient } from "@tinycloud/node-sdk/encryption-keys";
37558
+ import { CLI_NAME as CLI_NAME26 } from "@tinycloud/operations/cli-name";
37559
+ async function readStdin3() {
37560
+ const chunks = [];
37561
+ for await (const chunk of process.stdin) {
37562
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
37563
+ }
37564
+ return Buffer.concat(chunks);
37565
+ }
37566
+ async function readInput(file) {
37567
+ return file === void 0 ? readStdin3() : readFile12(file);
37568
+ }
37569
+ function cliErrorFromNamedKey(error, command, path) {
37570
+ if (!(error instanceof NamedKeyError)) return error;
37571
+ switch (error.code) {
37572
+ case "NAMED_KEYS_UNSUPPORTED":
37573
+ return new CLIError("NAMED_KEYS_UNAVAILABLE", "This node doesn't support named keys.", ExitCode.NODE_ERROR);
37574
+ case "INVALID_KEY_TARGET":
37575
+ case "KEY_PATH_INVALID":
37576
+ return new CLIError("INVALID_KEY_PATH", "That key name isn't valid.", ExitCode.USAGE_ERROR);
37577
+ case "INVALID_ENVELOPE":
37578
+ return new CLIError("INVALID_ENVELOPE", "The envelope is damaged or incomplete.", ExitCode.USAGE_ERROR);
37579
+ case "KEY_NOT_FOUND":
37580
+ return new CLIError(
37581
+ "NOT_FOUND",
37582
+ command === "decrypt" ? "The key this envelope needs doesn't exist on this node, so it can't be decrypted here." : `There's no key at ${path} yet. Create it with \`${CLI_NAME26} keys ensure ${path}\`.`,
37583
+ ExitCode.NOT_FOUND
37584
+ );
37585
+ case "KEY_BLOCKED":
37586
+ return new CLIError("KEY_BLOCKED", `Key ${path} is blocked, so it can't be used.`, ExitCode.PERMISSION_DENIED);
37587
+ case "DECRYPT_FAILED":
37588
+ return new CLIError("DECRYPT_FAILED", `Couldn't decrypt this envelope with key ${path}.`, ExitCode.ERROR);
37589
+ }
37590
+ if (error.status === 401 || error.status === 403) {
37591
+ return new CLIError("PERMISSION_DENIED", error.message, ExitCode.PERMISSION_DENIED, {
37592
+ hint: `Sign in with \`${CLI_NAME26} auth login --keys <folder>\` for this key's folder.`
37593
+ });
37594
+ }
37595
+ return new CLIError(error.code, error.message, error.status === void 0 ? ExitCode.ERROR : ExitCode.NODE_ERROR);
37596
+ }
37597
+ async function withNetworkSetup(node, call) {
37598
+ try {
37599
+ return await call();
37600
+ } catch (error) {
37601
+ if (!(error instanceof NamedKeyError) || error.code !== "NETWORK_NOT_FOUND") throw error;
37602
+ await node.ensureEncryptionNetwork();
37603
+ return call();
37604
+ }
37605
+ }
37606
+ async function grantedPrefix(deps, profileName2) {
37607
+ const profile = await deps.getProfile(profileName2);
37608
+ const keys = profile.namedKeys;
37609
+ if (keys === void 0) {
37610
+ throw new CLIError(
37611
+ "KEYS_NOT_GRANTED",
37612
+ `Profile "${profileName2}" can't use named keys yet. Sign in with \`${CLI_NAME26} --profile ${profileName2} auth login --keys <folder>\`.`,
37613
+ ExitCode.PERMISSION_DENIED
37614
+ );
37615
+ }
37616
+ return keys.prefix;
37617
+ }
37618
+ function assertInGrantedPrefix(prefix, path, profileName2) {
37619
+ if (!keyPathInPrefix(prefix, path)) {
37620
+ throw new CLIError(
37621
+ "KEY_OUTSIDE_GRANTED_PREFIX",
37622
+ `Key "${path}" is outside the folder "${prefix}/" this sign-in covers. To use it, sign in with \`${CLI_NAME26} --profile ${profileName2} auth login --method local --keys <folder>\`.`,
37623
+ ExitCode.PERMISSION_DENIED
37624
+ );
37625
+ }
37626
+ }
37627
+ function parseEnvelope(bytes) {
37628
+ let parsed;
37629
+ try {
37630
+ parsed = JSON.parse(bytes.toString("utf8"));
37631
+ } catch {
37632
+ throw new CLIError("INVALID_ENVELOPE", "The input isn't an envelope (expected JSON).", ExitCode.USAGE_ERROR);
37633
+ }
37634
+ const envelope = parsed;
37635
+ if (typeof envelope !== "object" || envelope === null || envelope.v !== 2 || typeof envelope.keyPath !== "string") {
37636
+ throw new CLIError("INVALID_ENVELOPE", 'The input isn\'t a named-key envelope (it needs `"v": 2` and a `keyPath`).', ExitCode.USAGE_ERROR);
37637
+ }
37638
+ return envelope;
37639
+ }
37640
+ var defaultDeps = {
37641
+ resolveContext: (options) => ProfileManager.resolveContext(options),
37642
+ getProfile: (name) => ProfileManager.getProfile(name),
37643
+ authenticate: (ctx) => ensureAuthenticated(ctx),
37644
+ createClient: createNamedKeyClient,
37645
+ outputJson,
37646
+ handleError
37647
+ };
37648
+ function registerKeysCommand(program, deps = defaultDeps) {
37649
+ const { outputJson: outputJson2, handleError: handleError2 } = deps;
37650
+ const keys = program.command("keys").description(`Named encryption keys (sign in with \`${CLI_NAME26} auth login --keys <folder>\` first)`);
37651
+ keys.command("ensure <path>").description("Create the named key if it doesn't exist yet, then show it").action(async (path, _options, cmd) => {
37652
+ try {
37653
+ const ctx = await deps.resolveContext(cmd.optsWithGlobals());
37654
+ assertInGrantedPrefix(await grantedPrefix(deps, ctx.profile), path, ctx.profile);
37655
+ const node = await deps.authenticate(ctx);
37656
+ const client = deps.createClient(node);
37657
+ const descriptor = await withSpinner(`Getting or creating key ${path}\u2026`, () => withNetworkSetup(node, () => client.ensureKey(path))).catch((error) => {
37658
+ throw cliErrorFromNamedKey(error, "ensure", path);
37659
+ });
37660
+ outputJson2({ ...descriptor });
37661
+ } catch (error) {
37662
+ await handleError2(error);
37663
+ }
37664
+ });
37665
+ keys.command("encrypt <path>").description("Encrypt data with a named key and output the encrypted envelope as JSON").option("--in <file>", "Read the data to encrypt from a file (default: standard input)").option("--out <file>", "Write the envelope to a file (default: standard output)").action(async (path, options, cmd) => {
37666
+ try {
37667
+ const ctx = await deps.resolveContext(cmd.optsWithGlobals());
37668
+ assertInGrantedPrefix(await grantedPrefix(deps, ctx.profile), path, ctx.profile);
37669
+ if (options.out) await validatePrivateOutput(options.out, "--out");
37670
+ const plaintext = await readInput(options.in);
37671
+ const node = await deps.authenticate(ctx);
37672
+ const client = deps.createClient(node);
37673
+ const envelope = await withSpinner(`Encrypting with ${path}\u2026`, () => withNetworkSetup(node, () => client.encrypt(path, plaintext))).catch((error) => {
37674
+ throw cliErrorFromNamedKey(error, "encrypt", path);
37675
+ });
37676
+ const json = JSON.stringify(envelope);
37677
+ if (options.out) {
37678
+ await writePrivateOutput(options.out, new TextEncoder().encode(json));
37679
+ outputJson2({ keyPath: envelope.keyPath, networkId: envelope.networkId, written: options.out });
37680
+ return;
37681
+ }
37682
+ process.stdout.write(json + "\n");
37683
+ } catch (error) {
37684
+ await handleError2(error);
37685
+ }
37686
+ });
37687
+ keys.command("decrypt").description("Decrypt an envelope made with a named key and output the original data").option("--in <file>", "Read the envelope from a file (default: standard input)").option("--out <file>", "Write the decrypted data to a file (default: standard output)").action(async (options, cmd) => {
37688
+ try {
37689
+ const ctx = await deps.resolveContext(cmd.optsWithGlobals());
37690
+ const prefix = await grantedPrefix(deps, ctx.profile);
37691
+ if (options.out) await validatePrivateOutput(options.out, "--out");
37692
+ const envelope = parseEnvelope(await readInput(options.in));
37693
+ assertInGrantedPrefix(prefix, envelope.keyPath, ctx.profile);
37694
+ const profile = await deps.getProfile(ctx.profile);
37695
+ if (profile.ownerDid !== void 0 && envelope.networkId !== namedKeysNetworkId(profile.ownerDid)) {
37696
+ throw new CLIError(
37697
+ "KEY_OUTSIDE_GRANTED_PREFIX",
37698
+ "This envelope was made with a different encryption network than this profile's, so this profile can't decrypt it.",
37699
+ ExitCode.PERMISSION_DENIED
37700
+ );
37701
+ }
37702
+ const node = await deps.authenticate(ctx);
37703
+ const client = deps.createClient(node);
37704
+ const plaintext = await withSpinner("Decrypting\u2026", () => client.decrypt(envelope)).catch((error) => {
37705
+ throw cliErrorFromNamedKey(error, "decrypt", envelope.keyPath);
37706
+ });
37707
+ if (options.out) {
37708
+ await writePrivateOutput(options.out, plaintext);
37709
+ outputJson2({ keyPath: envelope.keyPath, written: options.out });
37710
+ return;
37711
+ }
37712
+ process.stdout.write(plaintext);
37713
+ } catch (error) {
37714
+ await handleError2(error);
37715
+ }
37716
+ });
37717
+ }
37718
+
37340
37719
  // src/commands/manifest.ts
37341
37720
  init_profiles();
37342
37721
  init_sdk();
@@ -37346,16 +37725,16 @@ init_errors();
37346
37725
  init_constants();
37347
37726
  init_theme();
37348
37727
  import { resolveManifestKnowledgeRoot as resolveManifestKnowledgeRoot2 } from "@tinycloud/sdk-core";
37349
- import { CLI_NAME as CLI_NAME26 } from "@tinycloud/operations/cli-name";
37350
- import { readFile as readFile12 } from "fs/promises";
37728
+ import { CLI_NAME as CLI_NAME27 } from "@tinycloud/operations/cli-name";
37729
+ import { readFile as readFile13 } from "fs/promises";
37351
37730
  var DEFAULT_APP_SPACE = "applications";
37352
37731
  function registerManifestCommand(program) {
37353
37732
  const manifest = program.command("manifest").description("Inspect TinyCloud app manifests");
37354
37733
  manifest.command("resolve <source>").description("Resolve a manifest file or URL to its effective space, paths, and DB basenames").addHelpText("after", `
37355
37734
 
37356
37735
  Examples:
37357
- $ ${CLI_NAME26} manifest resolve ./manifest.json
37358
- $ ${CLI_NAME26} manifest resolve https://app.example.com/manifest.json --json
37736
+ $ ${CLI_NAME27} manifest resolve ./manifest.json
37737
+ $ ${CLI_NAME27} manifest resolve https://app.example.com/manifest.json --json
37359
37738
 
37360
37739
  What it shows:
37361
37740
  - app_id, name, manifest_version
@@ -37365,7 +37744,7 @@ What it shows:
37365
37744
 
37366
37745
  This command is read-only and does NOT contact the node \u2014 it just resolves
37367
37746
  the manifest against the active profile's address/chain so you know which
37368
- \`--space\` and \`--db\` values to pass to other ${CLI_NAME26} commands.
37747
+ \`--space\` and \`--db\` values to pass to other ${CLI_NAME27} commands.
37369
37748
  `).action(async (source, _options, cmd) => {
37370
37749
  try {
37371
37750
  const globalOpts = cmd.optsWithGlobals();
@@ -37434,7 +37813,7 @@ ${theme.heading("SQL databases")}
37434
37813
  `);
37435
37814
  }
37436
37815
  process.stdout.write(theme.muted(`
37437
- Use with: ${CLI_NAME26} sql query --space ${spaceName} --db <db> "..."
37816
+ Use with: ${CLI_NAME27} sql query --space ${spaceName} --db <db> "..."
37438
37817
  `));
37439
37818
  }
37440
37819
  if (permissions.length > 0) {
@@ -37461,7 +37840,7 @@ async function loadManifestSource2(source) {
37461
37840
  }
37462
37841
  return response.text();
37463
37842
  }
37464
- return readFile12(source, "utf8");
37843
+ return readFile13(source, "utf8");
37465
37844
  }
37466
37845
  function prefixWithAppId(path, appId) {
37467
37846
  const slash = path.indexOf("/");
@@ -37554,7 +37933,7 @@ init_profiles();
37554
37933
  init_formatter();
37555
37934
  init_errors();
37556
37935
  init_constants();
37557
- import { CLI_NAME as CLI_NAME27 } from "@tinycloud/operations/cli-name";
37936
+ import { CLI_NAME as CLI_NAME28 } from "@tinycloud/operations/cli-name";
37558
37937
  import { createInterface as createInterface5 } from "readline";
37559
37938
  init_theme();
37560
37939
  init_types();
@@ -37700,9 +38079,9 @@ profile at command time. Precedence for every kv/sql command:
37700
38079
  explicit --space flag > profile defaultSpace > primary space.
37701
38080
 
37702
38081
  Examples:
37703
- $ ${CLI_NAME27} profile set-default-space applications
37704
- $ ${CLI_NAME27} profile set-default-space applications --profile cli-test
37705
- $ ${CLI_NAME27} profile set-default-space --unset
38082
+ $ ${CLI_NAME28} profile set-default-space applications
38083
+ $ ${CLI_NAME28} profile set-default-space applications --profile cli-test
38084
+ $ ${CLI_NAME28} profile set-default-space --unset
37706
38085
  `).action(async (name, options, cmd) => {
37707
38086
  try {
37708
38087
  const globalOpts = cmd.optsWithGlobals();
@@ -37714,7 +38093,7 @@ Examples:
37714
38093
  if (!options.unset && (name === void 0 || name === "")) {
37715
38094
  throw new CLIError(
37716
38095
  "USAGE_ERROR",
37717
- `Provide a space name (e.g. \`${CLI_NAME27} profile set-default-space applications\`) or pass --unset.`,
38096
+ `Provide a space name (e.g. \`${CLI_NAME28} profile set-default-space applications\`) or pass --unset.`,
37718
38097
  ExitCode.USAGE_ERROR
37719
38098
  );
37720
38099
  }
@@ -37773,7 +38152,7 @@ function parseOperatorType(raw) {
37773
38152
 
37774
38153
  // src/commands/replica.ts
37775
38154
  init_profile_replicas();
37776
- import { CLI_NAME as CLI_NAME28 } from "@tinycloud/operations/cli-name";
38155
+ import { CLI_NAME as CLI_NAME29 } from "@tinycloud/operations/cli-name";
37777
38156
  import { randomBytes as randomBytes7 } from "crypto";
37778
38157
  import { constants as fsConstants5 } from "fs";
37779
38158
  import { mkdir as mkdir5, open as open11, readdir as readdir6, rename as rename4, unlink as unlink5, lstat as lstat9 } from "fs/promises";
@@ -37914,11 +38293,11 @@ var EXIT_BY_CODE = {
37914
38293
  var READ_FAILURE = {
37915
38294
  absent: { code: "KEY_ABSENT", exit: ExitCode.NOT_FOUND, what: "is not in the replica (the replica covers its whole prefix)" },
37916
38295
  deleted: { code: "KEY_DELETED", exit: ExitCode.NOT_FOUND, what: "was deleted at the source" },
37917
- content_missing: { code: "CONTENT_MISSING", exit: ExitCode.NOT_FOUND, what: `is known but its content has not been fetched yet; run ${CLI_NAME28} replica sync` },
38296
+ content_missing: { code: "CONTENT_MISSING", exit: ExitCode.NOT_FOUND, what: `is known but its content has not been fetched yet; run ${CLI_NAME29} replica sync` },
37918
38297
  coverage_incomplete: {
37919
38298
  code: "COVERAGE_INCOMPLETE",
37920
38299
  exit: ExitCode.NOT_FOUND,
37921
- what: `is not in the replica, which has not finished its first sync; run ${CLI_NAME28} replica sync`
38300
+ what: `is not in the replica, which has not finished its first sync; run ${CLI_NAME29} replica sync`
37922
38301
  },
37923
38302
  not_covered: { code: "NOT_COVERED", exit: ExitCode.USAGE_ERROR, what: "is outside the replica's prefix" }
37924
38303
  };
@@ -38036,7 +38415,7 @@ async function existingReplicaName(profile, option) {
38036
38415
  const names = await replicaNames(profile);
38037
38416
  if (names.length === 1) return names[0];
38038
38417
  if (names.length === 0) {
38039
- throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas. Create one with: ${CLI_NAME28} replica sync --prefix <prefix>`);
38418
+ throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas. Create one with: ${CLI_NAME29} replica sync --prefix <prefix>`);
38040
38419
  }
38041
38420
  throw new CLIError("USAGE_ERROR", `Profile "${profile}" has several replicas (${names.join(", ")}); pass --replica <name>.`, ExitCode.USAGE_ERROR);
38042
38421
  }
@@ -38144,9 +38523,9 @@ async function storedGrants(profile) {
38144
38523
  function grantHint(space, prefix) {
38145
38524
  return [
38146
38525
  "A replica needs a device grant with get and sync on its prefix:",
38147
- ` device: ${CLI_NAME28} auth request --cap tinycloud.kv:${space}:${prefix}:get,list,metadata,sync --expiry 30d --emit req.json`,
38148
- ` owner: ${CLI_NAME28} auth grant req.json > grant.json`,
38149
- ` device: ${CLI_NAME28} auth import grant.json`
38526
+ ` device: ${CLI_NAME29} auth request --cap tinycloud.kv:${space}:${prefix}:get,list,metadata,sync --expiry 30d --emit req.json`,
38527
+ ` owner: ${CLI_NAME29} auth grant req.json > grant.json`,
38528
+ ` device: ${CLI_NAME29} auth import grant.json`
38150
38529
  ].join("\n");
38151
38530
  }
38152
38531
  function resolveSpace(input, prefix, grants) {
@@ -38237,7 +38616,7 @@ function describeStatus(status) {
38237
38616
  formatField("Size", formatBytes(status.bytes)),
38238
38617
  formatField("Grant", status.device.delegationCid),
38239
38618
  formatField("Last error", status.lastError ? `${status.lastError.code}: ${status.lastError.message}` : null),
38240
- formatField("Purge", status.purgePending ? `incomplete: content files remain; the next ${CLI_NAME28} replica command retries` : null)
38619
+ formatField("Purge", status.purgePending ? `incomplete: content files remain; the next ${CLI_NAME29} replica command retries` : null)
38241
38620
  ].join("\n");
38242
38621
  }
38243
38622
  async function writeFileAtomic(path, bytes) {
@@ -38401,7 +38780,7 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
38401
38780
  (options, cmd) => run(async () => {
38402
38781
  const profile = await profileName(cmd);
38403
38782
  if (options.purge) {
38404
- throw new CLIError("REPORT_READ_ONLY", `Replica report is read-only. To remove a named local replica, run ${CLI_NAME28} replica reset --replica <name> --purge; flag-owned purge has no equivalent yet (TC-1309).`, ExitCode.USAGE_ERROR);
38783
+ throw new CLIError("REPORT_READ_ONLY", `Replica report is read-only. To remove a named local replica, run ${CLI_NAME29} replica reset --replica <name> --purge; flag-owned purge has no equivalent yet (TC-1309).`, ExitCode.USAGE_ERROR);
38405
38784
  }
38406
38785
  if (options.clearPending) {
38407
38786
  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);
@@ -38503,7 +38882,7 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
38503
38882
  if (differs !== void 0) {
38504
38883
  throw new ReplicaError(
38505
38884
  ReplicaErrorCode.CONFIG_MISMATCH,
38506
- `Replica ${name} already follows ${differs}. Use another --replica name, or ${CLI_NAME28} replica reset --replica ${name} --purge first.`
38885
+ `Replica ${name} already follows ${differs}. Use another --replica name, or ${CLI_NAME29} replica reset --replica ${name} --purge first.`
38507
38886
  );
38508
38887
  }
38509
38888
  }
@@ -38668,8 +39047,8 @@ init_theme();
38668
39047
  init_errors();
38669
39048
  init_warnings();
38670
39049
  init_constants();
38671
- import { CLI_NAME as CLI_NAME30 } from "@tinycloud/operations/cli-name";
38672
- import { readFile as readFile13 } from "fs/promises";
39050
+ import { CLI_NAME as CLI_NAME31 } from "@tinycloud/operations/cli-name";
39051
+ import { readFile as readFile14 } from "fs/promises";
38673
39052
  import { join as join17 } from "path";
38674
39053
  import { homedir, hostname, userInfo } from "os";
38675
39054
  import { appSecretScope } from "@tinycloud/sdk-core";
@@ -38879,7 +39258,7 @@ function parseStatusEnvelope(line) {
38879
39258
  // src/lib/secrets-wait.ts
38880
39259
  init_constants();
38881
39260
  init_errors();
38882
- import { CLI_NAME as CLI_NAME29 } from "@tinycloud/operations/cli-name";
39261
+ import { CLI_NAME as CLI_NAME30 } from "@tinycloud/operations/cli-name";
38883
39262
  var WAIT_BACKOFF_MS = [3e3, 4500, 6750, 10125];
38884
39263
  var WAIT_STEADY_INTERVAL_MS = 15e3;
38885
39264
  var DEFAULT_WAIT_TIMEOUT = "5m";
@@ -38887,7 +39266,7 @@ var MAX_WAIT_TIMEOUT_MS = 9 * 6e4;
38887
39266
  var MAX_CONSECUTIVE_FAILURES = 5;
38888
39267
  var TIMEOUT_RE = /^(0|\d+s|\d+m)$/;
38889
39268
  var RETRYABLE_STATUS_ERRORS = { NODE_UNREACHABLE: true, NODE_ERROR: true };
38890
- var EXPLICIT_KEY_HINT = `${CLI_NAME29} secrets wait never signs in, and an explicit private key has no stored session. Use a local profile signed in once with ${CLI_NAME29} --profile <profile> auth login --method local, without TC_PRIVATE_KEY.`;
39269
+ var EXPLICIT_KEY_HINT = `${CLI_NAME30} secrets wait never signs in, and an explicit private key has no stored session. Use a local profile signed in once with ${CLI_NAME30} --profile <profile> auth login --method local, without TC_PRIVATE_KEY.`;
38891
39270
  function abortableSleep(ms2, signal) {
38892
39271
  return new Promise((resolve9) => {
38893
39272
  if (signal.aborted) {
@@ -38923,7 +39302,7 @@ async function runSecretsWait(request, deps) {
38923
39302
  let step = 0;
38924
39303
  const cancelled = () => new CLIError(
38925
39304
  "CANCELLED",
38926
- `Interrupted: ${CLI_NAME29} secrets wait stopped before the secrets were ready.`,
39305
+ `Interrupted: ${CLI_NAME30} secrets wait stopped before the secrets were ready.`,
38927
39306
  ExitCode.CANCELLED,
38928
39307
  progress === void 0 ? {} : { progress }
38929
39308
  );
@@ -39067,13 +39446,13 @@ function statusError(error, profile, warnings) {
39067
39446
  if (error.code === "AUTH_REQUIRED" || error.code === "SESSION_NOT_FOUND") {
39068
39447
  const recovery = recoveryOf(error, "sign_in");
39069
39448
  if (recovery !== void 0) {
39070
- const hint = recovery.command !== void 0 ? `Sign in once with: ${recovery.command}, then retry.` : `Sign in to profile "${profile}" once with ${CLI_NAME29} auth login, then retry.`;
39449
+ const hint = recovery.command !== void 0 ? `Sign in once with: ${recovery.command}, then retry.` : `Sign in to profile "${profile}" once with ${CLI_NAME30} auth login, then retry.`;
39071
39450
  return new CLIError("AUTH_REQUIRED", error.message, ExitCode.AUTH_REQUIRED, { hint, recovery, ...extra });
39072
39451
  }
39073
39452
  }
39074
39453
  if (error.code === "PROFILE_MIGRATION_REQUIRED") {
39075
39454
  const recovery = recoveryOf(error, "complete_migration");
39076
- const hint = recovery?.command !== void 0 ? `Run ${recovery.command} once to finish this profile's one-time upgrade, then retry wait.` : `Run ${CLI_NAME29} auth caps once with profile "${profile}" to finish its one-time upgrade, then retry wait.`;
39455
+ const hint = recovery?.command !== void 0 ? `Run ${recovery.command} once to finish this profile's one-time upgrade, then retry wait.` : `Run ${CLI_NAME30} auth caps once with profile "${profile}" to finish its one-time upgrade, then retry wait.`;
39077
39456
  return new CLIError("PROFILE_MIGRATION_REQUIRED", error.message, ExitCode.ERROR, {
39078
39457
  hint,
39079
39458
  ...recovery === void 0 ? {} : { recovery },
@@ -39099,7 +39478,7 @@ var SECRET_KV_ABILITIES = {
39099
39478
  del: secretCapabilityAction("del"),
39100
39479
  list: secretCapabilityAction("list")
39101
39480
  };
39102
- async function readStdin3() {
39481
+ async function readStdin4() {
39103
39482
  const chunks = [];
39104
39483
  for await (const chunk of process.stdin) {
39105
39484
  chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
@@ -39265,11 +39644,11 @@ async function runSecretOperation(params) {
39265
39644
  }
39266
39645
  function secretPermissionReason(action, name) {
39267
39646
  const target = name ? ` secret "${name}"` : " secrets";
39268
- return `Allow \`${CLI_NAME30} secrets ${action}${name ? ` ${name}` : ""}\` to access${target} with the required TinyCloud permissions.`;
39647
+ return `Allow \`${CLI_NAME31} secrets ${action}${name ? ` ${name}` : ""}\` to access${target} with the required TinyCloud permissions.`;
39269
39648
  }
39270
39649
  function assertOwnerApprovalPossible(profileName2, profile, action, name, missing = []) {
39271
39650
  if (profile.authMethod !== "openkey" || process.stdin.isTTY || process.stderr.isTTY) return;
39272
- const command = `${CLI_NAME30} secrets ${action === "del" ? "delete" : action}${name ? ` ${name}` : ""}`;
39651
+ const command = `${CLI_NAME31} secrets ${action === "del" ? "delete" : action}${name ? ` ${name}` : ""}`;
39273
39652
  throw new CLIError(
39274
39653
  "PERMISSION_DENIED",
39275
39654
  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.`,
@@ -39280,7 +39659,7 @@ function assertOwnerApprovalPossible(profileName2, profile, action, name, missin
39280
39659
  );
39281
39660
  }
39282
39661
  function scopedSecretLoginHint(profileName2) {
39283
- return `Have the owner approve a scoped login whose manifest names the secret: ${CLI_NAME30} --profile ${profileName2} auth login --method openkey --paste --manifest <manifest.json>. Pass the owner's code on stdin, newline-terminated.`;
39662
+ return `Have the owner approve a scoped login whose manifest names the secret: ${CLI_NAME31} --profile ${profileName2} auth login --method openkey --paste --manifest <manifest.json>. Pass the owner's code on stdin, newline-terminated.`;
39284
39663
  }
39285
39664
  async function runSecretOperationAttempt(label, operation) {
39286
39665
  try {
@@ -39414,7 +39793,7 @@ ${message}`,
39414
39793
  "AUTH_REQUIRED",
39415
39794
  "Not signed in to TinyCloud.",
39416
39795
  ExitCode.AUTH_REQUIRED,
39417
- { hint: `Sign in with: ${CLI_NAME30} --profile ${result.context.profile} auth login` }
39796
+ { hint: `Sign in with: ${CLI_NAME31} --profile ${result.context.profile} auth login` }
39418
39797
  );
39419
39798
  }
39420
39799
  if (result.error.code === "NODE_UNREACHABLE") {
@@ -39563,7 +39942,7 @@ function permissionsFromDelegation2(delegation) {
39563
39942
  }
39564
39943
  async function loadDelegationCandidates(source) {
39565
39944
  try {
39566
- const raw = JSON.parse(await readFile13(source, "utf8"));
39945
+ const raw = JSON.parse(await readFile14(source, "utf8"));
39567
39946
  return normalizeDelegationCandidates(raw, source);
39568
39947
  } catch (error) {
39569
39948
  if (error?.code !== "ENOENT") {
@@ -39583,7 +39962,7 @@ async function loadDelegationCandidates(source) {
39583
39962
  }
39584
39963
  try {
39585
39964
  const importedPath = join17(resolveProfilesDir(), source, "additional-delegations.json");
39586
- const raw = JSON.parse(await readFile13(importedPath, "utf8"));
39965
+ const raw = JSON.parse(await readFile14(importedPath, "utf8"));
39587
39966
  return normalizeDelegationCandidates(raw, source);
39588
39967
  } catch (error) {
39589
39968
  if (error?.code === "ENOENT") {
@@ -39891,7 +40270,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
39891
40270
  name: "Encryption network",
39892
40271
  ok: false,
39893
40272
  detail: `${networkName} not found`,
39894
- hint: `${CLI_NAME30} secrets network init ${networkName}`
40273
+ hint: `${CLI_NAME31} secrets network init ${networkName}`
39895
40274
  }
39896
40275
  ];
39897
40276
  let secret;
@@ -39937,7 +40316,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
39937
40316
  name: "Secret access",
39938
40317
  ok: false,
39939
40318
  detail: notFound ? `${resolved.permissionPaths.vault} not found` : result.error.message,
39940
- hint: notFound ? local ? `${CLI_NAME30} secrets put ${resolved.name}${formatSecretScopeFlag(scopeOptions)} --stdin` : `${CLI_NAME30} secrets request ${resolved.name}${formatSecretScopeFlag(scopeOptions)}` : `Ask the owner profile to grant ${secretCapabilityAction("get")} and ${SECRET_DECRYPT_CAPABILITY}.`
40319
+ hint: notFound ? local ? `${CLI_NAME31} secrets put ${resolved.name}${formatSecretScopeFlag(scopeOptions)} --stdin` : `${CLI_NAME31} secrets request ${resolved.name}${formatSecretScopeFlag(scopeOptions)}` : `Ask the owner profile to grant ${secretCapabilityAction("get")} and ${SECRET_DECRYPT_CAPABILITY}.`
39941
40320
  });
39942
40321
  }
39943
40322
  } else {
@@ -40090,9 +40469,9 @@ function registerSecretsCommand(program, openKeyAcquisition) {
40090
40469
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
40091
40470
  }
40092
40471
  if (options.file) {
40093
- secretValue = await readFile13(options.file, "utf-8");
40472
+ secretValue = await readFile14(options.file, "utf-8");
40094
40473
  } else if (options.stdin) {
40095
- secretValue = (await readStdin3()).toString("utf-8");
40474
+ secretValue = (await readStdin4()).toString("utf-8");
40096
40475
  } else {
40097
40476
  secretValue = value;
40098
40477
  }
@@ -40181,7 +40560,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
40181
40560
  if (effectiveLocal(profile, options)) {
40182
40561
  throw new CLIError(
40183
40562
  "LOCAL_OWNER_PROFILE",
40184
- `Profile "${ctx.profile}" uses a local key, which owns its secrets: store them with ${CLI_NAME30} secrets put NAME --stdin instead of requesting them.`,
40563
+ `Profile "${ctx.profile}" uses a local key, which owns its secrets: store them with ${CLI_NAME31} secrets put NAME --stdin instead of requesting them.`,
40185
40564
  ExitCode.USAGE_ERROR
40186
40565
  );
40187
40566
  }
@@ -40304,7 +40683,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
40304
40683
 
40305
40684
  // src/commands/share.ts
40306
40685
  import { Option } from "commander";
40307
- import { CLI_NAME as CLI_NAME31 } from "@tinycloud/operations/cli-name";
40686
+ import { CLI_NAME as CLI_NAME32 } from "@tinycloud/operations/cli-name";
40308
40687
  import { ProfileLockTimeoutError as ProfileLockTimeoutError3 } from "@tinycloud/operations/state";
40309
40688
  init_errors();
40310
40689
 
@@ -40399,7 +40778,7 @@ function receiveJson(result, path) {
40399
40778
 
40400
40779
  // src/share/io.ts
40401
40780
  import { constants } from "fs";
40402
- import { lstat as lstat10, mkdir as mkdir6, mkdtemp, open as open13, readFile as readFile14, realpath as realpath2, stat as stat6, link as link3, rename as rename5, rm as rm6, unlink as unlink6 } from "fs/promises";
40781
+ import { lstat as lstat10, mkdir as mkdir6, mkdtemp, open as open13, readFile as readFile15, realpath as realpath2, stat as stat6, link as link3, rename as rename5, rm as rm6, unlink as unlink6 } from "fs/promises";
40403
40782
  import { randomBytes as randomBytes8 } from "crypto";
40404
40783
  import { basename as basename4, join as join18, resolve as resolve6, sep as sep3 } from "path";
40405
40784
  init_errors();
@@ -40446,7 +40825,7 @@ async function readShareInput(input, name, limit = MAX_SHARE_STDIN_BYTES) {
40446
40825
  const path = resolve6(input);
40447
40826
  const info = await stat6(path);
40448
40827
  if (!info.isFile() || info.size > limit) throw new Error("MAX_BYTES_EXCEEDED");
40449
- const bytes = new Uint8Array(await readFile14(path));
40828
+ const bytes = new Uint8Array(await readFile15(path));
40450
40829
  if (bytes.byteLength > limit) throw new Error("MAX_BYTES_EXCEEDED");
40451
40830
  return { bytes, filename };
40452
40831
  }
@@ -41765,7 +42144,7 @@ function shareCliError(error, operation = "publish") {
41765
42144
  "SHARE_HISTORY_UNREADABLE",
41766
42145
  "sender history cannot be read with the current session key",
41767
42146
  1,
41768
- { hint: `Run \`${CLI_NAME31} share history recover\` to move the unreadable file aside. The old file is kept, not deleted.` }
42147
+ { hint: `Run \`${CLI_NAME32} share history recover\` to move the unreadable file aside. The old file is kept, not deleted.` }
41769
42148
  );
41770
42149
  }
41771
42150
  if (error instanceof SharePublishAuthorityError) {
@@ -41773,12 +42152,12 @@ function shareCliError(error, operation = "publish") {
41773
42152
  const profileName2 = "profileName" in failure ? failure.profileName : void 0;
41774
42153
  const localKey = "localKey" in failure && failure.localKey === true;
41775
42154
  const profileHint = profileName2 === void 0 ? "" : `--profile ${profileName2} `;
41776
- const loginHint = localKey ? `\`${CLI_NAME31} ${profileHint}auth login --method local\`` : `\`${CLI_NAME31} ${profileHint}auth login --device --manifest builtin:share-publishing\` (or \`${CLI_NAME31} ${profileHint}enable share\`)`;
42155
+ const loginHint = localKey ? `\`${CLI_NAME32} ${profileHint}auth login --method local\`` : `\`${CLI_NAME32} ${profileHint}auth login --device --manifest builtin:share-publishing\` (or \`${CLI_NAME32} ${profileHint}enable share\`)`;
41777
42156
  if (failure.kind === "caveated-session") {
41778
42157
  const holder = profileName2 === void 0 ? "this session" : `profile ${profileName2}'s session`;
41779
42158
  return new CLIError(
41780
42159
  "PERMISSION_DENIED",
41781
- `${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_NAME31} init --name publisher --key-only && ${CLI_NAME31} --profile publisher enable share\``,
42160
+ `${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_NAME32} init --name publisher --key-only && ${CLI_NAME32} --profile publisher enable share\``,
41782
42161
  5
41783
42162
  );
41784
42163
  }
@@ -41879,7 +42258,7 @@ function shareCliError(error, operation = "publish") {
41879
42258
  if (nodeCode === "ENOENT") return new CLIError("INVALID_ARGUMENT", "share input was not found", 2);
41880
42259
  if (nodeCode === "EISDIR") return new CLIError("INVALID_ARGUMENT", "share input must be a Markdown file", 2);
41881
42260
  const mapped = typeof nodeCode === "string" && known[nodeCode] !== void 0 ? known[nodeCode] : known[message];
41882
- const classified = mapped === void 0 ? classifyShareFailure(error, `\`${CLI_NAME31} auth login\``) : void 0;
42261
+ const classified = mapped === void 0 ? classifyShareFailure(error, `\`${CLI_NAME32} auth login\``) : void 0;
41883
42262
  if (classified !== void 0) return Object.assign(new CLIError(classified.code, classified.reason, classified.exitCode), { cause: error });
41884
42263
  if (error instanceof TypeError) return new CLIError("INVALID_ARGUMENT", "share input is invalid", 2);
41885
42264
  return new CLIError(mapped?.code ?? "INVALID_ARGUMENT", mapped ? mapped.code : "share operation failed", mapped?.exit ?? 2);
@@ -42289,7 +42668,7 @@ init_constants();
42289
42668
  init_sdk();
42290
42669
  init_host();
42291
42670
  init_theme();
42292
- import { CLI_NAME as CLI_NAME32 } from "@tinycloud/operations/cli-name";
42671
+ import { CLI_NAME as CLI_NAME33 } from "@tinycloud/operations/cli-name";
42293
42672
  import { ACCOUNT_REGISTRY_SPACE } from "@tinycloud/node-sdk";
42294
42673
  import { randomBytes as randomBytes9 } from "crypto";
42295
42674
  import { mkdir as mkdir7, writeFile as writeFile4 } from "fs/promises";
@@ -42336,7 +42715,7 @@ function registerSpaceCommand(program) {
42336
42715
  } else {
42337
42716
  const profile = await ProfileManager.getProfile(ctx.profile);
42338
42717
  const cap = "tinycloud.kv:account:spaces/:get,list";
42339
- const hint = resolveProfilePosture(profile) === "delegate-session" ? `Ask the space owner to grant it. Create the request with: ${CLI_NAME32} --profile ${ctx.profile} auth request --cap ${cap} --emit` : `Grant it to this session with: ${CLI_NAME32} --profile ${ctx.profile} auth request --grant --cap ${cap}. The default OpenKey owner sign-in covers only the default space, so it includes neither.`;
42718
+ const hint = resolveProfilePosture(profile) === "delegate-session" ? `Ask the space owner to grant it. Create the request with: ${CLI_NAME33} --profile ${ctx.profile} auth request --cap ${cap} --emit` : `Grant it to this session with: ${CLI_NAME33} --profile ${ctx.profile} auth request --grant --cap ${cap}. The default OpenKey owner sign-in covers only the default space, so it includes neither.`;
42340
42719
  throw new CLIError(
42341
42720
  "PERMISSION_DENIED",
42342
42721
  `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.`,
@@ -42373,10 +42752,10 @@ function registerSpaceCommand(program) {
42373
42752
 
42374
42753
  A delegate cannot host a space \u2014 only its owner (root authority) can. This
42375
42754
  emits a tinycloud.host.request artifact naming the space and its owner so you
42376
- can hand it to the owner; they run \`${CLI_NAME32} space host <name>\` and confirm.
42755
+ can hand it to the owner; they run \`${CLI_NAME33} space host <name>\` and confirm.
42377
42756
 
42378
42757
  If you ARE the owner of the resolved space, this refuses and tells you to host
42379
- it directly with \`${CLI_NAME32} space host <name>\` (no request needed).
42758
+ it directly with \`${CLI_NAME33} space host <name>\` (no request needed).
42380
42759
  `).action(async (name, options, cmd) => {
42381
42760
  try {
42382
42761
  const globalOpts = cmd.optsWithGlobals();
@@ -42387,7 +42766,7 @@ it directly with \`${CLI_NAME32} space host <name>\` (no request needed).
42387
42766
  if (await isRootAuthority(spaceId, ctx.profile)) {
42388
42767
  throw new CLIError(
42389
42768
  "ALREADY_ROOT_AUTHORITY",
42390
- `You are the owner of ${spaceId}. Host it directly: ${CLI_NAME32} space host ${spaceName}`,
42769
+ `You are the owner of ${spaceId}. Host it directly: ${CLI_NAME33} space host ${spaceName}`,
42391
42770
  ExitCode.USAGE_ERROR
42392
42771
  );
42393
42772
  }
@@ -42473,7 +42852,7 @@ init_sdk();
42473
42852
  init_space();
42474
42853
  init_host();
42475
42854
  init_theme();
42476
- import { CLI_NAME as CLI_NAME33 } from "@tinycloud/operations/cli-name";
42855
+ import { CLI_NAME as CLI_NAME34 } from "@tinycloud/operations/cli-name";
42477
42856
  import { writeFile as writeFile5 } from "fs/promises";
42478
42857
  import { resolve as resolve8 } from "path";
42479
42858
  async function dbHandle(node, dbName, spaceInput, profileName2) {
@@ -42497,12 +42876,12 @@ database for simple apps, or pass --db to target a named database. Pass
42497
42876
  --space to target a non-primary space (e.g. the manifest "applications" space).
42498
42877
 
42499
42878
  Common workflows:
42500
- $ ${CLI_NAME33} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
42501
- $ ${CLI_NAME33} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["ship docs"]'
42502
- $ ${CLI_NAME33} sql query "SELECT id, body FROM notes ORDER BY id"
42503
- $ ${CLI_NAME33} sql query "SELECT * FROM events WHERE type = ?" --db analytics --params '["signup"]'
42504
- $ ${CLI_NAME33} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
42505
- $ ${CLI_NAME33} sql export --db analytics --output analytics.db
42879
+ $ ${CLI_NAME34} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
42880
+ $ ${CLI_NAME34} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["ship docs"]'
42881
+ $ ${CLI_NAME34} sql query "SELECT id, body FROM notes ORDER BY id"
42882
+ $ ${CLI_NAME34} sql query "SELECT * FROM events WHERE type = ?" --db analytics --params '["signup"]'
42883
+ $ ${CLI_NAME34} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
42884
+ $ ${CLI_NAME34} sql export --db analytics --output analytics.db
42506
42885
 
42507
42886
  Commands:
42508
42887
  query Read rows with SELECT statements
@@ -42519,10 +42898,10 @@ Tips:
42519
42898
  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", `
42520
42899
 
42521
42900
  Examples:
42522
- $ ${CLI_NAME33} sql query "SELECT * FROM notes ORDER BY id"
42523
- $ ${CLI_NAME33} sql query "SELECT * FROM notes WHERE id = ?" --params '[42]'
42524
- $ ${CLI_NAME33} sql query "SELECT count(*) AS total FROM events" --db analytics --json
42525
- $ ${CLI_NAME33} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
42901
+ $ ${CLI_NAME34} sql query "SELECT * FROM notes ORDER BY id"
42902
+ $ ${CLI_NAME34} sql query "SELECT * FROM notes WHERE id = ?" --params '[42]'
42903
+ $ ${CLI_NAME34} sql query "SELECT count(*) AS total FROM events" --db analytics --json
42904
+ $ ${CLI_NAME34} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
42526
42905
 
42527
42906
  Output:
42528
42907
  Human output is formatted as a table. Piped output or --json returns
@@ -42563,11 +42942,11 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
42563
42942
  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", `
42564
42943
 
42565
42944
  Examples:
42566
- $ ${CLI_NAME33} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
42567
- $ ${CLI_NAME33} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["first note"]'
42568
- $ ${CLI_NAME33} sql execute "UPDATE notes SET body = ? WHERE id = ?" --params '["edited", 1]'
42569
- $ ${CLI_NAME33} sql execute "DROP TABLE old_notes" --db archive
42570
- $ ${CLI_NAME33} sql execute "DELETE FROM conversation WHERE id = ?" --space applications --db xyz.tinycloud.listen/conversations --params '["abc"]'
42945
+ $ ${CLI_NAME34} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
42946
+ $ ${CLI_NAME34} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["first note"]'
42947
+ $ ${CLI_NAME34} sql execute "UPDATE notes SET body = ? WHERE id = ?" --params '["edited", 1]'
42948
+ $ ${CLI_NAME34} sql execute "DROP TABLE old_notes" --db archive
42949
+ $ ${CLI_NAME34} sql execute "DELETE FROM conversation WHERE id = ?" --space applications --db xyz.tinycloud.listen/conversations --params '["abc"]'
42571
42950
 
42572
42951
  Output:
42573
42952
  Returns JSON with the changed row count and last inserted row id when available.
@@ -42596,9 +42975,9 @@ Output:
42596
42975
  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", `
42597
42976
 
42598
42977
  Examples:
42599
- $ ${CLI_NAME33} sql export
42600
- $ ${CLI_NAME33} sql export --db analytics --output analytics.db
42601
- $ ${CLI_NAME33} sql export --space applications --db xyz.tinycloud.listen/conversations --output listen.db
42978
+ $ ${CLI_NAME34} sql export
42979
+ $ ${CLI_NAME34} sql export --db analytics --output analytics.db
42980
+ $ ${CLI_NAME34} sql export --space applications --db xyz.tinycloud.listen/conversations --output listen.db
42602
42981
 
42603
42982
  Output:
42604
42983
  Writes the database file locally and returns JSON with the path and size.
@@ -42631,19 +43010,19 @@ Output:
42631
43010
  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", `
42632
43011
 
42633
43012
  Examples:
42634
- $ ${CLI_NAME33} sql copy --from-db com.tinycloud.conversation-sync/conversations \\
43013
+ $ ${CLI_NAME34} sql copy --from-db com.tinycloud.conversation-sync/conversations \\
42635
43014
  --to-db xyz.tinycloud.listen/conversations \\
42636
43015
  --space applications --dry-run
42637
- $ ${CLI_NAME33} sql copy --from-space applications --from-db com.foo/data \\
43016
+ $ ${CLI_NAME34} sql copy --from-space applications --from-db com.foo/data \\
42638
43017
  --to-space applications --to-db com.bar/data \\
42639
43018
  --table conversation --table participant
42640
43019
 
42641
43020
  Notes:
42642
43021
  - Refuses to run when (resolved space, db) is identical for source and destination.
42643
- - Does NOT create destination tables. Run the target app once (or use \`${CLI_NAME33} sql execute\`)
43022
+ - Does NOT create destination tables. Run the target app once (or use \`${CLI_NAME34} sql execute\`)
42644
43023
  to materialize the schema before copying.
42645
43024
  - One row at a time; suitable for small/medium datasets. Large copies should
42646
- use \`${CLI_NAME33} sql export\` + bulk import.
43025
+ use \`${CLI_NAME34} sql export\` + bulk import.
42647
43026
  - Authorization: the active session/delegation must cover sql/read on source
42648
43027
  AND sql/write on destination. Otherwise the relevant operation will fail.
42649
43028
  `).action(async (options, cmd) => {
@@ -42743,7 +43122,7 @@ init_errors();
42743
43122
  init_formatter();
42744
43123
  init_theme();
42745
43124
  init_permissions();
42746
- import { CLI_NAME as CLI_NAME34 } from "@tinycloud/operations/cli-name";
43125
+ import { CLI_NAME as CLI_NAME35 } from "@tinycloud/operations/cli-name";
42747
43126
  import {
42748
43127
  NodeWasmBindings as NodeWasmBindings2
42749
43128
  } from "@tinycloud/node-sdk";
@@ -43017,7 +43396,7 @@ function formatStatus(summary) {
43017
43396
  lines.push(`Default profile: ${theme.value(summary.defaultProfile)}`);
43018
43397
  lines.push("");
43019
43398
  if (summary.profiles.length === 0) {
43020
- lines.push(theme.muted(`No profiles configured. Run: ${CLI_NAME34} init`));
43399
+ lines.push(theme.muted(`No profiles configured. Run: ${CLI_NAME35} init`));
43021
43400
  return `${lines.join("\n")}
43022
43401
  `;
43023
43402
  }
@@ -43173,14 +43552,14 @@ function registerUpgradeCommand(program) {
43173
43552
  }
43174
43553
 
43175
43554
  // src/commands/vault.ts
43176
- import { readFile as readFile15 } from "fs/promises";
43555
+ import { readFile as readFile16 } from "fs/promises";
43177
43556
  init_profiles();
43178
43557
  init_formatter();
43179
43558
  init_errors();
43180
43559
  init_constants();
43181
43560
  init_sdk();
43182
43561
  import { PrivateKeySigner as PrivateKeySigner2 } from "@tinycloud/node-sdk";
43183
- async function readStdin4() {
43562
+ async function readStdin5() {
43184
43563
  const chunks = [];
43185
43564
  for await (const chunk of process.stdin) {
43186
43565
  chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
@@ -43235,9 +43614,9 @@ function registerVaultCommand(program) {
43235
43614
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
43236
43615
  }
43237
43616
  if (options.file) {
43238
- putValue = new Uint8Array(await readFile15(options.file));
43617
+ putValue = new Uint8Array(await readFile16(options.file));
43239
43618
  } else if (options.stdin) {
43240
- putValue = new Uint8Array(await readStdin4());
43619
+ putValue = new Uint8Array(await readStdin5());
43241
43620
  } else {
43242
43621
  putValue = value;
43243
43622
  }
@@ -43350,14 +43729,14 @@ function registerVaultCommand(program) {
43350
43729
  }
43351
43730
 
43352
43731
  // src/commands/vars.ts
43353
- import { readFile as readFile16 } from "fs/promises";
43732
+ import { readFile as readFile17 } from "fs/promises";
43354
43733
  init_profiles();
43355
43734
  init_formatter();
43356
43735
  init_errors();
43357
43736
  init_constants();
43358
43737
  init_sdk();
43359
43738
  var VARIABLES_PREFIX = "variables/";
43360
- async function readStdin5() {
43739
+ async function readStdin6() {
43361
43740
  const chunks = [];
43362
43741
  for await (const chunk of process.stdin) {
43363
43742
  chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
@@ -43455,9 +43834,9 @@ function registerVarsCommand(program) {
43455
43834
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
43456
43835
  }
43457
43836
  if (options.file) {
43458
- varValue = await readFile16(options.file, "utf-8");
43837
+ varValue = await readFile17(options.file, "utf-8");
43459
43838
  } else if (options.stdin) {
43460
- varValue = (await readStdin5()).toString("utf-8");
43839
+ varValue = (await readStdin6()).toString("utf-8");
43461
43840
  } else {
43462
43841
  varValue = value;
43463
43842
  }
@@ -43500,6 +43879,7 @@ function registerTinyCloudCommands(program) {
43500
43879
  registerAuthCommand(program);
43501
43880
  registerEnableCommand(program);
43502
43881
  registerKvCommand(program);
43882
+ registerKeysCommand(program);
43503
43883
  registerReplicaCommand(program);
43504
43884
  registerCourierCommand(program);
43505
43885
  registerSpaceCommand(program);