Skip to content

feat(app-scopes): add brevo app scopes update for M2M apps [BEX-486] - #111

Open
shubham773 wants to merge 4 commits into
feature/m2mfrom
feat/bex-486-m2m-scopes-update
Open

feat(app-scopes): add brevo app scopes update for M2M apps [BEX-486]#111
shubham773 wants to merge 4 commits into
feature/m2mfrom
feat/bex-486-m2m-scopes-update

Conversation

@shubham773

@shubham773 shubham773 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds brevo app scopes update --app-id <id> --scopes <a,b,c> [--yes] [--json] to change an existing M2M app's granted OAuth scopes — previously M2M scopes were fixed at creation with no way to edit them.
  • Shows a current → new diff (calling out any removal) and asks to confirm unless --yes; a request that changes nothing is a no-op (exit 0, no network call).
  • Extends command-registry.ts with an optional one-level-deeper groups field so app scopes update can exist without reshaping the flat CommandDefinition model.
  • Corrects several "M2M scopes are fixed at creation" messages/comments (en.ts, create.ts, scope-prompts.ts, both agent docs, the pending M2M changeset) that this command makes untrue, and extracts the typed-scope fallback prompt into scope-prompts.ts so app create and this command share one implementation.
  • Smoke coverage: two new steps in private-app.ts's M2M lifecycle — an update that adds a scope (verified via a fresh app credentials read, not just the command's own echoed output) and a no-op resubmit that must not prompt for confirmation. Gated by a new m2m-scopes-update capability (core.ts), detected the same way m2m-flag is, so --against=published skips cleanly on an npm release that predates this command — and it self-downgrades at runtime (markFeatureUnavailable) if the backend call fails, the same pattern public-distribution uses.

Test plan

  • yarn lint — clean
  • yarn test:ci — 74 suites / 1698 tests passing, including coverage for isM2mApp, updateAppScopes, nested command registration, and the full scopes update command flow (pre-filled picker/typed-prompt, --scopes as full replace, no-op detection, app picker, non-M2M refusal, JSON output, error propagation)
  • yarn build and PREVIEW=1 yarn build — both green
  • scripts/smoke/*.ts type-check and lint clean (eslint 'scripts/**/*.ts', prettier --check)
  • Manually verified brevo app scopes update --help, brevo app scopes --help, app --help, and that --mode is correctly rejected as an unknown option

🤖 Generated with Claude Code

Shubham Verma and others added 3 commits September 10, 2026 17:25
Machine-to-machine apps had no way to change their granted scopes after
creation. Adds `brevo app scopes update --app-id <id> --scopes <a,b,c>
--mode append|replace`, which reads the app's current scopes for a
confirmation preview, then sends the requested scopes plus the mode to a
new scopes-only PATCH endpoint so the server performs the actual
append/replace.

Supporting changes:
- `SubcommandGroupDefinition` gains an optional `groups` field so
  `command-registry.ts` can register one level of subcommand nesting
  (`app scopes update`), without reshaping `CommandDefinition`.
- `services/app.ts` gains `updateAppScopes` and a heuristic `isM2mApp`
  detector (no server-sent discriminator exists yet).
- Corrects several "M2M scopes are fixed at creation" messages and code
  comments (in `en.ts`, `create.ts`, `scope-prompts.ts`, and both agent
  docs) that this command makes untrue, and extracts the typed-scope
  fallback prompt into `scope-prompts.ts` so `create` and this command
  share one implementation.

The backend scopes-update API this depends on had not shipped at the
time of this change — the request/response shape is built from that
API's own design and is called out as unverified (comments in
constants.ts/services/app.ts, changeset, agent docs) pending a
follow-up once it lands.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… scope selector

Drop the `--mode append|replace` flag from `brevo app scopes update` in
favor of pre-filling the interactive scope picker (and its typed
fallback) with the app's current scopes. Whatever a partner submits —
scopes ticked/unticked from that starting point, or edited text —
already is the complete desired scope set, so the CLI always sends a
plain `{ scopes }` replace and the server never has to reconcile a
partial add/remove delta. `--scopes`, passed directly, keeps the same
contract non-interactively: it's the full desired list, not a delta.

Simplifies the endpoint contract back to a plain replace (removing the
CLI-invented `mode` field), and updates messages, docs, and tests
accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…BEX-486]

Adds two steps to private-app.ts's M2M lifecycle: an update that adds a
scope (verified via a fresh `app credentials` read, not just the
command's own echoed output) and a no-op resubmit of the same scopes
that must not prompt for confirmation.

Gated by a new `m2m-scopes-update` capability (core.ts), detected the
same way `m2m-flag` is (a help-text probe), so a `--against=published`
run against an npm release that predates this command skips rather
than fails. It also downgrades itself at runtime if the call fails —
the scopes-update backend (BEX-481) had not shipped as of this PR, so
an environment where it's still unavailable server-side skips with
`markFeatureUnavailable`, the same pattern `public-distribution` uses
for a build that offers the flag but whose environment declines it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@piyushsarin-sib piyushsarin-sib left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review notes on the app scopes update surface. One blocker (the --json gate), a couple of correctness items, and some copy/coverage nits — details inline.

One item that isn't on a changed line: README.md still says "Scopes are fixed at creation (there is no config to edit…)" and the command table doesn't list brevo app scopes update. Per CLAUDE.md the README update belongs on the chore(release) PR, so flagging it for there rather than asking for it here.

Comment thread src/commands/app/scopes-update.ts Outdated
Comment thread src/commands/app/scopes-update.ts Outdated
Comment thread src/commands/app/scopes-update.ts
Comment thread src/services/app.ts Outdated
Comment thread src/commands/app/scope-prompts.ts
Comment thread src/commands/app/scope-prompts.ts Outdated
Comment thread scripts/smoke/private-app.ts
@piyushsarin-sib

piyushsarin-sib commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Manually Review:
Proposal: should app upload stop accepting scope changes altogether?

Following on from the comment above — an M2M app has no app-config.json and never reaches app upload, so for M2M the only scope-change path is brevo app scopes update, which is already M2M-gated. That leaves app upload pushing auth.scopes for consent apps only.

If the backend permits scope mutation on M2M auths only, then upload's scope path can never do anything except produce a server-side rejection. So I'd suggest blocking it there too:

  • app upload refuses when diff.currentScopes differs from diff.nextScopes, with a message pointing at brevo app scopes update instead of a raw 400.
  • Scope changes then have exactly one entry point in the CLI, and that entry point is M2M-only by construction — the rule stops depending on which command you happen to use.
  • Docs consequence: README.md, CLAUDE.md, agent-context/SKILL.md and agent-context/AGENTS.md all currently tell users to change scopes by editing app-config.json and running brevo app upload. Those four plus a note on the pending changeset would have to move in the same PR.

What do you think — hard block in upload, or keep it and only warn until the backend behaviour is confirmed?

@shubham773

Copy link
Copy Markdown
Contributor Author

Manually Review: Proposal: should app upload stop accepting scope changes altogether?

Following on from the comment above — an M2M app has no app-config.json and never reaches app upload, so for M2M the only scope-change path is brevo app scopes update, which is already M2M-gated. That leaves app upload pushing auth.scopes for consent apps only.

If the backend permits scope mutation on M2M auths only, then upload's scope path can never do anything except produce a server-side rejection. So I'd suggest blocking it there too:

  • app upload refuses when diff.currentScopes differs from diff.nextScopes, with a message pointing at brevo app scopes update instead of a raw 400.
  • Scope changes then have exactly one entry point in the CLI, and that entry point is M2M-only by construction — the rule stops depending on which command you happen to use.
  • Docs consequence: README.md, CLAUDE.md, agent-context/SKILL.md and agent-context/AGENTS.md all currently tell users to change scopes by editing app-config.json and running brevo app upload. Those four plus a note on the pending changeset would have to move in the same PR.

What do you think — hard block in upload, or keep it and only warn until the backend behaviour is confirmed?

For m2m apps, scopes wont be allowed to be updated via upload command/API. That access control will be added in a separate PR.

- scopes-update.ts: `--json` alone now skips confirmation too (was
  missing the `!options.json` half of the gate, so --json without
  --yes printed a human diff and then died on inquirer off a TTY).
- scopes-update.ts: refuse with a message naming `--scopes`, not
  `--app-id`, when scopes can't be prompted for non-interactively —
  the app was already named by that point.
- scopes-update.ts: fall back to the fetched app's own name for the
  confirm-prompt label when `--app-id` was passed directly (picker-only
  label previously left it blank, showing the ID twice).
- services/app.ts: stop running the scopes-PATCH response through
  `normalizeAppId` — the unverified response shape could plausibly
  omit `app_id`, and the caller only ever reads `.scopes`, so a
  successful update should never fail over a field nobody uses.
- scope-prompts.ts: only pre-check a scope-picker heading under the
  cascading prompt; the plain-checkbox fallback has no cascade wiring,
  so a pre-checked heading there silently re-adds a scope the partner
  just unticked.
- scope-prompts.ts: correct the typed-fallback prompt's behavior/copy
  — inquirer 8's `input` prompt does not pre-populate an editable line
  from `default` (verified against inquirer's own source), so typing
  anything discards the prefill entirely. Adds an explicit intro
  message stating the current scopes and that Enter alone keeps them.
- scripts/smoke/private-app.ts: narrow the scopes-update step's catch
  to the "endpoint not there yet" shape (404/405/501-ish) and rethrow
  everything else, so a genuine regression can't report as a skip.

Regression tests added for all of the above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@piyushsarin-sib piyushsarin-sib left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants