@unito/integration-sdk 0.1.9 → 0.1.11
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/dist/src/handler.d.ts +5 -6
- package/dist/src/handler.js +9 -0
- package/dist/src/index.cjs +30 -29
- package/dist/src/index.d.ts +1 -0
- package/dist/src/integration.js +3 -18
- package/dist/src/middlewares/secrets.d.ts +13 -0
- package/dist/src/middlewares/secrets.js +17 -0
- package/dist/src/resources/cache.d.ts +0 -1
- package/dist/src/resources/cache.js +0 -9
- package/dist/src/resources/context.d.ts +8 -1
- package/dist/src/resources/provider.d.ts +3 -3
- package/dist/test/middlewares/secrets.test.d.ts +1 -0
- package/dist/test/middlewares/secrets.test.js +28 -0
- package/dist/test/resources/cache.test.js +2 -2
- package/package.json +1 -1
- package/src/handler.ts +14 -5
- package/src/index.ts +1 -0
- package/src/integration.ts +3 -21
- package/src/middlewares/secrets.ts +35 -0
- package/src/resources/cache.ts +0 -12
- package/src/resources/context.ts +8 -1
- package/src/resources/provider.ts +3 -3
- package/test/middlewares/secrets.test.ts +39 -0
- package/test/resources/cache.test.ts +2 -2
package/dist/src/handler.d.ts
CHANGED
|
@@ -37,23 +37,23 @@ export type UpdateWebhookSubscriptionsHandler = (context: UpdateWebhookSubscript
|
|
|
37
37
|
* Handler called to acknowledge the reception of a webhook.
|
|
38
38
|
*/
|
|
39
39
|
export type AcknowledgeWebhooksHandler = (context: AcknowledgeWebhooksContext<any, any, any>) => Promise<API.WebhookAcknowledgeResponsePayload>;
|
|
40
|
-
type ItemHandlers = {
|
|
40
|
+
export type ItemHandlers = {
|
|
41
41
|
getItem?: GetItemHandler;
|
|
42
42
|
getCollection?: GetCollectionHandler;
|
|
43
43
|
createItem?: CreateItemHandler;
|
|
44
44
|
updateItem?: UpdateItemHandler;
|
|
45
45
|
deleteItem?: DeleteItemHandler;
|
|
46
46
|
};
|
|
47
|
-
type CredentialAccountHandlers = {
|
|
47
|
+
export type CredentialAccountHandlers = {
|
|
48
48
|
getCredentialAccount: GetCredentialAccountHandler;
|
|
49
49
|
};
|
|
50
|
-
type ParseWebhookHandlers = {
|
|
50
|
+
export type ParseWebhookHandlers = {
|
|
51
51
|
parseWebhooks: ParseWebhooksHandler;
|
|
52
52
|
};
|
|
53
|
-
type WebhookSubscriptionHandlers = {
|
|
53
|
+
export type WebhookSubscriptionHandlers = {
|
|
54
54
|
updateWebhookSubscriptions: UpdateWebhookSubscriptionsHandler;
|
|
55
55
|
};
|
|
56
|
-
type AcknowledgeWebhookHandlers = {
|
|
56
|
+
export type AcknowledgeWebhookHandlers = {
|
|
57
57
|
acknowledgeWebhooks: AcknowledgeWebhooksHandler;
|
|
58
58
|
};
|
|
59
59
|
export type HandlersInput = ItemHandlers | CredentialAccountHandlers | ParseWebhookHandlers | WebhookSubscriptionHandlers | AcknowledgeWebhookHandlers;
|
|
@@ -64,4 +64,3 @@ export declare class Handler {
|
|
|
64
64
|
constructor(inputPath: string, handlers: HandlersInput);
|
|
65
65
|
generate(): Router;
|
|
66
66
|
}
|
|
67
|
-
export {};
|
package/dist/src/handler.js
CHANGED
|
@@ -90,6 +90,7 @@ export class Handler {
|
|
|
90
90
|
}
|
|
91
91
|
const collection = await handler({
|
|
92
92
|
credentials: res.locals.credentials,
|
|
93
|
+
secrets: res.locals.secrets,
|
|
93
94
|
selects: res.locals.selects,
|
|
94
95
|
filters: res.locals.filters,
|
|
95
96
|
logger: res.locals.logger,
|
|
@@ -109,6 +110,7 @@ export class Handler {
|
|
|
109
110
|
assertCreateItemRequestPayload(req.body);
|
|
110
111
|
const createItemSummary = await handler({
|
|
111
112
|
credentials: res.locals.credentials,
|
|
113
|
+
secrets: res.locals.secrets,
|
|
112
114
|
body: req.body,
|
|
113
115
|
logger: res.locals.logger,
|
|
114
116
|
params: req.params,
|
|
@@ -126,6 +128,7 @@ export class Handler {
|
|
|
126
128
|
}
|
|
127
129
|
const item = await handler({
|
|
128
130
|
credentials: res.locals.credentials,
|
|
131
|
+
secrets: res.locals.secrets,
|
|
129
132
|
logger: res.locals.logger,
|
|
130
133
|
params: req.params,
|
|
131
134
|
query: req.query,
|
|
@@ -143,6 +146,7 @@ export class Handler {
|
|
|
143
146
|
assertUpdateItemRequestPayload(req.body);
|
|
144
147
|
const item = await handler({
|
|
145
148
|
credentials: res.locals.credentials,
|
|
149
|
+
secrets: res.locals.secrets,
|
|
146
150
|
body: req.body,
|
|
147
151
|
logger: res.locals.logger,
|
|
148
152
|
params: req.params,
|
|
@@ -160,6 +164,7 @@ export class Handler {
|
|
|
160
164
|
}
|
|
161
165
|
await handler({
|
|
162
166
|
credentials: res.locals.credentials,
|
|
167
|
+
secrets: res.locals.secrets,
|
|
163
168
|
logger: res.locals.logger,
|
|
164
169
|
params: req.params,
|
|
165
170
|
query: req.query,
|
|
@@ -176,6 +181,7 @@ export class Handler {
|
|
|
176
181
|
}
|
|
177
182
|
const credentialAccount = await handler({
|
|
178
183
|
credentials: res.locals.credentials,
|
|
184
|
+
secrets: res.locals.secrets,
|
|
179
185
|
logger: res.locals.logger,
|
|
180
186
|
params: req.params,
|
|
181
187
|
query: req.query,
|
|
@@ -189,6 +195,7 @@ export class Handler {
|
|
|
189
195
|
router.post(this.pathWithIdentifier, async (req, res) => {
|
|
190
196
|
assertWebhookParseRequestPayload(req.body);
|
|
191
197
|
const response = await handler({
|
|
198
|
+
secrets: res.locals.secrets,
|
|
192
199
|
logger: res.locals.logger,
|
|
193
200
|
params: req.params,
|
|
194
201
|
query: req.query,
|
|
@@ -203,6 +210,7 @@ export class Handler {
|
|
|
203
210
|
router.post(this.pathWithIdentifier, async (req, res) => {
|
|
204
211
|
assertWebhookParseRequestPayload(req.body);
|
|
205
212
|
const response = await handler({
|
|
213
|
+
secrets: res.locals.secrets,
|
|
206
214
|
logger: res.locals.logger,
|
|
207
215
|
params: req.params,
|
|
208
216
|
query: req.query,
|
|
@@ -220,6 +228,7 @@ export class Handler {
|
|
|
220
228
|
}
|
|
221
229
|
assertWebhookSubscriptionRequestPayload(req.body);
|
|
222
230
|
const response = await handler({
|
|
231
|
+
secrets: res.locals.secrets,
|
|
223
232
|
credentials: res.locals.credentials,
|
|
224
233
|
body: req.body,
|
|
225
234
|
logger: res.locals.logger,
|
package/dist/src/index.cjs
CHANGED
|
@@ -118,13 +118,6 @@ class Logger {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
/**
|
|
122
|
-
* Array of created caches kept to allow for graceful shutdown on exit signals.
|
|
123
|
-
*/
|
|
124
|
-
const caches = [];
|
|
125
|
-
const shutdownCaches = async () => {
|
|
126
|
-
return Promise.allSettled(caches.map(cache => cache.quit()));
|
|
127
|
-
};
|
|
128
121
|
class Cache {
|
|
129
122
|
cacheInstance;
|
|
130
123
|
constructor(cacheInstance) {
|
|
@@ -153,8 +146,6 @@ class Cache {
|
|
|
153
146
|
*/
|
|
154
147
|
static create(redisUrl) {
|
|
155
148
|
const cacheInstance = redisUrl ? new cachette.WriteThroughCache(redisUrl) : new cachette.LocalCache();
|
|
156
|
-
// Push to the array of caches for graceful shutdown on exit signals.
|
|
157
|
-
caches.push(cacheInstance);
|
|
158
149
|
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
159
150
|
const correlationId = uuid__namespace.v4();
|
|
160
151
|
const logger = new Logger({ correlation_id: correlationId });
|
|
@@ -265,13 +256,13 @@ function buildHttpError(responseStatus, message) {
|
|
|
265
256
|
return httpError;
|
|
266
257
|
}
|
|
267
258
|
|
|
268
|
-
const middleware$
|
|
259
|
+
const middleware$7 = (req, res, next) => {
|
|
269
260
|
res.locals.correlationId = req.header('X-Unito-Correlation-Id') ?? uuid__namespace.v4();
|
|
270
261
|
next();
|
|
271
262
|
};
|
|
272
263
|
|
|
273
264
|
const ADDITIONAL_CONTEXT_HEADER = 'X-Unito-Additional-Logging-Context';
|
|
274
|
-
const middleware$
|
|
265
|
+
const middleware$6 = (req, res, next) => {
|
|
275
266
|
const logger = new Logger({ correlation_id: res.locals.correlationId });
|
|
276
267
|
res.locals.logger = logger;
|
|
277
268
|
const rawAdditionalContext = req.header(ADDITIONAL_CONTEXT_HEADER);
|
|
@@ -288,7 +279,7 @@ const middleware$5 = (req, res, next) => {
|
|
|
288
279
|
};
|
|
289
280
|
|
|
290
281
|
const CREDENTIALS_HEADER = 'X-Unito-Credentials';
|
|
291
|
-
const middleware$
|
|
282
|
+
const middleware$5 = (req, res, next) => {
|
|
292
283
|
const credentialsHeader = req.header(CREDENTIALS_HEADER);
|
|
293
284
|
if (credentialsHeader) {
|
|
294
285
|
let credentials;
|
|
@@ -303,6 +294,22 @@ const middleware$4 = (req, res, next) => {
|
|
|
303
294
|
next();
|
|
304
295
|
};
|
|
305
296
|
|
|
297
|
+
const SECRETS_HEADER = 'X-Unito-Secrets';
|
|
298
|
+
const middleware$4 = (req, res, next) => {
|
|
299
|
+
const secretsHeader = req.header(SECRETS_HEADER);
|
|
300
|
+
if (secretsHeader) {
|
|
301
|
+
let secrets;
|
|
302
|
+
try {
|
|
303
|
+
secrets = JSON.parse(Buffer.from(secretsHeader, 'base64').toString('utf8'));
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
throw new BadRequestError(`Malformed HTTP header ${SECRETS_HEADER}`);
|
|
307
|
+
}
|
|
308
|
+
res.locals.secrets = secrets;
|
|
309
|
+
}
|
|
310
|
+
next();
|
|
311
|
+
};
|
|
312
|
+
|
|
306
313
|
const middleware$3 = (req, res, next) => {
|
|
307
314
|
const rawSelect = req.query.select;
|
|
308
315
|
if (typeof rawSelect === 'string') {
|
|
@@ -480,6 +487,7 @@ class Handler {
|
|
|
480
487
|
}
|
|
481
488
|
const collection = await handler({
|
|
482
489
|
credentials: res.locals.credentials,
|
|
490
|
+
secrets: res.locals.secrets,
|
|
483
491
|
selects: res.locals.selects,
|
|
484
492
|
filters: res.locals.filters,
|
|
485
493
|
logger: res.locals.logger,
|
|
@@ -499,6 +507,7 @@ class Handler {
|
|
|
499
507
|
assertCreateItemRequestPayload(req.body);
|
|
500
508
|
const createItemSummary = await handler({
|
|
501
509
|
credentials: res.locals.credentials,
|
|
510
|
+
secrets: res.locals.secrets,
|
|
502
511
|
body: req.body,
|
|
503
512
|
logger: res.locals.logger,
|
|
504
513
|
params: req.params,
|
|
@@ -516,6 +525,7 @@ class Handler {
|
|
|
516
525
|
}
|
|
517
526
|
const item = await handler({
|
|
518
527
|
credentials: res.locals.credentials,
|
|
528
|
+
secrets: res.locals.secrets,
|
|
519
529
|
logger: res.locals.logger,
|
|
520
530
|
params: req.params,
|
|
521
531
|
query: req.query,
|
|
@@ -533,6 +543,7 @@ class Handler {
|
|
|
533
543
|
assertUpdateItemRequestPayload(req.body);
|
|
534
544
|
const item = await handler({
|
|
535
545
|
credentials: res.locals.credentials,
|
|
546
|
+
secrets: res.locals.secrets,
|
|
536
547
|
body: req.body,
|
|
537
548
|
logger: res.locals.logger,
|
|
538
549
|
params: req.params,
|
|
@@ -550,6 +561,7 @@ class Handler {
|
|
|
550
561
|
}
|
|
551
562
|
await handler({
|
|
552
563
|
credentials: res.locals.credentials,
|
|
564
|
+
secrets: res.locals.secrets,
|
|
553
565
|
logger: res.locals.logger,
|
|
554
566
|
params: req.params,
|
|
555
567
|
query: req.query,
|
|
@@ -566,6 +578,7 @@ class Handler {
|
|
|
566
578
|
}
|
|
567
579
|
const credentialAccount = await handler({
|
|
568
580
|
credentials: res.locals.credentials,
|
|
581
|
+
secrets: res.locals.secrets,
|
|
569
582
|
logger: res.locals.logger,
|
|
570
583
|
params: req.params,
|
|
571
584
|
query: req.query,
|
|
@@ -579,6 +592,7 @@ class Handler {
|
|
|
579
592
|
router.post(this.pathWithIdentifier, async (req, res) => {
|
|
580
593
|
assertWebhookParseRequestPayload(req.body);
|
|
581
594
|
const response = await handler({
|
|
595
|
+
secrets: res.locals.secrets,
|
|
582
596
|
logger: res.locals.logger,
|
|
583
597
|
params: req.params,
|
|
584
598
|
query: req.query,
|
|
@@ -593,6 +607,7 @@ class Handler {
|
|
|
593
607
|
router.post(this.pathWithIdentifier, async (req, res) => {
|
|
594
608
|
assertWebhookParseRequestPayload(req.body);
|
|
595
609
|
const response = await handler({
|
|
610
|
+
secrets: res.locals.secrets,
|
|
596
611
|
logger: res.locals.logger,
|
|
597
612
|
params: req.params,
|
|
598
613
|
query: req.query,
|
|
@@ -610,6 +625,7 @@ class Handler {
|
|
|
610
625
|
}
|
|
611
626
|
assertWebhookSubscriptionRequestPayload(req.body);
|
|
612
627
|
const response = await handler({
|
|
628
|
+
secrets: res.locals.secrets,
|
|
613
629
|
credentials: res.locals.credentials,
|
|
614
630
|
body: req.body,
|
|
615
631
|
logger: res.locals.logger,
|
|
@@ -626,7 +642,7 @@ class Handler {
|
|
|
626
642
|
|
|
627
643
|
function printErrorMessage(message) {
|
|
628
644
|
console.error();
|
|
629
|
-
console.error(`\x1b[31m
|
|
645
|
+
console.error(`\x1b[31m Oops! Something went wrong! \x1b[0m`);
|
|
630
646
|
console.error(message);
|
|
631
647
|
}
|
|
632
648
|
class Integration {
|
|
@@ -677,6 +693,7 @@ class Integration {
|
|
|
677
693
|
app.use(express.json());
|
|
678
694
|
// Must be one of the first handlers (to catch all the errors).
|
|
679
695
|
app.use(middleware$1);
|
|
696
|
+
app.use(middleware$7);
|
|
680
697
|
app.use(middleware$6);
|
|
681
698
|
app.use(middleware$5);
|
|
682
699
|
app.use(middleware$4);
|
|
@@ -701,22 +718,6 @@ class Integration {
|
|
|
701
718
|
app.use(middleware);
|
|
702
719
|
// Start the server.
|
|
703
720
|
this.instance = app.listen(this.port, () => console.info(`Server started on port ${this.port}.`));
|
|
704
|
-
// Trap exit signals.
|
|
705
|
-
['SIGTERM', 'SIGINT', 'SIGUSR2'].forEach(signalType => {
|
|
706
|
-
process.once(signalType, async () => {
|
|
707
|
-
console.info(`Received termination signal ${signalType}. Exiting.`);
|
|
708
|
-
try {
|
|
709
|
-
if (this.instance) {
|
|
710
|
-
this.instance.close();
|
|
711
|
-
}
|
|
712
|
-
await shutdownCaches();
|
|
713
|
-
}
|
|
714
|
-
catch (e) {
|
|
715
|
-
console.error('Failed to gracefully exit', e);
|
|
716
|
-
}
|
|
717
|
-
process.exit();
|
|
718
|
-
});
|
|
719
|
-
});
|
|
720
721
|
}
|
|
721
722
|
}
|
|
722
723
|
|
package/dist/src/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { Cache } from './resources/cache.js';
|
|
|
3
3
|
export { default as Integration } from './integration.js';
|
|
4
4
|
export * from './handler.js';
|
|
5
5
|
export { Provider, type Response as ProviderResponse, type RequestOptions as ProviderRequestOptions, type RateLimiter, } from './resources/provider.js';
|
|
6
|
+
export type { Secrets } from './middlewares/secrets.js';
|
|
6
7
|
export type { Credentials } from './middlewares/credentials.js';
|
|
7
8
|
export * as HttpErrors from './httpErrors.js';
|
|
8
9
|
export * from './resources/context.js';
|
package/dist/src/integration.js
CHANGED
|
@@ -3,15 +3,15 @@ import { InvalidHandler } from './errors.js';
|
|
|
3
3
|
import correlationIdMiddleware from './middlewares/correlationId.js';
|
|
4
4
|
import loggerMiddleware from './middlewares/logger.js';
|
|
5
5
|
import credentialsMiddleware from './middlewares/credentials.js';
|
|
6
|
+
import secretsMiddleware from './middlewares/secrets.js';
|
|
6
7
|
import selectsMiddleware from './middlewares/selects.js';
|
|
7
8
|
import errorsMiddleware from './middlewares/errors.js';
|
|
8
9
|
import finishMiddleware from './middlewares/finish.js';
|
|
9
10
|
import notFoundMiddleware from './middlewares/notFound.js';
|
|
10
|
-
import { shutdownCaches } from './resources/cache.js';
|
|
11
11
|
import { Handler } from './handler.js';
|
|
12
12
|
function printErrorMessage(message) {
|
|
13
13
|
console.error();
|
|
14
|
-
console.error(`\x1b[31m
|
|
14
|
+
console.error(`\x1b[31m Oops! Something went wrong! \x1b[0m`);
|
|
15
15
|
console.error(message);
|
|
16
16
|
}
|
|
17
17
|
export default class Integration {
|
|
@@ -65,6 +65,7 @@ export default class Integration {
|
|
|
65
65
|
app.use(correlationIdMiddleware);
|
|
66
66
|
app.use(loggerMiddleware);
|
|
67
67
|
app.use(credentialsMiddleware);
|
|
68
|
+
app.use(secretsMiddleware);
|
|
68
69
|
app.use(selectsMiddleware);
|
|
69
70
|
// Load handlers as needed.
|
|
70
71
|
if (this.handlers.length) {
|
|
@@ -86,21 +87,5 @@ export default class Integration {
|
|
|
86
87
|
app.use(notFoundMiddleware);
|
|
87
88
|
// Start the server.
|
|
88
89
|
this.instance = app.listen(this.port, () => console.info(`Server started on port ${this.port}.`));
|
|
89
|
-
// Trap exit signals.
|
|
90
|
-
['SIGTERM', 'SIGINT', 'SIGUSR2'].forEach(signalType => {
|
|
91
|
-
process.once(signalType, async () => {
|
|
92
|
-
console.info(`Received termination signal ${signalType}. Exiting.`);
|
|
93
|
-
try {
|
|
94
|
-
if (this.instance) {
|
|
95
|
-
this.instance.close();
|
|
96
|
-
}
|
|
97
|
-
await shutdownCaches();
|
|
98
|
-
}
|
|
99
|
-
catch (e) {
|
|
100
|
-
console.error('Failed to gracefully exit', e);
|
|
101
|
-
}
|
|
102
|
-
process.exit();
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
90
|
}
|
|
106
91
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
declare global {
|
|
3
|
+
namespace Express {
|
|
4
|
+
interface Locals {
|
|
5
|
+
secrets: Secrets;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export type Secrets = {
|
|
10
|
+
[keys: string]: unknown;
|
|
11
|
+
};
|
|
12
|
+
declare const middleware: (req: Request, res: Response, next: NextFunction) => void;
|
|
13
|
+
export default middleware;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BadRequestError } from '../httpErrors.js';
|
|
2
|
+
const SECRETS_HEADER = 'X-Unito-Secrets';
|
|
3
|
+
const middleware = (req, res, next) => {
|
|
4
|
+
const secretsHeader = req.header(SECRETS_HEADER);
|
|
5
|
+
if (secretsHeader) {
|
|
6
|
+
let secrets;
|
|
7
|
+
try {
|
|
8
|
+
secrets = JSON.parse(Buffer.from(secretsHeader, 'base64').toString('utf8'));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new BadRequestError(`Malformed HTTP header ${SECRETS_HEADER}`);
|
|
12
|
+
}
|
|
13
|
+
res.locals.secrets = secrets;
|
|
14
|
+
}
|
|
15
|
+
next();
|
|
16
|
+
};
|
|
17
|
+
export default middleware;
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { WriteThroughCache, LocalCache } from 'cachette';
|
|
2
2
|
import * as uuid from 'uuid';
|
|
3
3
|
import Logger from './logger.js';
|
|
4
|
-
/**
|
|
5
|
-
* Array of created caches kept to allow for graceful shutdown on exit signals.
|
|
6
|
-
*/
|
|
7
|
-
const caches = [];
|
|
8
|
-
export const shutdownCaches = async () => {
|
|
9
|
-
return Promise.allSettled(caches.map(cache => cache.quit()));
|
|
10
|
-
};
|
|
11
4
|
export class Cache {
|
|
12
5
|
cacheInstance;
|
|
13
6
|
constructor(cacheInstance) {
|
|
@@ -36,8 +29,6 @@ export class Cache {
|
|
|
36
29
|
*/
|
|
37
30
|
static create(redisUrl) {
|
|
38
31
|
const cacheInstance = redisUrl ? new WriteThroughCache(redisUrl) : new LocalCache();
|
|
39
|
-
// Push to the array of caches for graceful shutdown on exit signals.
|
|
40
|
-
caches.push(cacheInstance);
|
|
41
32
|
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
42
33
|
const correlationId = uuid.v4();
|
|
43
34
|
const logger = new Logger({ correlation_id: correlationId });
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import * as API from '@unito/integration-api';
|
|
2
2
|
import Logger from './logger.js';
|
|
3
3
|
import { Credentials } from '../middlewares/credentials.js';
|
|
4
|
+
import { Secrets } from 'src/middlewares/secrets.js';
|
|
4
5
|
import { Filter } from '../middlewares/filters.js';
|
|
5
|
-
type Context<P extends Record<string, string>, Q extends ParsedQueryString> = {
|
|
6
|
+
export type Context<P extends Record<string, string>, Q extends ParsedQueryString> = {
|
|
6
7
|
/**
|
|
7
8
|
* The parsed credentials associated with the request through the X-Unito-Credentials header.
|
|
8
9
|
*
|
|
9
10
|
* Will contain the keys for the variables defined in the corresponding configuration's authorization.
|
|
10
11
|
*/
|
|
11
12
|
credentials: Credentials;
|
|
13
|
+
/**
|
|
14
|
+
* The parsed secrets associated with the request through the X-Unito-Secrets header.
|
|
15
|
+
*
|
|
16
|
+
* Will contain the keys for the secrets defined in the corresponding configuration's secrets.
|
|
17
|
+
*/
|
|
18
|
+
secrets: Secrets;
|
|
12
19
|
/**
|
|
13
20
|
* The logger pre decorated with the correlation ID and the additionnal metadata provided through the request headers.
|
|
14
21
|
*/
|
|
@@ -9,14 +9,14 @@ import Logger from '../resources/logger.js';
|
|
|
9
9
|
* NOTE: make sure to return one of the supported HttpErrors from the SDK, otherwise the error will be translated to a
|
|
10
10
|
* generic server (500) error.
|
|
11
11
|
*
|
|
12
|
-
* @param
|
|
12
|
+
* @param options - The credentials and the logger from the RequestOptions passed with the provider call.
|
|
13
13
|
* @param targetFunction - The function to call the provider.
|
|
14
14
|
* @returns The response from the provider.
|
|
15
15
|
* @throws RateLimitExceededError when the rate limit is exceeded.
|
|
16
16
|
* @throws WouldExceedRateLimitError when the next call would exceed the rate limit.
|
|
17
17
|
* @throws HttpError when the provider returns an error.
|
|
18
18
|
*/
|
|
19
|
-
export type RateLimiter = <T>(
|
|
19
|
+
export type RateLimiter = <T>(options: {
|
|
20
20
|
credentials: Credentials;
|
|
21
21
|
logger: Logger;
|
|
22
22
|
}, targetFunction: () => Promise<Response<T>>) => Promise<Response<T>>;
|
|
@@ -37,7 +37,7 @@ export interface Response<T> {
|
|
|
37
37
|
}
|
|
38
38
|
export declare class Provider {
|
|
39
39
|
protected rateLimiter: RateLimiter | undefined;
|
|
40
|
-
protected prepareRequest: (
|
|
40
|
+
protected prepareRequest: (options: {
|
|
41
41
|
credentials: Credentials;
|
|
42
42
|
logger: Logger;
|
|
43
43
|
}) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import middleware from '../../src/middlewares/secrets.js';
|
|
4
|
+
import { BadRequestError } from '../../src/httpErrors.js';
|
|
5
|
+
describe('secrets middleware', () => {
|
|
6
|
+
it('uses header', () => {
|
|
7
|
+
const secrets = Buffer.from(JSON.stringify({
|
|
8
|
+
chut: 'abc',
|
|
9
|
+
})).toString('base64');
|
|
10
|
+
const request = { header: (_key) => secrets };
|
|
11
|
+
const response = { locals: {} };
|
|
12
|
+
middleware(request, response, () => { });
|
|
13
|
+
assert.deepEqual(response.locals, {
|
|
14
|
+
secrets: {
|
|
15
|
+
chut: 'abc',
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
it('malformed header', async () => {
|
|
20
|
+
const request = { header: (_key) => 'nope' };
|
|
21
|
+
const response = { locals: {} };
|
|
22
|
+
assert.throws(() => middleware(request, response, () => { }), BadRequestError);
|
|
23
|
+
});
|
|
24
|
+
it('undefined', () => {
|
|
25
|
+
const response = { locals: {} };
|
|
26
|
+
assert.deepEqual(response.locals, {});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import { LocalCache } from 'cachette';
|
|
4
|
-
import { Cache
|
|
4
|
+
import { Cache } from '../../src/resources/cache.js';
|
|
5
5
|
describe('Cache', () => {
|
|
6
6
|
describe('initializeCache', () => {
|
|
7
7
|
it('no redis url returns Cache with a inner LocalCache', async () => {
|
|
8
8
|
const cache = Cache.create();
|
|
9
9
|
assert.ok(cache instanceof Cache);
|
|
10
10
|
assert.ok(cache['cacheInstance'] instanceof LocalCache);
|
|
11
|
-
await
|
|
11
|
+
await cache['cacheInstance'].quit();
|
|
12
12
|
});
|
|
13
13
|
it('redis url returns (tries) WriteThroughCache', () => {
|
|
14
14
|
assert.throws(() => Cache.create('fakeredis'), Error, 'Invalid redis url fakereis.');
|
package/package.json
CHANGED
package/src/handler.ts
CHANGED
|
@@ -67,7 +67,7 @@ export type AcknowledgeWebhooksHandler = (
|
|
|
67
67
|
context: AcknowledgeWebhooksContext<any, any, any>,
|
|
68
68
|
) => Promise<API.WebhookAcknowledgeResponsePayload>;
|
|
69
69
|
|
|
70
|
-
type ItemHandlers = {
|
|
70
|
+
export type ItemHandlers = {
|
|
71
71
|
getItem?: GetItemHandler;
|
|
72
72
|
getCollection?: GetCollectionHandler;
|
|
73
73
|
createItem?: CreateItemHandler;
|
|
@@ -75,19 +75,19 @@ type ItemHandlers = {
|
|
|
75
75
|
deleteItem?: DeleteItemHandler;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
-
type CredentialAccountHandlers = {
|
|
78
|
+
export type CredentialAccountHandlers = {
|
|
79
79
|
getCredentialAccount: GetCredentialAccountHandler;
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
-
type ParseWebhookHandlers = {
|
|
82
|
+
export type ParseWebhookHandlers = {
|
|
83
83
|
parseWebhooks: ParseWebhooksHandler;
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
-
type WebhookSubscriptionHandlers = {
|
|
86
|
+
export type WebhookSubscriptionHandlers = {
|
|
87
87
|
updateWebhookSubscriptions: UpdateWebhookSubscriptionsHandler;
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
-
type AcknowledgeWebhookHandlers = {
|
|
90
|
+
export type AcknowledgeWebhookHandlers = {
|
|
91
91
|
acknowledgeWebhooks: AcknowledgeWebhooksHandler;
|
|
92
92
|
};
|
|
93
93
|
|
|
@@ -230,6 +230,7 @@ export class Handler {
|
|
|
230
230
|
|
|
231
231
|
const collection = await handler({
|
|
232
232
|
credentials: res.locals.credentials,
|
|
233
|
+
secrets: res.locals.secrets,
|
|
233
234
|
selects: res.locals.selects,
|
|
234
235
|
filters: res.locals.filters,
|
|
235
236
|
logger: res.locals.logger,
|
|
@@ -255,6 +256,7 @@ export class Handler {
|
|
|
255
256
|
|
|
256
257
|
const createItemSummary = await handler({
|
|
257
258
|
credentials: res.locals.credentials,
|
|
259
|
+
secrets: res.locals.secrets,
|
|
258
260
|
body: req.body,
|
|
259
261
|
logger: res.locals.logger,
|
|
260
262
|
params: req.params,
|
|
@@ -277,6 +279,7 @@ export class Handler {
|
|
|
277
279
|
|
|
278
280
|
const item = await handler({
|
|
279
281
|
credentials: res.locals.credentials,
|
|
282
|
+
secrets: res.locals.secrets,
|
|
280
283
|
logger: res.locals.logger,
|
|
281
284
|
params: req.params,
|
|
282
285
|
query: req.query,
|
|
@@ -300,6 +303,7 @@ export class Handler {
|
|
|
300
303
|
|
|
301
304
|
const item = await handler({
|
|
302
305
|
credentials: res.locals.credentials,
|
|
306
|
+
secrets: res.locals.secrets,
|
|
303
307
|
body: req.body,
|
|
304
308
|
logger: res.locals.logger,
|
|
305
309
|
params: req.params,
|
|
@@ -322,6 +326,7 @@ export class Handler {
|
|
|
322
326
|
|
|
323
327
|
await handler({
|
|
324
328
|
credentials: res.locals.credentials,
|
|
329
|
+
secrets: res.locals.secrets,
|
|
325
330
|
logger: res.locals.logger,
|
|
326
331
|
params: req.params,
|
|
327
332
|
query: req.query,
|
|
@@ -343,6 +348,7 @@ export class Handler {
|
|
|
343
348
|
|
|
344
349
|
const credentialAccount = await handler({
|
|
345
350
|
credentials: res.locals.credentials,
|
|
351
|
+
secrets: res.locals.secrets,
|
|
346
352
|
logger: res.locals.logger,
|
|
347
353
|
params: req.params,
|
|
348
354
|
query: req.query,
|
|
@@ -361,6 +367,7 @@ export class Handler {
|
|
|
361
367
|
assertWebhookParseRequestPayload(req.body);
|
|
362
368
|
|
|
363
369
|
const response = await handler({
|
|
370
|
+
secrets: res.locals.secrets,
|
|
364
371
|
logger: res.locals.logger,
|
|
365
372
|
params: req.params,
|
|
366
373
|
query: req.query,
|
|
@@ -380,6 +387,7 @@ export class Handler {
|
|
|
380
387
|
assertWebhookParseRequestPayload(req.body);
|
|
381
388
|
|
|
382
389
|
const response = await handler({
|
|
390
|
+
secrets: res.locals.secrets,
|
|
383
391
|
logger: res.locals.logger,
|
|
384
392
|
params: req.params,
|
|
385
393
|
query: req.query,
|
|
@@ -403,6 +411,7 @@ export class Handler {
|
|
|
403
411
|
assertWebhookSubscriptionRequestPayload(req.body);
|
|
404
412
|
|
|
405
413
|
const response = await handler({
|
|
414
|
+
secrets: res.locals.secrets,
|
|
406
415
|
credentials: res.locals.credentials,
|
|
407
416
|
body: req.body,
|
|
408
417
|
logger: res.locals.logger,
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export {
|
|
|
9
9
|
type RequestOptions as ProviderRequestOptions,
|
|
10
10
|
type RateLimiter,
|
|
11
11
|
} from './resources/provider.js';
|
|
12
|
+
export type { Secrets } from './middlewares/secrets.js';
|
|
12
13
|
export type { Credentials } from './middlewares/credentials.js';
|
|
13
14
|
export * as HttpErrors from './httpErrors.js';
|
|
14
15
|
export * from './resources/context.js';
|
package/src/integration.ts
CHANGED
|
@@ -5,16 +5,16 @@ import { InvalidHandler } from './errors.js';
|
|
|
5
5
|
import correlationIdMiddleware from './middlewares/correlationId.js';
|
|
6
6
|
import loggerMiddleware from './middlewares/logger.js';
|
|
7
7
|
import credentialsMiddleware from './middlewares/credentials.js';
|
|
8
|
+
import secretsMiddleware from './middlewares/secrets.js';
|
|
8
9
|
import selectsMiddleware from './middlewares/selects.js';
|
|
9
10
|
import errorsMiddleware from './middlewares/errors.js';
|
|
10
11
|
import finishMiddleware from './middlewares/finish.js';
|
|
11
12
|
import notFoundMiddleware from './middlewares/notFound.js';
|
|
12
|
-
import { shutdownCaches } from './resources/cache.js';
|
|
13
13
|
import { HandlersInput, Handler } from './handler.js';
|
|
14
14
|
|
|
15
15
|
function printErrorMessage(message: string) {
|
|
16
16
|
console.error();
|
|
17
|
-
console.error(`\x1b[31m
|
|
17
|
+
console.error(`\x1b[31m Oops! Something went wrong! \x1b[0m`);
|
|
18
18
|
console.error(message);
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -81,6 +81,7 @@ export default class Integration {
|
|
|
81
81
|
app.use(correlationIdMiddleware);
|
|
82
82
|
app.use(loggerMiddleware);
|
|
83
83
|
app.use(credentialsMiddleware);
|
|
84
|
+
app.use(secretsMiddleware);
|
|
84
85
|
app.use(selectsMiddleware);
|
|
85
86
|
|
|
86
87
|
// Load handlers as needed.
|
|
@@ -105,24 +106,5 @@ export default class Integration {
|
|
|
105
106
|
|
|
106
107
|
// Start the server.
|
|
107
108
|
this.instance = app.listen(this.port, () => console.info(`Server started on port ${this.port}.`));
|
|
108
|
-
|
|
109
|
-
// Trap exit signals.
|
|
110
|
-
['SIGTERM', 'SIGINT', 'SIGUSR2'].forEach(signalType => {
|
|
111
|
-
process.once(signalType, async () => {
|
|
112
|
-
console.info(`Received termination signal ${signalType}. Exiting.`);
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
if (this.instance) {
|
|
116
|
-
this.instance.close();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
await shutdownCaches();
|
|
120
|
-
} catch (e) {
|
|
121
|
-
console.error('Failed to gracefully exit', e);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
process.exit();
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
109
|
}
|
|
128
110
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { BadRequestError } from '../httpErrors.js';
|
|
3
|
+
|
|
4
|
+
declare global {
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
6
|
+
namespace Express {
|
|
7
|
+
interface Locals {
|
|
8
|
+
secrets: Secrets;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type Secrets = { [keys: string]: unknown };
|
|
14
|
+
|
|
15
|
+
const SECRETS_HEADER = 'X-Unito-Secrets';
|
|
16
|
+
|
|
17
|
+
const middleware = (req: Request, res: Response, next: NextFunction) => {
|
|
18
|
+
const secretsHeader = req.header(SECRETS_HEADER);
|
|
19
|
+
|
|
20
|
+
if (secretsHeader) {
|
|
21
|
+
let secrets: Secrets;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
secrets = JSON.parse(Buffer.from(secretsHeader, 'base64').toString('utf8'));
|
|
25
|
+
} catch {
|
|
26
|
+
throw new BadRequestError(`Malformed HTTP header ${SECRETS_HEADER}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
res.locals.secrets = secrets;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
next();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default middleware;
|
package/src/resources/cache.ts
CHANGED
|
@@ -2,15 +2,6 @@ import { WriteThroughCache, LocalCache, CacheInstance, FetchingFunction, Cachabl
|
|
|
2
2
|
import * as uuid from 'uuid';
|
|
3
3
|
import Logger from './logger.js';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Array of created caches kept to allow for graceful shutdown on exit signals.
|
|
7
|
-
*/
|
|
8
|
-
const caches: CacheInstance[] = [];
|
|
9
|
-
|
|
10
|
-
export const shutdownCaches = async () => {
|
|
11
|
-
return Promise.allSettled(caches.map(cache => cache.quit()));
|
|
12
|
-
};
|
|
13
|
-
|
|
14
5
|
export class Cache {
|
|
15
6
|
private cacheInstance: CacheInstance;
|
|
16
7
|
|
|
@@ -53,9 +44,6 @@ export class Cache {
|
|
|
53
44
|
public static create(redisUrl?: string): Cache {
|
|
54
45
|
const cacheInstance: CacheInstance = redisUrl ? new WriteThroughCache(redisUrl) : new LocalCache();
|
|
55
46
|
|
|
56
|
-
// Push to the array of caches for graceful shutdown on exit signals.
|
|
57
|
-
caches.push(cacheInstance);
|
|
58
|
-
|
|
59
47
|
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
60
48
|
const correlationId = uuid.v4();
|
|
61
49
|
|
package/src/resources/context.ts
CHANGED
|
@@ -3,15 +3,22 @@ import * as API from '@unito/integration-api';
|
|
|
3
3
|
|
|
4
4
|
import Logger from './logger.js';
|
|
5
5
|
import { Credentials } from '../middlewares/credentials.js';
|
|
6
|
+
import { Secrets } from 'src/middlewares/secrets.js';
|
|
6
7
|
import { Filter } from '../middlewares/filters.js';
|
|
7
8
|
|
|
8
|
-
type Context<P extends Record<string, string>, Q extends ParsedQueryString> = {
|
|
9
|
+
export type Context<P extends Record<string, string>, Q extends ParsedQueryString> = {
|
|
9
10
|
/**
|
|
10
11
|
* The parsed credentials associated with the request through the X-Unito-Credentials header.
|
|
11
12
|
*
|
|
12
13
|
* Will contain the keys for the variables defined in the corresponding configuration's authorization.
|
|
13
14
|
*/
|
|
14
15
|
credentials: Credentials;
|
|
16
|
+
/**
|
|
17
|
+
* The parsed secrets associated with the request through the X-Unito-Secrets header.
|
|
18
|
+
*
|
|
19
|
+
* Will contain the keys for the secrets defined in the corresponding configuration's secrets.
|
|
20
|
+
*/
|
|
21
|
+
secrets: Secrets;
|
|
15
22
|
/**
|
|
16
23
|
* The logger pre decorated with the correlation ID and the additionnal metadata provided through the request headers.
|
|
17
24
|
*/
|
|
@@ -11,7 +11,7 @@ import Logger from '../resources/logger.js';
|
|
|
11
11
|
* NOTE: make sure to return one of the supported HttpErrors from the SDK, otherwise the error will be translated to a
|
|
12
12
|
* generic server (500) error.
|
|
13
13
|
*
|
|
14
|
-
* @param
|
|
14
|
+
* @param options - The credentials and the logger from the RequestOptions passed with the provider call.
|
|
15
15
|
* @param targetFunction - The function to call the provider.
|
|
16
16
|
* @returns The response from the provider.
|
|
17
17
|
* @throws RateLimitExceededError when the rate limit is exceeded.
|
|
@@ -19,7 +19,7 @@ import Logger from '../resources/logger.js';
|
|
|
19
19
|
* @throws HttpError when the provider returns an error.
|
|
20
20
|
*/
|
|
21
21
|
export type RateLimiter = <T>(
|
|
22
|
-
|
|
22
|
+
options: { credentials: Credentials; logger: Logger },
|
|
23
23
|
targetFunction: () => Promise<Response<T>>,
|
|
24
24
|
) => Promise<Response<T>>;
|
|
25
25
|
|
|
@@ -38,7 +38,7 @@ export interface Response<T> {
|
|
|
38
38
|
|
|
39
39
|
export class Provider {
|
|
40
40
|
protected rateLimiter: RateLimiter | undefined = undefined;
|
|
41
|
-
protected prepareRequest: (
|
|
41
|
+
protected prepareRequest: (options: { credentials: Credentials; logger: Logger }) => {
|
|
42
42
|
url: string;
|
|
43
43
|
headers: Record<string, string>;
|
|
44
44
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { describe, it } from 'node:test';
|
|
4
|
+
import middleware from '../../src/middlewares/secrets.js';
|
|
5
|
+
import { BadRequestError } from '../../src/httpErrors.js';
|
|
6
|
+
|
|
7
|
+
describe('secrets middleware', () => {
|
|
8
|
+
it('uses header', () => {
|
|
9
|
+
const secrets = Buffer.from(
|
|
10
|
+
JSON.stringify({
|
|
11
|
+
chut: 'abc',
|
|
12
|
+
}),
|
|
13
|
+
).toString('base64');
|
|
14
|
+
|
|
15
|
+
const request = { header: (_key: string) => secrets } as express.Request;
|
|
16
|
+
const response = { locals: {} } as express.Response;
|
|
17
|
+
|
|
18
|
+
middleware(request, response, () => {});
|
|
19
|
+
|
|
20
|
+
assert.deepEqual(response.locals, {
|
|
21
|
+
secrets: {
|
|
22
|
+
chut: 'abc',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('malformed header', async () => {
|
|
28
|
+
const request = { header: (_key: string) => 'nope' } as express.Request;
|
|
29
|
+
const response = { locals: {} } as express.Response;
|
|
30
|
+
|
|
31
|
+
assert.throws(() => middleware(request, response, () => {}), BadRequestError);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('undefined', () => {
|
|
35
|
+
const response = { locals: {} } as express.Response;
|
|
36
|
+
|
|
37
|
+
assert.deepEqual(response.locals, {});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import { LocalCache } from 'cachette';
|
|
4
|
-
import { Cache
|
|
4
|
+
import { Cache } from '../../src/resources/cache.js';
|
|
5
5
|
|
|
6
6
|
describe('Cache', () => {
|
|
7
7
|
describe('initializeCache', () => {
|
|
@@ -11,7 +11,7 @@ describe('Cache', () => {
|
|
|
11
11
|
assert.ok(cache instanceof Cache);
|
|
12
12
|
assert.ok(cache['cacheInstance'] instanceof LocalCache);
|
|
13
13
|
|
|
14
|
-
await
|
|
14
|
+
await cache['cacheInstance'].quit();
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it('redis url returns (tries) WriteThroughCache', () => {
|