@tinycloud/cli 1.1.0-beta.17 → 1.1.0-beta.19

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.
@@ -13804,6 +13804,9 @@ function permissionRequestsPath(profile) {
13804
13804
  function grantHistoryPath(profile) {
13805
13805
  return join4(PROFILES_DIR, profile, "auth-grants.jsonl");
13806
13806
  }
13807
+ function localGrantArtifactsPath(profile) {
13808
+ return join4(PROFILES_DIR, profile, "auth-grant-artifacts.json");
13809
+ }
13807
13810
  function createPermissionRequestArtifact(params) {
13808
13811
  return buildPermissionRequestArtifact({
13809
13812
  profile: params.profileName,
@@ -13828,6 +13831,32 @@ function didWithoutFragment(did) {
13828
13831
  async function loadAdditionalDelegations(profile) {
13829
13832
  return readAdditionalDelegations(profile);
13830
13833
  }
13834
+ async function loadLocalGrantArtifacts(profile) {
13835
+ const path = localGrantArtifactsPath(profile);
13836
+ if (!await fileExists(path)) return [];
13837
+ let parsed;
13838
+ try {
13839
+ parsed = JSON.parse(await readFile2(path, "utf8"));
13840
+ } catch {
13841
+ return [];
13842
+ }
13843
+ if (!Array.isArray(parsed)) return [];
13844
+ return parsed.filter(
13845
+ (value) => value !== null && typeof value === "object" && typeof value.delegationCid === "string" && value.delegation !== null && typeof value.delegation === "object" && value.delegation.cid === value.delegationCid && typeof value.delegation.delegationHeader?.Authorization === "string"
13846
+ );
13847
+ }
13848
+ async function appendLocalGrantArtifact(profile, delegation) {
13849
+ await ProfileManager.ensureProfileDir(profile);
13850
+ await withProfileLock2(profile, async () => {
13851
+ await refuseWriteToDeletedProfile2(profile);
13852
+ const records = await loadLocalGrantArtifacts(profile);
13853
+ if (records.some((record) => record.delegationCid === delegation.cid)) return;
13854
+ await writeJsonAtomic(localGrantArtifactsPath(profile), [
13855
+ ...records,
13856
+ { delegationCid: delegation.cid, delegation }
13857
+ ]);
13858
+ });
13859
+ }
13831
13860
  async function appendAdditionalDelegations(profile, entries) {
13832
13861
  const { mergeDelegationsWithoutRequest } = await import("@tinycloud/operations/delegation-binding");
13833
13862
  await updateProfileStore(profile, "additional-delegations", (stored) => ({
@@ -15246,11 +15275,15 @@ function ownerSpaceId(space, ownerDid) {
15246
15275
  function permissionTuples(permissions, ownerDid) {
15247
15276
  return new Set(permissions.flatMap((permission) => actionTuples(permission, ownerDid)));
15248
15277
  }
15278
+ function isRawCidRevocationPermission(permission) {
15279
+ return permission.service === "tinycloud.delegation" && permission.space?.startsWith("urn:cid:") === true && permission.path === "" && permission.actions.includes("tinycloud.delegation/revoke");
15280
+ }
15249
15281
  function actionTuples(permission, ownerDid) {
15250
15282
  const service = permission.service.startsWith("tinycloud.") ? permission.service : `tinycloud.${permission.service}`;
15251
- const raw = isVerifiedRawEncryptionPermission(permission);
15252
- const space = raw ? ENCRYPTION_MANIFEST_SPACE2 : normalizePkhIdentifier(ownerSpaceId(permission.space ?? "", ownerDid));
15253
- const path = raw ? normalizePkhIdentifier(permission.path) : permission.path;
15283
+ const rawEncryption = isVerifiedRawEncryptionPermission(permission);
15284
+ const rawCidRevocation = isRawCidRevocationPermission(permission);
15285
+ const space = rawEncryption ? ENCRYPTION_MANIFEST_SPACE2 : rawCidRevocation ? permission.space : normalizePkhIdentifier(ownerSpaceId(permission.space ?? "", ownerDid));
15286
+ const path = rawEncryption ? normalizePkhIdentifier(permission.path) : permission.path;
15254
15287
  return permission.actions.map((action) => JSON.stringify([service, space, path, action.includes("/") ? action : `${service}/${action}`]));
15255
15288
  }
15256
15289
  function isLegacyNestedDecrypt(permission, requested, ownerDid, spaceId) {
@@ -15321,7 +15354,7 @@ function declinedPermissions(requested, signed, ownerDid) {
15321
15354
  const missing = [...permissionTuples(requested, ownerDid)].filter((tuple) => !granted.has(tuple));
15322
15355
  const requestedScopes = /* @__PURE__ */ new Map();
15323
15356
  for (const entry of requested) {
15324
- const space = isVerifiedRawEncryptionPermission(entry) ? entry.space ?? ENCRYPTION_MANIFEST_SPACE2 : ownerSpaceId(entry.space ?? "", ownerDid);
15357
+ const space = isVerifiedRawEncryptionPermission(entry) ? entry.space ?? ENCRYPTION_MANIFEST_SPACE2 : isRawCidRevocationPermission(entry) ? entry.space : ownerSpaceId(entry.space ?? "", ownerDid);
15325
15358
  for (const tuple of actionTuples(entry, ownerDid)) {
15326
15359
  requestedScopes.set(tuple, { space, path: entry.path });
15327
15360
  }
@@ -15344,7 +15377,10 @@ function scopedLoginPermissions(permissions) {
15344
15377
  }
15345
15378
  function grantRequestPermissions(group, anchorSpace) {
15346
15379
  const request = group.map((p) => isRawEncryptionPermission(p) ? { ...p, space: ENCRYPTION_MANIFEST_SPACE2, path: canonicalNetworkUrn(p.path) } : p);
15347
- return withCapabilitiesRead(request, request.find((p) => !isRawEncryptionPermission(p))?.space ?? anchorSpace);
15380
+ const capabilitySpace = request.find(
15381
+ (permission) => !isRawEncryptionPermission(permission) && !isRawCidRevocationPermission(permission)
15382
+ )?.space ?? anchorSpace;
15383
+ return withCapabilitiesRead(request, capabilitySpace);
15348
15384
  }
15349
15385
  function withCapabilitiesRead(permissions, space) {
15350
15386
  const hasRead = permissions.some((p) => p.service === "tinycloud.capabilities" && p.path === "" && p.actions.includes(CAPABILITIES_READ) && normalizePkhIdentifier(p.space ?? "") === normalizePkhIdentifier(space));
@@ -15378,7 +15414,10 @@ function verifySignedSession(data, key, sessionDid, expected = {}) {
15378
15414
  if (/expir/i.test(error instanceof Error ? error.message : String(error))) {
15379
15415
  throw new CLIError("AUTH_EXPIRED", `The approved session has expired. ${notStored}`, ExitCode.AUTH_REQUIRED);
15380
15416
  }
15381
- throw new CLIError("OPENKEY_PROOF_INVALID", `OpenKey did not return a complete, verifiable session proof. ${notStored}`, ExitCode.AUTH_REQUIRED);
15417
+ throw Object.assign(
15418
+ new CLIError("OPENKEY_PROOF_INVALID", `OpenKey did not return a complete, verifiable session proof. ${notStored}`, ExitCode.AUTH_REQUIRED),
15419
+ { cause: error }
15420
+ );
15382
15421
  }
15383
15422
  const signedExpiry = Date.parse(expiresAt);
15384
15423
  if (signedExpiry <= Date.now()) {
@@ -15395,10 +15434,23 @@ function verifySignedSession(data, key, sessionDid, expected = {}) {
15395
15434
  }
15396
15435
  function verifyScopedLogin(data, key, sessionDid, requested, expected = {}) {
15397
15436
  const notStored = expected.purpose === "grant" ? "No grant was stored." : "No scoped session was saved.";
15398
- const signed = verifySignedSession(data, key, sessionDid, expected);
15437
+ let signed;
15438
+ try {
15439
+ signed = verifySignedSession(data, key, sessionDid, expected);
15440
+ } catch (error) {
15441
+ const cause = error instanceof Error ? error.cause : void 0;
15442
+ if (requested.some(isRawCidRevocationPermission) && /invalid ReCap resource URI/i.test(cause instanceof Error ? cause.message : String(cause ?? ""))) {
15443
+ throw new CLIError(
15444
+ "RAW_RECAP_RESOURCE_UNSUPPORTED",
15445
+ "This WASM build cannot verify raw urn:cid ReCap resources.",
15446
+ ExitCode.PERMISSION_DENIED
15447
+ );
15448
+ }
15449
+ throw error;
15450
+ }
15399
15451
  const spaceId = data.spaceId;
15400
15452
  for (const permission of requested) {
15401
- if (isRawEncryptionPermission(permission)) continue;
15453
+ if (isRawEncryptionPermission(permission) || isRawCidRevocationPermission(permission)) continue;
15402
15454
  if (normalizePkhIdentifier(ownerSpaceId(permission.space ?? "", signed.ownerDid)) !== normalizePkhIdentifier(spaceId)) {
15403
15455
  throw new CLIError("OPENKEY_SCOPE_MISMATCH", `The approved space differs from the requested space. ${notStored}`, ExitCode.PERMISSION_DENIED);
15404
15456
  }
@@ -16439,6 +16491,13 @@ function registerAuthCommand(program) {
16439
16491
  yes: options.yes === true
16440
16492
  });
16441
16493
  const grant = await grantAuthRequest(node, resolvedRequest);
16494
+ await appendLocalGrantArtifact(ctx.profile, grant.delegation);
16495
+ await appendGrantHistory(ctx.profile, {
16496
+ addedCaps: grant.permissions,
16497
+ source: "cli",
16498
+ delegationCid: grant.delegationCid,
16499
+ expiry: grant.expiry
16500
+ });
16442
16501
  outputJson(grant);
16443
16502
  } catch (error) {
16444
16503
  handleError(error);
@@ -16795,7 +16854,10 @@ async function activateAndStoreOpenKeyGrants(profileName2, host, node, grants, s
16795
16854
  });
16796
16855
  }
16797
16856
  async function ensureDelegationAuthority(params) {
16798
- if (!params.force && params.node.hasRuntimePermissions(params.requested)) return;
16857
+ if (!params.force && params.node.hasRuntimePermissions(params.requested)) return void 0;
16858
+ if (!params.consentAlreadyGiven && (params.persist === false || params.profile.authMethod !== "openkey")) {
16859
+ await requirePermissionConsent(params.requested, params.yes);
16860
+ }
16799
16861
  if (params.profile.authMethod === "openkey") {
16800
16862
  const key = await ProfileManager.getKey(params.ctx.profile);
16801
16863
  if (!key) {
@@ -16815,6 +16877,37 @@ async function ensureDelegationAuthority(params) {
16815
16877
  expiry: expiryCap
16816
16878
  };
16817
16879
  const anchorSpace = params.anchorSpace ?? params.profile.spaceId ?? params.profile.spaceName;
16880
+ if (params.persist === false) {
16881
+ const request = grantRequestPermissions(params.requested, anchorSpace);
16882
+ let delegationData;
16883
+ try {
16884
+ delegationData = await acquireOpenKey(params.profile.did, {
16885
+ jwk: key,
16886
+ host: params.ctx.host,
16887
+ permissions: request,
16888
+ reason: permissionGrantReason(params.reason, request),
16889
+ openkeyHost,
16890
+ expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap)
16891
+ });
16892
+ } catch (error) {
16893
+ if (params.requested.some(isRawCidRevocationPermission) && /invalid ReCap resource URI/i.test(error instanceof Error ? error.message : String(error))) {
16894
+ throw new CLIError(
16895
+ "RAW_RECAP_RESOURCE_UNSUPPORTED",
16896
+ "The OpenKey/WASM ReCap encoder does not support raw urn:cid revocation resources.",
16897
+ ExitCode.PERMISSION_DENIED
16898
+ );
16899
+ }
16900
+ throw error;
16901
+ }
16902
+ const delegation = portableFromOpenKeyDelegation(
16903
+ delegationData,
16904
+ request,
16905
+ params.ctx.host,
16906
+ proof
16907
+ );
16908
+ await params.node.useRuntimeDelegation(delegation);
16909
+ return delegation;
16910
+ }
16818
16911
  const grants = [];
16819
16912
  for (const group of groupPermissionsBySpace(params.requested)) {
16820
16913
  const request = grantRequestPermissions(group, anchorSpace);
@@ -16832,21 +16925,11 @@ async function ensureDelegationAuthority(params) {
16832
16925
  await activateAndStoreOpenKeyGrants(params.ctx.profile, params.ctx.host, params.node, grants, "cli");
16833
16926
  return;
16834
16927
  }
16835
- if (isInteractive()) {
16836
- if (!params.yes) {
16837
- await confirmPermissionRequest(params.requested);
16838
- }
16839
- } else if (!params.yes) {
16840
- throw new CLIError(
16841
- "CONFIRMATION_REQUIRED",
16842
- "Local-key auth grants in non-interactive mode require --yes.",
16843
- ExitCode.USAGE_ERROR
16844
- );
16845
- }
16846
16928
  const delegations = await params.node.grantRuntimePermissions(
16847
16929
  params.requested,
16848
16930
  params.expiryOption !== void 0 ? { expiry: params.expiryOption } : void 0
16849
16931
  );
16932
+ if (params.persist === false) return delegations[0];
16850
16933
  for (const delegation of delegations) {
16851
16934
  const covering = permissionsFromDelegation(delegation);
16852
16935
  await appendAdditionalDelegation(
@@ -16924,6 +17007,17 @@ async function confirmPermissionRequest(permissions) {
16924
17007
  throw new CLIError("REQUEST_CANCELLED", "Permission request cancelled.", ExitCode.ERROR);
16925
17008
  }
16926
17009
  }
17010
+ async function requirePermissionConsent(permissions, yes) {
17011
+ if (isInteractive()) {
17012
+ if (!yes) await confirmPermissionRequest(permissions);
17013
+ } else if (!yes) {
17014
+ throw new CLIError(
17015
+ "CONFIRMATION_REQUIRED",
17016
+ "Permission grants in non-interactive mode require --yes.",
17017
+ ExitCode.USAGE_ERROR
17018
+ );
17019
+ }
17020
+ }
16927
17021
  function isDangerousPermission(permission) {
16928
17022
  if (permission.path === "" || permission.path === "/") return true;
16929
17023
  return permission.actions.some(
@@ -16952,7 +17046,7 @@ function groupPermissionsBySpace(permissions) {
16952
17046
  const groups = /* @__PURE__ */ new Map();
16953
17047
  const rawEntries = [];
16954
17048
  for (const permission of permissions) {
16955
- if (isRawEncryptionPermission(permission)) {
17049
+ if (isRawEncryptionPermission(permission) || isRawCidRevocationPermission(permission)) {
16956
17050
  rawEntries.push(permission);
16957
17051
  continue;
16958
17052
  }
@@ -16983,11 +17077,13 @@ function portableFromOpenKeyDelegation(data, requested, host, proof) {
16983
17077
  }
16984
17078
  const returnedSpace = data.spaceId;
16985
17079
  const effective = session.permissions;
16986
- const spaced = effective.filter((permission) => !isRawEncryptionPermission(permission));
17080
+ const spaced = effective.filter(
17081
+ (permission) => !isRawEncryptionPermission(permission) && !isRawCidRevocationPermission(permission)
17082
+ );
16987
17083
  const primary = spaced.find((permission) => permission.service !== "tinycloud.capabilities") ?? spaced[0] ?? effective[0];
16988
17084
  const resources = effective.map((permission) => ({
16989
17085
  service: permission.service.slice("tinycloud.".length),
16990
- space: isVerifiedRawEncryptionPermission(permission) ? "encryption" : returnedSpace,
17086
+ space: isRawCidRevocationPermission(permission) ? permission.space : isVerifiedRawEncryptionPermission(permission) ? "encryption" : returnedSpace,
16991
17087
  path: permission.path,
16992
17088
  actions: [...permission.actions],
16993
17089
  ...permission.caveats === void 0 ? {} : { caveats: structuredClone(permission.caveats) }
@@ -17463,6 +17559,7 @@ init_profiles();
17463
17559
  init_formatter();
17464
17560
  init_errors();
17465
17561
  init_constants();
17562
+ import { parseSignedCompactUcanAttenuation } from "@tinycloud/sdk-core";
17466
17563
  function normalizeDid(input) {
17467
17564
  const normalized = input.trim();
17468
17565
  const fragmentIndex = normalized.indexOf("#");
@@ -17470,11 +17567,35 @@ function normalizeDid(input) {
17470
17567
  }
17471
17568
  function didMatches(actual, expected) {
17472
17569
  if (!actual) return false;
17473
- try {
17474
- return normalizeDid(actual) === normalizeDid(expected);
17475
- } catch {
17476
- return actual === expected;
17570
+ return normalizeDid(actual) === normalizeDid(expected);
17571
+ }
17572
+ function ownerDidFromSpace(spaceId) {
17573
+ const owner = /^tinycloud:((?:did:)?pkh:eip155:[1-9]\d*:0x[0-9a-fA-F]{40})(?::|\/|$)/.exec(spaceId ?? "")?.[1];
17574
+ if (!owner) return void 0;
17575
+ return owner.startsWith("did:") ? owner : `did:${owner}`;
17576
+ }
17577
+ async function resolveCidBoundTargetSpace(params) {
17578
+ if (!params.ownerDid) return void 0;
17579
+ const grants = await loadLocalGrantArtifacts(params.profileName);
17580
+ for (const grant of grants) {
17581
+ const delegation = grant.delegation;
17582
+ if (grant.delegationCid !== params.cid || delegation.cid !== params.cid) continue;
17583
+ try {
17584
+ const authorization = delegation.delegationHeader.Authorization.replace(/^Bearer /i, "");
17585
+ const signed = parseSignedCompactUcanAttenuation(authorization, params.cid);
17586
+ for (const resource of Object.keys(signed.payload.att)) {
17587
+ const space = /^(tinycloud:[^/]+)\/[^/]+\/.*$/.exec(resource)?.[1];
17588
+ if (!space) continue;
17589
+ const ownerMatch = /^tinycloud:((?:did:)?pkh:eip155:[1-9]\d*:0x[0-9a-fA-F]{40}):/.exec(space);
17590
+ const owner = ownerMatch?.[1];
17591
+ const ownerDid = owner?.startsWith("did:") ? owner : owner ? `did:${owner}` : void 0;
17592
+ if (ownerDid && normalizeDid(ownerDid) === normalizeDid(params.ownerDid)) return space;
17593
+ }
17594
+ } catch {
17595
+ continue;
17596
+ }
17477
17597
  }
17598
+ return void 0;
17478
17599
  }
17479
17600
  function registerDelegationCommand(program) {
17480
17601
  const delegation = program.command("delegation").description("Manage delegations");
@@ -17554,16 +17675,158 @@ function registerDelegationCommand(program) {
17554
17675
  handleError(error);
17555
17676
  }
17556
17677
  });
17557
- delegation.command("revoke <cid>").description("Revoke a delegation").action(async (cid, _options, cmd) => {
17678
+ delegation.command("revoke <cid>").description("Revoke a delegation").option("--yes", "Skip local-key TTY confirmation", false).action(async (cid, options, cmd) => {
17558
17679
  try {
17559
17680
  const globalOpts = cmd.optsWithGlobals();
17560
17681
  const ctx = await ProfileManager.resolveContext(globalOpts);
17561
17682
  const node = await ensureAuthenticated(ctx);
17562
- const result = await node.delegationManager.revoke(cid);
17683
+ const profile = await ProfileManager.getProfile(ctx.profile);
17684
+ const revokePermission = {
17685
+ service: "tinycloud.delegation",
17686
+ space: `urn:cid:${cid}`,
17687
+ path: "",
17688
+ actions: ["tinycloud.delegation/revoke"]
17689
+ };
17690
+ const rawAuthorityCovered = node.hasRuntimePermissions([revokePermission]);
17691
+ if (!rawAuthorityCovered) await requirePermissionConsent([revokePermission], options.yes);
17692
+ let targetFound = false;
17693
+ let targetSpaceSource;
17694
+ let queryDenied = false;
17695
+ let boundSpace;
17696
+ const accountSpaceId = node.accountSpaceId;
17697
+ if (!accountSpaceId) {
17698
+ throw new CLIError(
17699
+ "DELEGATION_QUERY_AUTHORITY_UNAVAILABLE",
17700
+ "Cannot determine the account space needed to query delegation records.",
17701
+ ExitCode.AUTH_REQUIRED
17702
+ );
17703
+ }
17704
+ let cursor;
17705
+ do {
17706
+ const query = await node.delegationManager.query({
17707
+ direction: "all",
17708
+ limit: 100,
17709
+ ...cursor === void 0 ? {} : { cursor }
17710
+ });
17711
+ if (!query.ok) {
17712
+ if (query.error.meta?.status === 403) {
17713
+ queryDenied = true;
17714
+ break;
17715
+ }
17716
+ throw cliErrorFromService(query.error);
17717
+ }
17718
+ targetFound = query.data.items.some((delegation2) => delegation2.cid === cid);
17719
+ cursor = targetFound ? void 0 : query.data.nextCursor;
17720
+ } while (cursor !== void 0);
17721
+ targetSpaceSource = "node";
17722
+ if (!targetFound) {
17723
+ const grantHistory = await readGrantHistory(ctx.profile);
17724
+ const recordedGrant = [...grantHistory].reverse().find((entry) => entry.delegationCid === cid);
17725
+ if (recordedGrant) {
17726
+ targetSpaceSource = "local-grant-history";
17727
+ } else if (queryDenied) {
17728
+ boundSpace = await resolveCidBoundTargetSpace({
17729
+ profileName: ctx.profile,
17730
+ ownerDid: profile.ownerDid ?? ownerDidFromSpace(node.accountSpaceId) ?? ownerDidFromSpace(profile.spaceId),
17731
+ cid
17732
+ });
17733
+ if (boundSpace) {
17734
+ targetSpaceSource = "local-signed-grant-artifact";
17735
+ } else {
17736
+ throw new CLIError(
17737
+ "TARGET_NOT_FOUND",
17738
+ `Delegation "${cid}" was not found by the node or local grant sources.`,
17739
+ ExitCode.NOT_FOUND
17740
+ );
17741
+ }
17742
+ } else {
17743
+ throw new CLIError(
17744
+ "TARGET_NOT_FOUND",
17745
+ `Delegation "${cid}" was not found by the node or local grant history.`,
17746
+ ExitCode.NOT_FOUND
17747
+ );
17748
+ }
17749
+ }
17750
+ let authorityScopeSource = "cid-resource";
17751
+ let authorityScopeReason = "The revoke authority is scoped to the exact delegation CID.";
17752
+ let fallbackSpaceId;
17753
+ let commandAuthorityCid;
17754
+ const authorizeFallbackSpace = async (spaceId) => {
17755
+ fallbackSpaceId = spaceId;
17756
+ authorityScopeSource = "local-signed-grant-artifact";
17757
+ authorityScopeReason = "Raw-CID revoke authority could not be issued; the fallback space came from an owner-signed local grant whose Authorization recomputes to this CID.";
17758
+ targetSpaceSource = "local-signed-grant-artifact";
17759
+ const acquired = await ensureDelegationAuthority({
17760
+ ctx,
17761
+ profile,
17762
+ node,
17763
+ requested: [{
17764
+ service: "tinycloud.delegation",
17765
+ space: spaceId === node.accountSpaceId ? "default" : spaceId,
17766
+ path: "",
17767
+ actions: ["tinycloud.delegation/revoke"]
17768
+ }],
17769
+ expiryOption: void 0,
17770
+ reason: `Revoke delegation ${cid} using its CID-bound owner-signed grant artifact`,
17771
+ yes: options.yes,
17772
+ consentAlreadyGiven: true,
17773
+ persist: false
17774
+ });
17775
+ commandAuthorityCid = acquired?.cid;
17776
+ };
17777
+ if (!rawAuthorityCovered) {
17778
+ try {
17779
+ const acquired = await ensureDelegationAuthority({
17780
+ ctx,
17781
+ profile,
17782
+ node,
17783
+ requested: [revokePermission],
17784
+ expiryOption: void 0,
17785
+ reason: `Revoke delegation ${cid}`,
17786
+ yes: options.yes,
17787
+ consentAlreadyGiven: true,
17788
+ persist: false
17789
+ });
17790
+ commandAuthorityCid = acquired?.cid;
17791
+ } catch (error) {
17792
+ if (error === null || typeof error !== "object" || !("code" in error) || error.code !== "RAW_RECAP_RESOURCE_UNSUPPORTED") {
17793
+ throw error;
17794
+ }
17795
+ const fallbackSpace = boundSpace ?? await resolveCidBoundTargetSpace({
17796
+ profileName: ctx.profile,
17797
+ ownerDid: profile.ownerDid ?? ownerDidFromSpace(node.accountSpaceId) ?? ownerDidFromSpace(profile.spaceId),
17798
+ cid
17799
+ });
17800
+ if (!fallbackSpace) {
17801
+ throw new CLIError(
17802
+ "RAW_RECAP_RESOURCE_UNSUPPORTED",
17803
+ `Cannot safely revoke "${cid}": raw CID ReCap resources are unavailable and no matching owner-signed local grant artifact binds its space to the CID.`,
17804
+ ExitCode.PERMISSION_DENIED
17805
+ );
17806
+ }
17807
+ await authorizeFallbackSpace(fallbackSpace);
17808
+ }
17809
+ }
17810
+ const result = fallbackSpaceId ? await node.delegationManager.revoke(cid, {
17811
+ targetSpaceId: fallbackSpaceId,
17812
+ ...commandAuthorityCid === void 0 ? {} : { authorityCid: commandAuthorityCid }
17813
+ }) : commandAuthorityCid === void 0 ? await node.revokeDelegation(cid) : await node.delegationManager.revoke(cid, { authorityCid: commandAuthorityCid });
17563
17814
  if (!result.ok) {
17815
+ if (result.error.meta?.status === 403 && result.error.message === "Failed to revoke delegation: 403 - Unauthorized Revoker") {
17816
+ const profileDid = profile.ownerDid ?? ownerDidFromSpace(node.accountSpaceId) ?? ownerDidFromSpace(profile.spaceId) ?? "unknown";
17817
+ throw new CLIError(
17818
+ "REVOKE_UNAUTHORIZED",
17819
+ "The node rejected this revocation: Unauthorized Revoker.",
17820
+ ExitCode.PERMISSION_DENIED,
17821
+ {
17822
+ status: 403,
17823
+ hint: `Use a different profile belonging to the delegation's grantor, its recipient, or the owner of a space it covers; the rejected profile was "${ctx.profile}" (DID ${profileDid}). Retry with \`tc --profile <profile-name> delegation revoke ${cid}\`.`
17824
+ }
17825
+ );
17826
+ }
17564
17827
  throw cliErrorFromService(result.error);
17565
17828
  }
17566
- outputJson({ cid, revoked: true });
17829
+ outputJson({ cid, revoked: true, targetSpaceSource, authorityScopeSource, authorityScopeReason });
17567
17830
  } catch (error) {
17568
17831
  handleError(error);
17569
17832
  }
@@ -20418,6 +20681,8 @@ var ReplicaErrorCode = {
20418
20681
  CONFIG_MISMATCH: "REPLICA_CONFIG_MISMATCH",
20419
20682
  /** The runtime has no supported durable store (e.g. Node < 22.13). */
20420
20683
  RUNTIME_UNSUPPORTED: "RUNTIME_UNSUPPORTED",
20684
+ /** A required open() argument is missing or malformed (e.g. `principal`). */
20685
+ INVALID_ARGUMENT: "REPLICA_INVALID_ARGUMENT",
20421
20686
  /** The local store failed (I/O, corruption, schema). */
20422
20687
  STORAGE_ERROR: "STORAGE_ERROR",
20423
20688
  /** The local disk is full. */
@@ -20458,7 +20723,9 @@ var ReplicaErrorCode = {
20458
20723
  /** Fetched bytes do not hash to the ETag the node attested for them. */
20459
20724
  CONTENT_MISMATCH: "CONTENT_MISMATCH",
20460
20725
  /** A stored blob no longer hashes to its name. */
20461
- INTEGRITY_ERROR: "INTEGRITY_ERROR"
20726
+ INTEGRITY_ERROR: "INTEGRITY_ERROR",
20727
+ /** The replica handle was closed; in-flight calls settle with this code. */
20728
+ CLOSED: "REPLICA_CLOSED"
20462
20729
  };
20463
20730
  var REPLICA_ERROR = /* @__PURE__ */ Symbol.for("tinycloud.replica.error");
20464
20731
  var ReplicaError = class extends Error {
@@ -20568,12 +20835,14 @@ var Replica = class {
20568
20835
  #transportFor;
20569
20836
  #now;
20570
20837
  #leaseTtlMs;
20838
+ #onCommit;
20571
20839
  constructor(options) {
20572
20840
  this.#store = options.store;
20573
20841
  const transport = options.transport;
20574
20842
  this.#transportFor = options.transportFor ?? (transport === void 0 ? void 0 : () => transport);
20575
20843
  this.#now = options.now ?? Date.now;
20576
20844
  this.#leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
20845
+ this.#onCommit = options.onCommit;
20577
20846
  }
20578
20847
  async #state() {
20579
20848
  const state = await this.#store.open();
@@ -20768,9 +21037,10 @@ var Replica = class {
20768
21037
  let cursor = state.cursor;
20769
21038
  let pinned = state.nodeDid;
20770
21039
  let coverage = state.coverage;
21040
+ const retentionGrantCid = config.retentionGrantCid;
20771
21041
  for (; ; ) {
20772
21042
  let page;
20773
- const retentionGrant = config.retentionGrantCid;
21043
+ const retentionGrant = retentionGrantCid;
20774
21044
  try {
20775
21045
  page = await attempt.transport.syncPage({
20776
21046
  prefix: config.prefix,
@@ -20806,6 +21076,7 @@ var Replica = class {
20806
21076
  authority: page.authority,
20807
21077
  coverage,
20808
21078
  at,
21079
+ retentionGrantCid: retentionGrant,
20809
21080
  window,
20810
21081
  complete: !page.more,
20811
21082
  promoteGrant: attempt.promote
@@ -20815,6 +21086,7 @@ var Replica = class {
20815
21086
  attempt.promote = null;
20816
21087
  }
20817
21088
  report.pages += 1;
21089
+ this.#onCommit?.();
20818
21090
  report.changes += page.changes.length;
20819
21091
  report.deleted += page.changes.filter((change) => change.deleted).length;
20820
21092
  report.fetched += verified.fetched;
@@ -20824,7 +21096,7 @@ var Replica = class {
20824
21096
  await this.#store.renewLease(attempt.lease, this.#leaseTtlMs);
20825
21097
  if (!page.more) break;
20826
21098
  }
20827
- report.repaired = await this.#repair(attempt, cursor, pinned, coverage);
21099
+ report.repaired = await this.#repair(attempt, cursor, pinned, coverage, retentionGrantCid);
20828
21100
  report.blobsCollected = await this.#store.collectGarbage(attempt.lease);
20829
21101
  report.coverage = coverage;
20830
21102
  report.contentMissing = (await this.#store.pendingRepairs(Number.MAX_SAFE_INTEGER)).length;
@@ -20906,7 +21178,7 @@ var Replica = class {
20906
21178
  }
20907
21179
  return { changes, blobs, fetched: fetched2 };
20908
21180
  }
20909
- async #repair(attempt, cursor, nodeDid, coverage) {
21181
+ async #repair(attempt, cursor, nodeDid, coverage, retentionGrantCid) {
20910
21182
  const pending = await this.#store.pendingRepairs(Number.MAX_SAFE_INTEGER);
20911
21183
  let repaired = 0;
20912
21184
  for (let start = 0; start < pending.length; start += REPAIR_BATCH) {
@@ -20934,11 +21206,13 @@ var Replica = class {
20934
21206
  authority: null,
20935
21207
  coverage,
20936
21208
  at: new Date(this.#now()).toISOString(),
21209
+ retentionGrantCid,
20937
21210
  window: this.#syncWindow(state.authority, attempt.grant),
20938
21211
  complete: false,
20939
21212
  promoteGrant: null
20940
21213
  });
20941
21214
  repaired += changes.length;
21215
+ this.#onCommit?.();
20942
21216
  }
20943
21217
  return repaired;
20944
21218
  }
@@ -21286,6 +21560,8 @@ var ReplicaErrorCode2 = {
21286
21560
  CONFIG_MISMATCH: "REPLICA_CONFIG_MISMATCH",
21287
21561
  /** The runtime has no supported durable store (e.g. Node < 22.13). */
21288
21562
  RUNTIME_UNSUPPORTED: "RUNTIME_UNSUPPORTED",
21563
+ /** A required open() argument is missing or malformed (e.g. `principal`). */
21564
+ INVALID_ARGUMENT: "REPLICA_INVALID_ARGUMENT",
21289
21565
  /** The local store failed (I/O, corruption, schema). */
21290
21566
  STORAGE_ERROR: "STORAGE_ERROR",
21291
21567
  /** The local disk is full. */
@@ -21326,7 +21602,9 @@ var ReplicaErrorCode2 = {
21326
21602
  /** Fetched bytes do not hash to the ETag the node attested for them. */
21327
21603
  CONTENT_MISMATCH: "CONTENT_MISMATCH",
21328
21604
  /** A stored blob no longer hashes to its name. */
21329
- INTEGRITY_ERROR: "INTEGRITY_ERROR"
21605
+ INTEGRITY_ERROR: "INTEGRITY_ERROR",
21606
+ /** The replica handle was closed; in-flight calls settle with this code. */
21607
+ CLOSED: "REPLICA_CLOSED"
21330
21608
  };
21331
21609
  var REPLICA_ERROR2 = /* @__PURE__ */ Symbol.for("tinycloud.replica.error");
21332
21610
  var ReplicaError2 = class extends Error {
@@ -21729,9 +22007,10 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
21729
22007
  async setRetentionGrant(cid) {
21730
22008
  await this.#guardedWrite("Updating the retention grant", () => {
21731
22009
  this.#db.run(
21732
- "UPDATE replica SET retention_grant_cid = ?, local_read_policy = ? WHERE id = 1",
22010
+ "UPDATE replica SET retention_grant_cid = ?, local_read_policy = ?, retain_until = NULL WHERE id = 1 AND retention_grant_cid IS NOT ?",
21733
22011
  cid,
21734
- cid === null ? "whileGrantValid" : "retainAfterExpiry"
22012
+ cid === null ? "whileGrantValid" : "retainAfterExpiry",
22013
+ cid
21735
22014
  );
21736
22015
  });
21737
22016
  }
@@ -21934,9 +22213,13 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
21934
22213
  const sets = ["cursor = ?", "coverage = ?"];
21935
22214
  const params = [p.cursor, p.coverage];
21936
22215
  if (p.authority !== null) {
21937
- sets.push("node_did = ?", "attested = 1", "not_before = ?", "expires_at = ?", "retain_until = ?", "last_sync_at = ?");
21938
- params.push(p.source.nodeDid, p.authority.notBefore, p.authority.expiresAt, p.authority.retainUntil, p.at);
21939
- if (p.authority.retainUntil !== null) sets.push("retention_revoked = NULL");
22216
+ sets.push("node_did = ?", "attested = 1", "not_before = ?", "expires_at = ?", "last_sync_at = ?");
22217
+ params.push(p.source.nodeDid, p.authority.notBefore, p.authority.expiresAt, p.at);
22218
+ if (this.#row().retention_grant_cid === p.retentionGrantCid) {
22219
+ sets.push("retain_until = ?");
22220
+ params.push(p.authority.retainUntil);
22221
+ if (p.authority.retainUntil !== null) sets.push("retention_revoked = NULL");
22222
+ }
21940
22223
  }
21941
22224
  if (p.complete) {
21942
22225
  sets.push("last_complete_at = ?", "last_error = NULL");