@tinycloud/cli 1.3.0-beta.52 → 1.3.0-beta.53

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.
@@ -29616,6 +29616,9 @@ function verifyScopedLogin(data, key, sessionDid, requested, expected = {}) {
29616
29616
  if (!scopeCovers(requested, approved, signed.ownerDid)) {
29617
29617
  throw new CLIError("OPENKEY_GRANT_BROADENED", `The signed grant contains authority beyond the requested ${expected.purpose === "grant" ? "grant" : "manifest"}. ${notStored}`, ExitCode.PERMISSION_DENIED);
29618
29618
  }
29619
+ if (expected.exactApproval && !scopeCovers(approved, requested, signed.ownerDid)) {
29620
+ throw new CLIError("OPENKEY_APPROVAL_NARROWER", `The approval was narrower than the request. Approve all of it, or ask the agent to request less. ${notStored}`, ExitCode.PERMISSION_DENIED);
29621
+ }
29619
29622
  const session = withVerifiedAuthority({ ...data, jwk: key }, { ...signed, permissions: approved });
29620
29623
  return { session, legacyNested };
29621
29624
  }
@@ -31384,7 +31387,7 @@ function withinPresetSet(entry, applicationsSpace) {
31384
31387
  const grants = Object.values(APPS_AGENT_VARIANTS).flatMap(({ grants: variantGrants }) => variantGrants.filter((grant) => grant.service === entry.service));
31385
31388
  return entry.actions.length > 0 && entry.actions.every((action) => grants.some((grant) => grant.actions.includes(action) && (!(action.endsWith("/put") || action.endsWith("/write")) || folder !== void 0 && grant.within.some((within) => folder.startsWith(within)))));
31386
31389
  }
31387
- function describeGrantScope(requested, requestedExpiry, applicationsSpace) {
31390
+ function describeGrantScope(requested, requestedExpiry, applicationsSpace, maxLifetimeMs) {
31388
31391
  const preset = matchAppsAgentRequest(requested, requestedExpiry, applicationsSpace);
31389
31392
  if (preset !== void 0) {
31390
31393
  return { text: appsAgentScopeNotice(preset.mode, preset.appIds), entries: requested, extras: [] };
@@ -31401,7 +31404,7 @@ function describeGrantScope(requested, requestedExpiry, applicationsSpace) {
31401
31404
  lines.push("Because of how TinyCloud permissions work today (TC-1581), SQL write access can also change or delete tables (structure).");
31402
31405
  }
31403
31406
  const entryLifetimes = requested.filter((entry) => entry.expiry !== void 0).map((entry) => lifetimeMs(entry.expiry));
31404
- const durations = [lifetimeMs(requestedExpiry), ...entryLifetimes].filter((ms2) => ms2 !== void 0);
31407
+ const durations = [lifetimeMs(requestedExpiry), ...entryLifetimes].filter((ms2) => ms2 !== void 0).map((ms2) => Math.min(ms2, maxLifetimeMs ?? Number.POSITIVE_INFINITY));
31405
31408
  const longest = Math.max(...durations);
31406
31409
  lines.push(
31407
31410
  durations.length === 0 || !Number.isFinite(longest) ? "Its lifetime could not be read from the request." : `It lasts up to ${lifetimePhrase(longest)}${requestedExpiry === void 0 ? " (no lifetime was requested; this is the default)" : ""}; revoke it any time with \`${CLI_NAME16} delegation revoke <id>\`.`
@@ -32511,7 +32514,12 @@ ${appsAgentScopeNotice(preset.mode, preset.appIds)}
32511
32514
  [{ service: "tinycloud.kv", space: APPS_AGENT_SPACE, path: "", actions: ["tinycloud.kv/get"] }],
32512
32515
  ctx.profile
32513
32516
  );
32514
- const scope = describeGrantScope(requested, parsed.requestedExpiry, applications.space ?? APPS_AGENT_SPACE);
32517
+ const scope = describeGrantScope(
32518
+ requested,
32519
+ parsed.requestedExpiry,
32520
+ applications.space ?? APPS_AGENT_SPACE,
32521
+ profile.authMethod === "openkey" ? OPENKEY_AGENT_MAX_LIFETIME_MS : void 0
32522
+ );
32515
32523
  process.stderr.write(`
32516
32524
  ${scope.text}
32517
32525
 
@@ -32530,16 +32538,43 @@ ${scope.text}
32530
32538
  }
32531
32539
  const resolvedRequest = { ...parsed, requested };
32532
32540
  const node = await ensureAuthenticated(ctx);
32533
- await ensureDelegationAuthority({
32534
- ctx,
32535
- profile,
32536
- node,
32537
- requested,
32538
- expiryOption: parsed.requestedExpiry,
32539
- reason: "Grant permissions requested by a TinyCloud auth request artifact.",
32540
- yes: options2.yes === true || confirmedExtras
32541
- });
32542
- const grant = await grantAuthRequest(node, resolvedRequest);
32541
+ let grant;
32542
+ if (profile.authMethod === "openkey") {
32543
+ assertOpenKeyAgentRequest(requested);
32544
+ const approval = await acquireOpenKeyApproval({
32545
+ ctx,
32546
+ profile,
32547
+ requested,
32548
+ expiryOption: Math.min(
32549
+ parseRequestedExpiry(parsed.requestedExpiry ?? APPS_AGENT_EXPIRY).durationMs,
32550
+ OPENKEY_AGENT_MAX_LIFETIME_MS
32551
+ ),
32552
+ reason: "Grant permissions requested by a TinyCloud auth request artifact.",
32553
+ exactApproval: true,
32554
+ noPopup: !(process.stdin.isTTY || process.stderr.isTTY)
32555
+ });
32556
+ const approvalNode = await nodeFromOpenKeyApproval(ctx.host, approval.session);
32557
+ grant = {
32558
+ ...await grantAuthRequest(approvalNode, resolvedRequest, {
32559
+ expiry: approval.delegation.expiry.getTime() - Date.now()
32560
+ }),
32561
+ // The owner approved this grant on the OpenKey approval screen, so it was prompted.
32562
+ prompted: true
32563
+ };
32564
+ process.stderr.write(`Grant lasts until ${grant.expiry}, the lifetime of the OpenKey approval.
32565
+ `);
32566
+ } else {
32567
+ await ensureDelegationAuthority({
32568
+ ctx,
32569
+ profile,
32570
+ node,
32571
+ requested,
32572
+ expiryOption: parsed.requestedExpiry,
32573
+ reason: "Grant permissions requested by a TinyCloud auth request artifact.",
32574
+ yes: options2.yes === true || confirmedExtras
32575
+ });
32576
+ grant = await grantAuthRequest(node, resolvedRequest);
32577
+ }
32543
32578
  await appendLocalGrantArtifact(ctx.profile, grant.delegation);
32544
32579
  await appendGrantHistory(ctx.profile, {
32545
32580
  addedCaps: grant.permissions,
@@ -32919,12 +32954,72 @@ async function activateAndStoreOpenKeyGrants(profileName2, host, node, grants, s
32919
32954
  await appendAdditionalDelegations(profileName2, records);
32920
32955
  });
32921
32956
  }
32957
+ async function acquireOpenKeyApproval(params) {
32958
+ if (params.requested.some(isRawCidRevocationPermission)) {
32959
+ throw new CLIError(
32960
+ "RAW_RECAP_RESOURCE_UNSUPPORTED",
32961
+ "OpenKey cannot sign raw urn:cid revocation authority.",
32962
+ ExitCode.PERMISSION_DENIED
32963
+ );
32964
+ }
32965
+ const key = await ProfileManager.getKey(params.ctx.profile);
32966
+ if (!key) {
32967
+ throw new CLIError(
32968
+ "NO_KEY",
32969
+ `No key found for profile "${params.ctx.profile}". Run \`${CLI_NAME17} --profile ${params.ctx.profile} auth rotate\` to create a new key and sign in.`,
32970
+ ExitCode.AUTH_REQUIRED
32971
+ );
32972
+ }
32973
+ const expiryCap = params.expiryOption === void 0 ? void 0 : parseRequestedExpiry(params.expiryOption);
32974
+ const anchorSpace = params.anchorSpace ?? params.profile.spaceId ?? params.profile.spaceName;
32975
+ const request = grantRequestPermissions(params.requested, anchorSpace);
32976
+ const acquireOpenKey = params.openKeyAcquisition ?? startAuthFlow;
32977
+ const delegationData = await acquireOpenKey(params.profile.did, {
32978
+ jwk: key,
32979
+ host: params.ctx.host,
32980
+ permissions: request,
32981
+ reason: permissionGrantReason(params.reason, request),
32982
+ openkeyHost: resolveOpenKeyHost(params.profile),
32983
+ expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap),
32984
+ noPopup: params.noPopup
32985
+ });
32986
+ return verifyOpenKeyApproval(delegationData, request, params.ctx.host, {
32987
+ key,
32988
+ sessionDid: params.profile.sessionDid ?? params.profile.did,
32989
+ expectedOwner: pinnedOwner(params.profile),
32990
+ expiry: expiryCap,
32991
+ exactApproval: params.exactApproval === true
32992
+ });
32993
+ }
32994
+ async function nodeFromOpenKeyApproval(host, session) {
32995
+ const ownerParts = session.ownerDid.split(":");
32996
+ const node = new TinyCloudNode3({ host });
32997
+ await node.restoreSession({
32998
+ delegationHeader: session.delegationHeader,
32999
+ delegationCid: session.delegationCid,
33000
+ spaceId: session.spaceId,
33001
+ jwk: session.jwk,
33002
+ verificationMethod: session.verificationMethod,
33003
+ address: ownerParts[4],
33004
+ chainId: Number(ownerParts[3]),
33005
+ siwe: session.siwe,
33006
+ signature: session.signature,
33007
+ expiresAt: session.expiresAt,
33008
+ tinycloudHosts: [host]
33009
+ });
33010
+ return node;
33011
+ }
32922
33012
  async function ensureDelegationAuthority(params) {
32923
33013
  if (!params.force && params.node.hasRuntimePermissions(params.requested)) return void 0;
32924
33014
  if (!params.consentAlreadyGiven && (params.persist === false || params.profile.authMethod !== "openkey")) {
32925
33015
  await requirePermissionConsent(params.requested, params.yes);
32926
33016
  }
32927
33017
  if (params.profile.authMethod === "openkey") {
33018
+ if (params.persist === false) {
33019
+ const { delegation } = await acquireOpenKeyApproval(params);
33020
+ await params.node.useRuntimeDelegation(delegation);
33021
+ return delegation;
33022
+ }
32928
33023
  if (params.requested.some(isRawCidRevocationPermission)) {
32929
33024
  throw new CLIError(
32930
33025
  "RAW_RECAP_RESOURCE_UNSUPPORTED",
@@ -32947,28 +33042,10 @@ async function ensureDelegationAuthority(params) {
32947
33042
  key,
32948
33043
  sessionDid: params.profile.sessionDid ?? params.profile.did,
32949
33044
  expectedOwner: pinnedOwner(params.profile),
32950
- expiry: expiryCap
33045
+ expiry: expiryCap,
33046
+ exactApproval: params.exactApproval === true
32951
33047
  };
32952
33048
  const anchorSpace = params.anchorSpace ?? params.profile.spaceId ?? params.profile.spaceName;
32953
- if (params.persist === false) {
32954
- const request = grantRequestPermissions(params.requested, anchorSpace);
32955
- const delegationData = await acquireOpenKey(params.profile.did, {
32956
- jwk: key,
32957
- host: params.ctx.host,
32958
- permissions: request,
32959
- reason: permissionGrantReason(params.reason, request),
32960
- openkeyHost,
32961
- expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap)
32962
- });
32963
- const delegation = portableFromOpenKeyDelegation(
32964
- delegationData,
32965
- request,
32966
- params.ctx.host,
32967
- proof
32968
- );
32969
- await params.node.useRuntimeDelegation(delegation);
32970
- return delegation;
32971
- }
32972
33049
  const grants = [];
32973
33050
  for (const group of groupPermissionsBySpace(params.requested)) {
32974
33051
  const request = grantRequestPermissions(group, anchorSpace);
@@ -32978,7 +33055,8 @@ async function ensureDelegationAuthority(params) {
32978
33055
  permissions: request,
32979
33056
  reason: permissionGrantReason(params.reason, group),
32980
33057
  openkeyHost,
32981
- expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap)
33058
+ expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap),
33059
+ noPopup: params.noPopup
32982
33060
  });
32983
33061
  const delegation = portableFromOpenKeyDelegation(delegationData, request, params.ctx.host, proof);
32984
33062
  grants.push({ delegation, effective: permissionsFromDelegation(delegation) });
@@ -33005,6 +33083,23 @@ async function ensureDelegationAuthority(params) {
33005
33083
  });
33006
33084
  }
33007
33085
  }
33086
+ var OPENKEY_AGENT_MAX_LIFETIME_MS = parseRequestedExpiry(APPS_AGENT_EXPIRY).durationMs;
33087
+ function assertOpenKeyAgentRequest(requested) {
33088
+ if (requested.some((permission) => isRawEncryptionPermission(permission) || isRawCidRevocationPermission(permission))) {
33089
+ throw new CLIError(
33090
+ "RAW_RECAP_RESOURCE_UNSUPPORTED",
33091
+ "OpenKey cannot approve raw urn:cid or raw encryption authority for an agent request.",
33092
+ ExitCode.PERMISSION_DENIED
33093
+ );
33094
+ }
33095
+ if (groupPermissionsBySpace(requested).length !== 1) {
33096
+ throw new CLIError(
33097
+ "AGENT_GRANT_MULTIPLE_SPACES",
33098
+ "An agent grant must cover exactly one space. Request one space per auth request.",
33099
+ ExitCode.PERMISSION_DENIED
33100
+ );
33101
+ }
33102
+ }
33008
33103
  function permissionGrantReason(context, permissions) {
33009
33104
  const first = permissions[0];
33010
33105
  const summary = first ? compactPermission(first) : "no permissions";
@@ -33127,10 +33222,14 @@ function groupPermissionsBySpace(permissions) {
33127
33222
  return grouped;
33128
33223
  }
33129
33224
  function portableFromOpenKeyDelegation(data, requested, host, proof) {
33225
+ return verifyOpenKeyApproval(data, requested, host, proof).delegation;
33226
+ }
33227
+ function verifyOpenKeyApproval(data, requested, host, proof) {
33130
33228
  const { session, legacyNested } = verifyScopedLogin(data, proof.key, proof.sessionDid, requested, {
33131
33229
  expectedOwner: proof.expectedOwner,
33132
33230
  expiry: proof.expiry,
33133
- purpose: "grant"
33231
+ purpose: "grant",
33232
+ exactApproval: proof.exactApproval
33134
33233
  });
33135
33234
  if (legacyNested.length > 0) {
33136
33235
  throw new CLIError(
@@ -33153,7 +33252,7 @@ function portableFromOpenKeyDelegation(data, requested, host, proof) {
33153
33252
  ...permission.caveats === void 0 ? {} : { caveats: structuredClone(permission.caveats) }
33154
33253
  }));
33155
33254
  const ownerParts = session.ownerDid.split(":");
33156
- return {
33255
+ const delegation = {
33157
33256
  cid: data.delegationCid,
33158
33257
  delegationHeader: data.delegationHeader,
33159
33258
  spaceId: returnedSpace,
@@ -33168,6 +33267,7 @@ function portableFromOpenKeyDelegation(data, requested, host, proof) {
33168
33267
  // Verified above; replay rebuilds the CACAO from it before trusting the grant.
33169
33268
  siweProof: { siwe: data.siwe, signature: data.signature }
33170
33269
  };
33270
+ return { delegation, session };
33171
33271
  }
33172
33272
  async function rotateAuthKey(profileName2, host, options = {}) {
33173
33273
  await recoverPendingLogout(profileName2);