@canton-network/wallet-gateway-remote 1.4.0 → 1.5.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 (116) 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 +60 -4
  10. package/dist/config/Config.d.ts.map +1 -1
  11. package/dist/config/Config.js +1 -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.map +1 -1
  15. package/dist/dapp-api/controller.js +54 -24
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +10 -0
  18. package/dist/init.d.ts.map +1 -1
  19. package/dist/init.js +3 -6
  20. package/dist/ledger/transaction-service.d.ts +14 -11
  21. package/dist/ledger/transaction-service.d.ts.map +1 -1
  22. package/dist/ledger/transaction-service.js +95 -8
  23. package/dist/ledger/transaction-service.test.d.ts +2 -0
  24. package/dist/ledger/transaction-service.test.d.ts.map +1 -0
  25. package/dist/ledger/transaction-service.test.js +356 -0
  26. package/dist/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.d.ts +5 -2
  27. package/dist/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.d.ts.map +1 -1
  28. package/dist/ledger/wallet-allocation/signing-providers/fireblocks-wallet-allocator.js +7 -6
  29. package/dist/ledger/wallet-allocation/wallet-allocation-service.d.ts +11 -5
  30. package/dist/ledger/wallet-allocation/wallet-allocation-service.d.ts.map +1 -1
  31. package/dist/ledger/wallet-allocation/wallet-allocation-service.js +28 -14
  32. package/dist/ledger/wallet-allocation/wallet-allocation-service.test.js +550 -19
  33. package/dist/ledger/wallet-sync-service.js +3 -3
  34. package/dist/middleware/apiKeyAuth.d.ts +6 -0
  35. package/dist/middleware/apiKeyAuth.d.ts.map +1 -0
  36. package/dist/middleware/apiKeyAuth.js +88 -0
  37. package/dist/middleware/apiKeyAuth.test.d.ts +2 -0
  38. package/dist/middleware/apiKeyAuth.test.d.ts.map +1 -0
  39. package/dist/middleware/apiKeyAuth.test.js +126 -0
  40. package/dist/middleware/jsonRpcHandler.d.ts +11 -1
  41. package/dist/middleware/jsonRpcHandler.d.ts.map +1 -1
  42. package/dist/middleware/jsonRpcHandler.js +14 -15
  43. package/dist/middleware/jsonRpcHandler.test.d.ts +2 -0
  44. package/dist/middleware/jsonRpcHandler.test.d.ts.map +1 -0
  45. package/dist/middleware/jsonRpcHandler.test.js +254 -0
  46. package/dist/middleware/jwtAuth.d.ts.map +1 -1
  47. package/dist/middleware/jwtAuth.js +4 -0
  48. package/dist/middleware/jwtAuth.test.d.ts +2 -0
  49. package/dist/middleware/jwtAuth.test.d.ts.map +1 -0
  50. package/dist/middleware/jwtAuth.test.js +93 -0
  51. package/dist/middleware/rateLimit.test.js +12 -0
  52. package/dist/middleware/sessionHandler.js +1 -1
  53. package/dist/middleware/sessionHandler.test.d.ts +2 -0
  54. package/dist/middleware/sessionHandler.test.d.ts.map +1 -0
  55. package/dist/middleware/sessionHandler.test.js +121 -0
  56. package/dist/notification/NotificationService.test.d.ts +2 -0
  57. package/dist/notification/NotificationService.test.d.ts.map +1 -0
  58. package/dist/notification/NotificationService.test.js +110 -0
  59. package/dist/signing/signing-drivers.d.ts +7 -0
  60. package/dist/signing/signing-drivers.d.ts.map +1 -0
  61. package/dist/signing/signing-drivers.js +3 -0
  62. package/dist/user-api/controller.d.ts +6 -4
  63. package/dist/user-api/controller.d.ts.map +1 -1
  64. package/dist/user-api/controller.js +111 -94
  65. package/dist/user-api/controller.test.d.ts +2 -0
  66. package/dist/user-api/controller.test.d.ts.map +1 -0
  67. package/dist/user-api/controller.test.js +862 -0
  68. package/dist/user-api/rpc-gen/index.d.ts +12 -0
  69. package/dist/user-api/rpc-gen/index.d.ts.map +1 -1
  70. package/dist/user-api/rpc-gen/index.js +4 -0
  71. package/dist/user-api/rpc-gen/typings.d.ts +52 -3
  72. package/dist/user-api/rpc-gen/typings.d.ts.map +1 -1
  73. package/dist/utils.d.ts +17 -1
  74. package/dist/utils.d.ts.map +1 -1
  75. package/dist/utils.js +53 -2
  76. package/dist/utils.test.d.ts +2 -0
  77. package/dist/utils.test.d.ts.map +1 -0
  78. package/dist/utils.test.js +24 -0
  79. package/dist/web/frontend/404/index.html +2 -2
  80. package/dist/web/frontend/activities/index.html +3 -3
  81. package/dist/web/frontend/approve/index.html +4 -4
  82. package/dist/web/frontend/assets/404-MVLWUPwN.js +5 -0
  83. package/dist/web/frontend/assets/{activities-NY8oZAkH.js → activities-DaWkmGqA.js} +2 -2
  84. package/dist/web/frontend/assets/{addIdentityProvider-38ghz3RT.js → addIdentityProvider--rBb0ihb.js} +2 -2
  85. package/dist/web/frontend/assets/{addNetwork-BaDhbrZl.js → addNetwork-C6UDsRkU.js} +4 -4
  86. package/dist/web/frontend/assets/addParty-B3fFD1eM.js +45 -0
  87. package/dist/web/frontend/assets/approve-WbGL4E82.js +21 -0
  88. package/dist/web/frontend/assets/callback--tihEryB.js +1 -0
  89. package/dist/web/frontend/assets/{identityProviders-BZ9h_VOJ.js → identityProviders-Dra4qG6t.js} +2 -2
  90. package/dist/web/frontend/assets/{index-B-HiKugW.js → index-Bm76pXA_.js} +2 -2
  91. package/dist/web/frontend/assets/index-D1dC4Cl9.js +4477 -0
  92. package/dist/web/frontend/assets/{login-CqwMEG9J.js → login-BC8a-kD_.js} +2 -2
  93. package/dist/web/frontend/assets/{networks-BJvRi47M.js → networks-B3ztsogL.js} +2 -2
  94. package/dist/web/frontend/assets/{reviewIdentityProvider-ySlQnw5R.js → reviewIdentityProvider-Co420Dbk.js} +3 -3
  95. package/dist/web/frontend/assets/{reviewNetwork-DrVxeHd4.js → reviewNetwork-B-Sh8pCH.js} +3 -3
  96. package/dist/web/frontend/assets/{settings-BVNkaUyJ.js → settings-DvyUjqLl.js} +1 -1
  97. package/dist/web/frontend/assets/{state-qAsNw5qf.js → state-Bq65GwdF.js} +1 -1
  98. package/dist/web/frontend/assets/{utils-BDkHxi8V.js → utils-B2iziW8V.js} +1 -1
  99. package/dist/web/frontend/callback/index.html +2 -2
  100. package/dist/web/frontend/identity-providers/add/index.html +3 -3
  101. package/dist/web/frontend/identity-providers/index.html +3 -3
  102. package/dist/web/frontend/identity-providers/review/index.html +3 -3
  103. package/dist/web/frontend/index.html +1 -1
  104. package/dist/web/frontend/login/index.html +3 -3
  105. package/dist/web/frontend/networks/add/index.html +3 -3
  106. package/dist/web/frontend/networks/index.html +3 -3
  107. package/dist/web/frontend/networks/review/index.html +3 -3
  108. package/dist/web/frontend/parties/add/index.html +5 -5
  109. package/dist/web/frontend/parties/index.html +4 -4
  110. package/dist/web/frontend/settings/index.html +3 -3
  111. package/package.json +36 -32
  112. package/dist/web/frontend/assets/404-C7V2yl2z.js +0 -5
  113. package/dist/web/frontend/assets/addParty-j3JWZX5a.js +0 -41
  114. package/dist/web/frontend/assets/approve-43nVzAAC.js +0 -21
  115. package/dist/web/frontend/assets/callback-BBMiCjHR.js +0 -1
  116. package/dist/web/frontend/assets/index-DoNqknyE.js +0 -4042
@@ -297,7 +297,7 @@ export class WalletSyncService {
297
297
  reason: WALLET_DISABLED_REASON.NO_SIGNING_PROVIDER_MATCHED,
298
298
  }),
299
299
  };
300
- this.logger.info({ ...wallet }, 'Wallet sync result');
300
+ this.logger.info({ wallet }, 'Wallet sync result');
301
301
  await this.store.addWallet(wallet);
302
302
  return wallet;
303
303
  }));
@@ -324,7 +324,7 @@ export class WalletSyncService {
324
324
  try {
325
325
  const participantNamespace = await this.getParticipantNamespace();
326
326
  const network = await this.store.getCurrentNetwork();
327
- this.logger.info(network, 'Current network');
327
+ this.logger.info({ network }, 'Current network');
328
328
  const { rightsByParty, rightsByUser } = await this.getRightsSnapshot();
329
329
  const partiesWithRights = Array.from(rightsByParty.keys());
330
330
  await this.store.setUserRights(network.id, [
@@ -332,7 +332,7 @@ export class WalletSyncService {
332
332
  new Set()),
333
333
  ]);
334
334
  const existingWallets = await this.store.getWallets();
335
- this.logger.info(existingWallets, 'Existing wallets');
335
+ this.logger.info({ existingWallets }, 'Existing wallets');
336
336
  // Skips wallets for which we didn't allocate a party
337
337
  const existingAllocatedWallets = existingWallets.filter((w) => w.status === 'allocated');
338
338
  const existingPartiesOnNetwork = new Set(existingAllocatedWallets.map((w) => `${w.partyId}:${w.networkId}`));
@@ -0,0 +1,6 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ import { AuthAware } from '@canton-network/core-wallet-auth';
3
+ import { Logger } from 'pino';
4
+ import { Store } from '@canton-network/core-wallet-store';
5
+ export declare function apiKeyAuth(store: Store & AuthAware<Store>, dappApiPath: string, logger: Logger): (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
6
+ //# sourceMappingURL=apiKeyAuth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiKeyAuth.d.ts","sourceRoot":"","sources":["../../src/middleware/apiKeyAuth.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC9D,OAAO,EACH,SAAS,EAGZ,MAAM,kCAAkC,CAAA;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAC7B,OAAO,EAAE,KAAK,EAAE,MAAM,mCAAmC,CAAA;AAMzD,wBAAgB,UAAU,CACtB,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,EAC/B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,IAEA,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,wDAuHhE"}
@@ -0,0 +1,88 @@
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
+ import crypto from 'crypto';
5
+ import { v4 } from 'uuid';
6
+ import { rpcErrors } from '@canton-network/core-rpc-errors';
7
+ import { jsonRpcResponse } from '@canton-network/core-rpc-transport';
8
+ export function apiKeyAuth(store, dappApiPath, logger) {
9
+ return async (req, res, next) => {
10
+ const authHeader = req.headers.authorization;
11
+ // skip if no Authorization header or if it doesn't start with "ApiKey"
12
+ if (!authHeader?.startsWith('ApiKey')) {
13
+ logger.trace('No API Key provided, skipping API Key authentication');
14
+ return next();
15
+ }
16
+ const path = req.baseUrl + req.path;
17
+ const reqId = req.body?.id ?? null;
18
+ logger.trace({ baseUrl: req.baseUrl, path: req.path }, 'Received request with API Key authentication');
19
+ // reject if the path is not in the allowed list
20
+ if (!path.includes(dappApiPath)) {
21
+ logger.debug(`Path ${path} is not in the allowed list, rejecting API Key authentication`);
22
+ return res.status(401).json(jsonRpcResponse(reqId, {
23
+ error: {
24
+ code: rpcErrors.invalidRequest().code,
25
+ message: `Requested path '${path}' cannot be called with API Key authentication`,
26
+ },
27
+ }));
28
+ }
29
+ const apiKey = authHeader.slice('ApiKey'.length).trim();
30
+ const hashedApiKey = crypto
31
+ .createHash('sha256')
32
+ .update(apiKey)
33
+ .digest('hex');
34
+ const matchingKey = await store.getApiKey(hashedApiKey);
35
+ if (matchingKey) {
36
+ logger.debug({ apiKeyId: matchingKey.id, userId: matchingKey.userId }, 'API Key authentication successful');
37
+ // temporary auth context to access the store with the API key user
38
+ const authStore = store.withAuthContext({
39
+ userId: matchingKey.userId,
40
+ accessToken: 'unused',
41
+ });
42
+ // automatically initiate a session for the API key user
43
+ await authStore.setSession({
44
+ id: v4(),
45
+ network: matchingKey.networkId,
46
+ accessToken: 'unused',
47
+ });
48
+ const network = await authStore.getNetwork(matchingKey.networkId);
49
+ const idp = await authStore.getIdp(network.identityProviderId);
50
+ if (!network.serviceAccountAuth) {
51
+ return res.status(401).json(jsonRpcResponse(reqId, {
52
+ error: {
53
+ code: rpcErrors.invalidRequest().code,
54
+ message: `Network '${network.id}' does not have a service account configured`,
55
+ },
56
+ }));
57
+ }
58
+ if (network.serviceAccountAuth.method !== 'client_credentials') {
59
+ return res.status(401).json(jsonRpcResponse(reqId, {
60
+ error: {
61
+ code: rpcErrors.invalidRequest().code,
62
+ message: `Network '${network.id}' does not have a service account configured with client_credentials method`,
63
+ },
64
+ }));
65
+ }
66
+ const accessTokenProvider = AuthTokenProvider.fromGatewayConfig(idp, network.serviceAccountAuth, logger);
67
+ const serviceAccountCtx = await accessTokenProvider.getAuthContext();
68
+ const context = {
69
+ isApiKey: true,
70
+ userId: matchingKey.userId,
71
+ ledgerUserId: serviceAccountCtx.userId,
72
+ accessToken: serviceAccountCtx.accessToken,
73
+ email: matchingKey.email || undefined,
74
+ };
75
+ req.authContext = context;
76
+ return next();
77
+ }
78
+ else {
79
+ logger.warn({ apiKey: apiKey.slice(0, 4) + '*****' }, 'Rejecting invalid API Key');
80
+ return res.status(401).json(jsonRpcResponse(reqId, {
81
+ error: {
82
+ code: rpcErrors.invalidRequest().code,
83
+ message: 'API Key is invalid',
84
+ },
85
+ }));
86
+ }
87
+ };
88
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=apiKeyAuth.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiKeyAuth.test.d.ts","sourceRoot":"","sources":["../../src/middleware/apiKeyAuth.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,126 @@
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 { apiKeyAuth } from './apiKeyAuth.js';
5
+ import { pino } from 'pino';
6
+ import { sink } from 'pino-test';
7
+ import crypto from 'crypto';
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
+ describe('apiKeyAuth', () => {
23
+ const getApiKey = vi.fn();
24
+ const logger = pino({ level: 'silent' }, sink());
25
+ const store = {
26
+ getApiKey,
27
+ getNetwork: vi.fn().mockResolvedValue({
28
+ id: 'canton:local-oauth',
29
+ identityProviderId: 'idp-mock-oauth',
30
+ serviceAccountAuth: {
31
+ method: 'client_credentials',
32
+ scope: 'daml_ledger_api',
33
+ audience: 'aud',
34
+ clientId: 'service_account',
35
+ clientSecret: 'service-account-secret',
36
+ },
37
+ }),
38
+ getIdp: vi.fn().mockResolvedValue({
39
+ id: 'idp-mock-oauth',
40
+ type: 'oauth',
41
+ issuer: 'http://127.0.0.1:8889',
42
+ configUrl: 'http://127.0.0.1:8889/.well-known/openid-configuration',
43
+ }),
44
+ setSession: vi.fn(),
45
+ authContext: undefined,
46
+ withAuthContext(context) {
47
+ this.authContext = context;
48
+ return this;
49
+ },
50
+ };
51
+ let next;
52
+ let status;
53
+ let json;
54
+ beforeEach(() => {
55
+ getApiKey.mockReset();
56
+ next = vi.fn();
57
+ status = vi.fn().mockReturnThis();
58
+ json = vi.fn();
59
+ });
60
+ function makeReq(partial) {
61
+ return {
62
+ baseUrl: '/api/v0/dapp',
63
+ path: '/',
64
+ headers: {},
65
+ query: {},
66
+ ...partial,
67
+ };
68
+ }
69
+ function makeRes() {
70
+ return { status, json };
71
+ }
72
+ it('skips processing if using JWT auth', async () => {
73
+ const req = makeReq({
74
+ headers: { authorization: 'Bearer abc' },
75
+ });
76
+ const res = makeRes();
77
+ const middleware = apiKeyAuth(store, '/api/v0/dapp', logger);
78
+ await middleware(req, res, next);
79
+ expect(getApiKey).not.toHaveBeenCalled();
80
+ expect(req.authContext).toBeUndefined();
81
+ expect(next).toHaveBeenCalledOnce();
82
+ });
83
+ it('sets authContext and calls next when verification succeeds', async () => {
84
+ const ctx = {
85
+ userId: 'alice',
86
+ ledgerUserId: 'service_account',
87
+ accessToken: 'abc',
88
+ isApiKey: true,
89
+ email: undefined,
90
+ };
91
+ getApiKey.mockResolvedValue({
92
+ id: 'key1',
93
+ digest: crypto.createHash('sha256').update('abc').digest('hex'),
94
+ userId: 'alice',
95
+ });
96
+ const req = makeReq({
97
+ headers: { authorization: 'ApiKey abc' },
98
+ });
99
+ const res = makeRes();
100
+ const middleware = apiKeyAuth(store, '/api/v0/dapp', logger);
101
+ await middleware(req, res, next);
102
+ expect(getApiKey).toHaveBeenCalled();
103
+ expect(req.authContext).toEqual(ctx);
104
+ expect(next).toHaveBeenCalledOnce();
105
+ expect(status).not.toHaveBeenCalled();
106
+ });
107
+ it('returns 401 JSON when no matching API key is found', async () => {
108
+ getApiKey.mockResolvedValue(null);
109
+ const req = makeReq({
110
+ headers: { authorization: 'ApiKey invalid' },
111
+ });
112
+ const res = makeRes();
113
+ const middleware = apiKeyAuth(store, '/api/v0/dapp', logger);
114
+ await middleware(req, res, next);
115
+ expect(next).not.toHaveBeenCalled();
116
+ expect(status).toHaveBeenCalledWith(401);
117
+ expect(json).toHaveBeenCalledWith({
118
+ error: {
119
+ code: -32600,
120
+ message: 'API Key is invalid',
121
+ },
122
+ id: null,
123
+ jsonrpc: '2.0',
124
+ });
125
+ });
126
+ });
@@ -1,9 +1,19 @@
1
1
  import { NextFunction, Request, Response } from 'express';
2
2
  import { Logger } from 'pino';
3
+ import { JsonRpcResponse } from '@canton-network/core-types';
3
4
  interface JsonRpcHttpOptions<T> {
4
5
  logger: Logger;
5
6
  controller: T;
6
7
  }
7
- export declare const jsonRpcHandler: <T extends Record<string, (...args: any[]) => any>>({ controller, logger: _logger, }: JsonRpcHttpOptions<T>) => (req: Request, res: Response, next: NextFunction) => void;
8
+ /**
9
+ * Handles JSON-RPC errors and maps them to HTTP responses.
10
+ * @param error The error that occurred.
11
+ * @param id The JSON-RPC request ID.
12
+ * @param logger The logger instance.
13
+ * @param method The name of the JSON-RPC method being called.
14
+ * @returns A tuple containing the HTTP status code and the JSON-RPC response.
15
+ */
16
+ export declare const handleRpcError: (error: unknown, id: string | number | null, method?: string) => [number, JsonRpcResponse];
17
+ export declare const jsonRpcHandler: <T extends Record<string, (...args: any[]) => any>>({ controller, logger: _logger, }: JsonRpcHttpOptions<T>) => (req: Request, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
8
18
  export {};
9
19
  //# sourceMappingURL=jsonRpcHandler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"jsonRpcHandler.d.ts","sourceRoot":"","sources":["../../src/middleware/jsonRpcHandler.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAc7B,UAAU,kBAAkB,CAAC,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,CAAC,CAAA;CAChB;AAiED,eAAO,MAAM,cAAc,GAEtB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,EAAE,kCAGjD,kBAAkB,CAAC,CAAC,CAAC,MAMZ,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,SA4E1D,CAAA"}
1
+ {"version":3,"file":"jsonRpcHandler.d.ts","sourceRoot":"","sources":["../../src/middleware/jsonRpcHandler.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAM7B,OAAO,EAGH,eAAe,EAClB,MAAM,4BAA4B,CAAA;AAInC,UAAU,kBAAkB,CAAC,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,CAAC,CAAA;CAChB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GACvB,OAAO,OAAO,EACd,IAAI,MAAM,GAAG,MAAM,GAAG,IAAI,EAC1B,SAAS,MAAM,KAChB,CAAC,MAAM,EAAE,eAAe,CA8C1B,CAAA;AAED,eAAO,MAAM,cAAc,GAEtB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,EAAE,kCAGjD,kBAAkB,CAAC,CAAC,CAAC,MAMZ,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,8CAgF1D,CAAA"}
@@ -12,7 +12,7 @@ import { isJsCantonError } from '@canton-network/core-ledger-client';
12
12
  * @param method The name of the JSON-RPC method being called.
13
13
  * @returns A tuple containing the HTTP status code and the JSON-RPC response.
14
14
  */
15
- const handleRpcError = (error, id, logger, method) => {
15
+ export const handleRpcError = (error, id, method) => {
16
16
  const genericMessage = method
17
17
  ? `Something went wrong while calling ${method}`
18
18
  : 'Something went wrong';
@@ -54,7 +54,6 @@ const handleRpcError = (error, id, logger, method) => {
54
54
  response.error.data = error;
55
55
  }
56
56
  const jsonResponse = jsonRpcResponse(id, response);
57
- logger.error(jsonResponse, 'RPC response');
58
57
  return [500, jsonResponse];
59
58
  };
60
59
  export const jsonRpcHandler =
@@ -63,7 +62,7 @@ export const jsonRpcHandler =
63
62
  const logger = _logger.child({ component: 'json-rpc-http' });
64
63
  return (req, res, next) => {
65
64
  if (req.method !== 'POST') {
66
- next();
65
+ return next();
67
66
  }
68
67
  const parsed = JsonRpcRequest.safeParse(req.body);
69
68
  if (!parsed.success) {
@@ -73,37 +72,37 @@ export const jsonRpcHandler =
73
72
  }, 'RPC request: Invalid request format');
74
73
  const [status, response] = handleRpcError(rpcErrors.invalidRequest({
75
74
  message: 'Invalid JSON-RPC request format',
76
- }), null, logger);
77
- res.status(status).json(response);
75
+ }), null);
76
+ return res.status(status).json(response);
78
77
  }
79
78
  else {
80
79
  const { method, params, id = null } = parsed.data;
81
- logger.debug({
80
+ logger.trace({
82
81
  request: {
83
- id: id,
84
- method: method,
85
- params: params,
82
+ id,
83
+ method,
84
+ params,
86
85
  authContext: req.authContext,
87
86
  },
88
- }, `RPC request: ${method}`);
87
+ }, `RPC request: Method called ${method}`);
89
88
  const methodFn = controller[method];
90
89
  if (!methodFn) {
91
90
  const [status, response] = handleRpcError(rpcErrors.methodNotFound({
92
91
  message: `Method ${method} not found`,
93
- }), null, logger, method);
94
- res.status(status).json(response);
92
+ }), null, method);
93
+ return res.status(status).json(response);
95
94
  }
96
95
  // TODO: validate params match the expected schema for the method
97
96
  methodFn(params)
98
97
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
98
  .then((result) => {
100
99
  const response = jsonRpcResponse(id, { result });
101
- logger.debug(response, 'RPC response');
100
+ logger.trace({ response }, 'RPC response: success with response');
102
101
  res.json(response);
103
102
  })
104
103
  .catch((error) => {
105
- const [status, response] = handleRpcError(error, id, logger, method);
106
- logger.error(response, 'RPC response');
104
+ const [status, response] = handleRpcError(error, id, method);
105
+ logger.error({ response }, 'RPC response: error with response');
107
106
  res.status(status).json(response);
108
107
  });
109
108
  }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=jsonRpcHandler.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonRpcHandler.test.d.ts","sourceRoot":"","sources":["../../src/middleware/jsonRpcHandler.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,254 @@
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 { rpcErrors, toHttpErrorCode } from '@canton-network/core-rpc-errors';
7
+ import { handleRpcError, jsonRpcHandler } from './jsonRpcHandler.js';
8
+ function errorPayload(body) {
9
+ if (!('error' in body)) {
10
+ throw new Error('expected JSON-RPC error response');
11
+ }
12
+ return body.error;
13
+ }
14
+ async function waitForRpcResponse(res) {
15
+ const json = res.json;
16
+ await vi.waitUntil(() => json.mock.calls.length > 0);
17
+ }
18
+ describe('jsonRpcHandler', () => {
19
+ const logger = pino({ level: 'silent' }, sink());
20
+ const resolve = vi.fn(async () => 'response');
21
+ const reject = vi.fn(async () => {
22
+ throw new Error('error');
23
+ });
24
+ const rpcError = vi.fn(async () => {
25
+ throw rpcErrors.invalidParams({ message: 'bad params' });
26
+ });
27
+ beforeEach(() => {
28
+ resolve.mockClear();
29
+ reject.mockClear();
30
+ rpcError.mockClear();
31
+ });
32
+ function makeHandler() {
33
+ return jsonRpcHandler({
34
+ controller: { resolve, reject, rpcError },
35
+ logger,
36
+ });
37
+ }
38
+ function makeRes() {
39
+ const res = {
40
+ status: vi.fn(),
41
+ json: vi.fn(),
42
+ };
43
+ // Express response methods are chainable, like `res.status(500).json(body)`
44
+ // Make the mocked status() return this fake res object so .json() can be called after it.
45
+ res.status.mockReturnValue(res);
46
+ return res;
47
+ }
48
+ it('delegates to next() if method is not POST', () => {
49
+ const handler = makeHandler();
50
+ const next = vi.fn();
51
+ const res = makeRes();
52
+ const req = {
53
+ method: 'GET',
54
+ body: {},
55
+ };
56
+ handler(req, res, next);
57
+ expect(next).toHaveBeenCalledOnce();
58
+ expect(res.status).not.toHaveBeenCalled();
59
+ });
60
+ it('responds with invalid request when body is not valid JSON-RPC 2.0', async () => {
61
+ const handler = makeHandler();
62
+ const next = vi.fn();
63
+ const res = makeRes();
64
+ const req = {
65
+ method: 'POST',
66
+ body: { jsonrpc: '1.0', method: 'resolve', id: 1 },
67
+ };
68
+ handler(req, res, next);
69
+ await waitForRpcResponse(res);
70
+ expect(next).not.toHaveBeenCalled();
71
+ expect(res.status).toHaveBeenCalled();
72
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
73
+ jsonrpc: '2.0',
74
+ id: null,
75
+ error: expect.objectContaining({
76
+ code: expect.any(Number),
77
+ }),
78
+ }));
79
+ });
80
+ it('returns method not found when controller has no such method', async () => {
81
+ const handler = makeHandler();
82
+ const next = vi.fn();
83
+ const res = makeRes();
84
+ const req = {
85
+ method: 'POST',
86
+ body: {
87
+ jsonrpc: '2.0',
88
+ id: 42,
89
+ method: 'missing',
90
+ params: [],
91
+ },
92
+ };
93
+ handler(req, res, next);
94
+ await waitForRpcResponse(res);
95
+ expect(res.status).toHaveBeenCalled();
96
+ const payload = res.json.mock.calls[0][0];
97
+ expect(payload.id).toBeNull();
98
+ expect(payload.error.message).toContain('missing');
99
+ });
100
+ it('returns JSON-RPC success when the method resolves', async () => {
101
+ const handler = makeHandler();
102
+ const next = vi.fn();
103
+ const res = makeRes();
104
+ const req = {
105
+ method: 'POST',
106
+ body: {
107
+ jsonrpc: '2.0',
108
+ id: 7,
109
+ method: 'resolve',
110
+ params: { x: 1 },
111
+ },
112
+ authContext: { userId: 'u', accessToken: 't' },
113
+ };
114
+ handler(req, res, next);
115
+ await waitForRpcResponse(res);
116
+ expect(resolve).toHaveBeenCalledWith({ x: 1 });
117
+ expect(res.json).toHaveBeenCalledWith({
118
+ jsonrpc: '2.0',
119
+ id: 7,
120
+ result: 'response',
121
+ });
122
+ });
123
+ it('maps thrown Error to JSON-RPC error with HTTP 500', async () => {
124
+ const handler = makeHandler();
125
+ const next = vi.fn();
126
+ const res = makeRes();
127
+ const req = {
128
+ method: 'POST',
129
+ body: {
130
+ jsonrpc: '2.0',
131
+ id: 'rid',
132
+ method: 'reject',
133
+ params: [],
134
+ },
135
+ };
136
+ handler(req, res, next);
137
+ await waitForRpcResponse(res);
138
+ expect(reject).toHaveBeenCalled();
139
+ expect(res.status).toHaveBeenCalledWith(500);
140
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
141
+ jsonrpc: '2.0',
142
+ id: 'rid',
143
+ error: expect.objectContaining({
144
+ message: 'error',
145
+ }),
146
+ }));
147
+ });
148
+ it('maps JsonRpcError to the corresponding HTTP status', async () => {
149
+ const handler = makeHandler();
150
+ const next = vi.fn();
151
+ const res = makeRes();
152
+ const req = {
153
+ method: 'POST',
154
+ body: {
155
+ jsonrpc: '2.0',
156
+ id: 0,
157
+ method: 'rpcError',
158
+ params: [],
159
+ },
160
+ };
161
+ handler(req, res, next);
162
+ await waitForRpcResponse(res);
163
+ expect(rpcError).toHaveBeenCalled();
164
+ expect(res.status).toHaveBeenCalledWith(toHttpErrorCode(rpcErrors.invalidParams().code));
165
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
166
+ jsonrpc: '2.0',
167
+ id: 0,
168
+ error: expect.objectContaining({
169
+ message: 'bad params',
170
+ }),
171
+ }));
172
+ });
173
+ });
174
+ describe('handleRpcError', () => {
175
+ const logger = pino({ level: 'silent' }, sink());
176
+ const errorLog = vi.spyOn(logger, 'error');
177
+ it('maps JsonRpcError to HTTP status from toHttpErrorCode and does not log RPC response as error', () => {
178
+ const err = rpcErrors.invalidParams({ message: 'bad' });
179
+ const [status, body] = handleRpcError(err, 99, 'whateverMethod');
180
+ expect(status).toBe(toHttpErrorCode(err.code));
181
+ expect(status).toBe(400);
182
+ expect(body).toEqual({
183
+ jsonrpc: '2.0',
184
+ id: 99,
185
+ error: err,
186
+ });
187
+ expect(errorLog).not.toHaveBeenCalled();
188
+ });
189
+ it('uses generic method-specific message for non-JsonRpcError then replaces with Error.message', () => {
190
+ const [status, body] = handleRpcError(new Error('some error'), 'id', 'submit');
191
+ expect(status).toBe(500);
192
+ expect(body).toEqual({
193
+ jsonrpc: '2.0',
194
+ id: 'id',
195
+ error: expect.objectContaining({
196
+ code: rpcErrors.internal().code,
197
+ message: 'some error',
198
+ data: expect.any(Error),
199
+ }),
200
+ });
201
+ });
202
+ it('uses generic message when method name is omitted', () => {
203
+ const [status, body] = handleRpcError(new Error('x'), null);
204
+ expect(status).toBe(500);
205
+ expect(errorPayload(body)).toMatchObject({
206
+ message: 'x',
207
+ });
208
+ });
209
+ it('maps string errors to the error message', () => {
210
+ const [status, body] = handleRpcError('plain', 0);
211
+ expect(status).toBe(500);
212
+ expect(errorPayload(body)).toMatchObject({
213
+ message: 'plain',
214
+ });
215
+ });
216
+ it('accepts a full ErrorResponse object when safeParse succeeds', () => {
217
+ const custom = {
218
+ error: {
219
+ code: -32000,
220
+ message: 'from client',
221
+ data: { hint: 1 },
222
+ },
223
+ };
224
+ const [status, body] = handleRpcError(custom, 3);
225
+ expect(status).toBe(500);
226
+ expect(body).toEqual({
227
+ jsonrpc: '2.0',
228
+ id: 3,
229
+ error: custom.error,
230
+ });
231
+ });
232
+ it('maps JsCantonError objects to internal code with cause as message', () => {
233
+ const ledgerErr = {
234
+ code: 'CODE',
235
+ cause: 'something went wrong',
236
+ };
237
+ const [status, body] = handleRpcError(ledgerErr, null);
238
+ expect(status).toBe(500);
239
+ expect(errorPayload(body)).toMatchObject({
240
+ code: rpcErrors.internal().code,
241
+ message: 'something went wrong',
242
+ data: ledgerErr,
243
+ });
244
+ });
245
+ it('falls back to generic internal error for unknown payloads', () => {
246
+ const [status, body] = handleRpcError({ foo: 'bar' }, 2, 'wrongMethod');
247
+ expect(status).toBe(500);
248
+ expect(errorPayload(body)).toMatchObject({
249
+ code: rpcErrors.internal().code,
250
+ message: 'Something went wrong while calling wrongMethod',
251
+ data: { foo: 'bar' },
252
+ });
253
+ });
254
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"jwtAuth.d.ts","sourceRoot":"","sources":["../../src/middleware/jwtAuth.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAE7B,wBAAgB,OAAO,CAAC,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,IAC9C,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBAkBhE"}
1
+ {"version":3,"file":"jwtAuth.d.ts","sourceRoot":"","sources":["../../src/middleware/jwtAuth.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAE7B,wBAAgB,OAAO,CAAC,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,IAC9C,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBAuBhE"}
@@ -2,6 +2,10 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  export function jwtAuth(authService, logger) {
4
4
  return async (req, res, next) => {
5
+ // If the request has been verified as an API key, skip JWT verification
6
+ if (req.authContext?.isApiKey) {
7
+ return next();
8
+ }
5
9
  // Support both Authorization header and token query parameter (for EventSource)
6
10
  const authHeader = req.headers.authorization ||
7
11
  (req.query.token ? `Bearer ${req.query.token}` : undefined);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=jwtAuth.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwtAuth.test.d.ts","sourceRoot":"","sources":["../../src/middleware/jwtAuth.test.ts"],"names":[],"mappings":""}