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.
- package/.github/workflows/publish.yml +29 -0
- package/README.md +311 -283
- package/config/defaults.js +46 -46
- package/db/model.cds +18 -18
- package/index.cds +1 -1
- package/index.js +14 -14
- package/lib/bootstrap.js +71 -71
- package/lib/config.js +67 -67
- package/lib/dedup.js +91 -91
- package/lib/formatter.js +214 -214
- package/lib/interceptor.js +59 -48
- package/lib/scheduler.js +125 -125
- package/package.json +40 -34
- package/providers/index.js +40 -40
- package/providers/mock.js +23 -23
- package/providers/o365.js +164 -164
- package/providers/smtp.js +71 -71
- package/tests/config.test.js +92 -0
- package/tests/dedup.test.js +159 -0
- package/tests/formatter.test.js +90 -0
- package/tests/scheduler.test.js +147 -0
|
@@ -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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/cds-error-outbox)
|
|
10
|
+
[](https://cap.cloud.sap)
|
|
11
|
+
[](./LICENSE)
|
|
12
|
+
[](#)
|
|
13
|
+
|
|
14
|
+
[](#o365--microsoft-graph-api)
|
|
15
|
+
[](#smtp)
|
|
16
|
+
[](#db-entity)
|
|
17
|
+
[](#db-entity)
|
|
18
|
+
[](#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
|