@app-connect/core 1.7.34 → 1.7.36

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.
Files changed (49) hide show
  1. package/connector/mock.js +9 -4
  2. package/connector/proxy/engine.js +3 -2
  3. package/connector/proxy/index.js +2 -2
  4. package/docs/architecture.md +1 -0
  5. package/docs/handlers.md +1 -1
  6. package/docs/models.md +2 -0
  7. package/docs/routes.md +1 -1
  8. package/handlers/disposition.js +3 -2
  9. package/handlers/log.js +18 -6
  10. package/handlers/plugin.js +13 -6
  11. package/index.js +22 -11
  12. package/lib/callLogLookup.js +82 -9
  13. package/lib/migrateCallLogsSchema.js +128 -7
  14. package/models/callLogModel.js +6 -0
  15. package/package.json +1 -1
  16. package/releaseNotes.json +44 -0
  17. package/test/connector/developerPortal.test.js +166 -0
  18. package/test/connector/mock.test.js +131 -0
  19. package/test/connector/proxy/engine.test.js +85 -0
  20. package/test/connector/proxy/index.test.js +246 -0
  21. package/test/connector/proxy/sample.json +1 -0
  22. package/test/handlers/admin.test.js +344 -0
  23. package/test/handlers/appointment.test.js +260 -0
  24. package/test/handlers/auth.test.js +6 -2
  25. package/test/handlers/calldown.test.js +310 -0
  26. package/test/handlers/disposition.test.js +396 -0
  27. package/test/handlers/log.test.js +327 -3
  28. package/test/handlers/managedOAuth.test.js +262 -0
  29. package/test/handlers/plugin.test.js +305 -0
  30. package/test/handlers/user.test.js +381 -0
  31. package/test/index.test.js +166 -1
  32. package/test/lib/analytics.test.js +146 -0
  33. package/test/lib/authSession.test.js +173 -0
  34. package/test/lib/encode.test.js +59 -0
  35. package/test/lib/errorHandler.test.js +246 -0
  36. package/test/lib/generalErrorMessage.test.js +82 -0
  37. package/test/lib/s3ErrorLogReport.test.js +187 -0
  38. package/test/mcp/mcpHandlerMore.test.js +384 -0
  39. package/test/mcp/tools/appointmentTools.test.js +362 -0
  40. package/test/models/callDownListModel.test.js +125 -0
  41. package/test/models/dynamo/lockSchema.test.js +37 -0
  42. package/test/models/dynamo/noteCacheSchema.test.js +45 -0
  43. package/test/models/llmSessionModel.test.js +91 -0
  44. package/test/models/models.test.js +92 -0
  45. package/test/routes/calldownRoutes.test.js +224 -0
  46. package/test/routes/coreRouterBroadRoutes.test.js +855 -0
  47. package/test/routes/dispositionRoutes.test.js +192 -0
  48. package/test/routes/managedAuthRoutes.test.js +151 -0
  49. package/test/routes/pluginRoutes.test.js +262 -0
@@ -20,6 +20,7 @@ const jwt = require('../lib/jwt');
20
20
  const authCore = require('../handlers/auth');
21
21
  const logCore = require('../handlers/log');
22
22
  const { createCoreRouter, createCoreMiddleware } = require('../index');
23
+ const connectorRegistry = require('../connector/registry');
23
24
 
24
25
  function buildApp() {
25
26
  const app = express();
@@ -33,7 +34,7 @@ describe('Core Router JWT normalization', () => {
33
34
  jest.clearAllMocks();
34
35
  });
35
36
 
36
- test('should accept query jwtToken without refreshing it', async () => {
37
+ test('should accept query jwtToken without exp without refreshing it', async () => {
37
38
  jwt.decodeJwt.mockReturnValue({ id: 'user-1', platform: 'testCRM' });
38
39
  authCore.authValidation.mockResolvedValue({
39
40
  successful: true,
@@ -54,6 +55,69 @@ describe('Core Router JWT normalization', () => {
54
55
  expect(jwt.generateJwt).not.toHaveBeenCalled();
55
56
  });
56
57
 
58
+ test('should refresh near-expiry query jwtToken and expose header', async () => {
59
+ const nowMs = 1700000000000;
60
+ const nowSeconds = Math.floor(nowMs / 1000);
61
+ const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(nowMs);
62
+ jwt.decodeJwt.mockImplementation((token) => {
63
+ if (token === 'query-token') {
64
+ return { id: 'user-1', platform: 'testCRM', exp: nowSeconds + 60 };
65
+ }
66
+ if (token === 'new-token') {
67
+ return { id: 'user-1', platform: 'testCRM', exp: nowSeconds + (14 * 24 * 60 * 60) };
68
+ }
69
+ return null;
70
+ });
71
+ jwt.generateJwt.mockReturnValue('new-token');
72
+ authCore.authValidation.mockResolvedValue({
73
+ successful: true,
74
+ returnMessage: { message: 'ok' },
75
+ failReason: null,
76
+ status: 200,
77
+ });
78
+ const app = buildApp();
79
+
80
+ const response = await request(app)
81
+ .get('/authValidation?jwtToken=query-token')
82
+ .set('Origin', 'https://example.com');
83
+
84
+ expect(response.status).toBe(200);
85
+ expect(response.headers['x-refreshed-jwt-token']).toBe('new-token');
86
+ expect(response.headers['access-control-expose-headers']).toContain('x-refreshed-jwt-token');
87
+ expect(jwt.generateJwt).toHaveBeenCalledWith({ id: 'user-1', platform: 'testCRM' });
88
+ expect(authCore.authValidation).toHaveBeenCalledWith({
89
+ platform: 'testCRM',
90
+ userId: 'user-1',
91
+ });
92
+ expect(jwt.decodeJwt).toHaveBeenCalledWith('new-token');
93
+ nowSpy.mockRestore();
94
+ });
95
+
96
+ test('should not rotate long-lived query jwtToken', async () => {
97
+ const nowMs = 1700000000000;
98
+ const nowSeconds = Math.floor(nowMs / 1000);
99
+ const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(nowMs);
100
+ jwt.decodeJwt.mockReturnValue({
101
+ id: 'user-1',
102
+ platform: 'testCRM',
103
+ exp: nowSeconds + (120 * 365 * 24 * 60 * 60),
104
+ });
105
+ authCore.authValidation.mockResolvedValue({
106
+ successful: true,
107
+ returnMessage: { message: 'ok' },
108
+ failReason: null,
109
+ status: 200,
110
+ });
111
+ const app = buildApp();
112
+
113
+ const response = await request(app).get('/authValidation?jwtToken=query-token');
114
+
115
+ expect(response.status).toBe(200);
116
+ expect(response.headers['x-refreshed-jwt-token']).toBeUndefined();
117
+ expect(jwt.generateJwt).not.toHaveBeenCalled();
118
+ nowSpy.mockRestore();
119
+ });
120
+
57
121
  test('should refresh near-expiry bearer token and expose header', async () => {
58
122
  const nowMs = 1700000000000;
59
123
  const nowSeconds = Math.floor(nowMs / 1000);
@@ -133,4 +197,105 @@ describe('Core Router JWT normalization', () => {
133
197
  });
134
198
  });
135
199
  });
200
+ describe('Core Router implementedInterfaces contract', () => {
201
+ const originalUseCache = process.env.USE_CACHE;
202
+
203
+ beforeEach(() => {
204
+ connectorRegistry.connectors.clear();
205
+ connectorRegistry.manifests.clear();
206
+ connectorRegistry.platformInterfaces.clear();
207
+ delete process.env.USE_CACHE;
208
+ });
136
209
 
210
+ afterEach(() => {
211
+ connectorRegistry.connectors.clear();
212
+ connectorRegistry.manifests.clear();
213
+ connectorRegistry.platformInterfaces.clear();
214
+ if (originalUseCache === undefined) {
215
+ delete process.env.USE_CACHE;
216
+ } else {
217
+ process.env.USE_CACHE = originalUseCache;
218
+ }
219
+ });
220
+
221
+ test('returns client capability flags for an oauth platform', async () => {
222
+ connectorRegistry.registerConnector('contractCRM', {
223
+ getAuthType: jest.fn().mockReturnValue('oauth'),
224
+ getOauthInfo: jest.fn(),
225
+ getUserInfo: jest.fn(),
226
+ createCallLog: jest.fn(),
227
+ updateCallLog: jest.fn(),
228
+ createMessageLog: jest.fn(),
229
+ findContactWithName: jest.fn(),
230
+ getLicenseStatus: jest.fn(),
231
+ });
232
+ const app = buildApp();
233
+
234
+ const response = await request(app).get('/implementedInterfaces?platform=contractCRM');
235
+
236
+ expect(response.status).toBe(200);
237
+ expect(response.body).toEqual({
238
+ getAuthType: true,
239
+ getOauthInfo: true,
240
+ getUserInfo: true,
241
+ createCallLog: true,
242
+ updateCallLog: true,
243
+ getCallLog: false,
244
+ createMessageLog: true,
245
+ updateMessageLog: false,
246
+ createContact: false,
247
+ findContact: false,
248
+ listAppointments: false,
249
+ createAppointment: false,
250
+ updateAppointment: false,
251
+ refreshAppointment: false,
252
+ confirmAppointment: false,
253
+ cancelAppointment: false,
254
+ unAuthorize: false,
255
+ upsertCallDisposition: false,
256
+ findContactWithName: true,
257
+ getUserList: false,
258
+ getLicenseStatus: true,
259
+ getLogFormatType: false,
260
+ refreshUserInfo: false,
261
+ cacheCallNote: false,
262
+ });
263
+ expect(response.body).not.toHaveProperty('getBasicAuth');
264
+ });
265
+
266
+ test('returns basic auth and cache flags for an apiKey platform', async () => {
267
+ process.env.USE_CACHE = 'true';
268
+ connectorRegistry.registerConnector('apiKeyCRM', {
269
+ getAuthType: jest.fn().mockReturnValue('apiKey'),
270
+ getBasicAuth: jest.fn(),
271
+ createCallLog: jest.fn(),
272
+ updateCallLog: jest.fn(),
273
+ updateMessageLog: jest.fn(),
274
+ refreshUserInfo: jest.fn(),
275
+ });
276
+ const app = buildApp();
277
+
278
+ const response = await request(app).get('/implementedInterfaces?platform=apiKeyCRM');
279
+
280
+ expect(response.status).toBe(200);
281
+ expect(response.body).toEqual(expect.objectContaining({
282
+ getAuthType: true,
283
+ getBasicAuth: true,
284
+ createCallLog: true,
285
+ updateCallLog: true,
286
+ updateMessageLog: true,
287
+ refreshUserInfo: true,
288
+ cacheCallNote: true,
289
+ }));
290
+ expect(response.body).not.toHaveProperty('getOauthInfo');
291
+ });
292
+
293
+ test('rejects requests without a platform parameter', async () => {
294
+ const app = buildApp();
295
+
296
+ const response = await request(app).get('/implementedInterfaces');
297
+
298
+ expect(response.status).toBe(400);
299
+ expect(response.text).toBe('Please provide platform.');
300
+ });
301
+ });
@@ -0,0 +1,146 @@
1
+ describe('analytics', () => {
2
+ let mixpanelInit;
3
+ let peopleSetOnce;
4
+ let track;
5
+ let parseUserAgent;
6
+
7
+ function loadAnalytics({ token } = {}) {
8
+ jest.resetModules();
9
+ peopleSetOnce = jest.fn();
10
+ track = jest.fn();
11
+ mixpanelInit = jest.fn(() => ({
12
+ people: {
13
+ set_once: peopleSetOnce
14
+ },
15
+ track
16
+ }));
17
+ parseUserAgent = jest.fn(() => ({
18
+ browser: { name: 'Chrome' },
19
+ os: { name: 'Windows' },
20
+ device: { type: 'desktop' }
21
+ }));
22
+
23
+ if (token) {
24
+ process.env.MIXPANEL_TOKEN = token;
25
+ } else {
26
+ delete process.env.MIXPANEL_TOKEN;
27
+ }
28
+
29
+ jest.doMock('mixpanel', () => ({
30
+ init: mixpanelInit
31
+ }));
32
+ jest.doMock('ua-parser-js', () => parseUserAgent);
33
+ jest.doMock('../../lib/logger', () => ({
34
+ info: jest.fn(),
35
+ warn: jest.fn(),
36
+ error: jest.fn()
37
+ }));
38
+
39
+ return {
40
+ analytics: require('../../lib/analytics'),
41
+ logger: require('../../lib/logger')
42
+ };
43
+ }
44
+
45
+ afterEach(() => {
46
+ delete process.env.MIXPANEL_TOKEN;
47
+ jest.dontMock('mixpanel');
48
+ jest.dontMock('ua-parser-js');
49
+ jest.dontMock('../../lib/logger');
50
+ jest.resetModules();
51
+ jest.clearAllMocks();
52
+ });
53
+
54
+ test('does not initialize Mixpanel when token is missing and skips tracking without an initialized client', () => {
55
+ const { analytics } = loadAnalytics();
56
+
57
+ analytics.init();
58
+ analytics.track({
59
+ eventName: 'Call Log Created',
60
+ connectorName: 'salesforce',
61
+ extensionId: 'ext-1'
62
+ });
63
+
64
+ expect(mixpanelInit).not.toHaveBeenCalled();
65
+ expect(peopleSetOnce).not.toHaveBeenCalled();
66
+ expect(track).not.toHaveBeenCalled();
67
+ });
68
+
69
+ test('skips tracking when extension id is missing', () => {
70
+ const { analytics } = loadAnalytics({ token: 'mixpanel-token' });
71
+
72
+ analytics.init();
73
+ analytics.track({
74
+ eventName: 'Call Log Created',
75
+ connectorName: 'salesforce',
76
+ accountId: 'account-1'
77
+ });
78
+
79
+ expect(mixpanelInit).toHaveBeenCalledWith('mixpanel-token');
80
+ expect(peopleSetOnce).not.toHaveBeenCalled();
81
+ expect(track).not.toHaveBeenCalled();
82
+ });
83
+
84
+ test('tracks event properties, parsed user agent fields, extras, and default source', () => {
85
+ const { analytics, logger } = loadAnalytics({ token: 'mixpanel-token' });
86
+
87
+ analytics.init();
88
+ analytics.track({
89
+ eventName: 'Call Log Created',
90
+ interfaceName: 'desktop',
91
+ connectorName: 'salesforce',
92
+ accountId: 'account-1',
93
+ extensionId: 'ext-1',
94
+ success: true,
95
+ requestDuration: 123,
96
+ userAgent: 'Mozilla/5.0',
97
+ ip: '127.0.0.1',
98
+ author: 'unit-test',
99
+ extras: {
100
+ extraKey: 'extra-value'
101
+ }
102
+ });
103
+
104
+ expect(peopleSetOnce).toHaveBeenCalledWith('ext-1', {
105
+ version: expect.any(String),
106
+ appName: 'App Connect',
107
+ crmPlatform: 'salesforce'
108
+ });
109
+ expect(parseUserAgent).toHaveBeenCalledWith('Mozilla/5.0');
110
+ expect(track).toHaveBeenCalledWith('Call Log Created', expect.objectContaining({
111
+ distinct_id: 'ext-1',
112
+ interfaceName: 'desktop',
113
+ adapterName: 'salesforce',
114
+ rcAccountId: 'account-1',
115
+ extensionId: 'ext-1',
116
+ success: true,
117
+ requestDuration: 123,
118
+ collectedFrom: 'server',
119
+ appName: 'App Connect',
120
+ eventAddedVia: 'server',
121
+ $browser: 'Chrome',
122
+ $os: 'Windows',
123
+ $device: 'desktop',
124
+ ip: '127.0.0.1',
125
+ author: 'unit-test',
126
+ extraKey: 'extra-value'
127
+ }));
128
+ expect(logger.info).toHaveBeenCalledWith('Event: Call Log Created');
129
+ });
130
+
131
+ test('uses explicit eventAddedVia when provided', () => {
132
+ const { analytics } = loadAnalytics({ token: 'mixpanel-token' });
133
+
134
+ analytics.init();
135
+ analytics.track({
136
+ eventName: 'Disposition Saved',
137
+ connectorName: 'hubspot',
138
+ extensionId: 'ext-2',
139
+ eventAddedVia: 'browser'
140
+ });
141
+
142
+ expect(track).toHaveBeenCalledWith('Disposition Saved', expect.objectContaining({
143
+ eventAddedVia: 'browser'
144
+ }));
145
+ });
146
+ });
@@ -0,0 +1,173 @@
1
+ const { CacheModel } = require('../../models/cacheModel');
2
+ const {
3
+ createAuthSession,
4
+ getAuthSession,
5
+ updateAuthSession,
6
+ } = require('../../lib/authSession');
7
+
8
+ describe('authSession', () => {
9
+ const baseTime = new Date(Date.now() + (60 * 60 * 1000));
10
+
11
+ beforeEach(async () => {
12
+ await CacheModel.destroy({ where: {} });
13
+ });
14
+
15
+ afterEach(() => {
16
+ jest.restoreAllMocks();
17
+ });
18
+
19
+ test('creates a pending auth session record with generated id and expiry', async () => {
20
+ jest.spyOn(Date, 'now').mockReturnValue(baseTime.getTime());
21
+
22
+ await createAuthSession('session-1', {
23
+ platform: 'clio',
24
+ hostname: 'https://example.clio.com',
25
+ });
26
+
27
+ const record = await CacheModel.findByPk('auth-session-session-1');
28
+ expect(record).not.toBeNull();
29
+ expect(record.userId).toBe('session-1');
30
+ expect(record.cacheKey).toBe('auth-session');
31
+ expect(record.status).toBe('pending');
32
+ expect(record.expiry.getTime()).toBe(baseTime.getTime() + (5 * 60 * 1000));
33
+ expect(record.data).toMatchObject({
34
+ platform: 'clio',
35
+ hostname: 'https://example.clio.com',
36
+ });
37
+ expect(new Date(record.data.createdAt).toString()).not.toBe('Invalid Date');
38
+
39
+ await expect(getAuthSession('session-1')).resolves.toMatchObject({
40
+ sessionId: 'session-1',
41
+ status: 'pending',
42
+ platform: 'clio',
43
+ hostname: 'https://example.clio.com',
44
+ });
45
+ });
46
+
47
+ test('creates with an empty hostname when hostname is missing', async () => {
48
+ await createAuthSession('session-no-hostname', {
49
+ platform: 'googleSheets',
50
+ });
51
+
52
+ const session = await getAuthSession('session-no-hostname');
53
+
54
+ expect(session).toMatchObject({
55
+ sessionId: 'session-no-hostname',
56
+ status: 'pending',
57
+ platform: 'googleSheets',
58
+ hostname: '',
59
+ });
60
+ });
61
+
62
+ test('resets an existing session record to pending with new session data', async () => {
63
+ await createAuthSession('retry-session', {
64
+ platform: 'clio',
65
+ hostname: 'old-host',
66
+ });
67
+ const existing = await CacheModel.findByPk('auth-session-retry-session');
68
+ await existing.update({
69
+ status: 'completed',
70
+ data: {
71
+ platform: 'clio',
72
+ hostname: 'old-host',
73
+ jwtToken: 'old-token',
74
+ },
75
+ });
76
+
77
+ await createAuthSession('retry-session', {
78
+ platform: 'bullhorn',
79
+ });
80
+
81
+ const record = await CacheModel.findByPk('auth-session-retry-session');
82
+ expect(record.status).toBe('pending');
83
+ expect(record.data).toMatchObject({
84
+ platform: 'bullhorn',
85
+ hostname: '',
86
+ });
87
+ expect(record.data.jwtToken).toBeUndefined();
88
+ });
89
+
90
+ test('returns null for missing sessions', async () => {
91
+ await expect(getAuthSession('missing-session')).resolves.toBeNull();
92
+ });
93
+
94
+ test('marks expired sessions as expired and returns current session data', async () => {
95
+ await CacheModel.create({
96
+ id: 'auth-session-expired-session',
97
+ cacheKey: 'auth-session',
98
+ userId: 'expired-session',
99
+ status: 'pending',
100
+ data: {
101
+ platform: 'pipedrive',
102
+ hostname: 'pipedrive-host',
103
+ },
104
+ expiry: new Date(Date.now() - 1000),
105
+ });
106
+
107
+ const session = await getAuthSession('expired-session');
108
+
109
+ expect(session).toEqual({
110
+ sessionId: 'expired-session',
111
+ status: 'expired',
112
+ platform: 'pipedrive',
113
+ hostname: 'pipedrive-host',
114
+ });
115
+ const record = await CacheModel.findByPk('auth-session-expired-session');
116
+ expect(record.status).toBe('expired');
117
+ });
118
+
119
+ test('updates an existing session by merging data and extending settled expiry', async () => {
120
+ jest.spyOn(Date, 'now').mockReturnValue(baseTime.getTime());
121
+ await createAuthSession('update-session', {
122
+ platform: 'netsuite',
123
+ hostname: 'netsuite-host',
124
+ });
125
+ jest.spyOn(Date, 'now').mockReturnValue(baseTime.getTime() + 1000);
126
+
127
+ await updateAuthSession('update-session', {
128
+ status: 'completed',
129
+ jwtToken: 'jwt-token',
130
+ rcExtensionId: 'extension-1',
131
+ });
132
+
133
+ const record = await CacheModel.findByPk('auth-session-update-session');
134
+ expect(record.status).toBe('completed');
135
+ expect(record.expiry.getTime()).toBe(baseTime.getTime() + 1000 + (15 * 60 * 1000));
136
+ expect(record.data).toMatchObject({
137
+ platform: 'netsuite',
138
+ hostname: 'netsuite-host',
139
+ status: 'completed',
140
+ jwtToken: 'jwt-token',
141
+ rcExtensionId: 'extension-1',
142
+ });
143
+ expect(new Date(record.data.updatedAt).toString()).not.toBe('Invalid Date');
144
+ });
145
+
146
+ test('keeps pending expiry when update does not settle the session', async () => {
147
+ jest.spyOn(Date, 'now').mockReturnValue(baseTime.getTime());
148
+ await createAuthSession('pending-session', {
149
+ platform: 'redtail',
150
+ });
151
+ jest.spyOn(Date, 'now').mockReturnValue(baseTime.getTime() + 2000);
152
+
153
+ await updateAuthSession('pending-session', {
154
+ hostname: 'new-host',
155
+ });
156
+
157
+ const record = await CacheModel.findByPk('auth-session-pending-session');
158
+ expect(record.status).toBe('pending');
159
+ expect(record.expiry.getTime()).toBe(baseTime.getTime() + 2000 + (5 * 60 * 1000));
160
+ expect(record.data).toMatchObject({
161
+ platform: 'redtail',
162
+ hostname: 'new-host',
163
+ });
164
+ });
165
+
166
+ test('does nothing when updating a missing session', async () => {
167
+ await expect(updateAuthSession('missing-session', {
168
+ status: 'completed',
169
+ })).resolves.toBeUndefined();
170
+
171
+ await expect(CacheModel.findByPk('auth-session-missing-session')).resolves.toBeNull();
172
+ });
173
+ });
@@ -0,0 +1,59 @@
1
+ const { decoded, encode } = require('../../lib/encode');
2
+
3
+ describe('encode', () => {
4
+ const originalSecret = process.env.APP_SERVER_SECRET_KEY;
5
+
6
+ afterEach(() => {
7
+ if (originalSecret === undefined) {
8
+ delete process.env.APP_SERVER_SECRET_KEY;
9
+ } else {
10
+ process.env.APP_SERVER_SECRET_KEY = originalSecret;
11
+ }
12
+ });
13
+
14
+ test('encodes and decodes a string with a 32-character secret', () => {
15
+ process.env.APP_SERVER_SECRET_KEY = '12345678901234567890123456789012';
16
+
17
+ const encrypted = encode('sensitive token value');
18
+
19
+ expect(encrypted).toMatch(/^[0-9a-f]+$/);
20
+ expect(encrypted).not.toBe('sensitive token value');
21
+ expect(decoded(encrypted)).toBe('sensitive token value');
22
+ });
23
+
24
+ test('throws a deterministic error when secret is missing', () => {
25
+ delete process.env.APP_SERVER_SECRET_KEY;
26
+
27
+ expect(() => encode('data')).toThrow('APP_SERVER_SECRET_KEY is not defined');
28
+ expect(() => decoded('abcd')).toThrow('APP_SERVER_SECRET_KEY is not defined');
29
+ });
30
+
31
+ test('pads short secrets with spaces to 32 bytes', () => {
32
+ process.env.APP_SERVER_SECRET_KEY = 'short-secret';
33
+ const encryptedWithShortSecret = encode('payload');
34
+
35
+ process.env.APP_SERVER_SECRET_KEY = 'short-secret'.padEnd(32, ' ');
36
+ const encryptedWithPaddedSecret = encode('payload');
37
+
38
+ expect(encryptedWithShortSecret).toBe(encryptedWithPaddedSecret);
39
+ expect(decoded(encryptedWithShortSecret)).toBe('payload');
40
+ });
41
+
42
+ test('truncates long secrets to 32 bytes', () => {
43
+ process.env.APP_SERVER_SECRET_KEY = '12345678901234567890123456789012-first-suffix';
44
+ const encryptedWithLongSecret = encode('payload');
45
+
46
+ process.env.APP_SERVER_SECRET_KEY = '12345678901234567890123456789012-second-suffix';
47
+ const encryptedWithSamePrefix = encode('payload');
48
+
49
+ expect(encryptedWithLongSecret).toBe(encryptedWithSamePrefix);
50
+ expect(decoded(encryptedWithLongSecret)).toBe('payload');
51
+ });
52
+
53
+ test('throws when encrypted input is not valid hex ciphertext', () => {
54
+ process.env.APP_SERVER_SECRET_KEY = '12345678901234567890123456789012';
55
+
56
+ expect(() => decoded('not-valid-ciphertext')).toThrow();
57
+ });
58
+ });
59
+