@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.
Files changed (220) hide show
  1. package/CHANGELOG.md +237 -0
  2. package/README.md +32 -6
  3. package/agent-context/AGENTS.md +62 -15
  4. package/agent-context/SKILL.md +98 -14
  5. package/dist/bin/files/AGENTS.md.tmpl +119 -0
  6. package/dist/bin/files/CLAUDE.md.tmpl +139 -0
  7. package/dist/bin/files/README.md.tmpl +184 -0
  8. package/dist/bin/files/app-config.json.tmpl +16 -0
  9. package/dist/{templates → bin}/files/src/oauth/.env.example.tmpl +8 -0
  10. package/dist/{templates → bin}/files/src/oauth/.env.local.tmpl +2 -0
  11. package/dist/{templates → bin}/files/src/oauth/handler.js.tmpl +49 -7
  12. package/dist/{templates → bin}/files/src/oauth/server.js.tmpl +20 -14
  13. package/dist/{templates → bin}/files/src/oauth/token-store.js.tmpl +19 -0
  14. package/dist/bin/index.js +680 -235
  15. package/dist/bin/index.js.map +7 -1
  16. package/package.json +6 -4
  17. package/dist/api/client.d.ts +0 -25
  18. package/dist/api/client.d.ts.map +0 -1
  19. package/dist/api/client.js +0 -205
  20. package/dist/api/client.js.map +0 -1
  21. package/dist/bin/index.d.ts +0 -3
  22. package/dist/bin/index.d.ts.map +0 -1
  23. package/dist/commands/app/create.d.ts +0 -8
  24. package/dist/commands/app/create.d.ts.map +0 -1
  25. package/dist/commands/app/create.js +0 -325
  26. package/dist/commands/app/create.js.map +0 -1
  27. package/dist/commands/app/credentials.d.ts +0 -6
  28. package/dist/commands/app/credentials.d.ts.map +0 -1
  29. package/dist/commands/app/credentials.js +0 -114
  30. package/dist/commands/app/credentials.js.map +0 -1
  31. package/dist/commands/app/delete.d.ts +0 -6
  32. package/dist/commands/app/delete.d.ts.map +0 -1
  33. package/dist/commands/app/delete.js +0 -167
  34. package/dist/commands/app/delete.js.map +0 -1
  35. package/dist/commands/app/list.d.ts +0 -4
  36. package/dist/commands/app/list.d.ts.map +0 -1
  37. package/dist/commands/app/list.js +0 -71
  38. package/dist/commands/app/list.js.map +0 -1
  39. package/dist/commands/app/scaffold.d.ts +0 -5
  40. package/dist/commands/app/scaffold.d.ts.map +0 -1
  41. package/dist/commands/app/scaffold.js +0 -213
  42. package/dist/commands/app/scaffold.js.map +0 -1
  43. package/dist/commands/app/scopes.d.ts +0 -2
  44. package/dist/commands/app/scopes.d.ts.map +0 -1
  45. package/dist/commands/app/scopes.js +0 -81
  46. package/dist/commands/app/scopes.js.map +0 -1
  47. package/dist/commands/app/start.d.ts +0 -5
  48. package/dist/commands/app/start.d.ts.map +0 -1
  49. package/dist/commands/app/start.js +0 -220
  50. package/dist/commands/app/start.js.map +0 -1
  51. package/dist/commands/app/update.d.ts +0 -2
  52. package/dist/commands/app/update.d.ts.map +0 -1
  53. package/dist/commands/app/update.js +0 -444
  54. package/dist/commands/app/update.js.map +0 -1
  55. package/dist/commands/definitions.d.ts +0 -5
  56. package/dist/commands/definitions.d.ts.map +0 -1
  57. package/dist/commands/definitions.js +0 -262
  58. package/dist/commands/definitions.js.map +0 -1
  59. package/dist/commands/init.d.ts +0 -2
  60. package/dist/commands/init.d.ts.map +0 -1
  61. package/dist/commands/init.js +0 -138
  62. package/dist/commands/init.js.map +0 -1
  63. package/dist/commands/login.d.ts +0 -6
  64. package/dist/commands/login.d.ts.map +0 -1
  65. package/dist/commands/login.js +0 -251
  66. package/dist/commands/login.js.map +0 -1
  67. package/dist/commands/logout.d.ts +0 -5
  68. package/dist/commands/logout.d.ts.map +0 -1
  69. package/dist/commands/logout.js +0 -54
  70. package/dist/commands/logout.js.map +0 -1
  71. package/dist/commands/skill/install.d.ts +0 -4
  72. package/dist/commands/skill/install.d.ts.map +0 -1
  73. package/dist/commands/skill/install.js +0 -29
  74. package/dist/commands/skill/install.js.map +0 -1
  75. package/dist/commands/skill/uninstall.d.ts +0 -4
  76. package/dist/commands/skill/uninstall.d.ts.map +0 -1
  77. package/dist/commands/skill/uninstall.js +0 -23
  78. package/dist/commands/skill/uninstall.js.map +0 -1
  79. package/dist/commands/whoami.d.ts +0 -4
  80. package/dist/commands/whoami.d.ts.map +0 -1
  81. package/dist/commands/whoami.js +0 -85
  82. package/dist/commands/whoami.js.map +0 -1
  83. package/dist/container.d.ts +0 -7
  84. package/dist/container.d.ts.map +0 -1
  85. package/dist/container.js +0 -28
  86. package/dist/container.js.map +0 -1
  87. package/dist/lang/en.d.ts +0 -162
  88. package/dist/lang/en.d.ts.map +0 -1
  89. package/dist/lang/en.js +0 -189
  90. package/dist/lang/en.js.map +0 -1
  91. package/dist/lib/abort.d.ts +0 -3
  92. package/dist/lib/abort.d.ts.map +0 -1
  93. package/dist/lib/abort.js +0 -30
  94. package/dist/lib/abort.js.map +0 -1
  95. package/dist/lib/auth-guard.d.ts +0 -13
  96. package/dist/lib/auth-guard.d.ts.map +0 -1
  97. package/dist/lib/auth-guard.js +0 -60
  98. package/dist/lib/auth-guard.js.map +0 -1
  99. package/dist/lib/browser.d.ts +0 -2
  100. package/dist/lib/browser.d.ts.map +0 -1
  101. package/dist/lib/browser.js +0 -16
  102. package/dist/lib/browser.js.map +0 -1
  103. package/dist/lib/cli-version.d.ts +0 -2
  104. package/dist/lib/cli-version.d.ts.map +0 -1
  105. package/dist/lib/cli-version.js +0 -59
  106. package/dist/lib/cli-version.js.map +0 -1
  107. package/dist/lib/command-handler.d.ts +0 -10
  108. package/dist/lib/command-handler.d.ts.map +0 -1
  109. package/dist/lib/command-handler.js +0 -16
  110. package/dist/lib/command-handler.js.map +0 -1
  111. package/dist/lib/command-registry.d.ts +0 -28
  112. package/dist/lib/command-registry.d.ts.map +0 -1
  113. package/dist/lib/command-registry.js +0 -54
  114. package/dist/lib/command-registry.js.map +0 -1
  115. package/dist/lib/config.d.ts +0 -83
  116. package/dist/lib/config.d.ts.map +0 -1
  117. package/dist/lib/config.js +0 -430
  118. package/dist/lib/config.js.map +0 -1
  119. package/dist/lib/constants.d.ts +0 -48
  120. package/dist/lib/constants.d.ts.map +0 -1
  121. package/dist/lib/constants.js +0 -121
  122. package/dist/lib/constants.js.map +0 -1
  123. package/dist/lib/errors.d.ts +0 -30
  124. package/dist/lib/errors.d.ts.map +0 -1
  125. package/dist/lib/errors.js +0 -66
  126. package/dist/lib/errors.js.map +0 -1
  127. package/dist/lib/exit-codes.d.ts +0 -9
  128. package/dist/lib/exit-codes.d.ts.map +0 -1
  129. package/dist/lib/exit-codes.js +0 -12
  130. package/dist/lib/exit-codes.js.map +0 -1
  131. package/dist/lib/hidden-input.d.ts +0 -2
  132. package/dist/lib/hidden-input.d.ts.map +0 -1
  133. package/dist/lib/hidden-input.js +0 -78
  134. package/dist/lib/hidden-input.js.map +0 -1
  135. package/dist/lib/json-output.d.ts +0 -6
  136. package/dist/lib/json-output.d.ts.map +0 -1
  137. package/dist/lib/json-output.js +0 -11
  138. package/dist/lib/json-output.js.map +0 -1
  139. package/dist/lib/logger.d.ts +0 -11
  140. package/dist/lib/logger.d.ts.map +0 -1
  141. package/dist/lib/logger.js +0 -110
  142. package/dist/lib/logger.js.map +0 -1
  143. package/dist/lib/oauth-freshness.d.ts +0 -30
  144. package/dist/lib/oauth-freshness.d.ts.map +0 -1
  145. package/dist/lib/oauth-freshness.js +0 -61
  146. package/dist/lib/oauth-freshness.js.map +0 -1
  147. package/dist/lib/port.d.ts +0 -25
  148. package/dist/lib/port.d.ts.map +0 -1
  149. package/dist/lib/port.js +0 -103
  150. package/dist/lib/port.js.map +0 -1
  151. package/dist/lib/telemetry.d.ts +0 -9
  152. package/dist/lib/telemetry.d.ts.map +0 -1
  153. package/dist/lib/telemetry.js +0 -57
  154. package/dist/lib/telemetry.js.map +0 -1
  155. package/dist/lib/ui.d.ts +0 -14
  156. package/dist/lib/ui.d.ts.map +0 -1
  157. package/dist/lib/ui.js +0 -75
  158. package/dist/lib/ui.js.map +0 -1
  159. package/dist/lib/update-notifier.d.ts +0 -37
  160. package/dist/lib/update-notifier.d.ts.map +0 -1
  161. package/dist/lib/update-notifier.js +0 -298
  162. package/dist/lib/update-notifier.js.map +0 -1
  163. package/dist/lib/validators.d.ts +0 -55
  164. package/dist/lib/validators.d.ts.map +0 -1
  165. package/dist/lib/validators.js +0 -166
  166. package/dist/lib/validators.js.map +0 -1
  167. package/dist/services/account.d.ts +0 -8
  168. package/dist/services/account.d.ts.map +0 -1
  169. package/dist/services/account.js +0 -15
  170. package/dist/services/account.js.map +0 -1
  171. package/dist/services/app.d.ts +0 -38
  172. package/dist/services/app.d.ts.map +0 -1
  173. package/dist/services/app.js +0 -145
  174. package/dist/services/app.js.map +0 -1
  175. package/dist/services/browser-auth.d.ts +0 -15
  176. package/dist/services/browser-auth.d.ts.map +0 -1
  177. package/dist/services/browser-auth.js +0 -188
  178. package/dist/services/browser-auth.js.map +0 -1
  179. package/dist/services/normalize-app-id.d.ts +0 -12
  180. package/dist/services/normalize-app-id.d.ts.map +0 -1
  181. package/dist/services/normalize-app-id.js +0 -38
  182. package/dist/services/normalize-app-id.js.map +0 -1
  183. package/dist/services/oauth-metadata.d.ts +0 -7
  184. package/dist/services/oauth-metadata.d.ts.map +0 -1
  185. package/dist/services/oauth-metadata.js +0 -43
  186. package/dist/services/oauth-metadata.js.map +0 -1
  187. package/dist/services/oauth-refresh.d.ts +0 -14
  188. package/dist/services/oauth-refresh.d.ts.map +0 -1
  189. package/dist/services/oauth-refresh.js +0 -70
  190. package/dist/services/oauth-refresh.js.map +0 -1
  191. package/dist/services/scopes-html.d.ts +0 -3
  192. package/dist/services/scopes-html.d.ts.map +0 -1
  193. package/dist/services/scopes-html.js +0 -516
  194. package/dist/services/scopes-html.js.map +0 -1
  195. package/dist/services/scopes-web.d.ts +0 -12
  196. package/dist/services/scopes-web.d.ts.map +0 -1
  197. package/dist/services/scopes-web.js +0 -95
  198. package/dist/services/scopes-web.js.map +0 -1
  199. package/dist/services/skill.d.ts +0 -48
  200. package/dist/services/skill.d.ts.map +0 -1
  201. package/dist/services/skill.js +0 -223
  202. package/dist/services/skill.js.map +0 -1
  203. package/dist/skills/index.d.ts +0 -47
  204. package/dist/skills/index.d.ts.map +0 -1
  205. package/dist/skills/index.js +0 -61
  206. package/dist/skills/index.js.map +0 -1
  207. package/dist/templates/files/AGENTS.md.tmpl +0 -52
  208. package/dist/templates/files/CLAUDE.md.tmpl +0 -59
  209. package/dist/templates/files/README.md.tmpl +0 -85
  210. package/dist/templates/files/app-config.json.tmpl +0 -25
  211. package/dist/templates/index.d.ts +0 -36
  212. package/dist/templates/index.d.ts.map +0 -1
  213. package/dist/templates/index.js +0 -105
  214. package/dist/templates/index.js.map +0 -1
  215. package/dist/types.d.ts +0 -32
  216. package/dist/types.d.ts.map +0 -1
  217. package/dist/types.js +0 -3
  218. package/dist/types.js.map +0 -1
  219. /package/dist/{templates → bin}/files/gitignore.tmpl +0 -0
  220. /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
@@ -2,7 +2,9 @@
2
2
  # Generated by: brevo app scaffold
3
3
 
4
4
  CLIENT_ID={{CLIENT_ID}}
5
+ {{#if private}}
5
6
  CLIENT_SECRET={{CLIENT_SECRET}}
7
+ {{/if}}
6
8
  REDIRECT_URI={{REDIRECT_URI}}
7
9
 
8
10
  # 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
- // Step 1: Redirect user to authorization page
59
- router.get('/login', (req, res) => {
60
- const state = require('crypto').randomBytes(16).toString('hex');
61
- tokenStore.setState(state);
62
-
63
- const authUrl = `${realmPath}/authorize?` +
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(authUrl);
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
- const REDIRECT_URI = process.env.REDIRECT_URI || '{{REDIRECT_URI}}';
41
- let SCOPES = 'all';
42
- try {
43
- const appConfig = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'app-config.json'), 'utf-8'));
44
- if (appConfig.auth && appConfig.auth.scopes) SCOPES = appConfig.auth.scopes.join(' ');
45
- } catch {
46
- console.warn('Warning: Could not read app-config.json — defaulting scopes to "all".');
47
- }
48
- const AUTH_URL = `${process.env.OAUTH_BASE}/realms/${process.env.OAUTH_REALM}/oauth/authorize`
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
  };