@getbrevo/cli 0.0.1-alpha.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.
Files changed (171) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +182 -0
  4. package/dist/api/client.d.ts +23 -0
  5. package/dist/api/client.d.ts.map +1 -0
  6. package/dist/api/client.js +186 -0
  7. package/dist/api/client.js.map +1 -0
  8. package/dist/bin/index.d.ts +3 -0
  9. package/dist/bin/index.d.ts.map +1 -0
  10. package/dist/bin/index.js +179 -0
  11. package/dist/bin/index.js.map +1 -0
  12. package/dist/commands/app/create.d.ts +7 -0
  13. package/dist/commands/app/create.d.ts.map +1 -0
  14. package/dist/commands/app/create.js +261 -0
  15. package/dist/commands/app/create.js.map +1 -0
  16. package/dist/commands/app/credentials.d.ts +6 -0
  17. package/dist/commands/app/credentials.d.ts.map +1 -0
  18. package/dist/commands/app/credentials.js +106 -0
  19. package/dist/commands/app/credentials.js.map +1 -0
  20. package/dist/commands/app/delete.d.ts +6 -0
  21. package/dist/commands/app/delete.d.ts.map +1 -0
  22. package/dist/commands/app/delete.js +146 -0
  23. package/dist/commands/app/delete.js.map +1 -0
  24. package/dist/commands/app/list.d.ts +4 -0
  25. package/dist/commands/app/list.d.ts.map +1 -0
  26. package/dist/commands/app/list.js +46 -0
  27. package/dist/commands/app/list.js.map +1 -0
  28. package/dist/commands/app/scaffold.d.ts +5 -0
  29. package/dist/commands/app/scaffold.d.ts.map +1 -0
  30. package/dist/commands/app/scaffold.js +199 -0
  31. package/dist/commands/app/scaffold.js.map +1 -0
  32. package/dist/commands/app/start.d.ts +5 -0
  33. package/dist/commands/app/start.d.ts.map +1 -0
  34. package/dist/commands/app/start.js +138 -0
  35. package/dist/commands/app/start.js.map +1 -0
  36. package/dist/commands/app/test.d.ts +6 -0
  37. package/dist/commands/app/test.d.ts.map +1 -0
  38. package/dist/commands/app/test.js +495 -0
  39. package/dist/commands/app/test.js.map +1 -0
  40. package/dist/commands/app/update.d.ts +2 -0
  41. package/dist/commands/app/update.d.ts.map +1 -0
  42. package/dist/commands/app/update.js +324 -0
  43. package/dist/commands/app/update.js.map +1 -0
  44. package/dist/commands/definitions.d.ts +4 -0
  45. package/dist/commands/definitions.d.ts.map +1 -0
  46. package/dist/commands/definitions.js +197 -0
  47. package/dist/commands/definitions.js.map +1 -0
  48. package/dist/commands/init.d.ts +2 -0
  49. package/dist/commands/init.d.ts.map +1 -0
  50. package/dist/commands/init.js +90 -0
  51. package/dist/commands/init.js.map +1 -0
  52. package/dist/commands/login.d.ts +6 -0
  53. package/dist/commands/login.d.ts.map +1 -0
  54. package/dist/commands/login.js +219 -0
  55. package/dist/commands/login.js.map +1 -0
  56. package/dist/commands/logout.d.ts +5 -0
  57. package/dist/commands/logout.d.ts.map +1 -0
  58. package/dist/commands/logout.js +54 -0
  59. package/dist/commands/logout.js.map +1 -0
  60. package/dist/commands/whoami.d.ts +4 -0
  61. package/dist/commands/whoami.d.ts.map +1 -0
  62. package/dist/commands/whoami.js +74 -0
  63. package/dist/commands/whoami.js.map +1 -0
  64. package/dist/container.d.ts +7 -0
  65. package/dist/container.d.ts.map +1 -0
  66. package/dist/container.js +28 -0
  67. package/dist/container.js.map +1 -0
  68. package/dist/lang/en.d.ts +110 -0
  69. package/dist/lang/en.d.ts.map +1 -0
  70. package/dist/lang/en.js +133 -0
  71. package/dist/lang/en.js.map +1 -0
  72. package/dist/lib/abort.d.ts +3 -0
  73. package/dist/lib/abort.d.ts.map +1 -0
  74. package/dist/lib/abort.js +30 -0
  75. package/dist/lib/abort.js.map +1 -0
  76. package/dist/lib/auth-guard.d.ts +3 -0
  77. package/dist/lib/auth-guard.d.ts.map +1 -0
  78. package/dist/lib/auth-guard.js +25 -0
  79. package/dist/lib/auth-guard.js.map +1 -0
  80. package/dist/lib/browser.d.ts +2 -0
  81. package/dist/lib/browser.d.ts.map +1 -0
  82. package/dist/lib/browser.js +16 -0
  83. package/dist/lib/browser.js.map +1 -0
  84. package/dist/lib/command-handler.d.ts +10 -0
  85. package/dist/lib/command-handler.d.ts.map +1 -0
  86. package/dist/lib/command-handler.js +16 -0
  87. package/dist/lib/command-handler.js.map +1 -0
  88. package/dist/lib/command-registry.d.ts +28 -0
  89. package/dist/lib/command-registry.d.ts.map +1 -0
  90. package/dist/lib/command-registry.js +54 -0
  91. package/dist/lib/command-registry.js.map +1 -0
  92. package/dist/lib/config.d.ts +76 -0
  93. package/dist/lib/config.d.ts.map +1 -0
  94. package/dist/lib/config.js +314 -0
  95. package/dist/lib/config.js.map +1 -0
  96. package/dist/lib/constants.d.ts +35 -0
  97. package/dist/lib/constants.d.ts.map +1 -0
  98. package/dist/lib/constants.js +97 -0
  99. package/dist/lib/constants.js.map +1 -0
  100. package/dist/lib/errors.d.ts +30 -0
  101. package/dist/lib/errors.d.ts.map +1 -0
  102. package/dist/lib/errors.js +66 -0
  103. package/dist/lib/errors.js.map +1 -0
  104. package/dist/lib/exit-codes.d.ts +9 -0
  105. package/dist/lib/exit-codes.d.ts.map +1 -0
  106. package/dist/lib/exit-codes.js +12 -0
  107. package/dist/lib/exit-codes.js.map +1 -0
  108. package/dist/lib/hidden-input.d.ts +2 -0
  109. package/dist/lib/hidden-input.d.ts.map +1 -0
  110. package/dist/lib/hidden-input.js +78 -0
  111. package/dist/lib/hidden-input.js.map +1 -0
  112. package/dist/lib/json-output.d.ts +6 -0
  113. package/dist/lib/json-output.d.ts.map +1 -0
  114. package/dist/lib/json-output.js +11 -0
  115. package/dist/lib/json-output.js.map +1 -0
  116. package/dist/lib/logger.d.ts +11 -0
  117. package/dist/lib/logger.d.ts.map +1 -0
  118. package/dist/lib/logger.js +87 -0
  119. package/dist/lib/logger.js.map +1 -0
  120. package/dist/lib/port.d.ts +12 -0
  121. package/dist/lib/port.d.ts.map +1 -0
  122. package/dist/lib/port.js +79 -0
  123. package/dist/lib/port.js.map +1 -0
  124. package/dist/lib/ui.d.ts +14 -0
  125. package/dist/lib/ui.d.ts.map +1 -0
  126. package/dist/lib/ui.js +75 -0
  127. package/dist/lib/ui.js.map +1 -0
  128. package/dist/lib/validators.d.ts +32 -0
  129. package/dist/lib/validators.d.ts.map +1 -0
  130. package/dist/lib/validators.js +96 -0
  131. package/dist/lib/validators.js.map +1 -0
  132. package/dist/services/account.d.ts +8 -0
  133. package/dist/services/account.d.ts.map +1 -0
  134. package/dist/services/account.js +15 -0
  135. package/dist/services/account.js.map +1 -0
  136. package/dist/services/app.d.ts +35 -0
  137. package/dist/services/app.d.ts.map +1 -0
  138. package/dist/services/app.js +117 -0
  139. package/dist/services/app.js.map +1 -0
  140. package/dist/services/browser-auth.d.ts +15 -0
  141. package/dist/services/browser-auth.d.ts.map +1 -0
  142. package/dist/services/browser-auth.js +187 -0
  143. package/dist/services/browser-auth.js.map +1 -0
  144. package/dist/services/normalize-app-id.d.ts +12 -0
  145. package/dist/services/normalize-app-id.d.ts.map +1 -0
  146. package/dist/services/normalize-app-id.js +38 -0
  147. package/dist/services/normalize-app-id.js.map +1 -0
  148. package/dist/services/oauth-refresh.d.ts +14 -0
  149. package/dist/services/oauth-refresh.d.ts.map +1 -0
  150. package/dist/services/oauth-refresh.js +70 -0
  151. package/dist/services/oauth-refresh.js.map +1 -0
  152. package/dist/templates/files/AGENTS.md.tmpl +41 -0
  153. package/dist/templates/files/CLAUDE.md.tmpl +50 -0
  154. package/dist/templates/files/README.md.tmpl +77 -0
  155. package/dist/templates/files/app-config.json.tmpl +23 -0
  156. package/dist/templates/files/gitignore.tmpl +9 -0
  157. package/dist/templates/files/src/oauth/.env.example.tmpl +13 -0
  158. package/dist/templates/files/src/oauth/.env.local.tmpl +10 -0
  159. package/dist/templates/files/src/oauth/handler.js.tmpl +203 -0
  160. package/dist/templates/files/src/oauth/package.json.tmpl +15 -0
  161. package/dist/templates/files/src/oauth/server.js.tmpl +226 -0
  162. package/dist/templates/files/src/oauth/token-store.js.tmpl +39 -0
  163. package/dist/templates/index.d.ts +36 -0
  164. package/dist/templates/index.d.ts.map +1 -0
  165. package/dist/templates/index.js +105 -0
  166. package/dist/templates/index.js.map +1 -0
  167. package/dist/types.d.ts +30 -0
  168. package/dist/types.d.ts.map +1 -0
  169. package/dist/types.js +3 -0
  170. package/dist/types.js.map +1 -0
  171. package/package.json +81 -0
@@ -0,0 +1,77 @@
1
+ # {{APP_NAME}}
2
+
3
+ Brevo OAuth integration scaffolded with the Brevo CLI.
4
+
5
+ > **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.
6
+
7
+ ## Setup
8
+
9
+ 1. Install dependencies for the oauth feature:
10
+
11
+ ```bash
12
+ yarn --cwd src/oauth install
13
+ # or: npm --prefix src/oauth install
14
+ ```
15
+
16
+ 2. Start the local OAuth test server (the CLI reads `app-config.json` for port and redirect URLs):
17
+
18
+ ```bash
19
+ brevo app start oauth
20
+ # or choose a port: brevo app start oauth --port 3000
21
+ ```
22
+
23
+ 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.
24
+
25
+ ## Project structure
26
+
27
+ ```
28
+ .
29
+ ├── app-config.json App config — ID, scopes, redirect URLs
30
+ ├── .gitignore Ignores .env.local and build artefacts
31
+ ├── AGENTS.md AI agent guidance
32
+ ├── CLAUDE.md Claude Code guidance
33
+ ├── README.md (this file)
34
+ └── src/
35
+ └── oauth/ Self-contained OAuth test feature
36
+ ├── package.json Dependencies & `yarn start` script
37
+ ├── server.js Test server entry point
38
+ ├── handler.js OAuth routes (/auth/login, /auth/callback)
39
+ ├── token-store.js In-memory token management
40
+ ├── .env.example Credential template (committed)
41
+ └── .env.local Local credentials (gitignored, chmod 600)
42
+ ```
43
+
44
+ ## Credentials
45
+
46
+ - Client ID and secret are written to `src/oauth/.env.local` during scaffold (file permissions `600`).
47
+ - Re-fetch them any time with `brevo app credentials --app-id {{APP_ID}} --reveal-secret`.
48
+ - `.env.local` is in `.gitignore` — **never commit it**.
49
+
50
+ ## Security
51
+
52
+ - Never commit `src/oauth/.env.local` — it contains your client secret.
53
+ - The test server uses a random `state` parameter for CSRF protection and binds to localhost only.
54
+ - Access tokens are kept in process memory; restarting the server clears them.
55
+
56
+ ## Updating your app
57
+
58
+ Push changes to Brevo directly from the CLI:
59
+
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
+ ```
65
+
66
+ `--redirect-url` is repeatable and **appends** to existing URLs. Running `brevo app update` without flags pushes the full `app-config.json`. When flags are provided, the local `app-config.json` is updated automatically after a successful API call.
67
+
68
+ Outside this project directory, target an app by ID:
69
+
70
+ ```bash
71
+ brevo app update --app-id {{APP_ID}} --name "New Name"
72
+ ```
73
+
74
+ ## Docs
75
+
76
+ - [Brevo API Documentation](https://developers.brevo.com)
77
+ - [Brevo CLI](https://github.com/getbrevo/brevo-cli)
@@ -0,0 +1,23 @@
1
+ {
2
+ "appId": "{{APP_ID}}",
3
+ "appName": "{{APP_NAME}}",
4
+ "auth": {
5
+ "type": "private",
6
+ "scopes": {{SCOPES_JSON}},
7
+ "redirectUrls": {{REDIRECT_URLS_JSON}}
8
+ },
9
+ "distribution": "private",
10
+ "permittedUrls": {
11
+ "fetch": [],
12
+ "img": [],
13
+ "iframe": [],
14
+ "js": [],
15
+ "css": []
16
+ },
17
+ "support": {
18
+ "supportEmail": "",
19
+ "documentationUrl": "",
20
+ "supportUrl": "",
21
+ "supportPhone": ""
22
+ }
23
+ }
@@ -0,0 +1,9 @@
1
+ # Local credentials — never commit
2
+ src/oauth/.env.local
3
+
4
+ # Dependencies
5
+ node_modules/
6
+
7
+ # Logs
8
+ *.log
9
+ npm-debug.log*
@@ -0,0 +1,13 @@
1
+ # Brevo OAuth Credentials — copy this file to .env.local and fill in your values
2
+ # Get CLIENT_ID and CLIENT_SECRET: brevo app credentials --reveal-secret
3
+
4
+ CLIENT_ID=your_client_id_here
5
+ CLIENT_SECRET=your_client_secret_here
6
+ REDIRECT_URI={{REDIRECT_URI}}
7
+
8
+ # OAuth server
9
+ OAUTH_BASE={{OAUTH_BASE}}
10
+ OAUTH_REALM={{OAUTH_REALM}}
11
+
12
+ # Optional
13
+ # PORT=3009
@@ -0,0 +1,10 @@
1
+ # Local credentials — DO NOT COMMIT
2
+ # Generated by: brevo app scaffold
3
+
4
+ CLIENT_ID={{CLIENT_ID}}
5
+ CLIENT_SECRET={{CLIENT_SECRET}}
6
+ REDIRECT_URI={{REDIRECT_URI}}
7
+
8
+ # OAuth server
9
+ OAUTH_BASE={{OAUTH_BASE}}
10
+ OAUTH_REALM={{OAUTH_REALM}}
@@ -0,0 +1,203 @@
1
+ // Test handler — for local OAuth testing only. Not for production use.
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const express = require('express');
5
+ const axios = require('axios');
6
+ const tokenStore = require('./token-store');
7
+
8
+ const router = express.Router();
9
+
10
+ // Sanitise untrusted values before embedding in HTML to prevent XSS
11
+ function escapeHtml(str) {
12
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
13
+ }
14
+
15
+ // Shared styles for token display pages (callback + refresh)
16
+ const PAGE_STYLES =
17
+ '<style>' +
18
+ '*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}' +
19
+ 'body{font-family:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:rgb(250,245,227);min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 16px}' +
20
+ '.page-card{background:#fff;border:1px solid #e3e3e3;border-radius:16px;padding:40px;width:100%;max-width:560px;text-align:center}' +
21
+ '.page-card h2{color:#1b1b1b;font-size:22px;margin-bottom:8px}' +
22
+ '.page-card .subtitle{color:#696969;font-size:14px;margin-bottom:24px}' +
23
+ 'a{color:#1b1b1b}' +
24
+ '.token-wrapper{position:relative;margin-top:6px}' +
25
+ '.token-value{display:block;padding:10px;background:#fff;border:1px solid #cfcfcf;border-radius:8px;font-family:monospace;font-size:13px;filter:blur(5px);user-select:none;cursor:pointer;transition:filter .2s}' +
26
+ '.token-value.revealed{filter:none;user-select:text;cursor:text}' +
27
+ '.reveal-btn{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#1b1b1b;color:#fff;border:none;padding:8px 16px;border-radius:16px;font-size:13px;font-weight:600;cursor:pointer}' +
28
+ '.reveal-btn.hidden{display:none}' +
29
+ '.request-details{text-align:left;margin:16px 0;background:#f5f5f5;border:1px solid #e3e3e3;padding:16px 20px;border-radius:16px;font-size:13px}' +
30
+ '.request-details strong{color:#1b1b1b}' +
31
+ '.request-details dt{font-weight:600;margin-top:8px;color:#1b1b1b}' +
32
+ '.request-details dd{margin:2px 0 0 0;font-family:monospace;word-break:break-all;color:#696969}' +
33
+ '.token-section{text-align:left;margin:16px 0;background:#f5f5f5;border:1px solid #e3e3e3;padding:20px;border-radius:16px;word-break:break-all}' +
34
+ '.footer-text{color:#858585;font-size:12px;margin-top:16px}' +
35
+ '</style>';
36
+
37
+ const REVEAL_SCRIPT =
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
+
40
+ const CLIENT_ID = process.env.CLIENT_ID;
41
+ const CLIENT_SECRET = process.env.CLIENT_SECRET;
42
+ const REDIRECT_URI = process.env.REDIRECT_URI || '{{REDIRECT_URI}}';
43
+ const OAUTH_BASE = process.env.OAUTH_BASE || '{{OAUTH_BASE}}';
44
+ const OAUTH_REALM = process.env.OAUTH_REALM || '{{OAUTH_REALM}}';
45
+
46
+ const realmPath = `${OAUTH_BASE}/realms/${OAUTH_REALM}/oauth`;
47
+
48
+ // Read scopes from app-config.json (default to 'all' if missing/corrupt)
49
+ let SCOPES = 'all';
50
+ try {
51
+ const appConfig = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'app-config.json'), 'utf-8'));
52
+ if (appConfig.auth && appConfig.auth.scopes) SCOPES = appConfig.auth.scopes.join(' ');
53
+ } catch {
54
+ console.warn('Warning: Could not read app-config.json — defaulting scopes to "all".');
55
+ }
56
+
57
+ // Step 1: Redirect user to authorization page
58
+ router.get('/login', (req, res) => {
59
+ const state = require('crypto').randomBytes(16).toString('hex');
60
+ tokenStore.setState(state);
61
+
62
+ const authUrl = `${realmPath}/authorize?` +
63
+ `response_type=code` +
64
+ `&client_id=${encodeURIComponent(CLIENT_ID)}` +
65
+ `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
66
+ `&state=${state}` +
67
+ `&scope=${encodeURIComponent(SCOPES)}`;
68
+
69
+ res.redirect(authUrl);
70
+ });
71
+
72
+ // Step 2: Handle the OAuth callback
73
+ router.get('/callback', async (req, res) => {
74
+ const { code, state, error, error_description } = req.query;
75
+
76
+ if (error) {
77
+ console.error('OAuth error:', error, error_description || '');
78
+ return res.status(400).send(`Authorization denied: ${escapeHtml(error_description || error)}`);
79
+ }
80
+
81
+ if (!code || typeof code !== 'string') {
82
+ return res.status(400).send('Missing authorization code.');
83
+ }
84
+
85
+ if (state !== tokenStore.getState()) {
86
+ return res.status(400).send('Invalid state parameter.');
87
+ }
88
+
89
+ try {
90
+ // Step 3: Exchange authorization code for tokens
91
+ const tokenResponse = await axios.post(
92
+ `${realmPath}/token`,
93
+ new URLSearchParams({
94
+ grant_type: 'authorization_code',
95
+ client_id: CLIENT_ID,
96
+ client_secret: CLIENT_SECRET,
97
+ code,
98
+ redirect_uri: REDIRECT_URI,
99
+ }),
100
+ { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
101
+ );
102
+
103
+ const { access_token, refresh_token, expires_in } = tokenResponse.data;
104
+ tokenStore.saveTokens({ access_token, refresh_token, expires_in });
105
+
106
+ // Step 4: Verify the access token via userinfo
107
+ const userInfoResponse = await axios.get(`${realmPath}/userinfo`, {
108
+ headers: { Authorization: `Bearer ${access_token}` },
109
+ });
110
+
111
+ // Step 5: Log token status (tokens are only visible in the browser)
112
+ console.log(`\nAccess token received (expires in ${expires_in}s)`);
113
+ console.log('Refresh token received');
114
+ console.log('View tokens in your browser (masked — click to reveal).');
115
+
116
+ // Set no-cache headers — tokens are sensitive (local development only)
117
+ res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
118
+ res.set('Pragma', 'no-cache');
119
+ res.set('Expires', '0');
120
+ res.send(
121
+ `<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Brevo OAuth — Success</title>${PAGE_STYLES}</head>` +
122
+ '<body>' +
123
+ '<div class="page-card">' +
124
+ '<h2>Authorization successful!</h2>' +
125
+ '<p class="subtitle">Tokens are masked for security. Click to reveal.</p>' +
126
+ '<div class="request-details"><strong>Authorization Request</strong><dl>' +
127
+ `<dt>Client ID</dt><dd>${escapeHtml(CLIENT_ID)}</dd>` +
128
+ `<dt>Redirect URI</dt><dd>${escapeHtml(REDIRECT_URI)}</dd>` +
129
+ `<dt>Scopes</dt><dd>${escapeHtml(SCOPES)}</dd>` +
130
+ '</dl></div>' +
131
+ '<div class="token-section">' +
132
+ `<p><strong>Access Token</strong> (expires in ${expires_in}s):</p>` +
133
+ '<div class="token-wrapper">' +
134
+ `<code class="token-value">${escapeHtml(access_token)}</code>` +
135
+ '<button class="reveal-btn">Click to reveal</button></div>' +
136
+ '<div style="margin-top:16px"><p><strong>Refresh Token:</strong></p>' +
137
+ '<div class="token-wrapper">' +
138
+ `<code class="token-value">${escapeHtml(refresh_token)}</code>` +
139
+ '<button class="reveal-btn">Click to reveal</button></div></div>' +
140
+ '</div>' +
141
+ '<p style="margin-top:16px"><a href="/auth/refresh">Refresh token</a></p>' +
142
+ '</div>' +
143
+ REVEAL_SCRIPT +
144
+ '</body></html>',
145
+ );
146
+ } catch (error) {
147
+ console.error('OAuth error:', error.response?.data || error.message);
148
+ res.status(500).send('Authorization failed. Check terminal for details.');
149
+ }
150
+ });
151
+
152
+ // Token refresh — exchanges the stored refresh token for a new access token
153
+ router.get('/refresh', async (req, res) => {
154
+ const refreshToken = tokenStore.getRefreshToken();
155
+ if (!refreshToken) {
156
+ return res.status(400).send('No refresh token available. Complete /auth/login first.');
157
+ }
158
+
159
+ try {
160
+ const tokenResponse = await axios.post(
161
+ `${realmPath}/token`,
162
+ new URLSearchParams({
163
+ grant_type: 'refresh_token',
164
+ client_id: CLIENT_ID,
165
+ client_secret: CLIENT_SECRET,
166
+ refresh_token: refreshToken,
167
+ }),
168
+ { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
169
+ );
170
+
171
+ const { access_token, refresh_token, expires_in } = tokenResponse.data;
172
+ tokenStore.saveTokens({ access_token, refresh_token, expires_in });
173
+
174
+ console.log(`\nAccess token refreshed (expires in ${expires_in}s)`);
175
+ console.log('View token in your browser (masked — click to reveal).');
176
+
177
+ res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
178
+ res.set('Pragma', 'no-cache');
179
+ res.set('Expires', '0');
180
+ res.send(
181
+ `<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Brevo OAuth — Token Refreshed</title>${PAGE_STYLES}</head>` +
182
+ '<body>' +
183
+ '<div class="page-card">' +
184
+ '<h2>Token refreshed!</h2>' +
185
+ '<p class="subtitle">Click to reveal the token.</p>' +
186
+ '<div class="token-section">' +
187
+ `<p><strong>Access Token</strong> (expires in ${expires_in}s):</p>` +
188
+ '<div class="token-wrapper">' +
189
+ `<code class="token-value">${escapeHtml(access_token)}</code>` +
190
+ '<button class="reveal-btn">Click to reveal</button></div>' +
191
+ '</div>' +
192
+ '<p class="footer-text">You can close this window and return to the terminal.</p>' +
193
+ '</div>' +
194
+ REVEAL_SCRIPT +
195
+ '</body></html>',
196
+ );
197
+ } catch (error) {
198
+ console.error('Refresh error:', error.response?.data || error.message);
199
+ res.status(500).send('Token refresh failed. Check terminal for details.');
200
+ }
201
+ });
202
+
203
+ module.exports = router;
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "{{APP_SLUG}}",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "My custom brevo app {{APP_NAME}}",
6
+ "main": "server.js",
7
+ "scripts": {
8
+ "start": "node server.js"
9
+ },
10
+ "dependencies": {
11
+ "axios": "^1.7.0",
12
+ "dotenv": "^16.4.0",
13
+ "express": "^4.21.0"
14
+ }
15
+ }
@@ -0,0 +1,226 @@
1
+ // Test server — for local OAuth testing only. Not for production use.
2
+ // Run with: brevo app start oauth
3
+
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ const envPath = path.join(__dirname, '.env.local');
8
+ if (!fs.existsSync(envPath)) {
9
+ console.error('\nError: .env.local not found at', envPath);
10
+ console.error('Copy .env.example to .env.local and fill in your credentials:\n');
11
+ console.error(` Source: ${path.join(__dirname, '.env.example')}\n`);
12
+ process.exit(1);
13
+ }
14
+
15
+ require('dotenv').config({ path: envPath });
16
+
17
+ if (!process.env.CLIENT_ID || !process.env.CLIENT_SECRET) {
18
+ console.error('\nError: CLIENT_ID and CLIENT_SECRET must be set in .env.local');
19
+ console.error('Get your credentials with: brevo app credentials --reveal-secret\n');
20
+ process.exit(1);
21
+ }
22
+
23
+ if (!process.env.OAUTH_BASE || process.env.OAUTH_BASE.startsWith('{{')) {
24
+ console.error('\nError: OAUTH_BASE is not set or contains a placeholder.');
25
+ console.error('Check your .env.local file and set a valid OAuth base URL.\n');
26
+ process.exit(1);
27
+ }
28
+ if (!process.env.OAUTH_REALM || process.env.OAUTH_REALM.startsWith('{{')) {
29
+ console.error('\nError: OAUTH_REALM is not set or contains a placeholder.');
30
+ console.error('Check your .env.local file and set a valid OAuth realm.\n');
31
+ process.exit(1);
32
+ }
33
+
34
+ const express = require('express');
35
+ const authRouter = require('./handler');
36
+
37
+ const app = express();
38
+ const PORT = process.env.PORT || 3009;
39
+
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)}`;
54
+
55
+ app.use('/auth', authRouter);
56
+
57
+ app.use(express.urlencoded({ extended: true }));
58
+
59
+ app.get('/', (req, res) => {
60
+ res.send(`<!DOCTYPE html>
61
+ <html lang="en">
62
+ <head>
63
+ <meta charset="utf-8" />
64
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
65
+ <title>{{APP_NAME}} — OAuth Test</title>
66
+ <style>
67
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
68
+ body {
69
+ font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
70
+ background: rgb(250, 245, 227);
71
+ min-height: 100vh;
72
+ display: flex;
73
+ flex-direction: column;
74
+ align-items: center;
75
+ justify-content: center;
76
+ padding: 40px 16px;
77
+ }
78
+ .brevo-logo {
79
+ margin-bottom: 32px;
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ }
84
+ .brevo-logo svg { height: 48px; }
85
+ .card {
86
+ background: #fff;
87
+ border: 1px solid #e3e3e3;
88
+ border-radius: 16px;
89
+ padding: 40px;
90
+ width: 100%;
91
+ max-width: 500px;
92
+ text-align: center;
93
+ }
94
+ .card h2 {
95
+ color: #1b1b1b;
96
+ font-size: 22px;
97
+ margin-bottom: 6px;
98
+ }
99
+ .card p.sub {
100
+ color: #696969;
101
+ font-size: 14px;
102
+ margin-bottom: 24px;
103
+ }
104
+ .btn {
105
+ display: block;
106
+ width: 100%;
107
+ padding: 8px 16px;
108
+ border: none;
109
+ border-radius: 16px;
110
+ font-size: 16px;
111
+ font-weight: 600;
112
+ cursor: pointer;
113
+ text-decoration: none;
114
+ text-align: center;
115
+ margin-bottom: 12px;
116
+ height: 40px;
117
+ line-height: 24px;
118
+ }
119
+ .btn:hover { opacity: 0.9; }
120
+ .btn-primary { background: #1b1b1b; color: #fff; }
121
+ .btn-secondary {
122
+ background: #fff;
123
+ color: #1b1b1b;
124
+ border: 1px solid #cfcfcf;
125
+ }
126
+ .hint {
127
+ background: #f5f5f5;
128
+ border: 1px solid #e3e3e3;
129
+ border-radius: 16px;
130
+ padding: 10px 14px;
131
+ font-size: 13px;
132
+ color: #696969;
133
+ margin-bottom: 16px;
134
+ text-align: left;
135
+ }
136
+ .hint code {
137
+ background: rgba(0,0,0,0.06);
138
+ padding: 1px 5px;
139
+ border-radius: 3px;
140
+ font-size: 12px;
141
+ }
142
+ .auth-url {
143
+ background: #f5f5f5;
144
+ border: 1px solid #e3e3e3;
145
+ border-radius: 16px;
146
+ padding: 10px 14px;
147
+ font-size: 12px;
148
+ color: #696969;
149
+ text-align: left;
150
+ margin-bottom: 16px;
151
+ word-break: break-all;
152
+ }
153
+ .auth-url strong {
154
+ display: block;
155
+ font-size: 13px;
156
+ margin-bottom: 4px;
157
+ color: #1b1b1b;
158
+ }
159
+ .auth-url code {
160
+ font-family: monospace;
161
+ font-size: 11px;
162
+ color: #858585;
163
+ }
164
+ .footer {
165
+ margin-top: 16px;
166
+ color: #858585;
167
+ font-size: 12px;
168
+ }
169
+ .btn-primary, .btn-secondary { display: flex; align-items: center; justify-content: center; gap: 8px; }
170
+ .spinner {
171
+ display: none;
172
+ width: 16px;
173
+ height: 16px;
174
+ border: 2px solid rgba(0,0,0,0.15);
175
+ border-top-color: #1b1b1b;
176
+ border-radius: 50%;
177
+ animation: spin 0.6s linear infinite;
178
+ }
179
+ .btn-primary .spinner { border-color: rgba(255,255,255,0.3); border-top-color: #fff; }
180
+ .btn.loading .spinner { display: inline-block; }
181
+ .btn.loading { pointer-events: none; opacity: 0.7; }
182
+ @keyframes spin { to { transform: rotate(360deg); } }
183
+ </style>
184
+ </head>
185
+ <body>
186
+ <div class="brevo-logo">
187
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 28" fill="none">
188
+ <path fill="#0B996E" d="M73.825 17.178c0-4.037 2.55-6.876 6.175-6.876 3.626 0 6.216 2.838 6.216 6.876 0 4.04-2.59 6.716-6.216 6.716-3.626 0-6.175-2.8-6.175-6.716Zm-3.785 0c0 5.957 4.144 10.155 9.96 10.155 5.816 0 10-4.198 10-10.155 0-5.957-4.144-10.314-10-10.314-5.857 0-9.96 4.279-9.96 10.314ZM50.717 7.104l7.81 19.989h3.665l7.81-19.99h-3.945l-5.658 15.433h-.08L54.662 7.104h-3.945Zm-15.18 9.354c.239-3.679 2.67-6.156 5.977-6.156 2.867 0 5.02 1.84 5.338 4.598h-6.614c-2.35 0-3.626.28-4.58 1.56h-.121v-.002Zm-3.784.6c0 5.957 4.183 10.274 9.96 10.274 3.904 0 7.33-1.999 8.803-5.158l-3.186-1.6c-1.115 2.079-3.267 3.318-5.618 3.318-2.83 0-5.379-2.159-5.379-4.238 0-1.078.718-1.56 1.753-1.56h12.63v-1.078c0-5.997-3.825-10.155-9.323-10.155-5.497 0-9.641 4.278-9.641 10.195M20.916 27.09h3.586V14.82c0-2.64 1.632-4.519 3.905-4.519.956 0 1.951.32 2.43.759.359-.96.917-1.918 1.753-2.878-.957-.799-2.59-1.32-4.184-1.32-4.382 0-7.49 3.278-7.49 7.956v12.274-.002ZM3.586 13.86V4.104h5.896c1.992 0 3.307 1.16 3.307 2.918 0 1.999-1.713 3.518-5.218 4.677-2.39.76-3.466 1.4-3.865 2.16l-.12.001Zm0 9.795v-4.077c0-1.8 1.514-3.558 3.626-4.238 1.873-.64 3.425-1.28 4.74-1.959 1.754 1.04 2.829 2.838 2.829 4.718 0 3.198-3.028 5.556-7.132 5.556H3.586ZM0 27.093h7.968c6.057 0 10.597-3.798 10.597-8.835 0-2.76-1.394-5.237-3.864-6.837 1.275-1.28 1.873-2.759 1.873-4.558 0-3.717-2.67-6.196-6.693-6.196H0v26.426Z"/>
189
+ </svg>
190
+ </div>
191
+
192
+ <div class="card">
193
+ <h2>{{APP_NAME}}</h2>
194
+ <p class="sub">Local OAuth testing server</p>
195
+ <p class="hint">Before starting, make sure the redirect URL in <code>app-config.json</code> matches this server's port (<code>${PORT}</code>).</p>
196
+ <div class="auth-url">
197
+ <strong>Authorization URL</strong>
198
+ <code>${AUTH_URL}</code>
199
+ </div>
200
+ <a href="/auth/login" id="start-btn" class="btn btn-primary"><span>Start OAuth Flow</span><span class="spinner"></span></a>
201
+ <a href="/auth/refresh" id="refresh-btn" class="btn btn-secondary"><span>Refresh Token</span><span class="spinner"></span></a>
202
+ <p class="footer">Running on localhost &middot; Not for production use</p>
203
+ </div>
204
+ <script>
205
+ document.querySelectorAll(".btn").forEach(function (btn) {
206
+ btn.addEventListener("click", function () { btn.classList.add("loading"); });
207
+ });
208
+ window.addEventListener("pageshow", function () {
209
+ document.querySelectorAll(".btn.loading").forEach(function (btn) { btn.classList.remove("loading"); });
210
+ });
211
+ </script>
212
+ </body>
213
+ </html>`);
214
+ });
215
+
216
+ app.listen(PORT, '127.0.0.1', () => {
217
+ const url = `http://localhost:${PORT}`;
218
+ console.log(`\nTest server running at ${url}`);
219
+ console.log(`Opening browser...\n`);
220
+
221
+ const { execFile } = require('child_process');
222
+ const open = process.platform === 'darwin' ? ['open', [url]]
223
+ : process.platform === 'win32' ? ['cmd', ['/c', 'start', url]]
224
+ : ['xdg-open', [url]];
225
+ execFile(open[0], open[1], () => {});
226
+ });
@@ -0,0 +1,39 @@
1
+ // In-memory token store — replace with your database in production
2
+
3
+ let tokens = {};
4
+ let csrfState = null;
5
+
6
+ module.exports = {
7
+ saveTokens({ access_token, refresh_token, expires_in }) {
8
+ tokens = {
9
+ accessToken: access_token,
10
+ refreshToken: refresh_token,
11
+ expiresAt: Date.now() + expires_in * 1000,
12
+ };
13
+ },
14
+
15
+ getAccessToken() {
16
+ return tokens.accessToken;
17
+ },
18
+
19
+ getRefreshToken() {
20
+ return tokens.refreshToken;
21
+ },
22
+
23
+ isExpired() {
24
+ return !tokens.expiresAt || Date.now() >= tokens.expiresAt;
25
+ },
26
+
27
+ setState(state) {
28
+ csrfState = state;
29
+ },
30
+
31
+ getState() {
32
+ return csrfState;
33
+ },
34
+
35
+ clear() {
36
+ tokens = {};
37
+ csrfState = null;
38
+ },
39
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Replace all occurrences of the given keys in a template string.
3
+ * Keys should include delimiters (e.g. '{{APP_NAME}}').
4
+ */
5
+ export declare function applyVars(template: string, vars: Record<string, string>): string;
6
+ /**
7
+ * Scaffold file manifest — maps output file paths to their .tmpl source files.
8
+ * Add new templates here; the scaffold command picks them up automatically.
9
+ */
10
+ export interface TemplateFile {
11
+ /** Relative output path inside the scaffolded project */
12
+ outputPath: string;
13
+ /** Relative path inside templates/files/ */
14
+ templatePath: string;
15
+ }
16
+ export declare const TEMPLATE_MANIFEST: TemplateFile[];
17
+ /**
18
+ * Load all templates from disk, apply variable substitution, and return
19
+ * an array of { name, content } ready to write.
20
+ */
21
+ export declare function loadAllTemplates(vars: Record<string, string>): Array<{
22
+ name: string;
23
+ content: string;
24
+ }>;
25
+ export declare const oauthServerJsTemplate: string;
26
+ export declare const oauthHandlerTemplate: string;
27
+ export declare const tokenStoreJsTemplate: string;
28
+ export declare const envExampleTemplate: string;
29
+ export declare const envLocalTemplate: string;
30
+ export declare const gitignoreTemplate: string;
31
+ export declare const packageJsonTemplate: string;
32
+ export declare const appConfigTemplate: string;
33
+ export declare const agentsMdTemplate: string;
34
+ export declare const claudeMdTemplate: string;
35
+ export declare const readmeTemplate: string;
36
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"AAaA;;;GAGG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAMhF;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,iBAAiB,EAAE,YAAY,EAqB3C,CAAC;AAEF;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3B,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAK1C;AAKD,eAAO,MAAM,qBAAqB,QAA2C,CAAC;AAC9E,eAAO,MAAM,oBAAoB,QAA4C,CAAC;AAC9E,eAAO,MAAM,oBAAoB,QAAgD,CAAC;AAClF,eAAO,MAAM,kBAAkB,QAA8C,CAAC;AAC9E,eAAO,MAAM,gBAAgB,QAA4C,CAAC;AAC1E,eAAO,MAAM,iBAAiB,QAAiC,CAAC;AAChE,eAAO,MAAM,mBAAmB,QAA8C,CAAC;AAC/E,eAAO,MAAM,iBAAiB,QAAuC,CAAC;AACtE,eAAO,MAAM,gBAAgB,QAAiC,CAAC;AAC/D,eAAO,MAAM,gBAAgB,QAAiC,CAAC;AAC/D,eAAO,MAAM,cAAc,QAAiC,CAAC"}