@tinycloud/cli 1.1.0-beta.7 → 1.1.0-beta.9

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.
@@ -163,7 +163,7 @@ import ora from "ora";
163
163
  function outputJson(data) {
164
164
  process.stdout.write(JSON.stringify(data, null, 2) + "\n");
165
165
  }
166
- function outputError(code3, message, hint) {
166
+ function outputError(code3, message, hint, meta) {
167
167
  if (isInteractive()) {
168
168
  process.stderr.write(
169
169
  `${theme.error("\u2717")} ${theme.label(code3)}: ${message}
@@ -180,6 +180,7 @@ function outputError(code3, message, hint) {
180
180
  error: { code: code3, message }
181
181
  };
182
182
  if (hint) payload.error.hint = hint;
183
+ if (meta) payload.error.meta = meta;
183
184
  process.stderr.write(JSON.stringify(payload, null, 2) + "\n");
184
185
  }
185
186
  }
@@ -260,24 +261,43 @@ var init_formatter = __esm({
260
261
  // src/output/errors.ts
261
262
  import { readFileSync, readdirSync } from "fs";
262
263
  import { join as join2 } from "path";
263
- import { ProfileLockTimeoutError } from "@tinycloud/operations/state";
264
+ import { ProfileDeletedError, ProfileLockTimeoutError } from "@tinycloud/operations/state";
265
+ import { authorizationVerdictOf, parseCapabilityResource, SERVICE_LONG_TO_SHORT, validatedCapabilityOf } from "@tinycloud/sdk-core";
264
266
  function setActiveProfileName(name) {
265
267
  activeProfileName = name;
266
268
  }
269
+ function cliErrorFromService(error, message = error.message) {
270
+ if (error instanceof CLIError) return error;
271
+ const meta = { ...error.meta };
272
+ delete meta.hint;
273
+ const status = error.status ?? error.statusCode;
274
+ if (typeof status === "number" && Number.isInteger(status) && status >= 400 && status <= 599) meta.status = status;
275
+ const verdict = authorizationVerdictOf({ ...error, meta });
276
+ if (verdict === void 0 && message.includes("Missing private key parameter in JWK")) {
277
+ return missingPrivateKeyError();
278
+ }
279
+ const missingCapability = validatedCapabilityOf({ ...error, meta }) !== void 0;
280
+ const denied = verdict === "forbidden" || verdict === "unauthenticated" && missingCapability;
281
+ return new CLIError(
282
+ denied ? "PERMISSION_DENIED" : verdict === "unauthenticated" ? "AUTH_REQUIRED" : error.code,
283
+ message,
284
+ denied ? ExitCode.PERMISSION_DENIED : verdict === "unauthenticated" ? ExitCode.AUTH_REQUIRED : ExitCode.ERROR,
285
+ meta
286
+ );
287
+ }
267
288
  function wrapError(error) {
289
+ if (error instanceof CLIError) return error;
268
290
  const message = error instanceof Error ? error.message : String(error);
269
- if (message.includes("Missing private key parameter in JWK")) {
270
- const profileName = activeProfileName ?? process.env.TC_PROFILE ?? DEFAULT_PROFILE;
271
- return new CLIError(
272
- "AUTH_REQUIRED",
273
- `Profile "${profileName}" cannot restore its session because its private key material is missing.`,
274
- ExitCode.AUTH_REQUIRED,
275
- {
276
- hint: `Sign in again with: tc --profile ${profileName} auth login --method openkey`
277
- }
278
- );
291
+ const verdict = authorizationVerdictOf(error);
292
+ if (verdict === "unauthenticated") {
293
+ return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
294
+ }
295
+ if (verdict === "forbidden") {
296
+ return new CLIError("PERMISSION_DENIED", message, ExitCode.PERMISSION_DENIED);
297
+ }
298
+ if (verdict === void 0 && message.includes("Missing private key parameter in JWK")) {
299
+ return missingPrivateKeyError();
279
300
  }
280
- if (error instanceof CLIError) return error;
281
301
  if (error instanceof ProfileLockTimeoutError) {
282
302
  return new CLIError(
283
303
  "PROFILE_LOCK_TIMEOUT",
@@ -286,13 +306,16 @@ function wrapError(error) {
286
306
  { hint: "Wait for the other command to finish and retry. A crashed process's lock is reclaimed automatically after 30 s." }
287
307
  );
288
308
  }
289
- if (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired")) {
309
+ if (error instanceof ProfileDeletedError) {
310
+ return new CLIError("PROFILE_NOT_FOUND", message);
311
+ }
312
+ if (verdict === void 0 && (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired"))) {
290
313
  return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
291
314
  }
292
315
  if (message.includes("NOT_FOUND") || message.includes("KV_NOT_FOUND")) {
293
316
  return new CLIError("NOT_FOUND", message, ExitCode.NOT_FOUND);
294
317
  }
295
- if (message.includes("PERMISSION_DENIED")) {
318
+ if (verdict === void 0 && message.includes("PERMISSION_DENIED")) {
296
319
  return new CLIError("PERMISSION_DENIED", message, ExitCode.PERMISSION_DENIED);
297
320
  }
298
321
  if (message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("fetch failed")) {
@@ -300,39 +323,51 @@ function wrapError(error) {
300
323
  }
301
324
  return new CLIError("ERROR", message, ExitCode.ERROR);
302
325
  }
326
+ function missingPrivateKeyError() {
327
+ const profileName = activeProfileName ?? process.env.TC_PROFILE ?? DEFAULT_PROFILE;
328
+ return new CLIError(
329
+ "AUTH_REQUIRED",
330
+ `Profile "${profileName}" cannot restore its session because its private key material is missing.`,
331
+ ExitCode.AUTH_REQUIRED,
332
+ { hint: `Sign in again with: tc --profile ${profileName} auth login --method openkey` }
333
+ );
334
+ }
303
335
  function handleError(error) {
304
336
  const cliError = wrapError(error);
305
337
  const prebuilt = typeof cliError.metadata?.hint === "string" ? cliError.metadata.hint : void 0;
306
338
  const hint = prebuilt ?? buildAuthHint(cliError) ?? (cliError.code === "NETWORK_ERROR" ? buildNetworkHint() : void 0);
307
- outputError(cliError.code, cliError.message, hint);
339
+ const authMeta = cliError.code === "AUTH_REQUIRED" || cliError.code === "PERMISSION_DENIED" ? cliError.metadata : void 0;
340
+ const meta = {};
341
+ if (cliError.status !== void 0 && authMeta !== void 0) meta.status = cliError.status;
342
+ const capability = validatedCapabilityOf({ meta: authMeta });
343
+ if (capability) {
344
+ meta.resource = capability.resource;
345
+ meta.requiredAction = capability.requiredAction;
346
+ }
347
+ outputError(cliError.code, cliError.message, hint, Object.keys(meta).length ? meta : void 0);
308
348
  process.exit(cliError.exitCode);
309
349
  }
310
350
  function buildAuthHint(error) {
311
- const resource = error.metadata?.resource;
312
- const requiredAction = error.metadata?.requiredAction;
313
- if (typeof resource !== "string" || typeof requiredAction !== "string") {
314
- return void 0;
315
- }
316
- const spec = capSpecFromAuthMeta(resource, requiredAction);
351
+ const capability = validatedCapabilityOf({ meta: error.metadata });
352
+ if (!capability) return void 0;
353
+ const spec = capSpecFromAuthMeta(capability.resource, capability.requiredAction);
317
354
  if (!spec) return void 0;
318
355
  return [
319
356
  "The active session is missing a TinyCloud capability.",
320
- `Request it with: tc auth request --cap "${spec}"`,
357
+ `Request it with: tc auth request --cap '${spec.replaceAll("'", "'\\''")}'`,
321
358
  "Then retry the original command."
322
359
  ].join("\n");
323
360
  }
324
361
  function capSpecFromAuthMeta(resource, action) {
325
- const slash = resource.indexOf("/");
326
- if (slash <= 0 || slash === resource.length - 1) return void 0;
327
- const spaceUri = resource.slice(0, slash);
328
- const rest = resource.slice(slash + 1);
329
- const nextSlash = rest.indexOf("/");
330
- if (nextSlash <= 0) return void 0;
331
- const serviceShort = rest.slice(0, nextSlash);
332
- const path = rest.slice(nextSlash + 1);
333
- const actionName = action.includes("/") ? action.slice(action.indexOf("/") + 1) : action;
334
- const spaceName = spaceUri.startsWith("tinycloud:") ? spaceUri.slice(spaceUri.lastIndexOf(":") + 1) : spaceUri;
335
- return `tinycloud.${serviceShort}:${spaceName}:${path}:${actionName}`;
362
+ const slash = action.indexOf("/");
363
+ if (slash < 0) return void 0;
364
+ const longService = action.slice(0, slash);
365
+ const serviceShort = SERVICE_LONG_TO_SHORT[longService];
366
+ if (!serviceShort) return void 0;
367
+ const parsed = parseCapabilityResource(resource, serviceShort);
368
+ if (!parsed) return void 0;
369
+ const spaceName = parsed.space.slice(parsed.space.lastIndexOf(":") + 1);
370
+ return `${longService}:${spaceName}:${parsed.path}:${action.slice(slash + 1)}`;
336
371
  }
337
372
  function buildNetworkHint() {
338
373
  const readHost = (name) => {
@@ -386,7 +421,9 @@ var init_errors = __esm({
386
421
  this.exitCode = exitCode;
387
422
  this.metadata = metadata;
388
423
  this.name = "CLIError";
424
+ if (typeof metadata?.status === "number" && Number.isInteger(metadata.status) && metadata.status >= 400 && metadata.status <= 599) this.status = metadata.status;
389
425
  }
426
+ status;
390
427
  };
391
428
  }
392
429
  });
@@ -644,6 +681,8 @@ import { join as join3 } from "path";
644
681
  import {
645
682
  profilePath,
646
683
  readSession,
684
+ recordProfileDeletion,
685
+ refuseWriteToDeletedProfile,
647
686
  removeSession,
648
687
  withProfileLock,
649
688
  writeSession
@@ -758,10 +797,12 @@ var init_profiles = __esm({
758
797
  * removed while holding the profile lock, so another writer's critical
759
798
  * section never sees them vanish midway: session and key first, settings
760
799
  * last, so a crash midway never leaves a session or key without its
761
- * profile. `.lock` itself is left to the lock's release; the then-empty
762
- * directory is removed afterwards unless another writer took the lock (or
763
- * wrote) meanwhile. A profile directory that is a symlink is unlinked, its
764
- * target left alone.
800
+ * profile. The deletion is then recorded, so a store write that waited for
801
+ * the lock meanwhile refuses rather than recreating a profile with only a
802
+ * session in it. `.lock` itself is left to the lock's release; the
803
+ * then-empty directory is removed afterwards unless another writer took
804
+ * the lock (or wrote) meanwhile. A profile directory that is a symlink is
805
+ * unlinked, its target left alone.
765
806
  * Throws if the name is not one path segment or names the default profile.
766
807
  */
767
808
  static async deleteProfile(name) {
@@ -782,11 +823,12 @@ var init_profiles = __esm({
782
823
  `Cannot delete the default profile "${name}". Change the default first with \`tc profile default <other>\`.`
783
824
  );
784
825
  }
785
- const isLink = await lstat(profileDir).then((stats) => stats.isSymbolicLink(), (error) => {
786
- if (error.code === "ENOENT") return false;
826
+ const kind = await lstat(profileDir).then((stats) => stats.isSymbolicLink() ? "link" : "present", (error) => {
827
+ if (error.code === "ENOENT") return "missing";
787
828
  throw error;
788
829
  });
789
- if (isLink) {
830
+ if (kind === "missing") return;
831
+ if (kind === "link") {
790
832
  await rm2(profileDir, { force: true });
791
833
  return;
792
834
  }
@@ -796,6 +838,7 @@ var init_profiles = __esm({
796
838
  if (entry !== ".lock" && entry !== "profile.json") await rm2(join3(profileDir, entry), { recursive: true, force: true });
797
839
  }
798
840
  await rm2(join3(profileDir, "profile.json"), { force: true });
841
+ await recordProfileDeletion(name);
799
842
  });
800
843
  await rmdir(profileDir).catch((error) => {
801
844
  if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY" && error.code !== "EEXIST") throw error;
@@ -808,9 +851,14 @@ var init_profiles = __esm({
808
851
  static async getKey(name) {
809
852
  return readJson(join3(PROFILES_DIR, name, "key.json"));
810
853
  }
811
- /** Saves a JWK key under the profile lock (0600, in an owner-only profile directory). */
854
+ /**
855
+ * Saves a JWK key under the profile lock (0600, in an owner-only profile
856
+ * directory). Refused (PROFILE_NOT_FOUND) if the profile was deleted while
857
+ * this waited for the lock, rather than leaving a key-only profile.
858
+ */
812
859
  static async setKey(name, jwk) {
813
860
  await _ProfileManager.withLock(name, async () => {
861
+ await refuseWriteToDeletedProfile(name);
814
862
  await writeJson(join3(await _ProfileManager.ensureProfileDir(name), "key.json"), jwk);
815
863
  });
816
864
  }
@@ -1779,6 +1827,7 @@ import {
1779
1827
  profileStoreMetadataPath,
1780
1828
  readAdditionalDelegations,
1781
1829
  readAuthRequests,
1830
+ refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
1782
1831
  updateProfileStore,
1783
1832
  withProfileLock as withProfileLock2,
1784
1833
  writeJsonAtomic
@@ -1865,6 +1914,7 @@ var CAPABILITIES2 = deriveServiceConstants("capabilities");
1865
1914
  var HOOKS = deriveServiceConstants("hooks");
1866
1915
  var ENCRYPTION = deriveServiceConstants("encryption");
1867
1916
  var SPACE = deriveServiceConstants("space");
1917
+ var CAPABILITY_REGISTRY = CAPABILITIES;
1868
1918
  var DEFAULT_MANIFEST_SPACE = "applications";
1869
1919
  var ACCOUNT_REGISTRY_SPACE = "account";
1870
1920
  var ACCOUNT_REGISTRY_PATH = "applications/";
@@ -6805,6 +6855,75 @@ var ServiceSessionSchema = external_exports.object({
6805
6855
  /** The session key JWK (required for invoke) */
6806
6856
  jwk: external_exports.object({}).passthrough()
6807
6857
  });
6858
+ var URN_PREFIX = "urn:tinycloud:encryption:";
6859
+ var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
6860
+ var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
6861
+ var NetworkIdError = class extends Error {
6862
+ constructor(message) {
6863
+ super(message);
6864
+ this.name = "NetworkIdError";
6865
+ }
6866
+ };
6867
+ function parseNetworkId(networkId) {
6868
+ if (typeof networkId !== "string" || networkId.length === 0) {
6869
+ throw new NetworkIdError("networkId must be a non-empty string");
6870
+ }
6871
+ if (!networkId.startsWith(URN_PREFIX)) {
6872
+ throw new NetworkIdError(
6873
+ `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
6874
+ );
6875
+ }
6876
+ const body = networkId.slice(URN_PREFIX.length);
6877
+ const lastColon = body.lastIndexOf(":");
6878
+ if (lastColon <= 0 || lastColon === body.length - 1) {
6879
+ throw new NetworkIdError(
6880
+ `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
6881
+ );
6882
+ }
6883
+ const ownerDid = body.slice(0, lastColon);
6884
+ const name = body.slice(lastColon + 1);
6885
+ if (!ownerDid.startsWith("did:")) {
6886
+ throw new NetworkIdError(
6887
+ `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
6888
+ );
6889
+ }
6890
+ const didParts = ownerDid.split(":");
6891
+ if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
6892
+ throw new NetworkIdError(
6893
+ `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
6894
+ );
6895
+ }
6896
+ if (!NETWORK_NAME_RE.test(name)) {
6897
+ throw new NetworkIdError(
6898
+ `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
6899
+ );
6900
+ }
6901
+ return { networkId, ownerDid, name };
6902
+ }
6903
+ function parsePkhOwnerDid(ownerDid) {
6904
+ const match = ownerDid.match(PKH_EIP155_DID_RE);
6905
+ if (!match) return null;
6906
+ return {
6907
+ chainId: match[1],
6908
+ address: match[2].toLowerCase()
6909
+ };
6910
+ }
6911
+ function ownerDidMatches(a, b) {
6912
+ const aPkh = parsePkhOwnerDid(a);
6913
+ const bPkh = parsePkhOwnerDid(b);
6914
+ if (aPkh && bPkh) {
6915
+ return aPkh.chainId === bPkh.chainId && aPkh.address === bPkh.address;
6916
+ }
6917
+ return a === b;
6918
+ }
6919
+ function networkDiscoveryKey(name) {
6920
+ if (!NETWORK_NAME_RE.test(name)) {
6921
+ throw new NetworkIdError(
6922
+ `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
6923
+ );
6924
+ }
6925
+ return `.well-known/encryption/network/${name}`;
6926
+ }
6808
6927
  function parsePermissionHint(value) {
6809
6928
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
6810
6929
  const candidate = value;
@@ -6872,6 +6991,101 @@ function parseAuthError(responseText) {
6872
6991
  function authUnauthorizedError(service, message, meta) {
6873
6992
  return serviceError(ErrorCodes.AUTH_UNAUTHORIZED, message, service, { meta });
6874
6993
  }
6994
+ var grantableActions = new Set(
6995
+ CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
6996
+ );
6997
+ var capabilityServices = new Set(
6998
+ CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
6999
+ );
7000
+ function parseCapabilityResource2(resource, service) {
7001
+ if (resource.length === 0 || resource.length > 1024 || !capabilityServices.has(service) || /[\s\p{C}*#]/u.test(resource) || resource.includes("://")) return void 0;
7002
+ const segments = resource.split("/");
7003
+ if (!/^tinycloud:[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(segments[0])) return void 0;
7004
+ const serviceIndex = segments.findIndex(
7005
+ (segment, index) => index > 0 && capabilityServices.has(segment)
7006
+ );
7007
+ if (serviceIndex < 1 || segments[serviceIndex] !== service) return void 0;
7008
+ if (segments.slice(1, serviceIndex).some(
7009
+ (segment) => segment.length === 0 || segment === "." || segment === ".."
7010
+ )) return void 0;
7011
+ const pathSegments = segments.slice(serviceIndex + 1);
7012
+ if (pathSegments.some((segment) => segment === "." || segment === "..")) return void 0;
7013
+ if (service !== "kv" && (pathSegments.length === 0 || pathSegments[pathSegments.length - 1] === "")) return void 0;
7014
+ return {
7015
+ space: segments.slice(0, serviceIndex).join("/"),
7016
+ path: pathSegments.join("/")
7017
+ };
7018
+ }
7019
+ function safeCapabilityResource(value, service, action) {
7020
+ if (typeof value !== "string" || value.length === 0 || value.length > 1024) return false;
7021
+ if (service === "encryption" && value.startsWith("urn:tinycloud:encryption:")) {
7022
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.:/-]*$/.test(value) || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..")) return false;
7023
+ try {
7024
+ parseNetworkId(value);
7025
+ return true;
7026
+ } catch {
7027
+ return false;
7028
+ }
7029
+ }
7030
+ const parsed = parseCapabilityResource2(value, service);
7031
+ return parsed !== void 0 && (!parsed.path.endsWith("/") || service === "kv" && action === "tinycloud.kv/list") && (parsed.path !== "" || service === "kv" && action === "tinycloud.kv/list");
7032
+ }
7033
+ function validatedCapabilityOf2(error) {
7034
+ const verdict = authorizationVerdictOf2(error);
7035
+ if (verdict !== "unauthenticated" && verdict !== "forbidden") return void 0;
7036
+ const seen = /* @__PURE__ */ new Set();
7037
+ let node = error;
7038
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
7039
+ seen.add(node);
7040
+ const record = node;
7041
+ const status = errorStatusOf(record);
7042
+ const meta = record.meta;
7043
+ if ((status === 401 || status === 403 || status === void 0 && record.code === ErrorCodes.AUTH_UNAUTHORIZED) && typeof meta === "object" && meta !== null && !Array.isArray(meta)) {
7044
+ const metadata = meta;
7045
+ const { resource, requiredAction } = metadata;
7046
+ if (typeof requiredAction === "string" && grantableActions.has(requiredAction)) {
7047
+ const actionService = requiredAction.slice("tinycloud.".length, requiredAction.indexOf("/"));
7048
+ const explicitService = record.service === void 0 ? metadata.service : record.service;
7049
+ if (safeCapabilityResource(resource, actionService, requiredAction) && (explicitService === void 0 || explicitService === actionService || explicitService === `tinycloud.${actionService}`)) {
7050
+ return { resource, requiredAction };
7051
+ }
7052
+ }
7053
+ }
7054
+ node = record.cause;
7055
+ }
7056
+ return void 0;
7057
+ }
7058
+ var MAX_CAUSE_DEPTH = 8;
7059
+ function errorStatusOf(node) {
7060
+ const meta = node.meta;
7061
+ const candidates = [
7062
+ node.status,
7063
+ node.statusCode,
7064
+ typeof meta === "object" && meta !== null ? meta.status : void 0
7065
+ ];
7066
+ for (const candidate of candidates) {
7067
+ if (typeof candidate === "number" && Number.isInteger(candidate) && candidate >= 400 && candidate <= 599) {
7068
+ return candidate;
7069
+ }
7070
+ }
7071
+ return void 0;
7072
+ }
7073
+ function authorizationVerdictOf2(error) {
7074
+ const seen = /* @__PURE__ */ new Set();
7075
+ let sawUnauthorizedCode = false;
7076
+ let node = error;
7077
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
7078
+ seen.add(node);
7079
+ const record = node;
7080
+ const status = errorStatusOf(record);
7081
+ if (status === 401) return "unauthenticated";
7082
+ if (status === 403) return "forbidden";
7083
+ if (status !== void 0) return "other";
7084
+ if (record.code === ErrorCodes.AUTH_UNAUTHORIZED) sawUnauthorizedCode = true;
7085
+ node = record.cause;
7086
+ }
7087
+ return sawUnauthorizedCode ? "forbidden" : void 0;
7088
+ }
6875
7089
  function storageQuotaExceededError(service, message, meta) {
6876
7090
  return {
6877
7091
  code: ErrorCodes.STORAGE_QUOTA_EXCEEDED,
@@ -7567,12 +7781,14 @@ var KVService = class extends BaseService {
7567
7781
  signal: this.combineSignals(options?.signal)
7568
7782
  });
7569
7783
  if (!response.ok) {
7570
- const errorText = await response.text();
7784
+ const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
7571
7785
  if (response.status === 401 || response.status === 403) {
7572
7786
  return this.authorizationFailure(
7573
7787
  `Failed to batch read ${keys.length} key(s)`,
7574
7788
  response,
7575
- errorText
7789
+ errorText,
7790
+ paths,
7791
+ action
7576
7792
  );
7577
7793
  }
7578
7794
  if (response.status === 413) {
@@ -7662,7 +7878,7 @@ var KVService = class extends BaseService {
7662
7878
  return text;
7663
7879
  }
7664
7880
  }
7665
- async createSignedReadUrlError(response, key) {
7881
+ async createSignedReadUrlError(response, key, path) {
7666
7882
  let errorText = response.statusText;
7667
7883
  try {
7668
7884
  const text = await response.text();
@@ -7675,7 +7891,9 @@ var KVService = class extends BaseService {
7675
7891
  return this.authorizationFailure(
7676
7892
  `Failed to create signed read URL for key ${JSON.stringify(key)}`,
7677
7893
  response,
7678
- errorText
7894
+ errorText,
7895
+ [path],
7896
+ KVAction.GET
7679
7897
  );
7680
7898
  }
7681
7899
  const code3 = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
@@ -7688,21 +7906,42 @@ var KVService = class extends BaseService {
7688
7906
  )
7689
7907
  );
7690
7908
  }
7909
+ /** A known 401/403 keeps its typed status even if reading its body fails. */
7910
+ async readAuthorizationText(response) {
7911
+ try {
7912
+ return await response.text();
7913
+ } catch {
7914
+ return "";
7915
+ }
7916
+ }
7691
7917
  /**
7692
7918
  * AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
7693
7919
  * status and the server text (`<context>: <status> - <text>`, the same
7694
7920
  * shape as every other KV failure) so callers that rethrow only the message
7695
7921
  * still see the status; `meta.status` carries it for typed callers.
7696
- * Resource/ability hints are parsed from the raw body, never the message.
7922
+ * Server hints must match the requested path/action (whether the node sends
7923
+ * a relative path or full resource); metadata uses only the request-derived
7924
+ * canonical resource under the authenticated session's space.
7697
7925
  */
7698
- authorizationFailure(context, response, errorText, extraMeta = {}) {
7926
+ authorizationFailure(context, response, errorText, paths, expectedAction, extraMeta = {}) {
7699
7927
  const { resource, action } = parseAuthError(errorText);
7928
+ const requestedPath = paths.find(
7929
+ (path) => resource === path || resource === `${this.context.session.spaceId}/kv/${path}`
7930
+ );
7931
+ const capability = requestedPath !== void 0 && action === expectedAction ? validatedCapabilityOf2({
7932
+ service: "kv",
7933
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
7934
+ meta: {
7935
+ status: response.status,
7936
+ resource: `${this.context.session.spaceId}/kv/${requestedPath}`,
7937
+ requiredAction: action
7938
+ }
7939
+ }) : void 0;
7700
7940
  const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
7701
7941
  return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
7702
7942
  status: response.status,
7703
- ...action && { requiredAction: action },
7704
- ...resource && { resource },
7705
- ...extraMeta
7943
+ ...extraMeta,
7944
+ ...capability
7706
7945
  }));
7707
7946
  }
7708
7947
  normalizeSignedReadUrlResponse(data) {
@@ -7746,13 +7985,24 @@ var KVService = class extends BaseService {
7746
7985
  );
7747
7986
  if (!response.ok) {
7748
7987
  if (response.status === 401 || response.status === 403) {
7749
- const errorText2 = await response.text();
7988
+ const errorText2 = await this.readAuthorizationText(response);
7750
7989
  const permissionHint = parsePermissionHintFromErrorText(errorText2);
7990
+ const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf2({
7991
+ service: "kv",
7992
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
7993
+ meta: {
7994
+ status: response.status,
7995
+ resource: `${this.context.session.spaceId}/kv/${path}`,
7996
+ requiredAction: KVAction.GET
7997
+ }
7998
+ }) : void 0;
7751
7999
  return this.authorizationFailure(
7752
8000
  `Failed to get key ${JSON.stringify(key)}`,
7753
8001
  response,
7754
8002
  errorText2,
7755
- permissionHint === void 0 ? {} : { permissionHint }
8003
+ [path],
8004
+ KVAction.GET,
8005
+ structuredCapability ? { permissionHint, ...structuredCapability } : {}
7756
8006
  );
7757
8007
  }
7758
8008
  if (response.status === 404) {
@@ -7829,7 +8079,9 @@ var KVService = class extends BaseService {
7829
8079
  return this.authorizationFailure(
7830
8080
  `Failed to put key ${JSON.stringify(key)}`,
7831
8081
  response,
7832
- await response.text()
8082
+ await this.readAuthorizationText(response),
8083
+ [path],
8084
+ KVAction.PUT
7833
8085
  );
7834
8086
  }
7835
8087
  if (!response.ok) {
@@ -7958,7 +8210,9 @@ var KVService = class extends BaseService {
7958
8210
  return this.authorizationFailure(
7959
8211
  `Failed to batch put ${items.length} key(s)`,
7960
8212
  response,
7961
- errorText
8213
+ errorText,
8214
+ paths,
8215
+ KVAction.PUT
7962
8216
  );
7963
8217
  }
7964
8218
  const quotaError = this.handleQuotaErrorResponse(
@@ -8079,11 +8333,13 @@ var KVService = class extends BaseService {
8079
8333
  }
8080
8334
  );
8081
8335
  if (!response.ok) {
8082
- if (response.status === 401) {
8336
+ if (response.status === 401 || response.status === 403) {
8083
8337
  return this.authorizationFailure(
8084
8338
  "Failed to list keys",
8085
8339
  response,
8086
- await response.text()
8340
+ await this.readAuthorizationText(response),
8341
+ [listPath],
8342
+ KVAction.LIST
8087
8343
  );
8088
8344
  }
8089
8345
  const errorText = await response.text();
@@ -8132,11 +8388,13 @@ var KVService = class extends BaseService {
8132
8388
  options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
8133
8389
  );
8134
8390
  if (!response.ok) {
8135
- if (response.status === 401) {
8391
+ if (response.status === 401 || response.status === 403) {
8136
8392
  return this.authorizationFailure(
8137
8393
  `Failed to delete key ${JSON.stringify(key)}`,
8138
8394
  response,
8139
- await response.text()
8395
+ await this.readAuthorizationText(response),
8396
+ [path],
8397
+ KVAction.DELETE
8140
8398
  );
8141
8399
  }
8142
8400
  if (response.status === 404) {
@@ -8194,11 +8452,13 @@ var KVService = class extends BaseService {
8194
8452
  options?.signal
8195
8453
  );
8196
8454
  if (!response.ok) {
8197
- if (response.status === 401) {
8455
+ if (response.status === 401 || response.status === 403) {
8198
8456
  return this.authorizationFailure(
8199
8457
  `Failed to get metadata for key ${JSON.stringify(key)}`,
8200
8458
  response,
8201
- await response.text()
8459
+ await this.readAuthorizationText(response),
8460
+ [path],
8461
+ KVAction.HEAD
8202
8462
  );
8203
8463
  }
8204
8464
  if (response.status === 404) {
@@ -8265,7 +8525,7 @@ var KVService = class extends BaseService {
8265
8525
  signal: this.combineSignals(options?.signal)
8266
8526
  });
8267
8527
  if (!response.ok) {
8268
- return this.createSignedReadUrlError(response, key);
8528
+ return this.createSignedReadUrlError(response, key, path);
8269
8529
  }
8270
8530
  const signedUrl = this.normalizeSignedReadUrlResponse(
8271
8531
  await response.json()
@@ -8489,14 +8749,15 @@ var SQLService = class extends BaseService {
8489
8749
  };
8490
8750
  if (options?.maxRows !== void 0) body.maxRows = options.maxRows;
8491
8751
  if (options?.maxBytes !== void 0) body.maxBytes = options.maxBytes;
8752
+ const action = this.actionForSql(sql, SQLAction.READ);
8492
8753
  const response = await this.invokeSQL(
8493
8754
  dbName,
8494
- this.actionForSql(sql, SQLAction.READ),
8755
+ action,
8495
8756
  body,
8496
8757
  options?.signal
8497
8758
  );
8498
8759
  if (!response.ok) {
8499
- return this.handleErrorResponse(response, "query");
8760
+ return this.handleErrorResponse(response, "query", dbName, [action]);
8500
8761
  }
8501
8762
  const data = await response.json();
8502
8763
  return ok(data);
@@ -8525,14 +8786,15 @@ var SQLService = class extends BaseService {
8525
8786
  (statement) => this.actionForSql(statement, SQLAction.SCHEMA)
8526
8787
  )
8527
8788
  ];
8789
+ const requestedActions2 = this.dedupeActions(actions);
8528
8790
  const response = await this.invokeSQL(
8529
8791
  dbName,
8530
- this.dedupeActions(actions),
8792
+ requestedActions2,
8531
8793
  body,
8532
8794
  options?.signal
8533
8795
  );
8534
8796
  if (!response.ok) {
8535
- return this.handleErrorResponse(response, "execute");
8797
+ return this.handleErrorResponse(response, "execute", dbName, requestedActions2);
8536
8798
  }
8537
8799
  const data = await response.json();
8538
8800
  return ok(data);
@@ -8547,14 +8809,15 @@ var SQLService = class extends BaseService {
8547
8809
  return err(authRequiredError("sql"));
8548
8810
  }
8549
8811
  try {
8812
+ const requestedActions2 = this.actionsForSqlBatch(statements);
8550
8813
  const response = await this.invokeSQL(
8551
8814
  dbName,
8552
- this.actionsForSqlBatch(statements),
8815
+ requestedActions2,
8553
8816
  { action: "batch", statements },
8554
8817
  options?.signal
8555
8818
  );
8556
8819
  if (!response.ok) {
8557
- return this.handleErrorResponse(response, "batch");
8820
+ return this.handleErrorResponse(response, "batch", dbName, requestedActions2);
8558
8821
  }
8559
8822
  const data = await response.json();
8560
8823
  return ok(data);
@@ -8576,7 +8839,7 @@ var SQLService = class extends BaseService {
8576
8839
  options?.signal
8577
8840
  );
8578
8841
  if (!response.ok) {
8579
- return this.handleErrorResponse(response, "executeStatement");
8842
+ return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
8580
8843
  }
8581
8844
  const data = await response.json();
8582
8845
  return ok(data);
@@ -8598,7 +8861,7 @@ var SQLService = class extends BaseService {
8598
8861
  options?.signal
8599
8862
  );
8600
8863
  if (!response.ok) {
8601
- return this.handleErrorResponse(response, "export");
8864
+ return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
8602
8865
  }
8603
8866
  const resp = response;
8604
8867
  if (typeof resp.blob === "function") {
@@ -8732,7 +8995,7 @@ var SQLService = class extends BaseService {
8732
8995
  }))
8733
8996
  );
8734
8997
  }
8735
- async handleErrorResponse(response, operation) {
8998
+ async handleErrorResponse(response, operation, dbName, requestedActions2) {
8736
8999
  const errorText = await response.text();
8737
9000
  const meta = responseErrorMeta(response.status, response.statusText, errorText);
8738
9001
  if (response.status === 402) {
@@ -8751,9 +9014,11 @@ var SQLService = class extends BaseService {
8751
9014
  errorBody
8752
9015
  );
8753
9016
  if (response.status === 401) {
8754
- const { resource, action } = parseAuthError(errorText);
8755
- if (action) meta.requiredAction = action;
8756
- if (resource) meta.resource = resource;
9017
+ const { action } = parseAuthError(errorText);
9018
+ if (action && requestedActions2.includes(action)) {
9019
+ meta.requiredAction = action;
9020
+ meta.resource = `${this.context.session.spaceId}/sql/${dbName}`;
9021
+ }
8757
9022
  }
8758
9023
  return err(
8759
9024
  serviceError(errorCode2, message, "sql", { meta })
@@ -11502,75 +11767,6 @@ function canonicalHashHex(sha2563, value) {
11502
11767
  const canonical = canonicalize(value);
11503
11768
  return hexEncode2(sha2563(utf8Encode(canonical)));
11504
11769
  }
11505
- var URN_PREFIX = "urn:tinycloud:encryption:";
11506
- var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
11507
- var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
11508
- var NetworkIdError = class extends Error {
11509
- constructor(message) {
11510
- super(message);
11511
- this.name = "NetworkIdError";
11512
- }
11513
- };
11514
- function parseNetworkId(networkId) {
11515
- if (typeof networkId !== "string" || networkId.length === 0) {
11516
- throw new NetworkIdError("networkId must be a non-empty string");
11517
- }
11518
- if (!networkId.startsWith(URN_PREFIX)) {
11519
- throw new NetworkIdError(
11520
- `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
11521
- );
11522
- }
11523
- const body = networkId.slice(URN_PREFIX.length);
11524
- const lastColon = body.lastIndexOf(":");
11525
- if (lastColon <= 0 || lastColon === body.length - 1) {
11526
- throw new NetworkIdError(
11527
- `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
11528
- );
11529
- }
11530
- const ownerDid = body.slice(0, lastColon);
11531
- const name = body.slice(lastColon + 1);
11532
- if (!ownerDid.startsWith("did:")) {
11533
- throw new NetworkIdError(
11534
- `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
11535
- );
11536
- }
11537
- const didParts = ownerDid.split(":");
11538
- if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
11539
- throw new NetworkIdError(
11540
- `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
11541
- );
11542
- }
11543
- if (!NETWORK_NAME_RE.test(name)) {
11544
- throw new NetworkIdError(
11545
- `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
11546
- );
11547
- }
11548
- return { networkId, ownerDid, name };
11549
- }
11550
- function parsePkhOwnerDid(ownerDid) {
11551
- const match = ownerDid.match(PKH_EIP155_DID_RE);
11552
- if (!match) return null;
11553
- return {
11554
- chainId: match[1],
11555
- address: match[2].toLowerCase()
11556
- };
11557
- }
11558
- function ownerDidMatches(a, b) {
11559
- const aPkh = parsePkhOwnerDid(a);
11560
- const bPkh = parsePkhOwnerDid(b);
11561
- if (aPkh && bPkh) {
11562
- return aPkh.chainId === bPkh.chainId && aPkh.address === bPkh.address;
11563
- }
11564
- return a === b;
11565
- }
11566
- function networkDiscoveryKey(name) {
11567
- if (!NETWORK_NAME_RE.test(name)) {
11568
- throw new NetworkIdError(
11569
- `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
11570
- );
11571
- }
11572
- return `.well-known/encryption/network/${name}`;
11573
- }
11574
11770
  var DEFAULT_ENCRYPTION_ALG = "x25519-aes256gcm/v1";
11575
11771
  var ENVELOPE_VERSION = 1;
11576
11772
  var DEFAULT_KEY_VERSION = 1;
@@ -12406,7 +12602,7 @@ var SERVICE_SHORT_TO_LONG = Object.freeze({
12406
12602
  });
12407
12603
  var ENCRYPTION_PERMISSION_SERVICE2 = "tinycloud.encryption";
12408
12604
  var ENCRYPTION_MANIFEST_SPACE2 = "encryption";
12409
- var SERVICE_LONG_TO_SHORT = Object.freeze(
12605
+ var SERVICE_LONG_TO_SHORT2 = Object.freeze(
12410
12606
  Object.fromEntries(
12411
12607
  Object.entries(SERVICE_SHORT_TO_LONG).map(([s, l]) => [l, s])
12412
12608
  )
@@ -13146,6 +13342,7 @@ async function appendPermissionRequestArtifact(profile, artifact) {
13146
13342
  });
13147
13343
  }
13148
13344
  async function writeSharedRecords(profile, store, entries) {
13345
+ await refuseWriteToDeletedProfile2(profile);
13149
13346
  const path = store === "additional-delegations" ? additionalDelegationsPath(profile) : permissionRequestsPath(profile);
13150
13347
  await writeJsonAtomic(path, entries);
13151
13348
  await writeJsonAtomic(profileStoreMetadataPath(profile, store), { formatVersion: 1 });
@@ -13556,6 +13753,7 @@ async function createSDKInstance(ctx, options) {
13556
13753
  });
13557
13754
  return node;
13558
13755
  }
13756
+ var BOOTSTRAP_PROFILE_FIELDS = ["sessionDid", "spaceId"];
13559
13757
  async function bootstrapDelegatedSession(ctx, delegation) {
13560
13758
  const written = await ProfileManager.withLock(ctx.profile, async () => {
13561
13759
  const previousProfile = await ProfileManager.getProfile(ctx.profile);
@@ -13605,13 +13803,13 @@ async function bootstrapDelegatedSession(ctx, delegation) {
13605
13803
  throw error;
13606
13804
  });
13607
13805
  const session = await ProfileManager.getSession(ctx.profile);
13608
- if (JSON.stringify(profile) !== JSON.stringify(written.profile) || JSON.stringify(session) !== JSON.stringify(written.session)) {
13806
+ if (profile === null || BOOTSTRAP_PROFILE_FIELDS.some((field) => profile[field] !== written.profile[field]) || JSON.stringify(session) !== JSON.stringify(written.session)) {
13609
13807
  return `Profile "${ctx.profile}" changed while the import was pending (another login, logout or profile update), so its newer state was kept and the provisional session was not rolled back.`;
13610
13808
  }
13611
13809
  return restoreBeforeBootstrap(ctx.profile, written.previousProfile);
13612
13810
  }, { timeoutMs: PROFILE_COMMIT_LOCK_TIMEOUT_MS }).catch((error) => (
13613
13811
  // The lock or a read failed: the import's error stays the one reported.
13614
- `Rolling back the provisional session of profile "${ctx.profile}" could not run (${error instanceof Error ? error.message : String(error)}); check \`tc --profile ${ctx.profile} context\`.`
13812
+ `Rolling back the provisional session of profile "${ctx.profile}" could not run (${failureName(error)}); check \`tc --profile ${ctx.profile} context\`.`
13615
13813
  ));
13616
13814
  throw annotate(cause, note);
13617
13815
  };
@@ -13627,15 +13825,27 @@ async function restoreBeforeBootstrap(profileName, previousProfile) {
13627
13825
  const failures = [];
13628
13826
  for (const write of [
13629
13827
  () => ProfileManager.clearSession(profileName),
13630
- () => ProfileManager.setProfile(profileName, previousProfile)
13828
+ () => ProfileManager.updateProfile(profileName, (current) => {
13829
+ const restored = { ...current };
13830
+ for (const field of BOOTSTRAP_PROFILE_FIELDS) {
13831
+ if (previousProfile[field] === void 0) delete restored[field];
13832
+ else restored[field] = previousProfile[field];
13833
+ }
13834
+ return restored;
13835
+ })
13631
13836
  ]) {
13632
13837
  await write().catch((error) => {
13633
- failures.push(error instanceof Error ? error.message : String(error));
13838
+ failures.push(failureName(error));
13634
13839
  });
13635
13840
  }
13636
13841
  if (failures.length === 0) return void 0;
13637
13842
  return `Rolling back the provisional session of profile "${profileName}" failed too (${failures.join("; ")}); check \`tc --profile ${profileName} context\`.`;
13638
13843
  }
13844
+ function failureName(error) {
13845
+ if (error instanceof SyntaxError) return "a profile file is not valid JSON";
13846
+ if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") return error.code;
13847
+ return error instanceof Error ? error.name : "unknown error";
13848
+ }
13639
13849
  function annotate(error, note) {
13640
13850
  if (note === void 0) return error;
13641
13851
  const cause = wrapError(error);
@@ -13926,7 +14136,7 @@ async function authenticatedNode(cmd) {
13926
14136
  }
13927
14137
  function assertOk(result) {
13928
14138
  if (!result.ok) {
13929
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
14139
+ throw cliErrorFromService(result.error);
13930
14140
  }
13931
14141
  }
13932
14142
  async function loadManifestSource(source) {
@@ -15938,7 +16148,7 @@ async function importRequestBoundDelegation(ctx, artifact) {
15938
16148
  ExitCode.ERROR
15939
16149
  );
15940
16150
  case "error":
15941
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
16151
+ throw cliErrorFromService(result.error);
15942
16152
  }
15943
16153
  }
15944
16154
  function isStoredDelegationLike(value) {
@@ -16673,7 +16883,7 @@ function registerDelegationCommand(program) {
16673
16883
  expiry
16674
16884
  });
16675
16885
  if (!result.ok) {
16676
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
16886
+ throw cliErrorFromService(result.error);
16677
16887
  }
16678
16888
  outputJson({
16679
16889
  cid: result.data.cid,
@@ -16693,7 +16903,7 @@ function registerDelegationCommand(program) {
16693
16903
  const node = await ensureAuthenticated(ctx);
16694
16904
  const result = await node.delegationManager.list();
16695
16905
  if (!result.ok) {
16696
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
16906
+ throw cliErrorFromService(result.error);
16697
16907
  }
16698
16908
  let delegations = result.data;
16699
16909
  if (options.granted) {
@@ -16739,7 +16949,7 @@ function registerDelegationCommand(program) {
16739
16949
  const node = await ensureAuthenticated(ctx);
16740
16950
  const result = await node.delegationManager.revoke(cid);
16741
16951
  if (!result.ok) {
16742
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
16952
+ throw cliErrorFromService(result.error);
16743
16953
  }
16744
16954
  outputJson({ cid, revoked: true });
16745
16955
  } catch (error) {
@@ -16864,7 +17074,6 @@ function registerDoctorCommand(program) {
16864
17074
  init_profiles();
16865
17075
  init_formatter();
16866
17076
  init_errors();
16867
- init_constants();
16868
17077
  import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
16869
17078
  import { resolve } from "path";
16870
17079
  init_theme();
@@ -16881,7 +17090,7 @@ function registerDuckdbCommand(program) {
16881
17090
  () => node.duckdb.db(options.db).query(sqlStr, params)
16882
17091
  );
16883
17092
  if (!result.ok) {
16884
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
17093
+ throw cliErrorFromService(result.error);
16885
17094
  }
16886
17095
  const { columns, rows, rowCount } = result.data;
16887
17096
  if (shouldOutputJson()) {
@@ -16913,7 +17122,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
16913
17122
  () => node.duckdb.db(options.db).execute(sqlStr, params)
16914
17123
  );
16915
17124
  if (!result.ok) {
16916
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
17125
+ throw cliErrorFromService(result.error);
16917
17126
  }
16918
17127
  outputJson({ changes: result.data.changes });
16919
17128
  } catch (error) {
@@ -16930,7 +17139,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
16930
17139
  () => node.duckdb.db(options.db).describe()
16931
17140
  );
16932
17141
  if (!result.ok) {
16933
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
17142
+ throw cliErrorFromService(result.error);
16934
17143
  }
16935
17144
  const schema = result.data;
16936
17145
  if (shouldOutputJson()) {
@@ -16975,7 +17184,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
16975
17184
  () => node.duckdb.db(options.db).export()
16976
17185
  );
16977
17186
  if (!result.ok) {
16978
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
17187
+ throw cliErrorFromService(result.error);
16979
17188
  }
16980
17189
  const blob = result.data;
16981
17190
  const buffer = Buffer.from(await blob.arrayBuffer());
@@ -17002,7 +17211,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
17002
17211
  () => node.duckdb.db(options.db).import(bytes)
17003
17212
  );
17004
17213
  if (!result.ok) {
17005
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
17214
+ throw cliErrorFromService(result.error);
17006
17215
  }
17007
17216
  outputJson({
17008
17217
  file: filePath,
@@ -17139,7 +17348,7 @@ init_theme();
17139
17348
  async function throwKvError(error, spaceUri, profileName) {
17140
17349
  const hosted = await unhostedSpaceError(error, spaceUri, profileName);
17141
17350
  if (hosted) throw hosted;
17142
- throw new CLIError(error.code, error.message, ExitCode.ERROR);
17351
+ throw cliErrorFromService(error);
17143
17352
  }
17144
17353
  function isByteCount(value) {
17145
17354
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
@@ -18656,7 +18865,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
18656
18865
  operation: () => secrets2.list(scopeOptions)
18657
18866
  });
18658
18867
  if (!result.ok) {
18659
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
18868
+ throw cliErrorFromService(result.error);
18660
18869
  }
18661
18870
  const secretNames = Array.isArray(result.data) ? result.data : [];
18662
18871
  outputJson({
@@ -18773,7 +18982,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
18773
18982
  operation: () => secrets2.put(name, secretValue, scopeOptions)
18774
18983
  });
18775
18984
  if (!result.ok) {
18776
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
18985
+ throw cliErrorFromService(result.error);
18777
18986
  }
18778
18987
  outputJson({ name, written: true });
18779
18988
  } catch (error) {
@@ -18799,7 +19008,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
18799
19008
  operation: () => secrets2.delete(name, scopeOptions)
18800
19009
  });
18801
19010
  if (!result.ok) {
18802
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
19011
+ throw cliErrorFromService(result.error);
18803
19012
  }
18804
19013
  outputJson({ name, deleted: true });
18805
19014
  } catch (error) {
@@ -27482,7 +27691,6 @@ async function notifyShare(input) {
27482
27691
  };
27483
27692
  }
27484
27693
  let attempts = 0;
27485
- let lastError;
27486
27694
  while (attempts < attemptsLimit) {
27487
27695
  if (input.signal?.aborted) throw new ShareNotifyError("share delivery was cancelled");
27488
27696
  attempts += 1;
@@ -27509,10 +27717,19 @@ async function notifyShare(input) {
27509
27717
  reason: "delivery-window-expired"
27510
27718
  };
27511
27719
  }
27512
- lastError = error;
27720
+ if (error !== null && typeof error === "object" && "status" in error && (error.status === 401 || error.status === 403)) {
27721
+ return {
27722
+ protocol: "tinycloud-share",
27723
+ version: 1,
27724
+ shareId: input.shareId,
27725
+ state: "partial-failure",
27726
+ idempotencyKey,
27727
+ attempts,
27728
+ retryable: false
27729
+ };
27730
+ }
27513
27731
  }
27514
27732
  }
27515
- void lastError;
27516
27733
  return { protocol: "tinycloud-share", version: 1, shareId: input.shareId, state: "partial-failure", idempotencyKey, attempts, retryable: true };
27517
27734
  }
27518
27735
  async function defaultIdempotencyKey(shareId, recipient) {
@@ -29514,7 +29731,7 @@ function registerSpaceCommand(program) {
29514
29731
  const node = await ensureAuthenticated(ctx);
29515
29732
  const result = await node.spaces.list();
29516
29733
  if (!result.ok) {
29517
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
29734
+ throw cliErrorFromService(result.error);
29518
29735
  }
29519
29736
  if (shouldOutputJson()) {
29520
29737
  outputJson({ spaces: result.data, count: result.data.length });
@@ -29659,7 +29876,7 @@ async function throwSqlError(error, spaceUri, profileName, prefix) {
29659
29876
  const hosted = await unhostedSpaceError(error, spaceUri, profileName);
29660
29877
  if (hosted) throw hosted;
29661
29878
  const message = prefix ? `${prefix}${error.message}` : error.message;
29662
- throw new CLIError(error.code, message, ExitCode.ERROR, error.meta);
29879
+ throw cliErrorFromService(error, message);
29663
29880
  }
29664
29881
  function registerSqlCommand(program) {
29665
29882
  const sql = program.command("sql").description("SQLite database operations for your TinyCloud space").addHelpText("after", `
@@ -30371,7 +30588,7 @@ async function unlockVault(node, privateKey) {
30371
30588
  const signer = new PrivateKeySigner2(privateKey);
30372
30589
  const result = await node.vault.unlock(signer);
30373
30590
  if (result && !result.ok) {
30374
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
30591
+ throw cliErrorFromService(result.error);
30375
30592
  }
30376
30593
  }
30377
30594
  function registerVaultCommand(program) {
@@ -30412,7 +30629,7 @@ function registerVaultCommand(program) {
30412
30629
  }
30413
30630
  const result = await withSpinner(`Writing ${key}...`, () => node.vault.put(key, putValue));
30414
30631
  if (!result.ok) {
30415
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
30632
+ throw cliErrorFromService(result.error);
30416
30633
  }
30417
30634
  outputJson({ key, written: true });
30418
30635
  } catch (error) {
@@ -30431,7 +30648,7 @@ function registerVaultCommand(program) {
30431
30648
  if (result.error.code === "NOT_FOUND") {
30432
30649
  throw new CLIError("NOT_FOUND", `Key "${key}" not found`, ExitCode.NOT_FOUND);
30433
30650
  }
30434
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
30651
+ throw cliErrorFromService(result.error);
30435
30652
  }
30436
30653
  const data = result.data.data ?? result.data;
30437
30654
  if (options.output) {
@@ -30462,7 +30679,7 @@ function registerVaultCommand(program) {
30462
30679
  await withSpinner("Unlocking vault...", () => unlockVault(node, privateKey));
30463
30680
  const result = await withSpinner(`Deleting ${key}...`, () => node.vault.delete(key));
30464
30681
  if (!result.ok) {
30465
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
30682
+ throw cliErrorFromService(result.error);
30466
30683
  }
30467
30684
  outputJson({ key, deleted: true });
30468
30685
  } catch (error) {
@@ -30479,7 +30696,7 @@ function registerVaultCommand(program) {
30479
30696
  const listOptions = options.prefix ? { prefix: options.prefix } : void 0;
30480
30697
  const result = await withSpinner("Listing vault keys...", () => node.vault.list(listOptions));
30481
30698
  if (!result.ok) {
30482
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
30699
+ throw cliErrorFromService(result.error);
30483
30700
  }
30484
30701
  const keys = result.data.data ?? result.data;
30485
30702
  const keyList = Array.isArray(keys) ? keys : [];
@@ -30505,7 +30722,7 @@ function registerVaultCommand(program) {
30505
30722
  outputJson({ key, exists: false, metadata: {} });
30506
30723
  return;
30507
30724
  }
30508
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
30725
+ throw cliErrorFromService(result.error);
30509
30726
  }
30510
30727
  outputJson({
30511
30728
  key,
@@ -30555,7 +30772,7 @@ function registerVarsCommand(program) {
30555
30772
  const prefixedKv = node.kv.withPrefix(VARIABLES_PREFIX);
30556
30773
  const result = await withSpinner("Listing variables...", () => prefixedKv.list());
30557
30774
  if (!result.ok) {
30558
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
30775
+ throw cliErrorFromService(result.error);
30559
30776
  }
30560
30777
  const rawData = result.data.data ?? result.data;
30561
30778
  const keyList = Array.isArray(rawData) ? rawData : rawData?.keys ?? [];
@@ -30579,7 +30796,7 @@ function registerVarsCommand(program) {
30579
30796
  if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
30580
30797
  throw new CLIError("NOT_FOUND", `Variable "${name}" not found`, ExitCode.NOT_FOUND);
30581
30798
  }
30582
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
30799
+ throw cliErrorFromService(result.error);
30583
30800
  }
30584
30801
  const data = result.data.data;
30585
30802
  let value;
@@ -30637,7 +30854,7 @@ function registerVarsCommand(program) {
30637
30854
  const prefixedKv = node.kv.withPrefix(VARIABLES_PREFIX);
30638
30855
  const result = await withSpinner(`Setting variable ${name}...`, () => prefixedKv.put(name, payload));
30639
30856
  if (!result.ok) {
30640
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
30857
+ throw cliErrorFromService(result.error);
30641
30858
  }
30642
30859
  outputJson({ name, written: true });
30643
30860
  } catch (error) {
@@ -30653,7 +30870,7 @@ function registerVarsCommand(program) {
30653
30870
  const prefixedKv = node.kv.withPrefix(VARIABLES_PREFIX);
30654
30871
  const result = await withSpinner(`Deleting variable ${name}...`, () => prefixedKv.delete(name));
30655
30872
  if (!result.ok) {
30656
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
30873
+ throw cliErrorFromService(result.error);
30657
30874
  }
30658
30875
  outputJson({ name, deleted: true });
30659
30876
  } catch (error) {