@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.
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +323 -40
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +5 -5
package/dist/legacy-entry.js
CHANGED
|
@@ -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
|
|
15252
|
-
const
|
|
15253
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
17474
|
-
|
|
17475
|
-
|
|
17476
|
-
|
|
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,
|
|
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
|
|
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 =
|
|
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 =
|
|
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 = ?", "
|
|
21938
|
-
params.push(p.source.nodeDid, p.authority.notBefore, p.authority.expiresAt, p.
|
|
21939
|
-
if (
|
|
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");
|