@tinycloud/cli 1.3.0-beta.42 → 1.3.0-beta.44

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.
@@ -610,6 +610,7 @@ import { join as join2 } from "path";
610
610
  import { ProfileDeletedError, ProfileLockTimeoutError, ProfileSymlinkError } from "@tinycloud/operations/state";
611
611
  import { isSafeRecovery } from "@tinycloud/operations/recovery";
612
612
  import { CLI_NAME as CLI_NAME2 } from "@tinycloud/operations/cli-name";
613
+ import { isRevokedGrantRefusal, REVOKED_GRANT_MESSAGE } from "@tinycloud/operations/revoked-grant";
613
614
  import {
614
615
  buildSecretRequestUrl,
615
616
  isSafeRequestAccessRecovery,
@@ -637,6 +638,9 @@ function cliErrorFromService(error, message = error.message) {
637
638
  const missingCapability = validatedCapabilityOf({ ...error, meta }) !== void 0;
638
639
  const denied = verdict === "forbidden" || verdict === "unauthenticated" && missingCapability || verdict === void 0 && error.code === "PERMISSION_DENIED";
639
640
  const unauthenticated = !denied && (verdict === "unauthenticated" || verdict === void 0 && error.code === "AUTH_REQUIRED");
641
+ if (unauthenticated && isRevokedGrantRefusal({ ...error, meta, message })) {
642
+ return new CLIError("GRANT_REVOKED", REVOKED_GRANT_MESSAGE, GRANT_REVOKED_EXIT_CODE, { status: 401 });
643
+ }
640
644
  return new CLIError(
641
645
  denied ? "PERMISSION_DENIED" : unauthenticated ? "AUTH_REQUIRED" : error.code,
642
646
  message,
@@ -862,7 +866,7 @@ function buildNetworkHint() {
862
866
  lines.push(`Or override per-command with --host or TC_HOST.`);
863
867
  return lines.join("\n");
864
868
  }
865
- var activeProfileName, CLIError, SETUP_KEYS;
869
+ var activeProfileName, GRANT_REVOKED_EXIT_CODE, CLIError, SETUP_KEYS;
866
870
  var init_errors = __esm({
867
871
  "src/output/errors.ts"() {
868
872
  "use strict";
@@ -871,6 +875,7 @@ var init_errors = __esm({
871
875
  init_formatter();
872
876
  init_storage2();
873
877
  init_warnings();
878
+ GRANT_REVOKED_EXIT_CODE = ExitCode.PERMISSION_DENIED;
874
879
  CLIError = class extends Error {
875
880
  constructor(code4, message, exitCode = ExitCode.ERROR, metadata) {
876
881
  super(message);
@@ -12360,7 +12365,75 @@ import { CLI_NAME as CLI_NAME13 } from "@tinycloud/operations/cli-name";
12360
12365
  import { parseSignedCompactUcanAttenuation as parseSignedCompactUcanAttenuation2 } from "@tinycloud/sdk-core";
12361
12366
  import { inspectStoredDelegation, withoutBearer as withoutBearer2 } from "@tinycloud/operations/delegation-binding";
12362
12367
  init_permissions();
12368
+
12369
+ // src/lib/revoke-node-record.ts
12370
+ var PAGE_LIMIT = 100;
12371
+ var MAX_PAGES = 50;
12372
+ var MAX_RECORDS = PAGE_LIMIT * MAX_PAGES;
12373
+ var MAX_CHAIN_DEPTH = 8;
12363
12374
  function normalizeDid(input) {
12375
+ const fragment = input.indexOf("#");
12376
+ return (fragment < 0 ? input : input.slice(0, fragment)).trim().toLowerCase();
12377
+ }
12378
+ var SPACE = /^tinycloud:pkh:eip155:([1-9]\d{0,17}):(0x[0-9a-fA-F]{40}):[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
12379
+ function ownerOfSpace(space) {
12380
+ const match = SPACE.exec(space);
12381
+ return match === null ? void 0 : `did:pkh:eip155:${match[1]}:${match[2].toLowerCase()}`;
12382
+ }
12383
+ function ownerSpaceFromNodeRecords(records, cid, ownerDid, sessionDid) {
12384
+ if (records.length > MAX_RECORDS) return void 0;
12385
+ const matches = records.filter((record3) => record3.cid === cid);
12386
+ const target = matches[0];
12387
+ if (!target || new Set(matches.map(({ delegatorDid, delegateDid, resources }) => JSON.stringify([delegatorDid, delegateDid, resources]))).size !== 1) return void 0;
12388
+ const owner = normalizeDid(ownerDid);
12389
+ if (target.resources.length === 0) return void 0;
12390
+ const spaces = /* @__PURE__ */ new Set();
12391
+ for (const { resource } of target.resources) {
12392
+ const space = /^(tinycloud:[^/]+)\//.exec(resource)?.[1];
12393
+ if (!space || ownerOfSpace(space) !== owner) return void 0;
12394
+ spaces.add(space);
12395
+ }
12396
+ if (spaces.size !== 1) return void 0;
12397
+ const session = sessionDid === void 0 ? void 0 : normalizeDid(sessionDid);
12398
+ const byCid = /* @__PURE__ */ new Map();
12399
+ for (const record3 of records) {
12400
+ const sameCid = byCid.get(record3.cid);
12401
+ if (sameCid === void 0) byCid.set(record3.cid, [record3]);
12402
+ else sameCid.push(record3);
12403
+ }
12404
+ const verdicts = /* @__PURE__ */ new Map();
12405
+ const rooted = (record3, depth) => {
12406
+ const issuer = normalizeDid(record3.delegatorDid);
12407
+ if (issuer === owner || issuer === session) return true;
12408
+ const known = verdicts.get(record3.cid);
12409
+ if (known !== void 0) return known;
12410
+ verdicts.set(record3.cid, false);
12411
+ const verdict = depth < MAX_CHAIN_DEPTH && record3.parents.some((parentCid) => (byCid.get(parentCid) ?? []).some((parent) => normalizeDid(parent.delegateDid) === issuer && rooted(parent, depth + 1)));
12412
+ verdicts.set(record3.cid, verdict);
12413
+ return verdict;
12414
+ };
12415
+ return rooted(target, 0) ? [...spaces][0] : void 0;
12416
+ }
12417
+ async function lookupOwnerSpaceOnNode(node, cid, ownerDid) {
12418
+ const records = [];
12419
+ let cursor;
12420
+ for (let page = 0; page < MAX_PAGES; page += 1) {
12421
+ const query = await node.delegationManager.query({
12422
+ direction: "all",
12423
+ limit: PAGE_LIMIT,
12424
+ ...cursor === void 0 ? {} : { cursor }
12425
+ });
12426
+ if (!query.ok) return void 0;
12427
+ if (query.data.items.length > PAGE_LIMIT || records.length + query.data.items.length > MAX_RECORDS) return void 0;
12428
+ for (const item of query.data.items) records.push(item);
12429
+ cursor = query.data.nextCursor;
12430
+ if (cursor === void 0) return ownerSpaceFromNodeRecords(records, cid, ownerDid, node.sessionDid);
12431
+ }
12432
+ return void 0;
12433
+ }
12434
+
12435
+ // src/lib/revoke-delegation.ts
12436
+ function normalizeDid2(input) {
12364
12437
  const trimmed = input.trim();
12365
12438
  const fragment = trimmed.indexOf("#");
12366
12439
  return (fragment < 0 ? trimmed : trimmed.slice(0, fragment)).toLowerCase();
@@ -12379,7 +12452,7 @@ async function cidBoundOwnerSpace(record3, ownerDid, node, host) {
12379
12452
  const signed = parseSignedCompactUcanAttenuation2(withoutBearer2(authorization), delegation.cid);
12380
12453
  for (const resource of Object.keys(signed.payload.att)) {
12381
12454
  const space = /^(tinycloud:[^/]+)\/[^/]+(?:\/.*)?$/.exec(resource)?.[1];
12382
- if (space && ownerDidFromSpace(space) && normalizeDid(ownerDidFromSpace(space)) === normalizeDid(ownerDid)) {
12455
+ if (space && ownerDidFromSpace(space) && normalizeDid2(ownerDidFromSpace(space)) === normalizeDid2(ownerDid)) {
12383
12456
  return space;
12384
12457
  }
12385
12458
  }
@@ -12393,7 +12466,7 @@ async function cidBoundOwnerSpace(record3, ownerDid, node, host) {
12393
12466
  if (inspected.local !== "verified" || inspected.cid !== delegation.cid) return void 0;
12394
12467
  for (const permission of inspected.effectivePermissions) {
12395
12468
  const space = permission.space;
12396
- if (space && ownerDidFromSpace(space) && normalizeDid(ownerDidFromSpace(space)) === normalizeDid(ownerDid)) {
12469
+ if (space && ownerDidFromSpace(space) && normalizeDid2(ownerDidFromSpace(space)) === normalizeDid2(ownerDid)) {
12397
12470
  return space;
12398
12471
  }
12399
12472
  }
@@ -12461,7 +12534,11 @@ async function revokeDelegationOnNode(params) {
12461
12534
  host: ctx.host
12462
12535
  });
12463
12536
  if (!boundSpace) {
12464
- throw new CLIError("TARGET_NOT_FOUND", `Delegation "${cid}" was not found by the node or local grant sources.`, ExitCode.NOT_FOUND);
12537
+ throw new CLIError(
12538
+ "TARGET_NOT_FOUND",
12539
+ `This profile cannot list the owner's delegations on the node, so it cannot look up "${cid}", and it has no local record of that grant. Use the profile that created the grant, or revoke it from the Delegations screen of the account site (https://account.tinycloud.xyz).`,
12540
+ ExitCode.NOT_FOUND
12541
+ );
12465
12542
  }
12466
12543
  targetSpaceSource = boundSpace.source;
12467
12544
  } else {
@@ -12492,24 +12569,29 @@ async function revokeDelegationOnNode(params) {
12492
12569
  if (error === null || typeof error !== "object" || !("code" in error) || error.code !== "RAW_RECAP_RESOURCE_UNSUPPORTED") {
12493
12570
  throw error;
12494
12571
  }
12495
- const fallback = boundSpace ?? await resolveCidBoundTargetSpace({
12572
+ const ownerDid = profile.ownerDid ?? ownerDidFromSpace(node.accountSpaceId) ?? ownerDidFromSpace(profile.spaceId);
12573
+ let fallback = boundSpace ?? await resolveCidBoundTargetSpace({
12496
12574
  profileName: ctx.profile,
12497
- ownerDid: profile.ownerDid ?? ownerDidFromSpace(node.accountSpaceId) ?? ownerDidFromSpace(profile.spaceId),
12575
+ ownerDid,
12498
12576
  cid,
12499
12577
  node,
12500
12578
  host: ctx.host,
12501
12579
  localTarget: params.localTarget
12502
12580
  });
12581
+ if (!fallback && ownerDid) {
12582
+ const space = await lookupOwnerSpaceOnNode(node, cid, ownerDid);
12583
+ if (space) fallback = { space, source: "node-delegation-record" };
12584
+ }
12503
12585
  if (!fallback) {
12504
12586
  throw new CLIError(
12505
12587
  "RAW_RECAP_RESOURCE_UNSUPPORTED",
12506
- `Cannot safely revoke "${cid}": raw CID ReCap resources are unavailable and no matching owner-signed local grant binds its space to the CID.`,
12588
+ `Cannot revoke "${cid}": this CLI profile has no record of this grant and the node's record does not match the signed-in owner.`,
12507
12589
  ExitCode.PERMISSION_DENIED
12508
12590
  );
12509
12591
  }
12510
12592
  fallbackSpaceId = fallback.space;
12511
12593
  authorityScopeSource = fallback.source;
12512
- authorityScopeReason = fallback.source === "local-signed-grant-artifact" ? "Raw-CID revoke authority could not be issued; the fallback space came from an owner-signed local grant whose Authorization recomputes to this CID." : "Raw-CID revoke authority could not be issued; the fallback space came from a locally verified owner-signed stored grant bound to this CID.";
12594
+ authorityScopeReason = fallback.source === "local-signed-grant-artifact" ? "Raw-CID revoke authority could not be issued; the fallback space came from an owner-signed local grant whose Authorization recomputes to this CID." : fallback.source === "node-delegation-record" ? "Raw-CID revoke authority could not be issued; the fallback space came from the node's delegation record, whose issuer chains to the signed-in owner and whose space the owner holds." : "Raw-CID revoke authority could not be issued; the fallback space came from a locally verified owner-signed stored grant bound to this CID.";
12513
12595
  targetSpaceSource = fallback.source;
12514
12596
  const acquired = await ensureDelegationAuthority({
12515
12597
  ctx,
@@ -12522,7 +12604,7 @@ async function revokeDelegationOnNode(params) {
12522
12604
  actions: ["tinycloud.delegation/revoke"]
12523
12605
  }],
12524
12606
  expiryOption: void 0,
12525
- reason: `Revoke delegation ${cid} using its CID-bound owner-signed grant ${fallback.source === "local-stored-grant" ? "record" : "artifact"}`,
12607
+ reason: fallback.source === "node-delegation-record" ? `Revoke delegation ${cid} using the node's record of the owner's grant` : `Revoke delegation ${cid} using its CID-bound owner-signed grant ${fallback.source === "local-stored-grant" ? "record" : "artifact"}`,
12526
12608
  yes: params.yes,
12527
12609
  consentAlreadyGiven: true,
12528
12610
  persist: false,
@@ -17059,7 +17141,7 @@ var EXIT_BY_CODE = {
17059
17141
  [ReplicaErrorCode.GRANT_NOT_COVERING]: ExitCode.AUTH_REQUIRED,
17060
17142
  [ReplicaErrorCode.GRANT_NOT_YET_VALID]: ExitCode.PERMISSION_DENIED,
17061
17143
  [ReplicaErrorCode.GRANT_EXPIRED]: ExitCode.PERMISSION_DENIED,
17062
- [ReplicaErrorCode.GRANT_REVOKED]: ExitCode.PERMISSION_DENIED,
17144
+ [ReplicaErrorCode.GRANT_REVOKED]: GRANT_REVOKED_EXIT_CODE,
17063
17145
  [ReplicaErrorCode.CONSENT_EXPIRED]: ExitCode.PERMISSION_DENIED,
17064
17146
  [ReplicaErrorCode.GATED]: ExitCode.PERMISSION_DENIED,
17065
17147
  [ReplicaErrorCode.GRANT_UNAUTHORIZED]: ExitCode.PERMISSION_DENIED,
@@ -17844,7 +17926,7 @@ import {
17844
17926
  canonicalSecretRequestOwner,
17845
17927
  isValidSecretRequestScope,
17846
17928
  missingSecretRecovery,
17847
- ownerOfSpace,
17929
+ ownerOfSpace as ownerOfSpace2,
17848
17930
  resolveSecretsOwner as resolveSecretsOwner2,
17849
17931
  SecretRequestInputError,
17850
17932
  validSecretNames
@@ -18858,7 +18940,7 @@ async function readDelegatedSecretValue(params) {
18858
18940
  if (!envelopeResult.ok) {
18859
18941
  if (envelopeResult.error.code === "NOT_FOUND" || envelopeResult.error.code === "KEY_NOT_FOUND" || envelopeResult.error.code === "KV_NOT_FOUND") {
18860
18942
  const recovery = missingSecretRecovery({
18861
- owner: ownerOfSpace(params.space?.startsWith("tinycloud:") ? params.space : params.delegation.spaceId),
18943
+ owner: ownerOfSpace2(params.space?.startsWith("tinycloud:") ? params.space : params.delegation.spaceId),
18862
18944
  local: params.local,
18863
18945
  names: [params.name],
18864
18946
  ...params.scope === void 0 ? {} : { scope: params.scope }
@@ -37596,6 +37678,25 @@ var ShareHistoryRetryError = class extends Error {
37596
37678
  }
37597
37679
  code = "SHARE_HISTORY_RETRY";
37598
37680
  };
37681
+ var ShareHistoryUnreadableError = class extends Error {
37682
+ constructor() {
37683
+ super("share history is unavailable");
37684
+ this.name = "ShareHistoryUnreadableError";
37685
+ }
37686
+ };
37687
+ var ShareHistoryReadError = class extends Error {
37688
+ constructor() {
37689
+ super("share history could not be read");
37690
+ this.name = "ShareHistoryReadError";
37691
+ }
37692
+ };
37693
+ var HISTORY_WRITE_ERROR_CODES = { EACCES: true, EPERM: true, EROFS: true, ENOSPC: true, EDQUOT: true, EIO: true };
37694
+ function shareHistoryReason(error) {
37695
+ if (error instanceof ShareHistoryUnreadableError) return "HISTORY_UNREADABLE";
37696
+ if (error instanceof ShareHistoryReadError) return "HISTORY_READ_FAILED";
37697
+ const code4 = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
37698
+ return typeof code4 === "string" && HISTORY_WRITE_ERROR_CODES[code4] === true ? "HISTORY_WRITE_FAILED" : "HISTORY_UNAVAILABLE";
37699
+ }
37599
37700
 
37600
37701
  // src/commands/share.ts
37601
37702
  var SHARE_ORIGIN = "https://share.tinycloud.xyz";
@@ -37774,16 +37875,18 @@ function expires(value) {
37774
37875
  }
37775
37876
  async function rememberPublishedShare(result) {
37776
37877
  const record3 = historyRecordForPublishedShare(result);
37777
- if (shareServices.records === void 0) return { record: record3, recorded: false };
37778
37878
  try {
37879
+ if (shareServices.records === void 0) throw new Error("sender history is not configured");
37779
37880
  await shareServices.records.put(record3);
37780
- return { record: record3, recorded: true };
37781
- } catch {
37782
- process.stderr.write("Warning: share was published but not recorded in sender history; future share list, share notify, and share revoke by ID will not find it.\n");
37783
- return { record: record3, recorded: false };
37881
+ return { record: record3, history: { recorded: true } };
37882
+ } catch (error) {
37883
+ const reason = shareHistoryReason(error);
37884
+ process.stderr.write(`Warning: share was published but not recorded in sender history (${reason}); future share list, share notify, and share revoke by ID will not find it. Keep share ID ${result.metadata.shareId} and CID ${result.link.cid}.
37885
+ `);
37886
+ return { record: record3, history: { recorded: false, reason } };
37784
37887
  }
37785
37888
  }
37786
- async function notifyRecordedShare(record3, recipient, adapter, storage) {
37889
+ async function notifyRecordedShare(record3, recipient, adapter, storage, published) {
37787
37890
  const result = await notifyShare({ shareId: record3.shareId, recipient, record: record3, adapter, checkDeliveryWindow: true });
37788
37891
  if (result.state !== "partial-failure" && storage !== void 0) {
37789
37892
  const mailbox = canonicalMailbox2(recipient).email;
@@ -37795,8 +37898,11 @@ async function notifyRecordedShare(record3, recipient, adapter, storage) {
37795
37898
  deliveredRecipients: current.deliveredRecipients?.includes(mailbox) ? current.deliveredRecipients : [...current.deliveredRecipients ?? [], mailbox]
37796
37899
  }));
37797
37900
  if (updated === void 0) throw new Error("sender history record was removed");
37798
- } catch {
37799
- process.stderr.write("Warning: delivery succeeded, but could not record its confirmation in sender history.\n");
37901
+ } catch (error) {
37902
+ const reason = shareHistoryReason(error);
37903
+ const identifiers = published === void 0 ? "" : ` Keep share ID ${published.shareId} and CID ${published.cid}.`;
37904
+ process.stderr.write(`Warning: delivery succeeded, but could not record its confirmation in sender history (${reason}).${identifiers}
37905
+ `);
37800
37906
  }
37801
37907
  }
37802
37908
  }
@@ -37885,17 +37991,17 @@ function registerShareCommand(program) {
37885
37991
  }
37886
37992
  throw new CLIError(result.method === "openkey-device" ? "DEVICE_AUTH_REQUIRED" : "CLAIM_REQUIRED", "recipient authorization is required; continue through the configured authority adapter", 6);
37887
37993
  }
37888
- const { record: record3, recorded } = await rememberPublishedShare(result);
37994
+ const { record: record3, history } = await rememberPublishedShare(result);
37889
37995
  let notification;
37890
37996
  if (notifyRecipient !== void 0) {
37891
37997
  if (shareServices.delivery === void 0) throw new CLIError("AUTH_REQUIRED", "delivery authority is not configured", 3);
37892
- notification = await notifyRecordedShare(record3, notifyRecipient, shareServices.delivery, recorded ? shareServices.records : void 0);
37998
+ notification = await notifyRecordedShare(record3, notifyRecipient, shareServices.delivery, history.recorded ? shareServices.records : void 0, { shareId: result.metadata.shareId, cid: result.link.cid });
37893
37999
  if (notification.state === "partial-failure") process.exitCode = 9;
37894
38000
  if (notification.reason === "delivery-window-expired") process.stderr.write(NOTIFY_WINDOW_MESSAGE);
37895
38001
  }
37896
38002
  if (result.metadata.expiryClamped === true) process.stderr.write(`Share expiry clamped to session expiry (${result.metadata.expiresAt}).
37897
38003
  `);
37898
- if (json) writeJson2({ ...redactPublishedShare(result), expiryClamped: result.metadata.expiryClamped === true, ...artifact === void 0 || result.metadata.artifact === void 0 ? {} : { artifactId: result.metadata.artifact.id, version: result.metadata.artifact.version, mode: result.metadata.artifact.mode }, ...notification === void 0 ? {} : { notification } });
38004
+ if (json) writeJson2({ ...redactPublishedShare(result), history, expiryClamped: result.metadata.expiryClamped === true, ...artifact === void 0 || result.metadata.artifact === void 0 ? {} : { artifactId: result.metadata.artifact.id, version: result.metadata.artifact.version, mode: result.metadata.artifact.mode }, ...notification === void 0 ? {} : { notification } });
37899
38005
  else {
37900
38006
  if (result.metadata.artifact !== void 0) process.stderr.write(`Artifact ${result.metadata.artifact.id} v${result.metadata.artifact.version} (${result.metadata.artifact.mode})
37901
38007
  `);