From 185bc4b0ee7f41018240e751e6dd2786f4f60798 Mon Sep 17 00:00:00 2001 From: Michael Malave Date: Mon, 14 Sep 2026 15:53:09 -0700 Subject: [PATCH 1/2] Task 3: add --team flag to authorizations:revoke and authorizations:rotate (W-24132436) When --team is set, revoke targets DELETE /teams/${team}/oauth/authorizations/${id} and rotate targets POST /teams/${team}/oauth/authorizations/${id}/actions/regenerate-tokens. Without --team, both commands keep their existing user-scoped paths. The --team flag applies to both authorizations:revoke and its authorizations:destroy alias since they share the same command class. Adds unit test coverage for both the team and non-team paths on each command. --- src/commands/authorizations/revoke.ts | 14 +++++++++++--- src/commands/authorizations/rotate.ts | 14 +++++++++++--- .../commands/authorizations/revoke.unit.test.ts | 12 ++++++++++++ .../commands/authorizations/rotate.unit.test.ts | 15 +++++++++++++++ 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/commands/authorizations/revoke.ts b/src/commands/authorizations/revoke.ts index 42c33d7fb2..bc9c704d5f 100644 --- a/src/commands/authorizations/revoke.ts +++ b/src/commands/authorizations/revoke.ts @@ -1,4 +1,4 @@ -import {Command} from '@heroku-cli/command' +import {Command, flags} from '@heroku-cli/command' import * as Heroku from '@heroku-cli/schema' import * as color from '@heroku/heroku-cli-util/color' import {Args, ux} from '@oclif/core' @@ -13,11 +13,19 @@ export default class AuthorizationsRevoke extends Command { color.command('heroku authorizations:revoke 105a7bfa-34c3-476e-873a-b1ac3fdc12fb'), ] + static flags = { + team: flags.team(), + } + async run() { - const {args} = await this.parse(AuthorizationsRevoke) + const {args, flags} = await this.parse(AuthorizationsRevoke) + + const endpoint = flags.team + ? `/teams/${encodeURIComponent(flags.team)}/oauth/authorizations/${encodeURIComponent(args.id)}` + : `/oauth/authorizations/${encodeURIComponent(args.id)}` ux.action.start('Revoking OAuth Authorization') - const {body: auth} = await this.heroku.delete(`/oauth/authorizations/${encodeURIComponent(args.id)}`) + const {body: auth} = await this.heroku.delete(endpoint) ux.action.stop(`done, revoked authorization from ${color.cyan(auth.description)}`) } } diff --git a/src/commands/authorizations/rotate.ts b/src/commands/authorizations/rotate.ts index 993a6231c0..5335658dd4 100644 --- a/src/commands/authorizations/rotate.ts +++ b/src/commands/authorizations/rotate.ts @@ -1,4 +1,4 @@ -import {Command} from '@heroku-cli/command' +import {Command, flags} from '@heroku-cli/command' import * as Heroku from '@heroku-cli/schema' import {Args, ux} from '@oclif/core' @@ -10,11 +10,19 @@ export default class AuthorizationsRotate extends Command { } static description = 'updates an OAuth authorization token' + static flags = { + team: flags.team(), + } + async run() { - const {args} = await this.parse(AuthorizationsRotate) + const {args, flags} = await this.parse(AuthorizationsRotate) + + const endpoint = flags.team + ? `/teams/${encodeURIComponent(flags.team)}/oauth/authorizations/${encodeURIComponent(args.id)}/actions/regenerate-tokens` + : `/oauth/authorizations/${encodeURIComponent(args.id)}/actions/regenerate-tokens` ux.action.start('Rotating OAuth Authorization') - const {body: authorization} = await this.heroku.post(`/oauth/authorizations/${encodeURIComponent(args.id)}/actions/regenerate-tokens`) + const {body: authorization} = await this.heroku.post(endpoint) ux.action.stop() display(authorization) diff --git a/test/unit/commands/authorizations/revoke.unit.test.ts b/test/unit/commands/authorizations/revoke.unit.test.ts index 8087407ab9..8fe4c2e45d 100644 --- a/test/unit/commands/authorizations/revoke.unit.test.ts +++ b/test/unit/commands/authorizations/revoke.unit.test.ts @@ -27,6 +27,18 @@ describe('authorizations:revoke', function () { expect(stderr).to.contain('done, revoked authorization from Example Auth') }) + context('with --team', function () { + it('revokes the team-owned authorization', async function () { + api + .delete(`/teams/my-team/oauth/authorizations/${authorizationID}`) + .reply(200, {description: 'Example Auth'}) + + const {stderr} = await runCommand(AuthorizationsRevoke, [authorizationID, '--team', 'my-team']) + + expect(stderr).to.contain('done, revoked authorization from Example Auth') + }) + }) + context('without an ID argument', function () { it('shows required ID error', async function () { const {error} = await runCommand(AuthorizationsRevoke, []) diff --git a/test/unit/commands/authorizations/rotate.unit.test.ts b/test/unit/commands/authorizations/rotate.unit.test.ts index c25c1ade92..29ba6c8480 100644 --- a/test/unit/commands/authorizations/rotate.unit.test.ts +++ b/test/unit/commands/authorizations/rotate.unit.test.ts @@ -29,4 +29,19 @@ describe('authorizations:rotate', function () { expect(stdout).to.contain('Token: secrettoken\n') expect(stderr).to.contain('Rotating OAuth Authorization... done') }) + + context('with --team', function () { + it('rotates the team-owned authorization and prints the authentication', async function () { + api + .post(`/teams/my-team/oauth/authorizations/${authorizationID}/actions/regenerate-tokens`) + .reply(200, {access_token: {token: 'secrettoken'}, scope: ['global', 'app']}) + + const {stderr, stdout} = await runCommand(AuthorizationsRotate, [authorizationID, '--team', 'my-team']) + + expect(stdout).to.contain('Client: \n') + expect(stdout).to.contain('Scope: global,app\n') + expect(stdout).to.contain('Token: secrettoken\n') + expect(stderr).to.contain('Rotating OAuth Authorization... done') + }) + }) }) From 6c154f910b7ff8910f4f7d36e13d464efb6fc88b Mon Sep 17 00:00:00 2001 From: Michael Malave Date: Tue, 15 Sep 2026 09:39:35 -0700 Subject: [PATCH 2/2] fix: send version=3.sdk Accept header on --team authorizations revoke/rotate The team-owned authorizations API routes (/teams/:team/oauth/authorizations) are gated behind the 3.sdk API variant; without the Accept header the request 404s. Send SDK_HEADER on the --team path only and assert it in the team tests. --- src/commands/authorizations/revoke.ts | 8 ++++++-- src/commands/authorizations/rotate.ts | 7 +++++-- test/unit/commands/authorizations/revoke.unit.test.ts | 1 + test/unit/commands/authorizations/rotate.unit.test.ts | 1 + 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/commands/authorizations/revoke.ts b/src/commands/authorizations/revoke.ts index bc9c704d5f..506d84d123 100644 --- a/src/commands/authorizations/revoke.ts +++ b/src/commands/authorizations/revoke.ts @@ -3,6 +3,8 @@ import * as Heroku from '@heroku-cli/schema' import * as color from '@heroku/heroku-cli-util/color' import {Args, ux} from '@oclif/core' +import {SDK_HEADER} from '../../lib/api.js' + export default class AuthorizationsRevoke extends Command { static aliases = ['authorizations:destroy'] static args = { @@ -12,7 +14,6 @@ export default class AuthorizationsRevoke extends Command { static examples = [ color.command('heroku authorizations:revoke 105a7bfa-34c3-476e-873a-b1ac3fdc12fb'), ] - static flags = { team: flags.team(), } @@ -25,7 +26,10 @@ export default class AuthorizationsRevoke extends Command { : `/oauth/authorizations/${encodeURIComponent(args.id)}` ux.action.start('Revoking OAuth Authorization') - const {body: auth} = await this.heroku.delete(endpoint) + const {body: auth} = await this.heroku.delete( + endpoint, + flags.team ? {headers: {Accept: SDK_HEADER}} : {}, + ) ux.action.stop(`done, revoked authorization from ${color.cyan(auth.description)}`) } } diff --git a/src/commands/authorizations/rotate.ts b/src/commands/authorizations/rotate.ts index 5335658dd4..b7b2cb8ae6 100644 --- a/src/commands/authorizations/rotate.ts +++ b/src/commands/authorizations/rotate.ts @@ -2,6 +2,7 @@ import {Command, flags} from '@heroku-cli/command' import * as Heroku from '@heroku-cli/schema' import {Args, ux} from '@oclif/core' +import {SDK_HEADER} from '../../lib/api.js' import {display} from '../../lib/authorizations/authorizations.js' export default class AuthorizationsRotate extends Command { @@ -9,7 +10,6 @@ export default class AuthorizationsRotate extends Command { id: Args.string({description: 'ID of the authorization', required: true}), } static description = 'updates an OAuth authorization token' - static flags = { team: flags.team(), } @@ -22,7 +22,10 @@ export default class AuthorizationsRotate extends Command { : `/oauth/authorizations/${encodeURIComponent(args.id)}/actions/regenerate-tokens` ux.action.start('Rotating OAuth Authorization') - const {body: authorization} = await this.heroku.post(endpoint) + const {body: authorization} = await this.heroku.post( + endpoint, + flags.team ? {headers: {Accept: SDK_HEADER}} : {}, + ) ux.action.stop() display(authorization) diff --git a/test/unit/commands/authorizations/revoke.unit.test.ts b/test/unit/commands/authorizations/revoke.unit.test.ts index 8fe4c2e45d..581767973e 100644 --- a/test/unit/commands/authorizations/revoke.unit.test.ts +++ b/test/unit/commands/authorizations/revoke.unit.test.ts @@ -30,6 +30,7 @@ describe('authorizations:revoke', function () { context('with --team', function () { it('revokes the team-owned authorization', async function () { api + .matchHeader('accept', 'application/vnd.heroku+json; version=3.sdk') .delete(`/teams/my-team/oauth/authorizations/${authorizationID}`) .reply(200, {description: 'Example Auth'}) diff --git a/test/unit/commands/authorizations/rotate.unit.test.ts b/test/unit/commands/authorizations/rotate.unit.test.ts index 29ba6c8480..78aee6ac81 100644 --- a/test/unit/commands/authorizations/rotate.unit.test.ts +++ b/test/unit/commands/authorizations/rotate.unit.test.ts @@ -33,6 +33,7 @@ describe('authorizations:rotate', function () { context('with --team', function () { it('rotates the team-owned authorization and prints the authentication', async function () { api + .matchHeader('accept', 'application/vnd.heroku+json; version=3.sdk') .post(`/teams/my-team/oauth/authorizations/${authorizationID}/actions/regenerate-tokens`) .reply(200, {access_token: {token: 'secrettoken'}, scope: ['global', 'app']})