cds-error-outbox 1.0.0 → 1.0.2

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.
@@ -0,0 +1,29 @@
1
+ name: Test & Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ test-and-publish:
9
+ runs-on: ubuntu-latest
10
+
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Set up Node.js
15
+ uses: actions/setup-node@v4
16
+ with:
17
+ node-version: '22'
18
+ registry-url: 'https://registry.npmjs.org'
19
+
20
+ - name: Install dependencies
21
+ run: npm ci
22
+
23
+ - name: Run tests
24
+ run: npm test
25
+
26
+ - name: Publish to npm
27
+ run: npm publish --access public
28
+ env:
29
+ NODE_AUTH_TOKEN: ${{ secrets.npm_token }}
package/README.md CHANGED
@@ -1,283 +1,311 @@
1
- # cds-error-outbox
2
-
3
- A production-ready, reusable **SAP CAP** (Node.js) plugin that automatically captures service errors, deduplicates them, and sends batched HTML email notifications — with **zero production dependencies**.
4
-
5
- ---
6
-
7
- ## Features
8
-
9
- - Hooks into **all CAP services** via `srv.on('error')` — zero manual wiring required
10
- - Stores errors in a CDS-managed `error.outbox.Errors` DB entity (auto-deployed)
11
- - **Deduplicates** by `SHA-256(message + service + action)` — increments count instead of creating duplicate rows
12
- - Sends **batched HTML email reports** on a configurable interval
13
- - Pluggable email providers: **O365 (Microsoft Graph API)**, **SMTP (nodemailer)**, **Mock (dev/test)**
14
- - Fully configurable via `cds.env.requires.errorOutbox`
15
- - **Non-blocking** — error capture is fire-and-forget; the request pipeline is never delayed
16
- - Never crashes the application — all internal failures are logged and swallowed
17
-
18
- ---
19
-
20
- ## Installation
21
-
22
- ```bash
23
- npm install cds-error-outbox
24
- ```
25
-
26
- Because `package.json` declares `"cds": { "plugin": true }`, CAP automatically loads `index.js` at startup.
27
-
28
- > **Important:** Due to how CAP resolves symlinked local packages, you must explicitly require the plugin in your project's `srv/server.js` (or root `server.js`):
29
- >
30
- > ```js
31
- > require('cds-error-outbox'); // add as the very first line
32
- > // ... rest of your server.js
33
- > ```
34
-
35
- ---
36
-
37
- ## Configuration
38
-
39
- Add the following to your project's `package.json` under `cds.requires`, or to `.cdsrc.json`:
40
-
41
- ```json
42
- {
43
- "cds": {
44
- "requires": {
45
- "errorOutbox": {
46
- "enabled": true,
47
- "interval": 300000,
48
- "batchSize": 50,
49
- "dedup": {
50
- "enabled": true,
51
- "windowMinutes": 10
52
- },
53
- "mail": {
54
- "provider": "o365",
55
- "tenantId": "<YOUR_TENANT_ID>",
56
- "clientId": "<YOUR_CLIENT_ID>",
57
- "clientSecret": "<YOUR_CLIENT_SECRET>",
58
- "from": "errors@yourcompany.com",
59
- "to": "devops@yourcompany.com"
60
- }
61
- }
62
- }
63
- }
64
- }
65
- ```
66
-
67
- ### Configuration reference
68
-
69
- | Key | Type | Default | Description |
70
- |---|---|---|---|
71
- | `enabled` | boolean | `true` | Enable/disable the entire plugin |
72
- | `interval` | number (ms) | `300000` | Batch email job frequency |
73
- | `batchSize` | number | `50` | Max errors per email batch |
74
- | `dedup.enabled` | boolean | `true` | Enable hash-based deduplication |
75
- | `dedup.windowMinutes` | number | `10` | Rolling dedup window in minutes |
76
- | `mail.provider` | string | `'mock'` | `'o365'` \| `'smtp'` \| `'mock'` |
77
- | `mail.from` | string | `''` | Sender address |
78
- | `mail.to` | string | `''` | Recipient(s), comma-separated |
79
- | `mail.tenantId` | string | `''` | Azure AD tenant ID (O365 only) |
80
- | `mail.clientId` | string | `''` | Azure AD app client ID (O365 only) |
81
- | `mail.clientSecret` | string | `''` | Azure AD app client secret (O365 only) |
82
- | `mail.smtp.host` | string | `''` | SMTP host (SMTP only) |
83
- | `mail.smtp.port` | number | `587` | SMTP port (SMTP only) |
84
- | `mail.smtp.secure` | boolean | `false` | Use TLS (SMTP only) |
85
- | `mail.smtp.auth.user` | string | `''` | SMTP username (SMTP only) |
86
- | `mail.smtp.auth.pass` | string | `''` | SMTP password (SMTP only) |
87
-
88
- ---
89
-
90
- ## Email Providers
91
-
92
- ### `mock` _(default — development/testing)_
93
-
94
- Logs the email subject and metadata to the console. No external calls. Use this during local development.
95
-
96
- ### `o365` — Microsoft Graph API
97
-
98
- Uses the [Microsoft Graph `sendMail` API](https://learn.microsoft.com/en-us/graph/api/user-sendmail) with an OAuth 2.0 **client credentials** flow. No user login is required. Zero additional npm dependencies.
99
-
100
- #### O365 Setup
101
-
102
- 1. Go to [Azure Portal → App Registrations](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps) and click **New registration**
103
- 2. Note the **Application (client) ID** and **Directory (tenant) ID**
104
- 3. Go to **Certificates & Secrets** → **New client secret** — note the secret **Value** (shown once)
105
- 4. Go to **API Permissions** → **Add a permission** → **Microsoft Graph** → **Application permissions**
106
- - Add: `Mail.Send`
107
- 5. Click **Grant admin consent** for your organisation
108
- 6. The `mail.from` address must be a **licensed Exchange Online mailbox** that the app registration has permission to send from
109
- 7. Configure `cds.requires.errorOutbox.mail`:
110
-
111
- ```json
112
- {
113
- "provider": "o365",
114
- "tenantId": "<Directory (tenant) ID>",
115
- "clientId": "<Application (client) ID>",
116
- "clientSecret": "<Client Secret value>",
117
- "from": "errors@yourcompany.com",
118
- "to": "devops@yourcompany.com"
119
- }
120
- ```
121
-
122
- > **Security:** Never commit `clientSecret` to source control.
123
- >
124
- > The O365 provider reads credentials from **env variables as a fallback** — values in `package.json` take priority, but any missing field is automatically picked up from the environment:
125
- >
126
- > ```bash
127
- > export CDS_REQUIRES_ERROROUTBOX_MAIL_TENANTID="xxxx"
128
- > export CDS_REQUIRES_ERROROUTBOX_MAIL_CLIENTID="xxxx"
129
- > export CDS_REQUIRES_ERROROUTBOX_MAIL_CLIENTSECRET="xxxx"
130
- > export CDS_REQUIRES_ERROROUTBOX_MAIL_FROM="errors@company.com"
131
- > export CDS_REQUIRES_ERROROUTBOX_MAIL_TO="devops@company.com"
132
- > ```
133
- >
134
- > On BTP / Cloud Foundry use `cf set-env <app> CDS_REQUIRES_ERROROUTBOX_MAIL_CLIENTSECRET "<value>"` instead of putting the secret in `manifest.yml`.
135
-
136
- ### `smtp`
137
-
138
- Requires `nodemailer` (optional peer dependency):
139
-
140
- ```bash
141
- npm install nodemailer
142
- ```
143
-
144
- ```json
145
- {
146
- "provider": "smtp",
147
- "from": "errors@yourcompany.com",
148
- "to": "devops@yourcompany.com",
149
- "smtp": {
150
- "host": "smtp.yourcompany.com",
151
- "port": 587,
152
- "secure": false,
153
- "auth": { "user": "smtp-user", "pass": "smtp-password" }
154
- }
155
- }
156
- ```
157
-
158
- ---
159
-
160
- ## How it works
161
-
162
- ```
163
- CAP service throws an error
164
-
165
-
166
- srv.on('error') — fire-and-forget via setImmediate (non-blocking)
167
-
168
-
169
- SHA-256( message | service | action )
170
-
171
- ┌────┴────┐
172
- │ │
173
- duplicate? new?
174
- │ │
175
- ▼ ▼
176
- UPDATE INSERT
177
- count+1 count=1
178
- lastSeen firstSeen/lastSeen
179
-
180
-
181
- setInterval every `interval` ms
182
-
183
-
184
- SELECT sent=false LIMIT batchSize (oldest first)
185
-
186
-
187
- Format HTML (grouped by service)
188
-
189
-
190
- provider.send(...)
191
-
192
- (only on success)
193
- UPDATE sent=true WHERE ID IN [...]
194
- ```
195
-
196
- ---
197
-
198
- ## DB Entity
199
-
200
- The plugin automatically adds the following entity to your project's database schema:
201
-
202
- ```cds
203
- namespace error.outbox;
204
-
205
- entity Errors {
206
- key ID : UUID;
207
- hash : String(64);
208
- service : String;
209
- action : String;
210
- message : LargeString;
211
- stack : LargeString;
212
- count : Integer;
213
- firstSeen : Timestamp;
214
- lastSeen : Timestamp;
215
- sent : Boolean default false;
216
- }
217
- ```
218
-
219
- After installing the plugin, register the model in your project's `db/` folder. Create a file `db/error-outbox.cds`:
220
-
221
- ```cds
222
- using from '../plugins/cds-error-outbox/db/model';
223
- ```
224
-
225
- Then run deploy:
226
-
227
- ```bash
228
- cds deploy --to sqlite # local development
229
- cds build # production (BTP, HANA)
230
- ```
231
-
232
- ---
233
-
234
- ## Environment variables
235
-
236
- CAP maps nested config paths to environment variables. You can override any value at runtime without changing `package.json`:
237
-
238
- ```bash
239
- CDS_REQUIRES_ERROROUTBOX_ENABLED=true
240
- CDS_REQUIRES_ERROROUTBOX_INTERVAL=60000
241
- CDS_REQUIRES_ERROROUTBOX_MAIL_PROVIDER=o365
242
- CDS_REQUIRES_ERROROUTBOX_MAIL_TENANTID=...
243
- CDS_REQUIRES_ERROROUTBOX_MAIL_CLIENTID=...
244
- CDS_REQUIRES_ERROROUTBOX_MAIL_CLIENTSECRET=...
245
- CDS_REQUIRES_ERROROUTBOX_MAIL_FROM=errors@yourcompany.com
246
- CDS_REQUIRES_ERROROUTBOX_MAIL_TO=devops@yourcompany.com
247
- ```
248
-
249
- ---
250
-
251
- ## Project structure
252
-
253
- ```
254
- cds-error-outbox/
255
- ├── package.json ← CAP plugin declaration (cds.plugin: true)
256
- ├── index.js ← Entry point — calls bootstrap.initialize()
257
-
258
- ├── config/
259
- │ └── defaults.js ← Default config values
260
-
261
- ├── lib/
262
- │ ├── bootstrap.js ← Init orchestration (cds lifecycle hooks)
263
- │ ├── config.js ← Deep merge + config loader (singleton)
264
- │ ├── interceptor.js ← srv.on('error') hook (fire-and-forget)
265
- │ ├── dedup.js ← SHA-256 hash + DB upsert logic
266
- │ ├── scheduler.js ← Interval batch job
267
- │ └── formatter.js ← HTML email builder
268
-
269
- ├── providers/
270
- │ ├── index.js ← Provider factory
271
- │ ├── o365.js ← Microsoft Graph API (zero extra deps)
272
- │ ├── smtp.js ← nodemailer wrapper (optional peer dep)
273
- │ └── mock.js ← Console logger (dev/test)
274
-
275
- └── db/
276
- └── model.cds ← error.outbox.Errors entity
277
- ```
278
-
279
- ---
280
-
281
- ## License
282
-
283
- MIT
1
+ <div align="center">
2
+
3
+ # CDS Error Outbox
4
+
5
+ **Automatic error capture, deduplication & email alerting for SAP CAP applications.**
6
+
7
+ <br>
8
+
9
+ [![npm](https://img.shields.io/npm/v/cds-error-outbox?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/cds-error-outbox)
10
+ [![SAP CAP](https://img.shields.io/badge/SAP%20CAP-plugin-009FDB?style=for-the-badge&logo=sap&logoColor=white)](https://cap.cloud.sap)
11
+ [![MIT](https://img.shields.io/badge/license-MIT-22c55e?style=for-the-badge&logo=opensourceinitiative&logoColor=white)](./LICENSE)
12
+ [![zero deps](https://img.shields.io/badge/deps-zero-6366f1?style=for-the-badge&logo=nodedotjs&logoColor=white)](#)
13
+
14
+ [![Microsoft O365](https://img.shields.io/badge/Microsoft%20365-mail%20ready-0078D4?style=flat-square&logo=microsoft&logoColor=white)](#o365--microsoft-graph-api)
15
+ [![SMTP](https://img.shields.io/badge/SMTP-nodemailer-EA4335?style=flat-square&logo=gmail&logoColor=white)](#smtp)
16
+ [![SQLite](https://img.shields.io/badge/SQLite-supported-003B57?style=flat-square&logo=sqlite&logoColor=white)](#db-entity)
17
+ [![SAP HANA](https://img.shields.io/badge/SAP%20HANA-supported-1B6B3A?style=flat-square&logo=sap&logoColor=white)](#db-entity)
18
+ [![SAP BTP](https://img.shields.io/badge/SAP%20BTP-Cloud%20Foundry-0FAAFF?style=flat-square&logo=sap&logoColor=white)](#environment-variables)
19
+
20
+ </div>
21
+
22
+ ---
23
+
24
+ **[Installation](#installation) · [Configuration](#configuration) · [Email Providers](#email-providers) · [How it works](#how-it-works) · [DB Entity](#db-entity) · [Environment variables](#environment-variables) · [Project structure](#project-structure)**
25
+
26
+ ---
27
+
28
+ ## Features
29
+
30
+ - Hooks into **all CAP services** automatically — zero manual wiring required
31
+ - Persists errors to a CDS-managed `error.outbox.Errors` entity
32
+ - **Deduplicates** via `SHA-256(message + service + action)` — increments a counter instead of flooding the DB
33
+ - Sends **batched HTML email reports** on a configurable interval
34
+ - Pluggable providers: **O365 (Microsoft Graph)**, **SMTP (nodemailer)**, **Mock (dev/test)**
35
+ - **Non-blocking** — fire-and-forget capture, the request pipeline is never delayed
36
+ - **Resilient** — all internal failures are caught, logged, and swallowed; the app never crashes
37
+
38
+ ---
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ npm install cds-error-outbox
44
+ ```
45
+
46
+ Because `package.json` declares `"cds": { "plugin": true }`, CAP automatically loads `index.js` at startup.
47
+
48
+ > **Note:** Due to how CAP resolves symlinked local packages, you may need to explicitly require the plugin so it is loaded at startup.
49
+ >
50
+ > **Option A — with a custom `server.js`:**
51
+ > ```js
52
+ > require("cds-error-outbox"); // ← add as the very first line
53
+ > // ... rest of your server.js
54
+ > ```
55
+ >
56
+ > **Option B — without a custom `server.js`** (most common):
57
+ > Add the require at the top of any service file, e.g. `srv/admin-service.js`:
58
+ > ```js
59
+ > require("cds-error-outbox"); // ← add at the top
60
+ > // ... rest of your service
61
+ > ```
62
+
63
+ Then register the DB model in your project (e.g. in `db/schema.cds`):
64
+
65
+ ```cds
66
+ using from 'cds-error-outbox/db/model';
67
+ ```
68
+
69
+ And deploy:
70
+
71
+ ```bash
72
+ cds deploy --to sqlite # local development
73
+ cds build # production (BTP, HANA)
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Configuration
79
+
80
+ Add the following to your project's `package.json` under `cds.requires`, or to `.cdsrc.json`:
81
+
82
+ ```json
83
+ {
84
+ "cds": {
85
+ "requires": {
86
+ "errorOutbox": {
87
+ "enabled": true,
88
+ "interval": 300000,
89
+ "batchSize": 50,
90
+ "dedup": {
91
+ "enabled": true,
92
+ "windowMinutes": 10
93
+ },
94
+ "mail": {
95
+ "provider": "o365",
96
+ "tenantId": "<YOUR_TENANT_ID>",
97
+ "clientId": "<YOUR_CLIENT_ID>",
98
+ "clientSecret": "<YOUR_CLIENT_SECRET>",
99
+ "from": "errors@yourcompany.com",
100
+ "to": "devops@yourcompany.com"
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ ### Configuration reference
109
+
110
+ | Key | Type | Default | Description |
111
+ | --------------------- | ----------- | -------- | -------------------------------------- |
112
+ | `enabled` | boolean | `true` | Enable/disable the entire plugin |
113
+ | `interval` | number (ms) | `300000` | Batch email job frequency |
114
+ | `batchSize` | number | `50` | Max errors per email batch |
115
+ | `dedup.enabled` | boolean | `true` | Enable hash-based deduplication |
116
+ | `dedup.windowMinutes` | number | `10` | Rolling dedup window in minutes |
117
+ | `mail.provider` | string | `'mock'` | `'o365'` \| `'smtp'` \| `'mock'` |
118
+ | `mail.from` | string | `''` | Sender address |
119
+ | `mail.to` | string | `''` | Recipient(s), comma-separated |
120
+ | `mail.tenantId` | string | `''` | Azure AD tenant ID (O365 only) |
121
+ | `mail.clientId` | string | `''` | Azure AD app client ID (O365 only) |
122
+ | `mail.clientSecret` | string | `''` | Azure AD app client secret (O365 only) |
123
+ | `mail.smtp.host` | string | `''` | SMTP host (SMTP only) |
124
+ | `mail.smtp.port` | number | `587` | SMTP port (SMTP only) |
125
+ | `mail.smtp.secure` | boolean | `false` | Use TLS (SMTP only) |
126
+ | `mail.smtp.auth.user` | string | `''` | SMTP username (SMTP only) |
127
+ | `mail.smtp.auth.pass` | string | `''` | SMTP password (SMTP only) |
128
+
129
+ ---
130
+
131
+ ## Email Providers
132
+
133
+ ### `mock` _(default — development/testing)_
134
+
135
+ Logs the email subject and metadata to the console. No external calls. Use this during local development.
136
+
137
+ ### `o365` — Microsoft Graph API
138
+
139
+ Uses the [Microsoft Graph `sendMail` API](https://learn.microsoft.com/en-us/graph/api/user-sendmail) with an OAuth 2.0 **client credentials** flow. No user login is required. Zero additional npm dependencies.
140
+
141
+ #### O365 Setup
142
+
143
+ 1. Go to [Azure Portal → App Registrations](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps) and click **New registration**
144
+ 2. Note the **Application (client) ID** and **Directory (tenant) ID**
145
+ 3. Go to **Certificates & Secrets** → **New client secret** — note the secret **Value** (shown once)
146
+ 4. Go to **API Permissions** → **Add a permission** → **Microsoft Graph** → **Application permissions**
147
+ - Add: `Mail.Send`
148
+ 5. Click **Grant admin consent** for your organisation
149
+ 6. The `mail.from` address must be a **licensed Exchange Online mailbox** that the app registration has permission to send from
150
+ 7. Configure `cds.requires.errorOutbox.mail`:
151
+
152
+ ```json
153
+ {
154
+ "provider": "o365",
155
+ "tenantId": "<Directory (tenant) ID>",
156
+ "clientId": "<Application (client) ID>",
157
+ "clientSecret": "<Client Secret value>",
158
+ "from": "errors@yourcompany.com",
159
+ "to": "devops@yourcompany.com"
160
+ }
161
+ ```
162
+
163
+ > **Security:** Never commit `clientSecret` to source control.
164
+ >
165
+ > The O365 provider reads credentials from **env variables as a fallback** — values in `package.json` take priority, but any missing field is automatically picked up from the environment:
166
+ >
167
+ > ```bash
168
+ > export CDS_REQUIRES_ERROROUTBOX_MAIL_TENANTID="xxxx"
169
+ > export CDS_REQUIRES_ERROROUTBOX_MAIL_CLIENTID="xxxx"
170
+ > export CDS_REQUIRES_ERROROUTBOX_MAIL_CLIENTSECRET="xxxx"
171
+ > export CDS_REQUIRES_ERROROUTBOX_MAIL_FROM="errors@company.com"
172
+ > export CDS_REQUIRES_ERROROUTBOX_MAIL_TO="devops@company.com"
173
+ > ```
174
+ >
175
+ > On BTP / Cloud Foundry use `cf set-env <app> CDS_REQUIRES_ERROROUTBOX_MAIL_CLIENTSECRET "<value>"` instead of putting the secret in `manifest.yml`.
176
+
177
+ ### `smtp`
178
+
179
+ Requires `nodemailer` (optional peer dependency):
180
+
181
+ ```bash
182
+ npm install nodemailer
183
+ ```
184
+
185
+ ```json
186
+ {
187
+ "provider": "smtp",
188
+ "from": "errors@yourcompany.com",
189
+ "to": "devops@yourcompany.com",
190
+ "smtp": {
191
+ "host": "smtp.yourcompany.com",
192
+ "port": 587,
193
+ "secure": false,
194
+ "auth": { "user": "smtp-user", "pass": "smtp-password" }
195
+ }
196
+ }
197
+ ```
198
+
199
+ ---
200
+
201
+ ## How it works
202
+
203
+ ```
204
+ CAP service throws an error
205
+
206
+
207
+ srv.handle() wrapper — fire-and-forget via setImmediate (non-blocking)
208
+
209
+
210
+ SHA-256( message | service | action )
211
+
212
+ ┌────┴────┐
213
+ │ │
214
+ duplicate? new?
215
+ │ │
216
+ ▼ ▼
217
+ UPDATE INSERT
218
+ count+1 count=1
219
+ lastSeen firstSeen/lastSeen
220
+
221
+
222
+ setInterval every `interval` ms
223
+
224
+
225
+ SELECT sent=false LIMIT batchSize (oldest first)
226
+
227
+
228
+ Format HTML (grouped by service)
229
+
230
+
231
+ provider.send(...)
232
+
233
+ ▼ (only on success)
234
+ UPDATE sent=true WHERE ID IN [...]
235
+ ```
236
+
237
+ ---
238
+
239
+ ## DB Entity
240
+
241
+ The plugin automatically adds the following entity to your project's database schema:
242
+
243
+ ```cds
244
+ namespace error.outbox;
245
+
246
+ entity Errors {
247
+ key ID : UUID;
248
+ hash : String(64);
249
+ service : String;
250
+ action : String;
251
+ message : LargeString;
252
+ stack : LargeString;
253
+ count : Integer;
254
+ firstSeen : Timestamp;
255
+ lastSeen : Timestamp;
256
+ sent : Boolean default false;
257
+ }
258
+ ```
259
+
260
+ ---
261
+
262
+ ## Environment variables
263
+
264
+ CAP maps nested config paths to environment variables. You can override any value at runtime without changing `package.json`:
265
+
266
+ ```bash
267
+ CDS_REQUIRES_ERROROUTBOX_ENABLED=true
268
+ CDS_REQUIRES_ERROROUTBOX_INTERVAL=60000
269
+ CDS_REQUIRES_ERROROUTBOX_MAIL_PROVIDER=o365
270
+ CDS_REQUIRES_ERROROUTBOX_MAIL_TENANTID=...
271
+ CDS_REQUIRES_ERROROUTBOX_MAIL_CLIENTID=...
272
+ CDS_REQUIRES_ERROROUTBOX_MAIL_CLIENTSECRET=...
273
+ CDS_REQUIRES_ERROROUTBOX_MAIL_FROM=errors@yourcompany.com
274
+ CDS_REQUIRES_ERROROUTBOX_MAIL_TO=devops@yourcompany.com
275
+ ```
276
+
277
+ ---
278
+
279
+ ## Project structure
280
+
281
+ ```
282
+ cds-error-outbox/
283
+ ├── package.json ← CAP plugin declaration (cds.plugin: true)
284
+ ├── index.js ← Entry point — calls bootstrap.initialize()
285
+
286
+ ├── config/
287
+ │ └── defaults.js ← Default config values
288
+
289
+ ├── lib/
290
+ │ ├── bootstrap.js ← Init orchestration (cds lifecycle hooks)
291
+ │ ├── config.js ← Deep merge + config loader (singleton)
292
+ │ ├── interceptor.js ← srv.handle() wrapper (fire-and-forget)
293
+ │ ├── dedup.js ← SHA-256 hash + DB upsert logic
294
+ │ ├── scheduler.js ← Interval batch job
295
+ │ └── formatter.js ← HTML email builder
296
+
297
+ ├── providers/
298
+ │ ├── index.js ← Provider factory
299
+ │ ├── o365.js ← Microsoft Graph API (zero extra deps)
300
+ │ ├── smtp.js ← nodemailer wrapper (optional peer dep)
301
+ │ └── mock.js ← Console logger (dev/test)
302
+
303
+ └── db/
304
+ └── model.cds ← error.outbox.Errors entity
305
+ ```
306
+
307
+ ---
308
+
309
+ ## License
310
+
311
+ MIT