@canton-network/wallet-gateway-remote 1.4.0 → 1.6.0

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 (141) hide show
  1. package/dist/auth/jwt-auth-service.d.ts.map +1 -1
  2. package/dist/auth/jwt-auth-service.js +6 -5
  3. package/dist/auth/jwt-auth-service.test.d.ts +2 -0
  4. package/dist/auth/jwt-auth-service.test.d.ts.map +1 -0
  5. package/dist/auth/jwt-auth-service.test.js +259 -0
  6. package/dist/auth/jwt-unsafe-auth-service.test.d.ts +2 -0
  7. package/dist/auth/jwt-unsafe-auth-service.test.d.ts.map +1 -0
  8. package/dist/auth/jwt-unsafe-auth-service.test.js +120 -0
  9. package/dist/config/Config.d.ts +69 -4
  10. package/dist/config/Config.d.ts.map +1 -1
  11. package/dist/config/Config.js +6 -0
  12. package/dist/config/ConfigUtils.d.ts.map +1 -1
  13. package/dist/config/ConfigUtils.js +3 -0
  14. package/dist/dapp-api/controller.d.ts +5 -1
  15. package/dist/dapp-api/controller.d.ts.map +1 -1
  16. package/dist/dapp-api/controller.js +104 -30
  17. package/dist/dapp-api/controller.test.js +2 -2
  18. package/dist/dapp-api/server.d.ts +3 -2
  19. package/dist/dapp-api/server.d.ts.map +1 -1
  20. package/dist/dapp-api/server.js +2 -2
  21. package/dist/dapp-api/server.test.js +1 -6
  22. package/dist/example-config.d.ts +3 -0
  23. package/dist/example-config.d.ts.map +1 -1
  24. package/dist/example-config.js +3 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +10 -0
  27. package/dist/init.d.ts.map +1 -1
  28. package/dist/init.js +19 -7
  29. package/dist/ledger/transaction-service.d.ts +16 -11
  30. package/dist/ledger/transaction-service.d.ts.map +1 -1
  31. package/dist/ledger/transaction-service.js +135 -8
  32. package/dist/ledger/transaction-service.test.d.ts +2 -0
  33. package/dist/ledger/transaction-service.test.d.ts.map +1 -0
  34. package/dist/ledger/transaction-service.test.js +390 -0
  35. package/dist/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.d.ts +5 -2
  36. package/dist/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.d.ts.map +1 -1
  37. package/dist/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.js +7 -6
  38. package/dist/ledger/wallet-allocation/wallet-allocation-service.d.ts +11 -5
  39. package/dist/ledger/wallet-allocation/wallet-allocation-service.d.ts.map +1 -1
  40. package/dist/ledger/wallet-allocation/wallet-allocation-service.js +28 -14
  41. package/dist/ledger/wallet-allocation/wallet-allocation-service.test.js +550 -19
  42. package/dist/ledger/wallet-sync-service.js +3 -3
  43. package/dist/middleware/apiKeyAuth.d.ts +6 -0
  44. package/dist/middleware/apiKeyAuth.d.ts.map +1 -0
  45. package/dist/middleware/apiKeyAuth.js +90 -0
  46. package/dist/middleware/apiKeyAuth.test.d.ts +2 -0
  47. package/dist/middleware/apiKeyAuth.test.d.ts.map +1 -0
  48. package/dist/middleware/apiKeyAuth.test.js +126 -0
  49. package/dist/middleware/jsonRpcHandler.d.ts +11 -1
  50. package/dist/middleware/jsonRpcHandler.d.ts.map +1 -1
  51. package/dist/middleware/jsonRpcHandler.js +14 -15
  52. package/dist/middleware/jsonRpcHandler.test.d.ts +2 -0
  53. package/dist/middleware/jsonRpcHandler.test.d.ts.map +1 -0
  54. package/dist/middleware/jsonRpcHandler.test.js +254 -0
  55. package/dist/middleware/jwtAuth.d.ts.map +1 -1
  56. package/dist/middleware/jwtAuth.js +4 -0
  57. package/dist/middleware/jwtAuth.test.d.ts +2 -0
  58. package/dist/middleware/jwtAuth.test.d.ts.map +1 -0
  59. package/dist/middleware/jwtAuth.test.js +93 -0
  60. package/dist/middleware/rateLimit.test.js +12 -0
  61. package/dist/middleware/sessionHandler.js +1 -1
  62. package/dist/middleware/sessionHandler.test.d.ts +2 -0
  63. package/dist/middleware/sessionHandler.test.d.ts.map +1 -0
  64. package/dist/middleware/sessionHandler.test.js +121 -0
  65. package/dist/notification/NotificationService.test.d.ts +2 -0
  66. package/dist/notification/NotificationService.test.d.ts.map +1 -0
  67. package/dist/notification/NotificationService.test.js +110 -0
  68. package/dist/signing/service-account-session.d.ts +16 -0
  69. package/dist/signing/service-account-session.d.ts.map +1 -0
  70. package/dist/signing/service-account-session.js +25 -0
  71. package/dist/signing/service-account-session.test.d.ts +2 -0
  72. package/dist/signing/service-account-session.test.d.ts.map +1 -0
  73. package/dist/signing/service-account-session.test.js +89 -0
  74. package/dist/signing/signing-drivers.d.ts +7 -0
  75. package/dist/signing/signing-drivers.d.ts.map +1 -0
  76. package/dist/signing/signing-drivers.js +3 -0
  77. package/dist/signing/signing-worker.d.ts +83 -0
  78. package/dist/signing/signing-worker.d.ts.map +1 -0
  79. package/dist/signing/signing-worker.js +206 -0
  80. package/dist/signing/signing-worker.test.d.ts +2 -0
  81. package/dist/signing/signing-worker.test.d.ts.map +1 -0
  82. package/dist/signing/signing-worker.test.js +152 -0
  83. package/dist/user-api/controller.d.ts +6 -4
  84. package/dist/user-api/controller.d.ts.map +1 -1
  85. package/dist/user-api/controller.js +119 -95
  86. package/dist/user-api/controller.test.d.ts +2 -0
  87. package/dist/user-api/controller.test.d.ts.map +1 -0
  88. package/dist/user-api/controller.test.js +862 -0
  89. package/dist/user-api/rpc-gen/index.d.ts +12 -0
  90. package/dist/user-api/rpc-gen/index.d.ts.map +1 -1
  91. package/dist/user-api/rpc-gen/index.js +4 -0
  92. package/dist/user-api/rpc-gen/typings.d.ts +52 -3
  93. package/dist/user-api/rpc-gen/typings.d.ts.map +1 -1
  94. package/dist/utils.d.ts +17 -1
  95. package/dist/utils.d.ts.map +1 -1
  96. package/dist/utils.js +53 -2
  97. package/dist/utils.test.d.ts +2 -0
  98. package/dist/utils.test.d.ts.map +1 -0
  99. package/dist/utils.test.js +24 -0
  100. package/dist/web/frontend/404/index.html +2 -2
  101. package/dist/web/frontend/activities/index.html +3 -3
  102. package/dist/web/frontend/approve/index.html +4 -4
  103. package/dist/web/frontend/assets/{404-C7V2yl2z.js → 404-C4hCL2o8.js} +2 -2
  104. package/dist/web/frontend/assets/{activities-NY8oZAkH.js → activities-Bq7yMMQY.js} +2 -2
  105. package/dist/web/frontend/assets/addIdentityProvider-DRJWLhHF.js +28 -0
  106. package/dist/web/frontend/assets/addNetwork-CJUaKP1q.js +38 -0
  107. package/dist/web/frontend/assets/addParty-2ObTLGIL.js +45 -0
  108. package/dist/web/frontend/assets/approve-BuA5DGC2.js +21 -0
  109. package/dist/web/frontend/assets/callback-C5vJWFLq.js +1 -0
  110. package/dist/web/frontend/assets/{identityProviders-BZ9h_VOJ.js → identityProviders-78FbpzYL.js} +2 -2
  111. package/dist/web/frontend/assets/{index-B-HiKugW.js → index-BTH_md6b.js} +3 -3
  112. package/dist/web/frontend/assets/index-CFnOucPK.js +4737 -0
  113. package/dist/web/frontend/assets/{login-CqwMEG9J.js → login-Dyn1o6fW.js} +2 -2
  114. package/dist/web/frontend/assets/{networks-BJvRi47M.js → networks-_el10AWP.js} +2 -2
  115. package/dist/web/frontend/assets/reviewIdentityProvider-DodUZ2mY.js +43 -0
  116. package/dist/web/frontend/assets/reviewNetwork-BFMtmKEL.js +43 -0
  117. package/dist/web/frontend/assets/settings-BBmkIfYm.js +37 -0
  118. package/dist/web/frontend/assets/{state-qAsNw5qf.js → state-DkxLvywv.js} +1 -1
  119. package/dist/web/frontend/assets/{utils-BDkHxi8V.js → utils-DvJKIYxT.js} +1 -1
  120. package/dist/web/frontend/callback/index.html +2 -2
  121. package/dist/web/frontend/identity-providers/add/index.html +3 -3
  122. package/dist/web/frontend/identity-providers/index.html +3 -3
  123. package/dist/web/frontend/identity-providers/review/index.html +3 -3
  124. package/dist/web/frontend/index.html +1 -1
  125. package/dist/web/frontend/login/index.html +3 -3
  126. package/dist/web/frontend/networks/add/index.html +3 -3
  127. package/dist/web/frontend/networks/index.html +3 -3
  128. package/dist/web/frontend/networks/review/index.html +3 -3
  129. package/dist/web/frontend/parties/add/index.html +5 -5
  130. package/dist/web/frontend/parties/index.html +4 -4
  131. package/dist/web/frontend/settings/index.html +3 -3
  132. package/package.json +36 -32
  133. package/dist/web/frontend/assets/addIdentityProvider-38ghz3RT.js +0 -28
  134. package/dist/web/frontend/assets/addNetwork-BaDhbrZl.js +0 -38
  135. package/dist/web/frontend/assets/addParty-j3JWZX5a.js +0 -41
  136. package/dist/web/frontend/assets/approve-43nVzAAC.js +0 -21
  137. package/dist/web/frontend/assets/callback-BBMiCjHR.js +0 -1
  138. package/dist/web/frontend/assets/index-DoNqknyE.js +0 -4042
  139. package/dist/web/frontend/assets/reviewIdentityProvider-ySlQnw5R.js +0 -43
  140. package/dist/web/frontend/assets/reviewNetwork-DrVxeHd4.js +0 -43
  141. package/dist/web/frontend/assets/settings-BVNkaUyJ.js +0 -37
@@ -0,0 +1,93 @@
1
+ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
4
+ import { jwtAuth } from './jwtAuth.js';
5
+ import { pino } from 'pino';
6
+ import { sink } from 'pino-test';
7
+ describe('jwtAuth', () => {
8
+ const verifyToken = vi.fn();
9
+ const authService = { verifyToken };
10
+ const logger = pino({ level: 'silent' }, sink());
11
+ let next;
12
+ let status;
13
+ let json;
14
+ beforeEach(() => {
15
+ verifyToken.mockReset();
16
+ next = vi.fn();
17
+ status = vi.fn().mockReturnThis();
18
+ json = vi.fn();
19
+ });
20
+ function makeReq(partial) {
21
+ return {
22
+ headers: {},
23
+ query: {},
24
+ ...partial,
25
+ };
26
+ }
27
+ function makeRes() {
28
+ return { status, json };
29
+ }
30
+ it('sets authContext and calls next when verification succeeds', async () => {
31
+ const ctx = { userId: 'alice', accessToken: 'tok' };
32
+ verifyToken.mockResolvedValue(ctx);
33
+ const req = makeReq({
34
+ headers: { authorization: 'Bearer abc' },
35
+ });
36
+ const res = makeRes();
37
+ const middleware = jwtAuth(authService, logger);
38
+ await middleware(req, res, next);
39
+ expect(verifyToken).toHaveBeenCalledWith('Bearer abc');
40
+ expect(req.authContext).toEqual(ctx);
41
+ expect(next).toHaveBeenCalledOnce();
42
+ expect(status).not.toHaveBeenCalled();
43
+ });
44
+ it('uses Bearer token from query when Authorization header is absent', async () => {
45
+ const ctx = { userId: 'bob', accessToken: 'tok' };
46
+ verifyToken.mockResolvedValue(ctx);
47
+ const req = makeReq({
48
+ query: { token: 'query-jwt' },
49
+ });
50
+ const res = makeRes();
51
+ const middleware = jwtAuth(authService, logger);
52
+ await middleware(req, res, next);
53
+ expect(verifyToken).toHaveBeenCalledWith('Bearer query-jwt');
54
+ expect(req.authContext).toEqual(ctx);
55
+ expect(next).toHaveBeenCalledOnce();
56
+ });
57
+ it('passes undefined to verifyToken when no credentials are present', async () => {
58
+ verifyToken.mockResolvedValue(undefined);
59
+ const req = makeReq({});
60
+ const res = makeRes();
61
+ const middleware = jwtAuth(authService, logger);
62
+ await middleware(req, res, next);
63
+ expect(verifyToken).toHaveBeenCalledWith(undefined);
64
+ expect(req.authContext).toBeUndefined();
65
+ expect(next).toHaveBeenCalledOnce();
66
+ });
67
+ it('returns 401 JSON when verification throws', async () => {
68
+ verifyToken.mockRejectedValue(new Error('bad sig'));
69
+ const req = makeReq({
70
+ headers: { authorization: 'Bearer x' },
71
+ });
72
+ const res = makeRes();
73
+ const middleware = jwtAuth(authService, logger);
74
+ await middleware(req, res, next);
75
+ expect(next).not.toHaveBeenCalled();
76
+ expect(status).toHaveBeenCalledWith(401);
77
+ expect(json).toHaveBeenCalledWith({
78
+ error: 'Invalid or expired token: bad sig',
79
+ });
80
+ });
81
+ it('stringifies non-Error rejection values in the response', async () => {
82
+ verifyToken.mockRejectedValue('rejected');
83
+ const req = makeReq({
84
+ headers: { authorization: 'Bearer x' },
85
+ });
86
+ const res = makeRes();
87
+ const middleware = jwtAuth(authService, logger);
88
+ await middleware(req, res, next);
89
+ expect(json).toHaveBeenCalledWith({
90
+ error: 'Invalid or expired token: rejected',
91
+ });
92
+ });
93
+ });
@@ -24,6 +24,18 @@ describe('ipRateLimitKeyGenerator', () => {
24
24
  };
25
25
  expect(ipRateLimitKeyGenerator(req)).toBe('ip:2001:db8:abcd::/56');
26
26
  });
27
+ test('falls back to socket.remoteAddress when ip is unset', () => {
28
+ const req = {
29
+ socket: { remoteAddress: '10.0.0.2' },
30
+ };
31
+ expect(ipRateLimitKeyGenerator(req)).toBe('ip:10.0.0.2');
32
+ });
33
+ test('uses unknown when neither ip nor remoteAddress is set', () => {
34
+ const req = {
35
+ socket: {},
36
+ };
37
+ expect(ipRateLimitKeyGenerator(req)).toBe('ip:unknown');
38
+ });
27
39
  });
28
40
  describe('rateLimitKeyGenerator', () => {
29
41
  test('uses authenticated user id when available', () => {
@@ -12,7 +12,7 @@ export function sessionHandler(store, allowedPaths, logger) {
12
12
  const context = req.authContext;
13
13
  const allowedMethods = allowedPaths[req.baseUrl];
14
14
  if (req.method !== 'POST') {
15
- logger.debug('Skipping authentication for OPTIONS request to ' + req.baseUrl);
15
+ logger.debug(`Skipping authentication for ${req.method} request to ${req.baseUrl}`);
16
16
  next();
17
17
  }
18
18
  else if (allowedMethods &&
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sessionHandler.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionHandler.test.d.ts","sourceRoot":"","sources":["../../src/middleware/sessionHandler.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,121 @@
1
+ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { pino } from 'pino';
5
+ import { sink } from 'pino-test';
6
+ import { sessionHandler } from './sessionHandler.js';
7
+ describe('sessionHandler', () => {
8
+ const getSession = vi.fn();
9
+ const withAuthContext = vi.fn(() => ({ getSession }));
10
+ const store = { withAuthContext };
11
+ const logger = pino({ level: 'silent' }, sink());
12
+ const allowedPaths = {
13
+ '/api/v0/user': ['addSession', 'listNetworks', 'getUser'],
14
+ '/api/v0/dapp': ['*'],
15
+ };
16
+ let next;
17
+ let status;
18
+ let json;
19
+ const authContext = {
20
+ userId: 'user-1',
21
+ accessToken: 'access-token',
22
+ };
23
+ beforeEach(() => {
24
+ getSession.mockReset();
25
+ withAuthContext.mockClear();
26
+ withAuthContext.mockReturnValue({ getSession });
27
+ next = vi.fn();
28
+ status = vi.fn().mockReturnThis();
29
+ json = vi.fn();
30
+ });
31
+ function makeReq(partial) {
32
+ return {
33
+ method: 'POST',
34
+ baseUrl: '/api/v0/user',
35
+ body: { method: 'listWallets' },
36
+ authContext,
37
+ ...partial,
38
+ };
39
+ }
40
+ function makeRes() {
41
+ return { status, json };
42
+ }
43
+ it('skips session check for non-POST requests', async () => {
44
+ const req = makeReq({ method: 'GET' });
45
+ const res = makeRes();
46
+ const middleware = sessionHandler(store, allowedPaths, logger);
47
+ await middleware(req, res, next);
48
+ expect(withAuthContext).not.toHaveBeenCalled();
49
+ expect(next).toHaveBeenCalledOnce();
50
+ expect(status).not.toHaveBeenCalled();
51
+ });
52
+ it('allows unauthenticated POST when the RPC method is on the allow list', async () => {
53
+ const req = makeReq({ body: { method: 'addSession' } });
54
+ const res = makeRes();
55
+ const middleware = sessionHandler(store, allowedPaths, logger);
56
+ await middleware(req, res, next);
57
+ expect(withAuthContext).not.toHaveBeenCalled();
58
+ expect(next).toHaveBeenCalledOnce();
59
+ expect(status).not.toHaveBeenCalled();
60
+ });
61
+ it('allows unauthenticated POST when the path uses a wildcard allow list', async () => {
62
+ const req = makeReq({
63
+ baseUrl: '/api/v0/dapp',
64
+ body: { method: 'connect' },
65
+ });
66
+ const res = makeRes();
67
+ const middleware = sessionHandler(store, allowedPaths, logger);
68
+ await middleware(req, res, next);
69
+ expect(withAuthContext).not.toHaveBeenCalled();
70
+ expect(next).toHaveBeenCalledOnce();
71
+ });
72
+ it('calls next when an active session exists for a protected method', async () => {
73
+ getSession.mockResolvedValue({
74
+ id: 'session-1',
75
+ network: 'network1',
76
+ accessToken: 'session-token',
77
+ });
78
+ const req = makeReq({ body: { method: 'listWallets' } });
79
+ const res = makeRes();
80
+ const middleware = sessionHandler(store, allowedPaths, logger);
81
+ await middleware(req, res, next);
82
+ expect(withAuthContext).toHaveBeenCalledWith(authContext);
83
+ expect(getSession).toHaveBeenCalled();
84
+ expect(next).toHaveBeenCalledOnce();
85
+ expect(status).not.toHaveBeenCalled();
86
+ });
87
+ it('returns 401 when no session exists for a protected method', async () => {
88
+ getSession.mockResolvedValue(undefined);
89
+ const req = makeReq({ body: { method: 'listWallets' } });
90
+ const res = makeRes();
91
+ const middleware = sessionHandler(store, allowedPaths, logger);
92
+ await middleware(req, res, next);
93
+ expect(withAuthContext).toHaveBeenCalledWith(authContext);
94
+ expect(next).not.toHaveBeenCalled();
95
+ expect(status).toHaveBeenCalledWith(401);
96
+ expect(json).toHaveBeenCalledWith({
97
+ error: 'No active session found',
98
+ });
99
+ });
100
+ it('requires a session when the path is not in the allow list config', async () => {
101
+ getSession.mockResolvedValue(undefined);
102
+ const req = makeReq({
103
+ baseUrl: '/api/v0/other',
104
+ body: { method: 'addSession' },
105
+ });
106
+ const res = makeRes();
107
+ const middleware = sessionHandler(store, allowedPaths, logger);
108
+ await middleware(req, res, next);
109
+ expect(withAuthContext).toHaveBeenCalledWith(authContext);
110
+ expect(status).toHaveBeenCalledWith(401);
111
+ });
112
+ it('requires a session when the RPC method is not on the path allow list', async () => {
113
+ getSession.mockResolvedValue(undefined);
114
+ const req = makeReq({ body: { method: 'removeSession' } });
115
+ const res = makeRes();
116
+ const middleware = sessionHandler(store, allowedPaths, logger);
117
+ await middleware(req, res, next);
118
+ expect(withAuthContext).toHaveBeenCalledWith(authContext);
119
+ expect(status).toHaveBeenCalledWith(401);
120
+ });
121
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=NotificationService.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NotificationService.test.d.ts","sourceRoot":"","sources":["../../src/notification/NotificationService.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,110 @@
1
+ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import pino from 'pino';
5
+ import { consecutive, once, sink } from 'pino-test';
6
+ import { NotificationService } from './NotificationService.js';
7
+ describe('NotificationService', () => {
8
+ let mockLogger;
9
+ let logStream;
10
+ beforeEach(() => {
11
+ logStream = sink();
12
+ mockLogger = pino({ level: 'debug' }, logStream);
13
+ });
14
+ it('creates a notifier for a new notifierId', () => {
15
+ const service = new NotificationService(mockLogger);
16
+ const notifier = service.getNotifier('user-1');
17
+ expect(notifier).toBeDefined();
18
+ expect(typeof notifier.on).toBe('function');
19
+ expect(typeof notifier.emit).toBe('function');
20
+ expect(typeof notifier.removeListener).toBe('function');
21
+ });
22
+ it('returns the same notifier instance for the same notifierId', () => {
23
+ const service = new NotificationService(mockLogger);
24
+ const notifier1 = service.getNotifier('user-1');
25
+ const notifier2 = service.getNotifier('user-1');
26
+ expect(notifier1).toBe(notifier2);
27
+ });
28
+ it('returns different notifier instances for different notifierIds', () => {
29
+ const service = new NotificationService(mockLogger);
30
+ const notifier1 = service.getNotifier('user-1');
31
+ const notifier2 = service.getNotifier('user-2');
32
+ expect(notifier1).not.toBe(notifier2);
33
+ });
34
+ it('calls listeners when an event is emitted', () => {
35
+ const service = new NotificationService(mockLogger);
36
+ const notifier = service.getNotifier('user-1');
37
+ const listener = vi.fn();
38
+ notifier.on('txChanged', listener);
39
+ const result = notifier.emit('txChanged', { id: 123 });
40
+ expect(result).toBe(true);
41
+ expect(listener).toHaveBeenCalledTimes(1);
42
+ expect(listener).toHaveBeenCalledWith({ id: 123 });
43
+ });
44
+ it('returns false when emitting an event with no listeners', async () => {
45
+ const service = new NotificationService(mockLogger);
46
+ const notifier = service.getNotifier('user-1');
47
+ const result = notifier.emit('unknown-event');
48
+ expect(result).toBe(false);
49
+ await once(logStream, {
50
+ level: 20,
51
+ event: 'unknown-event',
52
+ args: [],
53
+ msg: 'Notifier emitted event: unknown-event for user-1',
54
+ });
55
+ });
56
+ it('logs every emitted event', async () => {
57
+ const service = new NotificationService(mockLogger);
58
+ const notifier = service.getNotifier('user-1');
59
+ notifier.emit('txChanged', { id: 123 });
60
+ await once(logStream, {
61
+ level: 20,
62
+ event: 'txChanged',
63
+ args: [{ id: 123 }],
64
+ msg: 'Notifier emitted event: txChanged for user-1',
65
+ });
66
+ });
67
+ it('removes listeners with removeListener', () => {
68
+ const service = new NotificationService(mockLogger);
69
+ const notifier = service.getNotifier('user-1');
70
+ const listener = vi.fn();
71
+ notifier.on('txChanged', listener);
72
+ notifier.removeListener('txChanged', listener);
73
+ const result = notifier.emit('txChanged', { id: 123 });
74
+ expect(result).toBe(false);
75
+ expect(listener).not.toHaveBeenCalled();
76
+ });
77
+ it('keeps listeners isolated between notifiers', () => {
78
+ const service = new NotificationService(mockLogger);
79
+ const notifier1 = service.getNotifier('user-1');
80
+ const notifier2 = service.getNotifier('user-2');
81
+ const listener1 = vi.fn();
82
+ const listener2 = vi.fn();
83
+ notifier1.on('txChanged', listener1);
84
+ notifier2.on('txChanged', listener2);
85
+ notifier1.emit('txChanged', { id: 1 });
86
+ expect(listener1).toHaveBeenCalledWith({ id: 1 });
87
+ expect(listener2).not.toHaveBeenCalled();
88
+ });
89
+ it('logs with the correct notifierId for each notifier', async () => {
90
+ const service = new NotificationService(mockLogger);
91
+ const notifier1 = service.getNotifier('user-1');
92
+ const notifier2 = service.getNotifier('user-2');
93
+ notifier1.emit('txChanged');
94
+ notifier2.emit('statusChanged');
95
+ await consecutive(logStream, [
96
+ {
97
+ level: 20,
98
+ event: 'txChanged',
99
+ args: [],
100
+ msg: 'Notifier emitted event: txChanged for user-1',
101
+ },
102
+ {
103
+ level: 20,
104
+ event: 'statusChanged',
105
+ args: [],
106
+ msg: 'Notifier emitted event: statusChanged for user-2',
107
+ },
108
+ ]);
109
+ });
110
+ });
@@ -0,0 +1,16 @@
1
+ import { Logger } from 'pino';
2
+ import { AuthAware, AuthContext, AuthTokenProvider } from '@canton-network/core-wallet-auth';
3
+ import { Network, Store } from '@canton-network/core-wallet-store';
4
+ export type AccessTokenProviderFactory = (network: Network) => Promise<AuthTokenProvider>;
5
+ export interface AutomationRunContext {
6
+ authContext: AuthContext;
7
+ scopedStore: Store;
8
+ network: Network;
9
+ }
10
+ /**
11
+ * Resolves auth for background completion of pending external transactions.
12
+ * Service-account networks can run without a pre-existing session by minting
13
+ * an access token. Interactive networks still require a valid stored session.
14
+ */
15
+ export declare function resolveAutomationRunContext(bootstrapStore: Store & AuthAware<Store>, userId: string, networkId: string, logger: Logger): Promise<AutomationRunContext | undefined>;
16
+ //# sourceMappingURL=service-account-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-account-session.d.ts","sourceRoot":"","sources":["../../src/signing/service-account-session.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAC7B,OAAO,EACH,SAAS,EACT,WAAW,EACX,iBAAiB,EACpB,MAAM,kCAAkC,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,mCAAmC,CAAA;AAElE,MAAM,MAAM,0BAA0B,GAAG,CACrC,OAAO,EAAE,OAAO,KACf,OAAO,CAAC,iBAAiB,CAAC,CAAA;AAE/B,MAAM,WAAW,oBAAoB;IACjC,WAAW,EAAE,WAAW,CAAA;IACxB,WAAW,EAAE,KAAK,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;CACnB;AAED;;;;GAIG;AACH,wBAAsB,2BAA2B,CAC7C,cAAc,EAAE,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,EACxC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAkC3C"}
@@ -0,0 +1,25 @@
1
+ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { AuthTokenProvider, } from '@canton-network/core-wallet-auth';
4
+ /**
5
+ * Resolves auth for background completion of pending external transactions.
6
+ * Service-account networks can run without a pre-existing session by minting
7
+ * an access token. Interactive networks still require a valid stored session.
8
+ */
9
+ export async function resolveAutomationRunContext(bootstrapStore, userId, networkId, logger) {
10
+ const network = await bootstrapStore.getNetwork(networkId);
11
+ if (!network) {
12
+ logger.warn({ userId, networkId }, 'Skipping signing worker tick: network not found');
13
+ return undefined;
14
+ }
15
+ if (!network.serviceAccountAuth) {
16
+ logger.debug({ userId, networkId }, 'Skipping signing worker tick: network is not service-account');
17
+ return undefined;
18
+ }
19
+ const idp = await bootstrapStore.getIdp(network.identityProviderId);
20
+ const provider = AuthTokenProvider.fromGatewayConfig(idp, network.serviceAccountAuth, logger);
21
+ const authContext = await provider.getAuthContext();
22
+ const scopedStore = bootstrapStore.withAuthContext(authContext);
23
+ logger.debug({ userId, networkId }, 'Signing worker prepared service account session');
24
+ return { authContext, scopedStore, network };
25
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=service-account-session.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-account-session.test.d.ts","sourceRoot":"","sources":["../../src/signing/service-account-session.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,89 @@
1
+ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+ vi.mock('uuid', () => ({ v4: vi.fn(() => 'session-new') }));
5
+ import { pino } from 'pino';
6
+ import { sink } from 'pino-test';
7
+ import { resolveAutomationRunContext } from './service-account-session.js';
8
+ vi.mock('@canton-network/core-wallet-auth', async () => {
9
+ const actual = await vi.importActual('@canton-network/core-wallet-auth');
10
+ return {
11
+ ...actual,
12
+ AuthTokenProvider: {
13
+ fromGatewayConfig: vi.fn().mockReturnValue({
14
+ getAuthContext: vi.fn().mockResolvedValue({
15
+ userId: 'service_account',
16
+ accessToken: 'abc',
17
+ }),
18
+ }),
19
+ },
20
+ };
21
+ });
22
+ const idp = {
23
+ id: 'idp1',
24
+ type: 'oauth',
25
+ issuer: 'https://issuer.example',
26
+ configUrl: 'https://issuer.example/.well-known/openid-configuration',
27
+ };
28
+ const m2mNetwork = {
29
+ id: 'net-m2m',
30
+ name: 'm2m',
31
+ description: '',
32
+ synchronizerId: 'sync::fp',
33
+ identityProviderId: 'idp1',
34
+ ledgerApi: { baseUrl: 'http://ledger' },
35
+ auth: {
36
+ method: 'authorization_code',
37
+ clientId: 'svc',
38
+ audience: 'aud',
39
+ scope: 'scope',
40
+ },
41
+ serviceAccountAuth: {
42
+ method: 'client_credentials',
43
+ clientId: 'svc',
44
+ clientSecret: 'secret',
45
+ audience: 'aud',
46
+ scope: 'scope',
47
+ },
48
+ };
49
+ const logger = pino({ level: 'silent' }, sink());
50
+ describe('resolveAutomationRunContext', () => {
51
+ afterEach(() => vi.clearAllMocks());
52
+ it('reuses a valid client-credentials session', async () => {
53
+ const session = {
54
+ id: 'session-1',
55
+ network: 'net-m2m',
56
+ accessToken: 'abc',
57
+ };
58
+ const scopedStore = { setSession: vi.fn() };
59
+ const store = {
60
+ getIdp: vi.fn().mockResolvedValue(idp),
61
+ getNetwork: vi.fn().mockResolvedValue(m2mNetwork),
62
+ withAuthContext: vi.fn().mockReturnValue(scopedStore),
63
+ };
64
+ const result = await resolveAutomationRunContext(store, 'service_account', 'net-m2m', logger);
65
+ expect(result?.authContext).toEqual({
66
+ userId: 'service_account',
67
+ accessToken: session.accessToken,
68
+ });
69
+ });
70
+ it('mints a token when no usable session exists', async () => {
71
+ const scopedStore = { setSession: vi.fn() };
72
+ const store = {
73
+ getIdp: vi.fn().mockResolvedValue(idp),
74
+ getNetwork: vi.fn().mockResolvedValue(m2mNetwork),
75
+ withAuthContext: vi.fn().mockReturnValue(scopedStore),
76
+ };
77
+ const result = await resolveAutomationRunContext(store, 'service_account', 'net-m2m', logger);
78
+ expect(result?.authContext.userId).toBe('service_account');
79
+ });
80
+ it('mints when the stored session is not a client-credentials token', async () => {
81
+ const scopedStore = { setSession: vi.fn() };
82
+ const store = {
83
+ getIdp: vi.fn().mockResolvedValue(idp),
84
+ getNetwork: vi.fn().mockResolvedValue(m2mNetwork),
85
+ withAuthContext: vi.fn().mockReturnValue(scopedStore),
86
+ };
87
+ await resolveAutomationRunContext(store, 'service_account', 'net-m2m', logger);
88
+ });
89
+ });
@@ -0,0 +1,7 @@
1
+ import { SigningDriverInterface, SigningProvider } from '@canton-network/core-signing-lib';
2
+ /**
3
+ * Signing drivers registered at Wallet Gateway startup.
4
+ * Partial because only configured providers are present (e.g. Dfns when env vars are set).
5
+ */
6
+ export type SigningDrivers = Partial<Record<SigningProvider, SigningDriverInterface>>;
7
+ //# sourceMappingURL=signing-drivers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signing-drivers.d.ts","sourceRoot":"","sources":["../../src/signing/signing-drivers.ts"],"names":[],"mappings":"AAGA,OAAO,EACH,sBAAsB,EACtB,eAAe,EAClB,MAAM,kCAAkC,CAAA;AAEzC;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,CAChC,MAAM,CAAC,eAAe,EAAE,sBAAsB,CAAC,CAClD,CAAA"}
@@ -0,0 +1,3 @@
1
+ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ export {};
@@ -0,0 +1,83 @@
1
+ import { Logger } from 'pino';
2
+ import { AuthAware } from '@canton-network/core-wallet-auth';
3
+ import { Store } from '@canton-network/core-wallet-store';
4
+ import { NotificationService } from '../notification/NotificationService.js';
5
+ import type { SigningDrivers } from './signing-drivers.js';
6
+ export type { AccessTokenProviderFactory } from './service-account-session.js';
7
+ /** Configuration for {@link SigningWorker}. */
8
+ export interface SigningWorkerOptions {
9
+ /** Poll interval in ms (`server.signingWorker.pollInterval`, default 5000). */
10
+ intervalMs: number;
11
+ signingDrivers: SigningDrivers;
12
+ store: Store & AuthAware<Store>;
13
+ notificationService: NotificationService;
14
+ logger: Logger;
15
+ }
16
+ /**
17
+ * Background poller that completes service-account transactions stuck in external
18
+ * custody approval.
19
+ *
20
+ * ## When this runs
21
+ *
22
+ * During M2M automation (`prepareExecute` → `signAndExecute`), external signing
23
+ * providers (Fireblocks, Blockdaemon, Dfns) may return `pending` instead of a
24
+ * signature. The gateway persists the transaction with `status: 'pending'` and
25
+ * an `externalTxId` from the provider. This worker picks up those rows on each
26
+ * tick via {@link Store.listAllPendingTransactions} (rows with an
27
+ * `externalTxId` are processed; others are skipped).
28
+ *
29
+ * Participant and wallet-kernel sign synchronously and never appear here.
30
+ *
31
+ * ## What happens on each tick
32
+ *
33
+ * For every pending row the worker resolves an M2M auth context
34
+ * ({@link resolveAutomationRunContext}), loads the user's primary wallet, and
35
+ * calls {@link TransactionService.signAndExecute}.
36
+ *
37
+ * Although the worker always invokes `signAndExecute`, **retries do not submit a
38
+ * new sign request**. `TransactionService.sign()` checks whether the stored
39
+ * transaction already has an `externalTxId`:
40
+ *
41
+ * - **First pass** (no `externalTxId`): `driver.signTransaction()` submits to
42
+ * custody and stores the returned `externalTxId`.
43
+ * - **Subsequent ticks** (`externalTxId` present): `driver.getTransaction()`
44
+ * polls the existing custody request for an updated status/signature.
45
+ *
46
+ * `signAndExecute` then:
47
+ *
48
+ * - returns early while the provider still reports `pending` (worker logs and
49
+ * waits for the next tick), or
50
+ * - calls `execute()` once signing is `signed` (Dfns is special: it broadcasts
51
+ * at sign-time, so execute only reconciles local state).
52
+ *
53
+ * A transaction is re-read before processing so a concurrent DApp call or an
54
+ * earlier tick that already completed it is skipped.
55
+ *
56
+ * ## Lifecycle
57
+ *
58
+ * Started once at gateway init (`start()`). Ticks are serialized: if a tick is
59
+ * still running, the next interval callback is a no-op. Use `stop()` on shutdown.
60
+ */
61
+ export declare class SigningWorker {
62
+ private readonly options;
63
+ private timer;
64
+ private running;
65
+ constructor(options: SigningWorkerOptions);
66
+ /** Starts the interval timer. Idempotent if already started. */
67
+ start(): void;
68
+ /** Clears the interval timer. */
69
+ stop(): void;
70
+ /**
71
+ * One poll cycle: list all pending transactions and attempt to complete
72
+ * external-custody rows via {@link processPending}. Safe to call directly in
73
+ * tests.
74
+ */
75
+ tick(): Promise<void>;
76
+ /**
77
+ * Drives a single pending transaction through `signAndExecute`. Skips when
78
+ * automation context, submitting wallet, or refreshed `pending` status is missing.
79
+ * Errors are logged per transaction; other rows in the same tick still run.
80
+ */
81
+ private processPending;
82
+ }
83
+ //# sourceMappingURL=signing-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signing-worker.d.ts","sourceRoot":"","sources":["../../src/signing/signing-worker.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAA;AAC5D,OAAO,EAAE,KAAK,EAAuB,MAAM,mCAAmC,CAAA;AAC9E,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AAE5E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAG1D,YAAY,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAA;AAE9E,+CAA+C;AAC/C,MAAM,WAAW,oBAAoB;IACjC,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,cAAc,CAAA;IAC9B,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,CAAA;IAC/B,mBAAmB,EAAE,mBAAmB,CAAA;IACxC,MAAM,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,qBAAa,aAAa;IAIV,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,OAAO,CAAQ;gBAEM,OAAO,EAAE,oBAAoB;IAE1D,gEAAgE;IAChE,KAAK,IAAI,IAAI;IAcb,iCAAiC;IACjC,IAAI,IAAI,IAAI;IAOZ;;;;OAIG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B3B;;;;OAIG;YACW,cAAc;CAqH/B"}