@tinycloud/cli 1.5.0-beta.1 → 1.5.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1857,7 +1857,7 @@ 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
1861
  function parseExpiry(duration) {
1862
1862
  if (typeof duration !== "string" || duration.length === 0) {
1863
1863
  throw new ManifestValidationError(
@@ -1900,6 +1900,14 @@ function expandEncryptionPermissionEntry(entry) {
1900
1900
  `tinycloud.encryption entries require path to be a networkId URN (got ${JSON.stringify(entry.path)})`
1901
1901
  );
1902
1902
  }
1903
+ if (entry.path.includes("/")) {
1904
+ throw new ManifestValidationError("named keys go in manifest.keys");
1905
+ }
1906
+ try {
1907
+ parseNetworkId(entry.path);
1908
+ } catch {
1909
+ throw new ManifestValidationError("tinycloud.encryption entries require a valid networkId URN");
1910
+ }
1903
1911
  const normalizedActions = [];
1904
1912
  for (const action of entry.actions) {
1905
1913
  if (action === "decrypt" || action === "tinycloud.encryption/decrypt") {
@@ -2004,8 +2012,52 @@ function validateManifest(input) {
2004
2012
  if (m.secrets !== void 0) {
2005
2013
  validateManifestSecrets(m.secrets);
2006
2014
  }
2015
+ if (Object.prototype.hasOwnProperty.call(m, "keys_app_id") && (typeof m.keys_app_id !== "string" || !KEY_APP_ID.test(m.keys_app_id))) {
2016
+ throw new ManifestValidationError("manifest.keys_app_id is invalid");
2017
+ }
2018
+ validateManifestKeys(m.keys_app_id ?? m.app_id, m.keys);
2007
2019
  return m;
2008
2020
  }
2021
+ function validateManifestKeys(appId, keys) {
2022
+ if (keys === void 0) return;
2023
+ if (!Array.isArray(keys) || keys.length > 32) {
2024
+ throw new ManifestValidationError("manifest.keys must contain at most 32 entries");
2025
+ }
2026
+ if (keys.length === 0) return;
2027
+ if (!KEY_APP_ID.test(appId)) {
2028
+ throw new ManifestValidationError("manifest.keys requires a valid app_id");
2029
+ }
2030
+ const paths = /* @__PURE__ */ new Set();
2031
+ let prefixSeen = false;
2032
+ for (const [index, value] of keys.entries()) {
2033
+ const label = `manifest.keys[${index}]`;
2034
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
2035
+ throw new ManifestValidationError(`${label} must be an object`);
2036
+ }
2037
+ const entry = value;
2038
+ const fields = Object.keys(entry);
2039
+ const hasPath = Object.prototype.hasOwnProperty.call(entry, "path");
2040
+ const hasPrefix = Object.prototype.hasOwnProperty.call(entry, "prefix");
2041
+ if (hasPath === hasPrefix || fields.some((field) => !["path", "prefix", "abilities"].includes(field))) {
2042
+ throw new ManifestValidationError(`${label} must have one path or prefix and abilities`);
2043
+ }
2044
+ 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))) {
2045
+ throw new ManifestValidationError(`${label}.abilities is invalid`);
2046
+ }
2047
+ if (hasPath) {
2048
+ const path = entry.path;
2049
+ if (typeof path !== "string" || path.length > 255 || !KEY_PATH.test(path) || !path.startsWith(`apps/${appId}/`) || paths.has(path)) {
2050
+ throw new ManifestValidationError(`${label}.path is invalid`);
2051
+ }
2052
+ paths.add(path);
2053
+ } else {
2054
+ if (entry.prefix !== `apps/${appId}/` || prefixSeen) {
2055
+ throw new ManifestValidationError(`${label}.prefix is invalid`);
2056
+ }
2057
+ prefixSeen = true;
2058
+ }
2059
+ }
2060
+ }
2009
2061
  function resolveManifestKnowledgeRoot(knowledge) {
2010
2062
  if (knowledge === void 0) {
2011
2063
  return void 0;
@@ -2405,7 +2457,7 @@ function withCapabilitiesReadForSpaces(resources) {
2405
2457
  ...[...spaces].map(capabilitiesReadPermission)
2406
2458
  ]);
2407
2459
  }
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;
2460
+ 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
2461
  var init_manifest = __esm({
2410
2462
  "../sdk-core/src/manifest.ts"() {
2411
2463
  "use strict";
@@ -2488,6 +2540,10 @@ var init_manifest = __esm({
2488
2540
  actions: ["read", "write"]
2489
2541
  }
2490
2542
  ];
2543
+ KEY_SEGMENT = "[a-z0-9][a-z0-9._-]{0,63}";
2544
+ KEY_APP_ID = new RegExp(`^${KEY_SEGMENT}$`);
2545
+ KEY_PATH = new RegExp(`^${KEY_SEGMENT}(?:/${KEY_SEGMENT}){2,7}$`);
2546
+ KEY_ABILITIES = ["key.create", "decrypt", "key.block", "key.unblock"];
2491
2547
  }
2492
2548
  });
2493
2549
 
@@ -15794,7 +15850,7 @@ async function refusal(response) {
15794
15850
  async function prepareLinkRecord(input) {
15795
15851
  const apiOrigin2 = checkApiOrigin(input.apiOrigin);
15796
15852
  const nowMs = toEpochMs(input.now);
15797
- const expiresMs = input.expiresAt.getTime();
15853
+ const expiresMs = Math.floor(input.expiresAt.getTime() / 1e3) * 1e3;
15798
15854
  if (!Number.isFinite(nowMs) || !Number.isFinite(expiresMs)) throw new LinkError("invalid_argument", "now and expiresAt must be valid dates");
15799
15855
  let blob;
15800
15856
  let key;
@@ -33002,7 +33058,8 @@ async function revokeDelegationOnNode(params) {
33002
33058
  throw new CLIError(
33003
33059
  "RAW_RECAP_RESOURCE_UNSUPPORTED",
33004
33060
  `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
33061
+ ExitCode.PERMISSION_DENIED,
33062
+ 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
33063
  );
33007
33064
  }
33008
33065
  fallbackSpaceId = fallback.space;
@@ -33827,7 +33884,7 @@ async function prepareProfileRemoteLogout(ctx, expectedSessionCid, discardLocalC
33827
33884
  "REVOCATION_AUTHORITY_UNAVAILABLE",
33828
33885
  "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
33886
  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." }
33887
+ { 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
33888
  );
33832
33889
  }
33833
33890
  const signerDid = addressToDID(
@@ -37447,9 +37504,12 @@ function registerKvCommand(program) {
37447
37504
  return handleError(error);
37448
37505
  }
37449
37506
  });
37450
- kv.command("put <key> [value]").description("Set a value").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, value, options, cmd) => {
37507
+ 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) => {
37451
37508
  try {
37452
37509
  assertAddressableKey(key);
37510
+ if (options.ifNoneMatch !== void 0 && options.ifNoneMatch !== "*") {
37511
+ throw new CLIError("USAGE_ERROR", "`--if-none-match` only accepts `'*'`.", ExitCode.USAGE_ERROR);
37512
+ }
37453
37513
  const globalOpts = cmd.optsWithGlobals();
37454
37514
  const ctx = await ProfileManager.resolveContext(globalOpts);
37455
37515
  const node = await ensureAuthenticated(ctx);
@@ -37473,8 +37533,15 @@ function registerKvCommand(program) {
37473
37533
  }
37474
37534
  }
37475
37535
  const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
37476
- const result = await withSpinner(`Writing ${key}...`, () => kv2.put(key, putValue));
37536
+ const result = await withSpinner(`Writing ${key}...`, () => kv2.put(
37537
+ key,
37538
+ putValue,
37539
+ options.ifNoneMatch === "*" ? { ifNoneMatch: "*" } : void 0
37540
+ ));
37477
37541
  if (!result.ok) {
37542
+ if (options.ifNoneMatch === "*" && result.error.code === "KV_PRECONDITION_FAILED") {
37543
+ throw new CLIError("KV_ALREADY_EXISTS", `Key "${key}" already exists, so nothing was written.`, ExitCode.PERMISSION_DENIED);
37544
+ }
37478
37545
  await throwKvError(result.error, spaceUri, ctx.profile);
37479
37546
  }
37480
37547
  outputJson({ key, written: true, ...retryFields(result.data?.retry) });