@fintekkers/ledger-models 0.4.13 → 0.4.15
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.
- package/node/wrappers/models/transaction/transaction.d.ts +10 -0
- package/node/wrappers/models/transaction/transaction.js +27 -1
- package/node/wrappers/models/transaction/transaction.js.map +1 -1
- package/node/wrappers/models/transaction/transaction.ts +25 -1
- package/node/wrappers/models/transaction/transaction_price_defaults.test.d.ts +1 -0
- package/node/wrappers/models/transaction/transaction_price_defaults.test.js +131 -0
- package/node/wrappers/models/transaction/transaction_price_defaults.test.js.map +1 -0
- package/node/wrappers/models/transaction/transaction_price_defaults.test.ts +150 -0
- package/node/wrappers/models/utils/requestcontext.auth.test.d.ts +7 -0
- package/node/wrappers/models/utils/requestcontext.auth.test.js +182 -0
- package/node/wrappers/models/utils/requestcontext.auth.test.js.map +1 -0
- package/node/wrappers/models/utils/requestcontext.auth.test.ts +194 -0
- package/node/wrappers/models/utils/requestcontext.d.ts +20 -3
- package/node/wrappers/models/utils/requestcontext.js +44 -13
- package/node/wrappers/models/utils/requestcontext.js.map +1 -1
- package/node/wrappers/models/utils/requestcontext.ts +51 -18
- package/node/wrappers/services/apikey.credentials.test.d.ts +8 -0
- package/node/wrappers/services/apikey.credentials.test.js +101 -0
- package/node/wrappers/services/apikey.credentials.test.js.map +1 -0
- package/node/wrappers/services/apikey.credentials.test.ts +88 -0
- package/package.json +1 -1
|
@@ -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
|
|
15
|
-
*
|
|
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
|
|
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 (
|
|
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
|
|
63
|
-
*
|
|
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
|
|
93
|
+
* For remote (SSL) channels, uses {@link EnvConfig.getAuthenticatedCredentials}.
|
|
69
94
|
*/
|
|
70
95
|
static getAuthenticatedClientOptions(apiKey) {
|
|
71
|
-
|
|
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
|
-
|
|
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:
|
|
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":"
|
|
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 (
|
|
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
|
|
44
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 {};
|