soajs.controller 4.3.12 → 4.3.14
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/docs/configuration-custom-registry.md +14 -1
- package/docs/middleware/lastSeen.md +46 -1
- package/mw/keyACL/index.js +1 -1
- package/mw/lastSeen/index.js +71 -31
- package/mw/mt/index.js +10 -5
- package/package.json +1 -1
- package/test/unit/mw/lastSeen/index.js +168 -0
- package/test/unit/utilities/utils.js +13 -0
- package/utilities/utils.js +3 -1
|
@@ -292,11 +292,24 @@ Tracks user activity by notifying a service (typically URAC) of user access.
|
|
|
292
292
|
"serviceName": "urac", // Service to notify
|
|
293
293
|
"serviceVersion": "3", // Service version
|
|
294
294
|
"api": "/user/last/seen", // API endpoint
|
|
295
|
-
"network": "internal"
|
|
295
|
+
"network": "internal", // Optional network identifier
|
|
296
|
+
"include": { // Optional whitelist, omit for all requests
|
|
297
|
+
"connectspaces": true
|
|
298
|
+
},
|
|
299
|
+
"targets": [ // Optional extra APIs, same event
|
|
300
|
+
{
|
|
301
|
+
"serviceName": "authenticator",
|
|
302
|
+
"serviceVersion": "1",
|
|
303
|
+
"api": "/my/device/network",
|
|
304
|
+
"method": "put"
|
|
305
|
+
}
|
|
306
|
+
]
|
|
296
307
|
}
|
|
297
308
|
}
|
|
298
309
|
```
|
|
299
310
|
|
|
311
|
+
See `docs/middleware/lastSeen.md` for the `include` matching rules and the target fields.
|
|
312
|
+
|
|
300
313
|
| Field | Type | Default | Description |
|
|
301
314
|
|-------|------|---------|-------------|
|
|
302
315
|
| `active` | boolean | `false` | Enable/disable activity tracking |
|
|
@@ -69,7 +69,18 @@ Located in `registry.custom.gateway.value.lastSeen`:
|
|
|
69
69
|
"serviceName": "urac", // Target service
|
|
70
70
|
"serviceVersion": "3", // Service version
|
|
71
71
|
"api": "/user/last/seen", // API endpoint
|
|
72
|
-
"network": "internal"
|
|
72
|
+
"network": "internal", // Optional network identifier
|
|
73
|
+
"include": { // Optional whitelist, omit to trigger on everything
|
|
74
|
+
"connectspaces": true
|
|
75
|
+
},
|
|
76
|
+
"targets": [ // Optional extra APIs notified on the same event
|
|
77
|
+
{
|
|
78
|
+
"serviceName": "authenticator",
|
|
79
|
+
"serviceVersion": "1",
|
|
80
|
+
"api": "/my/device/network",
|
|
81
|
+
"method": "put"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
73
84
|
}
|
|
74
85
|
}
|
|
75
86
|
```
|
|
@@ -81,6 +92,40 @@ Located in `registry.custom.gateway.value.lastSeen`:
|
|
|
81
92
|
| `serviceVersion` | string | `"3"` | Service version |
|
|
82
93
|
| `api` | string | `"/user/last/seen"` | API endpoint |
|
|
83
94
|
| `network` | string | - | Network identifier in request body |
|
|
95
|
+
| `include` | object | - | Whitelist of services and APIs, omit to trigger on every request |
|
|
96
|
+
| `targets` | array | - | Extra APIs notified on the same event |
|
|
97
|
+
|
|
98
|
+
### include
|
|
99
|
+
|
|
100
|
+
Decides whether a request counts as activity. Omitting it triggers on every request.
|
|
101
|
+
|
|
102
|
+
| Form | Meaning |
|
|
103
|
+
|------|---------|
|
|
104
|
+
| service missing from `include` | never triggers |
|
|
105
|
+
| `"service": true` | every API, every method |
|
|
106
|
+
| `"service": {"apis": {"/path": true}}` | that API, every method |
|
|
107
|
+
| `"service": {"apis": {"/path": ["get","put"]}}` | that API, listed methods only |
|
|
108
|
+
| `"apis": {"*": ...}` | any API, checked before the exact paths |
|
|
109
|
+
|
|
110
|
+
API keys accept the service relative path (`/active`) or the full path
|
|
111
|
+
(`/connectspaces/active`), and support path params (`/user/:id`).
|
|
112
|
+
|
|
113
|
+
### targets
|
|
114
|
+
|
|
115
|
+
Extra APIs notified whenever the main one is. They are not filtered on their own, the
|
|
116
|
+
`include` above has already decided that the request counts as activity, so a target is
|
|
117
|
+
notified on exactly the same set of requests.
|
|
118
|
+
|
|
119
|
+
| Field | Type | Default | Description |
|
|
120
|
+
|-------|------|---------|-------------|
|
|
121
|
+
| `serviceName` | string | required | Service to notify, must be in the registry |
|
|
122
|
+
| `api` | string | required | API endpoint |
|
|
123
|
+
| `serviceVersion` | string | - | Service version, resolved by awareness when absent |
|
|
124
|
+
| `method` | string | `"post"` | HTTP method |
|
|
125
|
+
|
|
126
|
+
Each target receives the same body and the same `soajsinjectobj` header as the main
|
|
127
|
+
notification. A target that is down, unknown to the registry, or missing `serviceName`
|
|
128
|
+
or `api` is skipped without affecting the others.
|
|
84
129
|
|
|
85
130
|
## Request to URAC
|
|
86
131
|
|
package/mw/keyACL/index.js
CHANGED
|
@@ -48,7 +48,7 @@ module.exports = () => {
|
|
|
48
48
|
if (obj.service_v) {
|
|
49
49
|
let san_v = coreLibs.version.sanitize(obj.service_v);
|
|
50
50
|
if (!aclObj[san_v]) {
|
|
51
|
-
|
|
51
|
+
//NOTE: the error context already reports the service and the version
|
|
52
52
|
return cb(154, null);
|
|
53
53
|
}
|
|
54
54
|
aclObj = aclObj[san_v];
|
package/mw/lastSeen/index.js
CHANGED
|
@@ -143,6 +143,56 @@ function isMethodMatch(methodConfig, method) {
|
|
|
143
143
|
return false;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Fire and forget the notification at one target.
|
|
148
|
+
*
|
|
149
|
+
* NOTE: the request is not awaited, a target that is down or slow must not affect
|
|
150
|
+
* the original request, which has already been handed to the next middleware.
|
|
151
|
+
*
|
|
152
|
+
* @param req
|
|
153
|
+
* @param lastSeen the custom registry block, carries the shared body
|
|
154
|
+
* @param item {name, version, api, method}
|
|
155
|
+
*/
|
|
156
|
+
function notifyTarget(req, lastSeen, item) {
|
|
157
|
+
//NOTE: getHost resolves against the registry, an unknown service would throw on
|
|
158
|
+
// the port lookup below and take the remaining targets down with it
|
|
159
|
+
if (!req.soajs.registry || !req.soajs.registry.services ||
|
|
160
|
+
!req.soajs.registry.services[item.name] || !req.soajs.registry.services[item.name].port) {
|
|
161
|
+
req.soajs.log.debug("lastSeen failed, service not in registry [" + item.name + "@" + item.version + " " + item.api + "]");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
req.soajs.awareness.getHost(item.name, item.version, (host) => {
|
|
165
|
+
if (!host) {
|
|
166
|
+
req.soajs.log.debug("lastSeen failed with no host for [" + item.name + "@" + item.version + " " + item.api + "]");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
item.host = host;
|
|
170
|
+
item.port = req.soajs.registry.services[item.name].port;
|
|
171
|
+
|
|
172
|
+
let options = {
|
|
173
|
+
"uri": "http://" + item.host + ":" + item.port + item.api,
|
|
174
|
+
"json": true,
|
|
175
|
+
"method": item.method || "POST"
|
|
176
|
+
};
|
|
177
|
+
if (lastSeen.network) {
|
|
178
|
+
options.body = { "network": lastSeen.network };
|
|
179
|
+
}
|
|
180
|
+
if (req.headers && req.headers.soajsinjectobj) {
|
|
181
|
+
options.headers = { "soajsinjectobj": req.headers.soajsinjectobj };
|
|
182
|
+
}
|
|
183
|
+
httpRequest(options)
|
|
184
|
+
.then(() => { })
|
|
185
|
+
.catch(({ error, body }) => {
|
|
186
|
+
if (error) {
|
|
187
|
+
req.soajs.log.debug("lastSeen failed for [" + item.name + "@" + item.version + " " + item.api + "]");
|
|
188
|
+
req.soajs.log.debug("lastSeen " + error.message);
|
|
189
|
+
} else {
|
|
190
|
+
req.soajs.log.debug("lastSeen ", body);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
146
196
|
/**
|
|
147
197
|
*
|
|
148
198
|
* @param configuration
|
|
@@ -180,39 +230,29 @@ module.exports = () => {
|
|
|
180
230
|
if (uracObj) {
|
|
181
231
|
userId = uracObj._id;
|
|
182
232
|
}
|
|
183
|
-
if (userId) {
|
|
184
|
-
req.soajs.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
options.headers = { "soajsinjectobj": req.headers.soajsinjectobj };
|
|
199
|
-
}
|
|
200
|
-
httpRequest(options)
|
|
201
|
-
.then(() => { })
|
|
202
|
-
.catch(({ error, body }) => {
|
|
203
|
-
if (error) {
|
|
204
|
-
req.soajs.log.debug("lastSeen failed for [" + item.name + "@" + item.version + " " + item.api + "]");
|
|
205
|
-
req.soajs.log.debug("lastSeen " + error.message);
|
|
206
|
-
} else {
|
|
207
|
-
req.soajs.log.debug("lastSeen ", body);
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
} else {
|
|
211
|
-
req.soajs.log.debug("lastSeen failed with no host for [" + item.name + "@" + item.version + " " + item.api + "]");
|
|
233
|
+
if (!userId) {
|
|
234
|
+
req.soajs.log.debug("lastSeen failed with no userId for [" + item.name + "@" + item.version + " " + item.api + "]");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
notifyTarget(req, lastSeen, item);
|
|
239
|
+
|
|
240
|
+
//NOTE: additional targets are notified on the same event, they are not
|
|
241
|
+
// filtered on their own, the include above already decided that this
|
|
242
|
+
// request counts as activity
|
|
243
|
+
if (Array.isArray(lastSeen.targets)) {
|
|
244
|
+
lastSeen.targets.forEach((target) => {
|
|
245
|
+
if (!target || !target.serviceName || !target.api) {
|
|
246
|
+
req.soajs.log.debug("lastSeen target skipped, serviceName and api are required");
|
|
247
|
+
return;
|
|
212
248
|
}
|
|
249
|
+
notifyTarget(req, lastSeen, {
|
|
250
|
+
"name": target.serviceName,
|
|
251
|
+
"version": target.serviceVersion || null,
|
|
252
|
+
"api": target.api,
|
|
253
|
+
"method": target.method
|
|
254
|
+
});
|
|
213
255
|
});
|
|
214
|
-
} else {
|
|
215
|
-
req.soajs.log.debug("lastSeen failed with no userId for [" + item.name + "@" + item.version + " " + item.api + "]");
|
|
216
256
|
}
|
|
217
257
|
}
|
|
218
258
|
}
|
package/mw/mt/index.js
CHANGED
|
@@ -48,7 +48,7 @@ module.exports = (configuration) => {
|
|
|
48
48
|
serviceInfo = req.soajs.controller.serviceParams.registry.versions[req.soajs.controller.serviceParams.version];
|
|
49
49
|
}
|
|
50
50
|
if (!serviceInfo) {
|
|
51
|
-
|
|
51
|
+
//NOTE: the error context already reports the service, the api and the version
|
|
52
52
|
return next(133);
|
|
53
53
|
}
|
|
54
54
|
let oauth = true;
|
|
@@ -160,12 +160,16 @@ module.exports = (configuration) => {
|
|
|
160
160
|
//if this is controller route: /key/permission/get, ignore async waterfall response
|
|
161
161
|
if (url_keyACL) {
|
|
162
162
|
if (!req.soajs.uracDriver) {
|
|
163
|
-
if (err && err.message) {
|
|
164
|
-
req.soajs.log.error(err.message);
|
|
165
|
-
}
|
|
166
163
|
if (err && err.name === "OAuth2Error") {
|
|
164
|
+
//NOTE: the code and the message travel with the error, the
|
|
165
|
+
// error context reports both with full attribution
|
|
167
166
|
return next(err);
|
|
168
167
|
}
|
|
168
|
+
//NOTE: the cause is replaced by 158 below, so it is only
|
|
169
|
+
// recoverable from here
|
|
170
|
+
if (err && err.message) {
|
|
171
|
+
req.soajs.log.error(err.message);
|
|
172
|
+
}
|
|
169
173
|
//doesn't work if you are not logged in
|
|
170
174
|
return next(158);
|
|
171
175
|
} else {
|
|
@@ -180,7 +184,8 @@ module.exports = (configuration) => {
|
|
|
180
184
|
}
|
|
181
185
|
|
|
182
186
|
if (err) {
|
|
183
|
-
|
|
187
|
+
//NOTE: the error context already reports the service, the api and
|
|
188
|
+
// the version alongside the code and the message
|
|
184
189
|
return next(err);
|
|
185
190
|
} else {
|
|
186
191
|
let serviceName = data.req.soajs.controller.serviceParams.name;
|
package/package.json
CHANGED
|
@@ -1009,4 +1009,172 @@ describe("Unit test for: mw - lastSeen", () => {
|
|
|
1009
1009
|
|
|
1010
1010
|
});
|
|
1011
1011
|
|
|
1012
|
+
describe("extra targets tests", () => {
|
|
1013
|
+
|
|
1014
|
+
let buildReq = (lastSeen) => {
|
|
1015
|
+
return {
|
|
1016
|
+
method: "GET",
|
|
1017
|
+
getClientIP: () => {
|
|
1018
|
+
return "127.0.0.1";
|
|
1019
|
+
},
|
|
1020
|
+
soajs: {
|
|
1021
|
+
log: {
|
|
1022
|
+
debug: () => {
|
|
1023
|
+
},
|
|
1024
|
+
error: () => {
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
uracDriver: {
|
|
1028
|
+
getProfile: () => {
|
|
1029
|
+
return { _id: "1111111111" };
|
|
1030
|
+
}
|
|
1031
|
+
},
|
|
1032
|
+
awareness: {
|
|
1033
|
+
getHost: (name, version, cb) => {
|
|
1034
|
+
return cb(name + ".fake");
|
|
1035
|
+
}
|
|
1036
|
+
},
|
|
1037
|
+
controller: {
|
|
1038
|
+
serviceParams: {
|
|
1039
|
+
name: "connectspaces",
|
|
1040
|
+
path: "/active",
|
|
1041
|
+
parsedUrl: { pathname: "/connectspaces/active" }
|
|
1042
|
+
}
|
|
1043
|
+
},
|
|
1044
|
+
registry: {
|
|
1045
|
+
services: {
|
|
1046
|
+
urac: { port: 4001 },
|
|
1047
|
+
authenticator: { port: 4002 }
|
|
1048
|
+
},
|
|
1049
|
+
custom: {
|
|
1050
|
+
gateway: {
|
|
1051
|
+
value: { lastSeen: lastSeen }
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
it("notifies the extra target alongside urac, honouring its method", (done) => {
|
|
1060
|
+
let req = buildReq({
|
|
1061
|
+
active: true,
|
|
1062
|
+
network: "YAYA",
|
|
1063
|
+
targets: [{
|
|
1064
|
+
serviceName: "authenticator",
|
|
1065
|
+
serviceVersion: "1",
|
|
1066
|
+
api: "/my/device/network",
|
|
1067
|
+
method: "put"
|
|
1068
|
+
}]
|
|
1069
|
+
});
|
|
1070
|
+
const uracScope = nock('http://urac.fake:4001')
|
|
1071
|
+
.post('/user/last/seen', "{\"network\":\"YAYA\"}")
|
|
1072
|
+
.reply(200, { result: true });
|
|
1073
|
+
const authScope = nock('http://authenticator.fake:4002')
|
|
1074
|
+
.put('/my/device/network', "{\"network\":\"YAYA\"}")
|
|
1075
|
+
.reply(200, { result: true });
|
|
1076
|
+
|
|
1077
|
+
let functionMw = mw({});
|
|
1078
|
+
functionMw(req, {}, (error) => {
|
|
1079
|
+
assert.ifError(error);
|
|
1080
|
+
});
|
|
1081
|
+
setTimeout(() => {
|
|
1082
|
+
assert.strictEqual(uracScope.isDone(), true, "urac was not notified");
|
|
1083
|
+
assert.strictEqual(authScope.isDone(), true, "the extra target was not notified");
|
|
1084
|
+
done();
|
|
1085
|
+
}, 100);
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
it("defaults the extra target to post when no method is set", (done) => {
|
|
1089
|
+
let req = buildReq({
|
|
1090
|
+
active: true,
|
|
1091
|
+
targets: [{
|
|
1092
|
+
serviceName: "authenticator",
|
|
1093
|
+
serviceVersion: "1",
|
|
1094
|
+
api: "/my/device/network"
|
|
1095
|
+
}]
|
|
1096
|
+
});
|
|
1097
|
+
nock('http://urac.fake:4001').post('/user/last/seen').reply(200, { result: true });
|
|
1098
|
+
const authScope = nock('http://authenticator.fake:4002')
|
|
1099
|
+
.post('/my/device/network')
|
|
1100
|
+
.reply(200, { result: true });
|
|
1101
|
+
|
|
1102
|
+
let functionMw = mw({});
|
|
1103
|
+
functionMw(req, {}, () => {
|
|
1104
|
+
});
|
|
1105
|
+
setTimeout(() => {
|
|
1106
|
+
assert.strictEqual(authScope.isDone(), true, "the extra target was not posted to");
|
|
1107
|
+
done();
|
|
1108
|
+
}, 100);
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it("still notifies urac when a target names a service that is not in the registry", (done) => {
|
|
1112
|
+
let req = buildReq({
|
|
1113
|
+
active: true,
|
|
1114
|
+
targets: [{
|
|
1115
|
+
serviceName: "doesnotexist",
|
|
1116
|
+
serviceVersion: "1",
|
|
1117
|
+
api: "/nope"
|
|
1118
|
+
}]
|
|
1119
|
+
});
|
|
1120
|
+
const uracScope = nock('http://urac.fake:4001')
|
|
1121
|
+
.post('/user/last/seen')
|
|
1122
|
+
.reply(200, { result: true });
|
|
1123
|
+
|
|
1124
|
+
let functionMw = mw({});
|
|
1125
|
+
assert.doesNotThrow(() => {
|
|
1126
|
+
functionMw(req, {}, () => {
|
|
1127
|
+
});
|
|
1128
|
+
});
|
|
1129
|
+
setTimeout(() => {
|
|
1130
|
+
assert.strictEqual(uracScope.isDone(), true, "urac was not notified");
|
|
1131
|
+
done();
|
|
1132
|
+
}, 100);
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
it("skips a malformed target and still notifies urac", (done) => {
|
|
1136
|
+
let req = buildReq({
|
|
1137
|
+
active: true,
|
|
1138
|
+
targets: [{ serviceVersion: "1" }, null]
|
|
1139
|
+
});
|
|
1140
|
+
const uracScope = nock('http://urac.fake:4001')
|
|
1141
|
+
.post('/user/last/seen')
|
|
1142
|
+
.reply(200, { result: true });
|
|
1143
|
+
|
|
1144
|
+
let functionMw = mw({});
|
|
1145
|
+
functionMw(req, {}, () => {
|
|
1146
|
+
});
|
|
1147
|
+
setTimeout(() => {
|
|
1148
|
+
assert.strictEqual(uracScope.isDone(), true, "urac was not notified");
|
|
1149
|
+
done();
|
|
1150
|
+
}, 100);
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
it("does not notify any target when the include filter excludes the request", (done) => {
|
|
1154
|
+
let req = buildReq({
|
|
1155
|
+
active: true,
|
|
1156
|
+
include: { "someotherservice": true },
|
|
1157
|
+
targets: [{
|
|
1158
|
+
serviceName: "authenticator",
|
|
1159
|
+
serviceVersion: "1",
|
|
1160
|
+
api: "/my/device/network",
|
|
1161
|
+
method: "put"
|
|
1162
|
+
}]
|
|
1163
|
+
});
|
|
1164
|
+
const uracScope = nock('http://urac.fake:4001').post('/user/last/seen').reply(200, {});
|
|
1165
|
+
const authScope = nock('http://authenticator.fake:4002').put('/my/device/network').reply(200, {});
|
|
1166
|
+
|
|
1167
|
+
let functionMw = mw({});
|
|
1168
|
+
functionMw(req, {}, () => {
|
|
1169
|
+
});
|
|
1170
|
+
setTimeout(() => {
|
|
1171
|
+
assert.strictEqual(uracScope.isDone(), false, "urac should have been skipped");
|
|
1172
|
+
assert.strictEqual(authScope.isDone(), false, "the extra target should have been skipped");
|
|
1173
|
+
nock.cleanAll();
|
|
1174
|
+
done();
|
|
1175
|
+
}, 100);
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1012
1180
|
});
|
|
@@ -146,6 +146,19 @@ describe("Testing utilities", () => {
|
|
|
146
146
|
done();
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
+
it("errorContext - falls back to service_n and service_v before gotoService resolves them", (done) => {
|
|
150
|
+
// key and keyACL run before gotoService, so serviceParams carries only the
|
|
151
|
+
// raw service_n / service_v at that point
|
|
152
|
+
let r = makeReq(true);
|
|
153
|
+
r.soajs.controller.serviceParams = { "service_n": "urac", "service_v": "3" };
|
|
154
|
+
utils.controllerErrorHandler(154, r, res, null);
|
|
155
|
+
|
|
156
|
+
let ctx = JSON.parse(logged.error[0]);
|
|
157
|
+
assert.strictEqual(ctx.service, "urac");
|
|
158
|
+
assert.strictEqual(ctx.version, "3");
|
|
159
|
+
done();
|
|
160
|
+
});
|
|
161
|
+
|
|
149
162
|
it("logErrors - string error", (done) => {
|
|
150
163
|
utils.logErrors("error", req, res, (error) => {
|
|
151
164
|
done();
|
package/utilities/utils.js
CHANGED
|
@@ -51,8 +51,10 @@ function errorContext(err, req) {
|
|
|
51
51
|
"code": code,
|
|
52
52
|
"msg": msg,
|
|
53
53
|
"method": req.method,
|
|
54
|
+
//NOTE: name and version are resolved by gotoService, the service_n and service_v
|
|
55
|
+
// fallbacks cover the middleware that runs before it, ex: key and keyACL
|
|
54
56
|
"service": serviceParams.name || serviceParams.service_n || null,
|
|
55
|
-
"version": serviceParams.version || null,
|
|
57
|
+
"version": serviceParams.version || serviceParams.service_v || null,
|
|
56
58
|
"api": (serviceParams.parsedUrl && serviceParams.parsedUrl.pathname) || null,
|
|
57
59
|
"url": redactUrl(req.url),
|
|
58
60
|
"ip": req.getClientIP ? req.getClientIP() : null,
|