@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.
@@ -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 {};
@@ -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,
@@ -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$6 = (req, res, next) => {
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$5 = (req, res, next) => {
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$4 = (req, res, next) => {
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 Oups! Something went wrong! \x1b[0m`);
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
 
@@ -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';
@@ -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 Oups! Something went wrong! \x1b[0m`);
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,5 +1,4 @@
1
1
  import { FetchingFunction, CachableValue } from 'cachette';
2
- export declare const shutdownCaches: () => Promise<PromiseSettledResult<void>[]>;
3
2
  export declare class Cache {
4
3
  private cacheInstance;
5
4
  private constructor();
@@ -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 context - The credentials of the caller.
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>(context: {
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: (context: {
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, shutdownCaches } from '../../src/resources/cache.js';
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 shutdownCaches();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-sdk",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Integration SDK",
5
5
  "type": "module",
6
6
  "types": "dist/src/index.d.ts",
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';
@@ -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 Oups! Something went wrong! \x1b[0m`);
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;
@@ -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
 
@@ -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 context - The credentials of the caller.
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
- context: { credentials: Credentials; logger: Logger },
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: (context: { credentials: Credentials; logger: Logger }) => {
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, shutdownCaches } from '../../src/resources/cache.js';
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 shutdownCaches();
14
+ await cache['cacheInstance'].quit();
15
15
  });
16
16
 
17
17
  it('redis url returns (tries) WriteThroughCache', () => {