opticedge-cloud-utils 1.1.18 → 1.1.20

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 (72) hide show
  1. package/dist/src/number.d.ts +1 -0
  2. package/dist/src/number.js +6 -0
  3. package/dist/{pub.js → src/pub.js} +5 -7
  4. package/dist/tests/auth.test.d.ts +1 -0
  5. package/dist/tests/auth.test.js +79 -0
  6. package/dist/tests/chunk.test.d.ts +1 -0
  7. package/dist/tests/chunk.test.js +45 -0
  8. package/dist/tests/db/mongo.test.d.ts +1 -0
  9. package/dist/tests/db/mongo.test.js +43 -0
  10. package/dist/tests/db/mongo2.test.d.ts +1 -0
  11. package/dist/tests/db/mongo2.test.js +49 -0
  12. package/dist/tests/db/mongo3.test.d.ts +1 -0
  13. package/dist/tests/db/mongo3.test.js +60 -0
  14. package/dist/tests/env.test.d.ts +1 -0
  15. package/dist/tests/env.test.js +17 -0
  16. package/dist/tests/number.test.d.ts +1 -0
  17. package/dist/tests/number.test.js +30 -0
  18. package/dist/tests/parser.test.d.ts +1 -0
  19. package/dist/tests/parser.test.js +24 -0
  20. package/dist/tests/pub.test.d.ts +1 -0
  21. package/dist/tests/pub.test.js +102 -0
  22. package/dist/tests/regex.test.d.ts +1 -0
  23. package/dist/tests/regex.test.js +60 -0
  24. package/dist/tests/retry.test.d.ts +1 -0
  25. package/dist/tests/retry.test.js +339 -0
  26. package/dist/tests/secrets.test.d.ts +1 -0
  27. package/dist/tests/secrets.test.js +38 -0
  28. package/dist/tests/task.test.d.ts +1 -0
  29. package/dist/tests/task.test.js +262 -0
  30. package/dist/tests/tw/utils.test.d.ts +1 -0
  31. package/dist/tests/tw/utils.test.js +26 -0
  32. package/dist/tests/tw/wallet.test.d.ts +1 -0
  33. package/dist/tests/tw/wallet.test.js +108 -0
  34. package/dist/tests/validator.d.ts +1 -0
  35. package/dist/tests/validator.js +34 -0
  36. package/package.json +1 -1
  37. package/src/number.ts +3 -0
  38. package/src/pub.ts +7 -9
  39. package/tests/number.test.ts +34 -0
  40. package/tests/pub.test.ts +47 -5
  41. package/tsconfig.json +1 -1
  42. /package/dist/{auth.d.ts → src/auth.d.ts} +0 -0
  43. /package/dist/{auth.js → src/auth.js} +0 -0
  44. /package/dist/{chunk.d.ts → src/chunk.d.ts} +0 -0
  45. /package/dist/{chunk.js → src/chunk.js} +0 -0
  46. /package/dist/{db → src/db}/mongo.d.ts +0 -0
  47. /package/dist/{db → src/db}/mongo.js +0 -0
  48. /package/dist/{db → src/db}/mongo2.d.ts +0 -0
  49. /package/dist/{db → src/db}/mongo2.js +0 -0
  50. /package/dist/{db → src/db}/mongo3.d.ts +0 -0
  51. /package/dist/{db → src/db}/mongo3.js +0 -0
  52. /package/dist/{env.d.ts → src/env.d.ts} +0 -0
  53. /package/dist/{env.js → src/env.js} +0 -0
  54. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  55. /package/dist/{index.js → src/index.js} +0 -0
  56. /package/dist/{parser.d.ts → src/parser.d.ts} +0 -0
  57. /package/dist/{parser.js → src/parser.js} +0 -0
  58. /package/dist/{pub.d.ts → src/pub.d.ts} +0 -0
  59. /package/dist/{regex.d.ts → src/regex.d.ts} +0 -0
  60. /package/dist/{regex.js → src/regex.js} +0 -0
  61. /package/dist/{retry.d.ts → src/retry.d.ts} +0 -0
  62. /package/dist/{retry.js → src/retry.js} +0 -0
  63. /package/dist/{secrets.d.ts → src/secrets.d.ts} +0 -0
  64. /package/dist/{secrets.js → src/secrets.js} +0 -0
  65. /package/dist/{task.d.ts → src/task.d.ts} +0 -0
  66. /package/dist/{task.js → src/task.js} +0 -0
  67. /package/dist/{tw → src/tw}/utils.d.ts +0 -0
  68. /package/dist/{tw → src/tw}/utils.js +0 -0
  69. /package/dist/{tw → src/tw}/wallet.d.ts +0 -0
  70. /package/dist/{tw → src/tw}/wallet.js +0 -0
  71. /package/dist/{validator.d.ts → src/validator.d.ts} +0 -0
  72. /package/dist/{validator.js → src/validator.js} +0 -0
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const utils_1 = require("../../src/tw/utils");
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ describe('isValidWebhookSignature', () => {
9
+ const secret = 'test_secret';
10
+ const body = '{"message":"hello"}';
11
+ it('returns true for a valid signature', () => {
12
+ const validSignature = crypto_1.default.createHmac('sha256', secret).update(body).digest('hex');
13
+ expect((0, utils_1.isValidWebhookSignature)(secret, body, validSignature)).toBe(true);
14
+ });
15
+ it('returns false for an invalid signature', () => {
16
+ const invalidSignature = 'invalidsignature123';
17
+ expect((0, utils_1.isValidWebhookSignature)(secret, body, invalidSignature)).toBe(false);
18
+ });
19
+ it('returns false if body or secret is tampered', () => {
20
+ const originalSignature = crypto_1.default.createHmac('sha256', secret).update(body).digest('hex');
21
+ // wrong body
22
+ expect((0, utils_1.isValidWebhookSignature)(secret, '{"message":"tampered"}', originalSignature)).toBe(false);
23
+ // wrong secret
24
+ expect((0, utils_1.isValidWebhookSignature)('wrong_secret', body, originalSignature)).toBe(false);
25
+ });
26
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ // tests/tw/wallet.test.ts
7
+ /* eslint-disable @typescript-eslint/no-explicit-any */
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const wallet_1 = require("../../src/tw/wallet");
10
+ jest.mock('axios');
11
+ const mockedAxios = axios_1.default;
12
+ function makeAxiosError(message, status, noResponse = false) {
13
+ const err = new Error(message);
14
+ err.isAxiosError = true;
15
+ if (!noResponse) {
16
+ err.response = {
17
+ status: typeof status === 'number' ? status : 500,
18
+ data: { message: `mocked ${status ?? 500}` }
19
+ };
20
+ }
21
+ else {
22
+ // simulate network error (no response)
23
+ delete err.response;
24
+ }
25
+ return err;
26
+ }
27
+ function makeSuccessResponse(data) {
28
+ return { data, status: 200 };
29
+ }
30
+ describe('pregenerateInAppWallet (current signature)', () => {
31
+ const mockClientId = 'test-client-id';
32
+ const mockSecretKey = 'test-secret-key';
33
+ const mockEmail = 'user@example.com';
34
+ const mockWalletAddress = '0xabc123';
35
+ const realMathRandom = Math.random;
36
+ const realWarn = console.warn;
37
+ const realError = console.error;
38
+ beforeEach(() => {
39
+ jest.clearAllMocks();
40
+ // make retry jitter deterministic (so delays don't interfere)
41
+ Math.random = jest.fn(() => 0);
42
+ // axios.isAxiosError helper
43
+ jest
44
+ .spyOn(axios_1.default, 'isAxiosError')
45
+ .mockImplementation((e) => Boolean(e && e.isAxiosError));
46
+ // silence logs during tests but keep spies
47
+ console.warn = jest.fn();
48
+ console.error = jest.fn();
49
+ });
50
+ afterEach(() => {
51
+ Math.random = realMathRandom;
52
+ console.warn = realWarn;
53
+ console.error = realError;
54
+ });
55
+ it('returns null and logs if params missing', async () => {
56
+ const res = await (0, wallet_1.pregenerateInAppWallet)('', mockSecretKey, mockEmail);
57
+ expect(mockedAxios.post).not.toHaveBeenCalled();
58
+ expect(res).toBeNull();
59
+ expect(console.error.mock.calls.length).toBeGreaterThanOrEqual(1);
60
+ expect(console.error.mock.calls[0][0]).toEqual('Missing client id or secret or email');
61
+ });
62
+ it('retries on 500 then succeeds and returns address', async () => {
63
+ // first: 500, then success
64
+ mockedAxios.post
65
+ .mockRejectedValueOnce(makeAxiosError('server error', 500))
66
+ .mockResolvedValueOnce({ data: { wallet: { address: mockWalletAddress } } });
67
+ const result = await (0, wallet_1.pregenerateInAppWallet)(mockClientId, mockSecretKey, mockEmail);
68
+ expect(mockedAxios.post).toHaveBeenCalledTimes(2);
69
+ expect(result).toBe(mockWalletAddress);
70
+ expect(console.warn.mock.calls.length).toBeGreaterThanOrEqual(1);
71
+ });
72
+ it('does NOT retry on 400 and returns null immediately', async () => {
73
+ mockedAxios.post.mockRejectedValue(makeAxiosError('bad request', 400));
74
+ const result = await (0, wallet_1.pregenerateInAppWallet)(mockClientId, mockSecretKey, mockEmail);
75
+ expect(mockedAxios.post).toHaveBeenCalledTimes(1);
76
+ expect(result).toBeNull();
77
+ // no retry log
78
+ expect(console.warn.mock.calls.length).toBe(0);
79
+ });
80
+ it('retries on network/no-response errors and then succeeds', async () => {
81
+ mockedAxios.post
82
+ .mockRejectedValueOnce(makeAxiosError('network down', undefined, true))
83
+ .mockResolvedValueOnce({ data: { wallet: { address: mockWalletAddress } } });
84
+ const result = await (0, wallet_1.pregenerateInAppWallet)(mockClientId, mockSecretKey, mockEmail);
85
+ expect(mockedAxios.post).toHaveBeenCalledTimes(2);
86
+ expect(result).toBe(mockWalletAddress);
87
+ });
88
+ it('exhausts retries on repeated 500s and returns null', async () => {
89
+ mockedAxios.post.mockRejectedValue(makeAxiosError('server error', 500));
90
+ const result = await (0, wallet_1.pregenerateInAppWallet)(mockClientId, mockSecretKey, mockEmail);
91
+ // retry behavior is implementation-dependent; assert at least one call and final null
92
+ expect(mockedAxios.post.mock.calls.length).toBeGreaterThanOrEqual(1);
93
+ expect(result).toBeNull();
94
+ expect(console.error.mock.calls.length).toBeGreaterThanOrEqual(1);
95
+ });
96
+ it('logs invalid wallet shape and returns null when wallet.address missing (2xx response)', async () => {
97
+ mockedAxios.post.mockResolvedValueOnce(makeSuccessResponse({ wallet: {} }));
98
+ const result = await (0, wallet_1.pregenerateInAppWallet)(mockClientId, mockSecretKey, mockEmail);
99
+ expect(mockedAxios.post).toHaveBeenCalledTimes(1);
100
+ expect(result).toBeNull();
101
+ // ensure we logged the invalid shape (the implementation logs then throws)
102
+ expect(console.error.mock.calls.length).toBeGreaterThanOrEqual(1);
103
+ const firstArg = console.error.mock.calls[0][0];
104
+ expect(firstArg).toEqual('Invalid wallet response shape');
105
+ const secondArg = console.error.mock.calls[0][1];
106
+ expect(secondArg).toEqual(expect.objectContaining({ status: 200, data: { wallet: {} } }));
107
+ });
108
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // isValidEmail.test.ts
4
+ const globals_1 = require("@jest/globals");
5
+ const src_1 = require("../src");
6
+ (0, globals_1.describe)('isValidEmail', () => {
7
+ (0, globals_1.it)('returns true for simple valid emails', () => {
8
+ (0, globals_1.expect)((0, src_1.isValidEmail)('test@example.com')).toBe(true);
9
+ (0, globals_1.expect)((0, src_1.isValidEmail)('user.name@domain.co')).toBe(true);
10
+ (0, globals_1.expect)((0, src_1.isValidEmail)('user_name+tag@sub.domain.org')).toBe(true);
11
+ });
12
+ (0, globals_1.it)('returns false for missing parts', () => {
13
+ (0, globals_1.expect)((0, src_1.isValidEmail)('')).toBe(false);
14
+ (0, globals_1.expect)((0, src_1.isValidEmail)('plainaddress')).toBe(false);
15
+ (0, globals_1.expect)((0, src_1.isValidEmail)('@no-local-part.com')).toBe(false);
16
+ (0, globals_1.expect)((0, src_1.isValidEmail)('username@')).toBe(false);
17
+ (0, globals_1.expect)((0, src_1.isValidEmail)('user@domain')).toBe(false);
18
+ (0, globals_1.expect)((0, src_1.isValidEmail)('username@.com')).toBe(false);
19
+ });
20
+ (0, globals_1.it)('handles whitespace correctly', () => {
21
+ (0, globals_1.expect)((0, src_1.isValidEmail)(' test@example.com ')).toBe(true); // trims input
22
+ (0, globals_1.expect)((0, src_1.isValidEmail)('\nuser@domain.com\t')).toBe(true);
23
+ });
24
+ (0, globals_1.it)('rejects invalid characters or formats', () => {
25
+ (0, globals_1.expect)((0, src_1.isValidEmail)('user@@domain.com')).toBe(false);
26
+ (0, globals_1.expect)((0, src_1.isValidEmail)('user@domain,com')).toBe(false);
27
+ (0, globals_1.expect)((0, src_1.isValidEmail)('user@domain..com')).toBe(false);
28
+ (0, globals_1.expect)((0, src_1.isValidEmail)('user@.domain.com')).toBe(false);
29
+ (0, globals_1.expect)((0, src_1.isValidEmail)('user@domain com')).toBe(false);
30
+ });
31
+ (0, globals_1.it)('accepts minimal valid domain structures', () => {
32
+ (0, globals_1.expect)((0, src_1.isValidEmail)('x@y.z')).toBe(true); // still valid
33
+ });
34
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opticedge-cloud-utils",
3
- "version": "1.1.18",
3
+ "version": "1.1.20",
4
4
  "description": "Common utilities for cloud functions",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/number.ts ADDED
@@ -0,0 +1,3 @@
1
+ export function round1(v: number): number {
2
+ return Math.round((v + Number.EPSILON) * 10) / 10
3
+ }
package/src/pub.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  // src/pub.ts
2
2
  import { PubSub } from '@google-cloud/pubsub'
3
3
 
4
- const pubsubCache = new Map<string, PubSub>()
4
+ let cachedPubSub: PubSub | null = null
5
5
 
6
6
  function getPubSub(projectId: string): PubSub {
7
7
  if (!projectId) throw new Error('projectId is required')
8
- let ps = pubsubCache.get(projectId)
9
- if (!ps) {
10
- ps = new PubSub({ projectId })
11
- pubsubCache.set(projectId, ps)
8
+
9
+ if (!cachedPubSub) {
10
+ cachedPubSub = new PubSub({ projectId })
12
11
  }
13
- return ps
12
+
13
+ return cachedPubSub
14
14
  }
15
15
 
16
16
  export async function publishMessage(
@@ -30,9 +30,7 @@ export async function publishMessage(
30
30
  data
31
31
  })
32
32
 
33
- console.info(
34
- `INFO: Pub/Sub publish project=${projectId} topic=${topicName} msg_id=${messageId}`
35
- )
33
+ console.info(`INFO: Pub/Sub publish topic=${topicName} msg_id=${messageId}`)
36
34
  return messageId
37
35
  } catch (err) {
38
36
  console.error('ERROR: publish failed:', err)
@@ -0,0 +1,34 @@
1
+ import { round1 } from '../src/number'
2
+
3
+ describe('round1', () => {
4
+ it('rounds down correctly', () => {
5
+ expect(round1(4.21)).toBe(4.2)
6
+ expect(round1(4.24)).toBe(4.2)
7
+ })
8
+
9
+ it('rounds up correctly', () => {
10
+ expect(round1(4.25)).toBe(4.3)
11
+ expect(round1(4.26)).toBe(4.3)
12
+ })
13
+
14
+ it('handles floating point precision issues', () => {
15
+ expect(round1(1.005)).toBe(1.0) // classic float issue
16
+ expect(round1(2.675)).toBe(2.7)
17
+ })
18
+
19
+ it('handles negative numbers', () => {
20
+ expect(round1(-4.24)).toBe(-4.2)
21
+ expect(round1(-4.25)).toBe(-4.2) // JS rounds toward +∞ for halves
22
+ expect(round1(-4.26)).toBe(-4.3)
23
+ })
24
+
25
+ it('handles zero', () => {
26
+ expect(round1(0)).toBe(0)
27
+ expect(round1(0.04)).toBe(0)
28
+ expect(round1(0.05)).toBe(0.1)
29
+ })
30
+
31
+ it('handles large numbers', () => {
32
+ expect(round1(123456.789)).toBe(123456.8)
33
+ })
34
+ })
package/tests/pub.test.ts CHANGED
@@ -27,8 +27,17 @@ const path = '../src/pub'
27
27
 
28
28
  describe('publishMessage', () => {
29
29
  beforeEach(() => {
30
- // Ensure a fresh module load so the mocked PubSub and src/pub share the same mock instance
30
+ // ensure fresh module state between tests
31
31
  jest.resetModules()
32
+ // clear any existing mock instances array if present
33
+ try {
34
+ const mockPubsub = require('@google-cloud/pubsub')
35
+ if (mockPubsub && Array.isArray(mockPubsub.__instances)) {
36
+ mockPubsub.__instances.length = 0
37
+ }
38
+ } catch {
39
+ // ignore if not loaded yet
40
+ }
32
41
  })
33
42
 
34
43
  test('publishes and returns messageId, sends JSON buffer', async () => {
@@ -57,7 +66,7 @@ describe('publishMessage', () => {
57
66
  await expect(publishMessage('p', 't', undefined as any)).rejects.toThrow('envelope is required')
58
67
  })
59
68
 
60
- test('caches one PubSub instance per project', async () => {
69
+ test('caches one PubSub instance across multiple calls for the same project', async () => {
61
70
  const mockPubsub = require('@google-cloud/pubsub')
62
71
  const { publishMessage } = require(path)
63
72
 
@@ -70,16 +79,49 @@ describe('publishMessage', () => {
70
79
  expect(publishMock).toHaveBeenCalledTimes(2)
71
80
  })
72
81
 
73
- test('creates separate PubSub instances for different projects', async () => {
82
+ test('reuses the same PubSub instance even when called with a different projectId', async () => {
74
83
  const mockPubsub = require('@google-cloud/pubsub')
75
84
  const { publishMessage } = require(path)
76
85
 
86
+ // first call creates instance with project-A
77
87
  await publishMessage('project-A', 't1', { x: 1 })
88
+ // second call passes a different project id but module currently reuses the cached instance
78
89
  await publishMessage('project-B', 't2', { y: 2 })
79
90
 
80
91
  const instances = mockPubsub.__instances
81
- expect(instances.length).toBe(2)
92
+ // because src/pub.ts caches a single PubSub instance, only one instance should exist
93
+ expect(instances.length).toBe(1)
94
+ // the created instance was constructed with the first project id
82
95
  expect(instances[0].opts).toEqual({ projectId: 'project-A' })
83
- expect(instances[1].opts).toEqual({ projectId: 'project-B' })
96
+
97
+ // ensure both publishes were invoked on the same instance (different topics)
98
+ expect(instances[0].topics.get('t1').publishMessage).toHaveBeenCalledTimes(1)
99
+ expect(instances[0].topics.get('t2').publishMessage).toHaveBeenCalledTimes(1)
100
+ })
101
+
102
+ test('logs error and rethrows when publish fails', async () => {
103
+ const mockPubsub = require('@google-cloud/pubsub')
104
+
105
+ // Override the mock PubSub.topic to return a publishMessage that rejects
106
+ mockPubsub.PubSub.prototype.topic = function (name: string) {
107
+ if (!this.topics.has(name)) {
108
+ const publishMessage = jest.fn(() => Promise.reject(new Error('boom')))
109
+ this.topics.set(name, { publishMessage })
110
+ }
111
+ return this.topics.get(name)
112
+ }
113
+
114
+ const { publishMessage } = require(path)
115
+
116
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
117
+
118
+ await expect(publishMessage('project-err', 'topic-err', { fail: true })).rejects.toThrow('boom')
119
+
120
+ expect(consoleErrorSpy).toHaveBeenCalled()
121
+ const firstArg = consoleErrorSpy.mock.calls[0][0]
122
+ expect(typeof firstArg).toBe('string')
123
+ expect(firstArg).toContain('ERROR: publish failed:')
124
+
125
+ consoleErrorSpy.mockRestore()
84
126
  })
85
127
  })
package/tsconfig.json CHANGED
@@ -12,6 +12,6 @@
12
12
  "forceConsistentCasingInFileNames": true,
13
13
  "types": ["node", "jest"]
14
14
  },
15
- "include": ["src"],
15
+ "include": ["src", "tests"],
16
16
  "exclude": ["node_modules", "dist"]
17
17
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes