@cap-js/audit-logging 0.8.0 → 0.8.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/CHANGELOG.md +15 -0
- package/README.md +22 -13
- package/lib/access.js +10 -3
- package/lib/modification.js +14 -8
- package/lib/utils.js +50 -25
- package/package.json +4 -4
- package/srv/log2console.js +1 -0
- package/srv/log2restv2.js +8 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
+
## Version 0.8.2 - 2024-11-27
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Erroneous modification log for non-updated key properties
|
|
12
|
+
- Error during non-modifying queries on database level
|
|
13
|
+
- Specify charset UTF-8 for requests to SAP Audit Log Service
|
|
14
|
+
|
|
15
|
+
## Version 0.8.1 - 2024-09-13
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Support for @sap/cds^8.2
|
|
20
|
+
- Reduce clutter in error raised for outbound requests
|
|
21
|
+
|
|
7
22
|
## Version 0.8.0 - 2024-05-24
|
|
8
23
|
|
|
9
24
|
### Added
|
package/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Welcome to @cap-js/audit-logging
|
|
2
|
+
|
|
2
3
|
[](https://api.reuse.software/info/github.com/cap-js/audit-logging)
|
|
3
4
|
|
|
4
5
|
`@cap-js/audit-logging` is a CDS plugin providing integration to the SAP Audit Log service as well as out-of-the-box personal data-related audit logging based on annotations.
|
|
@@ -19,18 +20,22 @@ cd incidents-app
|
|
|
19
20
|
npm i
|
|
20
21
|
```
|
|
21
22
|
|
|
23
|
+
|
|
22
24
|
## Setup
|
|
25
|
+
|
|
23
26
|
To enable audit logging, simply add this self-configuring plugin package to your project:
|
|
24
27
|
|
|
25
28
|
```sh
|
|
26
29
|
npm add @cap-js/audit-logging
|
|
27
30
|
```
|
|
28
31
|
|
|
32
|
+
|
|
29
33
|
## Annotate Personal Data
|
|
34
|
+
|
|
30
35
|
Identify entities and elements (potentially) holding personal data using `@PersonalData` annotations. Create a `db/data-privacy.cds` file and add the following:
|
|
31
36
|
|
|
32
37
|
```cds
|
|
33
|
-
using { sap.capire.incidents as my } from '
|
|
38
|
+
using { sap.capire.incidents as my } from './schema';
|
|
34
39
|
|
|
35
40
|
annotate my.Customers with @PersonalData : {
|
|
36
41
|
DataSubjectRole : 'Customer',
|
|
@@ -69,6 +74,7 @@ Learn more about the annotations in capire:
|
|
|
69
74
|
|
|
70
75
|
|
|
71
76
|
## Test-Drive Locally
|
|
77
|
+
|
|
72
78
|
You've prepared everything to log personal data-related events. Let's see that in action.
|
|
73
79
|
|
|
74
80
|
Start the server as usual:
|
|
@@ -78,36 +84,36 @@ cds watch
|
|
|
78
84
|
|
|
79
85
|
Send an update request that changes personal data:
|
|
80
86
|
```http
|
|
81
|
-
PATCH http://localhost:4004/admin/Customers(
|
|
87
|
+
PATCH http://localhost:4004/odata/v4/admin/Customers('1004155')
|
|
82
88
|
Authorization: Basic alice:in-wonderland
|
|
83
89
|
Content-Type: application/json
|
|
84
90
|
|
|
85
91
|
{
|
|
86
|
-
"firstName": "
|
|
87
|
-
"lastName": "
|
|
92
|
+
"firstName": "Danny",
|
|
93
|
+
"lastName": "Joules"
|
|
88
94
|
}
|
|
89
95
|
```
|
|
90
96
|
|
|
91
97
|
See the audit logs in the server's console output:
|
|
92
98
|
```sh
|
|
93
|
-
{
|
|
99
|
+
[audit-log] - PersonalDataModified: {
|
|
94
100
|
data_subject: {
|
|
95
|
-
|
|
96
|
-
id: { ID: '8e2f2640-6866-4dcf-8f4d-3027aa831cad' },
|
|
101
|
+
id: { ID: '1004155' },
|
|
97
102
|
role: 'Customer',
|
|
103
|
+
type: 'AdminService.Customers'
|
|
98
104
|
},
|
|
99
105
|
object: {
|
|
100
106
|
type: 'AdminService.Customers',
|
|
101
|
-
id: { ID: '
|
|
107
|
+
id: { ID: '1004155' }
|
|
102
108
|
},
|
|
103
109
|
attributes: [
|
|
104
|
-
{ name: 'firstName', old: '
|
|
105
|
-
{ name: 'lastName', old: '
|
|
110
|
+
{ name: 'firstName', old: 'Daniel', new: 'Danny' },
|
|
111
|
+
{ name: 'lastName', old: 'Watts', new: 'Joules' }
|
|
106
112
|
],
|
|
107
|
-
|
|
113
|
+
uuid: '71fa93d9-c993-405f-ba1b-a9ef42668199',
|
|
108
114
|
tenant: 't1',
|
|
109
|
-
|
|
110
|
-
time:
|
|
115
|
+
user: 'alice',
|
|
116
|
+
time: 2023-02-26T08:13:48.287Z
|
|
111
117
|
}
|
|
112
118
|
```
|
|
113
119
|
|
|
@@ -120,14 +126,17 @@ The end-to-end out-of-the-box functionality provided by this plugin requires a p
|
|
|
120
126
|
|
|
121
127
|
[_Learn more about custom audit logging._](https://cap.cloud.sap/docs/guides/data-privacy/audit-logging#custom-audit-logging)
|
|
122
128
|
|
|
129
|
+
|
|
123
130
|
## Support, Feedback, Contributing
|
|
124
131
|
|
|
125
132
|
This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/cap-js/audit-logging/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).
|
|
126
133
|
|
|
134
|
+
|
|
127
135
|
## Code of Conduct
|
|
128
136
|
|
|
129
137
|
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](CODE_OF_CONDUCT.md) at all times.
|
|
130
138
|
|
|
139
|
+
|
|
131
140
|
## Licensing
|
|
132
141
|
|
|
133
142
|
Copyright 2023 SAP SE or an SAP affiliate company and contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/cap-js/audit-logging).
|
package/lib/access.js
CHANGED
|
@@ -2,7 +2,6 @@ const cds = require('@sap/cds')
|
|
|
2
2
|
|
|
3
3
|
// REVISIT: don't require internal stuff
|
|
4
4
|
const getTemplate = require('@sap/cds/libx/_runtime/common/utils/template')
|
|
5
|
-
const templateProcessor = require('@sap/cds/libx/_runtime/common/utils/templateProcessor')
|
|
6
5
|
|
|
7
6
|
const {
|
|
8
7
|
getRootEntity,
|
|
@@ -56,8 +55,16 @@ const auditAccess = async function (data, req) {
|
|
|
56
55
|
if (!template.elements.size) return
|
|
57
56
|
|
|
58
57
|
const accessLogs = {}
|
|
59
|
-
const
|
|
60
|
-
|
|
58
|
+
const processFn = _processorFnAccess(accessLogs, this.model, req)
|
|
59
|
+
// cds internal templating mechanism api changed in 8.2.0 -> polyfill
|
|
60
|
+
if (!template.process) {
|
|
61
|
+
module.exports._templateProcessor ??= require('@sap/cds/libx/_runtime/common/utils/templateProcessor')
|
|
62
|
+
template.process = (data, processFn) => {
|
|
63
|
+
const _data = Array.isArray(data) ? data : [data]
|
|
64
|
+
_data.forEach(row => module.exports._templateProcessor({ processFn, row, template }))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
template.process(data, processFn)
|
|
61
68
|
|
|
62
69
|
for (const each of Object.keys(accessLogs)) if (!accessLogs[each].attributes.length) delete accessLogs[each]
|
|
63
70
|
if (!Object.keys(accessLogs).length) return
|
package/lib/modification.js
CHANGED
|
@@ -2,7 +2,6 @@ const cds = require('@sap/cds')
|
|
|
2
2
|
|
|
3
3
|
// REVISIT: don't require internal stuff
|
|
4
4
|
const getTemplate = require('@sap/cds/libx/_runtime/common/utils/template')
|
|
5
|
-
const templateProcessor = require('@sap/cds/libx/_runtime/common/utils/templateProcessor')
|
|
6
5
|
|
|
7
6
|
const {
|
|
8
7
|
getMapKeyForCurrentRequest,
|
|
@@ -77,7 +76,7 @@ const addDiffToCtx = async function (req) {
|
|
|
77
76
|
if (!_audit.diffs) _audit.diffs = new Map()
|
|
78
77
|
|
|
79
78
|
// get diff
|
|
80
|
-
let diff = await req.diff()
|
|
79
|
+
let diff = (await req.diff()) || {}
|
|
81
80
|
diff = _getDataWithAppliedTransitions(diff, req)
|
|
82
81
|
|
|
83
82
|
// add keys, if necessary
|
|
@@ -89,9 +88,9 @@ const addDiffToCtx = async function (req) {
|
|
|
89
88
|
}
|
|
90
89
|
addDiffToCtx._initial = true
|
|
91
90
|
|
|
92
|
-
const _getOldAndNew = (action, row, key) => {
|
|
91
|
+
const _getOldAndNew = (action, row, key, entity) => {
|
|
93
92
|
let oldValue = action === 'Create' ? null : row._old && row._old[key]
|
|
94
|
-
if (oldValue === undefined) oldValue = null
|
|
93
|
+
if (oldValue === undefined) oldValue = action === 'Update' && key in entity.keys ? row[key] : null
|
|
95
94
|
else if (Array.isArray(oldValue)) oldValue = JSON.stringify(oldValue)
|
|
96
95
|
let newValue = action === 'Delete' ? null : row[key]
|
|
97
96
|
if (newValue === undefined) newValue = null
|
|
@@ -99,9 +98,9 @@ const _getOldAndNew = (action, row, key) => {
|
|
|
99
98
|
return { oldValue, newValue }
|
|
100
99
|
}
|
|
101
100
|
|
|
102
|
-
const _addAttribute = (log, action, row, key) => {
|
|
101
|
+
const _addAttribute = (log, action, row, key, entity) => {
|
|
103
102
|
if (!log.attributes.find(ele => ele.name === key)) {
|
|
104
|
-
const { oldValue, newValue } = _getOldAndNew(action, row, key)
|
|
103
|
+
const { oldValue, newValue } = _getOldAndNew(action, row, key, entity)
|
|
105
104
|
if (oldValue !== newValue) {
|
|
106
105
|
const attr = { name: key }
|
|
107
106
|
if (action !== 'Create') attr.old = oldValue
|
|
@@ -140,7 +139,7 @@ const _processorFnModification = (modificationLogs, model, req, beforeWrite) =>
|
|
|
140
139
|
} else if (category === 'DataSubjectID') {
|
|
141
140
|
addDataSubject(modificationLog, row, key, entity)
|
|
142
141
|
} else if (category === 'IsPotentiallyPersonal' || category === 'IsPotentiallySensitive') {
|
|
143
|
-
_addAttribute(modificationLog, action, row, key)
|
|
142
|
+
_addAttribute(modificationLog, action, row, key, entity)
|
|
144
143
|
// do not log the value of a sensitive attribute
|
|
145
144
|
if (element['@PersonalData.IsPotentiallySensitive']) _maskAttribute(modificationLog.attributes, key)
|
|
146
145
|
}
|
|
@@ -166,7 +165,14 @@ const _getDataModificationLogs = (req, tx, diff, beforeWrite) => {
|
|
|
166
165
|
|
|
167
166
|
const modificationLogs = {}
|
|
168
167
|
const processFn = _processorFnModification(modificationLogs, tx.model, req, beforeWrite)
|
|
169
|
-
|
|
168
|
+
// cds internal templating mechanism api changed in 8.2.0 -> polyfill
|
|
169
|
+
if (!template.process) {
|
|
170
|
+
module.exports._templateProcessor ??= require('@sap/cds/libx/_runtime/common/utils/templateProcessor')
|
|
171
|
+
template.process = (data, processFn) => {
|
|
172
|
+
module.exports._templateProcessor({ processFn, row: data, template })
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
template.process(diff, processFn)
|
|
170
176
|
|
|
171
177
|
return modificationLogs
|
|
172
178
|
}
|
package/lib/utils.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
|
|
1
3
|
const WRITE = { CREATE: 1, UPDATE: 1, DELETE: 1 }
|
|
2
4
|
|
|
3
5
|
const $hasPersonalData = Symbol('@cap-js/audit-logging:hasPersonalData')
|
|
@@ -7,7 +9,7 @@ const $visitedUp = Symbol('@cap-js/audit-logging:visitedUp')
|
|
|
7
9
|
const $visitedDown = Symbol('@cap-js/audit-logging:visitedDown')
|
|
8
10
|
|
|
9
11
|
const hasPersonalData = entity => {
|
|
10
|
-
if (
|
|
12
|
+
if (entity.own($hasPersonalData) == null) {
|
|
11
13
|
if (!entity['@PersonalData.EntitySemantics']) entity.set($hasPersonalData, false)
|
|
12
14
|
else {
|
|
13
15
|
// default role to entity name
|
|
@@ -119,7 +121,17 @@ const _buildSubSelect = (model, { entity, relative, element, next }, row, previo
|
|
|
119
121
|
const targetAlias = _alias(element._target)
|
|
120
122
|
const relativeAlias = _alias(relative)
|
|
121
123
|
|
|
122
|
-
|
|
124
|
+
let w = relative._relations[element.name].join(targetAlias, relativeAlias)
|
|
125
|
+
|
|
126
|
+
// REVISIT: rewrite to path expression, if alias for relative is already used in subselect to avoid sql error
|
|
127
|
+
if (previousCqn?._aliases.has(relativeAlias)) {
|
|
128
|
+
let t
|
|
129
|
+
for (const a in entity.associations) if (entity.associations[a].target === relative.name) t = entity.associations[a]
|
|
130
|
+
if (t && w[0]?.xpr) for (const ele of w[0].xpr) if (ele.ref?.[0] === relativeAlias) ele.ref.splice(0, 1, as, t.name)
|
|
131
|
+
}
|
|
132
|
+
childCqn._aliases = new Set(previousCqn ? [...previousCqn._aliases.values(), as] : [as])
|
|
133
|
+
|
|
134
|
+
childCqn.where(w)
|
|
123
135
|
|
|
124
136
|
if (previousCqn) childCqn.where('exists', previousCqn)
|
|
125
137
|
else childCqn.where(_addKeysToWhere(keys, row, as))
|
|
@@ -145,22 +157,24 @@ const _getDataSubjectIdQuery = ({ dataSubjectEntity, subs }, row, model) => {
|
|
|
145
157
|
}
|
|
146
158
|
|
|
147
159
|
const _getUps = (entity, model) => {
|
|
148
|
-
if (entity.own($parents))
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
160
|
+
if (entity.own($parents) == null) {
|
|
161
|
+
const ups = []
|
|
162
|
+
for (const def of Object.values(model.definitions)) {
|
|
163
|
+
if (def.kind !== 'entity' || !def.associations) continue
|
|
164
|
+
for (const element of Object.values(def.associations)) {
|
|
165
|
+
if (element.target !== entity.name || element._isBacklink || element.name === 'SiblingEntity') continue
|
|
166
|
+
ups.push(element)
|
|
167
|
+
}
|
|
155
168
|
}
|
|
169
|
+
entity.set($parents, ups)
|
|
156
170
|
}
|
|
157
|
-
return entity.
|
|
171
|
+
return entity.own($parents)
|
|
158
172
|
}
|
|
159
173
|
|
|
160
174
|
const _getDataSubjectUp = (root, model, entity, prev, next, result) => {
|
|
161
175
|
for (const element of _getUps(entity, model)) {
|
|
162
176
|
// cycle detection
|
|
163
|
-
if (
|
|
177
|
+
if (element.own($visitedUp) == null) element.set($visitedUp, new Set())
|
|
164
178
|
if (element.own($visitedUp).has(root)) continue
|
|
165
179
|
element.own($visitedUp).add(root)
|
|
166
180
|
|
|
@@ -190,7 +204,7 @@ const _getDataSubjectDown = (root, entity, prev, next) => {
|
|
|
190
204
|
}
|
|
191
205
|
for (const element of associations) {
|
|
192
206
|
// cycle detection
|
|
193
|
-
if (
|
|
207
|
+
if (element.own($visitedDown) == null) element.set($visitedDown, new Set())
|
|
194
208
|
if (element.own($visitedDown).has(root)) continue
|
|
195
209
|
element.own($visitedDown).add(root)
|
|
196
210
|
|
|
@@ -202,12 +216,14 @@ const _getDataSubjectDown = (root, entity, prev, next) => {
|
|
|
202
216
|
}
|
|
203
217
|
|
|
204
218
|
const getDataSubject = (entity, model) => {
|
|
205
|
-
if (entity.own($dataSubject))
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
219
|
+
if (entity.own($dataSubject) == null) {
|
|
220
|
+
// entities with EntitySemantics 'DataSubjectDetails' or 'Other' must not necessarily
|
|
221
|
+
// be always below or always above 'DataSubject' entity in CSN tree
|
|
222
|
+
let dataSubjectInfo = _getDataSubjectUp(entity.name, model, entity)
|
|
223
|
+
if (!dataSubjectInfo) dataSubjectInfo = _getDataSubjectDown(entity.name, entity)
|
|
224
|
+
entity.set($dataSubject, dataSubjectInfo)
|
|
225
|
+
}
|
|
226
|
+
return entity.own($dataSubject)
|
|
211
227
|
}
|
|
212
228
|
|
|
213
229
|
const _getDataSubjectsMap = req => {
|
|
@@ -233,20 +249,29 @@ const addDataSubjectForDetailsEntity = (row, log, req, entity, model) => {
|
|
|
233
249
|
else map.set(role, _getDataSubjectIdQuery(dataSubjectInfo, row, model))
|
|
234
250
|
}
|
|
235
251
|
|
|
236
|
-
const resolveDataSubjects =
|
|
252
|
+
const resolveDataSubjects = (logs, req) => {
|
|
253
|
+
const ps = []
|
|
254
|
+
|
|
237
255
|
const map = _getDataSubjectsMap(req)
|
|
256
|
+
|
|
238
257
|
for (const each of Object.values(logs)) {
|
|
239
258
|
if (each.data_subject.id instanceof cds.ql.Query) {
|
|
240
259
|
const q = each.data_subject.id
|
|
241
|
-
if (map.has(q)) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
map.set(q, res)
|
|
246
|
-
each.data_subject.id = res
|
|
260
|
+
if (!map.has(q)) {
|
|
261
|
+
const p = cds.run(q).then(res => map.set(q, res))
|
|
262
|
+
map.set(q, p)
|
|
263
|
+
ps.push(p)
|
|
247
264
|
}
|
|
248
265
|
}
|
|
249
266
|
}
|
|
267
|
+
|
|
268
|
+
return Promise.all(ps).then(() => {
|
|
269
|
+
for (const each of Object.values(logs)) {
|
|
270
|
+
if (each.data_subject.id instanceof cds.ql.Query) {
|
|
271
|
+
each.data_subject.id = map.get(each.data_subject.id)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
})
|
|
250
275
|
}
|
|
251
276
|
|
|
252
277
|
module.exports = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/audit-logging",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "CDS plugin providing integration to the SAP Audit Log service as well as out-of-the-box personal data-related audit logging based on annotations.",
|
|
5
5
|
"repository": "cap-js/audit-logging",
|
|
6
6
|
"author": "SAP SE (https://www.sap.com)",
|
|
@@ -16,16 +16,16 @@
|
|
|
16
16
|
"lint": "npx eslint .",
|
|
17
17
|
"test": "npm run test-new-db && npm run test-old-db",
|
|
18
18
|
"test-new-db": "CDS_ENV='better-sqlite' npx jest --silent",
|
|
19
|
-
"test-old-db": "npx jest --silent"
|
|
19
|
+
"test-old-db": "CDS_ENV='legacy-sqlite' npx jest --silent"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
|
-
"@sap/cds": ">=7
|
|
22
|
+
"@sap/cds": ">=7"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@cap-js/audit-logging": "file:.",
|
|
26
26
|
"@cap-js/sqlite": "^1",
|
|
27
27
|
"axios": "^1",
|
|
28
|
-
"eslint": "^
|
|
28
|
+
"eslint": "^9",
|
|
29
29
|
"express": "^4",
|
|
30
30
|
"jest": "^29",
|
|
31
31
|
"sqlite3": "^5.1.6"
|
package/srv/log2console.js
CHANGED
package/srv/log2restv2.js
CHANGED
|
@@ -80,7 +80,7 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
async _send(data, path) {
|
|
83
|
-
const headers = { 'content-type': 'application/json' }
|
|
83
|
+
const headers = { 'content-type': 'application/json;charset=utf-8' }
|
|
84
84
|
if (this._vcap) {
|
|
85
85
|
headers.XS_AUDIT_ORG = this._vcap.organization_name
|
|
86
86
|
headers.XS_AUDIT_SPACE = this._vcap.space_name
|
|
@@ -154,12 +154,16 @@ async function _post(url, data, options) {
|
|
|
154
154
|
const chunks = []
|
|
155
155
|
res.on('data', chunk => chunks.push(chunk))
|
|
156
156
|
res.on('end', () => {
|
|
157
|
+
const { statusCode, statusMessage } = res
|
|
157
158
|
let body = Buffer.concat(chunks).toString()
|
|
158
159
|
if (res.headers['content-type']?.match(/json/)) body = JSON.parse(body)
|
|
159
160
|
if (res.statusCode >= 400) {
|
|
160
|
-
|
|
161
|
-
err
|
|
162
|
-
err.
|
|
161
|
+
// prettier-ignore
|
|
162
|
+
const err = new Error(`Request failed with${statusMessage ? `: ${statusCode} - ${statusMessage}` : ` status ${statusCode}`}`)
|
|
163
|
+
err.request = { method: options.method, url, headers: options.headers, body: data }
|
|
164
|
+
if (err.request.headers.authorization)
|
|
165
|
+
err.request.headers.authorization = err.request.headers.authorization.split(' ')[0] + ' ***'
|
|
166
|
+
err.response = { statusCode, statusMessage, headers: res.headers, body }
|
|
163
167
|
reject(err)
|
|
164
168
|
} else {
|
|
165
169
|
resolve(body)
|