@getbrevo/cli 2.0.2 → 2.1.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 (217) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/README.md +8 -5
  3. package/agent-context/AGENTS.md +59 -15
  4. package/agent-context/SKILL.md +70 -14
  5. package/dist/{templates → bin}/files/AGENTS.md.tmpl +11 -1
  6. package/dist/{templates → bin}/files/CLAUDE.md.tmpl +8 -0
  7. package/dist/{templates → bin}/files/README.md.tmpl +11 -14
  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 +660 -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/app-config.json.tmpl +0 -25
  208. package/dist/templates/index.d.ts +0 -36
  209. package/dist/templates/index.d.ts.map +0 -1
  210. package/dist/templates/index.js +0 -105
  211. package/dist/templates/index.js.map +0 -1
  212. package/dist/types.d.ts +0 -32
  213. package/dist/types.d.ts.map +0 -1
  214. package/dist/types.js +0 -3
  215. package/dist/types.js.map +0 -1
  216. /package/dist/{templates → bin}/files/gitignore.tmpl +0 -0
  217. /package/dist/{templates → bin}/files/src/oauth/package.json.tmpl +0 -0
@@ -33,11 +33,21 @@ Implements the authorization code flow to obtain access tokens for the Brevo API
33
33
  - Other grant types supported by the Brevo OAuth server but not wired into this scaffold: `client_credentials`
34
34
 
35
35
  ## OAuth flow
36
+ {{#if private}}
36
37
  1. `/auth/login` — generates CSRF state, redirects user to `{OAUTH_BASE}/realms/{realm}/oauth/authorize`
37
38
  2. User authorizes -> OAuth server redirects to `/auth/callback` with an auth code
38
- 3. Server validates state, exchanges code for access + refresh tokens via `POST /realms/{realm}/oauth/token`
39
+ 3. Server validates state, exchanges code for access + refresh tokens via `POST /realms/{realm}/oauth/token` (authenticated with `client_secret`)
39
40
  4. Verifies connection via `GET /realms/{realm}/oauth/userinfo`
40
41
  5. `/auth/refresh` — refreshes tokens using `refresh_token` grant
42
+ {{/if}}
43
+ {{#if public}}
44
+ This is a **public app** — the flow uses **PKCE (RFC 7636)** and sends **no `client_secret`**:
45
+ 1. `/auth/login` — generates CSRF state + a PKCE `code_verifier`, then redirects user to `{OAUTH_BASE}/realms/{realm}/oauth/authorize` with `code_challenge` (S256) + `code_challenge_method=S256`
46
+ 2. User authorizes -> OAuth server redirects to `/auth/callback` with an auth code
47
+ 3. Server validates state, exchanges code + `code_verifier` (no `client_secret`) for access + refresh tokens via `POST /realms/{realm}/oauth/token`
48
+ 4. Verifies connection via `GET /realms/{realm}/oauth/userinfo`
49
+ 5. `/auth/refresh` — refreshes tokens using `refresh_token` grant (no `client_secret`)
50
+ {{/if}}
41
51
 
42
52
  ## Development
43
53
  ```bash
@@ -47,9 +47,17 @@ brevo app start oauth # port is read from app-config.json; override with
47
47
  | `GET /realms/{realm}/.well-known/openid-configuration` | OIDC discovery |
48
48
 
49
49
  ## Key flows
50
+ {{#if private}}
50
51
  - `/auth/login` — generates random CSRF state, redirects to OAuth authorize endpoint
51
52
  - `/auth/callback` — validates state, POSTs code to `/realms/{realm}/oauth/token`, stores tokens
52
53
  - `/auth/refresh` — refreshes tokens using `refresh_token` grant
54
+ {{/if}}
55
+ {{#if public}}
56
+ This is a **public app**, so the flow uses **PKCE (RFC 7636)** and no client secret:
57
+ - `/auth/login` — generates random CSRF state **and a PKCE `code_verifier`**, then redirects to the OAuth authorize endpoint sending `code_challenge` (S256 hash of the verifier) + `code_challenge_method=S256`. The verifier is stored server-side and never sent to the browser.
58
+ - `/auth/callback` — validates state, POSTs code **plus the `code_verifier`** (no `client_secret`) to `/realms/{realm}/oauth/token`, stores tokens
59
+ - `/auth/refresh` — refreshes tokens using the `refresh_token` grant (no `client_secret`)
60
+ {{/if}}
53
61
 
54
62
  ## Next steps
55
63
  - Replace the in-memory token store in `src/oauth/token-store.js` with a real database (the global singleton is process-local so data is lost on restart, and it is not session-isolated, so multiple users/sessions/tabs can read or overwrite the same in-memory tokens)
@@ -55,28 +55,25 @@ Brevo OAuth integration for testing and implementing app authorization flows, sc
55
55
 
56
56
  ## Updating your app
57
57
 
58
- Push changes to Brevo directly from the CLI:
58
+ `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:
59
59
 
60
60
  ```bash
61
- brevo app update --name "New Name"
62
- brevo app update --redirect-url https://myapp.com/callback
63
- brevo app update --name "New Name" --redirect-url https://myapp.com/callback
64
- brevo app update --logo-uri https://example.com/logo.png
61
+ brevo app upload
65
62
  ```
66
63
 
67
- `--redirect-url` is repeatable and **appends** to existing URLs.
64
+ For example, to rename the app or add a redirect URL, edit `appName` or `auth.redirectUris` in `app-config.json`:
68
65
 
69
- To avoid unintentionally accumulating stale redirect URLs over time:
66
+ ```bash
67
+ # 1. Edit app-config.json (e.g. change "appName", or append to "auth.redirectUris")
68
+ # 2. Push the change
69
+ brevo app upload
70
+ ```
70
71
 
71
- - Periodically review your redirect list and remove obsolete entries in `app-config.json`.
72
- - Run `brevo app update` **without flags** to replace the remote configuration with the local file.
73
- - When flags are provided, the local `app-config.json` is updated automatically after a successful API call.
72
+ `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
73
 
75
- Outside this project directory, target an app by ID:
74
+ 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
75
 
77
- ```bash
78
- brevo app update --app-id {{APP_ID}} --name "New Name"
79
- ```
76
+ `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).
80
77
 
81
78
  ## Docs
82
79
 
@@ -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
  };