@getbrevo/cli 2.0.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +237 -0
- package/README.md +32 -6
- package/agent-context/AGENTS.md +62 -15
- package/agent-context/SKILL.md +98 -14
- package/dist/bin/files/AGENTS.md.tmpl +119 -0
- package/dist/bin/files/CLAUDE.md.tmpl +139 -0
- package/dist/bin/files/README.md.tmpl +184 -0
- package/dist/bin/files/app-config.json.tmpl +16 -0
- package/dist/{templates → bin}/files/src/oauth/.env.example.tmpl +8 -0
- package/dist/{templates → bin}/files/src/oauth/.env.local.tmpl +2 -0
- package/dist/{templates → bin}/files/src/oauth/handler.js.tmpl +49 -7
- package/dist/{templates → bin}/files/src/oauth/server.js.tmpl +20 -14
- package/dist/{templates → bin}/files/src/oauth/token-store.js.tmpl +19 -0
- package/dist/bin/index.js +680 -235
- package/dist/bin/index.js.map +7 -1
- package/package.json +6 -4
- package/dist/api/client.d.ts +0 -25
- package/dist/api/client.d.ts.map +0 -1
- package/dist/api/client.js +0 -205
- package/dist/api/client.js.map +0 -1
- package/dist/bin/index.d.ts +0 -3
- package/dist/bin/index.d.ts.map +0 -1
- package/dist/commands/app/create.d.ts +0 -8
- package/dist/commands/app/create.d.ts.map +0 -1
- package/dist/commands/app/create.js +0 -325
- package/dist/commands/app/create.js.map +0 -1
- package/dist/commands/app/credentials.d.ts +0 -6
- package/dist/commands/app/credentials.d.ts.map +0 -1
- package/dist/commands/app/credentials.js +0 -114
- package/dist/commands/app/credentials.js.map +0 -1
- package/dist/commands/app/delete.d.ts +0 -6
- package/dist/commands/app/delete.d.ts.map +0 -1
- package/dist/commands/app/delete.js +0 -167
- package/dist/commands/app/delete.js.map +0 -1
- package/dist/commands/app/list.d.ts +0 -4
- package/dist/commands/app/list.d.ts.map +0 -1
- package/dist/commands/app/list.js +0 -71
- package/dist/commands/app/list.js.map +0 -1
- package/dist/commands/app/scaffold.d.ts +0 -5
- package/dist/commands/app/scaffold.d.ts.map +0 -1
- package/dist/commands/app/scaffold.js +0 -213
- package/dist/commands/app/scaffold.js.map +0 -1
- package/dist/commands/app/scopes.d.ts +0 -2
- package/dist/commands/app/scopes.d.ts.map +0 -1
- package/dist/commands/app/scopes.js +0 -81
- package/dist/commands/app/scopes.js.map +0 -1
- package/dist/commands/app/start.d.ts +0 -5
- package/dist/commands/app/start.d.ts.map +0 -1
- package/dist/commands/app/start.js +0 -220
- package/dist/commands/app/start.js.map +0 -1
- package/dist/commands/app/update.d.ts +0 -2
- package/dist/commands/app/update.d.ts.map +0 -1
- package/dist/commands/app/update.js +0 -444
- package/dist/commands/app/update.js.map +0 -1
- package/dist/commands/definitions.d.ts +0 -5
- package/dist/commands/definitions.d.ts.map +0 -1
- package/dist/commands/definitions.js +0 -262
- package/dist/commands/definitions.js.map +0 -1
- package/dist/commands/init.d.ts +0 -2
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -138
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/login.d.ts +0 -6
- package/dist/commands/login.d.ts.map +0 -1
- package/dist/commands/login.js +0 -251
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.d.ts +0 -5
- package/dist/commands/logout.d.ts.map +0 -1
- package/dist/commands/logout.js +0 -54
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/skill/install.d.ts +0 -4
- package/dist/commands/skill/install.d.ts.map +0 -1
- package/dist/commands/skill/install.js +0 -29
- package/dist/commands/skill/install.js.map +0 -1
- package/dist/commands/skill/uninstall.d.ts +0 -4
- package/dist/commands/skill/uninstall.d.ts.map +0 -1
- package/dist/commands/skill/uninstall.js +0 -23
- package/dist/commands/skill/uninstall.js.map +0 -1
- package/dist/commands/whoami.d.ts +0 -4
- package/dist/commands/whoami.d.ts.map +0 -1
- package/dist/commands/whoami.js +0 -85
- package/dist/commands/whoami.js.map +0 -1
- package/dist/container.d.ts +0 -7
- package/dist/container.d.ts.map +0 -1
- package/dist/container.js +0 -28
- package/dist/container.js.map +0 -1
- package/dist/lang/en.d.ts +0 -162
- package/dist/lang/en.d.ts.map +0 -1
- package/dist/lang/en.js +0 -189
- package/dist/lang/en.js.map +0 -1
- package/dist/lib/abort.d.ts +0 -3
- package/dist/lib/abort.d.ts.map +0 -1
- package/dist/lib/abort.js +0 -30
- package/dist/lib/abort.js.map +0 -1
- package/dist/lib/auth-guard.d.ts +0 -13
- package/dist/lib/auth-guard.d.ts.map +0 -1
- package/dist/lib/auth-guard.js +0 -60
- package/dist/lib/auth-guard.js.map +0 -1
- package/dist/lib/browser.d.ts +0 -2
- package/dist/lib/browser.d.ts.map +0 -1
- package/dist/lib/browser.js +0 -16
- package/dist/lib/browser.js.map +0 -1
- package/dist/lib/cli-version.d.ts +0 -2
- package/dist/lib/cli-version.d.ts.map +0 -1
- package/dist/lib/cli-version.js +0 -59
- package/dist/lib/cli-version.js.map +0 -1
- package/dist/lib/command-handler.d.ts +0 -10
- package/dist/lib/command-handler.d.ts.map +0 -1
- package/dist/lib/command-handler.js +0 -16
- package/dist/lib/command-handler.js.map +0 -1
- package/dist/lib/command-registry.d.ts +0 -28
- package/dist/lib/command-registry.d.ts.map +0 -1
- package/dist/lib/command-registry.js +0 -54
- package/dist/lib/command-registry.js.map +0 -1
- package/dist/lib/config.d.ts +0 -83
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/config.js +0 -430
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/constants.d.ts +0 -48
- package/dist/lib/constants.d.ts.map +0 -1
- package/dist/lib/constants.js +0 -121
- package/dist/lib/constants.js.map +0 -1
- package/dist/lib/errors.d.ts +0 -30
- package/dist/lib/errors.d.ts.map +0 -1
- package/dist/lib/errors.js +0 -66
- package/dist/lib/errors.js.map +0 -1
- package/dist/lib/exit-codes.d.ts +0 -9
- package/dist/lib/exit-codes.d.ts.map +0 -1
- package/dist/lib/exit-codes.js +0 -12
- package/dist/lib/exit-codes.js.map +0 -1
- package/dist/lib/hidden-input.d.ts +0 -2
- package/dist/lib/hidden-input.d.ts.map +0 -1
- package/dist/lib/hidden-input.js +0 -78
- package/dist/lib/hidden-input.js.map +0 -1
- package/dist/lib/json-output.d.ts +0 -6
- package/dist/lib/json-output.d.ts.map +0 -1
- package/dist/lib/json-output.js +0 -11
- package/dist/lib/json-output.js.map +0 -1
- package/dist/lib/logger.d.ts +0 -11
- package/dist/lib/logger.d.ts.map +0 -1
- package/dist/lib/logger.js +0 -110
- package/dist/lib/logger.js.map +0 -1
- package/dist/lib/oauth-freshness.d.ts +0 -30
- package/dist/lib/oauth-freshness.d.ts.map +0 -1
- package/dist/lib/oauth-freshness.js +0 -61
- package/dist/lib/oauth-freshness.js.map +0 -1
- package/dist/lib/port.d.ts +0 -25
- package/dist/lib/port.d.ts.map +0 -1
- package/dist/lib/port.js +0 -103
- package/dist/lib/port.js.map +0 -1
- package/dist/lib/telemetry.d.ts +0 -9
- package/dist/lib/telemetry.d.ts.map +0 -1
- package/dist/lib/telemetry.js +0 -57
- package/dist/lib/telemetry.js.map +0 -1
- package/dist/lib/ui.d.ts +0 -14
- package/dist/lib/ui.d.ts.map +0 -1
- package/dist/lib/ui.js +0 -75
- package/dist/lib/ui.js.map +0 -1
- package/dist/lib/update-notifier.d.ts +0 -37
- package/dist/lib/update-notifier.d.ts.map +0 -1
- package/dist/lib/update-notifier.js +0 -298
- package/dist/lib/update-notifier.js.map +0 -1
- package/dist/lib/validators.d.ts +0 -55
- package/dist/lib/validators.d.ts.map +0 -1
- package/dist/lib/validators.js +0 -166
- package/dist/lib/validators.js.map +0 -1
- package/dist/services/account.d.ts +0 -8
- package/dist/services/account.d.ts.map +0 -1
- package/dist/services/account.js +0 -15
- package/dist/services/account.js.map +0 -1
- package/dist/services/app.d.ts +0 -38
- package/dist/services/app.d.ts.map +0 -1
- package/dist/services/app.js +0 -145
- package/dist/services/app.js.map +0 -1
- package/dist/services/browser-auth.d.ts +0 -15
- package/dist/services/browser-auth.d.ts.map +0 -1
- package/dist/services/browser-auth.js +0 -188
- package/dist/services/browser-auth.js.map +0 -1
- package/dist/services/normalize-app-id.d.ts +0 -12
- package/dist/services/normalize-app-id.d.ts.map +0 -1
- package/dist/services/normalize-app-id.js +0 -38
- package/dist/services/normalize-app-id.js.map +0 -1
- package/dist/services/oauth-metadata.d.ts +0 -7
- package/dist/services/oauth-metadata.d.ts.map +0 -1
- package/dist/services/oauth-metadata.js +0 -43
- package/dist/services/oauth-metadata.js.map +0 -1
- package/dist/services/oauth-refresh.d.ts +0 -14
- package/dist/services/oauth-refresh.d.ts.map +0 -1
- package/dist/services/oauth-refresh.js +0 -70
- package/dist/services/oauth-refresh.js.map +0 -1
- package/dist/services/scopes-html.d.ts +0 -3
- package/dist/services/scopes-html.d.ts.map +0 -1
- package/dist/services/scopes-html.js +0 -516
- package/dist/services/scopes-html.js.map +0 -1
- package/dist/services/scopes-web.d.ts +0 -12
- package/dist/services/scopes-web.d.ts.map +0 -1
- package/dist/services/scopes-web.js +0 -95
- package/dist/services/scopes-web.js.map +0 -1
- package/dist/services/skill.d.ts +0 -48
- package/dist/services/skill.d.ts.map +0 -1
- package/dist/services/skill.js +0 -223
- package/dist/services/skill.js.map +0 -1
- package/dist/skills/index.d.ts +0 -47
- package/dist/skills/index.d.ts.map +0 -1
- package/dist/skills/index.js +0 -61
- package/dist/skills/index.js.map +0 -1
- package/dist/templates/files/AGENTS.md.tmpl +0 -52
- package/dist/templates/files/CLAUDE.md.tmpl +0 -59
- package/dist/templates/files/README.md.tmpl +0 -85
- package/dist/templates/files/app-config.json.tmpl +0 -25
- package/dist/templates/index.d.ts +0 -36
- package/dist/templates/index.d.ts.map +0 -1
- package/dist/templates/index.js +0 -105
- package/dist/templates/index.js.map +0 -1
- package/dist/types.d.ts +0 -32
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- /package/dist/{templates → bin}/files/gitignore.tmpl +0 -0
- /package/dist/{templates → bin}/files/src/oauth/package.json.tmpl +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# {{APP_NAME}}
|
|
2
|
+
{{#if oauth}}
|
|
3
|
+
|
|
4
|
+
Brevo OAuth integration for testing and implementing app authorization flows, scaffolded with the Brevo CLI.
|
|
5
|
+
|
|
6
|
+
> **This is a starter / test app** to help you exercise the OAuth flow locally. Treat it as a reference — it is not intended for production use.
|
|
7
|
+
|
|
8
|
+
## Setup
|
|
9
|
+
|
|
10
|
+
1. Install dependencies for the oauth feature:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
yarn --cwd src/oauth install
|
|
14
|
+
# or: npm --prefix src/oauth install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
2. Start the local OAuth test server (the CLI reads `app-config.json` for port and redirect URLs):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
brevo app start oauth
|
|
21
|
+
# or choose a port: brevo app start oauth --port 3000
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
3. Open the login URL printed by the CLI (typically `http://localhost:<port>/auth/login`). The OAuth server redirects back to `http://localhost:<port>/auth/callback` — make sure that URL is registered on your app's redirect list.
|
|
25
|
+
|
|
26
|
+
## Project structure
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
.
|
|
30
|
+
├── app-config.json App config — ID, scopes, redirect URLs, logo URL
|
|
31
|
+
├── .gitignore Ignores .env.local and build artifacts
|
|
32
|
+
├── AGENTS.md AI agent guidance
|
|
33
|
+
├── CLAUDE.md Claude Code guidance
|
|
34
|
+
├── README.md (this file)
|
|
35
|
+
└── src/
|
|
36
|
+
└── oauth/ Self-contained OAuth test feature
|
|
37
|
+
├── package.json Dependencies & `yarn start` script
|
|
38
|
+
├── server.js Test server entry point
|
|
39
|
+
├── handler.js OAuth routes (/auth/login, /auth/callback, /auth/refresh)
|
|
40
|
+
├── token-store.js In-memory token management
|
|
41
|
+
├── .env.example Credential template (committed)
|
|
42
|
+
└── .env.local Local credentials (gitignored, chmod 600)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Credentials
|
|
46
|
+
|
|
47
|
+
- Client ID and secret are written to `src/oauth/.env.local` during scaffold (file permissions `600`).
|
|
48
|
+
- Re-fetch them any time with `brevo app credentials --app-id {{APP_ID}} --reveal-secret`.
|
|
49
|
+
- `.env.local` is in `.gitignore` — **never commit it**.
|
|
50
|
+
|
|
51
|
+
## Security
|
|
52
|
+
|
|
53
|
+
- Never commit `src/oauth/.env.local` — it contains your client secret.
|
|
54
|
+
- The test server uses a random `state` parameter for CSRF protection and binds to localhost only.
|
|
55
|
+
- Access tokens are kept in process memory; restarting the server clears them.
|
|
56
|
+
|
|
57
|
+
## Updating your app
|
|
58
|
+
|
|
59
|
+
`brevo app upload` has no edit flags — it always pushes the full contents of `app-config.json`. To change something, edit the file, then run:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
brevo app upload
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For example, to rename the app or add a redirect URL, edit `appName` or `auth.redirectUris` in `app-config.json`:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# 1. Edit app-config.json (e.g. change "appName", or append to "auth.redirectUris")
|
|
69
|
+
# 2. Push the change
|
|
70
|
+
brevo app upload
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`brevo app upload` always fetches the current server state first and shows a diff before pushing — review it before confirming (or pass `--yes` to skip the prompt).
|
|
74
|
+
|
|
75
|
+
To avoid unintentionally accumulating stale redirect URLs over time, periodically review `auth.redirectUris` in `app-config.json` and remove obsolete entries before running `brevo app upload`.
|
|
76
|
+
|
|
77
|
+
`brevo app upload` has no `--app-id` flag — it always resolves the app from this directory's `app-config.json`. To manage a different app, `cd` into that app's project directory (or use `brevo app credentials --app-id <id>` to inspect it without editing).
|
|
78
|
+
|
|
79
|
+
## Docs
|
|
80
|
+
|
|
81
|
+
- [Brevo API Documentation](https://developers.brevo.com)
|
|
82
|
+
- [Brevo CLI reference](https://developers.brevo.com/docs/cli-reference)
|
|
83
|
+
- [Brevo CLI](https://github.com/getbrevo/brevo-cli)
|
|
84
|
+
{{/if}}
|
|
85
|
+
{{#if ui_app}}
|
|
86
|
+
|
|
87
|
+
Brevo **UI app** — an action link that renders inside Brevo CRM records, scaffolded with the Brevo CLI.
|
|
88
|
+
|
|
89
|
+
> **There is no local server to run and no code to install.** A UI app has no OAuth callback. Brevo opens a URL you already host, and this project is the app's configuration.
|
|
90
|
+
|
|
91
|
+
## Setup
|
|
92
|
+
|
|
93
|
+
1. Open `app-config.json` and check the `ui_app` block — the placement, its label, and the URL Brevo will open.
|
|
94
|
+
|
|
95
|
+
2. Push it to Brevo (the CLI shows a diff of local vs. server first):
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
brevo app upload
|
|
99
|
+
# or skip the confirmation: brevo app upload --yes
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
3. Make it available in an account, then open a CRM record to see it:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
brevo app install
|
|
106
|
+
# or target a specific sub-account: brevo app install <account-id>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Remove it again with `brevo app uninstall` (same optional `<account-id>`).
|
|
110
|
+
|
|
111
|
+
## Project structure
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
.
|
|
115
|
+
├── app-config.json App metadata + the `ui_app` block (the whole app)
|
|
116
|
+
├── .gitignore Ignores .env.local and build artifacts
|
|
117
|
+
├── AGENTS.md AI agent guidance
|
|
118
|
+
├── CLAUDE.md Claude Code guidance
|
|
119
|
+
└── README.md (this file)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## The `ui_app` block
|
|
123
|
+
|
|
124
|
+
`ui_app` is what makes this a UI app rather than an OAuth one, which is why `auth` is `{}` — leave it empty.
|
|
125
|
+
|
|
126
|
+
`extension_type` sits at the root and picks how the app opens:
|
|
127
|
+
|
|
128
|
+
| Value | What it does | Destination field |
|
|
129
|
+
|-------|--------------|-------------------|
|
|
130
|
+
| `actionLink` | Opens your URL in a new tab | `redirect_link` |
|
|
131
|
+
| `iframeExtension` | Embeds your URL in a modal | `modal_iframe_url` |
|
|
132
|
+
|
|
133
|
+
Each entry in `surface_point_list` is one placement, with its own text and destination:
|
|
134
|
+
|
|
135
|
+
| Key | Required | Notes |
|
|
136
|
+
|-----|----------|-------|
|
|
137
|
+
| `surface_point_name` | yes | Where it appears, as a dotted slug — e.g. `contactDetails.header.menu` |
|
|
138
|
+
| `label` | yes | Max 48 chars — the menu entry's text, or a card's button |
|
|
139
|
+
| `more_info` | no | Max 255 chars — the menu entry's second line, or a card's description |
|
|
140
|
+
| `redirect_link` | for `actionLink` | `https://` (or `http://localhost`) |
|
|
141
|
+
| `modal_iframe_url` | for `iframeExtension` | `https://` (or `http://localhost`) |
|
|
142
|
+
| `context` | no | Narrows the record context Brevo sends |
|
|
143
|
+
| `size` | no | `{ "width": "280px", "height": "160px" }` — `px`, or `%` up to 100 |
|
|
144
|
+
|
|
145
|
+
A card's title is the app name — there is no field for it. Don't add `link_target`: the CLI sets it on upload, and a copy in this file shows up as drift you can't clear.
|
|
146
|
+
|
|
147
|
+
## Record context
|
|
148
|
+
|
|
149
|
+
Brevo appends the record context to your URL as **query parameters** — the path is never templated. For example:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
https://example.com/open?recordId=123&recordType=contact&accountId=456
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
These values reach you through the browser, so validate them like any other user input.
|
|
156
|
+
|
|
157
|
+
## Adding a placement
|
|
158
|
+
|
|
159
|
+
`brevo app create` sets up one. To add another, append a `surface_point_list` entry with its own `label` and destination, then:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
brevo app upload
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Slot names are checked by Brevo, not locally — `brevo app upload` fails and names any slot that isn't registered.
|
|
166
|
+
|
|
167
|
+
## Updating your app
|
|
168
|
+
|
|
169
|
+
`brevo app upload` has no edit flags — it always pushes the full contents of `app-config.json`. To change the app's name, label, destination or placement, edit the file and run:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
brevo app upload
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
It fetches the current server state first and shows a diff before pushing — review it before confirming (or pass `--yes` to skip the prompt).
|
|
176
|
+
|
|
177
|
+
`brevo app upload` has no `--app-id` flag either: it always resolves the app from this directory's `app-config.json`. To manage a different app, `cd` into that app's project directory.
|
|
178
|
+
|
|
179
|
+
## Docs
|
|
180
|
+
|
|
181
|
+
- [Brevo API Documentation](https://developers.brevo.com)
|
|
182
|
+
- [Brevo CLI reference](https://developers.brevo.com/docs/cli-reference)
|
|
183
|
+
- [Brevo CLI](https://github.com/getbrevo/brevo-cli)
|
|
184
|
+
{{/if}}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"appId": "{{APP_ID}}",
|
|
3
|
+
"appName": "{{APP_NAME}}",
|
|
4
|
+
"version": "{{APP_VERSION}}",
|
|
5
|
+
"logoUri": "{{LOGO_URI}}",
|
|
6
|
+
"distribution_type": "{{DISTRIBUTION}}",
|
|
7
|
+
{{#if ui_app}}
|
|
8
|
+
"ui_app": {{UI_APP_JSON}},
|
|
9
|
+
{{/if}}
|
|
10
|
+
"auth": {
|
|
11
|
+
{{#if oauth}}
|
|
12
|
+
"scopes": {{SCOPES_JSON}},
|
|
13
|
+
"redirectUris": {{REDIRECT_URLS_JSON}}
|
|
14
|
+
{{/if}}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
# Brevo OAuth Credentials — copy this file to .env.local and fill in your values
|
|
2
|
+
{{#if private}}
|
|
2
3
|
# Get CLIENT_ID and CLIENT_SECRET: brevo app credentials --reveal-secret
|
|
4
|
+
{{/if}}
|
|
5
|
+
{{#if public}}
|
|
6
|
+
# Get CLIENT_ID: brevo app credentials
|
|
7
|
+
# This is a public app — it uses PKCE (RFC 7636) and no client secret.
|
|
8
|
+
{{/if}}
|
|
3
9
|
|
|
4
10
|
CLIENT_ID=your_client_id_here
|
|
11
|
+
{{#if private}}
|
|
5
12
|
CLIENT_SECRET=your_client_secret_here
|
|
13
|
+
{{/if}}
|
|
6
14
|
REDIRECT_URI={{REDIRECT_URI}}
|
|
7
15
|
|
|
8
16
|
# OAuth server
|
|
@@ -38,7 +38,9 @@ const REVEAL_SCRIPT =
|
|
|
38
38
|
'<script>document.querySelectorAll(".reveal-btn").forEach(function(btn){btn.addEventListener("click",function(){btn.parentElement.querySelector(".token-value").classList.add("revealed");btn.classList.add("hidden")})});</script>';
|
|
39
39
|
|
|
40
40
|
const CLIENT_ID = process.env.CLIENT_ID;
|
|
41
|
+
{{#if private}}
|
|
41
42
|
const CLIENT_SECRET = process.env.CLIENT_SECRET;
|
|
43
|
+
{{/if}}
|
|
42
44
|
const REDIRECT_URI = process.env.REDIRECT_URI || '{{REDIRECT_URI}}';
|
|
43
45
|
const OAUTH_BASE = process.env.OAUTH_BASE || '{{OAUTH_BASE}}';
|
|
44
46
|
const OAUTH_REALM = process.env.OAUTH_REALM || '{{OAUTH_REALM}}';
|
|
@@ -55,19 +57,48 @@ try {
|
|
|
55
57
|
console.warn('Warning: Could not read app-config.json — defaulting scopes to "all".');
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
// Build the OAuth authorization URL. Extracted so /auth/login and the
|
|
61
|
+
// server.js landing page render the exact same URL — no drift between the two.
|
|
62
|
+
{{#if public}}
|
|
63
|
+
// `codeChallenge` is the PKCE (RFC 7636) S256 challenge. Base64url output is
|
|
64
|
+
// already URL-safe, so it is not re-encoded (matching the `state` treatment).
|
|
65
|
+
function buildAuthorizeUrl({ state, codeChallenge }) {
|
|
66
|
+
{{/if}}
|
|
67
|
+
{{#if private}}
|
|
68
|
+
function buildAuthorizeUrl({ state }) {
|
|
69
|
+
{{/if}}
|
|
70
|
+
let authUrl = `${realmPath}/authorize?` +
|
|
64
71
|
`response_type=code` +
|
|
65
72
|
`&client_id=${encodeURIComponent(CLIENT_ID)}` +
|
|
66
73
|
`&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
|
|
67
74
|
`&state=${state}` +
|
|
68
75
|
`&scope=${encodeURIComponent(SCOPES)}`;
|
|
76
|
+
{{#if public}}
|
|
77
|
+
authUrl += `&code_challenge=${codeChallenge}&code_challenge_method=S256`;
|
|
78
|
+
{{/if}}
|
|
79
|
+
return authUrl;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Step 1: Redirect user to authorization page
|
|
83
|
+
router.get('/login', (req, res) => {
|
|
84
|
+
const crypto = require('crypto');
|
|
85
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
86
|
+
tokenStore.setState(state);
|
|
87
|
+
{{#if public}}
|
|
88
|
+
|
|
89
|
+
// PKCE (RFC 7636): create a high-entropy verifier, keep it server-side, and
|
|
90
|
+
// send only its SHA-256 (S256) hash on the authorize request. The verifier
|
|
91
|
+
// itself is presented only later, on the token exchange in /auth/callback.
|
|
92
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
93
|
+
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
94
|
+
tokenStore.setVerifier(codeVerifier);
|
|
95
|
+
|
|
96
|
+
res.redirect(buildAuthorizeUrl({ state, codeChallenge }));
|
|
97
|
+
{{/if}}
|
|
98
|
+
{{#if private}}
|
|
69
99
|
|
|
70
|
-
res.redirect(
|
|
100
|
+
res.redirect(buildAuthorizeUrl({ state }));
|
|
101
|
+
{{/if}}
|
|
71
102
|
});
|
|
72
103
|
|
|
73
104
|
// Step 2: Handle the OAuth callback
|
|
@@ -94,7 +125,14 @@ router.get('/callback', async (req, res) => {
|
|
|
94
125
|
new URLSearchParams({
|
|
95
126
|
grant_type: 'authorization_code',
|
|
96
127
|
client_id: CLIENT_ID,
|
|
128
|
+
{{#if private}}
|
|
97
129
|
client_secret: CLIENT_SECRET,
|
|
130
|
+
{{/if}}
|
|
131
|
+
{{#if public}}
|
|
132
|
+
// PKCE (RFC 7636): prove possession of the verifier generated in /login.
|
|
133
|
+
// Public clients hold no secret, so this replaces client_secret entirely.
|
|
134
|
+
code_verifier: tokenStore.getVerifier(),
|
|
135
|
+
{{/if}}
|
|
98
136
|
code,
|
|
99
137
|
redirect_uri: REDIRECT_URI,
|
|
100
138
|
}),
|
|
@@ -165,7 +203,9 @@ router.get('/refresh', async (req, res) => {
|
|
|
165
203
|
new URLSearchParams({
|
|
166
204
|
grant_type: 'refresh_token',
|
|
167
205
|
client_id: CLIENT_ID,
|
|
206
|
+
{{#if private}}
|
|
168
207
|
client_secret: CLIENT_SECRET,
|
|
208
|
+
{{/if}}
|
|
169
209
|
refresh_token: refreshToken,
|
|
170
210
|
}),
|
|
171
211
|
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
|
|
@@ -204,3 +244,5 @@ router.get('/refresh', async (req, res) => {
|
|
|
204
244
|
});
|
|
205
245
|
|
|
206
246
|
module.exports = router;
|
|
247
|
+
// Exposed so server.js renders the same authorize URL the /login redirect uses.
|
|
248
|
+
module.exports.buildAuthorizeUrl = buildAuthorizeUrl;
|
|
@@ -14,11 +14,21 @@ if (!fs.existsSync(envPath)) {
|
|
|
14
14
|
|
|
15
15
|
require('dotenv').config({ path: envPath });
|
|
16
16
|
|
|
17
|
+
{{#if private}}
|
|
17
18
|
if (!process.env.CLIENT_ID || !process.env.CLIENT_SECRET) {
|
|
18
19
|
console.error('\nError: CLIENT_ID and CLIENT_SECRET must be set in .env.local');
|
|
19
20
|
console.error('Get your credentials with: brevo app credentials --reveal-secret\n');
|
|
20
21
|
process.exit(1);
|
|
21
22
|
}
|
|
23
|
+
{{/if}}
|
|
24
|
+
{{#if public}}
|
|
25
|
+
// Public apps use PKCE (RFC 7636) — no client secret is involved.
|
|
26
|
+
if (!process.env.CLIENT_ID) {
|
|
27
|
+
console.error('\nError: CLIENT_ID must be set in .env.local');
|
|
28
|
+
console.error('Get your credentials with: brevo app credentials\n');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
{{/if}}
|
|
22
32
|
|
|
23
33
|
if (!process.env.OAUTH_BASE || process.env.OAUTH_BASE.startsWith('{{')) {
|
|
24
34
|
console.error('\nError: OAUTH_BASE is not set or contains a placeholder.');
|
|
@@ -33,24 +43,20 @@ if (!process.env.OAUTH_REALM || process.env.OAUTH_REALM.startsWith('{{')) {
|
|
|
33
43
|
|
|
34
44
|
const express = require('express');
|
|
35
45
|
const authRouter = require('./handler');
|
|
46
|
+
const { buildAuthorizeUrl } = authRouter;
|
|
36
47
|
|
|
37
48
|
const app = express();
|
|
38
49
|
const PORT = process.env.PORT || 3009;
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
+ `?response_type=code`
|
|
50
|
-
+ `&client_id=${encodeURIComponent(process.env.CLIENT_ID)}`
|
|
51
|
-
+ `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}`
|
|
52
|
-
+ `&state=[random]`
|
|
53
|
-
+ `&scope=${encodeURIComponent(SCOPES)}`;
|
|
51
|
+
// Build the displayed authorization URL with the same helper /auth/login uses,
|
|
52
|
+
// so what the landing page shows always matches what the redirect actually
|
|
53
|
+
// sends. Placeholders (`[random]` state) stand in for the per-request values.
|
|
54
|
+
{{#if private}}
|
|
55
|
+
const AUTH_URL = buildAuthorizeUrl({ state: '[random]' });
|
|
56
|
+
{{/if}}
|
|
57
|
+
{{#if public}}
|
|
58
|
+
const AUTH_URL = buildAuthorizeUrl({ state: '[random]', codeChallenge: '[generated-per-login]' });
|
|
59
|
+
{{/if}}
|
|
54
60
|
|
|
55
61
|
app.use('/auth', authRouter);
|
|
56
62
|
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
let tokens = {};
|
|
4
4
|
let csrfState = null;
|
|
5
|
+
{{#if public}}
|
|
6
|
+
let codeVerifier = null;
|
|
7
|
+
{{/if}}
|
|
5
8
|
|
|
6
9
|
module.exports = {
|
|
7
10
|
saveTokens({ access_token, refresh_token, expires_in }) {
|
|
@@ -31,9 +34,25 @@ module.exports = {
|
|
|
31
34
|
getState() {
|
|
32
35
|
return csrfState;
|
|
33
36
|
},
|
|
37
|
+
{{#if public}}
|
|
38
|
+
|
|
39
|
+
// PKCE (RFC 7636): stash the code_verifier between /auth/login and
|
|
40
|
+
// /auth/callback. Same process-local, non-session-isolated limitation as the
|
|
41
|
+
// token store above — replace with your database in production.
|
|
42
|
+
setVerifier(verifier) {
|
|
43
|
+
codeVerifier = verifier;
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
getVerifier() {
|
|
47
|
+
return codeVerifier;
|
|
48
|
+
},
|
|
49
|
+
{{/if}}
|
|
34
50
|
|
|
35
51
|
clear() {
|
|
36
52
|
tokens = {};
|
|
37
53
|
csrfState = null;
|
|
54
|
+
{{#if public}}
|
|
55
|
+
codeVerifier = null;
|
|
56
|
+
{{/if}}
|
|
38
57
|
},
|
|
39
58
|
};
|