@fintekkers/ledger-models 0.4.12 → 0.4.14

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.
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Unit tests for Issue #114: API key support in EnvConfig.
3
+ *
4
+ * No network access: grpc-js credential factories are spied on and the
5
+ * metadata generator / interceptor are invoked directly.
6
+ */
7
+ export {};
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ /**
3
+ * Unit tests for Issue #114: API key support in EnvConfig.
4
+ *
5
+ * No network access: grpc-js credential factories are spied on and the
6
+ * metadata generator / interceptor are invoked directly.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || function (mod) {
25
+ if (mod && mod.__esModule) return mod;
26
+ var result = {};
27
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
28
+ __setModuleDefault(result, mod);
29
+ return result;
30
+ };
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ const grpc = __importStar(require("@grpc/grpc-js"));
33
+ const requestcontext_1 = __importStar(require("./requestcontext"));
34
+ const savedApiUrl = process.env.API_URL;
35
+ const savedApiKey = process.env.API_KEY;
36
+ function restoreEnv(name, value) {
37
+ if (value === undefined) {
38
+ delete process.env[name];
39
+ }
40
+ else {
41
+ process.env[name] = value;
42
+ }
43
+ }
44
+ beforeEach(() => {
45
+ restoreEnv('API_URL', undefined);
46
+ restoreEnv('API_KEY', undefined);
47
+ });
48
+ afterEach(() => {
49
+ restoreEnv('API_URL', savedApiUrl);
50
+ restoreEnv('API_KEY', savedApiKey);
51
+ jest.restoreAllMocks();
52
+ });
53
+ describe('EnvConfig.apiURL', () => {
54
+ test('uses API_URL as-is when it includes a port', () => {
55
+ process.env.API_URL = 'localhost:80';
56
+ expect(requestcontext_1.default.apiURL).toBe('localhost:80');
57
+ });
58
+ test('appends :8082 when API_URL has no port', () => {
59
+ process.env.API_URL = 'localhost';
60
+ expect(requestcontext_1.default.apiURL).toBe('localhost:8082');
61
+ });
62
+ test('defaults to api.fintekkers.org:8082 when API_URL is unset', () => {
63
+ expect(requestcontext_1.default.apiURL).toBe('api.fintekkers.org:8082');
64
+ });
65
+ });
66
+ describe('EnvConfig.apiKey', () => {
67
+ test('returns API_KEY when set', () => {
68
+ process.env.API_KEY = 'k';
69
+ expect(requestcontext_1.default.apiKey).toBe('k');
70
+ });
71
+ test('returns undefined when unset, without throwing', () => {
72
+ expect(() => requestcontext_1.default.apiKey).not.toThrow();
73
+ expect(requestcontext_1.default.apiKey).toBeUndefined();
74
+ });
75
+ });
76
+ describe('EnvConfig.apiCredentials', () => {
77
+ test.each(['localhost:80', '127.0.0.1:8085'])('uses insecure credentials for %s', (url) => {
78
+ process.env.API_URL = url;
79
+ const insecure = jest.spyOn(grpc.credentials, 'createInsecure');
80
+ const ssl = jest.spyOn(grpc.credentials, 'createSsl');
81
+ requestcontext_1.default.apiCredentials;
82
+ expect(insecure).toHaveBeenCalledTimes(1);
83
+ expect(ssl).not.toHaveBeenCalled();
84
+ });
85
+ test('uses SSL credentials when API_URL is unset', () => {
86
+ const insecure = jest.spyOn(grpc.credentials, 'createInsecure');
87
+ const ssl = jest.spyOn(grpc.credentials, 'createSsl');
88
+ requestcontext_1.default.apiCredentials;
89
+ expect(ssl).toHaveBeenCalledTimes(1);
90
+ expect(insecure).not.toHaveBeenCalled();
91
+ });
92
+ });
93
+ describe('EnvConfig.getAuthenticatedCredentials', () => {
94
+ test('metadata generator injects x-api-key', () => {
95
+ const generatorSpy = jest.spyOn(grpc.credentials, 'createFromMetadataGenerator');
96
+ const creds = requestcontext_1.default.getAuthenticatedCredentials('k');
97
+ expect(creds).toBeInstanceOf(grpc.ChannelCredentials);
98
+ expect(generatorSpy).toHaveBeenCalledTimes(1);
99
+ const generator = generatorSpy.mock.calls[0][0];
100
+ const callback = jest.fn();
101
+ generator({ service_url: 'https://api.fintekkers.org:8082' }, callback);
102
+ expect(callback).toHaveBeenCalledTimes(1);
103
+ const [err, metadata] = callback.mock.calls[0];
104
+ expect(err).toBeNull();
105
+ expect(metadata.get('x-api-key')).toEqual(['k']);
106
+ });
107
+ test.each(['localhost:80', '127.0.0.1:8085'])('throws InsecureChannelAuthError without leaking the key for %s', (url) => {
108
+ process.env.API_URL = url;
109
+ const secret = 'dummy-secret-123';
110
+ let caught;
111
+ try {
112
+ requestcontext_1.default.getAuthenticatedCredentials(secret);
113
+ }
114
+ catch (e) {
115
+ caught = e;
116
+ }
117
+ expect(caught).toBeInstanceOf(requestcontext_1.InsecureChannelAuthError);
118
+ const err = caught;
119
+ expect(err.message).not.toContain(secret);
120
+ expect(String(err)).not.toContain(secret);
121
+ });
122
+ test('never logs the key', () => {
123
+ const secret = 'dummy-secret-123';
124
+ const spies = [
125
+ jest.spyOn(console, 'log').mockImplementation(() => { }),
126
+ jest.spyOn(console, 'warn').mockImplementation(() => { }),
127
+ jest.spyOn(console, 'error').mockImplementation(() => { }),
128
+ ];
129
+ const generatorSpy = jest.spyOn(grpc.credentials, 'createFromMetadataGenerator');
130
+ requestcontext_1.default.getAuthenticatedCredentials(secret);
131
+ generatorSpy.mock.calls[0][0]({ service_url: '' }, () => { });
132
+ process.env.API_URL = 'localhost:80';
133
+ requestcontext_1.default.getAuthenticatedClientOptions(secret);
134
+ for (const spy of spies) {
135
+ for (const args of spy.mock.calls) {
136
+ expect(args.map(String).join(' ')).not.toContain(secret);
137
+ }
138
+ }
139
+ });
140
+ });
141
+ describe('EnvConfig.getAuthenticatedClientOptions', () => {
142
+ test.each(['localhost:80', '127.0.0.1:8085'])('uses insecure channel for %s', (url) => {
143
+ process.env.API_URL = url;
144
+ const insecure = jest.spyOn(grpc.credentials, 'createInsecure');
145
+ const ssl = jest.spyOn(grpc.credentials, 'createSsl');
146
+ const { interceptors } = requestcontext_1.default.getAuthenticatedClientOptions('k');
147
+ expect(insecure).toHaveBeenCalledTimes(1);
148
+ expect(ssl).not.toHaveBeenCalled();
149
+ expect(interceptors).toHaveLength(1);
150
+ });
151
+ test('uses SSL channel for api.fintekkers.org', () => {
152
+ process.env.API_URL = 'api.fintekkers.org';
153
+ const insecure = jest.spyOn(grpc.credentials, 'createInsecure');
154
+ const ssl = jest.spyOn(grpc.credentials, 'createSsl');
155
+ const { interceptors } = requestcontext_1.default.getAuthenticatedClientOptions('k');
156
+ expect(ssl).toHaveBeenCalledTimes(1);
157
+ expect(insecure).not.toHaveBeenCalled();
158
+ expect(interceptors).toHaveLength(0);
159
+ });
160
+ test('local interceptor injects x-api-key on start', () => {
161
+ process.env.API_URL = 'localhost:80';
162
+ const { interceptors } = requestcontext_1.default.getAuthenticatedClientOptions('k');
163
+ const nextStart = jest.fn();
164
+ const fakeNextCall = {
165
+ start: nextStart,
166
+ cancelWithStatus: jest.fn(),
167
+ getPeer: jest.fn(),
168
+ sendMessageWithContext: jest.fn(),
169
+ sendMessage: jest.fn(),
170
+ startRead: jest.fn(),
171
+ halfClose: jest.fn(),
172
+ setCredentials: jest.fn(),
173
+ };
174
+ const nextCall = jest.fn(() => fakeNextCall);
175
+ const call = interceptors[0]({}, nextCall);
176
+ call.start(new grpc.Metadata(), {});
177
+ expect(nextStart).toHaveBeenCalledTimes(1);
178
+ const sentMetadata = nextStart.mock.calls[0][0];
179
+ expect(sentMetadata.get('x-api-key')).toEqual(['k']);
180
+ });
181
+ });
182
+ //# sourceMappingURL=requestcontext.auth.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requestcontext.auth.test.js","sourceRoot":"","sources":["requestcontext.auth.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Unit tests for Issue #114: API key support in EnvConfig.
3
+ *
4
+ * No network access: grpc-js credential factories are spied on and the
5
+ * metadata generator / interceptor are invoked directly.
6
+ */
7
+
8
+ import * as grpc from '@grpc/grpc-js';
9
+ import EnvConfig, { InsecureChannelAuthError } from './requestcontext';
10
+
11
+ const savedApiUrl = process.env.API_URL;
12
+ const savedApiKey = process.env.API_KEY;
13
+
14
+ function restoreEnv(name: string, value: string | undefined) {
15
+ if (value === undefined) {
16
+ delete process.env[name];
17
+ } else {
18
+ process.env[name] = value;
19
+ }
20
+ }
21
+
22
+ beforeEach(() => {
23
+ restoreEnv('API_URL', undefined);
24
+ restoreEnv('API_KEY', undefined);
25
+ });
26
+
27
+ afterEach(() => {
28
+ restoreEnv('API_URL', savedApiUrl);
29
+ restoreEnv('API_KEY', savedApiKey);
30
+ jest.restoreAllMocks();
31
+ });
32
+
33
+ describe('EnvConfig.apiURL', () => {
34
+ test('uses API_URL as-is when it includes a port', () => {
35
+ process.env.API_URL = 'localhost:80';
36
+ expect(EnvConfig.apiURL).toBe('localhost:80');
37
+ });
38
+
39
+ test('appends :8082 when API_URL has no port', () => {
40
+ process.env.API_URL = 'localhost';
41
+ expect(EnvConfig.apiURL).toBe('localhost:8082');
42
+ });
43
+
44
+ test('defaults to api.fintekkers.org:8082 when API_URL is unset', () => {
45
+ expect(EnvConfig.apiURL).toBe('api.fintekkers.org:8082');
46
+ });
47
+ });
48
+
49
+ describe('EnvConfig.apiKey', () => {
50
+ test('returns API_KEY when set', () => {
51
+ process.env.API_KEY = 'k';
52
+ expect(EnvConfig.apiKey).toBe('k');
53
+ });
54
+
55
+ test('returns undefined when unset, without throwing', () => {
56
+ expect(() => EnvConfig.apiKey).not.toThrow();
57
+ expect(EnvConfig.apiKey).toBeUndefined();
58
+ });
59
+ });
60
+
61
+ describe('EnvConfig.apiCredentials', () => {
62
+ test.each(['localhost:80', '127.0.0.1:8085'])('uses insecure credentials for %s', (url) => {
63
+ process.env.API_URL = url;
64
+ const insecure = jest.spyOn(grpc.credentials, 'createInsecure');
65
+ const ssl = jest.spyOn(grpc.credentials, 'createSsl');
66
+
67
+ EnvConfig.apiCredentials;
68
+
69
+ expect(insecure).toHaveBeenCalledTimes(1);
70
+ expect(ssl).not.toHaveBeenCalled();
71
+ });
72
+
73
+ test('uses SSL credentials when API_URL is unset', () => {
74
+ const insecure = jest.spyOn(grpc.credentials, 'createInsecure');
75
+ const ssl = jest.spyOn(grpc.credentials, 'createSsl');
76
+
77
+ EnvConfig.apiCredentials;
78
+
79
+ expect(ssl).toHaveBeenCalledTimes(1);
80
+ expect(insecure).not.toHaveBeenCalled();
81
+ });
82
+ });
83
+
84
+ describe('EnvConfig.getAuthenticatedCredentials', () => {
85
+ test('metadata generator injects x-api-key', () => {
86
+ const generatorSpy = jest.spyOn(grpc.credentials, 'createFromMetadataGenerator');
87
+
88
+ const creds = EnvConfig.getAuthenticatedCredentials('k');
89
+
90
+ expect(creds).toBeInstanceOf(grpc.ChannelCredentials);
91
+ expect(generatorSpy).toHaveBeenCalledTimes(1);
92
+
93
+ const generator = generatorSpy.mock.calls[0][0];
94
+ const callback = jest.fn();
95
+ generator({ service_url: 'https://api.fintekkers.org:8082' }, callback);
96
+
97
+ expect(callback).toHaveBeenCalledTimes(1);
98
+ const [err, metadata] = callback.mock.calls[0];
99
+ expect(err).toBeNull();
100
+ expect((metadata as grpc.Metadata).get('x-api-key')).toEqual(['k']);
101
+ });
102
+
103
+ test.each(['localhost:80', '127.0.0.1:8085'])(
104
+ 'throws InsecureChannelAuthError without leaking the key for %s',
105
+ (url) => {
106
+ process.env.API_URL = url;
107
+ const secret = 'dummy-secret-123';
108
+
109
+ let caught: unknown;
110
+ try {
111
+ EnvConfig.getAuthenticatedCredentials(secret);
112
+ } catch (e) {
113
+ caught = e;
114
+ }
115
+
116
+ expect(caught).toBeInstanceOf(InsecureChannelAuthError);
117
+ const err = caught as Error;
118
+ expect(err.message).not.toContain(secret);
119
+ expect(String(err)).not.toContain(secret);
120
+ }
121
+ );
122
+
123
+ test('never logs the key', () => {
124
+ const secret = 'dummy-secret-123';
125
+ const spies = [
126
+ jest.spyOn(console, 'log').mockImplementation(() => {}),
127
+ jest.spyOn(console, 'warn').mockImplementation(() => {}),
128
+ jest.spyOn(console, 'error').mockImplementation(() => {}),
129
+ ];
130
+ const generatorSpy = jest.spyOn(grpc.credentials, 'createFromMetadataGenerator');
131
+
132
+ EnvConfig.getAuthenticatedCredentials(secret);
133
+ generatorSpy.mock.calls[0][0]({ service_url: '' }, () => {});
134
+ process.env.API_URL = 'localhost:80';
135
+ EnvConfig.getAuthenticatedClientOptions(secret);
136
+
137
+ for (const spy of spies) {
138
+ for (const args of spy.mock.calls) {
139
+ expect(args.map(String).join(' ')).not.toContain(secret);
140
+ }
141
+ }
142
+ });
143
+ });
144
+
145
+ describe('EnvConfig.getAuthenticatedClientOptions', () => {
146
+ test.each(['localhost:80', '127.0.0.1:8085'])('uses insecure channel for %s', (url) => {
147
+ process.env.API_URL = url;
148
+ const insecure = jest.spyOn(grpc.credentials, 'createInsecure');
149
+ const ssl = jest.spyOn(grpc.credentials, 'createSsl');
150
+
151
+ const { interceptors } = EnvConfig.getAuthenticatedClientOptions('k');
152
+
153
+ expect(insecure).toHaveBeenCalledTimes(1);
154
+ expect(ssl).not.toHaveBeenCalled();
155
+ expect(interceptors).toHaveLength(1);
156
+ });
157
+
158
+ test('uses SSL channel for api.fintekkers.org', () => {
159
+ process.env.API_URL = 'api.fintekkers.org';
160
+ const insecure = jest.spyOn(grpc.credentials, 'createInsecure');
161
+ const ssl = jest.spyOn(grpc.credentials, 'createSsl');
162
+
163
+ const { interceptors } = EnvConfig.getAuthenticatedClientOptions('k');
164
+
165
+ expect(ssl).toHaveBeenCalledTimes(1);
166
+ expect(insecure).not.toHaveBeenCalled();
167
+ expect(interceptors).toHaveLength(0);
168
+ });
169
+
170
+ test('local interceptor injects x-api-key on start', () => {
171
+ process.env.API_URL = 'localhost:80';
172
+ const { interceptors } = EnvConfig.getAuthenticatedClientOptions('k');
173
+
174
+ const nextStart = jest.fn();
175
+ const fakeNextCall = {
176
+ start: nextStart,
177
+ cancelWithStatus: jest.fn(),
178
+ getPeer: jest.fn(),
179
+ sendMessageWithContext: jest.fn(),
180
+ sendMessage: jest.fn(),
181
+ startRead: jest.fn(),
182
+ halfClose: jest.fn(),
183
+ setCredentials: jest.fn(),
184
+ } as unknown as grpc.InterceptingCall;
185
+ const nextCall = jest.fn(() => fakeNextCall);
186
+
187
+ const call = interceptors[0]({} as grpc.InterceptorOptions, nextCall);
188
+ call.start(new grpc.Metadata(), {});
189
+
190
+ expect(nextStart).toHaveBeenCalledTimes(1);
191
+ const sentMetadata = nextStart.mock.calls[0][0] as grpc.Metadata;
192
+ expect(sentMetadata.get('x-api-key')).toEqual(['k']);
193
+ });
194
+ });
@@ -9,19 +9,36 @@ declare class EnvConfig {
9
9
  * otherwise :8082 is appended for backward compatibility.
10
10
  */
11
11
  static get apiURL(): string;
12
+ private static get isLocalURL();
12
13
  static get apiCredentials(): grpc.ChannelCredentials;
14
+ private static addApiKey;
13
15
  /**
14
- * Returns channel credentials that inject `x-api-key: <apiKey>` into every
15
- * call's metadata. Uses insecure transport for localhost, SSL otherwise.
16
+ * Returns SSL channel credentials combined with per-call credentials that
17
+ * inject `x-api-key: <apiKey>` into every call's metadata.
18
+ *
19
+ * Remote (SSL) hosts only: grpc-js cannot compose call credentials with
20
+ * insecure channel credentials. For localhost / 127.0.0.1 use
21
+ * {@link EnvConfig.getAuthenticatedClientOptions}, which sends the key via
22
+ * an interceptor instead.
23
+ *
24
+ * @throws {InsecureChannelAuthError} when apiURL is localhost / 127.0.0.1.
16
25
  */
26
+ static getAuthenticatedCredentials(apiKey: string): grpc.ChannelCredentials;
17
27
  /**
18
28
  * Returns credentials and interceptors for authenticated calls.
19
29
  * For local (insecure) channels, injects the API key via an interceptor.
20
- * For remote (SSL) channels, uses combined channel + call credentials.
30
+ * For remote (SSL) channels, uses {@link EnvConfig.getAuthenticatedCredentials}.
21
31
  */
22
32
  static getAuthenticatedClientOptions(apiKey: string): {
23
33
  credentials: grpc.ChannelCredentials;
24
34
  interceptors: grpc.Interceptor[];
25
35
  };
26
36
  }
37
+ /**
38
+ * Thrown by EnvConfig.getAuthenticatedCredentials when API_URL points at an
39
+ * insecure (local) channel. The message never includes the API key.
40
+ */
41
+ export declare class InsecureChannelAuthError extends Error {
42
+ constructor();
43
+ }
27
44
  export default EnvConfig;
@@ -23,6 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.InsecureChannelAuthError = void 0;
26
27
  const dotenv = __importStar(require("dotenv"));
27
28
  dotenv.config();
28
29
  const grpc = __importStar(require("@grpc/grpc-js"));
@@ -50,46 +51,76 @@ class EnvConfig {
50
51
  const base = EnvConfig.getEnvVar('API_URL', 'api.fintekkers.org');
51
52
  return /:\d+$/.test(base) ? base : base + ':8082';
52
53
  }
54
+ static get isLocalURL() {
55
+ return /^localhost|^127\.0\.0\.1/.test(this.apiURL);
56
+ }
53
57
  static get apiCredentials() {
54
- if (/^localhost|^127\.0\.0\.1/.test(this.apiURL)) {
58
+ if (this.isLocalURL) {
55
59
  return grpc.credentials.createInsecure();
56
60
  }
57
61
  else {
58
62
  return grpc.credentials.createSsl();
59
63
  }
60
64
  }
65
+ static addApiKey(metadata, apiKey) {
66
+ metadata.add('x-api-key', apiKey);
67
+ }
61
68
  /**
62
- * Returns channel credentials that inject `x-api-key: <apiKey>` into every
63
- * call's metadata. Uses insecure transport for localhost, SSL otherwise.
69
+ * Returns SSL channel credentials combined with per-call credentials that
70
+ * inject `x-api-key: <apiKey>` into every call's metadata.
71
+ *
72
+ * Remote (SSL) hosts only: grpc-js cannot compose call credentials with
73
+ * insecure channel credentials. For localhost / 127.0.0.1 use
74
+ * {@link EnvConfig.getAuthenticatedClientOptions}, which sends the key via
75
+ * an interceptor instead.
76
+ *
77
+ * @throws {InsecureChannelAuthError} when apiURL is localhost / 127.0.0.1.
64
78
  */
79
+ static getAuthenticatedCredentials(apiKey) {
80
+ if (this.isLocalURL) {
81
+ throw new InsecureChannelAuthError();
82
+ }
83
+ const callCreds = grpc.credentials.createFromMetadataGenerator((_params, callback) => {
84
+ const metadata = new grpc.Metadata();
85
+ EnvConfig.addApiKey(metadata, apiKey);
86
+ callback(null, metadata);
87
+ });
88
+ return grpc.credentials.combineChannelCredentials(grpc.credentials.createSsl(), callCreds);
89
+ }
65
90
  /**
66
91
  * Returns credentials and interceptors for authenticated calls.
67
92
  * For local (insecure) channels, injects the API key via an interceptor.
68
- * For remote (SSL) channels, uses combined channel + call credentials.
93
+ * For remote (SSL) channels, uses {@link EnvConfig.getAuthenticatedCredentials}.
69
94
  */
70
95
  static getAuthenticatedClientOptions(apiKey) {
71
- const isLocal = /^localhost|^127\.0\.0\.1/.test(this.apiURL);
72
- if (isLocal) {
96
+ if (this.isLocalURL) {
73
97
  const interceptor = (_options, nextCall) => {
74
98
  return new grpc.InterceptingCall(nextCall(_options), {
75
99
  start(metadata, listener, next) {
76
- metadata.add('x-api-key', apiKey);
100
+ EnvConfig.addApiKey(metadata, apiKey);
77
101
  next(metadata, listener);
78
102
  }
79
103
  });
80
104
  };
81
105
  return { credentials: grpc.credentials.createInsecure(), interceptors: [interceptor] };
82
106
  }
83
- const callCreds = grpc.credentials.createFromMetadataGenerator((_params, callback) => {
84
- const metadata = new grpc.Metadata();
85
- metadata.add('x-api-key', apiKey);
86
- callback(null, metadata);
87
- });
88
107
  return {
89
- credentials: grpc.credentials.combineChannelCredentials(grpc.credentials.createSsl(), callCreds),
108
+ credentials: EnvConfig.getAuthenticatedCredentials(apiKey),
90
109
  interceptors: []
91
110
  };
92
111
  }
93
112
  }
113
+ /**
114
+ * Thrown by EnvConfig.getAuthenticatedCredentials when API_URL points at an
115
+ * insecure (local) channel. The message never includes the API key.
116
+ */
117
+ class InsecureChannelAuthError extends Error {
118
+ constructor() {
119
+ super('API key auth over an insecure channel requires getAuthenticatedClientOptions()');
120
+ this.name = 'InsecureChannelAuthError';
121
+ Object.setPrototypeOf(this, InsecureChannelAuthError.prototype);
122
+ }
123
+ }
124
+ exports.InsecureChannelAuthError = InsecureChannelAuthError;
94
125
  exports.default = EnvConfig;
95
126
  //# sourceMappingURL=requestcontext.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"requestcontext.js","sourceRoot":"","sources":["requestcontext.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAAA,+CAAiC;AACjC,MAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,oDAAsC;AAEtC,MAAM,SAAS;IACL,MAAM,CAAC,SAAS,CAAC,GAAW,EAAE,YAAqB;QACzD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,KAAK,KAAK,SAAS,EAAE;YACvB,IAAI,YAAY,KAAK,SAAS,EAAE;gBAC9B,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,cAAc,CAAC,CAAC;aAC5D;YACD,OAAO,YAAY,CAAC;SACrB;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,KAAK,MAAM;QACf,OAAO,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAChC,CAAC;IAED;;;;;OAKG;IACH,MAAM,KAAK,MAAM;QACf,MAAM,IAAI,GAAG,SAAS,CAAC,SAAS,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAC;QAClE,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,OAAO,CAAC;IACpD,CAAC;IAED,MAAM,KAAK,cAAc;QACvB,IAAI,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;YAChD,OAAO,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,CAAC;SAC1C;aACI;YACH,OAAO,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,CAAC;SACrC;IACH,CAAC;IAED;;;OAGG;IACH;;;;OAIG;IACH,MAAM,CAAC,6BAA6B,CAAC,MAAc;QAIjD,MAAM,OAAO,GAAG,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC7D,IAAI,OAAO,EAAE;YACX,MAAM,WAAW,GAAqB,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE;gBAC3D,OAAO,IAAI,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;oBACnD,KAAK,CAAC,QAAuB,EAAE,QAAuB,EAAE,IAAc;wBACpE,QAAQ,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;wBAClC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;oBAC3B,CAAC;iBACF,CAAC,CAAC;YACL,CAAC,CAAC;YACF,OAAO,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,EAAE,YAAY,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC;SACxF;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,2BAA2B,CAC5D,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE;YACpB,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACrC,QAAQ,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;YAClC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC3B,CAAC,CACF,CAAC;QACF,OAAO;YACL,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,yBAAyB,CACrD,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,EAC5B,SAAS,CACV;YACD,YAAY,EAAE,EAAE;SACjB,CAAC;IACJ,CAAC;CACF;AAED,kBAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"requestcontext.js","sourceRoot":"","sources":["requestcontext.ts"],"names":[],"mappings":"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"}
@@ -30,8 +30,12 @@ class EnvConfig {
30
30
  return /:\d+$/.test(base) ? base : base + ':8082';
31
31
  }
32
32
 
33
+ private static get isLocalURL(): boolean {
34
+ return /^localhost|^127\.0\.0\.1/.test(this.apiURL);
35
+ }
36
+
33
37
  static get apiCredentials(): grpc.ChannelCredentials {
34
- if (/^localhost|^127\.0\.0\.1/.test(this.apiURL)) {
38
+ if (this.isLocalURL) {
35
39
  return grpc.credentials.createInsecure();
36
40
  }
37
41
  else {
@@ -39,46 +43,75 @@ class EnvConfig {
39
43
  }
40
44
  }
41
45
 
46
+ private static addApiKey(metadata: grpc.Metadata, apiKey: string): void {
47
+ metadata.add('x-api-key', apiKey);
48
+ }
49
+
42
50
  /**
43
- * Returns channel credentials that inject `x-api-key: <apiKey>` into every
44
- * call's metadata. Uses insecure transport for localhost, SSL otherwise.
51
+ * Returns SSL channel credentials combined with per-call credentials that
52
+ * inject `x-api-key: <apiKey>` into every call's metadata.
53
+ *
54
+ * Remote (SSL) hosts only: grpc-js cannot compose call credentials with
55
+ * insecure channel credentials. For localhost / 127.0.0.1 use
56
+ * {@link EnvConfig.getAuthenticatedClientOptions}, which sends the key via
57
+ * an interceptor instead.
58
+ *
59
+ * @throws {InsecureChannelAuthError} when apiURL is localhost / 127.0.0.1.
45
60
  */
61
+ static getAuthenticatedCredentials(apiKey: string): grpc.ChannelCredentials {
62
+ if (this.isLocalURL) {
63
+ throw new InsecureChannelAuthError();
64
+ }
65
+ const callCreds = grpc.credentials.createFromMetadataGenerator(
66
+ (_params, callback) => {
67
+ const metadata = new grpc.Metadata();
68
+ EnvConfig.addApiKey(metadata, apiKey);
69
+ callback(null, metadata);
70
+ }
71
+ );
72
+ return grpc.credentials.combineChannelCredentials(
73
+ grpc.credentials.createSsl(),
74
+ callCreds
75
+ );
76
+ }
77
+
46
78
  /**
47
79
  * Returns credentials and interceptors for authenticated calls.
48
80
  * For local (insecure) channels, injects the API key via an interceptor.
49
- * For remote (SSL) channels, uses combined channel + call credentials.
81
+ * For remote (SSL) channels, uses {@link EnvConfig.getAuthenticatedCredentials}.
50
82
  */
51
83
  static getAuthenticatedClientOptions(apiKey: string): {
52
84
  credentials: grpc.ChannelCredentials;
53
85
  interceptors: grpc.Interceptor[];
54
86
  } {
55
- const isLocal = /^localhost|^127\.0\.0\.1/.test(this.apiURL);
56
- if (isLocal) {
87
+ if (this.isLocalURL) {
57
88
  const interceptor: grpc.Interceptor = (_options, nextCall) => {
58
89
  return new grpc.InterceptingCall(nextCall(_options), {
59
90
  start(metadata: grpc.Metadata, listener: grpc.Listener, next: Function) {
60
- metadata.add('x-api-key', apiKey);
91
+ EnvConfig.addApiKey(metadata, apiKey);
61
92
  next(metadata, listener);
62
93
  }
63
94
  });
64
95
  };
65
96
  return { credentials: grpc.credentials.createInsecure(), interceptors: [interceptor] };
66
97
  }
67
- const callCreds = grpc.credentials.createFromMetadataGenerator(
68
- (_params, callback) => {
69
- const metadata = new grpc.Metadata();
70
- metadata.add('x-api-key', apiKey);
71
- callback(null, metadata);
72
- }
73
- );
74
98
  return {
75
- credentials: grpc.credentials.combineChannelCredentials(
76
- grpc.credentials.createSsl(),
77
- callCreds
78
- ),
99
+ credentials: EnvConfig.getAuthenticatedCredentials(apiKey),
79
100
  interceptors: []
80
101
  };
81
102
  }
82
103
  }
83
104
 
105
+ /**
106
+ * Thrown by EnvConfig.getAuthenticatedCredentials when API_URL points at an
107
+ * insecure (local) channel. The message never includes the API key.
108
+ */
109
+ export class InsecureChannelAuthError extends Error {
110
+ constructor() {
111
+ super('API key auth over an insecure channel requires getAuthenticatedClientOptions()');
112
+ this.name = 'InsecureChannelAuthError';
113
+ Object.setPrototypeOf(this, InsecureChannelAuthError.prototype);
114
+ }
115
+ }
116
+
84
117
  export default EnvConfig;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Unit tests for Issue #114: each wrapper service hands the generated gRPC
3
+ * client the authenticated credentials when given an apiKey, and
4
+ * EnvConfig.apiCredentials otherwise.
5
+ *
6
+ * The generated *_grpc_pb clients are mocked, so no channel is opened.
7
+ */
8
+ export {};
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ /**
3
+ * Unit tests for Issue #114: each wrapper service hands the generated gRPC
4
+ * client the authenticated credentials when given an apiKey, and
5
+ * EnvConfig.apiCredentials otherwise.
6
+ *
7
+ * The generated *_grpc_pb clients are mocked, so no channel is opened.
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || function (mod) {
26
+ if (mod && mod.__esModule) return mod;
27
+ var result = {};
28
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
29
+ __setModuleDefault(result, mod);
30
+ return result;
31
+ };
32
+ var __importDefault = (this && this.__importDefault) || function (mod) {
33
+ return (mod && mod.__esModule) ? mod : { "default": mod };
34
+ };
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ jest.mock('../../fintekkers/services/portfolio-service/portfolio_service_grpc_pb', () => ({
37
+ PortfolioClient: jest.fn(),
38
+ }));
39
+ jest.mock('../../fintekkers/services/position-service/position_service_grpc_pb', () => ({
40
+ PositionClient: jest.fn(),
41
+ }));
42
+ jest.mock('../../fintekkers/services/price-service/price_service_grpc_pb', () => ({
43
+ PriceClient: jest.fn(),
44
+ }));
45
+ jest.mock('../../fintekkers/services/security-service/security_service_grpc_pb', () => ({
46
+ SecurityClient: jest.fn(),
47
+ }));
48
+ jest.mock('../../fintekkers/services/transaction-service/transaction_service_grpc_pb', () => ({
49
+ TransactionClient: jest.fn(),
50
+ }));
51
+ const grpc = __importStar(require("@grpc/grpc-js"));
52
+ const requestcontext_1 = __importDefault(require("../models/utils/requestcontext"));
53
+ const portfolio_service_grpc_pb_1 = require("../../fintekkers/services/portfolio-service/portfolio_service_grpc_pb");
54
+ const position_service_grpc_pb_1 = require("../../fintekkers/services/position-service/position_service_grpc_pb");
55
+ const price_service_grpc_pb_1 = require("../../fintekkers/services/price-service/price_service_grpc_pb");
56
+ const security_service_grpc_pb_1 = require("../../fintekkers/services/security-service/security_service_grpc_pb");
57
+ const transaction_service_grpc_pb_1 = require("../../fintekkers/services/transaction-service/transaction_service_grpc_pb");
58
+ const PortfolioService_1 = require("./portfolio-service/PortfolioService");
59
+ const PositionService_1 = require("./position-service/PositionService");
60
+ const PriceService_1 = require("./price-service/PriceService");
61
+ const SecurityService_1 = require("./security-service/SecurityService");
62
+ const TransactionService_1 = require("./transaction-service/TransactionService");
63
+ const cases = [
64
+ ['PortfolioService', PortfolioService_1.PortfolioService, portfolio_service_grpc_pb_1.PortfolioClient],
65
+ ['PositionService', PositionService_1.PositionService, position_service_grpc_pb_1.PositionClient],
66
+ ['PriceService', PriceService_1.PriceService, price_service_grpc_pb_1.PriceClient],
67
+ ['SecurityService', SecurityService_1.SecurityService, security_service_grpc_pb_1.SecurityClient],
68
+ ['TransactionService', TransactionService_1.TransactionService, transaction_service_grpc_pb_1.TransactionClient],
69
+ ];
70
+ beforeEach(() => {
71
+ jest.clearAllMocks();
72
+ });
73
+ afterEach(() => {
74
+ jest.restoreAllMocks();
75
+ });
76
+ describe.each(cases)('%s', (_name, Service, Client) => {
77
+ const ClientMock = Client;
78
+ test('with apiKey, client gets the authenticated credentials', () => {
79
+ const sentinel = {
80
+ credentials: grpc.credentials.createInsecure(),
81
+ interceptors: [],
82
+ };
83
+ const authSpy = jest
84
+ .spyOn(requestcontext_1.default, 'getAuthenticatedClientOptions')
85
+ .mockReturnValue(sentinel);
86
+ new Service('k');
87
+ expect(authSpy).toHaveBeenCalledWith('k');
88
+ expect(ClientMock).toHaveBeenCalledTimes(1);
89
+ expect(ClientMock).toHaveBeenCalledWith(requestcontext_1.default.apiURL, sentinel.credentials, { interceptors: sentinel.interceptors });
90
+ });
91
+ test('zero-arg constructor constructs with EnvConfig.apiCredentials', () => {
92
+ const plain = grpc.credentials.createInsecure();
93
+ jest.spyOn(requestcontext_1.default, 'apiCredentials', 'get').mockReturnValue(plain);
94
+ const authSpy = jest.spyOn(requestcontext_1.default, 'getAuthenticatedClientOptions');
95
+ expect(new Service()).toBeDefined();
96
+ expect(authSpy).not.toHaveBeenCalled();
97
+ expect(ClientMock).toHaveBeenCalledTimes(1);
98
+ expect(ClientMock).toHaveBeenCalledWith(requestcontext_1.default.apiURL, plain);
99
+ });
100
+ });
101
+ //# sourceMappingURL=apikey.credentials.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apikey.credentials.test.js","sourceRoot":"","sources":["apikey.credentials.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Unit tests for Issue #114: each wrapper service hands the generated gRPC
3
+ * client the authenticated credentials when given an apiKey, and
4
+ * EnvConfig.apiCredentials otherwise.
5
+ *
6
+ * The generated *_grpc_pb clients are mocked, so no channel is opened.
7
+ */
8
+
9
+ jest.mock('../../fintekkers/services/portfolio-service/portfolio_service_grpc_pb', () => ({
10
+ PortfolioClient: jest.fn(),
11
+ }));
12
+ jest.mock('../../fintekkers/services/position-service/position_service_grpc_pb', () => ({
13
+ PositionClient: jest.fn(),
14
+ }));
15
+ jest.mock('../../fintekkers/services/price-service/price_service_grpc_pb', () => ({
16
+ PriceClient: jest.fn(),
17
+ }));
18
+ jest.mock('../../fintekkers/services/security-service/security_service_grpc_pb', () => ({
19
+ SecurityClient: jest.fn(),
20
+ }));
21
+ jest.mock('../../fintekkers/services/transaction-service/transaction_service_grpc_pb', () => ({
22
+ TransactionClient: jest.fn(),
23
+ }));
24
+
25
+ import * as grpc from '@grpc/grpc-js';
26
+ import EnvConfig from '../models/utils/requestcontext';
27
+ import { PortfolioClient } from '../../fintekkers/services/portfolio-service/portfolio_service_grpc_pb';
28
+ import { PositionClient } from '../../fintekkers/services/position-service/position_service_grpc_pb';
29
+ import { PriceClient } from '../../fintekkers/services/price-service/price_service_grpc_pb';
30
+ import { SecurityClient } from '../../fintekkers/services/security-service/security_service_grpc_pb';
31
+ import { TransactionClient } from '../../fintekkers/services/transaction-service/transaction_service_grpc_pb';
32
+ import { PortfolioService } from './portfolio-service/PortfolioService';
33
+ import { PositionService } from './position-service/PositionService';
34
+ import { PriceService } from './price-service/PriceService';
35
+ import { SecurityService } from './security-service/SecurityService';
36
+ import { TransactionService } from './transaction-service/TransactionService';
37
+
38
+ const cases: Array<[string, new (apiKey?: string) => unknown, unknown]> = [
39
+ ['PortfolioService', PortfolioService, PortfolioClient],
40
+ ['PositionService', PositionService, PositionClient],
41
+ ['PriceService', PriceService, PriceClient],
42
+ ['SecurityService', SecurityService, SecurityClient],
43
+ ['TransactionService', TransactionService, TransactionClient],
44
+ ];
45
+
46
+ beforeEach(() => {
47
+ jest.clearAllMocks();
48
+ });
49
+
50
+ afterEach(() => {
51
+ jest.restoreAllMocks();
52
+ });
53
+
54
+ describe.each(cases)('%s', (_name, Service, Client) => {
55
+ const ClientMock = Client as jest.Mock;
56
+
57
+ test('with apiKey, client gets the authenticated credentials', () => {
58
+ const sentinel = {
59
+ credentials: grpc.credentials.createInsecure(),
60
+ interceptors: [] as grpc.Interceptor[],
61
+ };
62
+ const authSpy = jest
63
+ .spyOn(EnvConfig, 'getAuthenticatedClientOptions')
64
+ .mockReturnValue(sentinel);
65
+
66
+ new Service('k');
67
+
68
+ expect(authSpy).toHaveBeenCalledWith('k');
69
+ expect(ClientMock).toHaveBeenCalledTimes(1);
70
+ expect(ClientMock).toHaveBeenCalledWith(
71
+ EnvConfig.apiURL,
72
+ sentinel.credentials,
73
+ { interceptors: sentinel.interceptors }
74
+ );
75
+ });
76
+
77
+ test('zero-arg constructor constructs with EnvConfig.apiCredentials', () => {
78
+ const plain = grpc.credentials.createInsecure();
79
+ jest.spyOn(EnvConfig, 'apiCredentials', 'get').mockReturnValue(plain);
80
+ const authSpy = jest.spyOn(EnvConfig, 'getAuthenticatedClientOptions');
81
+
82
+ expect(new Service()).toBeDefined();
83
+
84
+ expect(authSpy).not.toHaveBeenCalled();
85
+ expect(ClientMock).toHaveBeenCalledTimes(1);
86
+ expect(ClientMock).toHaveBeenCalledWith(EnvConfig.apiURL, plain);
87
+ });
88
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fintekkers/ledger-models",
3
3
  "todo": "Replace the version with build script version number",
4
- "version": "0.4.12",
4
+ "version": "0.4.14",
5
5
  "description": "ledger model protos ",
6
6
  "authors": [
7
7
  "David Doherty",