@aloma.io/integration-sdk 3.0.0-4

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 (56) hide show
  1. package/build/builder/index.d.mts +11 -0
  2. package/build/builder/index.mjs +51 -0
  3. package/build/builder/runtime-context.d.mts +7 -0
  4. package/build/builder/runtime-context.mjs +42 -0
  5. package/build/builder/transform/index.d.mts +5 -0
  6. package/build/builder/transform/index.mjs +72 -0
  7. package/build/controller/index.d.mts +19 -0
  8. package/build/controller/index.mjs +44 -0
  9. package/build/index.d.mts +2 -0
  10. package/build/index.mjs +2 -0
  11. package/build/internal/dispatcher/index.cjs +151 -0
  12. package/build/internal/dispatcher/index.d.cts +32 -0
  13. package/build/internal/index.cjs +461 -0
  14. package/build/internal/index.d.cts +14 -0
  15. package/build/internal/util/jwe/cli.cjs +13 -0
  16. package/build/internal/util/jwe/cli.d.cts +1 -0
  17. package/build/internal/util/jwe/index.cjs +57 -0
  18. package/build/internal/util/jwe/index.d.cts +32 -0
  19. package/build/internal/websocket/config.cjs +79 -0
  20. package/build/internal/websocket/config.d.cts +41 -0
  21. package/build/internal/websocket/connection/constants.cjs +21 -0
  22. package/build/internal/websocket/connection/constants.d.cts +2 -0
  23. package/build/internal/websocket/connection/index.cjs +53 -0
  24. package/build/internal/websocket/connection/index.d.cts +11 -0
  25. package/build/internal/websocket/connection/registration.cjs +31 -0
  26. package/build/internal/websocket/connection/registration.d.cts +5 -0
  27. package/build/internal/websocket/index.cjs +41 -0
  28. package/build/internal/websocket/index.d.cts +16 -0
  29. package/build/internal/websocket/transport/durable.cjs +61 -0
  30. package/build/internal/websocket/transport/durable.d.cts +20 -0
  31. package/build/internal/websocket/transport/index.cjs +148 -0
  32. package/build/internal/websocket/transport/index.d.cts +37 -0
  33. package/build/internal/websocket/transport/packet.cjs +44 -0
  34. package/build/internal/websocket/transport/packet.d.cts +16 -0
  35. package/build/internal/websocket/transport/processor.cjs +58 -0
  36. package/build/internal/websocket/transport/processor.d.cts +11 -0
  37. package/package.json +44 -0
  38. package/src/builder/index.mts +70 -0
  39. package/src/builder/runtime-context.mts +45 -0
  40. package/src/builder/transform/index.mts +89 -0
  41. package/src/controller/index.mts +58 -0
  42. package/src/index.mts +2 -0
  43. package/src/internal/dispatcher/index.cjs +189 -0
  44. package/src/internal/index.cjs +547 -0
  45. package/src/internal/util/jwe/cli.cjs +14 -0
  46. package/src/internal/util/jwe/index.cjs +69 -0
  47. package/src/internal/websocket/config.cjs +103 -0
  48. package/src/internal/websocket/connection/constants.cjs +25 -0
  49. package/src/internal/websocket/connection/index.cjs +70 -0
  50. package/src/internal/websocket/connection/registration.cjs +40 -0
  51. package/src/internal/websocket/index.cjs +46 -0
  52. package/src/internal/websocket/transport/durable.cjs +71 -0
  53. package/src/internal/websocket/transport/index.cjs +183 -0
  54. package/src/internal/websocket/transport/packet.cjs +54 -0
  55. package/src/internal/websocket/transport/processor.cjs +66 -0
  56. package/tsconfig.json +27 -0
@@ -0,0 +1,547 @@
1
+ // @ts-nocheck
2
+ require('dotenv').config();
3
+ const fs = require('fs');
4
+ const {Config} = require('./websocket/config.cjs');
5
+ const {Connection} = require('./websocket/connection/index.cjs');
6
+ const {Transport} = require('./websocket/transport/index.cjs');
7
+ const {Dispatcher} = require('./dispatcher/index.cjs');
8
+ const {WebsocketConnector} = require('./websocket/index.cjs');
9
+ const JWE = require('./util/jwe/index.cjs');
10
+ const fetch = require('node-fetch');
11
+ const cuid = require('@paralleldrive/cuid2').init({length: 32});
12
+
13
+ // TODO fetch with retry
14
+
15
+ const handlePacketError = (packet, e, transport) => {
16
+ if (!packet.cb()) {
17
+ console.dir({msg: 'packet error', e, packet}, {depth: null});
18
+ return;
19
+ }
20
+
21
+ transport.send(transport.newPacket({c: packet.cb(), a: {error: '' + e}}));
22
+ };
23
+
24
+ const reply = (arg, packet, transport) => {
25
+ if (!packet.cb()) {
26
+ console.dir({msg: 'cannot reply to packet without cb', arg, packet}, {depth: null});
27
+ return;
28
+ }
29
+
30
+ transport.send(transport.newPacket({c: packet.cb(), a: {...arg}}));
31
+ };
32
+
33
+ const unwrap = async (ret, options) => {
34
+ if (options?.text) return await ret.text();
35
+ if (options?.base64) return (await ret.buffer()).toString('base64');
36
+
37
+ return await ret.json();
38
+ };
39
+
40
+ class Fetcher {
41
+ constructor({retry = 5, baseUrl}) {
42
+ this.retry = retry;
43
+ this.baseUrl = baseUrl;
44
+ }
45
+
46
+ async customize(options, args = {}) {}
47
+
48
+ async onError(e, url, options, retries, args) {
49
+ var local = this;
50
+
51
+ return new Promise((resolve, reject) => {
52
+ setTimeout(async () => {
53
+ try {
54
+ resolve(await local.fetch(url, options, retries, args));
55
+ } catch (e) {
56
+ reject(e);
57
+ }
58
+ }, 500);
59
+ });
60
+ }
61
+
62
+ async fetch(url, options = {}, retries, args = {}) {
63
+ var local = this,
64
+ baseUrl = local.baseUrl;
65
+
66
+ if (retries == null) retries = local.retry;
67
+
68
+ try {
69
+ const theURL = `${baseUrl?.endsWith('/') ? baseUrl : baseUrl + '/'}${url}`.replace(/\/\/+/gi, '/');
70
+
71
+ await local.customize(options, args);
72
+
73
+ const ret = await fetch(theURL, options);
74
+ const status = await ret.status;
75
+
76
+ if (status > 399) {
77
+ const text = await ret.text();
78
+ const e = new Error(status + ' ' + text);
79
+
80
+ e.status = status;
81
+ throw e;
82
+ }
83
+
84
+ return unwrap(ret, options);
85
+ } catch (e) {
86
+ --retries;
87
+
88
+ console.log(e);
89
+
90
+ if (retries <= 0) throw e;
91
+
92
+ return local.onError(e, url, options, retries, args);
93
+ }
94
+ }
95
+ }
96
+
97
+ class OAuthFetcher extends Fetcher {
98
+ constructor({oauth, retry = 5, getToken, baseUrl}) {
99
+ super({retry, baseUrl});
100
+
101
+ this.oauth = oauth;
102
+ this._getToken = getToken;
103
+ }
104
+
105
+ async getToken(force) {
106
+ var local = this,
107
+ oauth = local.oauth;
108
+
109
+ if (local._getToken) return local._getToken(force);
110
+
111
+ if (!force && oauth.accessToken()) return oauth.accessToken();
112
+
113
+ const refreshToken = oauth.refreshToken();
114
+ if (!refreshToken) throw new Error('have no access_token and no refresh_token');
115
+
116
+ const ret = await oauth.obtainViaRefreshToken(oauth.refreshToken());
117
+
118
+ if (ret.access_token) {
119
+ oauth.update(ret.access_token, ret.refresh_token);
120
+
121
+ return ret.access_token;
122
+ } else {
123
+ throw new Error('could not obtain access token via refresh token');
124
+ }
125
+ }
126
+
127
+ async onError(e, url, options, retries, args) {
128
+ var local = this;
129
+
130
+ return new Promise((resolve, reject) => {
131
+ setTimeout(async () => {
132
+ try {
133
+ resolve(await local.fetch(url, options, retries, {forceTokenRefresh: e.status === 401}));
134
+ } catch (e) {
135
+ reject(e);
136
+ }
137
+ }, 500);
138
+ });
139
+ }
140
+
141
+ async customize(options, args = {}) {
142
+ const token = await local.getToken(args.forceTokenRefresh);
143
+
144
+ options = {...options};
145
+ options.headers = {
146
+ ...options.headers,
147
+ Authorization: `Bearer ${token}`,
148
+ };
149
+ }
150
+ }
151
+
152
+ class OAuth {
153
+ constructor(data, saveOAuthResult, getRefreshToken) {
154
+ var local = this;
155
+ this._data = data || {};
156
+ this.saveOAuthResult = saveOAuthResult;
157
+ this.obtainViaRefreshToken = getRefreshToken;
158
+ }
159
+
160
+ data() {
161
+ return this._data;
162
+ }
163
+
164
+ accessToken() {
165
+ return this._data.access_token;
166
+ }
167
+
168
+ refreshToken() {
169
+ return this._data.refresh_token;
170
+ }
171
+
172
+ async update(accessToken, refreshToken) {
173
+ this._data.access_token = accessToken;
174
+
175
+ if (refreshToken) {
176
+ this._data.refresh_token = refreshToken;
177
+ }
178
+
179
+ await this.saveOAuthResult(this._data);
180
+ }
181
+
182
+ getClient(arg = {}) {
183
+ return new OAuthFetcher({...arg, oauth: this});
184
+ }
185
+ }
186
+
187
+ class Connector {
188
+ constructor({version, id, name}) {
189
+ this.id = id;
190
+ this.version = version;
191
+ this.name = name;
192
+ }
193
+
194
+ configure() {
195
+ return (this.dispatcher = new Dispatcher());
196
+ }
197
+
198
+ async run() {
199
+ var local = this;
200
+
201
+ const makeMetrics = () => {
202
+ const metrics = require('prom-client');
203
+
204
+ const defaultLabels = {
205
+ service: local.name,
206
+ connectorId: local.id,
207
+ connectorVersion: local.version,
208
+ node: process.env.HOSTNAME || 'test',
209
+ };
210
+ metrics.register.setDefaultLabels(defaultLabels);
211
+ metrics.collectDefaultMetrics();
212
+
213
+ return metrics;
214
+ };
215
+
216
+ const makeMetricsServer = (metrics) => {
217
+ const app = require('express')();
218
+
219
+ app.get('/metrics', async (request, response, next) => {
220
+ response.status(200);
221
+ response.set('Content-type', metrics.contentType);
222
+ response.send(await metrics.register.metrics());
223
+ response.end();
224
+ });
225
+
226
+ return app;
227
+ };
228
+
229
+ makeMetricsServer(makeMetrics()).listen(4050, '0.0.0.0');
230
+
231
+ const {processPacket, start, introspect, configSchema} = this.dispatcher.build();
232
+
233
+ const config = new Config({
234
+ id: this.id,
235
+ version: this.version,
236
+ name: process.env.HOSTNAME || this.name,
237
+ registrationToken: process.env.REGISTRATION_TOKEN,
238
+ endpoint: process.env.DEVICE_ENDPOINT || 'https://connect.aloma.io/',
239
+ wsEndpoint: process.env.WEBSOCKET_ENDPOINT || 'wss://transport.aloma.io/transport/',
240
+ privateKey: process.env.PRIVATE_KEY,
241
+ publicKey: process.env.PUBLIC_KEY,
242
+ introspect,
243
+ configSchema,
244
+ });
245
+
246
+ if (Object.keys(configSchema().fields).length) {
247
+ try {
248
+ await config.validateKeys();
249
+ } catch (e) {
250
+ const haveKey = !!process.env.PRIVATE_KEY;
251
+ const jwe = new JWE({});
252
+ var text = 'Please double check the env variables';
253
+
254
+ if (!haveKey) {
255
+ await jwe.newPair();
256
+ text =
257
+ 'fresh keys generated, set environment variables: \n\nPRIVATE_KEY: ' +
258
+ (await jwe.exportPrivateAsBase64()) +
259
+ '\n\nPUBLIC_KEY: ' +
260
+ (await jwe.exportPublicAsBase64()) +
261
+ '\n';
262
+ }
263
+
264
+ console.log(`
265
+ Error:
266
+
267
+ public (env.PUBLIC_KEY) and private key (env.PRIVATE_KEY) could not be loaded.
268
+
269
+ ${text}
270
+ `);
271
+
272
+ return;
273
+ }
274
+ }
275
+
276
+ const server = new WebsocketConnector({
277
+ config,
278
+ onConnect: (transport) => {
279
+ local.dispatcher.onConfig = async function (secrets) {
280
+ const decrypted = {};
281
+ const fields = configSchema().fields;
282
+
283
+ const keys = Object.keys(secrets);
284
+ const jwe = await config.validateKeys('RSA-OAEP-256');
285
+
286
+ for (var i = 0; i < keys.length; ++i) {
287
+ const key = keys[i];
288
+ const value = secrets[key];
289
+ if (!value) continue;
290
+
291
+ if (fields[key]?.plain) {
292
+ decrypted[key] = value;
293
+ } else {
294
+ try {
295
+ decrypted[key] = await jwe.decrypt(value, config.id());
296
+ } catch (e) {
297
+ console.log('failed to decrypt key', key, config.id(), e);
298
+ }
299
+ }
300
+ }
301
+
302
+ this.startOAuth = async function (args) {
303
+ if (!this._oauth) throw new Error('oauth not configured');
304
+
305
+ const clientId = this._oauth.clientId || process.env.OAUTH_CLIENT_ID || decrypted.clientId;
306
+ if (!clientId) throw new Error('clientId not configured');
307
+
308
+ const scopes = this._oauth.scope || process.env.OAUTH_SCOPE || decrypted.scope || '';
309
+ const useCodeChallenge = !!that._oauth.useCodeChallenge;
310
+
311
+ return {
312
+ url: this._oauth.authorizationURL
313
+ .replace(/\{\{clientId\}\}/gi, encodeURIComponent(clientId))
314
+ .replace(/\{\{scope\}\}/gi, encodeURIComponent(scopes)),
315
+ useCodeChallenge,
316
+ };
317
+ };
318
+
319
+ this.finishOAuth = async function (arg) {
320
+ var that = this;
321
+
322
+ if (!this._oauth) throw new Error('oauth not configured');
323
+ if (!this._oauth.tokenURL && !this._oauth.finishOAuth) throw new Error('need tokenURL or finishOAuth(arg)');
324
+
325
+ var data = null;
326
+
327
+ const doFinish = async () => {
328
+ if (!arg.code || !arg.redirectURI) throw new Error('need code and redirectUri');
329
+
330
+ const clientId = that._oauth.clientId || process.env.OAUTH_CLIENT_ID || decrypted.clientId;
331
+ if (!clientId) throw new Error('clientId not configured');
332
+
333
+ const clientSecret =
334
+ that._oauth.clientSecret || process.env.OAUTH_CLIENT_SECRET || decrypted.clientSecret;
335
+ if (!clientSecret) throw new Error('clientSecret not configured');
336
+
337
+ const additionalTokenArgs = that._oauth.additionalTokenArgs || {};
338
+ const useAuthHeader = !!that._oauth.useAuthHeader;
339
+ const useCodeChallenge = !!that._oauth.useCodeChallenge;
340
+
341
+ let body = {
342
+ ...additionalTokenArgs,
343
+ code: arg.code,
344
+ redirect_uri: arg.redirectURI,
345
+ };
346
+
347
+ if (useCodeChallenge) {
348
+ body.code_verifier = arg.codeVerifier;
349
+ }
350
+
351
+ let headers = {
352
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
353
+ Accept: 'application/json',
354
+ };
355
+
356
+ if (useAuthHeader) {
357
+ headers = {
358
+ ...headers,
359
+ Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
360
+ };
361
+ } else {
362
+ body = {
363
+ ...body,
364
+ client_id: clientId,
365
+ client_secret: clientSecret,
366
+ };
367
+ }
368
+
369
+ const response = await fetch(that._oauth.tokenURL, {
370
+ method: 'POST',
371
+ body: new URLSearchParams(body),
372
+ headers,
373
+ });
374
+
375
+ const status = await response.status;
376
+ const text = await response.text();
377
+
378
+ if (status === 200) {
379
+ const ret = JSON.parse(text);
380
+ if (ret.error) {
381
+ throw new Error(`${status} ${ret.error} ${ret.error_description || ''}`);
382
+ } else if (ret.access_token) {
383
+ return {...ret};
384
+ } else {
385
+ throw new Error(status + ' response has no access_token - ' + text);
386
+ }
387
+ } else {
388
+ throw new Error(status + ' ' + text);
389
+ }
390
+ };
391
+
392
+ if (this._oauth.finishOAuth) {
393
+ data = await this._oauth.finishOAuth({
394
+ arg,
395
+ doFinish,
396
+ transport,
397
+ });
398
+ } else {
399
+ data = await doFinish();
400
+ }
401
+
402
+ const jwe = await config.validateKeys('RSA-OAEP-256');
403
+
404
+ return {value: await jwe.encrypt(data, 'none', config.id())};
405
+ };
406
+
407
+ const saveOAuthResult = async (what) => {
408
+ const jwe = await config.validateKeys('RSA-OAEP-256');
409
+ const packet = transport.newPacket({});
410
+
411
+ packet.method('connector.config-update');
412
+ packet.args({
413
+ value: await jwe.encrypt(what, 'none', config.id()),
414
+ });
415
+
416
+ transport.send(packet);
417
+ };
418
+
419
+ const that = this;
420
+
421
+ const getRefreshToken = async (refreshToken) => {
422
+ const clientId = that._oauth.clientId || process.env.OAUTH_CLIENT_ID || decrypted.clientId;
423
+ if (!clientId) throw new Error('clientId not configured');
424
+
425
+ const clientSecret = that._oauth.clientSecret || process.env.OAUTH_CLIENT_SECRET || decrypted.clientSecret;
426
+ if (!clientSecret) throw new Error('clientSecret not configured');
427
+
428
+ const useAuthHeader = !!that._oauth.useAuthHeader;
429
+
430
+ let headers = {};
431
+
432
+ if (useAuthHeader) {
433
+ headers = {
434
+ ...headers,
435
+ Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
436
+ };
437
+ }
438
+
439
+ const response = await fetch(that._oauth.tokenURL, {
440
+ method: 'POST',
441
+ body: new URLSearchParams({
442
+ grant_type: 'refresh_token',
443
+ refresh_token: refreshToken,
444
+ client_id: clientId,
445
+ client_secret: clientSecret,
446
+ }),
447
+ headers: {
448
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
449
+ Accept: 'application/json',
450
+ ...headers,
451
+ },
452
+ });
453
+
454
+ const status = await response.status;
455
+ const text = await response.text();
456
+
457
+ if (status === 200) {
458
+ return JSON.parse(text);
459
+ } else {
460
+ throw new Error('could not get refresh token ' + status + ' ' + text);
461
+ }
462
+ };
463
+
464
+ const theOAuth = decrypted.oauthResult
465
+ ? new OAuth(decrypted.oauthResult, saveOAuthResult, getRefreshToken)
466
+ : null;
467
+ start({
468
+ config: decrypted,
469
+ oauth: theOAuth,
470
+ getClient: (arg) => (theOAuth ? theOAuth.getClient(arg) : new Fetcher({...arg})),
471
+ newTask: (name, data) => {
472
+ return new Promise((resolve, reject) => {
473
+ const packet = transport.newPacket(
474
+ {},
475
+ (ret) => (ret?.error ? reject(ret.error) : resolve(ret)),
476
+ `_req-${cuid()}`
477
+ );
478
+
479
+ packet.method('connector.task.new');
480
+ packet.args({
481
+ name,
482
+ a: data,
483
+ });
484
+
485
+ transport.send(packet);
486
+ });
487
+ },
488
+ updateTask: (id, data) => {
489
+ return new Promise((resolve, reject) => {
490
+ const packet = transport.newPacket(
491
+ {},
492
+ (ret) => (ret?.error ? reject(ret.error) : resolve(ret)),
493
+ `_req-${cuid()}`
494
+ );
495
+
496
+ packet.method('connector.task.update');
497
+ packet.args({
498
+ id,
499
+ a: data,
500
+ });
501
+
502
+ transport.send(packet);
503
+ });
504
+ },
505
+ });
506
+ };
507
+ },
508
+ onMessage: async (packet, transport) => {
509
+ try {
510
+ const ret = await processPacket(packet);
511
+ if (ret) reply(ret, packet, transport);
512
+ } catch (e) {
513
+ console.log(e);
514
+ handlePacketError(packet, e, transport);
515
+ }
516
+ },
517
+ });
518
+
519
+ const term = async () => {
520
+ await server.leaving();
521
+
522
+ await new Promise((resolve) => {
523
+ setTimeout(async () => {
524
+ await server.close();
525
+ resolve();
526
+ }, 10000);
527
+ });
528
+
529
+ process.exit(0);
530
+ };
531
+
532
+ process.on('uncaughtException', (e) => {
533
+ console.log(e);
534
+ });
535
+
536
+ process.on('unhandledRejection', (e) => {
537
+ console.log(e);
538
+ });
539
+
540
+ process.on('SIGTERM', term);
541
+ process.on('SIGINT', term);
542
+
543
+ await server.start();
544
+ }
545
+ }
546
+
547
+ module.exports = {Connector};
@@ -0,0 +1,14 @@
1
+ const JWE = require('./index');
2
+
3
+ const main = async () => {
4
+ const jwe = new JWE({});
5
+ await jwe.newPair();
6
+
7
+ console.log('private key');
8
+ console.log(await jwe.exportPrivateAsBase64());
9
+ console.log('public key');
10
+ console.log(await jwe.exportPublicAsBase64());
11
+ };
12
+
13
+ setTimeout(() => null, 100);
14
+ main();
@@ -0,0 +1,69 @@
1
+ const jose = require('jose');
2
+
3
+ class JWE {
4
+ constructor({algorithm = 'PS256'}) {
5
+ this.issuer = 'home.aloma.io';
6
+ this.algorithm = algorithm;
7
+ }
8
+
9
+ async newPair() {
10
+ this.pair = await jose.generateKeyPair(this.algorithm);
11
+ }
12
+
13
+ async exportPair() {
14
+ return {
15
+ publicKey: await jose.exportSPKI(this.pair.publicKey),
16
+ privateKey: await jose.exportPKCS8(this.pair.privateKey),
17
+ };
18
+ }
19
+
20
+ async exportPrivateAsBase64() {
21
+ const pair = await this.exportPair();
22
+
23
+ return Buffer.from(pair.privateKey).toString('base64');
24
+ }
25
+
26
+ async exportPublicAsBase64() {
27
+ const pair = await this.exportPair();
28
+
29
+ return Buffer.from(pair.publicKey).toString('base64');
30
+ }
31
+
32
+ async importPair({publicKey, privateKey, algorithm}) {
33
+ this.pair = {
34
+ publicKey: await jose.importSPKI(publicKey, algorithm),
35
+ privateKey: await jose.importPKCS8(privateKey, algorithm),
36
+ };
37
+ }
38
+
39
+ async importBase64Pair({publicKey, privateKey, algorithm}) {
40
+ this.importPair({
41
+ publicKey: Buffer.from(publicKey, 'base64').toString(),
42
+ privateKey: Buffer.from(privateKey, 'base64').toString(),
43
+ algorithm,
44
+ });
45
+ }
46
+
47
+ async encrypt(what, expiration = '7d', audience, algorithm = 'RSA-OAEP-256') {
48
+ const item = new jose.EncryptJWT({_data: {...what}})
49
+ .setProtectedHeader({alg: algorithm, enc: 'A256GCM'})
50
+ .setIssuedAt()
51
+ .setIssuer(this.issuer)
52
+ .setAudience(audience);
53
+
54
+ if (expiration && expiration !== 'none') item.setExpirationTime(expiration);
55
+
56
+ return await item.encrypt(this.pair.publicKey);
57
+ }
58
+
59
+ async decrypt(what, audience) {
60
+ const {payload, protectedHeader} = await jose.jwtDecrypt(what, this.pair.privateKey, {
61
+ issuer: this.issuer,
62
+ audience,
63
+ });
64
+
65
+ return payload._data;
66
+ }
67
+ }
68
+
69
+ module.exports = JWE;