@data-fair/lib-node 2.8.2 → 2.9.1
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/axios-auth.js +77 -65
- package/axios.js +50 -46
- package/events-queue.js +107 -101
- package/http-agents.js +44 -44
- package/locks.js +72 -70
- package/mongo.js +72 -68
- package/observer.js +86 -79
- package/package.json +1 -1
- package/session.d.ts +10 -0
- package/session.js +110 -0
- package/test-module-hooks.js +12 -12
- package/test-spies.js +39 -38
- package/upgrade-scripts.js +76 -70
- package/validation.js +1 -1
- package/ws-client.js +89 -85
- package/ws-emitter.js +15 -11
package/locks.js
CHANGED
|
@@ -1,75 +1,77 @@
|
|
|
1
|
-
import { hostname } from 'node:os'
|
|
2
|
-
import { randomBytes } from 'node:crypto'
|
|
3
|
-
import Debug from 'debug'
|
|
4
|
-
const debug = Debug('locks')
|
|
1
|
+
import { hostname } from 'node:os';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import Debug from 'debug';
|
|
4
|
+
const debug = Debug('locks');
|
|
5
5
|
export class Locks {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
get collection () {
|
|
15
|
-
return this.db.collection('locks')
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
constructor () {
|
|
19
|
-
this.pid = randomBytes(8).toString('hex')
|
|
20
|
-
debug('locks with pid', this.pid)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
start = async (db, ttl = 60) => {
|
|
24
|
-
if (this._db) { throw new Error('locks utils was already initialized') }
|
|
25
|
-
this._db = db
|
|
26
|
-
await this.collection.createIndex({ pid: 1 })
|
|
27
|
-
try {
|
|
28
|
-
await this.collection.createIndex({ updatedAt: 1 }, { expireAfterSeconds: ttl })
|
|
29
|
-
} catch (err) {
|
|
30
|
-
console.log('Failure to create TTL index. Probably because the value changed. Try to update it.')
|
|
31
|
-
db.command({ collMod: 'locks', index: { keyPattern: { updatedAt: 1 }, expireAfterSeconds: ttl } })
|
|
6
|
+
pid;
|
|
7
|
+
interval;
|
|
8
|
+
_db;
|
|
9
|
+
get db() {
|
|
10
|
+
if (!this._db)
|
|
11
|
+
throw new Error('locks utils was not initialized');
|
|
12
|
+
return this._db;
|
|
32
13
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
this.collection.updateMany({ pid: this.pid }, { $currentDate: { updatedAt: true } })
|
|
36
|
-
}, (ttl / 2) * 1000)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
stop = async () => {
|
|
40
|
-
clearInterval(this.interval)
|
|
41
|
-
if (this._db) { await this.collection.deleteMany({ pid: this.pid }) }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
acquire = async (_id, origin) => {
|
|
45
|
-
debug('acquire', _id, origin)
|
|
46
|
-
try {
|
|
47
|
-
await this.collection.insertOne({ _id, pid: this.pid, origin, hostname: hostname(), createdAt: new Date() })
|
|
48
|
-
try {
|
|
49
|
-
await this.collection.updateOne({ _id }, { $currentDate: { updatedAt: true } })
|
|
50
|
-
} catch (err) {
|
|
51
|
-
await this.collection.deleteOne({ _id, pid: this.pid })
|
|
52
|
-
throw err
|
|
53
|
-
}
|
|
54
|
-
debug('acquire ok', _id)
|
|
55
|
-
return true
|
|
56
|
-
} catch (err) {
|
|
57
|
-
if (err.code !== 11000) { throw err }
|
|
58
|
-
// duplicate means the lock was already acquired
|
|
59
|
-
debug('acquire ko', _id)
|
|
60
|
-
return false
|
|
14
|
+
get collection() {
|
|
15
|
+
return this.db.collection('locks');
|
|
61
16
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
debug('release', _id)
|
|
66
|
-
if (delay) {
|
|
67
|
-
const date = new Date((new Date()).getTime() + delay)
|
|
68
|
-
await this.collection.updateOne({ _id, pid: this.pid }, { $unset: { pid: 1 }, $set: { delayed: true, updatedAt: date } })
|
|
69
|
-
} else {
|
|
70
|
-
await this.collection.deleteOne({ _id, pid: this.pid })
|
|
17
|
+
constructor() {
|
|
18
|
+
this.pid = randomBytes(8).toString('hex');
|
|
19
|
+
debug('locks with pid', this.pid);
|
|
71
20
|
}
|
|
72
|
-
|
|
21
|
+
start = async (db, ttl = 60) => {
|
|
22
|
+
if (this._db)
|
|
23
|
+
throw new Error('locks utils was already initialized');
|
|
24
|
+
this._db = db;
|
|
25
|
+
await this.collection.createIndex({ pid: 1 });
|
|
26
|
+
try {
|
|
27
|
+
await this.collection.createIndex({ updatedAt: 1 }, { expireAfterSeconds: ttl });
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
console.log('Failure to create TTL index. Probably because the value changed. Try to update it.');
|
|
31
|
+
db.command({ collMod: 'locks', index: { keyPattern: { updatedAt: 1 }, expireAfterSeconds: ttl } });
|
|
32
|
+
}
|
|
33
|
+
// prolongate lock acquired by this process while it is still active
|
|
34
|
+
this.interval = setInterval(() => {
|
|
35
|
+
this.collection.updateMany({ pid: this.pid }, { $currentDate: { updatedAt: true } });
|
|
36
|
+
}, (ttl / 2) * 1000);
|
|
37
|
+
};
|
|
38
|
+
stop = async () => {
|
|
39
|
+
clearInterval(this.interval);
|
|
40
|
+
if (this._db)
|
|
41
|
+
await this.collection.deleteMany({ pid: this.pid });
|
|
42
|
+
};
|
|
43
|
+
acquire = async (_id, origin) => {
|
|
44
|
+
debug('acquire', _id, origin);
|
|
45
|
+
try {
|
|
46
|
+
await this.collection.insertOne({ _id, pid: this.pid, origin, hostname: hostname(), createdAt: new Date() });
|
|
47
|
+
try {
|
|
48
|
+
await this.collection.updateOne({ _id }, { $currentDate: { updatedAt: true } });
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
await this.collection.deleteOne({ _id, pid: this.pid });
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
debug('acquire ok', _id);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (err.code !== 11000)
|
|
59
|
+
throw err;
|
|
60
|
+
// duplicate means the lock was already acquired
|
|
61
|
+
debug('acquire ko', _id);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
release = async (_id, delay = 0) => {
|
|
66
|
+
debug('release', _id);
|
|
67
|
+
if (delay) {
|
|
68
|
+
const date = new Date((new Date()).getTime() + delay);
|
|
69
|
+
await this.collection.updateOne({ _id, pid: this.pid }, { $unset: { pid: 1 }, $set: { delayed: true, updatedAt: date } });
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
await this.collection.deleteOne({ _id, pid: this.pid });
|
|
73
|
+
}
|
|
74
|
+
};
|
|
73
75
|
}
|
|
74
|
-
const locks = new Locks()
|
|
75
|
-
export default locks
|
|
76
|
+
const locks = new Locks();
|
|
77
|
+
export default locks;
|
package/mongo.js
CHANGED
|
@@ -1,76 +1,80 @@
|
|
|
1
|
-
import { MongoClient, MongoError } from 'mongodb'
|
|
1
|
+
import { MongoClient, MongoError } from 'mongodb';
|
|
2
2
|
export class Mongo {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
get db () {
|
|
11
|
-
if (!this._db) { throw new Error('db was not connected') }
|
|
12
|
-
return this._db
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
connect = async (mongoUrl, options = {}) => {
|
|
16
|
-
if (this._client) {
|
|
17
|
-
console.warn('db already connected')
|
|
18
|
-
return
|
|
3
|
+
_client;
|
|
4
|
+
_db;
|
|
5
|
+
get client() {
|
|
6
|
+
if (!this._client)
|
|
7
|
+
throw new Error('db was not connected');
|
|
8
|
+
return this._client;
|
|
19
9
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Create an index if it does not exist and manage overwriting existing and conflicting index definitions
|
|
27
|
-
ensureIndex = async (collection, key, options = {}) => {
|
|
28
|
-
try {
|
|
29
|
-
await this.db.collection(collection).createIndex(key, options)
|
|
30
|
-
} catch (err) {
|
|
31
|
-
if (options.name && err instanceof MongoError && (err.code === 85 || err.code === 86)) {
|
|
32
|
-
// if the error is a conflict on keys or params of the index we automatically
|
|
33
|
-
// delete then recreate the index
|
|
34
|
-
console.log(`drop then recreate index ${collection}/${options.name}`)
|
|
35
|
-
await this.db.collection(collection).dropIndex(options.name)
|
|
36
|
-
await this.db.collection(collection).createIndex(key, options)
|
|
37
|
-
} else {
|
|
38
|
-
throw err
|
|
39
|
-
}
|
|
10
|
+
get db() {
|
|
11
|
+
if (!this._db)
|
|
12
|
+
throw new Error('db was not connected');
|
|
13
|
+
return this._db;
|
|
40
14
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
15
|
+
connect = async (mongoUrl, options = {}) => {
|
|
16
|
+
if (this._client) {
|
|
17
|
+
console.warn('db already connected');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.log('connecting to mongodb...');
|
|
21
|
+
this._client = await MongoClient.connect(mongoUrl, { maxPoolSize: 5, ignoreUndefined: true, ...options });
|
|
22
|
+
this._db = this._client.db();
|
|
23
|
+
console.log('...ok');
|
|
24
|
+
};
|
|
25
|
+
// Create an index if it does not exist and manage overwriting existing and conflicting index definitions
|
|
26
|
+
ensureIndex = async (collection, key, options = {}) => {
|
|
27
|
+
try {
|
|
28
|
+
await this.db.collection(collection).createIndex(key, options);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
if (options.name && err instanceof MongoError && (err.code === 85 || err.code === 86)) {
|
|
32
|
+
// if the error is a conflict on keys or params of the index we automatically
|
|
33
|
+
// delete then recreate the index
|
|
34
|
+
console.log(`drop then recreate index ${collection}/${options.name}`);
|
|
35
|
+
await this.db.collection(collection).dropIndex(options.name);
|
|
36
|
+
await this.db.collection(collection).createIndex(key, options);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
44
|
* create a bunch of indexes and overtwrite previous definitions
|
|
45
45
|
* the structure forces giving names to indexes which is better for managing them properly
|
|
46
46
|
*/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
47
|
+
configure = async (indexDefinitions) => {
|
|
48
|
+
for (const collectionName in indexDefinitions) {
|
|
49
|
+
try {
|
|
50
|
+
await this.db.createCollection(collectionName);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
// nothing TODO
|
|
54
|
+
}
|
|
55
|
+
for (const indexName in indexDefinitions[collectionName]) {
|
|
56
|
+
let key, options;
|
|
57
|
+
const indexDefinition = indexDefinitions[collectionName][indexName];
|
|
58
|
+
if (indexDefinition === null) {
|
|
59
|
+
if (await this.db.collection(collectionName).indexExists(indexName)) {
|
|
60
|
+
await this.db.collection(collectionName).dropIndex(indexName);
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
else if (Array.isArray(indexDefinition)) {
|
|
65
|
+
[key, options] = indexDefinition;
|
|
66
|
+
if (options.name && options.name !== indexName)
|
|
67
|
+
throw new Error(`inconsistent index name ${indexName} or ${options.name}`);
|
|
68
|
+
options.name = indexName;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
key = indexDefinition;
|
|
72
|
+
options = { name: indexName };
|
|
73
|
+
}
|
|
74
|
+
await this.ensureIndex(collectionName, key, options);
|
|
75
|
+
}
|
|
69
76
|
}
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
77
|
+
};
|
|
74
78
|
}
|
|
75
|
-
const mongo = new Mongo()
|
|
76
|
-
export default mongo
|
|
79
|
+
const mongo = new Mongo();
|
|
80
|
+
export default mongo;
|
package/observer.js
CHANGED
|
@@ -1,102 +1,109 @@
|
|
|
1
1
|
// very light web server to expose prometheus metrics, export a cpu profile and maybe other incoming obervability features
|
|
2
|
-
import { hostname } from 'node:os'
|
|
3
|
-
import { createServer } from 'node:http'
|
|
4
|
-
import { Registry, Counter, register, collectDefaultMetrics } from 'prom-client'
|
|
5
|
-
import eventPromise from '@data-fair/lib-utils/event-promise.js'
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { Registry, Counter, register, collectDefaultMetrics } from 'prom-client';
|
|
5
|
+
import eventPromise from '@data-fair/lib-utils/event-promise.js';
|
|
6
6
|
// a special registry for metrics that are global
|
|
7
7
|
// not based on this specific process state but instead on the content of db for example
|
|
8
|
-
export const servicePromRegistry = new Registry()
|
|
9
|
-
collectDefaultMetrics()
|
|
8
|
+
export const servicePromRegistry = new Registry();
|
|
9
|
+
collectDefaultMetrics();
|
|
10
10
|
// local metrics incremented throughout the code
|
|
11
11
|
const internalErrorCounter = new Counter({
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
})
|
|
12
|
+
name: 'df_internal_error',
|
|
13
|
+
help: 'A counter of errors from any service, worker, etc. Do not use for client errors, only for anomalies that should trigger alerts. Each increment should be accompanied by an error log with matching code.',
|
|
14
|
+
labelNames: ['errorCode']
|
|
15
|
+
});
|
|
16
16
|
export const internalError = (errorCode, error, ...optionalParams) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
internalErrorCounter.inc({ errorCode });
|
|
18
|
+
const message = error.message || error;
|
|
19
|
+
console.error(`[${errorCode}] ${message}`, ...optionalParams);
|
|
20
|
+
if (error.stack)
|
|
21
|
+
console.error(error.stack);
|
|
22
|
+
};
|
|
22
23
|
/**
|
|
23
24
|
* @param {import('node:http').ServerResponse} res
|
|
24
25
|
* @param {any} err
|
|
25
26
|
*/
|
|
26
27
|
const httpErrorHandler = (res, err) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
28
|
+
console.error('failed to server prometheus /metrics', err);
|
|
29
|
+
res.writeHead(500);
|
|
30
|
+
res.end();
|
|
31
|
+
};
|
|
31
32
|
const serveRegistry = async (res, registry) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
33
|
+
const metrics = await registry.metrics();
|
|
34
|
+
res.setHeader('Content-Type', registry.contentType);
|
|
35
|
+
res.writeHead(200);
|
|
36
|
+
res.write(metrics);
|
|
37
|
+
res.end();
|
|
38
|
+
};
|
|
38
39
|
// live CPU performance inspection
|
|
39
40
|
const serveCPUProfile = async (res, duration) => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
41
|
+
const { Session } = await import('node:inspector/promises');
|
|
42
|
+
const session = new Session();
|
|
43
|
+
session.connect();
|
|
44
|
+
await session.post('Profiler.enable');
|
|
45
|
+
await session.post('Profiler.start');
|
|
46
|
+
await new Promise(resolve => setTimeout(resolve, duration));
|
|
47
|
+
const { profile } = await session.post('Profiler.stop');
|
|
48
|
+
await session.post('Profiler.disable');
|
|
49
|
+
session.disconnect();
|
|
50
|
+
res.setHeader('Content-Disposition', `attachment; filename="data-fair-${hostname()}-${new Date().toISOString()}.cpuprofile"`);
|
|
51
|
+
res.write(JSON.stringify(profile));
|
|
52
|
+
res.end();
|
|
53
|
+
};
|
|
53
54
|
// live heap allocation sampler
|
|
54
55
|
const serveHeapSnapshot = async (res, includeCollected, duration) => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
let server
|
|
56
|
+
const { Session } = await import('node:inspector/promises');
|
|
57
|
+
const session = new Session();
|
|
58
|
+
session.connect();
|
|
59
|
+
await session.post('HeapProfiler.enable');
|
|
60
|
+
await session.post('HeapProfiler.startSampling', { includeObjectsCollectedByMajorGC: includeCollected, includeObjectsCollectedByMinorGC: true });
|
|
61
|
+
await new Promise(resolve => setTimeout(resolve, duration));
|
|
62
|
+
const { profile } = await session.post('HeapProfiler.stopSampling');
|
|
63
|
+
await session.post('HeapProfiler.disable');
|
|
64
|
+
session.disconnect();
|
|
65
|
+
res.setHeader('Content-Disposition', `attachment; filename="data-fair-${hostname()}-${new Date().toISOString()}.heapprofile"`);
|
|
66
|
+
res.write(JSON.stringify(profile));
|
|
67
|
+
res.end();
|
|
68
|
+
};
|
|
69
|
+
let server;
|
|
69
70
|
export const startObserver = async (port = 9090) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
71
|
+
server = createServer((req, res) => {
|
|
72
|
+
const url = new URL(req.url || '', `http://${req.headers.host}`);
|
|
73
|
+
if (req.method === 'GET' && url.pathname === '/metrics') {
|
|
74
|
+
serveRegistry(res, register).catch(err => httpErrorHandler(res, err));
|
|
75
|
+
}
|
|
76
|
+
else if (req.method === 'GET' && (url.pathname === '/global-metrics' || url.pathname === '/service-metrics')) {
|
|
77
|
+
serveRegistry(res, servicePromRegistry).catch(err => httpErrorHandler(res, err));
|
|
78
|
+
}
|
|
79
|
+
else if (req.method === 'GET' && url.pathname === '/cpu-profile') {
|
|
80
|
+
const duration = parseInt(url.searchParams.get('duration') || '2000');
|
|
81
|
+
serveCPUProfile(res, duration).catch(err => httpErrorHandler(res, err));
|
|
82
|
+
}
|
|
83
|
+
else if (req.method === 'GET' && url.pathname === '/heap-profile') {
|
|
84
|
+
const duration = parseInt(url.searchParams.get('duration') || '2000');
|
|
85
|
+
serveHeapSnapshot(res, false, duration).catch(err => httpErrorHandler(res, err));
|
|
86
|
+
}
|
|
87
|
+
else if (req.method === 'GET' && url.pathname === '/heap-profile-all') {
|
|
88
|
+
const duration = parseInt(url.searchParams.get('duration') || '2000');
|
|
89
|
+
serveHeapSnapshot(res, true, duration).catch(err => httpErrorHandler(res, err));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
res.writeHead(404);
|
|
93
|
+
res.end();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
server.listen(port);
|
|
97
|
+
await eventPromise(server, 'listening');
|
|
98
|
+
console.log(`Observer server available on http://localhost:${port}
|
|
93
99
|
GET /metrics -> get prometheus metrics for this instance
|
|
94
100
|
GET /service-metrics -> get prometheus metrics shared accross all instances
|
|
95
101
|
GET /cpu-profile -> generate and fetch a CPU profile
|
|
96
102
|
GET /heap-profile -> generate and fetch a sampled heap allocation snapshot
|
|
97
103
|
GET /heap-profile-all -> generate and fetch a sampled heap allocation snapshot including objects collected by GC
|
|
98
|
-
`)
|
|
99
|
-
}
|
|
104
|
+
`);
|
|
105
|
+
};
|
|
100
106
|
export const stopObserver = async () => {
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
if (server)
|
|
108
|
+
server.close();
|
|
109
|
+
};
|
package/package.json
CHANGED
package/session.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SessionState } from '@data-fair/lib-common-types/session/index.js';
|
|
2
|
+
import JwksClient from 'jwks-rsa';
|
|
3
|
+
export declare class SessionHandler {
|
|
4
|
+
private _jwksClient;
|
|
5
|
+
defaultLang: string;
|
|
6
|
+
get jwksClient(): JwksClient.JwksClient;
|
|
7
|
+
initJWKS(directoryUrl?: string): void;
|
|
8
|
+
verifyToken(token: string): Promise<any>;
|
|
9
|
+
readStateFromCookie(cookieStr: string | undefined, onlyDecode?: boolean, clearBrokenSession?: () => void): Promise<SessionState>;
|
|
10
|
+
}
|
package/session.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
import JwksClient from 'jwks-rsa';
|
|
3
|
+
import cookie from 'cookie';
|
|
4
|
+
import { httpError } from '@data-fair/lib-utils/http-errors.js';
|
|
5
|
+
export class SessionHandler {
|
|
6
|
+
_jwksClient;
|
|
7
|
+
defaultLang = 'fr';
|
|
8
|
+
get jwksClient() {
|
|
9
|
+
if (!this._jwksClient)
|
|
10
|
+
throw new Error('session management was not initialized');
|
|
11
|
+
return this._jwksClient;
|
|
12
|
+
}
|
|
13
|
+
initJWKS(directoryUrl = 'http://simple-directory:8080') {
|
|
14
|
+
this._jwksClient = JwksClient({ jwksUri: directoryUrl + '/.well-known/jwks.json' });
|
|
15
|
+
}
|
|
16
|
+
// used for the main session token, but can also be used to use the jwks to verify other types of tokens
|
|
17
|
+
async verifyToken(token) {
|
|
18
|
+
const decoded = jwt.decode(token, { complete: true });
|
|
19
|
+
if (!decoded)
|
|
20
|
+
throw new Error('failed to decode token');
|
|
21
|
+
const signingKey = await this.jwksClient.getSigningKey(decoded.header.kid);
|
|
22
|
+
return jwt.verify(token, signingKey.getPublicKey());
|
|
23
|
+
}
|
|
24
|
+
async readStateFromCookie(cookieStr, onlyDecode, clearBrokenSession) {
|
|
25
|
+
const session = { lang: this.defaultLang };
|
|
26
|
+
if (!cookieStr)
|
|
27
|
+
return session;
|
|
28
|
+
const cookies = cookie.parse(cookieStr);
|
|
29
|
+
session.dark = cookies.theme_dark === '1' || cookies.theme_dark === 'true';
|
|
30
|
+
if (cookies.i18n_lang)
|
|
31
|
+
session.lang = cookies.i18n_lang;
|
|
32
|
+
if (!cookies.id_token || !cookies.id_token_sign)
|
|
33
|
+
return session;
|
|
34
|
+
const token = cookies.id_token + '.' + cookies.id_token_sign;
|
|
35
|
+
let user;
|
|
36
|
+
if (onlyDecode) {
|
|
37
|
+
user = jwt.decode(token);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
try {
|
|
41
|
+
user = await this.verifyToken(token);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.warn(err);
|
|
45
|
+
if (err.name === 'JwksError') {
|
|
46
|
+
// happens in case of temporary unavailability if SD
|
|
47
|
+
// better not to disconnect user in this case
|
|
48
|
+
throw httpError(500, 'Session token public keys not initialized');
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
if (clearBrokenSession)
|
|
52
|
+
clearBrokenSession();
|
|
53
|
+
throw httpError(401);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!user)
|
|
58
|
+
return session;
|
|
59
|
+
// this is to prevent null values that are put by SD versions that do not strictly respect their schema
|
|
60
|
+
for (const org of user.organizations) {
|
|
61
|
+
if (!org.department) {
|
|
62
|
+
delete org.department;
|
|
63
|
+
delete org.departmentName;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
session.user = user;
|
|
67
|
+
const organizationId = cookies.id_token_org;
|
|
68
|
+
const departmentId = cookies.id_token_dep;
|
|
69
|
+
const switchedRole = cookies.id_token_role;
|
|
70
|
+
if (organizationId) {
|
|
71
|
+
session.organization = user.organizations.find(o => {
|
|
72
|
+
if (o.id !== organizationId)
|
|
73
|
+
return false;
|
|
74
|
+
if (departmentId && departmentId !== o.department)
|
|
75
|
+
return false;
|
|
76
|
+
if (switchedRole && switchedRole !== o.role)
|
|
77
|
+
return false;
|
|
78
|
+
return true;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (session.organization) {
|
|
82
|
+
session.account = {
|
|
83
|
+
type: 'organization',
|
|
84
|
+
id: session.organization.id,
|
|
85
|
+
name: session.organization.name,
|
|
86
|
+
department: session.organization.department,
|
|
87
|
+
departmentName: session.organization.departmentName
|
|
88
|
+
};
|
|
89
|
+
session.accountRole = session.organization.role;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
session.account = {
|
|
93
|
+
type: 'user',
|
|
94
|
+
id: user.id,
|
|
95
|
+
name: user.name
|
|
96
|
+
};
|
|
97
|
+
session.accountRole = 'admin';
|
|
98
|
+
return session;
|
|
99
|
+
}
|
|
100
|
+
if (session.user?.siteOwner) {
|
|
101
|
+
if (session.user.siteOwner.type === 'user' && session.user.siteOwner.id === session.user.id) {
|
|
102
|
+
session.siteRole = 'admin';
|
|
103
|
+
}
|
|
104
|
+
if (session.user.siteOwner.type === 'organization' && session.user.siteOwner.id === session.organization?.id) {
|
|
105
|
+
session.siteRole = session.organization.role;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return session;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/test-module-hooks.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
// cf https://nodejs.org/api/module.html#customization-hooks
|
|
3
|
-
const regexp = /\/\/ @test:spy\((.*)\)\n/g
|
|
4
|
-
export async function load
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
const regexp = /\/\/ @test:spy\((.*)\)\n/g;
|
|
4
|
+
export async function load(url, context, nextLoad) {
|
|
5
|
+
const result = await nextLoad(url, context);
|
|
6
|
+
if (result.format === 'module' || result.format === 'module-typescript') {
|
|
7
|
+
let source = result.source.toString();
|
|
8
|
+
const matches = source.match(regexp);
|
|
9
|
+
if (matches) {
|
|
10
|
+
source = 'import { emit as __emitTestSpy } from "@data-fair/lib-node/test-spies.js"\n' + source;
|
|
11
|
+
source = source.replace(regexp, '__emitTestSpy($1)\n');
|
|
12
|
+
result.source = Buffer.from(source, 'utf-8');
|
|
13
|
+
}
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
-
return result
|
|
15
|
+
return result;
|
|
16
16
|
}
|