@data-fair/lib-node 2.11.0 → 2.12.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.d.ts +2 -4
- package/axios-auth.js +10 -37
- package/axios-with-cookies.d.ts +13 -0
- package/axios-with-cookies.js +41 -0
- package/package.json +1 -1
- package/session.test.d.ts +1 -0
- package/session.test.js +40 -0
- package/ws-client.d.ts +4 -1
- package/ws-client.js +50 -8
package/axios-auth.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { CookieJar } from 'tough-cookie';
|
|
1
|
+
import { type AxiosWithCookiesInstance } from './axios-with-cookies.js';
|
|
3
2
|
export interface AxiosAuthOptions {
|
|
4
3
|
email: string;
|
|
5
4
|
password: string;
|
|
@@ -11,8 +10,7 @@ export interface AxiosAuthOptions {
|
|
|
11
10
|
orgStorage?: boolean;
|
|
12
11
|
globalCookies?: boolean;
|
|
13
12
|
}
|
|
14
|
-
export type AxiosAuthInstance =
|
|
15
|
-
cookieJar: CookieJar;
|
|
13
|
+
export type AxiosAuthInstance = AxiosWithCookiesInstance & {
|
|
16
14
|
setOrg: (org: string, dep?: string) => void;
|
|
17
15
|
};
|
|
18
16
|
export declare function axiosAuth(opts: AxiosAuthOptions): Promise<AxiosAuthInstance>;
|
package/axios-auth.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
// build axios instances with sessions on a simple-directory instance
|
|
2
2
|
// WARNING: use for integration testing only
|
|
3
3
|
import { Agent } from 'node:http';
|
|
4
|
-
import { axiosBuilder, axiosInstance } from './axios.js';
|
|
5
4
|
import { CookieJar } from 'tough-cookie';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
throw new Error('url is required');
|
|
9
|
-
return new URL((config.baseURL && config.url?.startsWith('/')) ? (config.baseURL + config.url) : config.url);
|
|
10
|
-
};
|
|
5
|
+
import { axiosInstance } from './axios.js';
|
|
6
|
+
import { axiosWithCookies } from './axios-with-cookies.js';
|
|
11
7
|
export async function axiosAuth(opts) {
|
|
12
8
|
const body = { email: opts.email, password: opts.password };
|
|
13
9
|
if (opts.org)
|
|
@@ -28,8 +24,8 @@ export async function axiosAuth(opts) {
|
|
|
28
24
|
if (callbackUrl.startsWith(directoryUrl + '/simple-directory')) {
|
|
29
25
|
callbackUrl = callbackUrl.replace(directoryUrl + '/simple-directory', directoryUrl);
|
|
30
26
|
}
|
|
31
|
-
const cookieJar = new CookieJar();
|
|
32
27
|
const sdOrigin = new URL(directoryUrl).origin;
|
|
28
|
+
const cookieJar = new CookieJar();
|
|
33
29
|
try {
|
|
34
30
|
await axiosInstance.get(callbackUrl, { maxRedirects: 0 });
|
|
35
31
|
}
|
|
@@ -43,38 +39,15 @@ export async function axiosAuth(opts) {
|
|
|
43
39
|
for (const cookie of err.headers['set-cookie'] ?? [])
|
|
44
40
|
cookieJar.setCookieSync(cookie, sdOrigin);
|
|
45
41
|
}
|
|
46
|
-
const ax =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (!opts.globalCookies || url.origin === sdOrigin) {
|
|
52
|
-
config.headers.Cookie = cookieJar.getCookiesSync(url.href);
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
config.headers.Cookie = cookieJar.getCookiesSync(sdOrigin);
|
|
56
|
-
}
|
|
57
|
-
return config;
|
|
58
|
-
});
|
|
59
|
-
ax.interceptors.response.use((res) => {
|
|
60
|
-
const origin = opts.globalCookies ? sdOrigin : getConfigUrl(res.config).origin;
|
|
61
|
-
for (const cookie of res.headers['set-cookie'] ?? [])
|
|
62
|
-
cookieJar.setCookieSync(cookie, origin);
|
|
63
|
-
return res;
|
|
64
|
-
}, async (error) => {
|
|
65
|
-
const res = error.response;
|
|
66
|
-
if (res) {
|
|
67
|
-
const origin = opts.globalCookies ? sdOrigin : getConfigUrl(res.config).origin;
|
|
68
|
-
for (const cookie of res.headers['set-cookie'] ?? [])
|
|
69
|
-
cookieJar.setCookieSync(cookie, origin);
|
|
70
|
-
}
|
|
71
|
-
return Promise.reject(error);
|
|
72
|
-
});
|
|
42
|
+
const ax = axiosWithCookies({
|
|
43
|
+
axiosOpts,
|
|
44
|
+
cookiesOrigin: sdOrigin,
|
|
45
|
+
globalCookies: opts.globalCookies,
|
|
46
|
+
cookieJar
|
|
73
47
|
});
|
|
74
|
-
ax.cookieJar = cookieJar;
|
|
75
48
|
ax.setOrg = (org, dep) => {
|
|
76
|
-
cookieJar.setCookieSync(`id_token_org=${org}`, sdOrigin);
|
|
77
|
-
cookieJar.setCookieSync(`id_token_dep=${dep ?? ''}`, sdOrigin);
|
|
49
|
+
ax.cookieJar.setCookieSync(`id_token_org=${org}`, sdOrigin);
|
|
50
|
+
ax.cookieJar.setCookieSync(`id_token_dep=${dep ?? ''}`, sdOrigin);
|
|
78
51
|
};
|
|
79
52
|
return ax;
|
|
80
53
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AxiosInstance } from 'axios';
|
|
2
|
+
import { CookieJar } from 'tough-cookie';
|
|
3
|
+
export interface AxiosWithCookiesOptions {
|
|
4
|
+
axiosOpts?: any;
|
|
5
|
+
cookiesOrigin?: string;
|
|
6
|
+
globalCookies?: boolean;
|
|
7
|
+
cookieJar?: CookieJar;
|
|
8
|
+
}
|
|
9
|
+
export type AxiosWithCookiesInstance = AxiosInstance & {
|
|
10
|
+
cookieJar: CookieJar;
|
|
11
|
+
};
|
|
12
|
+
export declare function axiosWithCookies(opts?: AxiosWithCookiesOptions): AxiosWithCookiesInstance;
|
|
13
|
+
export default axiosWithCookies;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// build axios instances with cookie jar support
|
|
2
|
+
import { axiosBuilder } from './axios.js';
|
|
3
|
+
import { CookieJar } from 'tough-cookie';
|
|
4
|
+
const getConfigUrl = (config) => {
|
|
5
|
+
if (!config.url)
|
|
6
|
+
throw new Error('url is required');
|
|
7
|
+
return new URL((config.baseURL && config.url?.startsWith('/')) ? (config.baseURL + config.url) : config.url);
|
|
8
|
+
};
|
|
9
|
+
export function axiosWithCookies(opts = {}) {
|
|
10
|
+
const cookieJar = opts.cookieJar ?? new CookieJar();
|
|
11
|
+
const cookiesOrigin = opts.cookiesOrigin ?? '';
|
|
12
|
+
const ax = axiosBuilder(opts.axiosOpts, (ax) => {
|
|
13
|
+
ax.interceptors.request.use((config) => {
|
|
14
|
+
const url = getConfigUrl(config);
|
|
15
|
+
if (!opts.globalCookies || url.origin === cookiesOrigin) {
|
|
16
|
+
config.headers.Cookie = cookieJar.getCookiesSync(url.href);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
config.headers.Cookie = cookieJar.getCookiesSync(cookiesOrigin);
|
|
20
|
+
}
|
|
21
|
+
return config;
|
|
22
|
+
});
|
|
23
|
+
ax.interceptors.response.use((res) => {
|
|
24
|
+
const origin = opts.globalCookies ? cookiesOrigin : getConfigUrl(res.config).origin;
|
|
25
|
+
for (const cookie of res.headers['set-cookie'] ?? [])
|
|
26
|
+
cookieJar.setCookieSync(cookie, origin);
|
|
27
|
+
return res;
|
|
28
|
+
}, async (error) => {
|
|
29
|
+
const res = error.response;
|
|
30
|
+
if (res) {
|
|
31
|
+
const origin = opts.globalCookies ? cookiesOrigin : getConfigUrl(res.config).origin;
|
|
32
|
+
for (const cookie of res.headers['set-cookie'] ?? [])
|
|
33
|
+
cookieJar.setCookieSync(cookie, origin);
|
|
34
|
+
}
|
|
35
|
+
return Promise.reject(error);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
ax.cookieJar = cookieJar;
|
|
39
|
+
return ax;
|
|
40
|
+
}
|
|
41
|
+
export default axiosWithCookies;
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/session.test.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'assert';
|
|
3
|
+
import { SessionHandler } from './session.js';
|
|
4
|
+
describe('SessionHandler.readStateFromCookie', () => {
|
|
5
|
+
it('should return default session with no cookies', async () => {
|
|
6
|
+
const handler = new SessionHandler();
|
|
7
|
+
const state = await handler.readStateFromCookie(undefined);
|
|
8
|
+
assert.equal(state.lang, 'fr');
|
|
9
|
+
assert.equal(state.user, undefined);
|
|
10
|
+
assert.equal(state.dark, undefined);
|
|
11
|
+
});
|
|
12
|
+
it('should parse dark mode cookie', async () => {
|
|
13
|
+
const handler = new SessionHandler();
|
|
14
|
+
const state = await handler.readStateFromCookie('theme_dark=true');
|
|
15
|
+
assert.equal(state.dark, true);
|
|
16
|
+
});
|
|
17
|
+
it('should parse i18n_lang cookie', async () => {
|
|
18
|
+
const handler = new SessionHandler();
|
|
19
|
+
const state = await handler.readStateFromCookie('i18n_lang=en');
|
|
20
|
+
assert.equal(state.lang, 'en');
|
|
21
|
+
});
|
|
22
|
+
it('should use defaultLang when no i18n_lang cookie', async () => {
|
|
23
|
+
const handler = new SessionHandler();
|
|
24
|
+
handler.defaultLang = 'de';
|
|
25
|
+
const state = await handler.readStateFromCookie('');
|
|
26
|
+
assert.equal(state.lang, 'de');
|
|
27
|
+
});
|
|
28
|
+
it('should return early when no id_token', async () => {
|
|
29
|
+
const handler = new SessionHandler();
|
|
30
|
+
const state = await handler.readStateFromCookie('i18n_lang=en; theme_dark=1');
|
|
31
|
+
assert.equal(state.lang, 'en');
|
|
32
|
+
assert.equal(state.dark, true);
|
|
33
|
+
assert.equal(state.user, undefined);
|
|
34
|
+
});
|
|
35
|
+
it('should return early when no id_token_sign and not onlyDecode', async () => {
|
|
36
|
+
const handler = new SessionHandler();
|
|
37
|
+
const state = await handler.readStateFromCookie('id_token=header.payload');
|
|
38
|
+
assert.equal(state.user, undefined);
|
|
39
|
+
});
|
|
40
|
+
});
|
package/ws-client.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { type Message } from '@data-fair/lib-common-types/ws.js';
|
|
2
2
|
import { type Account } from '@data-fair/lib-common-types/session/index.js';
|
|
3
3
|
import EventEmitter from 'node:events';
|
|
4
|
+
declare class WsClientError extends Error {
|
|
5
|
+
constructor();
|
|
6
|
+
}
|
|
4
7
|
type logFn = (msg: string, ...args: any[]) => void;
|
|
5
8
|
export interface WsClientOpts {
|
|
6
9
|
log?: {
|
|
@@ -23,7 +26,7 @@ export declare class WsClient extends EventEmitter {
|
|
|
23
26
|
private _connect;
|
|
24
27
|
private _reconnect;
|
|
25
28
|
subscribe(channel: string, force?: boolean, timeout?: number): Promise<void>;
|
|
26
|
-
waitFor(channel: string, filter?: (message: Message) => boolean, timeout?: number, skipSubscribe?: boolean, fullMessage?: boolean): Promise<Message>;
|
|
29
|
+
waitFor(channel: string, filter?: (message: Message) => boolean, timeout?: number, skipSubscribe?: boolean, fullMessage?: boolean, _errorContext?: WsClientError): Promise<Message>;
|
|
27
30
|
close(): void;
|
|
28
31
|
}
|
|
29
32
|
export declare class DataFairWsClient extends WsClient {
|
package/ws-client.js
CHANGED
|
@@ -4,6 +4,13 @@ import EventEmitter from 'node:events';
|
|
|
4
4
|
import WebSocket from 'ws';
|
|
5
5
|
import Debug from 'debug';
|
|
6
6
|
const debug = Debug('ws');
|
|
7
|
+
const { stackTraceLimit } = Error;
|
|
8
|
+
class WsClientError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
this.name = 'WsClientError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
7
14
|
export class WsClient extends EventEmitter {
|
|
8
15
|
_channels;
|
|
9
16
|
_ws;
|
|
@@ -49,6 +56,12 @@ export class WsClient extends EventEmitter {
|
|
|
49
56
|
if (this._channels.includes(channel) && !force)
|
|
50
57
|
return;
|
|
51
58
|
const ws = this._ws ?? await this._connect();
|
|
59
|
+
// better stack traces
|
|
60
|
+
// see https://github.com/axios/axios/issues/2387#issuecomment-652242713
|
|
61
|
+
Error.stackTraceLimit = 0;
|
|
62
|
+
const errorContext = new WsClientError();
|
|
63
|
+
Error.stackTraceLimit = stackTraceLimit;
|
|
64
|
+
Error.captureStackTrace(errorContext, this.subscribe);
|
|
52
65
|
debug('subscribe to channel', channel);
|
|
53
66
|
const subscribeMessage = { type: 'subscribe', channel };
|
|
54
67
|
if (this.opts.apiKey)
|
|
@@ -58,17 +71,37 @@ export class WsClient extends EventEmitter {
|
|
|
58
71
|
ws.send(JSON.stringify(subscribeMessage));
|
|
59
72
|
const event = await this.waitFor(channel, (e) => {
|
|
60
73
|
return e.type === 'subscribe-confirm' || e.type === 'error';
|
|
61
|
-
}, timeout, true, true);
|
|
62
|
-
if (event.type === 'error')
|
|
63
|
-
|
|
74
|
+
}, timeout, true, true, errorContext);
|
|
75
|
+
if (event.type === 'error') {
|
|
76
|
+
const error = new Error(event.data);
|
|
77
|
+
error.stack += '\nSubscribe context:\n' + errorContext.stack;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
64
80
|
if (this._channels.includes(channel))
|
|
65
81
|
this._channels.push(channel);
|
|
66
82
|
}
|
|
67
|
-
async waitFor(channel, filter, timeout = 300000, skipSubscribe = false, fullMessage = false) {
|
|
83
|
+
async waitFor(channel, filter, timeout = 300000, skipSubscribe = false, fullMessage = false, _errorContext) {
|
|
68
84
|
if (!skipSubscribe)
|
|
69
85
|
await this.subscribe(channel);
|
|
86
|
+
// better stack traces
|
|
87
|
+
// see https://github.com/axios/axios/issues/2387#issuecomment-652242713
|
|
88
|
+
let errorContext;
|
|
89
|
+
if (_errorContext) {
|
|
90
|
+
errorContext = _errorContext;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
Error.stackTraceLimit = 0;
|
|
94
|
+
errorContext = new WsClientError();
|
|
95
|
+
Error.stackTraceLimit = stackTraceLimit;
|
|
96
|
+
Error.captureStackTrace(errorContext, this.waitFor);
|
|
97
|
+
}
|
|
70
98
|
return await new Promise((resolve, reject) => {
|
|
71
|
-
const _timeout = setTimeout(() => {
|
|
99
|
+
const _timeout = setTimeout(() => {
|
|
100
|
+
this.off('message', messageCb);
|
|
101
|
+
const error = new Error(`Timeout of ${timeout}ms exceeded`);
|
|
102
|
+
error.stack += '\nWaitFor context:\n' + errorContext.stack;
|
|
103
|
+
reject(error);
|
|
104
|
+
}, timeout);
|
|
72
105
|
const messageCb = (message) => {
|
|
73
106
|
if (message.channel === channel && (!filter || filter(fullMessage ? message : message.data))) {
|
|
74
107
|
clearTimeout(_timeout);
|
|
@@ -87,11 +120,20 @@ export class WsClient extends EventEmitter {
|
|
|
87
120
|
export class DataFairWsClient extends WsClient {
|
|
88
121
|
async waitForJournal(datasetId, eventType, timeout = 300000) {
|
|
89
122
|
await this.opts.log.info(`wait for event "${eventType}" on dataset "${datasetId}"`);
|
|
123
|
+
// better stack traces
|
|
124
|
+
// see https://github.com/axios/axios/issues/2387#issuecomment-652242713
|
|
125
|
+
Error.stackTraceLimit = 0;
|
|
126
|
+
const errorContext = new WsClientError();
|
|
127
|
+
Error.stackTraceLimit = stackTraceLimit;
|
|
128
|
+
Error.captureStackTrace(errorContext, this.waitForJournal);
|
|
90
129
|
const event = await this.waitFor(`datasets/${datasetId}/journal`, (e) => {
|
|
91
130
|
return e.type === eventType || e.type === 'error';
|
|
92
|
-
}, timeout);
|
|
93
|
-
if (event.type === 'error')
|
|
94
|
-
|
|
131
|
+
}, timeout, false, false, errorContext);
|
|
132
|
+
if (event.type === 'error') {
|
|
133
|
+
const error = new Error(event.data);
|
|
134
|
+
error.stack += '\nWaitForJournal context:\n' + errorContext.stack;
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
95
137
|
return event;
|
|
96
138
|
}
|
|
97
139
|
}
|