@sanity/cli 8.20.0 → 8.21.0

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.
@@ -0,0 +1,18 @@
1
+ import { getProjectMembership } from '../../services/tokens.js';
2
+ import { ORG_ROLE_LABELS } from './grants.js';
3
+ export function formatTokenAccess(token, scope) {
4
+ if (scope.resourceType === 'project') {
5
+ return getProjectMembership(token, scope.resourceId)?.roleNames.join(', ') || 'No roles';
6
+ }
7
+ const access = token.memberships.flatMap(({ resourceId, resourceType, roleNames })=>{
8
+ if (resourceType !== 'organization') {
9
+ return [
10
+ `${resourceType}:${resourceId}: ${roleNames.join(', ') || 'No roles'}`
11
+ ];
12
+ }
13
+ return roleNames.map((name)=>ORG_ROLE_LABELS.get(name) ?? `${resourceType}:${resourceId}: ${name}`);
14
+ });
15
+ return access.join('; ') || 'No roles';
16
+ }
17
+
18
+ //# sourceMappingURL=formatTokenAccess.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/actions/tokens/formatTokenAccess.ts"],"sourcesContent":["import {getProjectMembership} from '../../services/tokens.js'\nimport {ORG_ROLE_LABELS} from './grants.js'\nimport {type Robot, type TokenScope} from './types.js'\n\nexport function formatTokenAccess(token: Robot, scope: TokenScope): string {\n if (scope.resourceType === 'project') {\n return getProjectMembership(token, scope.resourceId)?.roleNames.join(', ') || 'No roles'\n }\n\n const access = token.memberships.flatMap(({resourceId, resourceType, roleNames}) => {\n if (resourceType !== 'organization') {\n return [`${resourceType}:${resourceId}: ${roleNames.join(', ') || 'No roles'}`]\n }\n\n return roleNames.map(\n (name) => ORG_ROLE_LABELS.get(name) ?? `${resourceType}:${resourceId}: ${name}`,\n )\n })\n\n return access.join('; ') || 'No roles'\n}\n"],"names":["getProjectMembership","ORG_ROLE_LABELS","formatTokenAccess","token","scope","resourceType","resourceId","roleNames","join","access","memberships","flatMap","map","name","get"],"mappings":"AAAA,SAAQA,oBAAoB,QAAO,2BAA0B;AAC7D,SAAQC,eAAe,QAAO,cAAa;AAG3C,OAAO,SAASC,kBAAkBC,KAAY,EAAEC,KAAiB;IAC/D,IAAIA,MAAMC,YAAY,KAAK,WAAW;QACpC,OAAOL,qBAAqBG,OAAOC,MAAME,UAAU,GAAGC,UAAUC,KAAK,SAAS;IAChF;IAEA,MAAMC,SAASN,MAAMO,WAAW,CAACC,OAAO,CAAC,CAAC,EAACL,UAAU,EAAED,YAAY,EAAEE,SAAS,EAAC;QAC7E,IAAIF,iBAAiB,gBAAgB;YACnC,OAAO;gBAAC,GAAGA,aAAa,CAAC,EAAEC,WAAW,EAAE,EAAEC,UAAUC,IAAI,CAAC,SAAS,YAAY;aAAC;QACjF;QAEA,OAAOD,UAAUK,GAAG,CAClB,CAACC,OAASZ,gBAAgBa,GAAG,CAACD,SAAS,GAAGR,aAAa,CAAC,EAAEC,WAAW,EAAE,EAAEO,MAAM;IAEnF;IAEA,OAAOJ,OAAOD,IAAI,CAAC,SAAS;AAC9B"}
@@ -67,6 +67,18 @@ export const ORG_PERMISSIONS = [
67
67
  title: 'Deploy Studios (all projects)'
68
68
  }
69
69
  ];
70
+ export const ORG_ROLE_LABELS = new Map(ORG_PERMISSIONS.flatMap((permission)=>{
71
+ if ('roleName' in permission) return [
72
+ [
73
+ permission.roleName,
74
+ permission.title
75
+ ]
76
+ ];
77
+ return permission.roles.map((role)=>[
78
+ role.roleName,
79
+ `${permission.title}: ${role.title}`
80
+ ]);
81
+ }));
70
82
  const findOrgPermission = (key)=>ORG_PERMISSIONS.find((permission)=>permission.key === key);
71
83
  const isProjectGrant = (grant)=>grant.key.startsWith(PROJECT_PREFIX);
72
84
  const orgGrants = (grants)=>grants.filter((grant)=>!isProjectGrant(grant));
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/tokens/grants.ts"],"sourcesContent":["import {type Membership, type SelectedTokenRole} from './types.js'\n\ntype OrgRole = {key: string; roleName: string; title: string}\n\nexport type OrgPermission = {key: string; title: string} & ({roleName: string} | {roles: OrgRole[]})\n\nexport type Grant = {key: string; roles: string[]}\n\nexport const DEFAULT_ROLE = {\n name: 'viewer',\n title: 'Viewer',\n} satisfies SelectedTokenRole\n\nconst PROJECT_PREFIX = 'project:'\n\nconst viewer = (roleName: string): OrgRole => ({\n key: 'viewer',\n roleName,\n title: 'Viewer',\n})\nconst editor = (roleName: string): OrgRole => ({\n key: 'editor',\n roleName,\n title: 'Editor',\n})\n\nexport const ORG_PERMISSIONS: OrgPermission[] = [\n {\n key: 'context',\n roles: [\n viewer('knowledge-base-viewer-robot'),\n editor('knowledge-base-editor-robot'),\n {\n key: 'developer',\n roleName: 'knowledge-base-developer-robot',\n title: 'Developer',\n },\n ],\n title: 'Context',\n },\n {\n key: 'blueprints',\n roleName: 'blueprints-deployer-robot',\n title: 'Blueprints',\n },\n {\n key: 'media-library',\n roles: [viewer('media-library-viewer-robot'), editor('media-library-editor-robot')],\n title: 'Media Library',\n },\n {\n key: 'canvas',\n roles: [viewer('canvas-viewer-robot'), editor('canvas-editor-robot')],\n title: 'Canvas',\n },\n {\n key: 'sdk-apps',\n roleName: 'sdk-app-editor-robot',\n title: 'Manage SDK Apps',\n },\n {\n key: 'access',\n roles: [viewer('access-viewer-robot'), editor('access-manager-robot')],\n title: 'Manage Access',\n },\n {\n key: 'deploy-studios',\n roleName: 'deploy-studio-robot',\n title: 'Deploy Studios (all projects)',\n },\n]\n\nconst findOrgPermission = (key: string) =>\n ORG_PERMISSIONS.find((permission) => permission.key === key)\n\nconst isProjectGrant = (grant: Grant) => grant.key.startsWith(PROJECT_PREFIX)\n\nconst orgGrants = (grants: Grant[]) => grants.filter((grant) => !isProjectGrant(grant))\n\nexport function projectGrantKey(projectId: string): string {\n return `${PROJECT_PREFIX}${projectId}`\n}\n\nexport function parseGrant(value: string): {error: string} | {grant: Grant} {\n const [key = '', roleList, ...extra] = value.split('=')\n const roles = roleList?.split(',') ?? []\n if (extra.length > 0 || roles.includes('')) {\n return {\n error: `Invalid grant \"${value}\". Use <permission>[=<role>] or project:<projectId>[=<role>[,<role>]]`,\n }\n }\n const grant = {key, roles: roles.length > 0 ? roles : [DEFAULT_ROLE.name]}\n\n if (key.startsWith(PROJECT_PREFIX) && key.length > PROJECT_PREFIX.length) {\n return {grant}\n }\n\n const permission = findOrgPermission(key)\n if (!permission) {\n const keys = [...ORG_PERMISSIONS.map((p) => p.key), `${PROJECT_PREFIX}<projectId>`]\n return {error: `Unknown grant \"${key}\". Use one of: ${keys.join(', ')}`}\n }\n\n if ('roleName' in permission) {\n return roles.length > 0\n ? {\n error: `Grant \"${key}\" does not take a role. Pass it as --grant ${key}`,\n }\n : {grant: {key, roles: []}}\n }\n\n if (roles.length > 1) {\n return {\n error: `Grant \"${key}\" takes one role. Pass it as --grant ${key}=<role>`,\n }\n }\n\n const roleKeys = permission.roles.map((role) => role.key)\n if (roles[0] && !roleKeys.includes(roles[0])) {\n return {\n error: `Invalid role \"${roles[0]}\" for \"${key}\". Use one of: ${roleKeys.join(', ')}`,\n }\n }\n\n return {grant}\n}\n\nexport function formatGrant({key, roles}: Grant): string {\n return roles.length > 0 ? `${key}=${roles.join(',')}` : key\n}\n\nexport function findGrantConflict(grants: Grant[]): string | undefined {\n const roleByPermission = new Map<string, string | undefined>()\n for (const {key, roles} of orgGrants(grants)) {\n if (roleByPermission.has(key) && roleByPermission.get(key) !== roles[0]) {\n return `Grant \"${key}\" was given with different roles. Pass it once.`\n }\n roleByPermission.set(key, roles[0])\n }\n return undefined\n}\n\nfunction toOrganizationRoleNames(grants: Grant[]): string[] {\n const roleNames = orgGrants(grants).flatMap(({key, roles}) => {\n const permission = findOrgPermission(key)\n if (!permission) return []\n if ('roleName' in permission) return [permission.roleName]\n return permission.roles.filter((role) => roles.includes(role.key)).map((role) => role.roleName)\n })\n return [...new Set(roleNames)]\n}\n\nfunction toProjectMemberships(grants: Grant[]): Membership[] {\n const rolesByProject = new Map<string, Set<string>>()\n for (const grant of grants.filter((grant) => isProjectGrant(grant))) {\n const projectId = grant.key.slice(PROJECT_PREFIX.length)\n rolesByProject.set(\n projectId,\n new Set([...(rolesByProject.get(projectId) ?? []), ...grant.roles]),\n )\n }\n\n return [...rolesByProject].map(([resourceId, roleNames]) => ({\n resourceId,\n resourceType: 'project',\n roleNames: [...roleNames],\n }))\n}\n\nexport function getProjectTokenMembership(grants: Grant[]): Membership | undefined {\n const [membership, ...otherProjects] = toProjectMemberships(grants)\n return otherProjects.length === 0 && toOrganizationRoleNames(grants).length === 0\n ? membership\n : undefined\n}\n\nexport function toMemberships(grants: Grant[], organizationId: string): Membership[] {\n const roleNames = toOrganizationRoleNames(grants)\n const projectMemberships = toProjectMemberships(grants)\n return roleNames.length > 0\n ? [{resourceId: organizationId, resourceType: 'organization', roleNames}, ...projectMemberships]\n : projectMemberships\n}\n"],"names":["DEFAULT_ROLE","name","title","PROJECT_PREFIX","viewer","roleName","key","editor","ORG_PERMISSIONS","roles","findOrgPermission","find","permission","isProjectGrant","grant","startsWith","orgGrants","grants","filter","projectGrantKey","projectId","parseGrant","value","roleList","extra","split","length","includes","error","keys","map","p","join","roleKeys","role","formatGrant","findGrantConflict","roleByPermission","Map","has","get","set","undefined","toOrganizationRoleNames","roleNames","flatMap","Set","toProjectMemberships","rolesByProject","slice","resourceId","resourceType","getProjectTokenMembership","membership","otherProjects","toMemberships","organizationId","projectMemberships"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/actions/tokens/grants.ts"],"sourcesContent":["import {type Membership, type SelectedTokenRole} from './types.js'\n\ntype OrgRole = {key: string; roleName: string; title: string}\n\nexport type OrgPermission = {key: string; title: string} & ({roleName: string} | {roles: OrgRole[]})\n\nexport type Grant = {key: string; roles: string[]}\n\nexport const DEFAULT_ROLE = {\n name: 'viewer',\n title: 'Viewer',\n} satisfies SelectedTokenRole\n\nconst PROJECT_PREFIX = 'project:'\n\nconst viewer = (roleName: string): OrgRole => ({\n key: 'viewer',\n roleName,\n title: 'Viewer',\n})\nconst editor = (roleName: string): OrgRole => ({\n key: 'editor',\n roleName,\n title: 'Editor',\n})\n\nexport const ORG_PERMISSIONS: OrgPermission[] = [\n {\n key: 'context',\n roles: [\n viewer('knowledge-base-viewer-robot'),\n editor('knowledge-base-editor-robot'),\n {\n key: 'developer',\n roleName: 'knowledge-base-developer-robot',\n title: 'Developer',\n },\n ],\n title: 'Context',\n },\n {\n key: 'blueprints',\n roleName: 'blueprints-deployer-robot',\n title: 'Blueprints',\n },\n {\n key: 'media-library',\n roles: [viewer('media-library-viewer-robot'), editor('media-library-editor-robot')],\n title: 'Media Library',\n },\n {\n key: 'canvas',\n roles: [viewer('canvas-viewer-robot'), editor('canvas-editor-robot')],\n title: 'Canvas',\n },\n {\n key: 'sdk-apps',\n roleName: 'sdk-app-editor-robot',\n title: 'Manage SDK Apps',\n },\n {\n key: 'access',\n roles: [viewer('access-viewer-robot'), editor('access-manager-robot')],\n title: 'Manage Access',\n },\n {\n key: 'deploy-studios',\n roleName: 'deploy-studio-robot',\n title: 'Deploy Studios (all projects)',\n },\n]\n\nexport const ORG_ROLE_LABELS = new Map<string, string>(\n ORG_PERMISSIONS.flatMap((permission) => {\n if ('roleName' in permission) return [[permission.roleName, permission.title]]\n return permission.roles.map((role) => [role.roleName, `${permission.title}: ${role.title}`])\n }),\n)\n\nconst findOrgPermission = (key: string) =>\n ORG_PERMISSIONS.find((permission) => permission.key === key)\n\nconst isProjectGrant = (grant: Grant) => grant.key.startsWith(PROJECT_PREFIX)\n\nconst orgGrants = (grants: Grant[]) => grants.filter((grant) => !isProjectGrant(grant))\n\nexport function projectGrantKey(projectId: string): string {\n return `${PROJECT_PREFIX}${projectId}`\n}\n\nexport function parseGrant(value: string): {error: string} | {grant: Grant} {\n const [key = '', roleList, ...extra] = value.split('=')\n const roles = roleList?.split(',') ?? []\n if (extra.length > 0 || roles.includes('')) {\n return {\n error: `Invalid grant \"${value}\". Use <permission>[=<role>] or project:<projectId>[=<role>[,<role>]]`,\n }\n }\n const grant = {key, roles: roles.length > 0 ? roles : [DEFAULT_ROLE.name]}\n\n if (key.startsWith(PROJECT_PREFIX) && key.length > PROJECT_PREFIX.length) {\n return {grant}\n }\n\n const permission = findOrgPermission(key)\n if (!permission) {\n const keys = [...ORG_PERMISSIONS.map((p) => p.key), `${PROJECT_PREFIX}<projectId>`]\n return {error: `Unknown grant \"${key}\". Use one of: ${keys.join(', ')}`}\n }\n\n if ('roleName' in permission) {\n return roles.length > 0\n ? {\n error: `Grant \"${key}\" does not take a role. Pass it as --grant ${key}`,\n }\n : {grant: {key, roles: []}}\n }\n\n if (roles.length > 1) {\n return {\n error: `Grant \"${key}\" takes one role. Pass it as --grant ${key}=<role>`,\n }\n }\n\n const roleKeys = permission.roles.map((role) => role.key)\n if (roles[0] && !roleKeys.includes(roles[0])) {\n return {\n error: `Invalid role \"${roles[0]}\" for \"${key}\". Use one of: ${roleKeys.join(', ')}`,\n }\n }\n\n return {grant}\n}\n\nexport function formatGrant({key, roles}: Grant): string {\n return roles.length > 0 ? `${key}=${roles.join(',')}` : key\n}\n\nexport function findGrantConflict(grants: Grant[]): string | undefined {\n const roleByPermission = new Map<string, string | undefined>()\n for (const {key, roles} of orgGrants(grants)) {\n if (roleByPermission.has(key) && roleByPermission.get(key) !== roles[0]) {\n return `Grant \"${key}\" was given with different roles. Pass it once.`\n }\n roleByPermission.set(key, roles[0])\n }\n return undefined\n}\n\nfunction toOrganizationRoleNames(grants: Grant[]): string[] {\n const roleNames = orgGrants(grants).flatMap(({key, roles}) => {\n const permission = findOrgPermission(key)\n if (!permission) return []\n if ('roleName' in permission) return [permission.roleName]\n return permission.roles.filter((role) => roles.includes(role.key)).map((role) => role.roleName)\n })\n return [...new Set(roleNames)]\n}\n\nfunction toProjectMemberships(grants: Grant[]): Membership[] {\n const rolesByProject = new Map<string, Set<string>>()\n for (const grant of grants.filter((grant) => isProjectGrant(grant))) {\n const projectId = grant.key.slice(PROJECT_PREFIX.length)\n rolesByProject.set(\n projectId,\n new Set([...(rolesByProject.get(projectId) ?? []), ...grant.roles]),\n )\n }\n\n return [...rolesByProject].map(([resourceId, roleNames]) => ({\n resourceId,\n resourceType: 'project',\n roleNames: [...roleNames],\n }))\n}\n\nexport function getProjectTokenMembership(grants: Grant[]): Membership | undefined {\n const [membership, ...otherProjects] = toProjectMemberships(grants)\n return otherProjects.length === 0 && toOrganizationRoleNames(grants).length === 0\n ? membership\n : undefined\n}\n\nexport function toMemberships(grants: Grant[], organizationId: string): Membership[] {\n const roleNames = toOrganizationRoleNames(grants)\n const projectMemberships = toProjectMemberships(grants)\n return roleNames.length > 0\n ? [{resourceId: organizationId, resourceType: 'organization', roleNames}, ...projectMemberships]\n : projectMemberships\n}\n"],"names":["DEFAULT_ROLE","name","title","PROJECT_PREFIX","viewer","roleName","key","editor","ORG_PERMISSIONS","roles","ORG_ROLE_LABELS","Map","flatMap","permission","map","role","findOrgPermission","find","isProjectGrant","grant","startsWith","orgGrants","grants","filter","projectGrantKey","projectId","parseGrant","value","roleList","extra","split","length","includes","error","keys","p","join","roleKeys","formatGrant","findGrantConflict","roleByPermission","has","get","set","undefined","toOrganizationRoleNames","roleNames","Set","toProjectMemberships","rolesByProject","slice","resourceId","resourceType","getProjectTokenMembership","membership","otherProjects","toMemberships","organizationId","projectMemberships"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/tokens/types.ts"],"sourcesContent":["export interface Role {\n appliesToRobots: boolean\n appliesToUsers: boolean\n description: string\n isCustom: boolean\n name: string\n resourceId: string\n resourceType: string\n title: string\n}\n\nexport type SelectedTokenRole = Pick<Role, 'name' | 'title'>\n\nexport interface Membership {\n resourceId: string\n resourceType: string\n roleNames: string[]\n\n addedAt?: string\n lastSeenAt?: string | null\n resourceUserId?: string | null\n}\n\nexport interface Robot {\n createdAt: string\n id: string\n label: string\n memberships: Membership[]\n\n expiresAt?: string | null\n managedBy?: {\n resourceId: string\n resourceType: string\n }\n tokenId?: string\n}\n\nexport interface RobotWithToken extends Robot {\n token: string\n}\n"],"names":[],"mappings":"AAqCA,WAEC"}
1
+ {"version":3,"sources":["../../../src/actions/tokens/types.ts"],"sourcesContent":["export interface Role {\n appliesToRobots: boolean\n appliesToUsers: boolean\n description: string\n isCustom: boolean\n name: string\n resourceId: string\n resourceType: string\n title: string\n}\n\nexport type SelectedTokenRole = Pick<Role, 'name' | 'title'>\n\nexport interface Membership {\n resourceId: string\n resourceType: string\n roleNames: string[]\n\n addedAt?: string\n lastSeenAt?: string | null\n resourceUserId?: string | null\n}\n\nexport interface Robot {\n createdAt: string\n id: string\n label: string\n memberships: Membership[]\n\n expiresAt?: string | null\n managedBy?: {\n resourceId: string\n resourceType: string\n }\n tokenId?: string\n}\n\nexport interface RobotWithToken extends Robot {\n token: string\n}\n\nexport interface TokenScope {\n resourceId: string\n resourceType: 'organization' | 'project'\n}\n"],"names":[],"mappings":"AAyCA,WAGC"}
@@ -3,10 +3,11 @@ import { exitCodes, SanityCommand, subdebug } from '@sanity/cli-core';
3
3
  import { requiredWhenUnattended } from '@sanity/cli-core/flags';
4
4
  import { confirm, select } from '@sanity/cli-core/ux';
5
5
  import { ClientError } from '@sanity/client';
6
- import { promptForProject } from '../../prompts/promptForProject.js';
7
- import { deleteToken, getProjectMembership, getTokens } from '../../services/tokens.js';
6
+ import { formatTokenAccess } from '../../actions/tokens/formatTokenAccess.js';
7
+ import { deleteToken, getTokens } from '../../services/tokens.js';
8
8
  import { formatCliErrorMessages } from '../../util/formatCliErrorMessages.js';
9
- import { getProjectIdFlag } from '../../util/sharedFlags.js';
9
+ import { resolveTokenScope } from '../../util/resolveTokenScope.js';
10
+ import { getOrganizationFlag, getProjectIdFlag } from '../../util/sharedFlags.js';
10
11
  const deleteTokenDebug = subdebug('tokens:delete');
11
12
  export class DeleteTokensCommand extends SanityCommand {
12
13
  static args = {
@@ -15,8 +16,16 @@ export class DeleteTokensCommand extends SanityCommand {
15
16
  required: false
16
17
  })
17
18
  };
18
- static description = 'Delete an API token from the project';
19
+ static description = 'Delete an API token from a project or organization';
19
20
  static examples = [
21
+ {
22
+ command: '<%= config.bin %> <%= command.id %> --organization org123',
23
+ description: 'Interactively select and delete an organization token'
24
+ },
25
+ {
26
+ command: '<%= config.bin %> <%= command.id %> silJ2lFmK6dONB --organization org123 --yes',
27
+ description: 'Delete an organization token without confirmation'
28
+ },
20
29
  {
21
30
  command: '<%= config.bin %> <%= command.id %>',
22
31
  description: 'Interactively select and delete a token'
@@ -35,6 +44,13 @@ export class DeleteTokensCommand extends SanityCommand {
35
44
  }
36
45
  ];
37
46
  static flags = {
47
+ ...getOrganizationFlag({
48
+ description: 'Delete a token managed by this organization',
49
+ exclusive: [
50
+ 'project-id'
51
+ ],
52
+ semantics: 'specify'
53
+ }),
38
54
  ...getProjectIdFlag({
39
55
  description: 'Project ID to delete token from',
40
56
  semantics: 'override'
@@ -47,7 +63,6 @@ export class DeleteTokensCommand extends SanityCommand {
47
63
  static hiddenAliases = [
48
64
  'token:delete'
49
65
  ];
50
- projectId;
51
66
  async run() {
52
67
  const { args, flags } = await this.parse(DeleteTokensCommand);
53
68
  const skipConfirmation = flags.yes;
@@ -64,23 +79,16 @@ export class DeleteTokensCommand extends SanityCommand {
64
79
  });
65
80
  }
66
81
  }
67
- // Ensure we have project context
68
- const projectId = await this.getProjectId({
69
- fallback: ()=>promptForProject({
70
- requiredPermissions: [
71
- {
72
- grant: 'delete',
73
- permission: 'sanity.project.tokens'
74
- }
75
- ]
76
- })
82
+ const scope = await resolveTokenScope({
83
+ getProjectId: this.getProjectId.bind(this),
84
+ grant: 'delete',
85
+ organization: flags.organization
77
86
  });
78
- this.projectId = projectId;
79
- const tokenId = givenTokenId || await this.getTokenIdFromList();
87
+ const tokenId = givenTokenId || await this.getTokenIdFromList(scope);
80
88
  if (!skipConfirmation) {
81
89
  const confirmed = await confirm({
82
90
  default: false,
83
- message: `Delete API token "${tokenId}"?`
91
+ message: scope.resourceType === 'organization' ? `Delete API token "${tokenId}" from organization "${scope.resourceId}"? This revokes all of its organization and project access.` : `Delete API token "${tokenId}"?`
84
92
  });
85
93
  if (!confirmed) {
86
94
  this.log('API token not deleted');
@@ -88,10 +96,7 @@ export class DeleteTokensCommand extends SanityCommand {
88
96
  }
89
97
  }
90
98
  try {
91
- await deleteToken({
92
- projectId: this.projectId,
93
- tokenId
94
- });
99
+ await deleteToken(scope, tokenId);
95
100
  this.log('API token deleted');
96
101
  } catch (error) {
97
102
  if (error instanceof ClientError && error.response.statusCode === 404) {
@@ -106,26 +111,29 @@ export class DeleteTokensCommand extends SanityCommand {
106
111
  });
107
112
  }
108
113
  }
109
- async getTokenIdFromList() {
114
+ async getTokenIdFromList(scope) {
110
115
  let tokens;
111
116
  try {
112
- tokens = await getTokens(this.projectId);
117
+ tokens = await getTokens(scope);
113
118
  } catch (error) {
114
119
  const err = error;
115
- deleteTokenDebug(`Error fetching tokens for project ${this.projectId}`, err);
116
- this.error(`Could not list API tokens:\n${err.message}\nCheck the project ID and your access permissions, then try again.`, {
120
+ deleteTokenDebug(`Error fetching tokens for ${scope.resourceType} ${scope.resourceId}`, err);
121
+ this.error(`Could not list API tokens:\n${err.message}\nCheck the ${scope.resourceType} ID and your access permissions, then try again.`, {
117
122
  exit: exitCodes.RUNTIME_ERROR
118
123
  });
119
124
  }
120
125
  if (tokens.length === 0) {
121
- this.error('No API tokens found for this project.', {
126
+ this.error(`No API tokens found for this ${scope.resourceType}.`, {
122
127
  exit: exitCodes.RUNTIME_ERROR
123
128
  });
124
129
  }
125
- const choices = tokens.map((token)=>({
126
- name: `${token.label} (${getProjectMembership(token, this.projectId)?.roleNames.join(', ') ?? ''})`,
130
+ const choices = tokens.map((token)=>{
131
+ const access = formatTokenAccess(token, scope);
132
+ return {
133
+ name: `${token.label} [${token.id}] (${access})`,
127
134
  value: token.id
128
- }));
135
+ };
136
+ });
129
137
  return select({
130
138
  choices,
131
139
  message: 'Select token to delete:'
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/commands/tokens/delete.ts"],"sourcesContent":["import {Args, Flags} from '@oclif/core'\nimport {exitCodes, SanityCommand, subdebug} from '@sanity/cli-core'\nimport {requiredWhenUnattended} from '@sanity/cli-core/flags'\nimport {confirm, select} from '@sanity/cli-core/ux'\nimport {ClientError} from '@sanity/client'\n\nimport {promptForProject} from '../../prompts/promptForProject.js'\nimport {deleteToken, getProjectMembership, getTokens} from '../../services/tokens.js'\nimport {formatCliErrorMessages} from '../../util/formatCliErrorMessages.js'\nimport {getProjectIdFlag} from '../../util/sharedFlags.js'\n\nconst deleteTokenDebug = subdebug('tokens:delete')\n\nexport class DeleteTokensCommand extends SanityCommand<typeof DeleteTokensCommand> {\n static override args = {\n tokenId: Args.string({\n description: 'Token ID to delete (will prompt if not provided)',\n required: false,\n }),\n }\n\n static override description = 'Delete an API token from the project'\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'Interactively select and delete a token',\n },\n {\n command: '<%= config.bin %> <%= command.id %> silJ2lFmK6dONB',\n description: 'Delete a specific token by ID',\n },\n {\n command: '<%= config.bin %> <%= command.id %> silJ2lFmK6dONB --yes',\n description: 'Delete a specific token without confirmation prompt',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --project-id abc123',\n description: 'Delete a token from a specific project',\n },\n ]\n\n static override flags = {\n ...getProjectIdFlag({\n description: 'Project ID to delete token from',\n semantics: 'override',\n }),\n yes: requiredWhenUnattended(\n Flags.boolean({\n char: 'y',\n description: 'Skip confirmation prompt (unattended mode)',\n }),\n ),\n }\n\n static override hiddenAliases: string[] = ['token:delete']\n\n private projectId!: string\n\n public async run(): Promise<void> {\n const {args, flags} = await this.parse(DeleteTokensCommand)\n\n const skipConfirmation = flags.yes\n const unattended = this.isUnattended()\n const {tokenId: givenTokenId} = args\n\n if (unattended) {\n const errors: string[] = []\n\n if (!givenTokenId) {\n errors.push('Token ID is required. Pass it as the `<tokenId>` argument.')\n }\n if (errors.length > 0) {\n this.error(formatCliErrorMessages(errors), {\n exit: exitCodes.USAGE_ERROR,\n })\n }\n }\n\n // Ensure we have project context\n const projectId = await this.getProjectId({\n fallback: () =>\n promptForProject({\n requiredPermissions: [{grant: 'delete', permission: 'sanity.project.tokens'}],\n }),\n })\n\n this.projectId = projectId\n\n const tokenId = givenTokenId || (await this.getTokenIdFromList())\n\n if (!skipConfirmation) {\n const confirmed = await confirm({\n default: false,\n message: `Delete API token \"${tokenId}\"?`,\n })\n\n if (!confirmed) {\n this.log('API token not deleted')\n this.exit(exitCodes.USER_ABORT)\n }\n }\n\n try {\n await deleteToken({\n projectId: this.projectId,\n tokenId,\n })\n\n this.log('API token deleted')\n } catch (error) {\n if (error instanceof ClientError && error.response.statusCode === 404) {\n this.error(`Token with ID \"${tokenId}\" not found`, {exit: exitCodes.RUNTIME_ERROR})\n }\n\n const err = error as Error\n deleteTokenDebug(`Error deleting token`, err)\n this.error(`Token deletion failed:\\n${err.message}`, {exit: exitCodes.RUNTIME_ERROR})\n }\n }\n\n private async getTokenIdFromList() {\n let tokens: Awaited<ReturnType<typeof getTokens>>\n try {\n tokens = await getTokens(this.projectId)\n } catch (error) {\n const err = error as Error\n deleteTokenDebug(`Error fetching tokens for project ${this.projectId}`, err)\n this.error(\n `Could not list API tokens:\\n${err.message}\\nCheck the project ID and your access permissions, then try again.`,\n {exit: exitCodes.RUNTIME_ERROR},\n )\n }\n\n if (tokens.length === 0) {\n this.error('No API tokens found for this project.', {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n\n const choices = tokens.map((token) => ({\n name: `${token.label} (${getProjectMembership(token, this.projectId)?.roleNames.join(', ') ?? ''})`,\n value: token.id,\n }))\n\n return select({\n choices,\n message: 'Select token to delete:',\n })\n }\n}\n"],"names":["Args","Flags","exitCodes","SanityCommand","subdebug","requiredWhenUnattended","confirm","select","ClientError","promptForProject","deleteToken","getProjectMembership","getTokens","formatCliErrorMessages","getProjectIdFlag","deleteTokenDebug","DeleteTokensCommand","args","tokenId","string","description","required","examples","command","flags","semantics","yes","boolean","char","hiddenAliases","projectId","run","parse","skipConfirmation","unattended","isUnattended","givenTokenId","errors","push","length","error","exit","USAGE_ERROR","getProjectId","fallback","requiredPermissions","grant","permission","getTokenIdFromList","confirmed","default","message","log","USER_ABORT","response","statusCode","RUNTIME_ERROR","err","tokens","choices","map","token","name","label","roleNames","join","value","id"],"mappings":"AAAA,SAAQA,IAAI,EAAEC,KAAK,QAAO,cAAa;AACvC,SAAQC,SAAS,EAAEC,aAAa,EAAEC,QAAQ,QAAO,mBAAkB;AACnE,SAAQC,sBAAsB,QAAO,yBAAwB;AAC7D,SAAQC,OAAO,EAAEC,MAAM,QAAO,sBAAqB;AACnD,SAAQC,WAAW,QAAO,iBAAgB;AAE1C,SAAQC,gBAAgB,QAAO,oCAAmC;AAClE,SAAQC,WAAW,EAAEC,oBAAoB,EAAEC,SAAS,QAAO,2BAA0B;AACrF,SAAQC,sBAAsB,QAAO,uCAAsC;AAC3E,SAAQC,gBAAgB,QAAO,4BAA2B;AAE1D,MAAMC,mBAAmBX,SAAS;AAElC,OAAO,MAAMY,4BAA4Bb;IACvC,OAAgBc,OAAO;QACrBC,SAASlB,KAAKmB,MAAM,CAAC;YACnBC,aAAa;YACbC,UAAU;QACZ;IACF,EAAC;IAED,OAAgBD,cAAc,uCAAsC;IAEpE,OAAgBE,WAAW;QACzB;YACEC,SAAS;YACTH,aAAa;QACf;QACA;YACEG,SAAS;YACTH,aAAa;QACf;QACA;YACEG,SAAS;YACTH,aAAa;QACf;QACA;YACEG,SAAS;YACTH,aAAa;QACf;KACD,CAAA;IAED,OAAgBI,QAAQ;QACtB,GAAGV,iBAAiB;YAClBM,aAAa;YACbK,WAAW;QACb,EAAE;QACFC,KAAKrB,uBACHJ,MAAM0B,OAAO,CAAC;YACZC,MAAM;YACNR,aAAa;QACf;IAEJ,EAAC;IAED,OAAgBS,gBAA0B;QAAC;KAAe,CAAA;IAElDC,UAAkB;IAE1B,MAAaC,MAAqB;QAChC,MAAM,EAACd,IAAI,EAAEO,KAAK,EAAC,GAAG,MAAM,IAAI,CAACQ,KAAK,CAAChB;QAEvC,MAAMiB,mBAAmBT,MAAME,GAAG;QAClC,MAAMQ,aAAa,IAAI,CAACC,YAAY;QACpC,MAAM,EAACjB,SAASkB,YAAY,EAAC,GAAGnB;QAEhC,IAAIiB,YAAY;YACd,MAAMG,SAAmB,EAAE;YAE3B,IAAI,CAACD,cAAc;gBACjBC,OAAOC,IAAI,CAAC;YACd;YACA,IAAID,OAAOE,MAAM,GAAG,GAAG;gBACrB,IAAI,CAACC,KAAK,CAAC3B,uBAAuBwB,SAAS;oBACzCI,MAAMvC,UAAUwC,WAAW;gBAC7B;YACF;QACF;QAEA,iCAAiC;QACjC,MAAMZ,YAAY,MAAM,IAAI,CAACa,YAAY,CAAC;YACxCC,UAAU,IACRnC,iBAAiB;oBACfoC,qBAAqB;wBAAC;4BAACC,OAAO;4BAAUC,YAAY;wBAAuB;qBAAE;gBAC/E;QACJ;QAEA,IAAI,CAACjB,SAAS,GAAGA;QAEjB,MAAMZ,UAAUkB,gBAAiB,MAAM,IAAI,CAACY,kBAAkB;QAE9D,IAAI,CAACf,kBAAkB;YACrB,MAAMgB,YAAY,MAAM3C,QAAQ;gBAC9B4C,SAAS;gBACTC,SAAS,CAAC,kBAAkB,EAAEjC,QAAQ,EAAE,CAAC;YAC3C;YAEA,IAAI,CAAC+B,WAAW;gBACd,IAAI,CAACG,GAAG,CAAC;gBACT,IAAI,CAACX,IAAI,CAACvC,UAAUmD,UAAU;YAChC;QACF;QAEA,IAAI;YACF,MAAM3C,YAAY;gBAChBoB,WAAW,IAAI,CAACA,SAAS;gBACzBZ;YACF;YAEA,IAAI,CAACkC,GAAG,CAAC;QACX,EAAE,OAAOZ,OAAO;YACd,IAAIA,iBAAiBhC,eAAegC,MAAMc,QAAQ,CAACC,UAAU,KAAK,KAAK;gBACrE,IAAI,CAACf,KAAK,CAAC,CAAC,eAAe,EAAEtB,QAAQ,WAAW,CAAC,EAAE;oBAACuB,MAAMvC,UAAUsD,aAAa;gBAAA;YACnF;YAEA,MAAMC,MAAMjB;YACZzB,iBAAiB,CAAC,oBAAoB,CAAC,EAAE0C;YACzC,IAAI,CAACjB,KAAK,CAAC,CAAC,wBAAwB,EAAEiB,IAAIN,OAAO,EAAE,EAAE;gBAACV,MAAMvC,UAAUsD,aAAa;YAAA;QACrF;IACF;IAEA,MAAcR,qBAAqB;QACjC,IAAIU;QACJ,IAAI;YACFA,SAAS,MAAM9C,UAAU,IAAI,CAACkB,SAAS;QACzC,EAAE,OAAOU,OAAO;YACd,MAAMiB,MAAMjB;YACZzB,iBAAiB,CAAC,kCAAkC,EAAE,IAAI,CAACe,SAAS,EAAE,EAAE2B;YACxE,IAAI,CAACjB,KAAK,CACR,CAAC,4BAA4B,EAAEiB,IAAIN,OAAO,CAAC,mEAAmE,CAAC,EAC/G;gBAACV,MAAMvC,UAAUsD,aAAa;YAAA;QAElC;QAEA,IAAIE,OAAOnB,MAAM,KAAK,GAAG;YACvB,IAAI,CAACC,KAAK,CAAC,yCAAyC;gBAClDC,MAAMvC,UAAUsD,aAAa;YAC/B;QACF;QAEA,MAAMG,UAAUD,OAAOE,GAAG,CAAC,CAACC,QAAW,CAAA;gBACrCC,MAAM,GAAGD,MAAME,KAAK,CAAC,EAAE,EAAEpD,qBAAqBkD,OAAO,IAAI,CAAC/B,SAAS,GAAGkC,UAAUC,KAAK,SAAS,GAAG,CAAC,CAAC;gBACnGC,OAAOL,MAAMM,EAAE;YACjB,CAAA;QAEA,OAAO5D,OAAO;YACZoD;YACAR,SAAS;QACX;IACF;AACF"}
1
+ {"version":3,"sources":["../../../src/commands/tokens/delete.ts"],"sourcesContent":["import {Args, Flags} from '@oclif/core'\nimport {exitCodes, SanityCommand, subdebug} from '@sanity/cli-core'\nimport {requiredWhenUnattended} from '@sanity/cli-core/flags'\nimport {confirm, select} from '@sanity/cli-core/ux'\nimport {ClientError} from '@sanity/client'\n\nimport {formatTokenAccess} from '../../actions/tokens/formatTokenAccess.js'\nimport {type TokenScope} from '../../actions/tokens/types.js'\nimport {deleteToken, getTokens} from '../../services/tokens.js'\nimport {formatCliErrorMessages} from '../../util/formatCliErrorMessages.js'\nimport {resolveTokenScope} from '../../util/resolveTokenScope.js'\nimport {getOrganizationFlag, getProjectIdFlag} from '../../util/sharedFlags.js'\n\nconst deleteTokenDebug = subdebug('tokens:delete')\n\nexport class DeleteTokensCommand extends SanityCommand<typeof DeleteTokensCommand> {\n static override args = {\n tokenId: Args.string({\n description: 'Token ID to delete (will prompt if not provided)',\n required: false,\n }),\n }\n\n static override description = 'Delete an API token from a project or organization'\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %> --organization org123',\n description: 'Interactively select and delete an organization token',\n },\n {\n command: '<%= config.bin %> <%= command.id %> silJ2lFmK6dONB --organization org123 --yes',\n description: 'Delete an organization token without confirmation',\n },\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'Interactively select and delete a token',\n },\n {\n command: '<%= config.bin %> <%= command.id %> silJ2lFmK6dONB',\n description: 'Delete a specific token by ID',\n },\n {\n command: '<%= config.bin %> <%= command.id %> silJ2lFmK6dONB --yes',\n description: 'Delete a specific token without confirmation prompt',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --project-id abc123',\n description: 'Delete a token from a specific project',\n },\n ]\n\n static override flags = {\n ...getOrganizationFlag({\n description: 'Delete a token managed by this organization',\n exclusive: ['project-id'],\n semantics: 'specify',\n }),\n ...getProjectIdFlag({\n description: 'Project ID to delete token from',\n semantics: 'override',\n }),\n yes: requiredWhenUnattended(\n Flags.boolean({\n char: 'y',\n description: 'Skip confirmation prompt (unattended mode)',\n }),\n ),\n }\n\n static override hiddenAliases: string[] = ['token:delete']\n\n public async run(): Promise<void> {\n const {args, flags} = await this.parse(DeleteTokensCommand)\n\n const skipConfirmation = flags.yes\n const unattended = this.isUnattended()\n const {tokenId: givenTokenId} = args\n\n if (unattended) {\n const errors: string[] = []\n\n if (!givenTokenId) {\n errors.push('Token ID is required. Pass it as the `<tokenId>` argument.')\n }\n if (errors.length > 0) {\n this.error(formatCliErrorMessages(errors), {\n exit: exitCodes.USAGE_ERROR,\n })\n }\n }\n\n const scope = await resolveTokenScope({\n getProjectId: this.getProjectId.bind(this),\n grant: 'delete',\n organization: flags.organization,\n })\n\n const tokenId = givenTokenId || (await this.getTokenIdFromList(scope))\n\n if (!skipConfirmation) {\n const confirmed = await confirm({\n default: false,\n message:\n scope.resourceType === 'organization'\n ? `Delete API token \"${tokenId}\" from organization \"${scope.resourceId}\"? This revokes all of its organization and project access.`\n : `Delete API token \"${tokenId}\"?`,\n })\n\n if (!confirmed) {\n this.log('API token not deleted')\n this.exit(exitCodes.USER_ABORT)\n }\n }\n\n try {\n await deleteToken(scope, tokenId)\n\n this.log('API token deleted')\n } catch (error) {\n if (error instanceof ClientError && error.response.statusCode === 404) {\n this.error(`Token with ID \"${tokenId}\" not found`, {exit: exitCodes.RUNTIME_ERROR})\n }\n\n const err = error as Error\n deleteTokenDebug(`Error deleting token`, err)\n this.error(`Token deletion failed:\\n${err.message}`, {exit: exitCodes.RUNTIME_ERROR})\n }\n }\n\n private async getTokenIdFromList(scope: TokenScope) {\n let tokens: Awaited<ReturnType<typeof getTokens>>\n try {\n tokens = await getTokens(scope)\n } catch (error) {\n const err = error as Error\n deleteTokenDebug(`Error fetching tokens for ${scope.resourceType} ${scope.resourceId}`, err)\n this.error(\n `Could not list API tokens:\\n${err.message}\\nCheck the ${scope.resourceType} ID and your access permissions, then try again.`,\n {exit: exitCodes.RUNTIME_ERROR},\n )\n }\n\n if (tokens.length === 0) {\n this.error(`No API tokens found for this ${scope.resourceType}.`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n\n const choices = tokens.map((token) => {\n const access = formatTokenAccess(token, scope)\n return {name: `${token.label} [${token.id}] (${access})`, value: token.id}\n })\n\n return select({\n choices,\n message: 'Select token to delete:',\n })\n }\n}\n"],"names":["Args","Flags","exitCodes","SanityCommand","subdebug","requiredWhenUnattended","confirm","select","ClientError","formatTokenAccess","deleteToken","getTokens","formatCliErrorMessages","resolveTokenScope","getOrganizationFlag","getProjectIdFlag","deleteTokenDebug","DeleteTokensCommand","args","tokenId","string","description","required","examples","command","flags","exclusive","semantics","yes","boolean","char","hiddenAliases","run","parse","skipConfirmation","unattended","isUnattended","givenTokenId","errors","push","length","error","exit","USAGE_ERROR","scope","getProjectId","bind","grant","organization","getTokenIdFromList","confirmed","default","message","resourceType","resourceId","log","USER_ABORT","response","statusCode","RUNTIME_ERROR","err","tokens","choices","map","token","access","name","label","id","value"],"mappings":"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"}
@@ -1,14 +1,19 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { exitCodes, SanityCommand, subdebug } from '@sanity/cli-core';
3
3
  import { getErrorMessage } from '@sanity/cli-core/errors';
4
- import { promptForProject } from '../../prompts/promptForProject.js';
5
- import { getProjectMembership, getTokens } from '../../services/tokens.js';
4
+ import { formatTokenAccess } from '../../actions/tokens/formatTokenAccess.js';
5
+ import { getTokens } from '../../services/tokens.js';
6
+ import { resolveTokenScope } from '../../util/resolveTokenScope.js';
6
7
  import { Table } from '../../util/responsiveTable.js';
7
- import { getProjectIdFlag } from '../../util/sharedFlags.js';
8
+ import { getOrganizationFlag, getProjectIdFlag } from '../../util/sharedFlags.js';
8
9
  const listTokenDebug = subdebug('tokens:list');
9
10
  export class TokensListCommand extends SanityCommand {
10
- static description = 'List API tokens for the project';
11
+ static description = 'List API tokens for a project or organization';
11
12
  static examples = [
13
+ {
14
+ command: '<%= config.bin %> <%= command.id %> --organization org123',
15
+ description: 'List tokens managed by an organization'
16
+ },
12
17
  {
13
18
  command: '<%= config.bin %> <%= command.id %>',
14
19
  description: 'List tokens for the project'
@@ -23,6 +28,13 @@ export class TokensListCommand extends SanityCommand {
23
28
  }
24
29
  ];
25
30
  static flags = {
31
+ ...getOrganizationFlag({
32
+ description: 'List tokens managed by this organization',
33
+ exclusive: [
34
+ 'project-id'
35
+ ],
36
+ semantics: 'specify'
37
+ }),
26
38
  ...getProjectIdFlag({
27
39
  description: 'Project ID to list tokens for',
28
40
  semantics: 'override'
@@ -39,23 +51,17 @@ export class TokensListCommand extends SanityCommand {
39
51
  const { flags } = await this.parse(TokensListCommand);
40
52
  const { json } = flags;
41
53
  const outputJson = json ?? false;
42
- // Ensure we have project context
43
- const projectId = await this.getProjectId({
44
- fallback: ()=>promptForProject({
45
- requiredPermissions: [
46
- {
47
- grant: 'read',
48
- permission: 'sanity.project.tokens'
49
- }
50
- ]
51
- })
54
+ const scope = await resolveTokenScope({
55
+ getProjectId: this.getProjectId.bind(this),
56
+ grant: 'read',
57
+ organization: flags.organization
52
58
  });
53
59
  let tokens;
54
60
  try {
55
- tokens = await getTokens(projectId);
61
+ tokens = await getTokens(scope);
56
62
  } catch (error) {
57
63
  const message = getErrorMessage(error);
58
- listTokenDebug(`Error fetching tokens for project ${projectId}`, error);
64
+ listTokenDebug(`Error fetching tokens for ${scope.resourceType} ${scope.resourceId}`, error);
59
65
  this.error(`Token list retrieval failed:\n${message}`, {
60
66
  exit: exitCodes.RUNTIME_ERROR
61
67
  });
@@ -65,7 +71,7 @@ export class TokensListCommand extends SanityCommand {
65
71
  return;
66
72
  }
67
73
  if (tokens.length === 0) {
68
- this.log('No API tokens found for this project.');
74
+ this.log(`No API tokens found for this ${scope.resourceType}.`);
69
75
  return;
70
76
  }
71
77
  const table = new Table({
@@ -83,7 +89,7 @@ export class TokensListCommand extends SanityCommand {
83
89
  {
84
90
  alignment: 'left',
85
91
  name: 'roles',
86
- title: 'Roles'
92
+ title: scope.resourceType === 'organization' ? 'Access' : 'Roles'
87
93
  },
88
94
  {
89
95
  alignment: 'left',
@@ -94,7 +100,7 @@ export class TokensListCommand extends SanityCommand {
94
100
  title: `Found ${tokens.length} API tokens`
95
101
  });
96
102
  for (const token of tokens){
97
- const roles = getProjectMembership(token, projectId)?.roleNames.join(', ') || 'No roles';
103
+ const roles = formatTokenAccess(token, scope);
98
104
  table.addRow({
99
105
  expires: token.expiresAt ? token.expiresAt.slice(0, 10) : 'Never',
100
106
  id: token.id,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/commands/tokens/list.ts"],"sourcesContent":["import {Flags} from '@oclif/core'\nimport {exitCodes, SanityCommand, subdebug} from '@sanity/cli-core'\nimport {getErrorMessage} from '@sanity/cli-core/errors'\n\nimport {type Robot} from '../../actions/tokens/types.js'\nimport {promptForProject} from '../../prompts/promptForProject.js'\nimport {getProjectMembership, getTokens} from '../../services/tokens.js'\nimport {Table} from '../../util/responsiveTable.js'\nimport {getProjectIdFlag} from '../../util/sharedFlags.js'\n\nconst listTokenDebug = subdebug('tokens:list')\n\nexport class TokensListCommand extends SanityCommand<typeof TokensListCommand> {\n static override description = 'List API tokens for the project'\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'List tokens for the project',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --json',\n description: 'List tokens in JSON format',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --project-id abc123',\n description: 'List tokens for a specific project',\n },\n ]\n static override flags = {\n ...getProjectIdFlag({\n description: 'Project ID to list tokens for',\n semantics: 'override',\n }),\n json: Flags.boolean({\n default: false,\n description: 'Output tokens in JSON format',\n }),\n }\n\n static override hiddenAliases: string[] = ['token:list']\n\n public async run(): Promise<void> {\n const {flags} = await this.parse(TokensListCommand)\n const {json} = flags\n const outputJson = json ?? false\n\n // Ensure we have project context\n const projectId = await this.getProjectId({\n fallback: () =>\n promptForProject({\n requiredPermissions: [{grant: 'read', permission: 'sanity.project.tokens'}],\n }),\n })\n\n let tokens: Robot[]\n try {\n tokens = await getTokens(projectId)\n } catch (error) {\n const message = getErrorMessage(error)\n listTokenDebug(`Error fetching tokens for project ${projectId}`, error)\n this.error(`Token list retrieval failed:\\n${message}`, {exit: exitCodes.RUNTIME_ERROR})\n }\n\n if (outputJson) {\n this.log(JSON.stringify(tokens, null, 2))\n return\n }\n\n if (tokens.length === 0) {\n this.log('No API tokens found for this project.')\n return\n }\n\n const table = new Table({\n columns: [\n {alignment: 'left', name: 'label', title: 'Label'},\n {alignment: 'left', name: 'id', title: 'ID'},\n {alignment: 'left', name: 'roles', title: 'Roles'},\n {alignment: 'left', name: 'expires', title: 'Expires'},\n ],\n title: `Found ${tokens.length} API tokens`,\n })\n\n for (const token of tokens) {\n const roles = getProjectMembership(token, projectId)?.roleNames.join(', ') || 'No roles'\n\n table.addRow({\n expires: token.expiresAt ? token.expiresAt.slice(0, 10) : 'Never',\n id: token.id,\n label: token.label,\n roles,\n })\n }\n\n this.log(table.render())\n }\n}\n"],"names":["Flags","exitCodes","SanityCommand","subdebug","getErrorMessage","promptForProject","getProjectMembership","getTokens","Table","getProjectIdFlag","listTokenDebug","TokensListCommand","description","examples","command","flags","semantics","json","boolean","default","hiddenAliases","run","parse","outputJson","projectId","getProjectId","fallback","requiredPermissions","grant","permission","tokens","error","message","exit","RUNTIME_ERROR","log","JSON","stringify","length","table","columns","alignment","name","title","token","roles","roleNames","join","addRow","expires","expiresAt","slice","id","label","render"],"mappings":"AAAA,SAAQA,KAAK,QAAO,cAAa;AACjC,SAAQC,SAAS,EAAEC,aAAa,EAAEC,QAAQ,QAAO,mBAAkB;AACnE,SAAQC,eAAe,QAAO,0BAAyB;AAGvD,SAAQC,gBAAgB,QAAO,oCAAmC;AAClE,SAAQC,oBAAoB,EAAEC,SAAS,QAAO,2BAA0B;AACxE,SAAQC,KAAK,QAAO,gCAA+B;AACnD,SAAQC,gBAAgB,QAAO,4BAA2B;AAE1D,MAAMC,iBAAiBP,SAAS;AAEhC,OAAO,MAAMQ,0BAA0BT;IACrC,OAAgBU,cAAc,kCAAiC;IAC/D,OAAgBC,WAAW;QACzB;YACEC,SAAS;YACTF,aAAa;QACf;QACA;YACEE,SAAS;YACTF,aAAa;QACf;QACA;YACEE,SAAS;YACTF,aAAa;QACf;KACD,CAAA;IACD,OAAgBG,QAAQ;QACtB,GAAGN,iBAAiB;YAClBG,aAAa;YACbI,WAAW;QACb,EAAE;QACFC,MAAMjB,MAAMkB,OAAO,CAAC;YAClBC,SAAS;YACTP,aAAa;QACf;IACF,EAAC;IAED,OAAgBQ,gBAA0B;QAAC;KAAa,CAAA;IAExD,MAAaC,MAAqB;QAChC,MAAM,EAACN,KAAK,EAAC,GAAG,MAAM,IAAI,CAACO,KAAK,CAACX;QACjC,MAAM,EAACM,IAAI,EAAC,GAAGF;QACf,MAAMQ,aAAaN,QAAQ;QAE3B,iCAAiC;QACjC,MAAMO,YAAY,MAAM,IAAI,CAACC,YAAY,CAAC;YACxCC,UAAU,IACRrB,iBAAiB;oBACfsB,qBAAqB;wBAAC;4BAACC,OAAO;4BAAQC,YAAY;wBAAuB;qBAAE;gBAC7E;QACJ;QAEA,IAAIC;QACJ,IAAI;YACFA,SAAS,MAAMvB,UAAUiB;QAC3B,EAAE,OAAOO,OAAO;YACd,MAAMC,UAAU5B,gBAAgB2B;YAChCrB,eAAe,CAAC,kCAAkC,EAAEc,WAAW,EAAEO;YACjE,IAAI,CAACA,KAAK,CAAC,CAAC,8BAA8B,EAAEC,SAAS,EAAE;gBAACC,MAAMhC,UAAUiC,aAAa;YAAA;QACvF;QAEA,IAAIX,YAAY;YACd,IAAI,CAACY,GAAG,CAACC,KAAKC,SAAS,CAACP,QAAQ,MAAM;YACtC;QACF;QAEA,IAAIA,OAAOQ,MAAM,KAAK,GAAG;YACvB,IAAI,CAACH,GAAG,CAAC;YACT;QACF;QAEA,MAAMI,QAAQ,IAAI/B,MAAM;YACtBgC,SAAS;gBACP;oBAACC,WAAW;oBAAQC,MAAM;oBAASC,OAAO;gBAAO;gBACjD;oBAACF,WAAW;oBAAQC,MAAM;oBAAMC,OAAO;gBAAI;gBAC3C;oBAACF,WAAW;oBAAQC,MAAM;oBAASC,OAAO;gBAAO;gBACjD;oBAACF,WAAW;oBAAQC,MAAM;oBAAWC,OAAO;gBAAS;aACtD;YACDA,OAAO,CAAC,MAAM,EAAEb,OAAOQ,MAAM,CAAC,WAAW,CAAC;QAC5C;QAEA,KAAK,MAAMM,SAASd,OAAQ;YAC1B,MAAMe,QAAQvC,qBAAqBsC,OAAOpB,YAAYsB,UAAUC,KAAK,SAAS;YAE9ER,MAAMS,MAAM,CAAC;gBACXC,SAASL,MAAMM,SAAS,GAAGN,MAAMM,SAAS,CAACC,KAAK,CAAC,GAAG,MAAM;gBAC1DC,IAAIR,MAAMQ,EAAE;gBACZC,OAAOT,MAAMS,KAAK;gBAClBR;YACF;QACF;QAEA,IAAI,CAACV,GAAG,CAACI,MAAMe,MAAM;IACvB;AACF"}
1
+ {"version":3,"sources":["../../../src/commands/tokens/list.ts"],"sourcesContent":["import {Flags} from '@oclif/core'\nimport {exitCodes, SanityCommand, subdebug} from '@sanity/cli-core'\nimport {getErrorMessage} from '@sanity/cli-core/errors'\n\nimport {formatTokenAccess} from '../../actions/tokens/formatTokenAccess.js'\nimport {type Robot} from '../../actions/tokens/types.js'\nimport {getTokens} from '../../services/tokens.js'\nimport {resolveTokenScope} from '../../util/resolveTokenScope.js'\nimport {Table} from '../../util/responsiveTable.js'\nimport {getOrganizationFlag, getProjectIdFlag} from '../../util/sharedFlags.js'\n\nconst listTokenDebug = subdebug('tokens:list')\n\nexport class TokensListCommand extends SanityCommand<typeof TokensListCommand> {\n static override description = 'List API tokens for a project or organization'\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %> --organization org123',\n description: 'List tokens managed by an organization',\n },\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'List tokens for the project',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --json',\n description: 'List tokens in JSON format',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --project-id abc123',\n description: 'List tokens for a specific project',\n },\n ]\n static override flags = {\n ...getOrganizationFlag({\n description: 'List tokens managed by this organization',\n exclusive: ['project-id'],\n semantics: 'specify',\n }),\n ...getProjectIdFlag({\n description: 'Project ID to list tokens for',\n semantics: 'override',\n }),\n json: Flags.boolean({\n default: false,\n description: 'Output tokens in JSON format',\n }),\n }\n\n static override hiddenAliases: string[] = ['token:list']\n\n public async run(): Promise<void> {\n const {flags} = await this.parse(TokensListCommand)\n const {json} = flags\n const outputJson = json ?? false\n\n const scope = await resolveTokenScope({\n getProjectId: this.getProjectId.bind(this),\n grant: 'read',\n organization: flags.organization,\n })\n\n let tokens: Robot[]\n try {\n tokens = await getTokens(scope)\n } catch (error) {\n const message = getErrorMessage(error)\n listTokenDebug(`Error fetching tokens for ${scope.resourceType} ${scope.resourceId}`, error)\n this.error(`Token list retrieval failed:\\n${message}`, {exit: exitCodes.RUNTIME_ERROR})\n }\n\n if (outputJson) {\n this.log(JSON.stringify(tokens, null, 2))\n return\n }\n\n if (tokens.length === 0) {\n this.log(`No API tokens found for this ${scope.resourceType}.`)\n return\n }\n\n const table = new Table({\n columns: [\n {alignment: 'left', name: 'label', title: 'Label'},\n {alignment: 'left', name: 'id', title: 'ID'},\n {\n alignment: 'left',\n name: 'roles',\n title: scope.resourceType === 'organization' ? 'Access' : 'Roles',\n },\n {alignment: 'left', name: 'expires', title: 'Expires'},\n ],\n title: `Found ${tokens.length} API tokens`,\n })\n\n for (const token of tokens) {\n const roles = formatTokenAccess(token, scope)\n\n table.addRow({\n expires: token.expiresAt ? token.expiresAt.slice(0, 10) : 'Never',\n id: token.id,\n label: token.label,\n roles,\n })\n }\n\n this.log(table.render())\n }\n}\n"],"names":["Flags","exitCodes","SanityCommand","subdebug","getErrorMessage","formatTokenAccess","getTokens","resolveTokenScope","Table","getOrganizationFlag","getProjectIdFlag","listTokenDebug","TokensListCommand","description","examples","command","flags","exclusive","semantics","json","boolean","default","hiddenAliases","run","parse","outputJson","scope","getProjectId","bind","grant","organization","tokens","error","message","resourceType","resourceId","exit","RUNTIME_ERROR","log","JSON","stringify","length","table","columns","alignment","name","title","token","roles","addRow","expires","expiresAt","slice","id","label","render"],"mappings":"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"}
@@ -9,13 +9,16 @@ function getClient(token) {
9
9
  }
10
10
  });
11
11
  }
12
- async function fetchAllPages(client, url) {
12
+ async function fetchAllPages(client, url, query = {}) {
13
13
  const items = [];
14
14
  let cursor = null;
15
15
  do {
16
16
  const response = await client.request({
17
- query: cursor === null ? {} : {
18
- nextCursor: cursor
17
+ query: {
18
+ ...query,
19
+ ...cursor === null ? {} : {
20
+ nextCursor: cursor
21
+ }
19
22
  },
20
23
  url
21
24
  });
@@ -80,19 +83,26 @@ async function postRobot(url, { expiresAt, ...body }, query = {}) {
80
83
  const { organizationId, ...body } = options;
81
84
  return postRobot(`/access/organization/${organizationId}/robots`, body);
82
85
  }
83
- /**
84
- * Delete a token from a project
85
- * @param options - The options for deleting a token from a project; `tokenId`
86
- * is the id reported by `getTokens`/`createToken`
87
- * @returns A promise that resolves when the token is deleted
88
- *
89
- * @internal
90
- */ export async function deleteToken(options) {
91
- const { projectId, tokenId } = options;
86
+ function robotsUrl({ resourceId, resourceType }) {
87
+ return `/access/${resourceType}/${resourceId}/robots`;
88
+ }
89
+ function isManagedByOrganization(robot, organizationId) {
90
+ return robot.managedBy?.resourceType === 'organization' && robot.managedBy.resourceId === organizationId;
91
+ }
92
+ /** Delete a token from the requested scope. @internal */ export async function deleteToken(scope, tokenId) {
92
93
  const client = await getClient();
94
+ const url = `${robotsUrl(scope)}/${tokenId}`;
95
+ if (scope.resourceType === 'organization') {
96
+ const robot = await client.request({
97
+ url
98
+ });
99
+ if (!isManagedByOrganization(robot, scope.resourceId)) {
100
+ throw new Error(`Token "${tokenId}" is not managed by organization "${scope.resourceId}". Use tokens list --organization ${scope.resourceId} to find tokens managed by this organization.`);
101
+ }
102
+ }
93
103
  return client.request({
94
104
  method: 'DELETE',
95
- url: `/access/project/${projectId}/robots/${tokenId}`
105
+ url
96
106
  });
97
107
  }
98
108
  /**
@@ -110,16 +120,16 @@ async function postRobot(url, { expiresAt, ...body }, query = {}) {
110
120
  url: '/access/robots/me/rotate'
111
121
  });
112
122
  }
113
- /**
114
- * Get all tokens for a project. Tokens managed by an organization are
115
- * excluded: they cannot be managed at the project scope.
116
- * @param projectId - The project ID
117
- * @returns A promise that resolves to an array of robots
118
- *
119
- * @internal
120
- */ export async function getTokens(projectId) {
123
+ /** List tokens managed at the requested scope. @internal */ export async function getTokens(scope) {
121
124
  const client = await getClient();
122
- const robots = await fetchAllPages(client, `/access/project/${projectId}/robots`);
125
+ const query = scope.resourceType === 'organization' ? {
126
+ includeChildren: 'true',
127
+ limit: '500'
128
+ } : {};
129
+ const robots = await fetchAllPages(client, robotsUrl(scope), query);
130
+ if (scope.resourceType === 'organization') {
131
+ return robots.filter((robot)=>isManagedByOrganization(robot, scope.resourceId));
132
+ }
123
133
  return robots.filter((robot)=>robot.managedBy?.resourceType !== 'organization');
124
134
  }
125
135
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/services/tokens.ts"],"sourcesContent":["import {getGlobalCliClient} from '@sanity/cli-core'\nimport {type SanityClient} from '@sanity/client'\n\nimport {TOKENS_API_VERSION} from '../actions/tokens/constants.js'\nimport {\n type Membership,\n type Robot,\n type RobotWithToken,\n type Role,\n} from '../actions/tokens/types.js'\n\ninterface PaginatedResponse<T> {\n data: T[]\n nextCursor: string | null\n}\n\nfunction getClient(token?: string): Promise<SanityClient> {\n return getGlobalCliClient({\n apiVersion: TOKENS_API_VERSION,\n requireUser: true,\n ...(token === undefined ? {} : {token}),\n })\n}\n\nasync function fetchAllPages<T>(client: SanityClient, url: string): Promise<T[]> {\n const items: T[] = []\n let cursor: string | null = null\n\n do {\n const response: PaginatedResponse<T> = await client.request<PaginatedResponse<T>>({\n query: cursor === null ? {} : {nextCursor: cursor},\n url,\n })\n items.push(...response.data)\n cursor = response.nextCursor\n } while (cursor !== null)\n\n return items\n}\n\n/**\n * Get the membership granting a robot access to the given project\n * @param robot - The robot to inspect\n * @param projectId - The project ID\n * @returns The project membership, if any\n *\n * @internal\n */\nexport function getProjectMembership(robot: Robot, projectId: string): Membership | undefined {\n return robot.memberships.find(\n (membership) => membership.resourceType === 'project' && membership.resourceId === projectId,\n )\n}\n\ninterface RobotBody {\n label: string\n memberships: Membership[]\n\n expiresAt?: string\n}\n\nasync function postRobot(\n url: string,\n {expiresAt, ...body}: RobotBody,\n query: Record<string, string> = {},\n): Promise<RobotWithToken> {\n const client = await getClient()\n\n return client.request<RobotWithToken>({\n body: {...body, ...(expiresAt === undefined ? {} : {expiresAt})},\n method: 'POST',\n query,\n url,\n })\n}\n\ninterface CreateTokenOptions {\n label: string\n projectId: string\n roleNames: string[]\n\n expiresAt?: string\n sendNotification?: boolean\n}\n\n/**\n * Add a token to a project\n * @param options - The options for adding a token to a project\n * @returns A promise that resolves to the created robot, including its secret token\n *\n * @internal\n */\nexport async function createToken(options: CreateTokenOptions): Promise<RobotWithToken> {\n const {expiresAt, label, projectId, roleNames, sendNotification} = options\n\n return postRobot(\n `/access/project/${projectId}/robots`,\n {\n expiresAt,\n label,\n memberships: [{resourceId: projectId, resourceType: 'project', roleNames}],\n },\n sendNotification === false ? {sendNotification: 'false'} : {},\n )\n}\n\ninterface CreateOrganizationTokenOptions extends RobotBody {\n organizationId: string\n}\n\n/**\n * Create an organization token, which can hold organization permissions and roles on several projects\n * @param options - The options for the organization token\n * @returns A promise that resolves to the created robot, including its secret token\n *\n * @internal\n */\nexport async function createOrganizationToken(\n options: CreateOrganizationTokenOptions,\n): Promise<RobotWithToken> {\n const {organizationId, ...body} = options\n\n return postRobot(`/access/organization/${organizationId}/robots`, body)\n}\n\ninterface DeleteTokenOptions {\n projectId: string\n tokenId: string\n}\n\n/**\n * Delete a token from a project\n * @param options - The options for deleting a token from a project; `tokenId`\n * is the id reported by `getTokens`/`createToken`\n * @returns A promise that resolves when the token is deleted\n *\n * @internal\n */\nexport async function deleteToken(options: DeleteTokenOptions): Promise<void> {\n const {projectId, tokenId} = options\n\n const client = await getClient()\n\n return client.request({\n method: 'DELETE',\n url: `/access/project/${projectId}/robots/${tokenId}`,\n })\n}\n\n/**\n * Rotate a robot token, replacing its secret while preserving the robot,\n * its roles, and the token's expiry. The request authenticates as the robot\n * itself; the previous secret is revoked immediately.\n * @param token - The current robot token secret\n * @returns A promise that resolves to the robot with its new secret token\n *\n * @internal\n */\nexport async function rotateToken(token: string): Promise<RobotWithToken> {\n const client = await getClient(token)\n\n return client.request<RobotWithToken>({\n method: 'POST',\n url: '/access/robots/me/rotate',\n })\n}\n\n/**\n * Get all tokens for a project. Tokens managed by an organization are\n * excluded: they cannot be managed at the project scope.\n * @param projectId - The project ID\n * @returns A promise that resolves to an array of robots\n *\n * @internal\n */\nexport async function getTokens(projectId: string): Promise<Robot[]> {\n const client = await getClient()\n\n const robots = await fetchAllPages<Robot>(client, `/access/project/${projectId}/robots`)\n\n return robots.filter((robot) => robot.managedBy?.resourceType !== 'organization')\n}\n\n/**\n * Get all roles for a project\n * @param projectId - The project ID\n * @returns A promise that resolves to an array of project roles\n *\n * @internal\n */\nexport async function getTokenRoles(projectId: string): Promise<Role[]> {\n const client = await getClient()\n\n return fetchAllPages<Role>(client, `/access/project/${projectId}/roles`)\n}\n"],"names":["getGlobalCliClient","TOKENS_API_VERSION","getClient","token","apiVersion","requireUser","undefined","fetchAllPages","client","url","items","cursor","response","request","query","nextCursor","push","data","getProjectMembership","robot","projectId","memberships","find","membership","resourceType","resourceId","postRobot","expiresAt","body","method","createToken","options","label","roleNames","sendNotification","createOrganizationToken","organizationId","deleteToken","tokenId","rotateToken","getTokens","robots","filter","managedBy","getTokenRoles"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/services/tokens.ts"],"sourcesContent":["import {getGlobalCliClient} from '@sanity/cli-core'\nimport {type SanityClient} from '@sanity/client'\n\nimport {TOKENS_API_VERSION} from '../actions/tokens/constants.js'\nimport {\n type Membership,\n type Robot,\n type RobotWithToken,\n type Role,\n type TokenScope,\n} from '../actions/tokens/types.js'\n\ninterface PaginatedResponse<T> {\n data: T[]\n nextCursor: string | null\n}\n\nfunction getClient(token?: string): Promise<SanityClient> {\n return getGlobalCliClient({\n apiVersion: TOKENS_API_VERSION,\n requireUser: true,\n ...(token === undefined ? {} : {token}),\n })\n}\n\nasync function fetchAllPages<T>(\n client: SanityClient,\n url: string,\n query: Record<string, string> = {},\n): Promise<T[]> {\n const items: T[] = []\n let cursor: string | null = null\n\n do {\n const response: PaginatedResponse<T> = await client.request<PaginatedResponse<T>>({\n query: {...query, ...(cursor === null ? {} : {nextCursor: cursor})},\n url,\n })\n items.push(...response.data)\n cursor = response.nextCursor\n } while (cursor !== null)\n\n return items\n}\n\n/**\n * Get the membership granting a robot access to the given project\n * @param robot - The robot to inspect\n * @param projectId - The project ID\n * @returns The project membership, if any\n *\n * @internal\n */\nexport function getProjectMembership(robot: Robot, projectId: string): Membership | undefined {\n return robot.memberships.find(\n (membership) => membership.resourceType === 'project' && membership.resourceId === projectId,\n )\n}\n\ninterface RobotBody {\n label: string\n memberships: Membership[]\n\n expiresAt?: string\n}\n\nasync function postRobot(\n url: string,\n {expiresAt, ...body}: RobotBody,\n query: Record<string, string> = {},\n): Promise<RobotWithToken> {\n const client = await getClient()\n\n return client.request<RobotWithToken>({\n body: {...body, ...(expiresAt === undefined ? {} : {expiresAt})},\n method: 'POST',\n query,\n url,\n })\n}\n\ninterface CreateTokenOptions {\n label: string\n projectId: string\n roleNames: string[]\n\n expiresAt?: string\n sendNotification?: boolean\n}\n\n/**\n * Add a token to a project\n * @param options - The options for adding a token to a project\n * @returns A promise that resolves to the created robot, including its secret token\n *\n * @internal\n */\nexport async function createToken(options: CreateTokenOptions): Promise<RobotWithToken> {\n const {expiresAt, label, projectId, roleNames, sendNotification} = options\n\n return postRobot(\n `/access/project/${projectId}/robots`,\n {\n expiresAt,\n label,\n memberships: [{resourceId: projectId, resourceType: 'project', roleNames}],\n },\n sendNotification === false ? {sendNotification: 'false'} : {},\n )\n}\n\ninterface CreateOrganizationTokenOptions extends RobotBody {\n organizationId: string\n}\n\n/**\n * Create an organization token, which can hold organization permissions and roles on several projects\n * @param options - The options for the organization token\n * @returns A promise that resolves to the created robot, including its secret token\n *\n * @internal\n */\nexport async function createOrganizationToken(\n options: CreateOrganizationTokenOptions,\n): Promise<RobotWithToken> {\n const {organizationId, ...body} = options\n\n return postRobot(`/access/organization/${organizationId}/robots`, body)\n}\n\nfunction robotsUrl({resourceId, resourceType}: TokenScope): string {\n return `/access/${resourceType}/${resourceId}/robots`\n}\n\nfunction isManagedByOrganization(robot: Robot, organizationId: string): boolean {\n return (\n robot.managedBy?.resourceType === 'organization' &&\n robot.managedBy.resourceId === organizationId\n )\n}\n\n/** Delete a token from the requested scope. @internal */\nexport async function deleteToken(scope: TokenScope, tokenId: string): Promise<void> {\n const client = await getClient()\n const url = `${robotsUrl(scope)}/${tokenId}`\n\n if (scope.resourceType === 'organization') {\n const robot = await client.request<Robot>({url})\n if (!isManagedByOrganization(robot, scope.resourceId)) {\n throw new Error(\n `Token \"${tokenId}\" is not managed by organization \"${scope.resourceId}\". Use tokens list --organization ${scope.resourceId} to find tokens managed by this organization.`,\n )\n }\n }\n\n return client.request({method: 'DELETE', url})\n}\n\n/**\n * Rotate a robot token, replacing its secret while preserving the robot,\n * its roles, and the token's expiry. The request authenticates as the robot\n * itself; the previous secret is revoked immediately.\n * @param token - The current robot token secret\n * @returns A promise that resolves to the robot with its new secret token\n *\n * @internal\n */\nexport async function rotateToken(token: string): Promise<RobotWithToken> {\n const client = await getClient(token)\n\n return client.request<RobotWithToken>({\n method: 'POST',\n url: '/access/robots/me/rotate',\n })\n}\n\n/** List tokens managed at the requested scope. @internal */\nexport async function getTokens(scope: TokenScope): Promise<Robot[]> {\n const client = await getClient()\n const query: Record<string, string> =\n scope.resourceType === 'organization' ? {includeChildren: 'true', limit: '500'} : {}\n const robots = await fetchAllPages<Robot>(client, robotsUrl(scope), query)\n\n if (scope.resourceType === 'organization') {\n return robots.filter((robot) => isManagedByOrganization(robot, scope.resourceId))\n }\n\n return robots.filter((robot) => robot.managedBy?.resourceType !== 'organization')\n}\n\n/**\n * Get all roles for a project\n * @param projectId - The project ID\n * @returns A promise that resolves to an array of project roles\n *\n * @internal\n */\nexport async function getTokenRoles(projectId: string): Promise<Role[]> {\n const client = await getClient()\n\n return fetchAllPages<Role>(client, `/access/project/${projectId}/roles`)\n}\n"],"names":["getGlobalCliClient","TOKENS_API_VERSION","getClient","token","apiVersion","requireUser","undefined","fetchAllPages","client","url","query","items","cursor","response","request","nextCursor","push","data","getProjectMembership","robot","projectId","memberships","find","membership","resourceType","resourceId","postRobot","expiresAt","body","method","createToken","options","label","roleNames","sendNotification","createOrganizationToken","organizationId","robotsUrl","isManagedByOrganization","managedBy","deleteToken","scope","tokenId","Error","rotateToken","getTokens","includeChildren","limit","robots","filter","getTokenRoles"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import { promptForProject } from '../prompts/promptForProject.js';
2
+ export async function resolveTokenScope(options) {
3
+ const { getProjectId, grant, organization } = options;
4
+ if (organization) return {
5
+ resourceId: organization,
6
+ resourceType: 'organization'
7
+ };
8
+ const projectId = await getProjectId({
9
+ fallback: ()=>promptForProject({
10
+ requiredPermissions: [
11
+ {
12
+ grant,
13
+ permission: 'sanity.project.tokens'
14
+ }
15
+ ]
16
+ })
17
+ });
18
+ return {
19
+ resourceId: projectId,
20
+ resourceType: 'project'
21
+ };
22
+ }
23
+
24
+ //# sourceMappingURL=resolveTokenScope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/util/resolveTokenScope.ts"],"sourcesContent":["import {type SanityCommandInterface} from '@sanity/cli-core'\n\nimport {type TokenScope} from '../actions/tokens/types.js'\nimport {promptForProject} from '../prompts/promptForProject.js'\n\nexport async function resolveTokenScope(options: {\n getProjectId: SanityCommandInterface['getProjectId']\n grant: 'delete' | 'read'\n organization?: string\n}): Promise<TokenScope> {\n const {getProjectId, grant, organization} = options\n if (organization) return {resourceId: organization, resourceType: 'organization'}\n\n const projectId = await getProjectId({\n fallback: () =>\n promptForProject({requiredPermissions: [{grant, permission: 'sanity.project.tokens'}]}),\n })\n return {resourceId: projectId, resourceType: 'project'}\n}\n"],"names":["promptForProject","resolveTokenScope","options","getProjectId","grant","organization","resourceId","resourceType","projectId","fallback","requiredPermissions","permission"],"mappings":"AAGA,SAAQA,gBAAgB,QAAO,iCAAgC;AAE/D,OAAO,eAAeC,kBAAkBC,OAIvC;IACC,MAAM,EAACC,YAAY,EAAEC,KAAK,EAAEC,YAAY,EAAC,GAAGH;IAC5C,IAAIG,cAAc,OAAO;QAACC,YAAYD;QAAcE,cAAc;IAAc;IAEhF,MAAMC,YAAY,MAAML,aAAa;QACnCM,UAAU,IACRT,iBAAiB;gBAACU,qBAAqB;oBAAC;wBAACN;wBAAOO,YAAY;oBAAuB;iBAAE;YAAA;IACzF;IACA,OAAO;QAACL,YAAYE;QAAWD,cAAc;IAAS;AACxD"}