@getbrevo/cli 2.2.0 → 2.2.1

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @getbrevo/cli
2
2
 
3
+ ## 2.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 6a840d0: `brevo app create` seeds each authored placement's `size` from the slot's registry default (`default_size` on `GET /v3/app-store/surface-points`, BEX-461), the same way `context` is seeded from `default_context_field`: written explicitly into `app-config.json`, where it can be edited or removed — the entry's own value is what uploads. A slot with no declared default writes no `size` key, and a blank or malformed served default degrades to "no seed" rather than authoring a value the CLI's own validation would refuse. There is still no size prompt.
8
+
3
9
  ## 2.2.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -108,6 +108,34 @@ Most commands require a successful `brevo login` first, except authentication/he
108
108
 
109
109
  The table above is the complete command surface of a published release. Features that aren't live on the Brevo platform yet aren't built into the package — `brevo --help` always lists everything the binary can do, so there is nothing hidden behind a flag or an environment variable.
110
110
 
111
+ ### UI apps
112
+
113
+ `brevo app create`'s interactive prompt can build two kinds of app: an OAuth app, or a **UI app**
114
+ that renders directly inside a Brevo CRM record (interactive-only — there is no `--type` flag, so
115
+ `--json` and piped runs always create an OAuth app).
116
+
117
+ Today the prompt authors one integration type, an **action link** (`extension_type: "actionLink"`).
118
+ In short: it's a clickable menu entry or card CTA button that Brevo renders on a record page — no
119
+ embed, no iframe — and clicking it just opens a URL you host, with the record's data passed along
120
+ as query parameters, never in the path. Each authored placement lives in `app-config.json` under
121
+ `ui_app.surface_point_list` and carries:
122
+
123
+ - `surface_point_name` — which slot on which record page, chosen from Brevo's live registry at
124
+ create time
125
+ - `label` — the menu entry's text, or the card's CTA button
126
+ - `more_info` *(optional)* — a supporting line under the menu entry / card description
127
+ - `redirect_link` — the destination URL
128
+ - `context` *(optional)* — which record fields to pass along as query parameters, narrowed from
129
+ whatever that slot allows
130
+ - `size` *(optional)* — card sizing, e.g. `{ "width": "280px", "height": "160px" }`; seeded from
131
+ the slot's own registry default when it declares one, and freely editable afterwards
132
+
133
+ The interactive flow authors exactly one placement per run. More placements — or edits to any
134
+ field above — are hand-added as further `surface_point_list` entries in `app-config.json` and
135
+ pushed with `brevo app upload`, which validates every entry against the registry before it goes
136
+ live. See [Uploading a UI app that is already installed](#uploading-a-ui-app-that-is-already-installed)
137
+ below for what that push looks like.
138
+
111
139
  ### Uploading a UI app that is already installed
112
140
 
113
141
  A UI app's `ui_app` block is what every account it is installed in renders, and there is no
@@ -143,6 +171,7 @@ Environment overrides:
143
171
 
144
172
  - `BREVO_API_URL` — points the CLI at a different Brevo API (defaults to `https://api.brevo.com`).
145
173
  - `BREVO_OAUTH_PROXY_URL` — points the browser-login flow at a different OAuth proxy (defaults to `https://oauth-cli.brevo.com`; useful for local development or non-default environments).
174
+ - `BREVO_OAUTH_BASE_URL` — points scope lookups and scaffolded project templates at a different OAuth realm (defaults to `https://oauth.brevo.com`).
146
175
 
147
176
  ## Exit codes
148
177
 
@@ -162,6 +191,7 @@ Environment overrides:
162
191
  | `BREVO_API_KEY` | API key used for non-interactive `brevo login` | – |
163
192
  | `BREVO_API_URL` | API base URL (HTTPS required, except for `localhost`) | `https://api.brevo.com` |
164
193
  | `BREVO_OAUTH_PROXY_URL` | OAuth proxy used by browser login (HTTPS required, except for `localhost`) | `https://oauth-cli.brevo.com` |
194
+ | `BREVO_OAUTH_BASE_URL` | OAuth realm used for scope lookups and scaffolded project templates (HTTPS required, except for `localhost`) | `https://oauth.brevo.com` |
165
195
  | `BREVO_CONFIG_HOME` | Override for the credentials directory | `~/.brevo/` |
166
196
  | `BREVO_NO_SKILL_AUTOREFRESH` | Set to `1` to suppress automatic skill refresh on `brevo` runs | off |
167
197
  | `NO_COLOR` / `FORCE_COLOR` | Disable / force ANSI colour output | – |
@@ -94,7 +94,7 @@ Run `brevo --help` or `brevo <command> --help` for the full set.
94
94
  ## Conventions
95
95
 
96
96
  - **Every command supports `--json`** — prefer this when parsing output programmatically. It applies to failures too: a failing `--json` run writes a single `{"error": {...}}` document to stdout (see *JSON errors* below) while the human message goes to stderr.
97
- - **Two app types, one command surface.** `app-config.json` describes either an **OAuth app** — a populated `auth` block (`auth.scopes` / `auth.redirectUris`) and no `ui_app` — or a **UI app** — a `ui_app` block and an **empty** `auth: {}` (no callbacks, scopes, or credentials). The presence of `ui_app` is the discriminator; never mix the two in one file. Both types share the top-level `appId` / `appName` / `logoUri` / `version` / `distribution_type` and the same `create`/`upload`/`list`/`delete` commands; only UI apps take `install`/`uninstall`. **A UI app has one stored configuration, shared by every account it is installed in** — `brevo app upload` is therefore how you change what an installed app renders (edit `app-config.json`, upload, done), never uninstall-then-reinstall; both commands show what is about to change for that reason. The `ui_app` block holds `extension_type` at its root (`actionLink`, `iframeExtension`, `legacyComponent` — camelCase only) and a `surface_point_list` of placement entries, each carrying `surface_point_name` (the dot-notation slug from the platform's registry, e.g. `contactDetails.header.menu` — not the `<location>.<place>.<kind>` extension-point name like `contactDetails.headerMenu.action`, which is dotted too but a different string), `label`, optional `more_info`, `redirect_link` (record context arrives as query parameters), optional `context`, optional `size` (e.g. `{ "width": "280px", "height": "160px" }` — each axis a positive-integer `px` length or `1%`–`100%` of the host slot, shrink-only, both axes optional), and — `iframeExtension` only — `modal_iframe_url`. Do **not** write `link_target` or `extension_point_name` into the file: both are wire/server-stamped (`app upload` injects `link_target` itself). Write only the keys documented here — `brevo app upload` validates the whole file and rejects anything it doesn't recognise, including the pre-GA `heading`/`subheading` names (now `label`/`more_info`, per entry). A UI-app project is **configuration only** — no feature to scaffold and no `src/oauth/`, since an action link has no local server (`brevo app scaffold` inside one says so and exits `0`; `brevo app start` does not apply) — and the base docs the scaffold writes (`AGENTS.md` / `CLAUDE.md` / `README.md`) describe whichever type the app is, so a UI app's copies cover the `ui_app` block and the `upload` → `install` flow instead of an OAuth server.
97
+ - **Two app types, one command surface.** `app-config.json` describes either an **OAuth app** — a populated `auth` block (`auth.scopes` / `auth.redirectUris`) and no `ui_app` — or a **UI app** — a `ui_app` block and an **empty** `auth: {}` (no callbacks, scopes, or credentials). The presence of `ui_app` is the discriminator; never mix the two in one file. Both types share the top-level `appId` / `appName` / `logoUri` / `version` / `distribution_type` and the same `create`/`upload`/`list`/`delete` commands; only UI apps take `install`/`uninstall`. **A UI app has one stored configuration, shared by every account it is installed in** — `brevo app upload` is therefore how you change what an installed app renders (edit `app-config.json`, upload, done), never uninstall-then-reinstall; both commands show what is about to change for that reason. The `ui_app` block holds `extension_type` at its root (`actionLink`, `iframeExtension`, `legacyComponent` — camelCase only) and a `surface_point_list` of placement entries, each carrying `surface_point_name` (the dot-notation slug from the platform's registry, e.g. `contactDetails.header.menu` — not the `<location>.<place>.<kind>` extension-point name like `contactDetails.headerMenu.action`, which is dotted too but a different string), `label`, optional `more_info`, `redirect_link` (record context arrives as query parameters), optional `context`, optional `size` (e.g. `{ "width": "280px", "height": "160px" }` — each axis a positive-integer `px` length or `1%`–`100%` of the host slot, shrink-only, both axes optional; `brevo app create` seeds it from the slot's registry default when the platform declares one, same mechanism as `context` — the entry's own value is what uploads), and — `iframeExtension` only — `modal_iframe_url`. Do **not** write `link_target` or `extension_point_name` into the file: both are wire/server-stamped (`app upload` injects `link_target` itself). Write only the keys documented here — `brevo app upload` validates the whole file and rejects anything it doesn't recognise, including the pre-GA `heading`/`subheading` names (now `label`/`more_info`, per entry). A UI-app project is **configuration only** — no feature to scaffold and no `src/oauth/`, since an action link has no local server (`brevo app scaffold` inside one says so and exits `0`; `brevo app start` does not apply) — and the base docs the scaffold writes (`AGENTS.md` / `CLAUDE.md` / `README.md`) describe whichever type the app is, so a UI app's copies cover the `ui_app` block and the `upload` → `install` flow instead of an OAuth server.
98
98
  - **`brevo app create` refuses to run inside an already-linked directory.** If `app-config.json` exists in cwd, it throws immediately (no confirm, no override) — the error points at moving elsewhere or running `brevo app scaffold` there.
99
99
  - **`brevo app create` resolves its target directory before creating the app**, then writes the **basic project structure only** (`app-config.json` + `.gitignore`/`AGENTS.md`/`CLAUDE.md`/`README.md`) — the OAuth server code is a *feature*, not part of the base. Interactive mode prompts for the target directory (default `./<slugified-app-name>`, `cd`s into it) before the API call, how to handle an existing one (overwrite / merge / choose a different path), and — after the app is created — whether to scaffold a feature (*"Scaffold the Test OAuth App?"*, default **yes**). There is no follow-up "which feature?" question while the CLI ships one: a list of one is not asked, and the confirm names it instead. A second feature would bring the picker back. Non-interactive runs stay base-only: `--json` (and piped, non-TTY) create the app and write the base files but never scaffold a feature — run `brevo app scaffold` afterward for the OAuth code. Under `--json` the same default directory is used and `cd`d into if it doesn't already exist; if it already exists, both directory setup and scaffolding are skipped (the app is still created). The JSON response always includes `directory` (absolute path) alongside the app fields, plus either `scaffolded` (base file count, on success) or `scaffoldSkipped` (a message, when the directory already existed).
100
100
  - **`brevo app scaffold` adds a feature to an already-created project, or sets an empty directory up for an app that already exists.** It **requires** an `app-config.json` in cwd unless `--app-id` is passed or its bootstrap offer is accepted, and only the bootstrap mode ever creates a directory (the feature-add mode always writes into the project it was run in). **`--app-id <id>` bootstraps a project for an app that already exists**: it fetches the app, writes `app-config.json` + the base files, and then continues into the feature flow. That is the only command that produces a config for an existing app (`app create` creates a new one, `app upload` only reads the linked project), which makes it the migration path off the removed `brevo app update --app-id`. **Interactively, `--app-id` is optional**: in a config-less directory the command explains there is no app here, asks *"Set up a project for an app you already have?"* (default **yes**), and on yes runs the same app picker `app delete` uses — because a user who has lost their project folder has the app but not necessarily its ID. Declining is a normal outcome that exits `0` after printing the remaining routes; the offer is skipped entirely under `--json` or off a TTY, where the no-config error (naming all three ways out: `cd` into a project, `--app-id`, `brevo app create`) is raised instead, so scripts behave exactly as before. **An interactive bootstrap also asks where to put the project** — `Output directory:`, defaulted to `./<slugified app name>`, the same prompt (and the same overwrite / merge / choose-a-different-path follow-up on an existing directory) `app create` uses; it creates the directory, `cd`s the CLI process into it, writes and reports the project, then asks *"Scaffold the Test OAuth App?"* (default yes; declining leaves the project and exits `0`), and opens *Next steps* with `cd <dir>` since the user's shell stayed behind. Answering `.` keeps the current directory and drops that step. This too is interactive-only: under `--json` or off a TTY the files go into the current directory as they always have, which is what makes `scaffold --app-id` safe to script. In bootstrap mode the config is written from the server's copy of the app, since there is nothing local to read it from. Bootstrapping is refused, before any network call or write, in two cases: a directory already linked to a **different** app (passing the app it is already linked to changes nothing), and a directory **inside** an existing app project — `readProjectConfig` reads cwd only and never walks up, so without that check a stray `cd` would nest a second `app-config.json` inside the first and a later `app upload` from there would push the wrong app silently. The different-app check applies to the answer to `Output directory:` as well as to cwd, and there it is the only thing standing between you and a project whose `app-config.json` and `src/oauth/.env.local` name two different apps. **A target directory that already holds a project for the same app makes the bootstrap a refresh**: its config is diffed against the server and rewritten only on consent, and the directory question's **Merge** answer does not suppress that. The two answers address different things — Merge means "don't clobber my own files" and is implemented by skipping any path that already exists, which `app-config.json` always does here, so letting it govern the base write meant the command fetched the app, discarded every field, wrote nothing, and still printed its success box. No drift leaves `app-config.json` as it is with a one-line notice; the feature is still offered either way. It otherwise reads the linked app id from that config (no picker — the picker is only for the config-less bootstrap), diffs the local config against the server, and if fields drifted it shows them and asks consent to update `app-config.json` (and the other base files) 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 and only missing files added; Cancel aborts without writing). The `--overwrite` flag forces a full overwrite of feature files and skips that prompt (works interactively and under `--json`). **Under `--json` it never prompts**: a config diff comes back as `{ "cancelled": true, "reason": "...", "diffs": [...] }`; otherwise it writes the feature (merging existing files unless `--overwrite` is passed) and returns `{ "scaffolded": <n>, "directory": "..." }`.
@@ -156,6 +156,7 @@ Writing `app-config.json` for an app whose remote scopes contain `'all'` never p
156
156
  | `BREVO_API_KEY` | Non-interactive login |
157
157
  | `BREVO_API_URL` | Override API base (HTTPS required, except `localhost`) |
158
158
  | `BREVO_OAUTH_PROXY_URL` | Override OAuth proxy used by browser login |
159
+ | `BREVO_OAUTH_BASE_URL` | Override OAuth realm used for scope lookups and scaffolded project templates (HTTPS required, except `localhost`) |
159
160
  | `BREVO_APP_STORE_URL` | Override the app-store service base used for the update notice and the server-side block check (HTTPS required, except `localhost`) |
160
161
  | `BREVO_CONFIG_HOME` | Override credentials directory (default `~/.brevo/`) |
161
162
  | `BREVO_CLAUDE_HOME` | Override Claude Code home used by `skill:cli` (default `~/.claude/`) |
@@ -90,7 +90,7 @@ The `ui_app` block in `app-config.json`:
90
90
  - `more_info` — supporting text under the menu entry / a card's description. Optional.
91
91
  - `redirect_link` — the destination URL that entry opens; record context arrives as **query parameters** (the path is never templated).
92
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.
93
+ - `size` — optional card size for the widget card this placement renders, e.g. `{ "width": "280px", "height": "160px" }`. `brevo app create` seeds it from the slot's registry default when the platform declares one (same mechanism as `context`) — edit or remove it freely, the entry's own value is what uploads. 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
94
  - `modal_iframe_url` — `iframeExtension` entries only; rejected on an `actionLink`.
95
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
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.
package/dist/bin/index.js CHANGED
@@ -3,7 +3,8 @@
3
3
  `),strippedUrlSuffix=void 0)}function isLocalHttpAllowed(parsed){return parsed.protocol==="http:"&&(parsed.hostname==="localhost"||parsed.hostname==="127.0.0.1"||parsed.hostname==="::1")}function resolveApiBase(){let raw=process.env.BREVO_API_URL||"https://api.brevo.com",parsed;try{parsed=new URL(raw)}catch{throw new CliError(`Invalid BREVO_API_URL: "${raw}" is not a valid URL.`)}if(parsed.protocol==="https:"||isLocalHttpAllowed(parsed))return stripPath(parsed);throw new CliError(`BREVO_API_URL must use HTTPS. Got: ${raw}
4
4
  HTTP is only allowed for localhost/127.0.0.1.`)}var API_BASE=resolveApiBase();function resolveOauthProxyUrl(){let raw=process.env.BREVO_OAUTH_PROXY_URL||"https://oauth-cli.brevo.com",parsed;try{parsed=new URL(raw)}catch{throw new CliError(`Invalid BREVO_OAUTH_PROXY_URL: "${raw}" is not a valid URL.`)}if(parsed.protocol!=="https:"&&!isLocalHttpAllowed(parsed))throw new CliError(`BREVO_OAUTH_PROXY_URL must use HTTPS. Got: ${raw}
5
5
  HTTP is only allowed for localhost/127.0.0.1.`);return parsed.origin}var OAUTH_PROXY_URL=resolveOauthProxyUrl();function resolveAppStoreUrl(){let raw=process.env.BREVO_APP_STORE_URL||"https://app-store-bo-be.brevo.com",parsed;try{parsed=new URL(raw)}catch{throw new CliError(`Invalid BREVO_APP_STORE_URL: "${raw}" is not a valid URL.`)}if(parsed.protocol!=="https:"&&!isLocalHttpAllowed(parsed))throw new CliError(`BREVO_APP_STORE_URL must use HTTPS. Got: ${raw}
6
- HTTP is only allowed for localhost/127.0.0.1.`);return parsed.origin}var APP_STORE_BASE=resolveAppStoreUrl(),USER_AGENT_HEADER="User-Agent",CLI_AUTH_METHODS={API_KEY:"api_key",OAUTH:"oauth"},coreEndpoints={ACCOUNT:"/v3/account/info",CORPORATE_SUB_ACCOUNTS:"/v3/corporate/subAccount",APP_STORE_APPS:"/v3/app-store/apps",APP_STORE_APP:appId=>`/v3/app-store/apps/${encodeURIComponent(appId)}`,CLI_INFO:"/cli/info",APP_STORE_APP_UPLOAD:appId=>`/v3/app-store/apps/${encodeURIComponent(appId)}/upload`,APP_STORE_APP_INSTALLS:appId=>`/v3/app-store/apps/${encodeURIComponent(appId)}/installs`,APP_STORE_SURFACE_POINTS:"/v3/app-store/surface-points",APP_STORE_SURFACE_POINT_LOCATIONS:"/v3/app-store/surface-points/locations",OAUTH_AUTHORIZE:"/oauth/authorize",OAUTH_TOKEN:"/oauth/token"},ENDPOINTS={...coreEndpoints},EXAMPLE_APP_ID="3f8c1a2e-5b47-4d9c-8e10-6a2b7d4f0c93",coreCli={LOGIN:"brevo login",INIT:"brevo app init",HELP:"brevo --help",APP_CREATE:"brevo app create",APP_LIST:"brevo app list",APP_SCAFFOLD:"brevo app scaffold",APP_SCAFFOLD_APP_ID:appId=>appId?`brevo app scaffold --app-id ${appId}`:"brevo app scaffold --app-id <id>",APP_CREDENTIALS:appId=>appId?`brevo app credentials --app-id ${appId}`:"brevo app credentials --app-id <id>",APP_DELETE_APP_ID:appId=>appId?`brevo app delete --app-id ${appId}`:"brevo app delete --app-id <id>",APP_CREDENTIALS_REVEAL:appId=>appId?`brevo app credentials --reveal-secret --app-id ${appId}`:"brevo app credentials --reveal-secret",APP_UPLOAD:"brevo app upload",APP_INSTALL:accountId=>accountId?`brevo app install ${accountId}`:"brevo app install",APP_UNINSTALL:accountId=>accountId?`brevo app uninstall ${accountId}`:"brevo app uninstall",APP_INSTALL_APP_ID:appId=>appId?`brevo app install --app-id ${appId}`:"brevo app install --app-id <id>",APP_UNINSTALL_APP_ID:appId=>appId?`brevo app uninstall --app-id ${appId}`:"brevo app uninstall --app-id <id>",APP_DELETE:"brevo app delete",APP_START:feature=>feature?`brevo app start ${feature}`:"brevo app start <feature>",APP_SCOPES:"brevo app available-scopes",SKILL_INSTALL:"brevo skill:cli install",SKILL_UNINSTALL:"brevo skill:cli uninstall"},CLI={...coreCli};var DEFAULT_PORT=3009,DEFAULT_REDIRECT_URI=`http://localhost:${DEFAULT_PORT}/auth/callback`,PLACEHOLDER_CLIENT_ID="YOUR_CLIENT_ID",OAUTH_BASE="https://oauth.brevo.com",OAUTH_REALM="partner",OAUTH_SCOPES_URL=`${OAUTH_BASE}/realms/${OAUTH_REALM}/scopes`,LEGACY_ALL_SCOPE="all",DEFAULT_SCOPES=["contacts:read","contacts:write","crm:read","crm:write"],EXTENSION_TYPE_ACTION_LINK="actionLink",EXTENSION_TYPE_IFRAME="iframeExtension";var DEFAULT_LINK_TARGET="_blank",UPLOADABLE_LINK_TARGETS=[DEFAULT_LINK_TARGET],BREVO_DASHBOARD_API_KEYS_URL="https://app.brevo.com/settings/keys/api",BREVO_API_KEY_DOCS_URL="https://developers.brevo.com/docs/api-key-authentication";var BREVO_CLI_REFERENCE_URL="https://developers.brevo.com/docs/cli-reference",BREVO_OAUTH_SCOPES_DOCS_URL="https://developers.brevo.com/docs/oauth-scopes#scope-catalog";var APP_NAME_MAX_LENGTH=48,APP_NAME_REGEX=/^[a-zA-Z0-9 ._\-\u00C0-\u024F]+$/;function validateAppName(name){let trimmed=name.trim();return trimmed.length===0?"App name cannot be empty.":trimmed.length>APP_NAME_MAX_LENGTH?`App name must be at most ${APP_NAME_MAX_LENGTH} characters (got ${trimmed.length}).`:APP_NAME_REGEX.test(trimmed)?!0:"App name can only contain letters, numbers, spaces, hyphens, dots, underscores, and accented characters."}function validateYesNo(input){let val=String(input).toLowerCase().trim();return val==="y"||val==="yes"||val==="n"||val==="no"||val===""?!0:"Please enter y or n"}function validateEnum(value,allowed,flagName){if(value&&!allowed.includes(value))throw new CliError(`Invalid ${flagName} "${value}". Must be one of: ${allowed.join(", ")}.`)}function validateUrl(value,fieldName){if(value){if(/[\s,]/.test(value))throw new CliError(`Invalid ${fieldName}: "${value}" contains whitespace or a comma. Pass each URL with a separate --redirect-uri flag.`);try{let parsed=new URL(value);if(parsed.protocol!=="http:"&&parsed.protocol!=="https:")throw new Error("bad protocol")}catch{throw new CliError(`Invalid ${fieldName}: "${value}" is not a valid HTTP/HTTPS URL.`)}}}function collectUrls(value,previous=[]){return validateUrl(value,"redirect URL"),[...previous,value]}var SCOPE_TOKEN_REGEX=/^[A-Za-z0-9][A-Za-z0-9:_.-]*$/,SCOPE_SPLIT_REGEX=/[\s,]+/;function splitScopes(input){if(input==null)return[];let values=Array.isArray(input)?input:[input],out=[],seen=new Set;for(let v of values)if(typeof v=="string")for(let token of v.split(SCOPE_SPLIT_REGEX))token&&(seen.has(token)||(seen.add(token),out.push(token)));return out}function validateScopes(scopes){for(let scope of scopes)if(!SCOPE_TOKEN_REGEX.test(scope))throw new CliError(`Invalid scope: "${scope}" \u2014 scopes can only contain letters, numbers, ':', '_', '.', '-'.`)}function containsLegacyAllScope(scopes){return scopes?.includes(LEGACY_ALL_SCOPE)??!1}function isSafeUiAppUrl(parsed){return parsed.protocol==="https:"?!0:parsed.protocol==="http:"&&(parsed.hostname==="localhost"||parsed.hostname==="127.0.0.1"||parsed.hostname==="::1")}function validateUiAppUrl(value){let trimmed=value.trim();if(!trimmed)return"URL cannot be empty.";let parsed;try{parsed=new URL(trimmed)}catch{return`Invalid URL: "${trimmed}" is not a valid URL.`}return isSafeUiAppUrl(parsed)?!0:`Invalid URL: "${trimmed}" must use https:// (http:// is allowed only for localhost).`}var UI_APP_LABEL_MAX_LENGTH=48,UI_APP_MORE_INFO_MAX_LENGTH=255;function validateUiAppLabel(value){let trimmed=value.trim();return trimmed?trimmed.length>UI_APP_LABEL_MAX_LENGTH?`Label must be at most ${UI_APP_LABEL_MAX_LENGTH} characters (got ${trimmed.length}).`:!0:"Label cannot be empty."}function validateUiAppMoreInfo(value){let trimmed=value.trim();return trimmed.length>UI_APP_MORE_INFO_MAX_LENGTH?`More info must be at most ${UI_APP_MORE_INFO_MAX_LENGTH} characters (got ${trimmed.length}).`:!0}function validateSurfacePoint(point){return String(point??"").trim()?!0:"Surface point cannot be empty."}var AUTHORABLE_EXTENSION_TYPES=[EXTENSION_TYPE_ACTION_LINK,EXTENSION_TYPE_IFRAME];function validateUiAppContext(fields){let seen=new Set;for(let field of fields){let trimmed=String(field??"").trim();if(!trimmed)return"Context field names cannot be empty.";if(seen.has(trimmed))return`Duplicate context field "${trimmed}".`;seen.add(trimmed)}return!0}function asText(value){return typeof value=="string"?value:typeof value=="number"||typeof value=="boolean"||typeof value=="bigint"?String(value):""}function isPresentField(value){return value===void 0?!1:typeof value!="string"||value.trim()!==""}function validateUiApp(uiApp){if(!uiApp||typeof uiApp!="object")throw new CliError('app-config.json has an invalid "ui_app" block \u2014 expected an object. Fix the file, or recreate the app with `brevo app create` and choose "UI app".');let block=uiApp,extensionType=asText(block.extension_type);if(!AUTHORABLE_EXTENSION_TYPES.includes(extensionType))throw new CliError(`Unsupported ui_app.extension_type "${extensionType}". Must be one of: ${AUTHORABLE_EXTENSION_TYPES.join(", ")}.`);rejectPreBex290Fields(block),rejectRootCtaFields(block),validateSurfacePointList(block.surface_point_list,extensionType)}function rejectPreBex290Fields(block){if(block.heading!==void 0)throw new CliError("ui_app.heading was renamed to ui_app.label (it is the menu entry's text and the card's CTA). Rename the field in app-config.json.");if(block.subheading!==void 0)throw new CliError("ui_app.subheading was renamed to ui_app.more_info (it is the menu entry's second line and the card's description). Rename the field in app-config.json.");if(block.context!==void 0)throw new CliError('ui_app.context is no longer a top-level field \u2014 record context is now per placement. Move each field list into the matching `surface_point_list` entry, e.g. [{ "surface_point_name": "contactDetails.header.menu", "context": ["recordId"] }].')}function rejectRootCtaFields(block){let moved=[["label",'"label": "Open in Acme"'],["more_info",'"more_info": "See this record in Acme"'],["redirect_link",'"redirect_link": "https://example.com/open"'],["modal_iframe_url",'"modal_iframe_url": "https://example.com/embed"']];for(let[key,hint]of moved)if(block[key]!==void 0)throw new CliError(`ui_app.${key} moved into each surface_point_list entry (each placement carries its own) \u2014 e.g. [{ "surface_point_name": "contactDetails.header.menu", ${hint} }]. Move it in app-config.json.`);if(block.link_target!==void 0)throw new CliError("ui_app.link_target moved onto each surface_point_list entry (BEX-426), and is not authored in app-config.json at all \u2014 `brevo app upload` injects it per placement. Remove it from the file.")}function validateSurfacePointList(entries,extensionType){if(!Array.isArray(entries)||entries.length===0)throw new CliError('ui_app.surface_point_list must list at least one placement (e.g. [{ "surface_point_name": "contactDetails.header.menu", "context": ["recordId"] }]). An empty list makes the platform fall back to its default widget slots, which is unlikely to be where you want the app.');let names=[];for(let entry of entries){if(!entry||typeof entry!="object"||Array.isArray(entry))throw new CliError('ui_app.surface_point_list entries must be objects, e.g. { "surface_point_name": "contactDetails.header.menu", "context": ["recordId"] }. A bare string is the pre-BEX-290 shape.');let row=entry;if(row.surface_point_name===void 0)throw new CliError('ui_app.surface_point_list entries must carry "surface_point_name" (e.g. { "surface_point_name": "contactDetails.header.menu" }).'+(row.surface_point!==void 0?' "surface_point" is not a field \u2014 rename it to "surface_point_name".':""));let check=validateSurfacePoint(asText(row.surface_point_name));if(check!==!0)throw new CliError(`ui_app.surface_point_list: ${check}`);let name=asText(row.surface_point_name).trim();names.push(name),validateEntryContext(row,name),validateEntrySize(row,name),validateEntryCtaFields(row,name,extensionType)}if(new Set(names).size!==names.length)throw new CliError("ui_app.surface_point_list contains duplicate extension points.")}var SIZE_AXIS_PATTERN=/^([1-9]\d*)(px|%)$/;function validateSurfacePointSize(size){if(!size||typeof size!="object"||Array.isArray(size))return'must be an object, e.g. { "width": "280px", "height": "160px" }.';let{width,height}=size;for(let[axis,value]of Object.entries({width,height})){if(value===void 0)continue;let match=typeof value=="string"?SIZE_AXIS_PATTERN.exec(value):null;if(!match)return`${axis} must be a positive integer with a px or % unit, e.g. "280px" or "50%".`;if(match[2]==="%"&&Number(match[1])>100)return`${axis} "${match[0]}" is out of range \u2014 a % axis must be between 1% and 100%.`}return!0}function validateEntryContext(row,name){if(row.context===void 0)return;if(!Array.isArray(row.context))throw new CliError(`ui_app.surface_point_list["${name}"].context must be an array of field names, e.g. ["recordId"].`);let contextCheck=validateUiAppContext(row.context.map(asText));if(contextCheck!==!0)throw new CliError(`ui_app.surface_point_list["${name}"].context: ${contextCheck}`)}function validateEntrySize(row,name){if(row.size===void 0)return;let sizeCheck=validateSurfacePointSize(row.size);if(sizeCheck!==!0)throw new CliError(`ui_app.surface_point_list["${name}"].size: ${sizeCheck}`)}function validateEntryCtaFields(row,name,extensionType){let at=field=>`ui_app.surface_point_list["${name}"].${field}`,labelCheck=validateUiAppLabel(asText(row.label));if(labelCheck!==!0)throw new CliError(`${at("label")}: ${labelCheck}`);let moreInfoCheck=validateUiAppMoreInfo(asText(row.more_info));if(moreInfoCheck!==!0)throw new CliError(`${at("more_info")}: ${moreInfoCheck}`);if(extensionType===EXTENSION_TYPE_IFRAME){let urlCheck2=validateUiAppUrl(asText(row.modal_iframe_url));if(urlCheck2!==!0)throw new CliError(`${at("modal_iframe_url")}: ${urlCheck2}`);if(isPresentField(row.link_target))throw new CliError(`${at("link_target")} has no effect on "${EXTENSION_TYPE_IFRAME}" extensions, which embed their URL in a modal rather than navigating to it. Remove it.`);if(isPresentField(row.redirect_link))throw new CliError(`${at("redirect_link")} cannot be combined with "${EXTENSION_TYPE_IFRAME}": a menu entry would follow the redirect instead of opening the modal, while a card would open the modal. Remove it, or use "${EXTENSION_TYPE_ACTION_LINK}" instead.`);return}let urlCheck=validateUiAppUrl(asText(row.redirect_link));if(urlCheck!==!0)throw new CliError(`${at("redirect_link")}: ${urlCheck}`);if(row.link_target!==void 0&&!UPLOADABLE_LINK_TARGETS.includes(asText(row.link_target)))throw new CliError(`Invalid ${at("link_target")} "${asText(row.link_target)}". Must be one of: ${UPLOADABLE_LINK_TARGETS.join(", ")}.`);if(isPresentField(row.modal_iframe_url))throw new CliError(`${at("modal_iframe_url")} is only used by "${EXTENSION_TYPE_IFRAME}" extensions and is ignored for "${EXTENSION_TYPE_ACTION_LINK}". Remove it, or use redirect_link instead.`)}function parseAccountId(value){let trimmed=String(value??"").trim();if(!trimmed)throw new CliError("Invalid account ID: value cannot be empty.");if(!/^\d+$/.test(trimmed))throw new CliError(`Invalid account ID: "${trimmed}" is not a numeric Brevo account ID.`);return trimmed}function parsePositiveInt(value,flagName){let n=Number.parseInt(value,10);if(!Number.isFinite(n)||n<=0)throw new CliError(`Invalid ${flagName}: "${value}" is not a positive integer.`);return n}function parseAppId(value){let trimmed=value.trim();if(trimmed.length===0)throw new CliError("Invalid --app-id: value cannot be empty.");return trimmed}function isUiAppConfigShape(config){return!!config?.ui_app}function isUiAppRecordShape(app){return app?app.ui_app?!0:!app.client_id&&!app.redirect_uris?.length:!1}function getConfigDir(){return process.env.BREVO_CONFIG_HOME||path.join(os.homedir(),".brevo")}function getCredentialsPath(){return path.join(getConfigDir(),"credentials.json")}function ensureDir(){fs.mkdirSync(getConfigDir(),{recursive:!0,mode:448})}var APP_NAME_CACHE_TTL_MS=600*1e3;function sanitizeAppNames(value){if(!value||typeof value!="object")return;let out={};for(let[key,raw]of Object.entries(value))if(typeof raw=="string"&&raw.trim())out[key]={name:raw,savedAt:0};else if(raw&&typeof raw=="object"){let entry=raw;typeof entry.name=="string"&&entry.name.trim()&&typeof entry.savedAt=="number"&&Number.isFinite(entry.savedAt)&&(out[key]={name:entry.name,savedAt:entry.savedAt})}return Object.keys(out).length>0?out:void 0}function sanitizeApps(apps){let sanitized={};for(let[key,value]of Object.entries(apps))if(value&&typeof value=="object"){let entry=value;typeof entry.clientId=="string"&&typeof entry.clientSecret=="string"&&(sanitized[key]={clientId:entry.clientId,clientSecret:entry.clientSecret})}return sanitized}function readCredentials(){try{let parsed=JSON.parse(fs.readFileSync(getCredentialsPath(),"utf-8"));if(parsed.profiles){let profileName=typeof parsed.activeProfile=="string"&&parsed.activeProfile||"default",firstKey=Object.keys(parsed.profiles)[0],profile=parsed.profiles[profileName]??(firstKey?parsed.profiles[firstKey]:void 0),migrated={auth:typeof profile?.apiKey=="string"&&profile.apiKey?{kind:"api-key",apiKey:profile.apiKey}:void 0,accountEmail:profile?.accountEmail,organizationId:profile?.organizationId,userId:profile?.userId,apps:sanitizeApps(parsed.apps??{})};try{writeCredentials(migrated)}catch{}return migrated}if(!parsed.auth&&typeof parsed.apiKey=="string"&&parsed.apiKey){let migrated={auth:{kind:"api-key",apiKey:parsed.apiKey},accountEmail:parsed.accountEmail,organizationId:parsed.organizationId,userId:parsed.userId,apps:sanitizeApps(parsed.apps??{})};try{writeCredentials(migrated)}catch{}return migrated}return{auth:sanitizeAuth(parsed.auth),accountEmail:parsed.accountEmail,organizationId:parsed.organizationId,userId:parsed.userId,apps:sanitizeApps(parsed.apps??{}),appNames:sanitizeAppNames(parsed.appNames)}}catch{return{apps:{}}}}function sanitizeAuth(raw){if(!raw||typeof raw!="object")return;let v=raw;if(v.kind==="api-key"&&typeof v.apiKey=="string"&&v.apiKey)return{kind:"api-key",apiKey:v.apiKey};if(v.kind==="oauth"&&typeof v.accessToken=="string"&&v.accessToken&&typeof v.refreshToken=="string"&&v.refreshToken&&typeof v.tokenType=="string"&&v.tokenType&&typeof v.expiresAt=="number"&&Number.isFinite(v.expiresAt))return{kind:"oauth",accessToken:v.accessToken,refreshToken:v.refreshToken,expiresAt:v.expiresAt,tokenType:v.tokenType,scope:typeof v.scope=="string"?v.scope:void 0}}function writeCredentials(creds){ensureDir();let filePath=getCredentialsPath();fs.writeFileSync(filePath,JSON.stringify(creds,null,2),{mode:384});try{fs.chmodSync(filePath,384)}catch{}}function getAuthCred(){return process.env.BREVO_API_KEY?{kind:"api-key",apiKey:process.env.BREVO_API_KEY}:readCredentials().auth}function getEmail(){return readCredentials().accountEmail}function getOrganizationId(){return readCredentials().organizationId}function getUserId(){return readCredentials().userId}function saveCredentials(apiKey,account){let creds=readCredentials();creds.auth={kind:"api-key",apiKey},creds.accountEmail=account.email,creds.organizationId=account.organizationId,creds.userId=account.userId,writeCredentials(creds)}function saveOauthCredentials(tokens,account){let creds=readCredentials();creds.auth={kind:"oauth",accessToken:tokens.accessToken,refreshToken:tokens.refreshToken,expiresAt:Date.now()+tokens.expiresIn*1e3,tokenType:tokens.tokenType,scope:tokens.scope},account?(creds.accountEmail=account.email,creds.organizationId=account.organizationId,creds.userId=account.userId):(delete creds.accountEmail,delete creds.organizationId,delete creds.userId),writeCredentials(creds)}function updateOauthTokens(tokens){let creds=readCredentials();creds.auth={kind:"oauth",accessToken:tokens.accessToken,refreshToken:tokens.refreshToken,expiresAt:Date.now()+tokens.expiresIn*1e3,tokenType:tokens.tokenType,scope:tokens.scope},writeCredentials(creds)}function clearCredentials(){let creds=readCredentials();delete creds.auth,delete creds.accountEmail,delete creds.organizationId,delete creds.userId,writeCredentials(creds)}function deleteCredentialsFile(){try{fs.unlinkSync(getCredentialsPath())}catch(error){if(typeof error=="object"&&error!==null&&"code"in error&&error.code==="ENOENT")return;throw error}}function hasAppCredentials(){return Object.keys(readCredentials().apps).length>0}function countAppCredentials(){return Object.keys(readCredentials().apps).length}function isAuthenticated(){return!!getAuthCred()}function saveAppCredentials(appId,cred){let creds=readCredentials();creds.apps[appId]=cred,writeCredentials(creds)}function clearAppsCache(){let creds=readCredentials();creds.apps={},delete creds.appNames,writeCredentials(creds)}function getAppCredentials(appId){return readCredentials().apps[appId]}function deleteAppCredentials(appId){if(!appId)return;let creds=readCredentials();appId in creds.apps&&(delete creds.apps[appId],writeCredentials(creds))}function saveAppName(appId,name){if(!appId||!name)return;let creds=readCredentials();creds.appNames={...creds.appNames,[appId]:{name,savedAt:Date.now()}},writeCredentials(creds)}function getAppNames(){let creds=readCredentials(),cache=creds.appNames??{},now=Date.now(),fresh={},result={},pruned=!1;for(let[id,entry]of Object.entries(cache))now-entry.savedAt<APP_NAME_CACHE_TTL_MS?(fresh[id]=entry,result[id]=entry.name):pruned=!0;if(pruned){creds.appNames=Object.keys(fresh).length>0?fresh:void 0;try{writeCredentials(creds)}catch{}}return result}function deleteAppName(appId){if(!appId)return;let creds=readCredentials();if(!creds.appNames||!(appId in creds.appNames))return;let{[appId]:_removed,...rest}=creds.appNames;creds.appNames=Object.keys(rest).length>0?rest:void 0,writeCredentials(creds)}var PROJECT_CONFIG_FILE="app-config.json";function readProjectConfig(){return readProjectConfigAt(process.cwd())}function readNormalizedAppId(raw){let rawAppId=raw.appId;if(typeof rawAppId=="string")return rawAppId.trim()||void 0;if(typeof rawAppId=="number"&&Number.isFinite(rawAppId))return String(rawAppId)}function buildAuthOverride(rawAuth){if(!rawAuth||typeof rawAuth!="object")return;let auth=rawAuth,override,scopes=auth.scopes;if((Array.isArray(scopes)||typeof scopes=="string")&&(override={...auth,scopes:splitScopes(scopes)}),"redirectUrls"in auth){override=override??{...auth};let legacyRedirects=auth.redirectUrls;!Array.isArray(override.redirectUris)&&Array.isArray(legacyRedirects)&&(override.redirectUris=legacyRedirects),delete override.redirectUrls}return override&&"type"in override?delete override.type:"type"in auth&&(override={...auth},delete override.type),override}function readDistributionType(rawRecord,rawAuth){let newDistributionType=rawRecord.distribution_type;if(typeof newDistributionType=="string"&&newDistributionType.trim())return newDistributionType.trim();let legacyAuthType=rawAuth&&typeof rawAuth=="object"?rawAuth.type:void 0;if(typeof legacyAuthType=="string"&&legacyAuthType.trim()&&legacyAuthType!=="none")return legacyAuthType.trim();let legacyDistribution=rawRecord.distribution;return typeof legacyDistribution=="string"&&legacyDistribution.trim()?legacyDistribution.trim():"private"}function readProjectConfigAt(dir){try{let raw=JSON.parse(fs.readFileSync(path.resolve(dir,PROJECT_CONFIG_FILE),"utf-8"));if(!raw||typeof raw!="object")return null;let rawRecord=raw,appId=readNormalizedAppId(rawRecord);if(!appId)return null;let rawAuth=rawRecord.auth,authOverride=buildAuthOverride(rawAuth),distributionType=readDistributionType(rawRecord,rawAuth),{distribution:_legacyDistribution,permittedUrls:_permittedUrls,support:_support,...rawWithoutLegacyDistribution}=rawRecord,rawUiApp=rawWithoutLegacyDistribution.ui_app;return"ui_app"in rawWithoutLegacyDistribution&&(!rawUiApp||typeof rawUiApp!="object")&&delete rawWithoutLegacyDistribution.ui_app,{...rawWithoutLegacyDistribution,appId,distribution_type:distributionType,...authOverride?{auth:authOverride}:{}}}catch{return null}}function hasLocalApp(){let cfg=readProjectConfig();return cfg?.appId!=null&&cfg.appId!==""}function findEnclosingProjectDir(){let dir=path.dirname(process.cwd());for(;;){if(readProjectConfigAt(dir))return dir;let parent=path.dirname(dir);if(parent===dir)return null;dir=parent}}function isUiAppConfig(config){return isUiAppConfigShape(config)}function writeProjectConfig(config){let configPath=path.resolve(process.cwd(),PROJECT_CONFIG_FILE);fs.writeFileSync(configPath,JSON.stringify(config,null,2)+`
6
+ HTTP is only allowed for localhost/127.0.0.1.`);return parsed.origin}var APP_STORE_BASE=resolveAppStoreUrl(),USER_AGENT_HEADER="User-Agent",CLI_AUTH_METHODS={API_KEY:"api_key",OAUTH:"oauth"},coreEndpoints={ACCOUNT:"/v3/account/info",CORPORATE_SUB_ACCOUNTS:"/v3/corporate/subAccount",APP_STORE_APPS:"/v3/app-store/apps",APP_STORE_APP:appId=>`/v3/app-store/apps/${encodeURIComponent(appId)}`,CLI_INFO:"/cli/info",APP_STORE_APP_UPLOAD:appId=>`/v3/app-store/apps/${encodeURIComponent(appId)}/upload`,APP_STORE_APP_INSTALLS:appId=>`/v3/app-store/apps/${encodeURIComponent(appId)}/installs`,APP_STORE_SURFACE_POINTS:"/v3/app-store/surface-points",APP_STORE_SURFACE_POINT_LOCATIONS:"/v3/app-store/surface-points/locations",OAUTH_AUTHORIZE:"/oauth/authorize",OAUTH_TOKEN:"/oauth/token"},ENDPOINTS={...coreEndpoints},EXAMPLE_APP_ID="3f8c1a2e-5b47-4d9c-8e10-6a2b7d4f0c93",coreCli={LOGIN:"brevo login",INIT:"brevo app init",HELP:"brevo --help",APP_CREATE:"brevo app create",APP_LIST:"brevo app list",APP_SCAFFOLD:"brevo app scaffold",APP_SCAFFOLD_APP_ID:appId=>appId?`brevo app scaffold --app-id ${appId}`:"brevo app scaffold --app-id <id>",APP_CREDENTIALS:appId=>appId?`brevo app credentials --app-id ${appId}`:"brevo app credentials --app-id <id>",APP_DELETE_APP_ID:appId=>appId?`brevo app delete --app-id ${appId}`:"brevo app delete --app-id <id>",APP_CREDENTIALS_REVEAL:appId=>appId?`brevo app credentials --reveal-secret --app-id ${appId}`:"brevo app credentials --reveal-secret",APP_UPLOAD:"brevo app upload",APP_INSTALL:accountId=>accountId?`brevo app install ${accountId}`:"brevo app install",APP_UNINSTALL:accountId=>accountId?`brevo app uninstall ${accountId}`:"brevo app uninstall",APP_INSTALL_APP_ID:appId=>appId?`brevo app install --app-id ${appId}`:"brevo app install --app-id <id>",APP_UNINSTALL_APP_ID:appId=>appId?`brevo app uninstall --app-id ${appId}`:"brevo app uninstall --app-id <id>",APP_DELETE:"brevo app delete",APP_START:feature=>feature?`brevo app start ${feature}`:"brevo app start <feature>",APP_SCOPES:"brevo app available-scopes",SKILL_INSTALL:"brevo skill:cli install",SKILL_UNINSTALL:"brevo skill:cli uninstall"},CLI={...coreCli};var DEFAULT_PORT=3009,DEFAULT_REDIRECT_URI=`http://localhost:${DEFAULT_PORT}/auth/callback`,PLACEHOLDER_CLIENT_ID="YOUR_CLIENT_ID";function resolveOauthBaseUrl(){let raw=process.env.BREVO_OAUTH_BASE_URL||"https://oauth.brevo.com",parsed;try{parsed=new URL(raw)}catch{throw new CliError(`Invalid BREVO_OAUTH_BASE_URL: "${raw}" is not a valid URL.`)}if(parsed.protocol!=="https:"&&!isLocalHttpAllowed(parsed))throw new CliError(`BREVO_OAUTH_BASE_URL must use HTTPS. Got: ${raw}
7
+ HTTP is only allowed for localhost/127.0.0.1.`);return parsed.origin}var OAUTH_BASE=resolveOauthBaseUrl(),OAUTH_REALM="partner",OAUTH_SCOPES_URL=`${OAUTH_BASE}/realms/${OAUTH_REALM}/scopes`,LEGACY_ALL_SCOPE="all",DEFAULT_SCOPES=["contacts:read","contacts:write","crm:read","crm:write"],EXTENSION_TYPE_ACTION_LINK="actionLink",EXTENSION_TYPE_IFRAME="iframeExtension";var DEFAULT_LINK_TARGET="_blank",UPLOADABLE_LINK_TARGETS=[DEFAULT_LINK_TARGET],BREVO_DASHBOARD_API_KEYS_URL="https://app.brevo.com/settings/keys/api",BREVO_API_KEY_DOCS_URL="https://developers.brevo.com/docs/api-key-authentication";var BREVO_CLI_REFERENCE_URL="https://developers.brevo.com/docs/cli-reference",BREVO_OAUTH_SCOPES_DOCS_URL="https://developers.brevo.com/docs/oauth-scopes#scope-catalog";var APP_NAME_MAX_LENGTH=48,APP_NAME_REGEX=/^[a-zA-Z0-9 ._\-\u00C0-\u024F]+$/;function validateAppName(name){let trimmed=name.trim();return trimmed.length===0?"App name cannot be empty.":trimmed.length>APP_NAME_MAX_LENGTH?`App name must be at most ${APP_NAME_MAX_LENGTH} characters (got ${trimmed.length}).`:APP_NAME_REGEX.test(trimmed)?!0:"App name can only contain letters, numbers, spaces, hyphens, dots, underscores, and accented characters."}function validateYesNo(input){let val=String(input).toLowerCase().trim();return val==="y"||val==="yes"||val==="n"||val==="no"||val===""?!0:"Please enter y or n"}function validateEnum(value,allowed,flagName){if(value&&!allowed.includes(value))throw new CliError(`Invalid ${flagName} "${value}". Must be one of: ${allowed.join(", ")}.`)}function validateUrl(value,fieldName){if(value){if(/[\s,]/.test(value))throw new CliError(`Invalid ${fieldName}: "${value}" contains whitespace or a comma. Pass each URL with a separate --redirect-uri flag.`);try{let parsed=new URL(value);if(parsed.protocol!=="http:"&&parsed.protocol!=="https:")throw new Error("bad protocol")}catch{throw new CliError(`Invalid ${fieldName}: "${value}" is not a valid HTTP/HTTPS URL.`)}}}function collectUrls(value,previous=[]){return validateUrl(value,"redirect URL"),[...previous,value]}var SCOPE_TOKEN_REGEX=/^[A-Za-z0-9][A-Za-z0-9:_.-]*$/,SCOPE_SPLIT_REGEX=/[\s,]+/;function splitScopes(input){if(input==null)return[];let values=Array.isArray(input)?input:[input],out=[],seen=new Set;for(let v of values)if(typeof v=="string")for(let token of v.split(SCOPE_SPLIT_REGEX))token&&(seen.has(token)||(seen.add(token),out.push(token)));return out}function validateScopes(scopes){for(let scope of scopes)if(!SCOPE_TOKEN_REGEX.test(scope))throw new CliError(`Invalid scope: "${scope}" \u2014 scopes can only contain letters, numbers, ':', '_', '.', '-'.`)}function containsLegacyAllScope(scopes){return scopes?.includes(LEGACY_ALL_SCOPE)??!1}function isSafeUiAppUrl(parsed){return parsed.protocol==="https:"?!0:parsed.protocol==="http:"&&(parsed.hostname==="localhost"||parsed.hostname==="127.0.0.1"||parsed.hostname==="::1")}function validateUiAppUrl(value){let trimmed=value.trim();if(!trimmed)return"URL cannot be empty.";let parsed;try{parsed=new URL(trimmed)}catch{return`Invalid URL: "${trimmed}" is not a valid URL.`}return isSafeUiAppUrl(parsed)?!0:`Invalid URL: "${trimmed}" must use https:// (http:// is allowed only for localhost).`}var UI_APP_LABEL_MAX_LENGTH=48,UI_APP_MORE_INFO_MAX_LENGTH=255;function validateUiAppLabel(value){let trimmed=value.trim();return trimmed?trimmed.length>UI_APP_LABEL_MAX_LENGTH?`Label must be at most ${UI_APP_LABEL_MAX_LENGTH} characters (got ${trimmed.length}).`:!0:"Label cannot be empty."}function validateUiAppMoreInfo(value){let trimmed=value.trim();return trimmed.length>UI_APP_MORE_INFO_MAX_LENGTH?`More info must be at most ${UI_APP_MORE_INFO_MAX_LENGTH} characters (got ${trimmed.length}).`:!0}function validateSurfacePoint(point){return String(point??"").trim()?!0:"Surface point cannot be empty."}var AUTHORABLE_EXTENSION_TYPES=[EXTENSION_TYPE_ACTION_LINK,EXTENSION_TYPE_IFRAME];function validateUiAppContext(fields){let seen=new Set;for(let field of fields){let trimmed=String(field??"").trim();if(!trimmed)return"Context field names cannot be empty.";if(seen.has(trimmed))return`Duplicate context field "${trimmed}".`;seen.add(trimmed)}return!0}function asText(value){return typeof value=="string"?value:typeof value=="number"||typeof value=="boolean"||typeof value=="bigint"?String(value):""}function isPresentField(value){return value===void 0?!1:typeof value!="string"||value.trim()!==""}function validateUiApp(uiApp){if(!uiApp||typeof uiApp!="object")throw new CliError('app-config.json has an invalid "ui_app" block \u2014 expected an object. Fix the file, or recreate the app with `brevo app create` and choose "UI app".');let block=uiApp,extensionType=asText(block.extension_type);if(!AUTHORABLE_EXTENSION_TYPES.includes(extensionType))throw new CliError(`Unsupported ui_app.extension_type "${extensionType}". Must be one of: ${AUTHORABLE_EXTENSION_TYPES.join(", ")}.`);rejectPreBex290Fields(block),rejectRootCtaFields(block),validateSurfacePointList(block.surface_point_list,extensionType)}function rejectPreBex290Fields(block){if(block.heading!==void 0)throw new CliError("ui_app.heading was renamed to ui_app.label (it is the menu entry's text and the card's CTA). Rename the field in app-config.json.");if(block.subheading!==void 0)throw new CliError("ui_app.subheading was renamed to ui_app.more_info (it is the menu entry's second line and the card's description). Rename the field in app-config.json.");if(block.context!==void 0)throw new CliError('ui_app.context is no longer a top-level field \u2014 record context is now per placement. Move each field list into the matching `surface_point_list` entry, e.g. [{ "surface_point_name": "contactDetails.header.menu", "context": ["recordId"] }].')}function rejectRootCtaFields(block){let moved=[["label",'"label": "Open in Acme"'],["more_info",'"more_info": "See this record in Acme"'],["redirect_link",'"redirect_link": "https://example.com/open"'],["modal_iframe_url",'"modal_iframe_url": "https://example.com/embed"']];for(let[key,hint]of moved)if(block[key]!==void 0)throw new CliError(`ui_app.${key} moved into each surface_point_list entry (each placement carries its own) \u2014 e.g. [{ "surface_point_name": "contactDetails.header.menu", ${hint} }]. Move it in app-config.json.`);if(block.link_target!==void 0)throw new CliError("ui_app.link_target moved onto each surface_point_list entry (BEX-426), and is not authored in app-config.json at all \u2014 `brevo app upload` injects it per placement. Remove it from the file.")}function validateSurfacePointList(entries,extensionType){if(!Array.isArray(entries)||entries.length===0)throw new CliError('ui_app.surface_point_list must list at least one placement (e.g. [{ "surface_point_name": "contactDetails.header.menu", "context": ["recordId"] }]). An empty list makes the platform fall back to its default widget slots, which is unlikely to be where you want the app.');let names=[];for(let entry of entries){if(!entry||typeof entry!="object"||Array.isArray(entry))throw new CliError('ui_app.surface_point_list entries must be objects, e.g. { "surface_point_name": "contactDetails.header.menu", "context": ["recordId"] }. A bare string is the pre-BEX-290 shape.');let row=entry;if(row.surface_point_name===void 0)throw new CliError('ui_app.surface_point_list entries must carry "surface_point_name" (e.g. { "surface_point_name": "contactDetails.header.menu" }).'+(row.surface_point!==void 0?' "surface_point" is not a field \u2014 rename it to "surface_point_name".':""));let check=validateSurfacePoint(asText(row.surface_point_name));if(check!==!0)throw new CliError(`ui_app.surface_point_list: ${check}`);let name=asText(row.surface_point_name).trim();names.push(name),validateEntryContext(row,name),validateEntrySize(row,name),validateEntryCtaFields(row,name,extensionType)}if(new Set(names).size!==names.length)throw new CliError("ui_app.surface_point_list contains duplicate extension points.")}var SIZE_AXIS_PATTERN=/^([1-9]\d*)(px|%)$/;function validateSurfacePointSize(size){if(!size||typeof size!="object"||Array.isArray(size))return'must be an object, e.g. { "width": "280px", "height": "160px" }.';let{width,height}=size;for(let[axis,value]of Object.entries({width,height})){if(value===void 0)continue;let match=typeof value=="string"?SIZE_AXIS_PATTERN.exec(value):null;if(!match)return`${axis} must be a positive integer with a px or % unit, e.g. "280px" or "50%".`;if(match[2]==="%"&&Number(match[1])>100)return`${axis} "${match[0]}" is out of range \u2014 a % axis must be between 1% and 100%.`}return!0}function validateEntryContext(row,name){if(row.context===void 0)return;if(!Array.isArray(row.context))throw new CliError(`ui_app.surface_point_list["${name}"].context must be an array of field names, e.g. ["recordId"].`);let contextCheck=validateUiAppContext(row.context.map(asText));if(contextCheck!==!0)throw new CliError(`ui_app.surface_point_list["${name}"].context: ${contextCheck}`)}function validateEntrySize(row,name){if(row.size===void 0)return;let sizeCheck=validateSurfacePointSize(row.size);if(sizeCheck!==!0)throw new CliError(`ui_app.surface_point_list["${name}"].size: ${sizeCheck}`)}function validateEntryCtaFields(row,name,extensionType){let at=field=>`ui_app.surface_point_list["${name}"].${field}`,labelCheck=validateUiAppLabel(asText(row.label));if(labelCheck!==!0)throw new CliError(`${at("label")}: ${labelCheck}`);let moreInfoCheck=validateUiAppMoreInfo(asText(row.more_info));if(moreInfoCheck!==!0)throw new CliError(`${at("more_info")}: ${moreInfoCheck}`);if(extensionType===EXTENSION_TYPE_IFRAME){let urlCheck2=validateUiAppUrl(asText(row.modal_iframe_url));if(urlCheck2!==!0)throw new CliError(`${at("modal_iframe_url")}: ${urlCheck2}`);if(isPresentField(row.link_target))throw new CliError(`${at("link_target")} has no effect on "${EXTENSION_TYPE_IFRAME}" extensions, which embed their URL in a modal rather than navigating to it. Remove it.`);if(isPresentField(row.redirect_link))throw new CliError(`${at("redirect_link")} cannot be combined with "${EXTENSION_TYPE_IFRAME}": a menu entry would follow the redirect instead of opening the modal, while a card would open the modal. Remove it, or use "${EXTENSION_TYPE_ACTION_LINK}" instead.`);return}let urlCheck=validateUiAppUrl(asText(row.redirect_link));if(urlCheck!==!0)throw new CliError(`${at("redirect_link")}: ${urlCheck}`);if(row.link_target!==void 0&&!UPLOADABLE_LINK_TARGETS.includes(asText(row.link_target)))throw new CliError(`Invalid ${at("link_target")} "${asText(row.link_target)}". Must be one of: ${UPLOADABLE_LINK_TARGETS.join(", ")}.`);if(isPresentField(row.modal_iframe_url))throw new CliError(`${at("modal_iframe_url")} is only used by "${EXTENSION_TYPE_IFRAME}" extensions and is ignored for "${EXTENSION_TYPE_ACTION_LINK}". Remove it, or use redirect_link instead.`)}function parseAccountId(value){let trimmed=String(value??"").trim();if(!trimmed)throw new CliError("Invalid account ID: value cannot be empty.");if(!/^\d+$/.test(trimmed))throw new CliError(`Invalid account ID: "${trimmed}" is not a numeric Brevo account ID.`);return trimmed}function parsePositiveInt(value,flagName){let n=Number.parseInt(value,10);if(!Number.isFinite(n)||n<=0)throw new CliError(`Invalid ${flagName}: "${value}" is not a positive integer.`);return n}function parseAppId(value){let trimmed=value.trim();if(trimmed.length===0)throw new CliError("Invalid --app-id: value cannot be empty.");return trimmed}function isUiAppConfigShape(config){return!!config?.ui_app}function isUiAppRecordShape(app){return app?app.ui_app?!0:!app.client_id&&!app.redirect_uris?.length:!1}function getConfigDir(){return process.env.BREVO_CONFIG_HOME||path.join(os.homedir(),".brevo")}function getCredentialsPath(){return path.join(getConfigDir(),"credentials.json")}function ensureDir(){fs.mkdirSync(getConfigDir(),{recursive:!0,mode:448})}var APP_NAME_CACHE_TTL_MS=600*1e3;function sanitizeAppNames(value){if(!value||typeof value!="object")return;let out={};for(let[key,raw]of Object.entries(value))if(typeof raw=="string"&&raw.trim())out[key]={name:raw,savedAt:0};else if(raw&&typeof raw=="object"){let entry=raw;typeof entry.name=="string"&&entry.name.trim()&&typeof entry.savedAt=="number"&&Number.isFinite(entry.savedAt)&&(out[key]={name:entry.name,savedAt:entry.savedAt})}return Object.keys(out).length>0?out:void 0}function sanitizeApps(apps){let sanitized={};for(let[key,value]of Object.entries(apps))if(value&&typeof value=="object"){let entry=value;typeof entry.clientId=="string"&&typeof entry.clientSecret=="string"&&(sanitized[key]={clientId:entry.clientId,clientSecret:entry.clientSecret})}return sanitized}function readCredentials(){try{let parsed=JSON.parse(fs.readFileSync(getCredentialsPath(),"utf-8"));if(parsed.profiles){let profileName=typeof parsed.activeProfile=="string"&&parsed.activeProfile||"default",firstKey=Object.keys(parsed.profiles)[0],profile=parsed.profiles[profileName]??(firstKey?parsed.profiles[firstKey]:void 0),migrated={auth:typeof profile?.apiKey=="string"&&profile.apiKey?{kind:"api-key",apiKey:profile.apiKey}:void 0,accountEmail:profile?.accountEmail,organizationId:profile?.organizationId,userId:profile?.userId,apps:sanitizeApps(parsed.apps??{})};try{writeCredentials(migrated)}catch{}return migrated}if(!parsed.auth&&typeof parsed.apiKey=="string"&&parsed.apiKey){let migrated={auth:{kind:"api-key",apiKey:parsed.apiKey},accountEmail:parsed.accountEmail,organizationId:parsed.organizationId,userId:parsed.userId,apps:sanitizeApps(parsed.apps??{})};try{writeCredentials(migrated)}catch{}return migrated}return{auth:sanitizeAuth(parsed.auth),accountEmail:parsed.accountEmail,organizationId:parsed.organizationId,userId:parsed.userId,apps:sanitizeApps(parsed.apps??{}),appNames:sanitizeAppNames(parsed.appNames)}}catch{return{apps:{}}}}function sanitizeAuth(raw){if(!raw||typeof raw!="object")return;let v=raw;if(v.kind==="api-key"&&typeof v.apiKey=="string"&&v.apiKey)return{kind:"api-key",apiKey:v.apiKey};if(v.kind==="oauth"&&typeof v.accessToken=="string"&&v.accessToken&&typeof v.refreshToken=="string"&&v.refreshToken&&typeof v.tokenType=="string"&&v.tokenType&&typeof v.expiresAt=="number"&&Number.isFinite(v.expiresAt))return{kind:"oauth",accessToken:v.accessToken,refreshToken:v.refreshToken,expiresAt:v.expiresAt,tokenType:v.tokenType,scope:typeof v.scope=="string"?v.scope:void 0}}function writeCredentials(creds){ensureDir();let filePath=getCredentialsPath();fs.writeFileSync(filePath,JSON.stringify(creds,null,2),{mode:384});try{fs.chmodSync(filePath,384)}catch{}}function getAuthCred(){return process.env.BREVO_API_KEY?{kind:"api-key",apiKey:process.env.BREVO_API_KEY}:readCredentials().auth}function getEmail(){return readCredentials().accountEmail}function getOrganizationId(){return readCredentials().organizationId}function getUserId(){return readCredentials().userId}function saveCredentials(apiKey,account){let creds=readCredentials();creds.auth={kind:"api-key",apiKey},creds.accountEmail=account.email,creds.organizationId=account.organizationId,creds.userId=account.userId,writeCredentials(creds)}function saveOauthCredentials(tokens,account){let creds=readCredentials();creds.auth={kind:"oauth",accessToken:tokens.accessToken,refreshToken:tokens.refreshToken,expiresAt:Date.now()+tokens.expiresIn*1e3,tokenType:tokens.tokenType,scope:tokens.scope},account?(creds.accountEmail=account.email,creds.organizationId=account.organizationId,creds.userId=account.userId):(delete creds.accountEmail,delete creds.organizationId,delete creds.userId),writeCredentials(creds)}function updateOauthTokens(tokens){let creds=readCredentials();creds.auth={kind:"oauth",accessToken:tokens.accessToken,refreshToken:tokens.refreshToken,expiresAt:Date.now()+tokens.expiresIn*1e3,tokenType:tokens.tokenType,scope:tokens.scope},writeCredentials(creds)}function clearCredentials(){let creds=readCredentials();delete creds.auth,delete creds.accountEmail,delete creds.organizationId,delete creds.userId,writeCredentials(creds)}function deleteCredentialsFile(){try{fs.unlinkSync(getCredentialsPath())}catch(error){if(typeof error=="object"&&error!==null&&"code"in error&&error.code==="ENOENT")return;throw error}}function hasAppCredentials(){return Object.keys(readCredentials().apps).length>0}function countAppCredentials(){return Object.keys(readCredentials().apps).length}function isAuthenticated(){return!!getAuthCred()}function saveAppCredentials(appId,cred){let creds=readCredentials();creds.apps[appId]=cred,writeCredentials(creds)}function clearAppsCache(){let creds=readCredentials();creds.apps={},delete creds.appNames,writeCredentials(creds)}function getAppCredentials(appId){return readCredentials().apps[appId]}function deleteAppCredentials(appId){if(!appId)return;let creds=readCredentials();appId in creds.apps&&(delete creds.apps[appId],writeCredentials(creds))}function saveAppName(appId,name){if(!appId||!name)return;let creds=readCredentials();creds.appNames={...creds.appNames,[appId]:{name,savedAt:Date.now()}},writeCredentials(creds)}function getAppNames(){let creds=readCredentials(),cache=creds.appNames??{},now=Date.now(),fresh={},result={},pruned=!1;for(let[id,entry]of Object.entries(cache))now-entry.savedAt<APP_NAME_CACHE_TTL_MS?(fresh[id]=entry,result[id]=entry.name):pruned=!0;if(pruned){creds.appNames=Object.keys(fresh).length>0?fresh:void 0;try{writeCredentials(creds)}catch{}}return result}function deleteAppName(appId){if(!appId)return;let creds=readCredentials();if(!creds.appNames||!(appId in creds.appNames))return;let{[appId]:_removed,...rest}=creds.appNames;creds.appNames=Object.keys(rest).length>0?rest:void 0,writeCredentials(creds)}var PROJECT_CONFIG_FILE="app-config.json";function readProjectConfig(){return readProjectConfigAt(process.cwd())}function readNormalizedAppId(raw){let rawAppId=raw.appId;if(typeof rawAppId=="string")return rawAppId.trim()||void 0;if(typeof rawAppId=="number"&&Number.isFinite(rawAppId))return String(rawAppId)}function buildAuthOverride(rawAuth){if(!rawAuth||typeof rawAuth!="object")return;let auth=rawAuth,override,scopes=auth.scopes;if((Array.isArray(scopes)||typeof scopes=="string")&&(override={...auth,scopes:splitScopes(scopes)}),"redirectUrls"in auth){override=override??{...auth};let legacyRedirects=auth.redirectUrls;!Array.isArray(override.redirectUris)&&Array.isArray(legacyRedirects)&&(override.redirectUris=legacyRedirects),delete override.redirectUrls}return override&&"type"in override?delete override.type:"type"in auth&&(override={...auth},delete override.type),override}function readDistributionType(rawRecord,rawAuth){let newDistributionType=rawRecord.distribution_type;if(typeof newDistributionType=="string"&&newDistributionType.trim())return newDistributionType.trim();let legacyAuthType=rawAuth&&typeof rawAuth=="object"?rawAuth.type:void 0;if(typeof legacyAuthType=="string"&&legacyAuthType.trim()&&legacyAuthType!=="none")return legacyAuthType.trim();let legacyDistribution=rawRecord.distribution;return typeof legacyDistribution=="string"&&legacyDistribution.trim()?legacyDistribution.trim():"private"}function readProjectConfigAt(dir){try{let raw=JSON.parse(fs.readFileSync(path.resolve(dir,PROJECT_CONFIG_FILE),"utf-8"));if(!raw||typeof raw!="object")return null;let rawRecord=raw,appId=readNormalizedAppId(rawRecord);if(!appId)return null;let rawAuth=rawRecord.auth,authOverride=buildAuthOverride(rawAuth),distributionType=readDistributionType(rawRecord,rawAuth),{distribution:_legacyDistribution,permittedUrls:_permittedUrls,support:_support,...rawWithoutLegacyDistribution}=rawRecord,rawUiApp=rawWithoutLegacyDistribution.ui_app;return"ui_app"in rawWithoutLegacyDistribution&&(!rawUiApp||typeof rawUiApp!="object")&&delete rawWithoutLegacyDistribution.ui_app,{...rawWithoutLegacyDistribution,appId,distribution_type:distributionType,...authOverride?{auth:authOverride}:{}}}catch{return null}}function hasLocalApp(){let cfg=readProjectConfig();return cfg?.appId!=null&&cfg.appId!==""}function findEnclosingProjectDir(){let dir=path.dirname(process.cwd());for(;;){if(readProjectConfigAt(dir))return dir;let parent=path.dirname(dir);if(parent===dir)return null;dir=parent}}function isUiAppConfig(config){return isUiAppConfigShape(config)}function writeProjectConfig(config){let configPath=path.resolve(process.cwd(),PROJECT_CONFIG_FILE);fs.writeFileSync(configPath,JSON.stringify(config,null,2)+`
7
8
  `,"utf-8")}function isNonEmptyString(value){return typeof value=="string"&&value.trim()!==""}function backfillProjectConfigFromServer(appId,server){let configPath=path.resolve(process.cwd(),PROJECT_CONFIG_FILE),raw;try{raw=JSON.parse(fs.readFileSync(configPath,"utf-8"))}catch{return[]}if(!raw||typeof raw!="object")return[];let normalized=readProjectConfig();if(normalized?.appId!==appId)return[];let rawRecord=raw,backfilled=[],next={...normalized};!isNonEmptyString(rawRecord.version)&&isNonEmptyString(server.version)&&(next.version=server.version,backfilled.push("version"));let rawAuth=rawRecord.auth,legacyAuthType=rawAuth&&typeof rawAuth=="object"?rawAuth.type:void 0;return isNonEmptyString(rawRecord.distribution_type)||isNonEmptyString(rawRecord.distribution)||isNonEmptyString(legacyAuthType)&&legacyAuthType!=="none"||(next.distribution_type=server.distribution_type??normalized.distribution_type,backfilled.push("distribution_type")),backfilled.length===0?[]:(writeProjectConfig(next),backfilled)}function scaffoldFileCount(done,written,total){return written===total?`${done} (${total} files)`:written===0?`already in place (${total} files, nothing rewritten)`:`${done} (${written} of ${total} files written)`}function numberedSteps(cdDir,steps){let commandWidth=Math.max(...steps.map(([command])=>command.length)),offset=cdDir?1:0;return[...cdDir?[`1. cd ${cdDir}`]:[],...steps.map(([command,note],i)=>`${i+1+offset}. ${command.padEnd(commandWidth)} (${note})`)]}var coreMessages={UPDATE_AVAILABLE:(current,latest)=>`Update available: ${current} \u2192 ${latest}`,UPDATE_RUN:name=>`Run: npm install -g ${name}`,UPDATE_RUN_YARN:name=>`Or: yarn global add ${name}`,UPDATE_RUN_BREW:"Or: brew upgrade brevo",FORCE_UPDATE_REQUIRED:(current,latest)=>`Update required: v${current} is no longer supported (latest v${latest}).`,FORCE_UPDATE_HINT:"Update to continue using the Brevo CLI:",CLI_VERSION_NOTICE_FALLBACK:"A newer version of the Brevo CLI is available.",AUTH_WELCOME:"Welcome to Brevo CLI",AUTH_PROMPT_METHOD:"How would you like to authenticate?",AUTH_PROMPT_API_KEY:"Paste your API key:",AUTH_SUCCESS:email=>`Authenticated as ${email}`,AUTH_INVALID_KEY:"Invalid API key. Please check and try again.",AUTH_HINT:(keysUrl,docsUrl)=>`
8
9
  To authenticate, you need a Brevo API key.
9
10
  Create one at: ${keysUrl}
@@ -137,7 +138,7 @@ Examples:
137
138
  `)}function printFileTree(filePaths){for(let line of formatFileTree(filePaths).split(`
138
139
  `))logInfo(line)}function computeSlug(name){return(name||"my-app").toLowerCase().replaceAll(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")||"my-app"}async function fetchAppContext(appId,silent,uiApp,fallbackApp){let spinner=createSpinner("Fetching app details...",{silent}),result;try{result=await appService.resolveAppCredentials(appId,{tolerateMissing:!!fallbackApp})}finally{spinner.stop()}let appDetails=result?.app??null;result?(result.diffs.length>0&&logWarn(`Local credentials for app ${appId} differ from server (${result.diffs.join(", ")}). Updating local cache.`),appService.syncAppCredentials(appId,result.app)):fallbackApp&&(appDetails=fallbackApp,silent||logWarn(messages.APP_SCAFFOLD_SERVER_READBACK_FAILED(appId)));let serverRedirectUrls=appDetails?.redirect_uris??[],redirectUris=serverRedirectUrls.length>0?serverRedirectUrls:[DEFAULT_REDIRECT_URI],localhostUri=redirectUris.find(url=>url.startsWith("http://localhost")||url.startsWith("http://127.0.0.1"));return{appDetails,clientId:appDetails?.client_id||PLACEHOLDER_CLIENT_ID,clientSecret:appDetails?.client_secret||"YOUR_CLIENT_SECRET",redirectUris,redirectUri:localhostUri||DEFAULT_REDIRECT_URI,...uiApp?{uiApp}:{}}}async function resolveProjectDirectory(defaultDir,jsonMode=!1,validateTarget){let outputDir=jsonMode?defaultDir:(await import_inquirer3.default.prompt([{type:"input",name:"outputDir",message:messages.APP_SCAFFOLD_DIR_PROMPT,default:defaultDir}])).outputDir,targetDir=path4.resolve(outputDir),existed=fs4.existsSync(targetDir);if(validateTarget?.(targetDir),!existed)return{targetDir,mergeOnly:!1,chooseAgain:!1,existed:!1};if(jsonMode)return{targetDir,unresolved:!0};let{action}=await import_inquirer3.default.prompt([{type:"list",name:"action",message:messages.APP_SCAFFOLD_DIR_EXISTS,choices:indentChoices([{name:"Overwrite existing files",value:"overwrite"},{name:"Merge (keep existing, add missing)",value:"merge"},{name:"Choose a different path",value:"new"}])}]);return action==="new"?{targetDir,mergeOnly:!1,chooseAgain:!0,existed:!0}:{targetDir,mergeOnly:action==="merge",chooseAgain:!1,existed:!0}}function applyProjectDirectory(decision,jsonMode=!1){if(decision.unresolved||decision.chooseAgain)return;let{targetDir,existed}=decision;existed?jsonMode||(targetDir===process.cwd()?logInfo(messages.APP_SCAFFOLD_TARGET_IS_CWD):logInfo(messages.APP_SCAFFOLD_USING_EXISTING_DIR(path4.relative(process.cwd(),targetDir)))):(jsonMode||logInfo(messages.APP_SCAFFOLD_CREATING_DIR(path4.relative(process.cwd(),targetDir))),fs4.mkdirSync(targetDir,{recursive:!0})),process.chdir(targetDir)}function diffLocalConfig(localConfig,ctx){let diffs=[],serverName=ctx.appDetails?.name;serverName&&localConfig.appName!==serverName&&diffs.push({field:"appName",local:localConfig.appName||"(none)",server:serverName});let serverDistribution=ctx.appDetails?.distribution_type??"private";if(localConfig.distribution_type!==serverDistribution&&diffs.push({field:"distribution_type",local:localConfig.distribution_type,server:serverDistribution}),!isUiAppConfig(localConfig)){let localRedirects=[...localConfig.auth?.redirectUris??[]].sort((a,b)=>a.localeCompare(b)),serverRedirects=[...ctx.redirectUris].sort((a,b)=>a.localeCompare(b));JSON.stringify(localRedirects)!==JSON.stringify(serverRedirects)&&diffs.push({field:"redirectUris",local:localRedirects.join(", ")||"(none)",server:serverRedirects.join(", ")||"(none)"})}if(!isUiAppConfig(localConfig)){let localScopes=[...localConfig.auth?.scopes??[]].sort((a,b)=>a.localeCompare(b)),serverScopes=[...ctx.appDetails?.scopes??[]].filter(s=>s!==LEGACY_ALL_SCOPE).sort((a,b)=>a.localeCompare(b));JSON.stringify(localScopes)!==JSON.stringify(serverScopes)&&diffs.push({field:"scopes",local:localScopes.join(", ")||"(none)",server:serverScopes.join(", ")||"(none)"})}let localLogo=localConfig.logoUri??"",serverLogo=ctx.appDetails?.logo_uri??"";localLogo!==serverLogo&&diffs.push({field:"logoUri",local:localLogo||"(none)",server:serverLogo||"(none)"});let localVersion=localConfig.version??"",serverVersion=ctx.appDetails?.version??"";return localVersion!==serverVersion&&diffs.push({field:"version",local:localVersion||"(none)",server:serverVersion||"(none)"}),diffs}function writeScaffoldFiles(files,targetDir,mergeOnly){let written=0;for(let file of files){let filePath=path4.join(targetDir,file.name);if(fs4.mkdirSync(path4.dirname(filePath),{recursive:!0}),mergeOnly&&fs4.existsSync(filePath))continue;let writeOptions=file.name.endsWith(".env.local")?{mode:384}:{};fs4.writeFileSync(filePath,file.content,{encoding:"utf-8",...writeOptions}),written++}return written}function renderUiAppJson(uiApp){return uiApp?JSON.stringify(uiApp,null,2).replaceAll(`
139
140
  `,`
140
- `):""}function buildTemplateVars(appId,ctx,targetDir){let appName=(ctx.appDetails?.name||path4.basename(targetDir)).replaceAll(/["\\\n\r\t]/g,"").trim()||"my-app",remoteScopes=ctx.appDetails?.scopes,legacyAllSubstituted=!ctx.uiApp&&containsLegacyAllScope(remoteScopes),granularScopes=(remoteScopes??[]).filter(s=>s!==LEGACY_ALL_SCOPE),scopes;ctx.uiApp?scopes=[]:scopes=granularScopes.length>0?granularScopes:[...DEFAULT_SCOPES];let slug=computeSlug(ctx.appDetails?.name);return{vars:{"{{APP_NAME}}":appName,"{{APP_SLUG}}":slug,"{{APP_ID}}":String(appId),"{{CLIENT_ID}}":ctx.clientId,"{{CLIENT_SECRET}}":ctx.clientSecret,"{{REDIRECT_URI}}":ctx.redirectUri,"{{REDIRECT_URLS_JSON}}":JSON.stringify(ctx.redirectUris),"{{SCOPES_JSON}}":JSON.stringify(scopes),"{{DISTRIBUTION}}":ctx.appDetails?.distribution_type??"private","{{LOGO_URI}}":ctx.appDetails?.logo_uri??"","{{APP_VERSION}}":ctx.appDetails?.version??"","{{OAUTH_BASE}}":OAUTH_BASE,"{{OAUTH_REALM}}":OAUTH_REALM,"{{UI_APP_JSON}}":renderUiAppJson(ctx.uiApp)},scopes,legacyAllSubstituted}}function runBaseScaffold(appId,ctx,targetDir,mergeOnly){let{vars,scopes,legacyAllSubstituted}=buildTemplateVars(appId,ctx,targetDir),files=loadBaseTemplates(vars);return{written:writeScaffoldFiles(files,targetDir,mergeOnly),legacyAllSubstituted,scopes,files}}async function resolveFeatureConflict(featureType,appId,ctx,targetDir,opts){if(opts.overwrite)return"overwrite";let{vars}=buildTemplateVars(appId,ctx,targetDir);if(!loadFeatureTemplates(featureType,vars).some(f=>fs4.existsSync(path4.join(targetDir,f.name)))||opts.jsonMode)return"merge";let{action}=await import_inquirer3.default.prompt([{type:"list",name:"action",message:messages.APP_SCAFFOLD_FEATURE_EXISTS,choices:indentChoices([{name:messages.APP_SCAFFOLD_FEATURE_EXISTS_OVERWRITE,value:"overwrite"},{name:messages.APP_SCAFFOLD_FEATURE_EXISTS_MERGE,value:"merge"},{name:messages.APP_SCAFFOLD_FEATURE_EXISTS_CANCEL,value:"cancel"}])}]);return action}function runFeatureScaffold(featureType,appId,ctx,targetDir,mergeOnly){let{vars}=buildTemplateVars(appId,ctx,targetDir);featureType==="oauth"&&fs4.mkdirSync(path4.join(targetDir,"src","oauth"),{recursive:!0});let files=loadFeatureTemplates(featureType,vars);return{written:writeScaffoldFiles(files,targetDir,mergeOnly),files}}function reportBaseScaffoldSuccess(result){logSuccess(messages.APP_CREATE_BASE_SUCCESS(result.written,result.files.length)),result.legacyAllSubstituted&&logWarn(messages.LEGACY_ALL_SCOPE_SCAFFOLD_SUBSTITUTED(result.scopes.join(", "))),printFileTree(result.files.map(f=>f.name))}function reportScaffoldSuccess(result){logSuccess(messages.APP_SCAFFOLD_SUCCESS(result.written,result.files.length)),result.legacyAllSubstituted&&logWarn(messages.LEGACY_ALL_SCOPE_SCAFFOLD_SUBSTITUTED(result.scopes.join(", "))),printFileTree(result.files.map(f=>f.name)),printBox(messages.APP_SCAFFOLD_NEXT_STEPS_TITLE,messages.APP_SCAFFOLD_NEXT_STEPS_LINES(result.cdDir)),logInfo(messages.APP_SCAFFOLD_SCOPES_TIP)}function computeCdHint(originalCwd,targetDir){return path4.relative(originalCwd,targetDir)||void 0}var import_inquirer4=__toESM(require("inquirer"));function featureTypes(){return Object.keys(FEATURE_TEMPLATE_MANIFESTS)}function soleFeatureType(){let types=featureTypes();return types.length===1?types[0]:void 0}var FALLBACK_FEATURE="oauth";function soleFeatureLabel(){let only=soleFeatureType();return only?FEATURE_LABELS[only]:void 0}async function promptFeatureType(interactive){let types=featureTypes(),only=soleFeatureType();if(only)return only;if(!interactive)return types[0]??FALLBACK_FEATURE;let{featureType}=await import_inquirer4.default.prompt([{type:"list",name:"featureType",message:messages.APP_SCAFFOLD_FEATURE_TYPE_PROMPT,choices:indentChoices(types.map(type=>({name:FEATURE_LABELS[type],value:type})))}]);return featureType}async function promptScaffoldFeature(){let{scaffoldRaw}=await import_inquirer4.default.prompt([{type:"input",name:"scaffoldRaw",message:messages.APP_SCAFFOLD_FEATURE_CONFIRM(soleFeatureLabel())+" (Y/n)",default:"y",validate:validateYesNo}]),val=String(scaffoldRaw).toLowerCase().trim();return val===""||val.startsWith("y")}async function finishProject(params){let{appId,ctx,targetDir,cdDir,isUiApp}=params;if(isUiApp)return printBox(messages.APP_SCAFFOLD_NEXT_STEPS_TITLE,messages.APP_CREATE_UI_NEXT(cdDir)),{cancelled:!1,feature:null,written:0};if(!(params.offerFeature&&await promptScaffoldFeature()))return logInfo(messages.APP_SCAFFOLD_SCOPES_TIP),printBox(messages.APP_SCAFFOLD_NEXT_STEPS_TITLE,messages.APP_CREATE_BASE_ONLY_NEXT(cdDir)),{cancelled:!1,feature:null,written:0};let feature=await promptFeatureType(!0),mergeOnly;if(params.onConflict==="ask"){let choice=await resolveFeatureConflict(feature,appId,ctx,targetDir,{jsonMode:!!params.jsonMode,overwrite:!!params.overwriteFlag});if(choice==="cancel")return logInfo(messages.APP_SCAFFOLD_CANCELLED),{cancelled:!0};mergeOnly=choice==="merge"}else mergeOnly=params.onConflict==="merge";let feat=runFeatureScaffold(feature,appId,ctx,targetDir,mergeOnly);return reportScaffoldSuccess({written:feat.written,legacyAllSubstituted:!1,scopes:params.baseScopes,files:feat.files,targetDir,cdDir}),{cancelled:!1,feature,written:feat.written}}var import_inquirer5=__toESM(require("inquirer"));var NONE="(none)",VALUE_ROWS=[{label:"label: ",read:e=>e.label},{label:"more info: ",read:e=>e.more_info},{label:"redirect link: ",read:e=>e.redirect_link},{label:"modal URL: ",read:e=>e.modal_iframe_url},{label:"card size: ",read:e=>formatSize(e.size)}];function formatSize(size){if(!size)return;let axes=[...size.width?[`width ${size.width}`]:[],...size.height?[`height ${size.height}`]:[]];return axes.length?axes.join(", "):void 0}function formatContext(entry){return entry.context?.length?` (context: ${entry.context.join(", ")})`:""}function formatPlacementLines(uiApp){return(uiApp.surface_point_list??[]).flatMap(entry=>[`${entry.surface_point_name}${formatContext(entry)}`,...VALUE_ROWS.flatMap(({label,read})=>{let value=read(entry);return value?[` ${label}${value}`]:[]})])}function formatPlacementDiffLines(next,current){if(!current)return formatPlacementLines(next);let currentEntries=current.surface_point_list??[],nextEntries=next.surface_point_list??[],before=new Map(currentEntries.map(entry=>[entry.surface_point_name,entry])),nextNames=new Set(nextEntries.map(entry=>entry.surface_point_name));return[...nextEntries.flatMap(entry=>{let previous=before.get(entry.surface_point_name);if(!previous){let[slot,...rest]=formatPlacementLines({surface_point_list:[entry]});return[`${slot} (new)`,...rest]}return[`${entry.surface_point_name}${diffContext(previous,entry)}`,...VALUE_ROWS.flatMap(({label,read})=>{let from=read(previous),to=read(entry);return from===to?to?[` ${label}${to}`]:[]:[` ${label}${from??NONE} \u2192 ${to??NONE}`]})]}),...currentEntries.filter(entry=>!nextNames.has(entry.surface_point_name)).map(entry=>`${entry.surface_point_name} (removed)`)]}function diffContext(previous,entry){let from=previous.context??[],to=entry.context??[];return from.join(",")===to.join(",")?formatContext(entry):` (context: ${from.length?from.join(", "):NONE} \u2192 ${to.length?to.join(", "):NONE})`}var PLACEMENT_QUESTION_PREFIX="placement:";function toUsableRows(rows){let usable=[];for(let row of rows){let segments=row.extension_point_name.split("."),[locationToken,placeToken,kindToken]=segments.length===3?segments:["","",""],location=(row.location_name??"").trim()||locationToken,section=(row.section_name??"").trim()||placeToken,component=(row.component_type??"").trim()||kindToken,slug=(row.surface_point_name??"").trim();!location||!section||!component||!slug||usable.push({...row,location_name:location,section_name:section,component_type:component,surface_point_name:slug})}return usable}function rowSupportsExtensionType(row,extensionType){if(row.status?.trim()&&row.status.trim()!=="active")return!1;let types=row.extension_type_list;return!types||types.length===0?!0:types.includes(extensionType)}async function fetchRecordPageLocations(extensionType){let spinner=createSpinner(messages.APP_CREATE_UI_PAGES_SPINNER),locations;try{locations=await appService.fetchSurfacePointLocations(extensionType)}catch{throw new CliError(messages.APP_CREATE_UI_POINTS_FETCH_FAILED)}finally{spinner.stop()}if(locations.length===0)throw new CliError(messages.APP_CREATE_UI_POINTS_EMPTY);return locations}async function readSurfacePointRows(locations,extensionType){try{return await appService.fetchSurfacePoints(locations,extensionType)}catch{return null}}async function fetchSurfacePointsForPages(locations,extensionType){let onPickedPages=rows=>toUsableRows(rows).filter(row=>locations.includes(row.location_name)),pagesCovered=rows=>new Set(rows.map(row=>row.location_name)).size,spinner=createSpinner(messages.APP_CREATE_UI_POINTS_SPINNER),usable;try{let narrowed=await readSurfacePointRows(locations,extensionType);if(usable=onPickedPages(narrowed??[]),narrowed===null||pagesCovered(usable)<locations.length){let unfiltered=await readSurfacePointRows();if(unfiltered===null&&narrowed===null)throw new CliError(messages.APP_CREATE_UI_POINTS_FETCH_FAILED);let fallback=onPickedPages(unfiltered??[]);pagesCovered(fallback)>pagesCovered(usable)&&(usable=fallback)}}finally{spinner.stop()}let hostable=usable.filter(row=>rowSupportsExtensionType(row,extensionType));if(hostable.length===0)throw new CliError(usable.length>0?messages.APP_CREATE_UI_POINTS_NONE_FOR_TYPE(extensionType):messages.APP_CREATE_UI_POINTS_EMPTY);return hostable}function placementLabel(row){return`${row.section_name} \u2014 ${row.component_type}`}async function promptSurfacePoint(locations,extensionType){let{surface}=await import_inquirer5.default.prompt([{type:"list",name:"surface",message:messages.APP_CREATE_UI_SURFACE_PROMPT,choices:indentChoices(locations.map(location=>({name:location,value:location})))}]),page=locations.find(location=>location===String(surface??"").trim()),forPage=(await fetchSurfacePointsForPages(page?[page]:[],extensionType)).filter(row=>row.location_name===page),question=`${PLACEMENT_QUESTION_PREFIX}${page}`,answer=await import_inquirer5.default.prompt([{type:"list",name:question,message:messages.APP_CREATE_UI_PLACEMENT_PAGE_PROMPT(page??""),choices:indentChoices(forPage.map(row=>({name:placementLabel(row),value:row.surface_point_name})))}]),chosen=String(answer[question]??"").trim();return forPage.filter(row=>row.surface_point_name===chosen)}async function promptIntegrationType(){let{integrationType}=await import_inquirer5.default.prompt([{type:"list",name:"integrationType",message:messages.APP_CREATE_UI_INTEGRATION_PROMPT,choices:indentChoices([{name:messages.APP_CREATE_UI_INTEGRATION_EXTERNAL_LINK,value:EXTENSION_TYPE_ACTION_LINK}])}]);return integrationType}async function resolveUiApp(){let extensionType=await promptIntegrationType(),locations=await fetchRecordPageLocations(extensionType),selectedRows=await promptSurfacePoint(locations,extensionType),{label}=await import_inquirer5.default.prompt([{type:"input",name:"label",message:messages.APP_CREATE_UI_LABEL_PROMPT,validate:validateUiAppLabel}]),{more_info}=await import_inquirer5.default.prompt([{type:"input",name:"more_info",message:messages.APP_CREATE_UI_MORE_INFO_PROMPT,validate:validateUiAppMoreInfo}]),{url}=await import_inquirer5.default.prompt([{type:"input",name:"url",message:messages.APP_CREATE_UI_REDIRECT_LINK_PROMPT,validate:validateUiAppUrl}]),uiApp={extension_type:extensionType,surface_point_list:buildSurfacePointList(selectedRows,{contextFor:row=>row.default_context_field??[],label:String(label??"").trim(),more_info:String(more_info??"").trim(),redirect_link:String(url??"").trim()})};return validateUiApp(uiApp),uiApp}function buildSurfacePointList(rows,fields){let entries=[],seen=new Set;for(let row of rows){if(seen.has(row.surface_point_name))continue;seen.add(row.surface_point_name);let context=fields.contextFor(row).map(field=>String(field).trim()).filter(Boolean);entries.push({surface_point_name:row.surface_point_name,...context.length?{context}:{},label:fields.label,...fields.more_info?{more_info:fields.more_info}:{},redirect_link:fields.redirect_link})}return entries}function buildExampleContextUrl(redirectLink,context){let url;try{url=new URL(redirectLink)}catch{return null}for(let field of context)url.searchParams.set(field,field.replaceAll(/([a-z0-9])([A-Z])/g,"$1_$2").toUpperCase());return url.toString()}function renderExampleContextUrlLines(uiApp){let withContext=uiApp.surface_point_list.find(entry=>entry.context?.length&&entry.redirect_link);if(!withContext)return[];let example=buildExampleContextUrl(withContext.redirect_link,withContext.context??[]);return example?["",`${messages.APP_CREATE_UI_BOX_EXAMPLE_URL_LABEL}`,` ${example}`,messages.APP_CREATE_UI_BOX_EXAMPLE_URL_NOTE]:[]}function renderCreatedUiApp(result,appName,uiApp,logoUri){let boxLines=[`App name: ${appName}`,`App ID: ${result.app_id}`,`Extension type: ${uiApp.extension_type}`,...formatPlacementLines(uiApp).map((line,i)=>`${i===0?"Placement: ":" "}${line}`),...logoUri?[`Logo URL: ${logoUri}`]:[],...result.version?[`App version: ${result.version}`]:[],...renderExampleContextUrlLines(uiApp),"",messages.APP_CREATE_UI_BOX_LABEL_NOTE(uiApp.surface_point_list[0]?.label??"",appName),messages.APP_CREATE_UI_BOX_HINT];printBox(messages.APP_CREATE_UI_BOX_TITLE,boxLines)}function validateHttpUrl(trimmed,invalidMessage){try{let parsed=new URL(trimmed);return parsed.protocol!=="http:"&&parsed.protocol!=="https:"?invalidMessage:!0}catch{return invalidMessage}}var validateRedirectUrl=input=>{let trimmed=input.trim();return trimmed?validateHttpUrl(trimmed,messages.APP_CREATE_REDIRECT_INVALID):messages.APP_CREATE_REDIRECT_EMPTY},validateLogoUrl=input=>{let trimmed=input.trim();return trimmed?validateHttpUrl(trimmed,messages.APP_CREATE_LOGO_INVALID):!0};function guardAgainstLinkedApp(){if(!hasLocalApp())return;let projectConfig=readProjectConfig(),linkedName=projectConfig?.appName||String(projectConfig?.appId??"");throw new CliError(messages.APP_CREATE_ALREADY_LINKED(linkedName))}async function resolveAppName(nameFlag){if(nameFlag){let nameCheck=validateAppName(nameFlag);if(nameCheck!==!0)throw new CliError(nameCheck);return nameFlag}return(await import_inquirer6.default.prompt([{type:"input",name:"name",message:messages.APP_CREATE_NAME_PROMPT,validate:validateAppName}])).name}async function resolveAppType(interactive){if(!interactive)return"oauth";let choices=[{name:messages.APP_CREATE_APP_TYPE_OAUTH,value:"oauth"}];return isFeatureAvailable("ui-app-type")&&choices.push({name:messages.APP_CREATE_APP_TYPE_UI,value:"ui"}),(await import_inquirer6.default.prompt([{type:"list",name:"appType",message:messages.APP_CREATE_APP_TYPE_PROMPT,choices:indentChoices(choices)}])).appType}function assertDistributionFlag(distributionFlag){validateEnum(distributionFlag,["private","public"],"--distribution"),distributionFlag==="public"&&assertFeatureAvailable("public-distribution")}async function resolveDistribution(distributionFlag,interactive){if(distributionFlag)return distributionFlag;if(!interactive)return"private";let choices=[{name:"Private (Used exclusively by your organisation)",value:"private"}];return(await import_inquirer6.default.prompt([{type:"list",name:"distribution",message:messages.APP_CREATE_TYPE_PROMPT,choices:indentChoices(choices)}])).distribution}async function promptAddAnotherRedirect(){let{anotherRaw}=await import_inquirer6.default.prompt([{type:"input",name:"anotherRaw",message:messages.APP_CREATE_REDIRECT_ANOTHER+" (y/N)",default:"n",validate:validateYesNo}]);return String(anotherRaw).toLowerCase().trim().startsWith("y")}async function promptRedirectUrls(quiet){let availablePort=await findAvailablePort(DEFAULT_PORT),defaultRedirect=availablePort==null||availablePort===DEFAULT_PORT?DEFAULT_REDIRECT_URI:`http://localhost:${availablePort}/auth/callback`;quiet||(availablePort==null?logInfo(messages.APP_CREATE_PORT_SCAN_FAILED(DEFAULT_PORT)):availablePort!==DEFAULT_PORT&&logInfo(messages.APP_CREATE_PORT_IN_USE(DEFAULT_PORT,availablePort)),logInfo(messages.APP_CREATE_REDIRECT_HINT(CLI.APP_START("oauth"))));let redirectUris=[],{redirectUrl:firstUrl}=await import_inquirer6.default.prompt([{type:"input",name:"redirectUrl",message:messages.APP_CREATE_REDIRECT_PROMPT,default:defaultRedirect,validate:validateRedirectUrl}]);for(redirectUris.push(firstUrl.trim());await promptAddAnotherRedirect();){let{nextUrl}=await import_inquirer6.default.prompt([{type:"input",name:"nextUrl",message:messages.APP_CREATE_REDIRECT_PROMPT,validate:validateRedirectUrl}]);redirectUris.push(nextUrl.trim())}return redirectUris}async function resolveRedirectUrls(redirectUriFlag,quiet){let flagUrls=redirectUriFlag??[];return flagUrls.length>0?flagUrls:process.stdin.isTTY?promptRedirectUrls(quiet):[DEFAULT_REDIRECT_URI]}async function resolveLogoUri(logoUriFlag,jsonMode){if(logoUriFlag||!process.stdin.isTTY||jsonMode)return logoUriFlag;let{logoUrl}=await import_inquirer6.default.prompt([{type:"input",name:"logoUrl",message:messages.APP_CREATE_LOGO_PROMPT,validate:validateLogoUrl}]);return String(logoUrl??"").trim()||void 0}async function resolveCreateDirectory(appName,interactive){let slug=computeSlug(appName);if(!interactive){let targetDir=path5.resolve(`./${slug}`);return fs5.existsSync(targetDir)?{targetDir,skipped:!0}:{targetDir,mergeOnly:!1,skipped:!1,existed:!1}}let dir=await resolveProjectDirectory(`./${slug}`);for(;!dir.unresolved&&dir.chooseAgain;)dir=await resolveProjectDirectory(`./${slug}`);if(dir.unresolved)throw new CliError(messages.APP_CREATE_DIR_UNRESOLVED);return{targetDir:dir.targetDir,mergeOnly:dir.mergeOnly,skipped:!1,existed:dir.existed}}function applyCreateDirectory(dir,jsonMode){dir.skipped||applyProjectDirectory({targetDir:dir.targetDir,mergeOnly:dir.mergeOnly,chooseAgain:!1,existed:dir.existed},jsonMode)}function buildCreatePayload(inputs){let isUiApp=!!inputs.uiApp;return{name:inputs.appName,distribution_type:inputs.distribution,...isUiApp?{ui_app:inputs.uiApp}:{auth:{scopes:[...DEFAULT_SCOPES],redirect_uris:inputs.redirectUris}},...inputs.logoUri?{logo_uri:inputs.logoUri}:{}}}async function retryCreateWithNewName(inputs){logError(messages.APP_CREATE_NAME_TAKEN);let retry=await import_inquirer6.default.prompt([{type:"input",name:"name",message:messages.APP_CREATE_NAME_PROMPT,validate:validateAppName}]),retrySpinner=createSpinner("Creating app...");try{let result=await appService.createApp(buildCreatePayload({...inputs,appName:retry.name}));return retrySpinner.stop(),{result,appName:retry.name}}catch(retryErr){throw retrySpinner.stop(),retryErr}}async function retryCreateAfterLogin(inputs){logWarn(messages.APP_CREATE_SESSION_EXPIRED);let{relogin}=await import_inquirer6.default.prompt([{type:"confirm",name:"relogin",message:messages.APP_CREATE_RELOGIN_CONFIRM,default:!0}]);if(!relogin)throw new AuthExpiredError;if(await loginCommand({suppressNextSteps:!0}),!isAuthenticated())throw new AuthExpiredError;let spinner=createSpinner("Creating app...");try{return{result:await appService.createApp(buildCreatePayload(inputs)),appName:inputs.appName}}finally{spinner.stop()}}function isPublicDistributionRefusal(err,distribution){return err instanceof ApiError&&err.statusCode===400&&distribution==="public"&&/distribution_type/i.test(err.message)}async function createAppWithRetry(inputs,jsonMode,interactive){let spinner=createSpinner("Creating app...",{silent:jsonMode});try{let result=await appService.createApp(buildCreatePayload(inputs));return spinner.stop(),{result,appName:inputs.appName}}catch(err){if(spinner.stop(),err instanceof ApiError&&err.errorCode==="APP_LIMIT_REACHED")throw jsonMode&&jsonOutput({error:"APP_LIMIT_REACHED",message:messages.APP_CREATE_LIMIT_REACHED}),new CliError(messages.APP_CREATE_LIMIT_REACHED);if(isPublicDistributionRefusal(err,inputs.distribution))throw new CliError(messages.APP_CREATE_PUBLIC_REJECTED(err.message));if(err instanceof ApiError&&err.statusCode===409)return retryCreateWithNewName(inputs);if(err instanceof AuthExpiredError&&interactive)return retryCreateAfterLogin(inputs);throw err}}function renderCreatedApp(result,appName,logoUri){let boxLines=[`App name: ${appName}`,`App ID: ${result.app_id}`,`Client ID: ${result.client_id}`,`Client secret: ${messages.CLIENT_SECRET_HIDDEN_HUMAN}`,...(result.redirect_uris??[]).map((uri,i)=>`Redirect URL ${i+1}: ${uri}`),...logoUri?[`Logo URL: ${logoUri}`]:[],...result.version?[`App version: ${result.version}`]:[],`${messages.APP_CREATE_BOX_SCOPES_LABEL} ${[...DEFAULT_SCOPES].join(", ")}`,"",messages.APP_CREATE_BOX_SCOPE_HINT];printBox(messages.APP_CREATE_BOX_TITLE,boxLines)}var createCommand=withCommandHandler(async options=>{let jsonMode=!!options.json,originalCwd=process.cwd();guardAgainstLinkedApp(),assertDistributionFlag(options.distribution);let interactive=!jsonMode&&!!process.stdin.isTTY,appName=await resolveAppName(options.name),logoUri=await resolveLogoUri(options.logoUri,jsonMode),distribution=await resolveDistribution(options.distribution,interactive),appType=await resolveAppType(interactive),redirectUris=[],uiApp;appType==="ui"?uiApp=await resolveUiApp():redirectUris=await resolveRedirectUrls(options.redirectUri,jsonMode);let dir=await resolveCreateDirectory(appName,interactive),inputs={appName,distribution,redirectUris,logoUri,uiApp},{result,appName:finalAppName}=await createAppWithRetry(inputs,jsonMode,interactive);applyCreateDirectory(dir,jsonMode),result.client_id&&result.client_secret&&saveAppCredentials(result.app_id,{clientId:result.client_id,clientSecret:result.client_secret}),finalAppName&&saveAppName(result.app_id,finalAppName);let jsonBase={appId:result.app_id,appName:finalAppName,clientId:result.client_id,clientSecret:messages.CLIENT_SECRET_HIDDEN_JSON,appType,...uiApp?{uiApp}:{redirectUri:result.redirect_uris},...logoUri?{logoUri}:{},...result.version?{version:result.version}:{}},renderBox2=()=>uiApp?renderCreatedUiApp(result,finalAppName,uiApp,logoUri):renderCreatedApp(result,finalAppName,logoUri);if(dir.skipped){if(jsonMode){jsonOutput({...jsonBase,directory:dir.targetDir,scaffoldSkipped:messages.APP_CREATE_JSON_SCAFFOLD_DIR_EXISTS(dir.targetDir)});return}renderBox2(),logInfo(messages.APP_CREATE_DIR_EXISTS_SKIPPED(dir.targetDir));return}let fallbackApp={...result,client_id:result.client_id??"",redirect_uris:result.redirect_uris??null},ctx=await fetchAppContext(result.app_id,jsonMode,uiApp,fallbackApp),base=runBaseScaffold(result.app_id,ctx,dir.targetDir,dir.mergeOnly);if(jsonMode){jsonOutput({...jsonBase,directory:dir.targetDir,scaffolded:base.written});return}renderBox2(),reportBaseScaffoldSuccess(base),await finishProject({appId:result.app_id,ctx,targetDir:dir.targetDir,baseScopes:base.scopes,cdDir:computeCdHint(originalCwd,dir.targetDir),isUiApp:!!uiApp,offerFeature:interactive,onConflict:dir.mergeOnly?"merge":"overwrite"})});var http=__toESM(require("node:http")),import_node_crypto=require("node:crypto");var MAX_BODY_BYTES=16*1024,DEFAULT_TIMEOUT_MS=3e5;function normalizeTokens(raw){return typeof raw.access_token!="string"||!raw.access_token||typeof raw.refresh_token!="string"||!raw.refresh_token||typeof raw.expires_in!="number"||!Number.isFinite(raw.expires_in)||raw.expires_in<=0||typeof raw.token_type!="string"||!raw.token_type?null:{accessToken:raw.access_token,refreshToken:raw.refresh_token,expiresIn:raw.expires_in,tokenType:raw.token_type,scope:typeof raw.scope=="string"?raw.scope:void 0}}async function runBrowserLoginFlow(opts){let proxyOrigin=new URL(opts.proxyUrl).origin,timeoutMs=opts.timeoutMs??DEFAULT_TIMEOUT_MS,openBrowser2=opts.openBrowser??(()=>{});return new Promise((resolve11,reject)=>{let settled=!1,claimSettlement=()=>settled?!1:(settled=!0,!0),server=http.createServer((req,res)=>{let pathname=new URL(req.url??"/","http://127.0.0.1").pathname,origin=req.headers.origin;if(logDebug("loopback request",{method:req.method,url:req.url,pathname,origin}),req.method==="OPTIONS"&&pathname==="/callback"){if(origin!==proxyOrigin){logDebug("loopback OPTIONS rejected: origin mismatch",{origin,expected:proxyOrigin}),res.writeHead(403).end();return}res.writeHead(204,{"Access-Control-Allow-Origin":proxyOrigin,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type","Access-Control-Max-Age":"600"}).end();return}if(req.method==="POST"&&pathname==="/callback"){if(origin!==proxyOrigin){logDebug("loopback POST rejected: origin mismatch",{origin,expected:proxyOrigin}),res.writeHead(403).end();return}let bytes=0,chunks=[];req.on("data",chunk=>{if(bytes+=chunk.length,bytes>MAX_BODY_BYTES){logDebug("loopback POST rejected: body too large",{bytes,max:MAX_BODY_BYTES}),res.writeHead(413,{Connection:"close"}).end(),req.destroy();return}chunks.push(chunk)}),req.on("end",()=>{let parsed=null;try{parsed=JSON.parse(Buffer.concat(chunks).toString("utf-8"))}catch{parsed=null}let tokens=parsed?normalizeTokens(parsed):null;if(!tokens){logDebug("loopback POST rejected: bad payload shape",{hasParsed:parsed!==null,keys:parsed?Object.keys(parsed):null}),res.writeHead(400,{"Access-Control-Allow-Origin":proxyOrigin,"Content-Type":"text/plain"}).end("Bad payload");return}logDebug("loopback POST accepted",{hasScope:tokens.scope!==void 0}),res.writeHead(204,{"Access-Control-Allow-Origin":proxyOrigin}).end(),claimSettlement()&&(server.close(),resolve11(tokens))});return}if(req.method==="GET"&&(pathname==="/"||pathname==="/callback")){res.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),res.end('<!doctype html><meta charset="utf-8"><title>Brevo CLI login</title><p>Waiting for login to complete \u2014 you can close this tab once the CLI confirms success.</p>');return}logDebug("loopback request not matched",{method:req.method,pathname}),res.writeHead(404).end()});server.on("error",err=>{logDebug("loopback server error",{message:err.message}),claimSettlement()&&reject(err)}),server.listen(0,"127.0.0.1",()=>{let port=server.address().port,attemptToken=(0,import_node_crypto.randomUUID)(),loginUrl=`${opts.proxyUrl}/login?port=${port}&t=${attemptToken}`;logDebug("loopback listening",{host:"127.0.0.1",port,proxyOrigin}),opts.onWaiting?.(loginUrl);try{openBrowser2(loginUrl)}catch{}});let timer=setTimeout(()=>{claimSettlement()&&(server.close(),reject(new CliError(messages.AUTH_BROWSER_TIMEOUT)))},timeoutMs);timer.unref?.(),server.on("close",()=>clearTimeout(timer))})}function wipeAppsCacheIfAccountChanged(newOrganizationId){let previousOrganizationId=getOrganizationId();previousOrganizationId&&previousOrganizationId!==newOrganizationId&&clearAppsCache()}async function promptApiKey(){let{key}=await import_inquirer7.default.prompt([{type:"password",name:"key",message:messages.AUTH_PROMPT_API_KEY,mask:"*",validate:input=>input.trim().length>0||"API key cannot be empty"}]);return key}async function resolveLoginMethod(forceBrowser,apiKey){if(forceBrowser){if(!process.stdin.isTTY)throw new CliError(messages.AUTH_BROWSER_NON_INTERACTIVE);return"browser"}if(apiKey)return"api-key";if(!process.stdin.isTTY)throw new CliError(messages.AUTH_BROWSER_NON_INTERACTIVE);let{chosen}=await import_inquirer7.default.prompt([{type:"list",name:"chosen",message:messages.AUTH_PROMPT_METHOD,choices:indentChoices([{name:"Browser (sign in through your browser)",value:"browser"},{name:"API key (paste from your Brevo dashboard)",value:"api-key"}]),default:"browser"}]);return chosen}async function retryApiKeyValidation(quiet){let retryKey=await promptApiKey(),retrySpinner=createSpinner("Validating API key...",{silent:quiet});try{let account=await accountService.validateApiKey(retryKey);return retrySpinner.stop(),{account,apiKey:retryKey}}catch(retryErr){throw retrySpinner.stop(),retryErr instanceof ApiError&&retryErr.statusCode===401?new CliError(messages.AUTH_INVALID_KEY,EXIT_CODES.AUTH_FAILURE):retryErr}}async function validateApiKeyWithRetry(apiKey,quiet){let spinner=createSpinner("Validating API key...",{silent:quiet});try{let account=await accountService.validateApiKey(apiKey);return spinner.stop(),{account,apiKey}}catch(err){if(spinner.stop(),!(err instanceof ApiError&&err.statusCode===401)||(logError(messages.AUTH_INVALID_KEY),quiet||logInfo(` ${messages.AUTH_GET_KEY_URL}`),!process.stdin.isTTY))throw err;return retryApiKeyValidation(quiet)}}async function loginWithApiKey(envApiKey,quiet){let apiKey=envApiKey;if(apiKey||(openBrowser(BREVO_DASHBOARD_API_KEYS_URL),quiet||process.stdout.write(messages.AUTH_HINT(BREVO_DASHBOARD_API_KEYS_URL,BREVO_API_KEY_DOCS_URL)),apiKey=await promptApiKey()),!apiKey)throw new CliError("No API key provided.");let validated=await validateApiKeyWithRetry(apiKey,quiet);if(!validated.account)throw new CliError("Authentication failed.");return wipeAppsCacheIfAccountChanged(validated.account.organization_id),saveCredentials(validated.apiKey,{email:validated.account.email,organizationId:validated.account.organization_id,userId:validated.account.user_id}),validated.account}async function loginWithBrowser(quiet){quiet||logInfo(` ${messages.AUTH_BROWSER_OPENING}`);let tokens=await runBrowserLoginFlow({proxyUrl:OAUTH_PROXY_URL,openBrowser,onWaiting:url=>{quiet||(logInfo(` ${messages.AUTH_BROWSER_FALLBACK_URL(url)}`),logInfo(` ${messages.AUTH_BROWSER_WAITING}`))}}),tokensToStore={accessToken:tokens.accessToken,refreshToken:tokens.refreshToken,expiresIn:tokens.expiresIn,tokenType:tokens.tokenType,scope:tokens.scope};saveOauthCredentials(tokensToStore),quiet||logSuccess(messages.AUTH_BROWSER_TOKENS_RECEIVED(getCredentialsPath()));let spinner=createSpinner("Finishing login...",{silent:quiet}),account;try{account=await client.getWithBearer(ENDPOINTS.ACCOUNT,tokens.accessToken,tokens.tokenType)}catch(err){throw err instanceof ApiError&&err.statusCode===401&&clearCredentials(),err}finally{spinner.stop()}if(!account)throw new CliError("Authentication failed.");return wipeAppsCacheIfAccountChanged(account.organization_id),saveOauthCredentials(tokensToStore,{email:account.email,organizationId:account.organization_id,userId:account.user_id}),account}async function showNextSteps(){let apps=[],appsSpinner=createSpinner("Checking your apps...");try{apps=await appService.fetchAppsList()}catch{}finally{appsSpinner.stop()}if(apps.length>0){printBox("What's next?",[CLI.APP_CREATE,CLI.APP_LIST,CLI.APP_SCAFFOLD,CLI.APP_CREDENTIALS()]);return}if(!process.stdin.isTTY){logInfo(`
141
+ `):""}function buildTemplateVars(appId,ctx,targetDir){let appName=(ctx.appDetails?.name||path4.basename(targetDir)).replaceAll(/["\\\n\r\t]/g,"").trim()||"my-app",remoteScopes=ctx.appDetails?.scopes,legacyAllSubstituted=!ctx.uiApp&&containsLegacyAllScope(remoteScopes),granularScopes=(remoteScopes??[]).filter(s=>s!==LEGACY_ALL_SCOPE),scopes;ctx.uiApp?scopes=[]:scopes=granularScopes.length>0?granularScopes:[...DEFAULT_SCOPES];let slug=computeSlug(ctx.appDetails?.name);return{vars:{"{{APP_NAME}}":appName,"{{APP_SLUG}}":slug,"{{APP_ID}}":String(appId),"{{CLIENT_ID}}":ctx.clientId,"{{CLIENT_SECRET}}":ctx.clientSecret,"{{REDIRECT_URI}}":ctx.redirectUri,"{{REDIRECT_URLS_JSON}}":JSON.stringify(ctx.redirectUris),"{{SCOPES_JSON}}":JSON.stringify(scopes),"{{DISTRIBUTION}}":ctx.appDetails?.distribution_type??"private","{{LOGO_URI}}":ctx.appDetails?.logo_uri??"","{{APP_VERSION}}":ctx.appDetails?.version??"","{{OAUTH_BASE}}":OAUTH_BASE,"{{OAUTH_REALM}}":OAUTH_REALM,"{{UI_APP_JSON}}":renderUiAppJson(ctx.uiApp)},scopes,legacyAllSubstituted}}function runBaseScaffold(appId,ctx,targetDir,mergeOnly){let{vars,scopes,legacyAllSubstituted}=buildTemplateVars(appId,ctx,targetDir),files=loadBaseTemplates(vars);return{written:writeScaffoldFiles(files,targetDir,mergeOnly),legacyAllSubstituted,scopes,files}}async function resolveFeatureConflict(featureType,appId,ctx,targetDir,opts){if(opts.overwrite)return"overwrite";let{vars}=buildTemplateVars(appId,ctx,targetDir);if(!loadFeatureTemplates(featureType,vars).some(f=>fs4.existsSync(path4.join(targetDir,f.name)))||opts.jsonMode)return"merge";let{action}=await import_inquirer3.default.prompt([{type:"list",name:"action",message:messages.APP_SCAFFOLD_FEATURE_EXISTS,choices:indentChoices([{name:messages.APP_SCAFFOLD_FEATURE_EXISTS_OVERWRITE,value:"overwrite"},{name:messages.APP_SCAFFOLD_FEATURE_EXISTS_MERGE,value:"merge"},{name:messages.APP_SCAFFOLD_FEATURE_EXISTS_CANCEL,value:"cancel"}])}]);return action}function runFeatureScaffold(featureType,appId,ctx,targetDir,mergeOnly){let{vars}=buildTemplateVars(appId,ctx,targetDir);featureType==="oauth"&&fs4.mkdirSync(path4.join(targetDir,"src","oauth"),{recursive:!0});let files=loadFeatureTemplates(featureType,vars);return{written:writeScaffoldFiles(files,targetDir,mergeOnly),files}}function reportBaseScaffoldSuccess(result){logSuccess(messages.APP_CREATE_BASE_SUCCESS(result.written,result.files.length)),result.legacyAllSubstituted&&logWarn(messages.LEGACY_ALL_SCOPE_SCAFFOLD_SUBSTITUTED(result.scopes.join(", "))),printFileTree(result.files.map(f=>f.name))}function reportScaffoldSuccess(result){logSuccess(messages.APP_SCAFFOLD_SUCCESS(result.written,result.files.length)),result.legacyAllSubstituted&&logWarn(messages.LEGACY_ALL_SCOPE_SCAFFOLD_SUBSTITUTED(result.scopes.join(", "))),printFileTree(result.files.map(f=>f.name)),printBox(messages.APP_SCAFFOLD_NEXT_STEPS_TITLE,messages.APP_SCAFFOLD_NEXT_STEPS_LINES(result.cdDir)),logInfo(messages.APP_SCAFFOLD_SCOPES_TIP)}function computeCdHint(originalCwd,targetDir){return path4.relative(originalCwd,targetDir)||void 0}var import_inquirer4=__toESM(require("inquirer"));function featureTypes(){return Object.keys(FEATURE_TEMPLATE_MANIFESTS)}function soleFeatureType(){let types=featureTypes();return types.length===1?types[0]:void 0}var FALLBACK_FEATURE="oauth";function soleFeatureLabel(){let only=soleFeatureType();return only?FEATURE_LABELS[only]:void 0}async function promptFeatureType(interactive){let types=featureTypes(),only=soleFeatureType();if(only)return only;if(!interactive)return types[0]??FALLBACK_FEATURE;let{featureType}=await import_inquirer4.default.prompt([{type:"list",name:"featureType",message:messages.APP_SCAFFOLD_FEATURE_TYPE_PROMPT,choices:indentChoices(types.map(type=>({name:FEATURE_LABELS[type],value:type})))}]);return featureType}async function promptScaffoldFeature(){let{scaffoldRaw}=await import_inquirer4.default.prompt([{type:"input",name:"scaffoldRaw",message:messages.APP_SCAFFOLD_FEATURE_CONFIRM(soleFeatureLabel())+" (Y/n)",default:"y",validate:validateYesNo}]),val=String(scaffoldRaw).toLowerCase().trim();return val===""||val.startsWith("y")}async function finishProject(params){let{appId,ctx,targetDir,cdDir,isUiApp}=params;if(isUiApp)return printBox(messages.APP_SCAFFOLD_NEXT_STEPS_TITLE,messages.APP_CREATE_UI_NEXT(cdDir)),{cancelled:!1,feature:null,written:0};if(!(params.offerFeature&&await promptScaffoldFeature()))return logInfo(messages.APP_SCAFFOLD_SCOPES_TIP),printBox(messages.APP_SCAFFOLD_NEXT_STEPS_TITLE,messages.APP_CREATE_BASE_ONLY_NEXT(cdDir)),{cancelled:!1,feature:null,written:0};let feature=await promptFeatureType(!0),mergeOnly;if(params.onConflict==="ask"){let choice=await resolveFeatureConflict(feature,appId,ctx,targetDir,{jsonMode:!!params.jsonMode,overwrite:!!params.overwriteFlag});if(choice==="cancel")return logInfo(messages.APP_SCAFFOLD_CANCELLED),{cancelled:!0};mergeOnly=choice==="merge"}else mergeOnly=params.onConflict==="merge";let feat=runFeatureScaffold(feature,appId,ctx,targetDir,mergeOnly);return reportScaffoldSuccess({written:feat.written,legacyAllSubstituted:!1,scopes:params.baseScopes,files:feat.files,targetDir,cdDir}),{cancelled:!1,feature,written:feat.written}}var import_inquirer5=__toESM(require("inquirer"));var NONE="(none)",VALUE_ROWS=[{label:"label: ",read:e=>e.label},{label:"more info: ",read:e=>e.more_info},{label:"redirect link: ",read:e=>e.redirect_link},{label:"modal URL: ",read:e=>e.modal_iframe_url},{label:"card size: ",read:e=>formatSize(e.size)}];function formatSize(size){if(!size)return;let axes=[...size.width?[`width ${size.width}`]:[],...size.height?[`height ${size.height}`]:[]];return axes.length?axes.join(", "):void 0}function formatContext(entry){return entry.context?.length?` (context: ${entry.context.join(", ")})`:""}function formatPlacementLines(uiApp){return(uiApp.surface_point_list??[]).flatMap(entry=>[`${entry.surface_point_name}${formatContext(entry)}`,...VALUE_ROWS.flatMap(({label,read})=>{let value=read(entry);return value?[` ${label}${value}`]:[]})])}function formatPlacementDiffLines(next,current){if(!current)return formatPlacementLines(next);let currentEntries=current.surface_point_list??[],nextEntries=next.surface_point_list??[],before=new Map(currentEntries.map(entry=>[entry.surface_point_name,entry])),nextNames=new Set(nextEntries.map(entry=>entry.surface_point_name));return[...nextEntries.flatMap(entry=>{let previous=before.get(entry.surface_point_name);if(!previous){let[slot,...rest]=formatPlacementLines({surface_point_list:[entry]});return[`${slot} (new)`,...rest]}return[`${entry.surface_point_name}${diffContext(previous,entry)}`,...VALUE_ROWS.flatMap(({label,read})=>{let from=read(previous),to=read(entry);return from===to?to?[` ${label}${to}`]:[]:[` ${label}${from??NONE} \u2192 ${to??NONE}`]})]}),...currentEntries.filter(entry=>!nextNames.has(entry.surface_point_name)).map(entry=>`${entry.surface_point_name} (removed)`)]}function diffContext(previous,entry){let from=previous.context??[],to=entry.context??[];return from.join(",")===to.join(",")?formatContext(entry):` (context: ${from.length?from.join(", "):NONE} \u2192 ${to.length?to.join(", "):NONE})`}var PLACEMENT_QUESTION_PREFIX="placement:";function toUsableRows(rows){let usable=[];for(let row of rows){let segments=row.extension_point_name.split("."),[locationToken,placeToken,kindToken]=segments.length===3?segments:["","",""],location=(row.location_name??"").trim()||locationToken,section=(row.section_name??"").trim()||placeToken,component=(row.component_type??"").trim()||kindToken,slug=(row.surface_point_name??"").trim();!location||!section||!component||!slug||usable.push({...row,location_name:location,section_name:section,component_type:component,surface_point_name:slug})}return usable}function rowSupportsExtensionType(row,extensionType){if(row.status?.trim()&&row.status.trim()!=="active")return!1;let types=row.extension_type_list;return!types||types.length===0?!0:types.includes(extensionType)}async function fetchRecordPageLocations(extensionType){let spinner=createSpinner(messages.APP_CREATE_UI_PAGES_SPINNER),locations;try{locations=await appService.fetchSurfacePointLocations(extensionType)}catch{throw new CliError(messages.APP_CREATE_UI_POINTS_FETCH_FAILED)}finally{spinner.stop()}if(locations.length===0)throw new CliError(messages.APP_CREATE_UI_POINTS_EMPTY);return locations}async function readSurfacePointRows(locations,extensionType){try{return await appService.fetchSurfacePoints(locations,extensionType)}catch{return null}}async function fetchSurfacePointsForPages(locations,extensionType){let onPickedPages=rows=>toUsableRows(rows).filter(row=>locations.includes(row.location_name)),pagesCovered=rows=>new Set(rows.map(row=>row.location_name)).size,spinner=createSpinner(messages.APP_CREATE_UI_POINTS_SPINNER),usable;try{let narrowed=await readSurfacePointRows(locations,extensionType);if(usable=onPickedPages(narrowed??[]),narrowed===null||pagesCovered(usable)<locations.length){let unfiltered=await readSurfacePointRows();if(unfiltered===null&&narrowed===null)throw new CliError(messages.APP_CREATE_UI_POINTS_FETCH_FAILED);let fallback=onPickedPages(unfiltered??[]);pagesCovered(fallback)>pagesCovered(usable)&&(usable=fallback)}}finally{spinner.stop()}let hostable=usable.filter(row=>rowSupportsExtensionType(row,extensionType));if(hostable.length===0)throw new CliError(usable.length>0?messages.APP_CREATE_UI_POINTS_NONE_FOR_TYPE(extensionType):messages.APP_CREATE_UI_POINTS_EMPTY);return hostable}function placementLabel(row){return`${row.section_name} \u2014 ${row.component_type}`}async function promptSurfacePoint(locations,extensionType){let{surface}=await import_inquirer5.default.prompt([{type:"list",name:"surface",message:messages.APP_CREATE_UI_SURFACE_PROMPT,choices:indentChoices(locations.map(location=>({name:location,value:location})))}]),page=locations.find(location=>location===String(surface??"").trim()),forPage=(await fetchSurfacePointsForPages(page?[page]:[],extensionType)).filter(row=>row.location_name===page),question=`${PLACEMENT_QUESTION_PREFIX}${page}`,answer=await import_inquirer5.default.prompt([{type:"list",name:question,message:messages.APP_CREATE_UI_PLACEMENT_PAGE_PROMPT(page??""),choices:indentChoices(forPage.map(row=>({name:placementLabel(row),value:row.surface_point_name})))}]),chosen=String(answer[question]??"").trim();return forPage.filter(row=>row.surface_point_name===chosen)}async function promptIntegrationType(){let{integrationType}=await import_inquirer5.default.prompt([{type:"list",name:"integrationType",message:messages.APP_CREATE_UI_INTEGRATION_PROMPT,choices:indentChoices([{name:messages.APP_CREATE_UI_INTEGRATION_EXTERNAL_LINK,value:EXTENSION_TYPE_ACTION_LINK}])}]);return integrationType}async function resolveUiApp(){let extensionType=await promptIntegrationType(),locations=await fetchRecordPageLocations(extensionType),selectedRows=await promptSurfacePoint(locations,extensionType),{label}=await import_inquirer5.default.prompt([{type:"input",name:"label",message:messages.APP_CREATE_UI_LABEL_PROMPT,validate:validateUiAppLabel}]),{more_info}=await import_inquirer5.default.prompt([{type:"input",name:"more_info",message:messages.APP_CREATE_UI_MORE_INFO_PROMPT,validate:validateUiAppMoreInfo}]),{url}=await import_inquirer5.default.prompt([{type:"input",name:"url",message:messages.APP_CREATE_UI_REDIRECT_LINK_PROMPT,validate:validateUiAppUrl}]),uiApp={extension_type:extensionType,surface_point_list:buildSurfacePointList(selectedRows,{contextFor:row=>row.default_context_field??[],sizeFor:row=>row.default_size??void 0,label:String(label??"").trim(),more_info:String(more_info??"").trim(),redirect_link:String(url??"").trim()})};return validateUiApp(uiApp),uiApp}function buildSurfacePointList(rows,fields){let entries=[],seen=new Set;for(let row of rows){if(seen.has(row.surface_point_name))continue;seen.add(row.surface_point_name);let context=fields.contextFor(row).map(field=>String(field).trim()).filter(Boolean),size=sanitizeSeededSize(fields.sizeFor(row));entries.push({surface_point_name:row.surface_point_name,...context.length?{context}:{},...size?{size}:{},label:fields.label,...fields.more_info?{more_info:fields.more_info}:{},redirect_link:fields.redirect_link})}return entries}function sanitizeSeededSize(raw){if(!raw||typeof raw!="object")return;let width=typeof raw.width=="string"?raw.width.trim():"",height=typeof raw.height=="string"?raw.height.trim():"";if(!(!width&&!height))return{...width?{width}:{},...height?{height}:{}}}function buildExampleContextUrl(redirectLink,context){let url;try{url=new URL(redirectLink)}catch{return null}for(let field of context)url.searchParams.set(field,field.replaceAll(/([a-z0-9])([A-Z])/g,"$1_$2").toUpperCase());return url.toString()}function renderExampleContextUrlLines(uiApp){let withContext=uiApp.surface_point_list.find(entry=>entry.context?.length&&entry.redirect_link);if(!withContext)return[];let example=buildExampleContextUrl(withContext.redirect_link,withContext.context??[]);return example?["",`${messages.APP_CREATE_UI_BOX_EXAMPLE_URL_LABEL}`,` ${example}`,messages.APP_CREATE_UI_BOX_EXAMPLE_URL_NOTE]:[]}function renderCreatedUiApp(result,appName,uiApp,logoUri){let boxLines=[`App name: ${appName}`,`App ID: ${result.app_id}`,`Extension type: ${uiApp.extension_type}`,...formatPlacementLines(uiApp).map((line,i)=>`${i===0?"Placement: ":" "}${line}`),...logoUri?[`Logo URL: ${logoUri}`]:[],...result.version?[`App version: ${result.version}`]:[],...renderExampleContextUrlLines(uiApp),"",messages.APP_CREATE_UI_BOX_LABEL_NOTE(uiApp.surface_point_list[0]?.label??"",appName),messages.APP_CREATE_UI_BOX_HINT];printBox(messages.APP_CREATE_UI_BOX_TITLE,boxLines)}function validateHttpUrl(trimmed,invalidMessage){try{let parsed=new URL(trimmed);return parsed.protocol!=="http:"&&parsed.protocol!=="https:"?invalidMessage:!0}catch{return invalidMessage}}var validateRedirectUrl=input=>{let trimmed=input.trim();return trimmed?validateHttpUrl(trimmed,messages.APP_CREATE_REDIRECT_INVALID):messages.APP_CREATE_REDIRECT_EMPTY},validateLogoUrl=input=>{let trimmed=input.trim();return trimmed?validateHttpUrl(trimmed,messages.APP_CREATE_LOGO_INVALID):!0};function guardAgainstLinkedApp(){if(!hasLocalApp())return;let projectConfig=readProjectConfig(),linkedName=projectConfig?.appName||String(projectConfig?.appId??"");throw new CliError(messages.APP_CREATE_ALREADY_LINKED(linkedName))}async function resolveAppName(nameFlag){if(nameFlag){let nameCheck=validateAppName(nameFlag);if(nameCheck!==!0)throw new CliError(nameCheck);return nameFlag}return(await import_inquirer6.default.prompt([{type:"input",name:"name",message:messages.APP_CREATE_NAME_PROMPT,validate:validateAppName}])).name}async function resolveAppType(interactive){if(!interactive)return"oauth";let choices=[{name:messages.APP_CREATE_APP_TYPE_OAUTH,value:"oauth"}];return isFeatureAvailable("ui-app-type")&&choices.push({name:messages.APP_CREATE_APP_TYPE_UI,value:"ui"}),(await import_inquirer6.default.prompt([{type:"list",name:"appType",message:messages.APP_CREATE_APP_TYPE_PROMPT,choices:indentChoices(choices)}])).appType}function assertDistributionFlag(distributionFlag){validateEnum(distributionFlag,["private","public"],"--distribution"),distributionFlag==="public"&&assertFeatureAvailable("public-distribution")}async function resolveDistribution(distributionFlag,interactive){if(distributionFlag)return distributionFlag;if(!interactive)return"private";let choices=[{name:"Private (Used exclusively by your organisation)",value:"private"}];return(await import_inquirer6.default.prompt([{type:"list",name:"distribution",message:messages.APP_CREATE_TYPE_PROMPT,choices:indentChoices(choices)}])).distribution}async function promptAddAnotherRedirect(){let{anotherRaw}=await import_inquirer6.default.prompt([{type:"input",name:"anotherRaw",message:messages.APP_CREATE_REDIRECT_ANOTHER+" (y/N)",default:"n",validate:validateYesNo}]);return String(anotherRaw).toLowerCase().trim().startsWith("y")}async function promptRedirectUrls(quiet){let availablePort=await findAvailablePort(DEFAULT_PORT),defaultRedirect=availablePort==null||availablePort===DEFAULT_PORT?DEFAULT_REDIRECT_URI:`http://localhost:${availablePort}/auth/callback`;quiet||(availablePort==null?logInfo(messages.APP_CREATE_PORT_SCAN_FAILED(DEFAULT_PORT)):availablePort!==DEFAULT_PORT&&logInfo(messages.APP_CREATE_PORT_IN_USE(DEFAULT_PORT,availablePort)),logInfo(messages.APP_CREATE_REDIRECT_HINT(CLI.APP_START("oauth"))));let redirectUris=[],{redirectUrl:firstUrl}=await import_inquirer6.default.prompt([{type:"input",name:"redirectUrl",message:messages.APP_CREATE_REDIRECT_PROMPT,default:defaultRedirect,validate:validateRedirectUrl}]);for(redirectUris.push(firstUrl.trim());await promptAddAnotherRedirect();){let{nextUrl}=await import_inquirer6.default.prompt([{type:"input",name:"nextUrl",message:messages.APP_CREATE_REDIRECT_PROMPT,validate:validateRedirectUrl}]);redirectUris.push(nextUrl.trim())}return redirectUris}async function resolveRedirectUrls(redirectUriFlag,quiet){let flagUrls=redirectUriFlag??[];return flagUrls.length>0?flagUrls:process.stdin.isTTY?promptRedirectUrls(quiet):[DEFAULT_REDIRECT_URI]}async function resolveLogoUri(logoUriFlag,jsonMode){if(logoUriFlag||!process.stdin.isTTY||jsonMode)return logoUriFlag;let{logoUrl}=await import_inquirer6.default.prompt([{type:"input",name:"logoUrl",message:messages.APP_CREATE_LOGO_PROMPT,validate:validateLogoUrl}]);return String(logoUrl??"").trim()||void 0}async function resolveCreateDirectory(appName,interactive){let slug=computeSlug(appName);if(!interactive){let targetDir=path5.resolve(`./${slug}`);return fs5.existsSync(targetDir)?{targetDir,skipped:!0}:{targetDir,mergeOnly:!1,skipped:!1,existed:!1}}let dir=await resolveProjectDirectory(`./${slug}`);for(;!dir.unresolved&&dir.chooseAgain;)dir=await resolveProjectDirectory(`./${slug}`);if(dir.unresolved)throw new CliError(messages.APP_CREATE_DIR_UNRESOLVED);return{targetDir:dir.targetDir,mergeOnly:dir.mergeOnly,skipped:!1,existed:dir.existed}}function applyCreateDirectory(dir,jsonMode){dir.skipped||applyProjectDirectory({targetDir:dir.targetDir,mergeOnly:dir.mergeOnly,chooseAgain:!1,existed:dir.existed},jsonMode)}function buildCreatePayload(inputs){let isUiApp=!!inputs.uiApp;return{name:inputs.appName,distribution_type:inputs.distribution,...isUiApp?{ui_app:inputs.uiApp}:{auth:{scopes:[...DEFAULT_SCOPES],redirect_uris:inputs.redirectUris}},...inputs.logoUri?{logo_uri:inputs.logoUri}:{}}}async function retryCreateWithNewName(inputs){logError(messages.APP_CREATE_NAME_TAKEN);let retry=await import_inquirer6.default.prompt([{type:"input",name:"name",message:messages.APP_CREATE_NAME_PROMPT,validate:validateAppName}]),retrySpinner=createSpinner("Creating app...");try{let result=await appService.createApp(buildCreatePayload({...inputs,appName:retry.name}));return retrySpinner.stop(),{result,appName:retry.name}}catch(retryErr){throw retrySpinner.stop(),retryErr}}async function retryCreateAfterLogin(inputs){logWarn(messages.APP_CREATE_SESSION_EXPIRED);let{relogin}=await import_inquirer6.default.prompt([{type:"confirm",name:"relogin",message:messages.APP_CREATE_RELOGIN_CONFIRM,default:!0}]);if(!relogin)throw new AuthExpiredError;if(await loginCommand({suppressNextSteps:!0}),!isAuthenticated())throw new AuthExpiredError;let spinner=createSpinner("Creating app...");try{return{result:await appService.createApp(buildCreatePayload(inputs)),appName:inputs.appName}}finally{spinner.stop()}}function isPublicDistributionRefusal(err,distribution){return err instanceof ApiError&&err.statusCode===400&&distribution==="public"&&/distribution_type/i.test(err.message)}async function createAppWithRetry(inputs,jsonMode,interactive){let spinner=createSpinner("Creating app...",{silent:jsonMode});try{let result=await appService.createApp(buildCreatePayload(inputs));return spinner.stop(),{result,appName:inputs.appName}}catch(err){if(spinner.stop(),err instanceof ApiError&&err.errorCode==="APP_LIMIT_REACHED")throw jsonMode&&jsonOutput({error:"APP_LIMIT_REACHED",message:messages.APP_CREATE_LIMIT_REACHED}),new CliError(messages.APP_CREATE_LIMIT_REACHED);if(isPublicDistributionRefusal(err,inputs.distribution))throw new CliError(messages.APP_CREATE_PUBLIC_REJECTED(err.message));if(err instanceof ApiError&&err.statusCode===409)return retryCreateWithNewName(inputs);if(err instanceof AuthExpiredError&&interactive)return retryCreateAfterLogin(inputs);throw err}}function renderCreatedApp(result,appName,logoUri){let boxLines=[`App name: ${appName}`,`App ID: ${result.app_id}`,`Client ID: ${result.client_id}`,`Client secret: ${messages.CLIENT_SECRET_HIDDEN_HUMAN}`,...(result.redirect_uris??[]).map((uri,i)=>`Redirect URL ${i+1}: ${uri}`),...logoUri?[`Logo URL: ${logoUri}`]:[],...result.version?[`App version: ${result.version}`]:[],`${messages.APP_CREATE_BOX_SCOPES_LABEL} ${[...DEFAULT_SCOPES].join(", ")}`,"",messages.APP_CREATE_BOX_SCOPE_HINT];printBox(messages.APP_CREATE_BOX_TITLE,boxLines)}var createCommand=withCommandHandler(async options=>{let jsonMode=!!options.json,originalCwd=process.cwd();guardAgainstLinkedApp(),assertDistributionFlag(options.distribution);let interactive=!jsonMode&&!!process.stdin.isTTY,appName=await resolveAppName(options.name),logoUri=await resolveLogoUri(options.logoUri,jsonMode),distribution=await resolveDistribution(options.distribution,interactive),appType=await resolveAppType(interactive),redirectUris=[],uiApp;appType==="ui"?uiApp=await resolveUiApp():redirectUris=await resolveRedirectUrls(options.redirectUri,jsonMode);let dir=await resolveCreateDirectory(appName,interactive),inputs={appName,distribution,redirectUris,logoUri,uiApp},{result,appName:finalAppName}=await createAppWithRetry(inputs,jsonMode,interactive);applyCreateDirectory(dir,jsonMode),result.client_id&&result.client_secret&&saveAppCredentials(result.app_id,{clientId:result.client_id,clientSecret:result.client_secret}),finalAppName&&saveAppName(result.app_id,finalAppName);let jsonBase={appId:result.app_id,appName:finalAppName,clientId:result.client_id,clientSecret:messages.CLIENT_SECRET_HIDDEN_JSON,appType,...uiApp?{uiApp}:{redirectUri:result.redirect_uris},...logoUri?{logoUri}:{},...result.version?{version:result.version}:{}},renderBox2=()=>uiApp?renderCreatedUiApp(result,finalAppName,uiApp,logoUri):renderCreatedApp(result,finalAppName,logoUri);if(dir.skipped){if(jsonMode){jsonOutput({...jsonBase,directory:dir.targetDir,scaffoldSkipped:messages.APP_CREATE_JSON_SCAFFOLD_DIR_EXISTS(dir.targetDir)});return}renderBox2(),logInfo(messages.APP_CREATE_DIR_EXISTS_SKIPPED(dir.targetDir));return}let fallbackApp={...result,client_id:result.client_id??"",redirect_uris:result.redirect_uris??null},ctx=await fetchAppContext(result.app_id,jsonMode,uiApp,fallbackApp),base=runBaseScaffold(result.app_id,ctx,dir.targetDir,dir.mergeOnly);if(jsonMode){jsonOutput({...jsonBase,directory:dir.targetDir,scaffolded:base.written});return}renderBox2(),reportBaseScaffoldSuccess(base),await finishProject({appId:result.app_id,ctx,targetDir:dir.targetDir,baseScopes:base.scopes,cdDir:computeCdHint(originalCwd,dir.targetDir),isUiApp:!!uiApp,offerFeature:interactive,onConflict:dir.mergeOnly?"merge":"overwrite"})});var http=__toESM(require("node:http")),import_node_crypto=require("node:crypto");var MAX_BODY_BYTES=16*1024,DEFAULT_TIMEOUT_MS=3e5;function normalizeTokens(raw){return typeof raw.access_token!="string"||!raw.access_token||typeof raw.refresh_token!="string"||!raw.refresh_token||typeof raw.expires_in!="number"||!Number.isFinite(raw.expires_in)||raw.expires_in<=0||typeof raw.token_type!="string"||!raw.token_type?null:{accessToken:raw.access_token,refreshToken:raw.refresh_token,expiresIn:raw.expires_in,tokenType:raw.token_type,scope:typeof raw.scope=="string"?raw.scope:void 0}}async function runBrowserLoginFlow(opts){let proxyOrigin=new URL(opts.proxyUrl).origin,timeoutMs=opts.timeoutMs??DEFAULT_TIMEOUT_MS,openBrowser2=opts.openBrowser??(()=>{});return new Promise((resolve11,reject)=>{let settled=!1,claimSettlement=()=>settled?!1:(settled=!0,!0),server=http.createServer((req,res)=>{let pathname=new URL(req.url??"/","http://127.0.0.1").pathname,origin=req.headers.origin;if(logDebug("loopback request",{method:req.method,url:req.url,pathname,origin}),req.method==="OPTIONS"&&pathname==="/callback"){if(origin!==proxyOrigin){logDebug("loopback OPTIONS rejected: origin mismatch",{origin,expected:proxyOrigin}),res.writeHead(403).end();return}res.writeHead(204,{"Access-Control-Allow-Origin":proxyOrigin,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type","Access-Control-Max-Age":"600"}).end();return}if(req.method==="POST"&&pathname==="/callback"){if(origin!==proxyOrigin){logDebug("loopback POST rejected: origin mismatch",{origin,expected:proxyOrigin}),res.writeHead(403).end();return}let bytes=0,chunks=[];req.on("data",chunk=>{if(bytes+=chunk.length,bytes>MAX_BODY_BYTES){logDebug("loopback POST rejected: body too large",{bytes,max:MAX_BODY_BYTES}),res.writeHead(413,{Connection:"close"}).end(),req.destroy();return}chunks.push(chunk)}),req.on("end",()=>{let parsed=null;try{parsed=JSON.parse(Buffer.concat(chunks).toString("utf-8"))}catch{parsed=null}let tokens=parsed?normalizeTokens(parsed):null;if(!tokens){logDebug("loopback POST rejected: bad payload shape",{hasParsed:parsed!==null,keys:parsed?Object.keys(parsed):null}),res.writeHead(400,{"Access-Control-Allow-Origin":proxyOrigin,"Content-Type":"text/plain"}).end("Bad payload");return}logDebug("loopback POST accepted",{hasScope:tokens.scope!==void 0}),res.writeHead(204,{"Access-Control-Allow-Origin":proxyOrigin}).end(),claimSettlement()&&(server.close(),resolve11(tokens))});return}if(req.method==="GET"&&(pathname==="/"||pathname==="/callback")){res.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),res.end('<!doctype html><meta charset="utf-8"><title>Brevo CLI login</title><p>Waiting for login to complete \u2014 you can close this tab once the CLI confirms success.</p>');return}logDebug("loopback request not matched",{method:req.method,pathname}),res.writeHead(404).end()});server.on("error",err=>{logDebug("loopback server error",{message:err.message}),claimSettlement()&&reject(err)}),server.listen(0,"127.0.0.1",()=>{let port=server.address().port,attemptToken=(0,import_node_crypto.randomUUID)(),loginUrl=`${opts.proxyUrl}/login?port=${port}&t=${attemptToken}`;logDebug("loopback listening",{host:"127.0.0.1",port,proxyOrigin}),opts.onWaiting?.(loginUrl);try{openBrowser2(loginUrl)}catch{}});let timer=setTimeout(()=>{claimSettlement()&&(server.close(),reject(new CliError(messages.AUTH_BROWSER_TIMEOUT)))},timeoutMs);timer.unref?.(),server.on("close",()=>clearTimeout(timer))})}function wipeAppsCacheIfAccountChanged(newOrganizationId){let previousOrganizationId=getOrganizationId();previousOrganizationId&&previousOrganizationId!==newOrganizationId&&clearAppsCache()}async function promptApiKey(){let{key}=await import_inquirer7.default.prompt([{type:"password",name:"key",message:messages.AUTH_PROMPT_API_KEY,mask:"*",validate:input=>input.trim().length>0||"API key cannot be empty"}]);return key}async function resolveLoginMethod(forceBrowser,apiKey){if(forceBrowser){if(!process.stdin.isTTY)throw new CliError(messages.AUTH_BROWSER_NON_INTERACTIVE);return"browser"}if(apiKey)return"api-key";if(!process.stdin.isTTY)throw new CliError(messages.AUTH_BROWSER_NON_INTERACTIVE);let{chosen}=await import_inquirer7.default.prompt([{type:"list",name:"chosen",message:messages.AUTH_PROMPT_METHOD,choices:indentChoices([{name:"Browser (sign in through your browser)",value:"browser"},{name:"API key (paste from your Brevo dashboard)",value:"api-key"}]),default:"browser"}]);return chosen}async function retryApiKeyValidation(quiet){let retryKey=await promptApiKey(),retrySpinner=createSpinner("Validating API key...",{silent:quiet});try{let account=await accountService.validateApiKey(retryKey);return retrySpinner.stop(),{account,apiKey:retryKey}}catch(retryErr){throw retrySpinner.stop(),retryErr instanceof ApiError&&retryErr.statusCode===401?new CliError(messages.AUTH_INVALID_KEY,EXIT_CODES.AUTH_FAILURE):retryErr}}async function validateApiKeyWithRetry(apiKey,quiet){let spinner=createSpinner("Validating API key...",{silent:quiet});try{let account=await accountService.validateApiKey(apiKey);return spinner.stop(),{account,apiKey}}catch(err){if(spinner.stop(),!(err instanceof ApiError&&err.statusCode===401)||(logError(messages.AUTH_INVALID_KEY),quiet||logInfo(` ${messages.AUTH_GET_KEY_URL}`),!process.stdin.isTTY))throw err;return retryApiKeyValidation(quiet)}}async function loginWithApiKey(envApiKey,quiet){let apiKey=envApiKey;if(apiKey||(openBrowser(BREVO_DASHBOARD_API_KEYS_URL),quiet||process.stdout.write(messages.AUTH_HINT(BREVO_DASHBOARD_API_KEYS_URL,BREVO_API_KEY_DOCS_URL)),apiKey=await promptApiKey()),!apiKey)throw new CliError("No API key provided.");let validated=await validateApiKeyWithRetry(apiKey,quiet);if(!validated.account)throw new CliError("Authentication failed.");return wipeAppsCacheIfAccountChanged(validated.account.organization_id),saveCredentials(validated.apiKey,{email:validated.account.email,organizationId:validated.account.organization_id,userId:validated.account.user_id}),validated.account}async function loginWithBrowser(quiet){quiet||logInfo(` ${messages.AUTH_BROWSER_OPENING}`);let tokens=await runBrowserLoginFlow({proxyUrl:OAUTH_PROXY_URL,openBrowser,onWaiting:url=>{quiet||(logInfo(` ${messages.AUTH_BROWSER_FALLBACK_URL(url)}`),logInfo(` ${messages.AUTH_BROWSER_WAITING}`))}}),tokensToStore={accessToken:tokens.accessToken,refreshToken:tokens.refreshToken,expiresIn:tokens.expiresIn,tokenType:tokens.tokenType,scope:tokens.scope};saveOauthCredentials(tokensToStore),quiet||logSuccess(messages.AUTH_BROWSER_TOKENS_RECEIVED(getCredentialsPath()));let spinner=createSpinner("Finishing login...",{silent:quiet}),account;try{account=await client.getWithBearer(ENDPOINTS.ACCOUNT,tokens.accessToken,tokens.tokenType)}catch(err){throw err instanceof ApiError&&err.statusCode===401&&clearCredentials(),err}finally{spinner.stop()}if(!account)throw new CliError("Authentication failed.");return wipeAppsCacheIfAccountChanged(account.organization_id),saveOauthCredentials(tokensToStore,{email:account.email,organizationId:account.organization_id,userId:account.user_id}),account}async function showNextSteps(){let apps=[],appsSpinner=createSpinner("Checking your apps...");try{apps=await appService.fetchAppsList()}catch{}finally{appsSpinner.stop()}if(apps.length>0){printBox("What's next?",[CLI.APP_CREATE,CLI.APP_LIST,CLI.APP_SCAFFOLD,CLI.APP_CREDENTIALS()]);return}if(!process.stdin.isTTY){logInfo(`
141
142
  ${messages.AUTH_NEXT}
142
143
  `);return}process.stdout.write(`
143
144
  `);let{shouldCreate}=await import_inquirer7.default.prompt([{type:"confirm",name:"shouldCreate",message:messages.AUTH_CREATE_APP_PROMPT,default:!0}]);if(shouldCreate){process.stdout.write(`