@getbrevo/cli 2.0.2 → 2.2.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.
- package/CHANGELOG.md +237 -0
- package/README.md +32 -6
- package/agent-context/AGENTS.md +62 -15
- package/agent-context/SKILL.md +98 -14
- package/dist/bin/files/AGENTS.md.tmpl +119 -0
- package/dist/bin/files/CLAUDE.md.tmpl +139 -0
- package/dist/bin/files/README.md.tmpl +184 -0
- package/dist/bin/files/app-config.json.tmpl +16 -0
- package/dist/{templates → bin}/files/src/oauth/.env.example.tmpl +8 -0
- package/dist/{templates → bin}/files/src/oauth/.env.local.tmpl +2 -0
- package/dist/{templates → bin}/files/src/oauth/handler.js.tmpl +49 -7
- package/dist/{templates → bin}/files/src/oauth/server.js.tmpl +20 -14
- package/dist/{templates → bin}/files/src/oauth/token-store.js.tmpl +19 -0
- package/dist/bin/index.js +680 -235
- package/dist/bin/index.js.map +7 -1
- package/package.json +6 -4
- package/dist/api/client.d.ts +0 -25
- package/dist/api/client.d.ts.map +0 -1
- package/dist/api/client.js +0 -205
- package/dist/api/client.js.map +0 -1
- package/dist/bin/index.d.ts +0 -3
- package/dist/bin/index.d.ts.map +0 -1
- package/dist/commands/app/create.d.ts +0 -8
- package/dist/commands/app/create.d.ts.map +0 -1
- package/dist/commands/app/create.js +0 -325
- package/dist/commands/app/create.js.map +0 -1
- package/dist/commands/app/credentials.d.ts +0 -6
- package/dist/commands/app/credentials.d.ts.map +0 -1
- package/dist/commands/app/credentials.js +0 -114
- package/dist/commands/app/credentials.js.map +0 -1
- package/dist/commands/app/delete.d.ts +0 -6
- package/dist/commands/app/delete.d.ts.map +0 -1
- package/dist/commands/app/delete.js +0 -167
- package/dist/commands/app/delete.js.map +0 -1
- package/dist/commands/app/list.d.ts +0 -4
- package/dist/commands/app/list.d.ts.map +0 -1
- package/dist/commands/app/list.js +0 -71
- package/dist/commands/app/list.js.map +0 -1
- package/dist/commands/app/scaffold.d.ts +0 -5
- package/dist/commands/app/scaffold.d.ts.map +0 -1
- package/dist/commands/app/scaffold.js +0 -213
- package/dist/commands/app/scaffold.js.map +0 -1
- package/dist/commands/app/scopes.d.ts +0 -2
- package/dist/commands/app/scopes.d.ts.map +0 -1
- package/dist/commands/app/scopes.js +0 -81
- package/dist/commands/app/scopes.js.map +0 -1
- package/dist/commands/app/start.d.ts +0 -5
- package/dist/commands/app/start.d.ts.map +0 -1
- package/dist/commands/app/start.js +0 -220
- package/dist/commands/app/start.js.map +0 -1
- package/dist/commands/app/update.d.ts +0 -2
- package/dist/commands/app/update.d.ts.map +0 -1
- package/dist/commands/app/update.js +0 -444
- package/dist/commands/app/update.js.map +0 -1
- package/dist/commands/definitions.d.ts +0 -5
- package/dist/commands/definitions.d.ts.map +0 -1
- package/dist/commands/definitions.js +0 -262
- package/dist/commands/definitions.js.map +0 -1
- package/dist/commands/init.d.ts +0 -2
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -138
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/login.d.ts +0 -6
- package/dist/commands/login.d.ts.map +0 -1
- package/dist/commands/login.js +0 -251
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.d.ts +0 -5
- package/dist/commands/logout.d.ts.map +0 -1
- package/dist/commands/logout.js +0 -54
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/skill/install.d.ts +0 -4
- package/dist/commands/skill/install.d.ts.map +0 -1
- package/dist/commands/skill/install.js +0 -29
- package/dist/commands/skill/install.js.map +0 -1
- package/dist/commands/skill/uninstall.d.ts +0 -4
- package/dist/commands/skill/uninstall.d.ts.map +0 -1
- package/dist/commands/skill/uninstall.js +0 -23
- package/dist/commands/skill/uninstall.js.map +0 -1
- package/dist/commands/whoami.d.ts +0 -4
- package/dist/commands/whoami.d.ts.map +0 -1
- package/dist/commands/whoami.js +0 -85
- package/dist/commands/whoami.js.map +0 -1
- package/dist/container.d.ts +0 -7
- package/dist/container.d.ts.map +0 -1
- package/dist/container.js +0 -28
- package/dist/container.js.map +0 -1
- package/dist/lang/en.d.ts +0 -162
- package/dist/lang/en.d.ts.map +0 -1
- package/dist/lang/en.js +0 -189
- package/dist/lang/en.js.map +0 -1
- package/dist/lib/abort.d.ts +0 -3
- package/dist/lib/abort.d.ts.map +0 -1
- package/dist/lib/abort.js +0 -30
- package/dist/lib/abort.js.map +0 -1
- package/dist/lib/auth-guard.d.ts +0 -13
- package/dist/lib/auth-guard.d.ts.map +0 -1
- package/dist/lib/auth-guard.js +0 -60
- package/dist/lib/auth-guard.js.map +0 -1
- package/dist/lib/browser.d.ts +0 -2
- package/dist/lib/browser.d.ts.map +0 -1
- package/dist/lib/browser.js +0 -16
- package/dist/lib/browser.js.map +0 -1
- package/dist/lib/cli-version.d.ts +0 -2
- package/dist/lib/cli-version.d.ts.map +0 -1
- package/dist/lib/cli-version.js +0 -59
- package/dist/lib/cli-version.js.map +0 -1
- package/dist/lib/command-handler.d.ts +0 -10
- package/dist/lib/command-handler.d.ts.map +0 -1
- package/dist/lib/command-handler.js +0 -16
- package/dist/lib/command-handler.js.map +0 -1
- package/dist/lib/command-registry.d.ts +0 -28
- package/dist/lib/command-registry.d.ts.map +0 -1
- package/dist/lib/command-registry.js +0 -54
- package/dist/lib/command-registry.js.map +0 -1
- package/dist/lib/config.d.ts +0 -83
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/config.js +0 -430
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/constants.d.ts +0 -48
- package/dist/lib/constants.d.ts.map +0 -1
- package/dist/lib/constants.js +0 -121
- package/dist/lib/constants.js.map +0 -1
- package/dist/lib/errors.d.ts +0 -30
- package/dist/lib/errors.d.ts.map +0 -1
- package/dist/lib/errors.js +0 -66
- package/dist/lib/errors.js.map +0 -1
- package/dist/lib/exit-codes.d.ts +0 -9
- package/dist/lib/exit-codes.d.ts.map +0 -1
- package/dist/lib/exit-codes.js +0 -12
- package/dist/lib/exit-codes.js.map +0 -1
- package/dist/lib/hidden-input.d.ts +0 -2
- package/dist/lib/hidden-input.d.ts.map +0 -1
- package/dist/lib/hidden-input.js +0 -78
- package/dist/lib/hidden-input.js.map +0 -1
- package/dist/lib/json-output.d.ts +0 -6
- package/dist/lib/json-output.d.ts.map +0 -1
- package/dist/lib/json-output.js +0 -11
- package/dist/lib/json-output.js.map +0 -1
- package/dist/lib/logger.d.ts +0 -11
- package/dist/lib/logger.d.ts.map +0 -1
- package/dist/lib/logger.js +0 -110
- package/dist/lib/logger.js.map +0 -1
- package/dist/lib/oauth-freshness.d.ts +0 -30
- package/dist/lib/oauth-freshness.d.ts.map +0 -1
- package/dist/lib/oauth-freshness.js +0 -61
- package/dist/lib/oauth-freshness.js.map +0 -1
- package/dist/lib/port.d.ts +0 -25
- package/dist/lib/port.d.ts.map +0 -1
- package/dist/lib/port.js +0 -103
- package/dist/lib/port.js.map +0 -1
- package/dist/lib/telemetry.d.ts +0 -9
- package/dist/lib/telemetry.d.ts.map +0 -1
- package/dist/lib/telemetry.js +0 -57
- package/dist/lib/telemetry.js.map +0 -1
- package/dist/lib/ui.d.ts +0 -14
- package/dist/lib/ui.d.ts.map +0 -1
- package/dist/lib/ui.js +0 -75
- package/dist/lib/ui.js.map +0 -1
- package/dist/lib/update-notifier.d.ts +0 -37
- package/dist/lib/update-notifier.d.ts.map +0 -1
- package/dist/lib/update-notifier.js +0 -298
- package/dist/lib/update-notifier.js.map +0 -1
- package/dist/lib/validators.d.ts +0 -55
- package/dist/lib/validators.d.ts.map +0 -1
- package/dist/lib/validators.js +0 -166
- package/dist/lib/validators.js.map +0 -1
- package/dist/services/account.d.ts +0 -8
- package/dist/services/account.d.ts.map +0 -1
- package/dist/services/account.js +0 -15
- package/dist/services/account.js.map +0 -1
- package/dist/services/app.d.ts +0 -38
- package/dist/services/app.d.ts.map +0 -1
- package/dist/services/app.js +0 -145
- package/dist/services/app.js.map +0 -1
- package/dist/services/browser-auth.d.ts +0 -15
- package/dist/services/browser-auth.d.ts.map +0 -1
- package/dist/services/browser-auth.js +0 -188
- package/dist/services/browser-auth.js.map +0 -1
- package/dist/services/normalize-app-id.d.ts +0 -12
- package/dist/services/normalize-app-id.d.ts.map +0 -1
- package/dist/services/normalize-app-id.js +0 -38
- package/dist/services/normalize-app-id.js.map +0 -1
- package/dist/services/oauth-metadata.d.ts +0 -7
- package/dist/services/oauth-metadata.d.ts.map +0 -1
- package/dist/services/oauth-metadata.js +0 -43
- package/dist/services/oauth-metadata.js.map +0 -1
- package/dist/services/oauth-refresh.d.ts +0 -14
- package/dist/services/oauth-refresh.d.ts.map +0 -1
- package/dist/services/oauth-refresh.js +0 -70
- package/dist/services/oauth-refresh.js.map +0 -1
- package/dist/services/scopes-html.d.ts +0 -3
- package/dist/services/scopes-html.d.ts.map +0 -1
- package/dist/services/scopes-html.js +0 -516
- package/dist/services/scopes-html.js.map +0 -1
- package/dist/services/scopes-web.d.ts +0 -12
- package/dist/services/scopes-web.d.ts.map +0 -1
- package/dist/services/scopes-web.js +0 -95
- package/dist/services/scopes-web.js.map +0 -1
- package/dist/services/skill.d.ts +0 -48
- package/dist/services/skill.d.ts.map +0 -1
- package/dist/services/skill.js +0 -223
- package/dist/services/skill.js.map +0 -1
- package/dist/skills/index.d.ts +0 -47
- package/dist/skills/index.d.ts.map +0 -1
- package/dist/skills/index.js +0 -61
- package/dist/skills/index.js.map +0 -1
- package/dist/templates/files/AGENTS.md.tmpl +0 -52
- package/dist/templates/files/CLAUDE.md.tmpl +0 -59
- package/dist/templates/files/README.md.tmpl +0 -85
- package/dist/templates/files/app-config.json.tmpl +0 -25
- package/dist/templates/index.d.ts +0 -36
- package/dist/templates/index.d.ts.map +0 -1
- package/dist/templates/index.js +0 -105
- package/dist/templates/index.js.map +0 -1
- package/dist/types.d.ts +0 -32
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- /package/dist/{templates → bin}/files/gitignore.tmpl +0 -0
- /package/dist/{templates → bin}/files/src/oauth/package.json.tmpl +0 -0
package/agent-context/SKILL.md
CHANGED
|
@@ -7,6 +7,18 @@ description: Use when working in a project that uses the Brevo Developer CLI (th
|
|
|
7
7
|
|
|
8
8
|
This project uses the Brevo Developer CLI to create and manage Brevo OAuth applications. Treat the `brevo` binary as the canonical entry point — don't shell out to `curl https://api.brevo.com/...` for things the CLI already covers.
|
|
9
9
|
|
|
10
|
+
## `brevo --help` is the source of truth
|
|
11
|
+
|
|
12
|
+
Brevo features that haven't been released are **not built into the published CLI at all** — not as hidden commands, not behind a flag or an env var. `brevo --help` and `brevo app --help` list everything the binary can do. Treat that as the complete surface and build from it.
|
|
13
|
+
|
|
14
|
+
What this means in practice:
|
|
15
|
+
|
|
16
|
+
- **If a command isn't in `--help`, it doesn't exist here.** Invoking it gives Commander's `unknown command` and exit `1`. That is a final answer, not a transient failure and not a permissions problem — there is no flag, config edit, account setting, or environment variable that reveals it. Don't retry, don't hunt for an alternative route, and don't tell the user to get access. Say the feature isn't available in this CLI and offer the nearest thing that works.
|
|
17
|
+
- **Don't act on a command you remember rather than one you can see.** Your recollection of the Brevo CLI may include commands from a newer or unreleased build. `--help` in the current session is the only reliable source.
|
|
18
|
+
- **A flag can be rejected even when its command exists.** `--distribution` is a real flag, but a value tied to an unreleased feature is refused with *"That command is not available yet. It is part of a Brevo feature that has not been released."* Same rule: it's final. Use `--distribution private`.
|
|
19
|
+
|
|
20
|
+
The Brevo API enforces the same boundaries independently, so nothing is gained by trying to route around the CLI — a hand-rolled `curl` hits the platform's own refusal.
|
|
21
|
+
|
|
10
22
|
> **Reading this from the repo rather than `~/.claude/skills/brevo-cli/`?**
|
|
11
23
|
>
|
|
12
24
|
> - **If you're Claude Code** (CLI or desktop app — any agent that reads `~/.claude/skills/`): install it as a skill with `brevo skill:cli install` so your tooling loads it natively and keeps it auto-refreshed. The repo copy is bundled with the npm package; the installed copy is what Claude actually consumes. Note: the Claude Desktop **chat** app does not read `~/.claude/skills/`, so the installed skill won't load there — only Claude Code surfaces pick it up.
|
|
@@ -40,54 +52,126 @@ Don't fall back to raw HTTP against `api.brevo.com` — the `brevo` binary is th
|
|
|
40
52
|
- "Authenticate" → `brevo login` (or `BREVO_API_KEY=xkeysib-... brevo login` for CI)
|
|
41
53
|
- "Who am I logged in as?" → `brevo whoami --json`
|
|
42
54
|
- "Show / pick an app" → `brevo app list --json`
|
|
43
|
-
- "Create an app" → `brevo app create --name "<name>" --distribution private --redirect-uri <url> --json` (add `--logo-uri <https://…>` to set the app logo at creation time; new apps default to scopes `contacts:read`, `contacts:write`, `crm:read`, `crm:write`).
|
|
44
|
-
- "
|
|
45
|
-
- "
|
|
46
|
-
- "
|
|
55
|
+
- "Create an app" → `brevo app create --name "<name>" --distribution private --redirect-uri <url> --json` (add `--logo-uri <https://…>` to set the app logo at creation time; new apps default to scopes `contacts:read`, `contacts:write`, `crm:read`, `crm:write`). **Use `--distribution private`** — check `brevo app create --help` for the values your account accepts, and don't pass one it doesn't list. **Fails immediately if run from a directory that already has `app-config.json`** — `cd` elsewhere first, or use `brevo app scaffold` in that directory instead. Otherwise resolves (creates/`cd`s into) its target directory, creates the app, and writes the **basic project structure** (`app-config.json` + `.gitignore`/`AGENTS.md`/`CLAUDE.md`/`README.md`). It scaffolds a feature (the OAuth test server) **only** when the interactive confirm (*"Scaffold the Test OAuth App?"*, default yes) is answered yes — there is no separate "which feature?" question while the CLI ships one; **non-interactive runs (`--json` or piped) stay base-only** — run `brevo app scaffold` afterward to add the OAuth code. Under `--json`, the response's `directory` field is where it landed and `scaffolded` is the base file count; check for `scaffoldSkipped` instead of `scaffolded` if that directory already existed (both directory setup and scaffolding are skipped together in that case, but the app is still created).
|
|
56
|
+
- "Create a UI app" (an action link that renders inside Brevo CRM records) → run `brevo app create` **interactively** and pick *UI app* at the *"What type of app are you building?"* prompt. There is **no `--type` flag and no per-field flags** — a UI app can only be authored from an interactive terminal, and every non-interactive run (`--json` or piped stdin) creates an OAuth app. The flow then asks for the integration type, one record page, one placement on it, a label, optional supporting text, and the destination URL — it authors exactly **one** placement; add more by hand as further `surface_point_list` entries in `app-config.json` (each with its own `label` and `redirect_link`), then `brevo app upload`.
|
|
57
|
+
- "Install a UI app into an account" → `brevo app install [account-id] --app-id <id> --force --json`. **UI apps only** — an OAuth app has nothing to install (it becomes usable when a user authorizes it) and the CLI refuses with exit `1`. The `[account-id]` positional is optional: omitted, a plain account installs into itself (no prompt, so `--json`/CI works) and a corporate account picks a sub-account interactively (non-interactive corporate runs must pass it explicitly). The app must have been validated with `brevo app upload` first — installing before an upload is refused locally. Interactively, omitting `--app-id` outside a linked project opens an app picker that lists **only UI apps**; with no UI app to offer, the command errors (exit `1`) naming `brevo app create`. That picker needs a terminal: under `--json` or off a TTY, omitting `--app-id` outside a linked project is refused with exit `1` — so always pass `--app-id` when scripting from an unlinked directory. Before it acts, `install` prints the configuration it will install **as stored on the server** — app ID, name, `version`, extension type and every placement — because that is what the account will render, not whatever the local `app-config.json` now says; under `--json` the same facts come back as `version` and `ui_app` on the result. If the linked project's `ui_app` block has drifted from the stored one, it warns and names `brevo app upload`, then installs anyway (exit `0`) — the stored configuration is a legitimate thing to install, so this is a notice, not a refusal.
|
|
58
|
+
- "Uninstall a UI app from an account" → `brevo app uninstall [account-id] --app-id <id> --force --json`. Same target resolution as `install`. Uninstalling an app that isn't installed is **informational, exit `0`** — not an error.
|
|
59
|
+
- "Update app metadata" → edit the relevant field(s) in `app-config.json` (`appName`, `auth.redirectUris`, `auth.scopes`, `logoUri`, `version`) (older projects may still say `auth.redirectUrls` — the CLI reads it and migrates the file to `redirectUris` on its next write), then run `brevo app upload --json` (no `--app-id`/`--name`/`--redirect-uri`/`--scope`/`--logo-uri` flags exist — `upload` always pushes the whole file, resolved only from cwd's `app-config.json`). **`distribution_type` is immutable** — it's set at `app create` time and cannot be changed via `upload`; if the local value differs from the server, `upload` errors and tells you to restore it (create a new app to get a different distribution). For a **UI app**, `upload` is also the way to change what an installed app renders — there is no re-install and no publish step: the diff prints every `ui_app` placement field by field (`before → after`, plus `(new)` / `(removed)` placements), then warns that the app may already be installed in Brevo accounts and asks *"Proceed with upload and update every account this app is installed in?"*. `--yes` skips the question but still prints the warning; `--json` prints neither and stays a single parseable document. **There is no `brevo app update`** — it was removed, with no shim and no flag-for-flag equivalent; if you find it in a user's script, CI job, README, or your own recollection, replace it with the edit-then-`upload` flow above. Invoking it — with any of the old flags, with `--help`, or as `brevo app help update` — prints a message naming `brevo app upload` and exits `1` **without uploading anything**, so a `1` from `brevo app update` means the command is gone, not that an upload failed.
|
|
60
|
+
- "Get client credentials" → `brevo app credentials --app-id <id> --json` (add `--reveal-secret` to print the secret). **`--app-id` is not optional here** — without it the command wants an interactive app picker, and under `--json`/off a TTY it refuses with exit `1` rather than prompting. **OAuth apps only** — a UI app has no OAuth credentials (no client ID, secret, scopes, or callbacks), so the command refuses it with exit `1` and points at `brevo app list` for the app's type.
|
|
61
|
+
- "Set up a project for an app that already exists" → `brevo app scaffold --app-id <id>` (`brevo app list` gives the IDs). Fetches the app and writes `app-config.json` + the base files, then adds a feature as below. Interactively it asks which directory to write into first (default `./<slugified app name>`); under `--json`/off a TTY it uses the current one. **This is the only way to get an `app-config.json` for an existing app** — `brevo app create` makes a *new* app, and `brevo app upload` only ever reads the project in the current directory. It refuses if the directory is already linked to a *different* app; pointing it at the app the directory is already linked to is a no-op. **Interactively you can omit `--app-id`**: plain `brevo app scaffold` in a directory with no `app-config.json` says so, asks *"Set up a project for an app you already have?"* (default yes), and on yes lists the account's apps so you can pick one — no need to look the ID up first. **Every interactive bootstrap (picker or `--app-id`) then asks `Output directory:`, defaulted to `./<the app's name slugified>`** — the same question `brevo app create` asks — creates that directory and `cd`s into it, so the files don't land in whatever folder you happened to be standing in; answer `.` to use the current directory instead. It then writes the project (`app-config.json` + the base files), shows what it wrote, and asks *"Scaffold the Test OAuth App?"* (default yes) — declining is normal and leaves the project in place, exit `0`. The *Next steps* box opens with `cd <dir>`, because the CLI can only move its own process, never your shell. **Under `--json` or off a TTY there is no directory question and the files go into the current directory**, so scripted `brevo app scaffold --app-id <id>` runs are unchanged — `mkdir` and `cd` yourself first if you want them somewhere specific. Answering **no** is a normal outcome, not an error: it exits `0` after printing the remaining routes (`brevo app create` here, or `cd` into an existing project). **Always pass `--app-id` when scripting** — the offer needs a terminal, so under `--json` or off a TTY the command errors instead of prompting. **If the directory you point it at already holds a project, the bootstrap is a refresh, not a fresh write**: the config found there is diffed against the server and rewritten only on consent (*"…will update app-config.json to match the server. Continue?"*, default yes) — answering **Merge** at the directory question does not suppress that, because merging keeps the file that exists and so would skip the very file a bootstrap is for. No drift means `app-config.json` is left alone with a one-line notice, and the feature still gets added. A directory holding a project for a **different** app is refused outright, naming both apps. Two refusals apply to both forms, before any write: that different-app case, and the directory must not be **inside** an existing project (a nested second `app-config.json` would make a later `brevo app upload` push the wrong app).
|
|
62
|
+
- "Add a feature (e.g. the OAuth test server) to an existing project" → `brevo app scaffold` (run **inside** the project directory; it reads the linked app from `app-config.json`, so `--app-id` is only needed to bootstrap a directory that has none). Not needed right after `app create` if you already accepted the feature prompt there. If feature files already exist it prompts Overwrite / Merge / Cancel (default Merge); pass `--overwrite` to force a full overwrite without prompting. **The scaffolded OAuth flow is the confidential-client flow:** `/auth/callback` authenticates the token exchange with the `CLIENT_SECRET` written into the scaffolded `.env.local`.
|
|
47
63
|
- "Run the OAuth test server" → `brevo app start oauth --port 3009` (must be inside the scaffolded directory)
|
|
48
|
-
- "Delete an app" → `brevo app delete --app-id <id> --force`
|
|
64
|
+
- "Delete an app" → `brevo app delete --app-id <id> --force`. **`--app-id` is not optional here either** — omitting it means an interactive picker, which under `--json`/off a TTY is refused with exit `1`. Never script a delete without naming the app. `--force` skips the prompt but still prints the install-loss warning line; under `--json` stdout stays JSON only.
|
|
49
65
|
- "List supported OAuth scopes" → `brevo app available-scopes --json`
|
|
50
66
|
- "Sign out" → `brevo logout --force`
|
|
51
67
|
|
|
52
68
|
## Hard rules
|
|
53
69
|
|
|
54
|
-
1. **Always pass `--json`** when you intend to parse output. Every command supports it.
|
|
70
|
+
1. **Always pass `--json`** when you intend to parse output. Every command supports it, **on success and on failure alike** — a failing `--json` run writes a single `{"error": {...}}` document to stdout (see *JSON errors* below), so you can read the reason instead of only seeing a non-zero exit.
|
|
55
71
|
2. **Never print, log, or commit** API keys (`xkeysib-…`), client secrets, refresh tokens, or contents of `~/.brevo/credentials.json` / `.env.local`. Redact before sharing diagnostics.
|
|
56
72
|
3. **Don't use `--api-key`** — the flag was removed. Use the `BREVO_API_KEY` env var.
|
|
57
|
-
4.
|
|
73
|
+
4. **`brevo app create` refuses to run inside an already-linked directory** (`app-config.json` present) — `cd` elsewhere or use `brevo app scaffold` there instead. **`brevo app scaffold` requires an `app-config.json` in the current directory unless you pass `--app-id` or answer its bootstrap offer** (it adds a feature to an already-created project); with none present and no `--app-id` it offers to set the directory up for an existing app when interactive, and otherwise errors, listing the three ways out (`cd` into a project, `--app-id` to set this directory up for an app you already have, or `brevo app create` for a new one). With `--app-id <id>` in a directory that has no config it fetches that app, asks (interactively only) which directory to write into — default `./<slugified app name>`, `.` to stay put — writes `app-config.json` + the base files there, and continues; that is the migration path off the removed `brevo app update --app-id`, and off a TTY or under `--json` it writes into the current directory with no prompt. If the directory it is told to write into already holds a project for the **same** app, it diffs that config against the server and rewrites it only on consent — the directory prompt's **Merge** answer does not silently skip the refresh, and no drift leaves `app-config.json` untouched with a notice. It refuses (before any network call) if the directory is already linked to a *different* app — whether that is the directory the command ran in or the one it was pointed at — or if the directory is **inside** an existing app project (a nested second `app-config.json` would make a later `brevo app upload` from there push the wrong app — `cd` to the project root or outside it). It reads the linked app from that config, diffs the config against the server, and if fields drifted it tells you and (on consent) rewrites `app-config.json` to match before writing the feature files. When any feature file already exists it prompts **Overwrite / Merge / Cancel** (default **Merge** — existing, e.g. hand-edited, files are kept; Cancel aborts). Pass `--overwrite` to force a full overwrite and skip that prompt (works interactively and under `--json`). **Under `--json` it never prompts**: a config diff comes back as `{ "cancelled": true, "reason": "...", "diffs": [...] }`; otherwise it scaffolds the feature (merging existing files unless `--overwrite` is passed) and returns `{ "scaffolded": <n>, "directory": "..." }`.
|
|
58
74
|
5. **Prefer flag-driven over interactive** in agent contexts: `--name`, `--app-id`, `--force`, `--yes` so the command doesn't block on prompts.
|
|
75
|
+
6. **Write only the `app-config.json` keys this file documents.** `brevo app upload` validates the whole file and rejects keys it doesn't recognise, so an invented block fails at upload rather than doing anything useful.
|
|
76
|
+
7. **Never mix the two app types in one `app-config.json`.** The presence of the `ui_app` block is the app-type discriminator: an OAuth app has a populated `auth` block and no `ui_app`; a UI app has a `ui_app` block and an **empty** `auth: {}` (it has no OAuth callback, scopes, or credentials). See *UI apps* below for the block's shape.
|
|
77
|
+
|
|
78
|
+
## UI apps
|
|
79
|
+
|
|
80
|
+
A **UI app** is an action link that renders inside Brevo CRM record pages (contact, company, deal). It is the second app type next to OAuth apps — created by the same `brevo app create` (interactive only, see the decision tree), pushed by the same `brevo app upload`, and made visible in an account with `brevo app install`.
|
|
81
|
+
|
|
82
|
+
A UI-app project is **configuration only**: there is no feature to scaffold and no `src/oauth/`, because an action link has no local server to run — `brevo app scaffold` inside one says so and exits `0`, and `brevo app start` does not apply. The base docs the scaffold writes (`AGENTS.md` / `CLAUDE.md` / `README.md`) describe whichever type the app is, so a UI app's copies document the `ui_app` block and the `upload` → `install` flow rather than an OAuth server.
|
|
83
|
+
|
|
84
|
+
The `ui_app` block in `app-config.json`:
|
|
85
|
+
|
|
86
|
+
- `extension_type` (root) — one of `actionLink`, `iframeExtension`, `legacyComponent` (camelCase; snake_case spellings like `action_link` are rejected).
|
|
87
|
+
- `surface_point_list` — one entry per placement. Each entry carries:
|
|
88
|
+
- `surface_point_name` — the placement's dot-notation slug from the platform's registry (e.g. `contactDetails.header.menu`). The valid names come from the registry — the CLI prompts from it at create time, and `brevo app upload` validates against it (an unregistered name is a 400 naming the offender). Do **not** author the extension-point name (`contactDetails.headerMenu.action`, the `<location>.<place>.<kind>` grammar) — it is dotted too and looks right because specs quote it, but it is a different string and upload rejects it.
|
|
89
|
+
- `label` — the menu entry's text (and the CTA button on a card). Required.
|
|
90
|
+
- `more_info` — supporting text under the menu entry / a card's description. Optional.
|
|
91
|
+
- `redirect_link` — the destination URL that entry opens; record context arrives as **query parameters** (the path is never templated).
|
|
92
|
+
- `context` — optional narrowing of the record fields passed along; it can only narrow what the platform allows for that slot.
|
|
93
|
+
- `size` — optional card size for the widget card this placement renders, e.g. `{ "width": "280px", "height": "160px" }`. Each axis is a CSS length string — a positive integer with an explicit `px` unit, or `1%`–`100%` of the host slot's box (shrink-only; >100% is rejected). Both axes are optional; an omitted axis (or the whole key) stays on the host slot's default.
|
|
94
|
+
- `modal_iframe_url` — `iframeExtension` entries only; rejected on an `actionLink`.
|
|
95
|
+
- Do **not** write `link_target` or `extension_point_name` anywhere in the file — both are wire/server-stamped values (`app upload` injects `link_target: "_blank"` itself) and the CLI strips them from server echoes.
|
|
96
|
+
- The old `heading`/`subheading` names are rejected with a migration hint — they are `label`/`more_info` now, and they live **per entry**, not at the `ui_app` root.
|
|
97
|
+
|
|
98
|
+
Install semantics worth knowing: only UI apps install into an account; the app must have a `version` (written by a successful `brevo app upload`) before `install` will proceed; `uninstall` of a not-installed app is informational (exit `0`).
|
|
99
|
+
|
|
100
|
+
**An installed UI app tracks the server's configuration, not the account's copy of it.** There is one stored snapshot per app, so `brevo app upload` changes what every account it is installed in renders, immediately and with no re-install — which is why `upload` warns and asks before pushing a UI app, and why `install` shows the stored configuration it is about to make visible. Tell a user to edit `app-config.json` and run `brevo app upload`; never tell them to uninstall and re-install to pick up a change.
|
|
59
101
|
|
|
60
102
|
## Locating the linked app
|
|
61
103
|
|
|
62
|
-
If `app-config.json` exists in the working directory, it pins the app — `brevo app
|
|
104
|
+
If `app-config.json` exists in the working directory, it pins the app — `brevo app upload` and `brevo app start` use it automatically. `brevo app start` accepts an `--app-id` override to target a different app; `upload` does **not** — it only ever reads cwd's `app-config.json`, hard-erroring if that file is missing, invalid, or lacks `appId`.
|
|
63
105
|
|
|
64
|
-
`app-config.json` carries an optional top-level `logoUri` string. When set,
|
|
106
|
+
`app-config.json` carries an optional top-level `logoUri` string. When set, `brevo app upload` pushes it as `logo_uri`; when empty / absent, the field is left untouched on the API.
|
|
107
|
+
|
|
108
|
+
`app-config.json` also carries a top-level `version` string, shown by `brevo app create`/`brevo app list`. `brevo app upload` sends it on the wire as `version` (falling back to the server's current value if locally absent) and writes back whatever version the server confirms after a successful upload.
|
|
109
|
+
|
|
110
|
+
`brevo app credentials` also backfills a legacy `app-config.json` toward the current shape: when the file exists in cwd and its `appId` matches the app being inspected, any missing top-level `version` / `distribution_type` is filled in from the server (fill-only-when-missing — an existing local value is never overwritten). This runs silently in all modes; human output prints a one-line note when something was written. It's how projects that are never `upload`ed still converge.
|
|
65
111
|
|
|
66
112
|
## Scopes
|
|
67
113
|
|
|
68
|
-
- New apps created via `brevo app create` default to `contacts:read`, `contacts:write`, `crm:read`, `crm:write`. The CLI prints the default set on success and points to `brevo app
|
|
69
|
-
-
|
|
114
|
+
- New apps created via `brevo app create` default to `contacts:read`, `contacts:write`, `crm:read`, `crm:write`. The CLI prints the default set on success and points to editing `app-config.json` + `brevo app upload` for changes.
|
|
115
|
+
- To add, remove, or change scopes: edit `auth.scopes` in `app-config.json` directly, then run `brevo app upload`. Comma- or whitespace-separated values are normalized on read, whether they sit in a single array entry (`["crm:read", "crm:write, campaigns:read"]`) or replace the array entirely (`"crm:write, campaigns:read"`) — either becomes two scopes. To see what's currently set, run `brevo app credentials --app-id <id> --json`.
|
|
70
116
|
- `brevo app available-scopes [--json] [--web]` lists the OAuth scopes the IdP currently supports. It reads a **public** catalog and works **without `brevo login`** (no API key needed). Text output groups names by category (e.g. `account`, `data_crm`, `messaging`); `--json` returns a flat `{ scopes: string[] }` of names. OIDC-reserved scopes (`openid`, `profile`, `email`, `offline_access`) and magic wildcards are excluded. The CLI validates scope **format** locally (must match `[A-Za-z0-9][A-Za-z0-9:_.-]*`) but does **not** validate that a scope is recognized by the IdP — use `app available-scopes` to confirm spelling before passing an unfamiliar scope.
|
|
71
117
|
- Passing `--web` to `brevo app available-scopes` **also opens a browser** to a styled local page (loopback `http://127.0.0.1:<port>/`) and stays running until Ctrl+C. Without `--web` the command exits after printing the list — TTY detection no longer triggers the browser. `--json` always suppresses the browser, so agent invocations using `--json` behave the same regardless of `--web`.
|
|
72
118
|
|
|
73
119
|
### Legacy `'all'` scope deprecation
|
|
74
120
|
|
|
75
|
-
The legacy catch-all `'all'` OAuth scope is deprecated. The CLI **blocks** `brevo app
|
|
121
|
+
The legacy catch-all `'all'` OAuth scope is deprecated. The CLI **blocks** `brevo app upload` and `brevo app start oauth` when scopes still contain `'all'` (no escape hatch, no silent rewrite); the only mutating path that proceeds is editing `auth.scopes` in `app-config.json` and running `upload`. To handle a legacy app:
|
|
76
122
|
|
|
77
123
|
1. **Detect** `'all'` in `auth.scopes` of a local `app-config.json`, or on a remote app via `brevo app list --json` — affected apps carry `"legacy_all_scope": true` (text output appends `(legacy 'all' — deprecated)` to the scopes line).
|
|
78
124
|
2. **Prompt the user to pick granular scopes** — use `brevo app available-scopes --json` for the catalog, or fall back to the four defaults (`contacts:read`, `contacts:write`, `crm:read`, `crm:write`).
|
|
79
|
-
3. **Migrate**
|
|
125
|
+
3. **Migrate** by editing `auth.scopes` in `app-config.json` to replace `'all'` with the granular scopes your integration uses, then run `brevo app upload` (the summary shows a "Migrating from legacy 'all' scope" line and `all (removed)`).
|
|
80
126
|
|
|
81
|
-
`
|
|
127
|
+
Writing `app-config.json` for an app whose remote scopes contain `'all'` never propagates it: the file keeps the app's remaining granular scopes (or the four default scopes when `'all'` was the only scope), with a one-line substitution notice (suppressed under `--json`). This applies when `brevo app create` writes the base config, and when `brevo app scaffold` rewrites it after a detected server drift. Note the substitution is local-only — the remote app still needs the `app-config.json` edit + `brevo app upload` migration above.
|
|
82
128
|
|
|
83
129
|
## Exit codes
|
|
84
130
|
|
|
85
131
|
`0` success · `1` general error · `2` aborted · `3` auth failure · `4` network · `5` not found.
|
|
86
132
|
|
|
133
|
+
## JSON errors
|
|
134
|
+
|
|
135
|
+
Under `--json`, a command that fails writes **one** JSON document to stdout describing the failure, and the human-readable message still goes to stderr. The `error` key is the discriminator — no success payload has one:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{ "error": { "name": "CliError", "message": "Not authenticated. Run: brevo login", "exitCode": 1 } }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`name` is the error class (`CliError`, `ApiError`, `AuthExpiredError`, `AbortError`), `message` is the same text printed to stderr, and `exitCode` matches the process exit code. An `ApiError` adds `statusCode` (the HTTP status) and, when the API classified the failure, `code` — one of `AUTH_INVALID`, `AUTH_EXPIRED`, `ACCESS_DENIED`, `APP_NOT_FOUND`, `REDIRECT_INVALID`, `PORT_IN_USE`, `NETWORK_ERROR`, `RATE_LIMITED`, `APP_LIMIT_REACHED`, `REGISTRY_ERROR`, `AUTH_GATEWAY`:
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{ "error": { "name": "ApiError", "message": "App not found", "exitCode": 5, "code": "APP_NOT_FOUND", "statusCode": 404 } }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Two things to rely on: stdout is always **exactly one** parseable document, and commands that describe their own failure keep doing so instead of emitting this envelope — `brevo whoami --json` still returns `{"authenticated": false, "reason": "no_key"}` (exit `1`). Check for `error` first, then fall back to the command's own shape.
|
|
148
|
+
|
|
149
|
+
## Command help
|
|
150
|
+
|
|
151
|
+
`brevo --help` prints the grouped overview of every command. `brevo <command> --help` prints that command's own usage line, arguments, flags, and examples — e.g. `brevo app scaffold --help` documents `--app-id` / `--overwrite` / `--json`. When you need to confirm a flag exists on the version actually installed, read it from there rather than assuming from this file.
|
|
152
|
+
|
|
87
153
|
## Forced update
|
|
88
154
|
|
|
89
155
|
When the installed CLI is a full **major** version behind the latest npm release, every command except `--help` / `--version` prints a blocking update banner to stderr and exits `1` **without running** — so a `brevo` call that suddenly exits `1` with an update banner means the CLI must be upgraded (`npm install -g @getbrevo/cli` or `yarn global add @getbrevo/cli`) before it will work. The gate honors the same opt-outs as the soft update notice (`BREVO_NO_UPDATE_NOTIFIER=1`, `--no-update-notifier`, CI, non-TTY), so it never fires in those contexts.
|
|
90
156
|
|
|
157
|
+
## Update notice wording
|
|
158
|
+
|
|
159
|
+
The update banner's first line comes from the app-store service (`GET /cli/info`). It is called directly rather than through the v3 API gateway and requires no API key, so it works while logged out or with expired credentials. It runs once per invocation, **before** the command, and the response is cached at `~/.brevo/cli-info-cache.json` for **15 minutes**, keyed to the installed `cliVersion` — so reworded text or a new block reaches the CLI within minutes rather than after the old 12h npm-style TTL, without a network call on every single command. Whether an update banner appears is still decided from the npm registry, exactly as before, and a failed call just means the banner uses local wording (a failed call never overwrites a good cache entry). `BREVO_APP_STORE_URL` overrides that service's base URL for non-production testing.
|
|
160
|
+
|
|
161
|
+
## Server-side block
|
|
162
|
+
|
|
163
|
+
The same response may carry `"is_blocked": true`. When it does, every command except `--help` / `--version` prints a banner to stderr and exits `1` **without running** — a separate mechanism from the major-version gate above, and one the server can turn on without shipping a new CLI.
|
|
164
|
+
|
|
165
|
+
Those two are exempt from the **block**, not from the lookup: they still call `/cli/info` and still render the server's wording on their update banner. They simply never exit non-zero because of it, so a blocked CLI can always still report what version it is.
|
|
166
|
+
|
|
167
|
+
Two things to know when you hit this:
|
|
168
|
+
|
|
169
|
+
- **The notice opt-outs do not apply.** `BREVO_NO_UPDATE_NOTIFIER=1`, `--no-update-notifier`, CI, and a non-TTY all suppress the *notice*; none of them suppress a block. Don't suggest them as a workaround — the only fix is upgrading the CLI.
|
|
170
|
+
- **It fails open.** A timeout, a non-2xx, or an unparseable body lets the command run normally; only a literal `true` blocks. So a network problem never manifests as a block.
|
|
171
|
+
- **A block can persist up to 15 minutes after the server lifts it, and vice versa**, because the whole response is cached. If you need a verdict that reflects the server's *current* state, delete `~/.brevo/cli-info-cache.json` (or bump the version) before checking.
|
|
172
|
+
|
|
173
|
+
The soft (non-blocking) notice also prints after a command **fails**, not just after it succeeds — so stderr can hold the error message followed by the update box. This does not change exit codes: a failing command still exits with its own code, and the box is never printed twice in one run. A Ctrl-C abort skips it. If you parse stderr strictly, suppress the notice with `BREVO_NO_UPDATE_NOTIFIER=1` or `--no-update-notifier`.
|
|
174
|
+
|
|
91
175
|
## Before sharing or committing output
|
|
92
176
|
|
|
93
177
|
* [ ] No `xkeysib-…` API keys, client secrets, refresh tokens, or contents of `~/.brevo/credentials.json` / `.env.local` in messages, logs, or diffs.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# {{APP_NAME}} — Agent Context
|
|
2
|
+
{{#if oauth}}
|
|
3
|
+
|
|
4
|
+
This project is a Brevo OAuth 2.0 integration scaffolded with the Brevo CLI.
|
|
5
|
+
|
|
6
|
+
> **This is a test app** for local OAuth testing. Not for production use.
|
|
7
|
+
|
|
8
|
+
## What this does
|
|
9
|
+
Implements the authorization code flow to obtain access tokens for the Brevo API.
|
|
10
|
+
|
|
11
|
+
## Project structure
|
|
12
|
+
```
|
|
13
|
+
.
|
|
14
|
+
├── app-config.json App metadata (app ID, redirect URIs, scopes, logo URL)
|
|
15
|
+
├── .gitignore Ignores .env.local and build artifacts
|
|
16
|
+
├── AGENTS.md AI agent guidance (this file)
|
|
17
|
+
├── CLAUDE.md Claude Code guidance
|
|
18
|
+
├── README.md Human-facing setup guide
|
|
19
|
+
└── src/
|
|
20
|
+
└── oauth/ Self-contained OAuth test feature
|
|
21
|
+
├── package.json Dependencies and scripts
|
|
22
|
+
├── server.js Test server — Express app + dotenv + routes
|
|
23
|
+
├── handler.js OAuth routes (/auth/login, /auth/callback, /auth/refresh)
|
|
24
|
+
├── token-store.js In-memory token management (swap for DB in production)
|
|
25
|
+
├── .env.example Credential template (safe to commit)
|
|
26
|
+
└── .env.local Local credentials (gitignored — never commit)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## OAuth server
|
|
30
|
+
- OAuth uses realm-based paths: `/realms/{realm}/oauth/...`
|
|
31
|
+
- Default realm: `partner`
|
|
32
|
+
- Token endpoint expects `application/x-www-form-urlencoded` body (not query params)
|
|
33
|
+
- Grant types used by this scaffold: `authorization_code` (callback) and `refresh_token` (`/auth/refresh`)
|
|
34
|
+
- Other grant types supported by the Brevo OAuth server but not wired into this scaffold: `client_credentials`
|
|
35
|
+
|
|
36
|
+
## OAuth flow
|
|
37
|
+
{{#if private}}
|
|
38
|
+
1. `/auth/login` — generates CSRF state, redirects user to `{OAUTH_BASE}/realms/{realm}/oauth/authorize`
|
|
39
|
+
2. User authorizes -> OAuth server redirects to `/auth/callback` with an auth code
|
|
40
|
+
3. Server validates state, exchanges code for access + refresh tokens via `POST /realms/{realm}/oauth/token` (authenticated with `client_secret`)
|
|
41
|
+
4. Verifies connection via `GET /realms/{realm}/oauth/userinfo`
|
|
42
|
+
5. `/auth/refresh` — refreshes tokens using `refresh_token` grant
|
|
43
|
+
{{/if}}
|
|
44
|
+
{{#if public}}
|
|
45
|
+
This is a **public app** — the flow uses **PKCE (RFC 7636)** and sends **no `client_secret`**:
|
|
46
|
+
1. `/auth/login` — generates CSRF state + a PKCE `code_verifier`, then redirects user to `{OAUTH_BASE}/realms/{realm}/oauth/authorize` with `code_challenge` (S256) + `code_challenge_method=S256`
|
|
47
|
+
2. User authorizes -> OAuth server redirects to `/auth/callback` with an auth code
|
|
48
|
+
3. Server validates state, exchanges code + `code_verifier` (no `client_secret`) for access + refresh tokens via `POST /realms/{realm}/oauth/token`
|
|
49
|
+
4. Verifies connection via `GET /realms/{realm}/oauth/userinfo`
|
|
50
|
+
5. `/auth/refresh` — refreshes tokens using `refresh_token` grant (no `client_secret`)
|
|
51
|
+
{{/if}}
|
|
52
|
+
|
|
53
|
+
## Development
|
|
54
|
+
```bash
|
|
55
|
+
yarn --cwd src/oauth install
|
|
56
|
+
# or: npm --prefix src/oauth install
|
|
57
|
+
brevo app start oauth # port is read from app-config.json; override with --port
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Docs
|
|
61
|
+
- [Brevo API Documentation](https://developers.brevo.com)
|
|
62
|
+
- [Brevo CLI reference](https://developers.brevo.com/docs/cli-reference) — full command and option list
|
|
63
|
+
- [Brevo CLI repo](https://github.com/getbrevo/brevo-cli)
|
|
64
|
+
{{/if}}
|
|
65
|
+
{{#if ui_app}}
|
|
66
|
+
|
|
67
|
+
This project is a Brevo **UI app** scaffolded with the Brevo CLI — an action link that renders inside Brevo CRM records.
|
|
68
|
+
|
|
69
|
+
> **Nothing runs locally.** A UI app has no OAuth callback and no scaffolded server code. Brevo opens a URL you already host; this project is the app's configuration.
|
|
70
|
+
|
|
71
|
+
## What this does
|
|
72
|
+
Declares where the app appears inside Brevo's CRM (which record page, which slot) and where Brevo sends the user when they click it. The whole app is the `ui_app` block in `app-config.json`.
|
|
73
|
+
|
|
74
|
+
## Project structure
|
|
75
|
+
```
|
|
76
|
+
.
|
|
77
|
+
├── app-config.json App metadata + the `ui_app` block (the whole app)
|
|
78
|
+
├── .gitignore Ignores .env.local and build artifacts
|
|
79
|
+
├── AGENTS.md AI agent guidance (this file)
|
|
80
|
+
├── CLAUDE.md Claude Code guidance
|
|
81
|
+
└── README.md Human-facing setup guide
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## The `ui_app` contract
|
|
85
|
+
- The presence of `ui_app` is the app-type discriminator. There is no `appType` key.
|
|
86
|
+
- `auth` must be exactly `{}` — a UI app uses no OAuth, and `brevo app upload` refuses one carrying `scopes` or `redirectUris`.
|
|
87
|
+
- `extension_type` is the only field at the `ui_app` root: `actionLink` (opens `redirect_link` in a new tab) or `iframeExtension` (embeds `modal_iframe_url` in a modal). camelCase only — `action_link` is rejected, not aliased.
|
|
88
|
+
- Everything else lives **per entry** in `surface_point_list`, so each placement carries its own text and destination:
|
|
89
|
+
- `surface_point_name` (required) — the registry slug in dot notation, e.g. `contactDetails.header.menu`. Not the dotted extension-point name a spec quotes (`contactDetails.headerMenu.action`).
|
|
90
|
+
- `label` (required, max 48) — the menu entry's text on an `.action` slot; a card's CTA button on a `.widget` slot.
|
|
91
|
+
- `more_info` (optional, max 255) — the menu entry's second line; a card's description.
|
|
92
|
+
- `redirect_link` (`actionLink`) / `modal_iframe_url` (`iframeExtension`) — `https://`, or `http://localhost`. Each is rejected on the other extension type.
|
|
93
|
+
- `context` (optional) — narrows the record context Brevo sends; omitted, the slot's full allow-list applies.
|
|
94
|
+
- `size` (optional) — `{ "width": "280px", "height": "160px" }`; a positive integer with an explicit `px` or `%` unit, `%` capped at 100.
|
|
95
|
+
- A card's title is the app name. There is no field for it.
|
|
96
|
+
- Never author `link_target`, `version` or `extension_point_name` — the platform owns them, `brevo app upload` injects or strips them, and a copy in the file reads as permanent drift in the upload diff.
|
|
97
|
+
- Root-level `label` / `more_info` / `redirect_link` / `modal_iframe_url` / `context` / `link_target`, and the older `heading` / `subheading`, are all refused by name with a migration hint.
|
|
98
|
+
|
|
99
|
+
## Record context
|
|
100
|
+
Brevo appends the record context to the destination URL as **query parameters** — the path is never templated. Typical fields: `recordId`, `recordType`, `accountId`, `userId`, `clientId`, `extId`, `locale`. They arrive via the browser, so treat them as untrusted input.
|
|
101
|
+
|
|
102
|
+
## Placements
|
|
103
|
+
`brevo app create` authors exactly one placement. Add more by hand as extra `surface_point_list` entries — each with its own `label` and destination — then `brevo app upload`.
|
|
104
|
+
|
|
105
|
+
Slot names are **not** validated locally: the platform's extension-point registry is the only authority and the CLI keeps no copy that could lag it. `app upload` sends the block and the platform rejects the upload naming any unregistered slot.
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
```bash
|
|
109
|
+
brevo app upload # validate + save the configuration (diff shown first)
|
|
110
|
+
brevo app install # make it available in an account
|
|
111
|
+
brevo app uninstall # remove it from that account
|
|
112
|
+
```
|
|
113
|
+
`brevo app upload` has no edit flags — it pushes the whole of `app-config.json`. `install` / `uninstall` take an optional `[account-id]`; omitted, the target resolves from the authenticated account.
|
|
114
|
+
|
|
115
|
+
## Docs
|
|
116
|
+
- [Brevo API Documentation](https://developers.brevo.com)
|
|
117
|
+
- [Brevo CLI reference](https://developers.brevo.com/docs/cli-reference) — full command and option list
|
|
118
|
+
- [Brevo CLI repo](https://github.com/getbrevo/brevo-cli)
|
|
119
|
+
{{/if}}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# CLAUDE.md — {{APP_NAME}}
|
|
2
|
+
{{#if oauth}}
|
|
3
|
+
|
|
4
|
+
## Project
|
|
5
|
+
Brevo OAuth integration for app `{{APP_ID}}`.
|
|
6
|
+
Scaffolded with the [Brevo CLI](https://github.com/getbrevo/brevo-cli) — see the [Brevo CLI reference](https://developers.brevo.com/docs/cli-reference) for the full command list.
|
|
7
|
+
|
|
8
|
+
> **This is a test app** scaffolded by the Brevo CLI to help you test your OAuth integration locally.
|
|
9
|
+
|
|
10
|
+
## Run
|
|
11
|
+
```bash
|
|
12
|
+
yarn --cwd src/oauth install
|
|
13
|
+
# or: npm --prefix src/oauth install
|
|
14
|
+
brevo app start oauth # port is read from app-config.json; override with --port
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Structure
|
|
18
|
+
```
|
|
19
|
+
.
|
|
20
|
+
├── app-config.json App metadata (ID, scopes, redirect URLs, logo URL)
|
|
21
|
+
├── .gitignore Ignores .env.local and build artifacts
|
|
22
|
+
├── AGENTS.md AI agent guidance
|
|
23
|
+
├── CLAUDE.md Claude Code guidance (this file)
|
|
24
|
+
├── README.md Human-facing setup guide
|
|
25
|
+
└── src/
|
|
26
|
+
└── oauth/ Self-contained OAuth test feature
|
|
27
|
+
├── package.json Dependencies and scripts
|
|
28
|
+
├── server.js Test server entry point
|
|
29
|
+
├── handler.js OAuth routes (/auth/login, /auth/callback, /auth/refresh)
|
|
30
|
+
├── token-store.js In-memory token store
|
|
31
|
+
├── .env.example Credential template (committed)
|
|
32
|
+
└── .env.local Local credentials (gitignored — never commit)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## OAuth server
|
|
36
|
+
- OAuth endpoints use realm-based paths: `/realms/{realm}/oauth/...`
|
|
37
|
+
- Default realm: `partner`
|
|
38
|
+
- Env vars `OAUTH_BASE` and `OAUTH_REALM` control the OAuth server and realm
|
|
39
|
+
|
|
40
|
+
### Endpoints used
|
|
41
|
+
| Endpoint | Purpose |
|
|
42
|
+
|----------|---------|
|
|
43
|
+
| `GET /realms/{realm}/oauth/authorize` | Authorization (redirect user here) |
|
|
44
|
+
| `POST /realms/{realm}/oauth/token` | Exchange code or refresh token for access token |
|
|
45
|
+
| `GET /realms/{realm}/oauth/userinfo` | Fetch user profile with Bearer token |
|
|
46
|
+
| `POST /realms/{realm}/oauth/revoke` | Revoke a token |
|
|
47
|
+
| `POST /realms/{realm}/oauth/introspect` | Token introspection (RFC 7662) |
|
|
48
|
+
| `GET /realms/{realm}/.well-known/openid-configuration` | OIDC discovery |
|
|
49
|
+
|
|
50
|
+
## Key flows
|
|
51
|
+
{{#if private}}
|
|
52
|
+
- `/auth/login` — generates random CSRF state, redirects to OAuth authorize endpoint
|
|
53
|
+
- `/auth/callback` — validates state, POSTs code to `/realms/{realm}/oauth/token`, stores tokens
|
|
54
|
+
- `/auth/refresh` — refreshes tokens using `refresh_token` grant
|
|
55
|
+
{{/if}}
|
|
56
|
+
{{#if public}}
|
|
57
|
+
This is a **public app**, so the flow uses **PKCE (RFC 7636)** and no client secret:
|
|
58
|
+
- `/auth/login` — generates random CSRF state **and a PKCE `code_verifier`**, then redirects to the OAuth authorize endpoint sending `code_challenge` (S256 hash of the verifier) + `code_challenge_method=S256`. The verifier is stored server-side and never sent to the browser.
|
|
59
|
+
- `/auth/callback` — validates state, POSTs code **plus the `code_verifier`** (no `client_secret`) to `/realms/{realm}/oauth/token`, stores tokens
|
|
60
|
+
- `/auth/refresh` — refreshes tokens using the `refresh_token` grant (no `client_secret`)
|
|
61
|
+
{{/if}}
|
|
62
|
+
|
|
63
|
+
## Next steps
|
|
64
|
+
- Replace the in-memory token store in `src/oauth/token-store.js` with a real database (the global singleton is process-local so data is lost on restart, and it is not session-isolated, so multiple users/sessions/tabs can read or overwrite the same in-memory tokens)
|
|
65
|
+
- Handle token-refresh failures — `/auth/refresh` currently returns a 500 on any error; surface 401 vs 5xx distinctly so callers can re-trigger `/auth/login` when the refresh token has expired
|
|
66
|
+
- Add `/auth/logout` that calls `POST /realms/{realm}/oauth/revoke` and clears the token store (the endpoint is supported by the OAuth server but the scaffold doesn't use it yet)
|
|
67
|
+
- Add automated tests around the callback and refresh routes (e.g. with `supertest` + axios mocks) before extending the flow
|
|
68
|
+
- Persist (or rotate) the CSRF `state` per session — the scaffold stores a single global state, which is fine for solo localhost testing but not for anything else
|
|
69
|
+
{{/if}}
|
|
70
|
+
{{#if ui_app}}
|
|
71
|
+
|
|
72
|
+
## Project
|
|
73
|
+
Brevo **UI app** `{{APP_ID}}` — an action link that renders inside Brevo CRM records.
|
|
74
|
+
Scaffolded with the [Brevo CLI](https://github.com/getbrevo/brevo-cli) — see the [Brevo CLI reference](https://developers.brevo.com/docs/cli-reference) for the full command list.
|
|
75
|
+
|
|
76
|
+
> **There is no local server to run.** A UI app has no OAuth callback and no scaffolded code — Brevo opens a URL you already host. This project *is* the app's configuration.
|
|
77
|
+
|
|
78
|
+
## Structure
|
|
79
|
+
```
|
|
80
|
+
.
|
|
81
|
+
├── app-config.json App metadata + the `ui_app` block (the whole app)
|
|
82
|
+
├── .gitignore Ignores .env.local and build artifacts
|
|
83
|
+
├── AGENTS.md AI agent guidance
|
|
84
|
+
├── CLAUDE.md Claude Code guidance (this file)
|
|
85
|
+
└── README.md Human-facing setup guide
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Workflow
|
|
89
|
+
Everything about this app lives in the `ui_app` block of `app-config.json`. Edit the file, then:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
brevo app upload # validate + save the configuration (shows a diff first)
|
|
93
|
+
brevo app install # make it available in an account
|
|
94
|
+
brevo app uninstall # remove it from that account again
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`brevo app upload` has no edit flags — it always pushes the whole of `app-config.json`. `install` and `uninstall` both take an optional `[account-id]`; omitted, the target resolves from the authenticated account (a corporate account is asked which sub-account).
|
|
98
|
+
|
|
99
|
+
## The `ui_app` block
|
|
100
|
+
The presence of `ui_app` is the app-type discriminator — it is what makes this a UI app rather than an OAuth one. That is why `auth` is `{}`, and it must stay empty: `brevo app upload` refuses a UI app carrying `scopes` or `redirectUris`.
|
|
101
|
+
|
|
102
|
+
`extension_type` is the only field at the root, because an app is one kind of extension rather than several:
|
|
103
|
+
|
|
104
|
+
| Value | What it does | Destination field |
|
|
105
|
+
|-------|--------------|-------------------|
|
|
106
|
+
| `actionLink` | Opens your URL in a new tab | `redirect_link` |
|
|
107
|
+
| `iframeExtension` | Embeds your URL in a modal | `modal_iframe_url` |
|
|
108
|
+
|
|
109
|
+
Values are camelCase. The older snake_case spellings (`action_link`) are rejected outright, not aliased.
|
|
110
|
+
|
|
111
|
+
### `surface_point_list` entries
|
|
112
|
+
One entry is one placement, and each carries its own text and destination — so an app on three slots can label and deep-link each differently.
|
|
113
|
+
|
|
114
|
+
| Key | Required | Notes |
|
|
115
|
+
|-----|----------|-------|
|
|
116
|
+
| `surface_point_name` | yes | The registry **slug**, in dot notation — e.g. `contactDetails.header.menu`. This is *not* the dotted extension-point name a spec quotes (`contactDetails.headerMenu.action`); authoring that one fails validation at upload. |
|
|
117
|
+
| `label` | yes | Max 48 chars. The menu entry's text on an `.action` slot; the CTA button on a `.widget` slot's card. |
|
|
118
|
+
| `more_info` | no | Max 255 chars. The menu entry's second line; a card's description. |
|
|
119
|
+
| `redirect_link` | for `actionLink` | `https://` (or `http://localhost`). Record context arrives as **query parameters** — the path is never templated. |
|
|
120
|
+
| `modal_iframe_url` | for `iframeExtension` | Rejected on an `actionLink`, which navigates rather than embedding. |
|
|
121
|
+
| `context` | no | Narrows the record context Brevo sends. Omitted, the slot's full allow-list applies. |
|
|
122
|
+
| `size` | no | `{ "width": "280px", "height": "160px" }` — a positive integer with an explicit `px` or `%` unit (`%` capped at 100). An omitted axis keeps the slot's default. |
|
|
123
|
+
|
|
124
|
+
A card's **title** is the app name (`{{APP_NAME}}`) — there is no field for it.
|
|
125
|
+
|
|
126
|
+
`label`, `more_info`, `redirect_link`, `modal_iframe_url`, `context` and `size` all live **per entry**, never at the `ui_app` root. The root spellings are refused by name with a migration hint, as are the older `heading` / `subheading`.
|
|
127
|
+
|
|
128
|
+
Do not add `link_target`, `version` or `extension_point_name`. The platform owns all three — `brevo app upload` injects or strips them — so a copy in this file only shows up as permanent, unfixable drift in the upload diff.
|
|
129
|
+
|
|
130
|
+
## Adding a placement
|
|
131
|
+
`brevo app create` authors exactly one. Add further placements by hand as extra `surface_point_list` entries, each with its own `label` and destination, then run `brevo app upload`.
|
|
132
|
+
|
|
133
|
+
Slot names are **not** checked locally, by design: the platform's extension-point registry is the only authority on them and the CLI keeps no copy that could lag it. `brevo app upload` sends the block and the platform rejects the upload naming any unregistered slot — so let `upload` be the check rather than guessing.
|
|
134
|
+
|
|
135
|
+
## Next steps
|
|
136
|
+
- Host the `redirect_link` endpoint and read the record context off the query string (`recordId`, `recordType`, `accountId`, …) — treat every parameter as untrusted input; it arrives via the browser
|
|
137
|
+
- Run `brevo app upload`, then `brevo app install`, then open a CRM record to see the placement
|
|
138
|
+
- Keep `auth` as `{}` — a UI app uses no OAuth, and upload refuses one that carries OAuth fields
|
|
139
|
+
{{/if}}
|