@data-fair/lib-node 2.12.1 → 2.13.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 CHANGED
@@ -52,3 +52,4 @@ export async function axiosAuth(opts) {
52
52
  return ax;
53
53
  }
54
54
  export default axiosAuth;
55
+ //# sourceMappingURL=axios-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"axios-auth.js","sourceRoot":"","sources":["axios-auth.ts"],"names":[],"mappings":"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","sourcesContent":["// build axios instances with sessions on a simple-directory instance\n// WARNING: use for integration testing only\n\nimport { Agent } from 'node:http'\nimport { CookieJar } from 'tough-cookie'\nimport { axiosInstance } from './axios.js'\nimport { axiosWithCookies, type AxiosWithCookiesInstance } from './axios-with-cookies.js'\n\nexport interface AxiosAuthOptions {\n email: string\n password: string\n org?: string\n dep?: string\n adminMode?: boolean\n directoryUrl?: string\n axiosOpts?: any\n orgStorage?: boolean\n globalCookies?: boolean\n}\n\nexport type AxiosAuthInstance = AxiosWithCookiesInstance & {\n setOrg: (org: string, dep?: string) => void\n}\n\nexport async function axiosAuth (opts: AxiosAuthOptions): Promise<AxiosAuthInstance> {\n const body: any = { email: opts.email, password: opts.password }\n if (opts.org) body.org = opts.org\n if (opts.dep) body.dep = opts.dep\n if (opts.adminMode) body.adminMode = opts.adminMode\n if (opts.orgStorage) body.orgStorage = opts.orgStorage\n const axiosOpts = {\n httpAgent: new Agent({ keepAlive: false }),\n maxRedirects: 0,\n ...opts.axiosOpts\n }\n const directoryUrl = opts.directoryUrl ?? 'http://localhost:8080'\n let callbackUrl = (await axiosInstance.post(directoryUrl + '/api/auth/password', body, { params: { redirect: directoryUrl }, maxRedirects: 0 })).data\n if (callbackUrl.startsWith(directoryUrl + '/simple-directory')) {\n callbackUrl = callbackUrl.replace(directoryUrl + '/simple-directory', directoryUrl)\n }\n\n const sdOrigin = new URL(directoryUrl).origin\n const cookieJar = new CookieJar()\n try {\n await axiosInstance.get(callbackUrl, { maxRedirects: 0 })\n } catch (err: any) {\n if (err.status !== 302) throw err\n const redirectUrl = new URL(err.headers.location)\n const redirectError = redirectUrl.searchParams.get('error')\n if (redirectError) throw new Error(redirectError)\n for (const cookie of err.headers['set-cookie'] ?? []) cookieJar.setCookieSync(cookie, sdOrigin)\n }\n\n const ax = axiosWithCookies({\n axiosOpts,\n cookiesOrigin: sdOrigin,\n globalCookies: opts.globalCookies,\n cookieJar\n }) as AxiosAuthInstance\n\n ax.setOrg = (org: string, dep?: string) => {\n ax.cookieJar.setCookieSync(`id_token_org=${org}`, sdOrigin)\n ax.cookieJar.setCookieSync(`id_token_dep=${dep ?? ''}`, sdOrigin)\n }\n\n return ax\n}\n\nexport default axiosAuth\n"]}
@@ -39,3 +39,4 @@ export function axiosWithCookies(opts = {}) {
39
39
  return ax;
40
40
  }
41
41
  export default axiosWithCookies;
42
+ //# sourceMappingURL=axios-with-cookies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"axios-with-cookies.js","sourceRoot":"","sources":["axios-with-cookies.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAGhD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAaxC,MAAM,YAAY,GAAG,CAAC,MAAkC,EAAE,EAAE;IAC1D,IAAI,CAAC,MAAM,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAA;IACnD,OAAO,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,GAAG,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;AAC9G,CAAC,CAAA;AAED,MAAM,UAAU,gBAAgB,CAAE,OAAgC,EAAE;IAClE,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,SAAS,EAAE,CAAA;IACnD,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,EAAE,CAAA;IAE9C,MAAM,EAAE,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE;QAC7C,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;YACrC,MAAM,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,CAAA;YAChC,IAAI,CAAC,IAAI,CAAC,aAAa,IAAI,GAAG,CAAC,MAAM,KAAK,aAAa,EAAE,CAAC;gBACxD,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;YAC5D,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,cAAc,CAAC,aAAa,CAAC,CAAA;YACjE,CAAC;YACD,OAAO,MAAM,CAAA;QACf,CAAC,CAAC,CAAA;QACF,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;YACnC,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,CAAA;YACnF,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE;gBAAE,SAAS,CAAC,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;YAC7F,OAAO,GAAG,CAAA;QACZ,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;YACjB,MAAM,GAAG,GAAG,KAAK,CAAC,QAAqC,CAAA;YACvD,IAAI,GAAG,EAAE,CAAC;gBACR,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,CAAA;gBACnF,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE;oBAAE,SAAS,CAAC,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;YAC/F,CAAC;YACD,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAC9B,CAAC,CAAC,CAAA;IACJ,CAAC,CAA6B,CAAA;IAC9B,EAAE,CAAC,SAAS,GAAG,SAAS,CAAA;IAExB,OAAO,EAAE,CAAA;AACX,CAAC;AAED,eAAe,gBAAgB,CAAA","sourcesContent":["// build axios instances with cookie jar support\n\nimport type { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'\nimport { axiosBuilder } from './axios.js'\nimport { CookieJar } from 'tough-cookie'\n\nexport interface AxiosWithCookiesOptions {\n axiosOpts?: any\n cookiesOrigin?: string\n globalCookies?: boolean\n cookieJar?: CookieJar\n}\n\nexport type AxiosWithCookiesInstance = AxiosInstance & {\n cookieJar: CookieJar\n}\n\nconst getConfigUrl = (config: InternalAxiosRequestConfig) => {\n if (!config.url) throw new Error('url is required')\n return new URL((config.baseURL && config.url?.startsWith('/')) ? (config.baseURL + config.url) : config.url)\n}\n\nexport function axiosWithCookies (opts: AxiosWithCookiesOptions = {}): AxiosWithCookiesInstance {\n const cookieJar = opts.cookieJar ?? new CookieJar()\n const cookiesOrigin = opts.cookiesOrigin ?? ''\n\n const ax = axiosBuilder(opts.axiosOpts, (ax) => {\n ax.interceptors.request.use((config) => {\n const url = getConfigUrl(config)\n if (!opts.globalCookies || url.origin === cookiesOrigin) {\n config.headers.Cookie = cookieJar.getCookiesSync(url.href)\n } else {\n config.headers.Cookie = cookieJar.getCookiesSync(cookiesOrigin)\n }\n return config\n })\n ax.interceptors.response.use((res) => {\n const origin = opts.globalCookies ? cookiesOrigin : getConfigUrl(res.config).origin\n for (const cookie of res.headers['set-cookie'] ?? []) cookieJar.setCookieSync(cookie, origin)\n return res\n }, async (error) => {\n const res = error.response as AxiosResponse | undefined\n if (res) {\n const origin = opts.globalCookies ? cookiesOrigin : getConfigUrl(res.config).origin\n for (const cookie of res.headers['set-cookie'] ?? []) cookieJar.setCookieSync(cookie, origin)\n }\n return Promise.reject(error)\n })\n }) as AxiosWithCookiesInstance\n ax.cookieJar = cookieJar\n\n return ax\n}\n\nexport default axiosWithCookies\n"]}
package/axios.js CHANGED
@@ -39,8 +39,11 @@ export function axiosBuilder(opts = {}, beforeInterceptors) {
39
39
  error.response.config = { method: error.response.config.method, url: error.response.config.url, data: error.response.config.data };
40
40
  if (error.response.config.data && error.response.config.data._writableState)
41
41
  delete error.response.config.data;
42
- if (error.response.data && error.response.data._readableState)
42
+ // destroy() a streamed error body before dropping it, else its socket leaks
43
+ if (error.response.data && error.response.data._readableState) {
44
+ error.response.data.destroy();
43
45
  delete error.response.data;
46
+ }
44
47
  let messageText = error.response.statusText;
45
48
  if (error.response.data) {
46
49
  messageText = typeof error.response.data === 'string' ? error.response.data : JSON.stringify(error.response.data);
@@ -53,3 +56,4 @@ export function axiosBuilder(opts = {}, beforeInterceptors) {
53
56
  }
54
57
  export const axiosInstance = axiosBuilder();
55
58
  export default axiosInstance;
59
+ //# sourceMappingURL=axios.js.map
package/axios.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"axios.js","sourceRoot":"","sources":["axios.ts"],"names":[],"mappings":"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","sourcesContent":["// prepare an axios instance with improved error management and some performance tuning for nodejs usage\n\nimport type { InternalAxiosRequestConfig, AxiosInstance, CreateAxiosDefaults } from 'axios'\nimport axios from 'axios'\nimport { httpAgent, httpsAgent } from './http-agents.js'\n\nconst { stackTraceLimit } = Error\n\nclass AxiosRequestError extends Error {\n constructor () {\n super()\n this.name = 'AxiosRequestError'\n }\n}\n\nconst requestInterceptor = (config: InternalAxiosRequestConfig) => {\n // better stack traces\n // see https://github.com/axios/axios/issues/2387#issuecomment-652242713\n Error.stackTraceLimit = 0\n const errorContext = new AxiosRequestError()\n Error.stackTraceLimit = stackTraceLimit\n Error.captureStackTrace(errorContext, requestInterceptor)\n // @ts-ignore\n config.errorContext = errorContext\n return config\n}\n\nexport function axiosBuilder (opts: CreateAxiosDefaults = {}, beforeInterceptors?: (ax: AxiosInstance) => void): AxiosInstance {\n const ax = axios.create({\n httpAgent,\n httpsAgent,\n ...opts\n })\n\n if (beforeInterceptors) beforeInterceptors(ax)\n\n ax.interceptors.request.use(requestInterceptor)\n\n // shorter stack traces\n ax.interceptors.response.use(response => response, error => {\n if (!error.response) {\n if (error.config?.errorContext?.stack) error.stack += '\\nRequest context:\\n' + error.config?.errorContext?.stack\n return Promise.reject(error)\n }\n\n delete error.response.request\n error.response.config = { method: error.response.config.method, url: error.response.config.url, data: error.response.config.data }\n if (error.response.config.data && error.response.config.data._writableState) delete error.response.config.data\n // destroy() a streamed error body before dropping it, else its socket leaks\n if (error.response.data && error.response.data._readableState) { error.response.data.destroy(); delete error.response.data }\n let messageText = error.response.statusText\n if (error.response.data) {\n messageText = typeof error.response.data === 'string' ? error.response.data : JSON.stringify(error.response.data)\n }\n error.response.message = `${error.response.status} - ${messageText}`\n Object.assign(error.config.errorContext, error.response)\n return Promise.reject(error.config.errorContext)\n })\n return ax\n}\n\nexport const axiosInstance = axiosBuilder()\n\nexport default axiosInstance\n"]}
package/events-queue.js CHANGED
@@ -114,3 +114,4 @@ export class EventsQueue {
114
114
  }
115
115
  const eventsQueue = new EventsQueue();
116
116
  export default eventsQueue;
117
+ //# sourceMappingURL=events-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events-queue.js","sourceRoot":"","sources":["events-queue.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Event } from '@data-fair/lib-common-types/event/index.js'\nimport { type Notification } from '@data-fair/lib-common-types/notification/index.js'\nimport { internalError } from '@data-fair/lib-node/observer.js'\nimport axios from './axios.js'\nimport Debug from 'debug'\nimport { SessionState } from '@data-fair/lib-common-types/session/index.js'\n\nconst debug = Debug('events-queue')\n\nexport type EventsQueueOptions = {\n eventsUrl: string,\n eventsSecret: string,\n inactive?: boolean\n}\n\nexport type PushEvent = Omit<Event, 'date'>\nexport type PushNotification = Omit<Notification, 'date'>\n\nexport class EventsQueue {\n _options: EventsQueueOptions | undefined\n eventsQueue: Event[] = []\n notificationsQueue: Notification[] = []\n stopped = false\n currentDrainPromise: Promise<void> | undefined\n\n options () {\n if (!this._options) throw new Error('events queue was not initialized')\n return this._options\n }\n\n start = async (options: EventsQueueOptions) => {\n if (this._options) return // already initialized\n this._options = options\n this.loop()\n }\n\n stop = async () => {\n this.stopped = true\n if (this.currentDrainPromise) await this.currentDrainPromise\n await this.drain()\n }\n\n async loop () {\n while (!this.stopped) {\n await new Promise(resolve => setTimeout(resolve, 3000))\n this.currentDrainPromise = this.drain()\n await this.currentDrainPromise\n this.currentDrainPromise = undefined\n }\n }\n\n async drain () {\n if (this.eventsQueue.length) {\n const events = this.eventsQueue\n this.eventsQueue = []\n debug('drain events', events.length)\n try {\n await axios.post(this.options().eventsUrl + '/api/events', events, { headers: { 'x-secret-key': this.options().eventsSecret } })\n } catch (err: any) {\n internalError('events-queue-push-event', err)\n // retry later\n if (err.status >= 500 && events.length < 1000) this.eventsQueue = events.concat(this.eventsQueue)\n }\n }\n if (this.notificationsQueue.length) {\n const notifications = this.notificationsQueue\n this.notificationsQueue = []\n debug('drain notifications', notifications.length)\n for (const notification of notifications) {\n try {\n await axios.post(this.options().eventsUrl + '/api/notifications', notification, { headers: { 'x-secret-key': this.options().eventsSecret } })\n } catch (err: any) {\n internalError('events-queue-push-notif', err)\n // retry later\n if (err.status >= 500 && (notifications.length + this.notificationsQueue.length) < 1000) {\n this.notificationsQueue.unshift()\n }\n }\n }\n }\n }\n\n pushEvent (event: PushEvent, sessionState?: SessionState) {\n const options = this.options()\n if (options.inactive) return\n if (this.stopped) throw new Error('events queue has been stopped');\n (event as Event).date = new Date().toISOString()\n debug('pushEvent', event)\n if (!event.originator && sessionState?.user && sessionState.account) {\n if (sessionState.user.asAdmin) {\n event.originator = {\n user: { ...sessionState.user.asAdmin, admin: true }\n }\n } else if (sessionState.user.adminMode && (sessionState.account.type !== event.sender?.type || sessionState.account.id !== event.sender?.id)) {\n event.originator = {\n user: { id: sessionState.user.id, name: sessionState.user.name, admin: true }\n }\n } else {\n event.originator = {\n user: { id: sessionState.user.id, name: sessionState.user.name }\n }\n if (sessionState.organization) {\n event.originator.organization = {\n id: sessionState.organization.id,\n name: sessionState.organization.name,\n department: sessionState.organization.department,\n departmentName: sessionState.organization.departmentName\n }\n }\n }\n }\n this.eventsQueue.push(event as Event)\n }\n\n pushNotification (notification: PushNotification) {\n const options = this.options()\n if (options.inactive) return\n if (this.stopped) throw new Error('notifications queue has been stopped');\n (notification as Notification).date = new Date().toISOString()\n debug('pushNotification', notification)\n this.notificationsQueue.push(notification as Notification)\n }\n}\n\nconst eventsQueue = new EventsQueue()\n\nexport default eventsQueue\n"]}
package/fs.js CHANGED
@@ -31,3 +31,4 @@ export async function exists(file) {
31
31
  }
32
32
  return true;
33
33
  }
34
+ //# sourceMappingURL=fs.js.map
package/fs.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["fs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACjE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAErC,MAAM,CAAC,KAAK,UAAU,SAAS,CAAE,GAAW;IAC1C,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QACjD,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,UAAU,CAAC,CAAA;QACzB,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,GAAG,CAAA;QACtC,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAE,GAAW;IACzC,MAAM,SAAS,CAAC,GAAG,CAAC,CAAA;IACpB,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAA;IAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,MAAM,CAAC,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,CAAA;IAChC,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,MAAM,CAAE,IAAY;IACxC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,CAAC,CAAA;IACpB,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAA;QACvC,MAAM,GAAG,CAAA;IACX,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["import { mkdir, readdir, unlink, access } from 'node:fs/promises'\nimport { sep, join } from 'node:path'\n\nexport async function ensureDir (dir: string) {\n const parts = dir.split(sep)\n for (let i = 0; i < parts.length; i++) {\n const partialDir = join(...parts.slice(0, i + 1))\n try {\n await mkdir(partialDir)\n } catch (err: any) {\n if (err.code !== 'EEXIST') throw err\n }\n }\n}\n\nexport async function emptyDir (dir: string) {\n await ensureDir(dir)\n const files = await readdir(dir)\n for (const file of files) {\n await unlink(`${dir}/${file}`)\n }\n}\n\nexport async function exists (file: string) {\n try {\n await access(file)\n } catch (err: any) {\n if (err.code === 'ENOENT') return false\n throw err\n }\n return true\n}\n"]}
package/http-agents.js CHANGED
@@ -53,3 +53,4 @@ setInterval(() => {
53
53
  requestCounter.inc({ protocol }, agentStatus.requestCount);
54
54
  }
55
55
  }, 60000);
56
+ //# sourceMappingURL=http-agents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-agents.js","sourceRoot":"","sources":["http-agents.ts"],"names":[],"mappings":"AAAA,wFAAwF;AAExF,OAAO,eAAe,MAAM,kBAAkB,CAAA;AAC9C,OAAO,SAAS,EAAE,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AACtD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAE5C,oFAAoF;AACpF,4EAA4E;AAC5E,MAAM,aAAa,GAAG;IACpB,UAAU,EAAE,CAAC;IACb,cAAc,EAAE,CAAC;IACjB,OAAO,EAAE,KAAK;IACd,iBAAiB,EAAE,KAAK;CACzB,CAAA;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,SAAS,CAAC,aAAa,CAAC,CAAA;AACrD,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC,aAAa,CAAC,CAAA;AAEvD,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAA;AAC7C,eAAe,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;AAClC,eAAe,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;AAEnC,yBAAyB;AACzB,MAAM,MAAM,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,CAAA;AAErD,MAAM,YAAY,GAAG,IAAI,KAAK,CAAC;IAC7B,IAAI,EAAE,uBAAuB;IAC7B,IAAI,EAAE,0CAA0C;IAChD,UAAU,EAAE,CAAC,UAAU,EAAE,MAAM,CAAC;CACjC,CAAC,CAAA;AACF,MAAM,gBAAgB,GAAG,IAAI,KAAK,CAAC;IACjC,IAAI,EAAE,4BAA4B;IAClC,IAAI,EAAE,0CAA0C;IAChD,UAAU,EAAE,CAAC,UAAU,EAAE,MAAM,CAAC;CACjC,CAAC,CAAA;AACF,MAAM,mBAAmB,GAAG,IAAI,OAAO,CAAC;IACtC,IAAI,EAAE,mCAAmC;IACzC,IAAI,EAAE,mDAAmD;IACzD,UAAU,EAAE,CAAC,UAAU,CAAC;CACzB,CAAC,CAAA;AACF,MAAM,cAAc,GAAG,IAAI,OAAO,CAAC;IACjC,IAAI,EAAE,6BAA6B;IACnC,IAAI,EAAE,oDAAoD;IAC1D,UAAU,EAAE,CAAC,UAAU,CAAC;CACzB,CAAC,CAAA;AAEF,WAAW,CAAC,GAAG,EAAE;IACf,KAAK,MAAM,SAAS,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;QAC1C,MAAM,QAAQ,GAAG,SAA6B,CAAA;QAC9C,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,gBAAgB,EAAE,CAAA;QACvD,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,OAAO,EAAE,CAAC;YACvC,YAAY,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;QACjE,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,WAAW,EAAE,CAAC;YAC3C,gBAAgB,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,WAAW,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAA;QACzE,CAAC;QACD,mBAAmB,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAA;QACxC,mBAAmB,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW,CAAC,iBAAiB,CAAC,CAAA;QACpE,cAAc,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAA;QACnC,cAAc,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW,CAAC,YAAY,CAAC,CAAA;IAC5D,CAAC;AACH,CAAC,EAAE,KAAK,CAAC,CAAA","sourcesContent":["// use better DNS lookup than nodejs default and try to reduce number of socket openings\n\nimport CacheableLookup from 'cacheable-lookup'\nimport HttpAgent, { HttpsAgent } from 'agentkeepalive'\nimport { Counter, Gauge } from 'prom-client'\n\n// not very high number of sockets but we don't want to saturate our reverse proxies\n// if a higher number is needed the service should probably be scaled anyway\nconst keepaliveOpts = {\n maxSockets: 8,\n maxFreeSockets: 8,\n timeout: 60000,\n freeSocketTimeout: 30000\n}\n\nexport const httpAgent = new HttpAgent(keepaliveOpts)\nexport const httpsAgent = new HttpsAgent(keepaliveOpts)\n\nconst cacheableLookup = new CacheableLookup()\ncacheableLookup.install(httpAgent)\ncacheableLookup.install(httpsAgent)\n\n// monitor agent statuses\nconst agents = { http: httpAgent, https: httpsAgent }\n\nconst socketsGauge = new Gauge({\n name: 'df_http_agent_sockets',\n help: 'Number of open sockets in the http agent',\n labelNames: ['protocol', 'host']\n})\nconst freeSocketsGauge = new Gauge({\n name: 'df_http_agent_free_sockets',\n help: 'Number of free sockets in the http agent',\n labelNames: ['protocol', 'host']\n})\nconst createSocketCounter = new Counter({\n name: 'df_http_agent_create_socket_total',\n help: 'Total number of sockets created by the http agent',\n labelNames: ['protocol']\n})\nconst requestCounter = new Counter({\n name: 'df_http_agent_request_total',\n help: 'Total number of requests created by the http agent',\n labelNames: ['protocol']\n})\n\nsetInterval(() => {\n for (const _protocol of ['http', 'https']) {\n const protocol = _protocol as 'http' | 'https'\n const agentStatus = agents[protocol].getCurrentStatus()\n for (const host in agentStatus.sockets) {\n socketsGauge.set({ protocol, host }, agentStatus.sockets[host])\n }\n for (const host in agentStatus.freeSockets) {\n freeSocketsGauge.set({ protocol, host }, agentStatus.freeSockets[host])\n }\n createSocketCounter.remove({ protocol })\n createSocketCounter.inc({ protocol }, agentStatus.createSocketCount)\n requestCounter.remove({ protocol })\n requestCounter.inc({ protocol }, agentStatus.requestCount)\n }\n}, 60000)\n"]}
package/locks.js CHANGED
@@ -75,3 +75,4 @@ export class Locks {
75
75
  }
76
76
  const locks = new Locks();
77
77
  export default locks;
78
+ //# sourceMappingURL=locks.js.map
package/locks.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locks.js","sourceRoot":"","sources":["locks.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Db } from 'mongodb'\nimport { hostname } from 'node:os'\nimport { randomBytes } from 'node:crypto'\nimport Debug from 'debug'\n\nconst debug = Debug('locks')\n\nexport class Locks {\n pid: string\n private interval: ReturnType<typeof setInterval> | undefined\n private _db: Db | undefined\n get db () {\n if (!this._db) throw new Error('locks utils was not initialized')\n return this._db\n }\n\n get collection () {\n return this.db.collection<{ _id: string, pid: string, origin?: string, hostname: string, createdAt: Date }>('locks')\n }\n\n constructor () {\n this.pid = randomBytes(8).toString('hex')\n debug('locks with pid', this.pid)\n }\n\n start = async (db: Db, ttl = 60) => {\n if (this._db) throw new Error('locks utils was already initialized')\n this._db = db\n await this.collection.createIndex({ pid: 1 })\n try {\n await this.collection.createIndex({ updatedAt: 1 }, { expireAfterSeconds: ttl })\n } catch (err) {\n console.log('Failure to create TTL index. Probably because the value changed. Try to update it.')\n db.command({ collMod: 'locks', index: { keyPattern: { updatedAt: 1 }, expireAfterSeconds: ttl } })\n }\n\n // prolongate lock acquired by this process while it is still active\n this.interval = setInterval(() => {\n this.collection.updateMany({ pid: this.pid }, { $currentDate: { updatedAt: true } })\n }, (ttl / 2) * 1000)\n }\n\n stop = async () => {\n clearInterval(this.interval)\n if (this._db) await this.collection.deleteMany({ pid: this.pid })\n }\n\n acquire = async (_id: string, origin?: string): Promise<boolean> => {\n debug('acquire', _id, origin)\n try {\n await this.collection.insertOne({ _id, pid: this.pid, origin, hostname: hostname(), createdAt: new Date() })\n try {\n await this.collection.updateOne({ _id }, { $currentDate: { updatedAt: true } })\n } catch (err) {\n await this.collection.deleteOne({ _id, pid: this.pid })\n throw err\n }\n debug('acquire ok', _id)\n return true\n } catch (err: any) {\n if (err.code !== 11000) throw err\n // duplicate means the lock was already acquired\n debug('acquire ko', _id)\n return false\n }\n }\n\n release = async (_id: string, delay = 0) => {\n debug('release', _id)\n if (delay) {\n const date = new Date((new Date()).getTime() + delay)\n await this.collection.updateOne({ _id, pid: this.pid }, { $unset: { pid: 1 }, $set: { delayed: true, updatedAt: date } })\n } else {\n await this.collection.deleteOne({ _id, pid: this.pid })\n }\n }\n}\n\nconst locks = new Locks()\n\nexport default locks\n"]}
package/mongo.js CHANGED
@@ -78,3 +78,4 @@ export class Mongo {
78
78
  }
79
79
  const mongo = new Mongo();
80
80
  export default mongo;
81
+ //# sourceMappingURL=mongo.js.map
package/mongo.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mongo.js","sourceRoot":"","sources":["mongo.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IndexSpecification, CreateIndexesOptions, MongoClientOptions, IndexDirection, Db } from 'mongodb'\nimport { MongoClient, MongoError } from 'mongodb'\n\ntype IndexDirections = Record<string, IndexDirection>\n\nexport type IndexDefinitions = Record<string, Record<string, null | IndexDirections | [IndexDirections, CreateIndexesOptions]>>\n\nexport class Mongo {\n private _client: MongoClient | undefined\n private _db: Db | undefined\n\n get client () {\n if (!this._client) throw new Error('db was not connected')\n return this._client\n }\n\n get db () {\n if (!this._db) throw new Error('db was not connected')\n return this._db\n }\n\n connect = async (mongoUrl: string, options: MongoClientOptions = {}) => {\n if (this._client) {\n console.warn('db already connected')\n return\n }\n console.log('connecting to mongodb...')\n this._client = await MongoClient.connect(mongoUrl, { maxPoolSize: 5, ignoreUndefined: true, ...options })\n this._db = this._client.db()\n console.log('...ok')\n }\n\n // Create an index if it does not exist and manage overwriting existing and conflicting index definitions\n ensureIndex = async (collection: string, key: IndexSpecification, options: CreateIndexesOptions = {}) => {\n try {\n await this.db.collection(collection).createIndex(key, options)\n } catch (err) {\n if (options.name && err instanceof MongoError && (err.code === 85 || err.code === 86)) {\n // if the error is a conflict on keys or params of the index we automatically\n // delete then recreate the index\n console.log(`drop then recreate index ${collection}/${options.name}`)\n await this.db.collection(collection).dropIndex(options.name)\n await this.db.collection(collection).createIndex(key, options)\n } else {\n throw err\n }\n }\n }\n\n /**\n * create a bunch of indexes and overtwrite previous definitions\n * the structure forces giving names to indexes which is better for managing them properly\n */\n configure = async (indexDefinitions: IndexDefinitions) => {\n for (const collectionName in indexDefinitions) {\n try {\n await this.db.createCollection(collectionName)\n } catch (err) {\n // nothing TODO\n }\n for (const indexName in indexDefinitions[collectionName]) {\n let key: IndexDirections, options: CreateIndexesOptions\n const indexDefinition = indexDefinitions[collectionName][indexName]\n if (indexDefinition === null) {\n if (await this.db.collection(collectionName).indexExists(indexName)) {\n await this.db.collection(collectionName).dropIndex(indexName)\n }\n continue\n } else if (Array.isArray(indexDefinition)) {\n [key, options] = indexDefinition\n if (options.name && options.name !== indexName) throw new Error(`inconsistent index name ${indexName} or ${options.name}`)\n options.name = indexName\n } else {\n key = indexDefinition\n options = { name: indexName }\n }\n await this.ensureIndex(collectionName, key, options)\n }\n }\n }\n}\n\nconst mongo = new Mongo()\n\nexport default mongo\n"]}
package/observer.js CHANGED
@@ -1,6 +1,8 @@
1
1
  // very light web server to expose prometheus metrics, export a cpu profile and maybe other incoming obervability features
2
2
  import { hostname } from 'node:os';
3
3
  import { createServer } from 'node:http';
4
+ import { getHeapSnapshot } from 'node:v8';
5
+ import { pipeline } from 'node:stream/promises';
4
6
  import { Registry, Counter, register, collectDefaultMetrics } from 'prom-client';
5
7
  import eventPromise from '@data-fair/lib-utils/event-promise.js';
6
8
  // a special registry for metrics that are global
@@ -51,6 +53,17 @@ const serveCPUProfile = async (res, duration) => {
51
53
  res.write(JSON.stringify(profile));
52
54
  res.end();
53
55
  };
56
+ // full retained heap snapshot (the V8 inspector "take heap snapshot"). Blocks the event loop while V8
57
+ // walks the heap: ~5-20 ms per MB of live heap, so ~10-30 s for a 1-1.5 GB process; all in-flight
58
+ // requests stall for the duration. Use sparingly, ideally after detaching the pod from its Service.
59
+ const serveRetainedHeapSnapshot = async (res) => {
60
+ res.setHeader('Content-Disposition', `attachment; filename="data-fair-${hostname()}-${new Date().toISOString()}.heapsnapshot"`);
61
+ res.setHeader('Content-Type', 'application/octet-stream');
62
+ const start = Date.now();
63
+ console.log('observer: starting full heap snapshot — event loop will be blocked');
64
+ await pipeline(getHeapSnapshot(), res);
65
+ console.log(`observer: full heap snapshot done in ${Date.now() - start}ms`);
66
+ };
54
67
  // live heap allocation sampler
55
68
  const serveHeapSnapshot = async (res, includeCollected, duration) => {
56
69
  const { Session } = await import('node:inspector/promises');
@@ -88,6 +101,9 @@ export const startObserver = async (port = 9090) => {
88
101
  const duration = parseInt(url.searchParams.get('duration') || '2000');
89
102
  serveHeapSnapshot(res, true, duration).catch(err => httpErrorHandler(res, err));
90
103
  }
104
+ else if (req.method === 'GET' && url.pathname === '/heap-snapshot') {
105
+ serveRetainedHeapSnapshot(res).catch(err => httpErrorHandler(res, err));
106
+ }
91
107
  else {
92
108
  res.writeHead(404);
93
109
  res.end();
@@ -101,9 +117,11 @@ export const startObserver = async (port = 9090) => {
101
117
  GET /cpu-profile -> generate and fetch a CPU profile
102
118
  GET /heap-profile -> generate and fetch a sampled heap allocation snapshot
103
119
  GET /heap-profile-all -> generate and fetch a sampled heap allocation snapshot including objects collected by GC
120
+ GET /heap-snapshot -> dump a full retained heap snapshot (blocks the event loop while V8 walks the heap)
104
121
  `);
105
122
  };
106
123
  export const stopObserver = async () => {
107
124
  if (server)
108
125
  server.close();
109
126
  };
127
+ //# sourceMappingURL=observer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observer.js","sourceRoot":"","sources":["observer.ts"],"names":[],"mappings":"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","sourcesContent":["// very light web server to expose prometheus metrics, export a cpu profile and maybe other incoming obervability features\n\n// follow this doc for prometheus metrics naming conventions https://prometheus.io/docs/practices/naming/\n// /metrics serves container/process/pod specific metrics while /global-metrics\n// serves metrics for the whole service installation no matter the scaling\n\nimport type { Server, ServerResponse } from 'node:http'\nimport { hostname } from 'node:os'\nimport { createServer } from 'node:http'\nimport { getHeapSnapshot } from 'node:v8'\nimport { pipeline } from 'node:stream/promises'\nimport { Registry, Counter, register, collectDefaultMetrics } from 'prom-client'\nimport eventPromise from '@data-fair/lib-utils/event-promise.js'\n\n// a special registry for metrics that are global\n// not based on this specific process state but instead on the content of db for example\nexport const servicePromRegistry = new Registry()\n\ncollectDefaultMetrics()\n\n// local metrics incremented throughout the code\nconst internalErrorCounter = new Counter({\n name: 'df_internal_error',\n 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.',\n labelNames: ['errorCode']\n})\n\nexport const internalError = (errorCode: string, error: any, ...optionalParams: any) => {\n internalErrorCounter.inc({ errorCode })\n const message = error.message || error\n console.error(`[${errorCode}] ${message}`, ...optionalParams)\n if (error.stack) console.error(error.stack)\n}\n\n/**\n * @param {import('node:http').ServerResponse} res\n * @param {any} err\n */\nconst httpErrorHandler = (res: ServerResponse, err: any) => {\n console.error('failed to server prometheus /metrics', err)\n res.writeHead(500)\n res.end()\n}\n\nconst serveRegistry = async (res: ServerResponse, registry: Registry) => {\n const metrics = await registry.metrics()\n res.setHeader('Content-Type', registry.contentType)\n res.writeHead(200)\n res.write(metrics)\n res.end()\n}\n\n// live CPU performance inspection\nconst serveCPUProfile = async (res: ServerResponse, duration: number) => {\n const { Session } = await import('node:inspector/promises')\n const session = new Session()\n session.connect()\n await session.post('Profiler.enable')\n await session.post('Profiler.start')\n await new Promise(resolve => setTimeout(resolve, duration))\n const { profile } = await session.post('Profiler.stop')\n await session.post('Profiler.disable')\n session.disconnect()\n\n res.setHeader('Content-Disposition', `attachment; filename=\"data-fair-${hostname()}-${new Date().toISOString()}.cpuprofile\"`)\n res.write(JSON.stringify(profile))\n res.end()\n}\n\n// full retained heap snapshot (the V8 inspector \"take heap snapshot\"). Blocks the event loop while V8\n// walks the heap: ~5-20 ms per MB of live heap, so ~10-30 s for a 1-1.5 GB process; all in-flight\n// requests stall for the duration. Use sparingly, ideally after detaching the pod from its Service.\nconst serveRetainedHeapSnapshot = async (res: ServerResponse) => {\n res.setHeader('Content-Disposition', `attachment; filename=\"data-fair-${hostname()}-${new Date().toISOString()}.heapsnapshot\"`)\n res.setHeader('Content-Type', 'application/octet-stream')\n const start = Date.now()\n console.log('observer: starting full heap snapshot — event loop will be blocked')\n await pipeline(getHeapSnapshot(), res)\n console.log(`observer: full heap snapshot done in ${Date.now() - start}ms`)\n}\n\n// live heap allocation sampler\nconst serveHeapSnapshot = async (res: ServerResponse, includeCollected: boolean, duration: number) => {\n const { Session } = await import('node:inspector/promises')\n const session = new Session()\n session.connect()\n await session.post('HeapProfiler.enable')\n await session.post('HeapProfiler.startSampling', { includeObjectsCollectedByMajorGC: includeCollected, includeObjectsCollectedByMinorGC: true })\n await new Promise(resolve => setTimeout(resolve, duration))\n const { profile } = await session.post('HeapProfiler.stopSampling')\n await session.post('HeapProfiler.disable')\n session.disconnect()\n\n res.setHeader('Content-Disposition', `attachment; filename=\"data-fair-${hostname()}-${new Date().toISOString()}.heapprofile\"`)\n res.write(JSON.stringify(profile))\n res.end()\n}\n\nlet server: Server\n\nexport const startObserver = async (port = 9090) => {\n server = createServer((req, res) => {\n const url = new URL(req.url || '', `http://${req.headers.host}`)\n if (req.method === 'GET' && url.pathname === '/metrics') {\n serveRegistry(res, register).catch(err => httpErrorHandler(res, err))\n } else if (req.method === 'GET' && (url.pathname === '/global-metrics' || url.pathname === '/service-metrics')) {\n serveRegistry(res, servicePromRegistry).catch(err => httpErrorHandler(res, err))\n } else if (req.method === 'GET' && url.pathname === '/cpu-profile') {\n const duration = parseInt(url.searchParams.get('duration') || '2000')\n serveCPUProfile(res, duration).catch(err => httpErrorHandler(res, err))\n } else if (req.method === 'GET' && url.pathname === '/heap-profile') {\n const duration = parseInt(url.searchParams.get('duration') || '2000')\n serveHeapSnapshot(res, false, duration).catch(err => httpErrorHandler(res, err))\n } else if (req.method === 'GET' && url.pathname === '/heap-profile-all') {\n const duration = parseInt(url.searchParams.get('duration') || '2000')\n serveHeapSnapshot(res, true, duration).catch(err => httpErrorHandler(res, err))\n } else if (req.method === 'GET' && url.pathname === '/heap-snapshot') {\n serveRetainedHeapSnapshot(res).catch(err => httpErrorHandler(res, err))\n } else {\n res.writeHead(404)\n res.end()\n }\n })\n server.listen(port)\n await eventPromise(server, 'listening')\n console.log(`Observer server available on http://localhost:${port}\n GET /metrics -> get prometheus metrics for this instance\n GET /service-metrics -> get prometheus metrics shared accross all instances\n GET /cpu-profile -> generate and fetch a CPU profile\n GET /heap-profile -> generate and fetch a sampled heap allocation snapshot\n GET /heap-profile-all -> generate and fetch a sampled heap allocation snapshot including objects collected by GC\n GET /heap-snapshot -> dump a full retained heap snapshot (blocks the event loop while V8 walks the heap)\n`)\n}\n\nexport const stopObserver = async () => {\n if (server) server.close()\n}\n"]}
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@data-fair/lib-node",
3
- "version": "2.12.1",
3
+ "version": "2.13.1",
4
4
  "description": "Small nodejs related utility functions for the data-fair stack.",
5
5
  "main": "index.js",
6
6
  "files": [
7
7
  "**/*.js",
8
+ "**/*.js.map",
8
9
  "**/*.d.ts"
9
10
  ],
10
11
  "author": "",
@@ -15,18 +16,21 @@
15
16
  },
16
17
  "dependencies": {
17
18
  "@data-fair/lib-common-types": "^1.8.4",
18
- "@data-fair/lib-validation": "^1.0.0",
19
19
  "@data-fair/lib-utils": "^1.1.0",
20
+ "@data-fair/lib-validation": "^1.0.0",
20
21
  "agentkeepalive": "^4.5.0",
21
22
  "axios": "^1.7.7",
22
23
  "cacheable-lookup": "^7.0.0",
24
+ "cookie": "^0.7.2",
25
+ "jsonwebtoken": "^9.0.2",
26
+ "jwks-rsa": "^3.1.0",
23
27
  "semver": "^7.6.3"
24
28
  },
25
29
  "peerDependencies": {
26
30
  "mongodb": "6",
27
31
  "prom-client": "15",
28
- "ws": "8",
29
- "tough-cookie": "5"
32
+ "tough-cookie": "5",
33
+ "ws": "8"
30
34
  },
31
35
  "peerDependenciesMeta": {
32
36
  "mongodb": {
@@ -46,7 +50,7 @@
46
50
  "devDependencies": {
47
51
  "mongodb": "^6.9.0",
48
52
  "prom-client": "^15.1.3",
49
- "ws": "^8.18.0",
50
- "tough-cookie": "^5.0.0"
53
+ "tough-cookie": "^5.0.0",
54
+ "ws": "^8.18.0"
51
55
  }
52
56
  }
package/session.d.ts CHANGED
@@ -5,6 +5,9 @@ export declare class SessionHandler {
5
5
  defaultLang: string;
6
6
  get jwksClient(): JwksClient.JwksClient;
7
7
  initJWKS(directoryUrl?: string): void;
8
+ private _signingKeyObjects;
9
+ private getSigningKeyObject;
8
10
  verifyToken(token: string): Promise<any>;
9
- readStateFromCookie(cookieStr: string | undefined, onlyDecode?: boolean, clearBrokenSession?: () => void): Promise<SessionState>;
11
+ private buildTokenSessionState;
12
+ readStateFromCookie(rawCookies: string | Record<string, string | undefined> | undefined, onlyDecode?: boolean, clearBrokenSession?: () => void): Promise<SessionState>;
10
13
  }
package/session.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createPublicKey } from 'node:crypto';
1
2
  import jwt from 'jsonwebtoken';
2
3
  import JwksClient from 'jwks-rsa';
3
4
  import cookie from 'cookie';
@@ -13,51 +14,34 @@ export class SessionHandler {
13
14
  initJWKS(directoryUrl = 'http://simple-directory:8080') {
14
15
  this._jwksClient = JwksClient({ jwksUri: directoryUrl + '/.well-known/jwks.json' });
15
16
  }
17
+ // jwks-rsa caches the JWK per kid but it exports a new PEM string at every access
18
+ // and jsonwebtoken parses PEM strings again at every verification
19
+ // caching the parsed KeyObject per kid skips both costs
20
+ // the cache holds the pending promise so concurrent calls dedupe, and drops it on failure
21
+ _signingKeyObjects = new Map();
22
+ getSigningKeyObject(kid) {
23
+ const cacheKey = kid ?? '';
24
+ let promise = this._signingKeyObjects.get(cacheKey);
25
+ if (!promise) {
26
+ promise = (async () => {
27
+ const signingKey = await this.jwksClient.getSigningKey(kid);
28
+ return createPublicKey(signingKey.getPublicKey());
29
+ })();
30
+ promise.catch(() => this._signingKeyObjects.delete(cacheKey));
31
+ this._signingKeyObjects.set(cacheKey, promise);
32
+ }
33
+ return promise;
34
+ }
16
35
  // used for the main session token, but can also be used to use the jwks to verify other types of tokens
17
36
  async verifyToken(token) {
18
37
  const decoded = jwt.decode(token, { complete: true });
19
38
  if (!decoded)
20
39
  throw new Error('failed to decode token');
21
- const signingKey = await this.jwksClient.getSigningKey(decoded.header.kid);
22
- return jwt.verify(token, signingKey.getPublicKey());
40
+ const signingKeyObject = await this.getSigningKeyObject(decoded.header.kid);
41
+ return jwt.verify(token, signingKeyObject);
23
42
  }
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)
33
- return session;
34
- if (!cookies.id_token_sign && !onlyDecode)
35
- return session;
36
- const token = cookies.id_token + '.' + cookies.id_token_sign;
37
- let user;
38
- if (onlyDecode) {
39
- user = jwt.decode(token);
40
- }
41
- else {
42
- try {
43
- user = await this.verifyToken(token);
44
- }
45
- catch (err) {
46
- console.warn(err);
47
- if (err.name === 'JwksError') {
48
- // happens in case of temporary unavailability if SD
49
- // better not to disconnect user in this case
50
- throw httpError(500, 'Session token public keys not initialized');
51
- }
52
- else {
53
- if (clearBrokenSession)
54
- clearBrokenSession();
55
- throw httpError(401);
56
- }
57
- }
58
- }
59
- if (!user)
60
- return session;
43
+ buildTokenSessionState(user, organizationId, departmentId, switchedRole) {
44
+ const state = {};
61
45
  // this is to prevent null values that are put by SD versions that do not strictly respect their schema
62
46
  for (const org of user.organizations) {
63
47
  if (!org.department) {
@@ -65,12 +49,9 @@ export class SessionHandler {
65
49
  delete org.departmentName;
66
50
  }
67
51
  }
68
- session.user = user;
69
- const organizationId = cookies.id_token_org;
70
- const departmentId = cookies.id_token_dep;
71
- const switchedRole = cookies.id_token_role;
52
+ state.user = user;
72
53
  if (organizationId) {
73
- session.organization = user.organizations.find(o => {
54
+ state.organization = user.organizations.find(o => {
74
55
  if (o.id !== organizationId)
75
56
  return false;
76
57
  if (departmentId && departmentId !== o.department)
@@ -80,33 +61,77 @@ export class SessionHandler {
80
61
  return true;
81
62
  });
82
63
  }
83
- if (session.organization) {
84
- session.account = {
64
+ if (state.organization) {
65
+ state.account = {
85
66
  type: 'organization',
86
- id: session.organization.id,
87
- name: session.organization.name,
88
- department: session.organization.department,
89
- departmentName: session.organization.departmentName
67
+ id: state.organization.id,
68
+ name: state.organization.name,
69
+ department: state.organization.department,
70
+ departmentName: state.organization.departmentName
90
71
  };
91
- session.accountRole = session.organization.role;
72
+ state.accountRole = state.organization.role;
92
73
  }
93
74
  else {
94
- session.account = {
75
+ state.account = {
95
76
  type: 'user',
96
77
  id: user.id,
97
78
  name: user.name
98
79
  };
99
- session.accountRole = 'admin';
100
- return session;
80
+ state.accountRole = 'admin';
81
+ return state;
101
82
  }
102
- if (session.user?.siteOwner) {
103
- if (session.user.siteOwner.type === 'user' && session.user.siteOwner.id === session.user.id) {
104
- session.siteRole = 'admin';
83
+ if (state.user?.siteOwner) {
84
+ if (state.user.siteOwner.type === 'user' && state.user.siteOwner.id === state.user.id) {
85
+ state.siteRole = 'admin';
105
86
  }
106
- if (session.user.siteOwner.type === 'organization' && session.user.siteOwner.id === session.organization?.id) {
107
- session.siteRole = session.organization.role;
87
+ if (state.user.siteOwner.type === 'organization' && state.user.siteOwner.id === state.organization?.id) {
88
+ state.siteRole = state.organization.role;
108
89
  }
109
90
  }
110
- return session;
91
+ return state;
92
+ }
93
+ // accepts either the raw Cookie header or an already-parsed cookies object
94
+ // (e.g. req.cookies when a cookie-parser middleware already ran upstream) to
95
+ // avoid re-parsing and allocating a second cookies object per request
96
+ async readStateFromCookie(rawCookies, onlyDecode, clearBrokenSession) {
97
+ if (!rawCookies)
98
+ return { lang: this.defaultLang };
99
+ const cookies = typeof rawCookies === 'string' ? cookie.parse(rawCookies) : rawCookies;
100
+ const lang = cookies.i18n_lang || this.defaultLang;
101
+ const dark = cookies.theme_dark === '1' || cookies.theme_dark === 'true';
102
+ if (!cookies.id_token)
103
+ return { lang, dark };
104
+ if (!cookies.id_token_sign && !onlyDecode)
105
+ return { lang, dark };
106
+ const token = cookies.id_token + '.' + (cookies.id_token_sign ?? '');
107
+ const organizationId = cookies.id_token_org ?? '';
108
+ const departmentId = cookies.id_token_dep ?? '';
109
+ const switchedRole = cookies.id_token_role ?? '';
110
+ let user;
111
+ if (onlyDecode) {
112
+ user = jwt.decode(token);
113
+ }
114
+ else {
115
+ try {
116
+ user = await this.verifyToken(token);
117
+ }
118
+ catch (err) {
119
+ console.warn(err);
120
+ if (err.name === 'JwksError') {
121
+ // happens in case of temporary unavailability if SD
122
+ // better not to disconnect user in this case
123
+ throw httpError(500, 'Session token public keys not initialized');
124
+ }
125
+ else {
126
+ if (clearBrokenSession)
127
+ clearBrokenSession();
128
+ throw httpError(401);
129
+ }
130
+ }
131
+ }
132
+ if (!user)
133
+ return { lang, dark };
134
+ return Object.assign({ lang, dark }, this.buildTokenSessionState(user, organizationId, departmentId, switchedRole));
111
135
  }
112
136
  }
137
+ //# sourceMappingURL=session.js.map
package/session.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["session.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SessionState, User } from '@data-fair/lib-common-types/session/index.js'\nimport { createPublicKey, type KeyObject } from 'node:crypto'\nimport jwt from 'jsonwebtoken'\nimport JwksClient from 'jwks-rsa'\nimport cookie from 'cookie'\nimport { httpError } from '@data-fair/lib-utils/http-errors.js'\n\ntype TokenSessionState = Omit<SessionState, 'lang' | 'dark'>\n\nexport class SessionHandler {\n private _jwksClient: JwksClient.JwksClient | undefined\n public defaultLang: string = 'fr'\n\n get jwksClient () {\n if (!this._jwksClient) throw new Error('session management was not initialized')\n return this._jwksClient\n }\n\n initJWKS (directoryUrl = 'http://simple-directory:8080') {\n this._jwksClient = JwksClient({ jwksUri: directoryUrl + '/.well-known/jwks.json' })\n }\n\n // jwks-rsa caches the JWK per kid but it exports a new PEM string at every access\n // and jsonwebtoken parses PEM strings again at every verification\n // caching the parsed KeyObject per kid skips both costs\n // the cache holds the pending promise so concurrent calls dedupe, and drops it on failure\n private _signingKeyObjects = new Map<string, Promise<KeyObject>>()\n private getSigningKeyObject (kid?: string): Promise<KeyObject> {\n const cacheKey = kid ?? ''\n let promise = this._signingKeyObjects.get(cacheKey)\n if (!promise) {\n promise = (async () => {\n const signingKey = await this.jwksClient.getSigningKey(kid)\n return createPublicKey(signingKey.getPublicKey())\n })()\n promise.catch(() => this._signingKeyObjects.delete(cacheKey))\n this._signingKeyObjects.set(cacheKey, promise)\n }\n return promise\n }\n\n // used for the main session token, but can also be used to use the jwks to verify other types of tokens\n async verifyToken (token: string): Promise<any> {\n const decoded = jwt.decode(token, { complete: true })\n if (!decoded) throw new Error('failed to decode token')\n const signingKeyObject = await this.getSigningKeyObject(decoded.header.kid)\n return jwt.verify(token, signingKeyObject)\n }\n\n private buildTokenSessionState (user: User, organizationId: string, departmentId: string, switchedRole: string): TokenSessionState {\n const state: TokenSessionState = {}\n\n // this is to prevent null values that are put by SD versions that do not strictly respect their schema\n for (const org of user.organizations) {\n if (!org.department) {\n delete org.department\n delete org.departmentName\n }\n }\n\n state.user = user\n\n if (organizationId) {\n state.organization = user.organizations.find(o => {\n if (o.id !== organizationId) return false\n if (departmentId && departmentId !== o.department) return false\n if (switchedRole && switchedRole !== o.role) return false\n return true\n })\n }\n if (state.organization) {\n state.account = {\n type: 'organization',\n id: state.organization.id,\n name: state.organization.name,\n department: state.organization.department,\n departmentName: state.organization.departmentName\n }\n state.accountRole = state.organization.role\n } else {\n state.account = {\n type: 'user',\n id: user.id,\n name: user.name\n }\n state.accountRole = 'admin'\n return state\n }\n\n if (state.user?.siteOwner) {\n if (state.user.siteOwner.type === 'user' && state.user.siteOwner.id === state.user.id) {\n state.siteRole = 'admin'\n }\n if (state.user.siteOwner.type === 'organization' && state.user.siteOwner.id === state.organization?.id) {\n state.siteRole = state.organization.role\n }\n }\n return state\n }\n\n // accepts either the raw Cookie header or an already-parsed cookies object\n // (e.g. req.cookies when a cookie-parser middleware already ran upstream) to\n // avoid re-parsing and allocating a second cookies object per request\n async readStateFromCookie (rawCookies: string | Record<string, string | undefined> | undefined, onlyDecode?: boolean, clearBrokenSession?: () => void): Promise<SessionState> {\n if (!rawCookies) return { lang: this.defaultLang }\n const cookies = typeof rawCookies === 'string' ? cookie.parse(rawCookies) : rawCookies\n\n const lang = cookies.i18n_lang || this.defaultLang\n const dark = cookies.theme_dark === '1' || cookies.theme_dark === 'true'\n\n if (!cookies.id_token) return { lang, dark }\n if (!cookies.id_token_sign && !onlyDecode) return { lang, dark }\n const token = cookies.id_token + '.' + (cookies.id_token_sign ?? '')\n const organizationId = cookies.id_token_org ?? ''\n const departmentId = cookies.id_token_dep ?? ''\n const switchedRole = cookies.id_token_role ?? ''\n\n let user: User\n if (onlyDecode) {\n user = jwt.decode(token) as User\n } else {\n try {\n user = await this.verifyToken(token) as User\n } catch (err: any) {\n console.warn(err)\n if (err.name === 'JwksError') {\n // happens in case of temporary unavailability if SD\n // better not to disconnect user in this case\n throw httpError(500, 'Session token public keys not initialized')\n } else {\n if (clearBrokenSession) clearBrokenSession()\n throw httpError(401)\n }\n }\n }\n if (!user) return { lang, dark }\n return Object.assign({ lang, dark } as SessionState, this.buildTokenSessionState(user, organizationId, departmentId, switchedRole))\n }\n}\n"]}
package/session.test.js CHANGED
@@ -1,6 +1,27 @@
1
1
  import { describe, it } from 'node:test';
2
2
  import { strict as assert } from 'assert';
3
+ import { generateKeyPairSync } from 'node:crypto';
4
+ import jwt from 'jsonwebtoken';
3
5
  import { SessionHandler } from './session.js';
6
+ const testUser = {
7
+ email: 'test@example.com',
8
+ id: 'user1',
9
+ name: 'Test User',
10
+ organizations: [
11
+ { id: 'org1', name: 'Test Org', role: 'admin' },
12
+ { id: 'org2', name: 'Other Org', role: 'user', department: 'dep1', departmentName: 'Department 1' }
13
+ ]
14
+ };
15
+ // a handler whose token verification is stubbed, counts the number of verifications
16
+ const stubbedHandler = () => {
17
+ const handler = new SessionHandler();
18
+ const calls = { count: 0 };
19
+ handler.verifyToken = async () => {
20
+ calls.count++;
21
+ return structuredClone(testUser);
22
+ };
23
+ return { handler, calls };
24
+ };
4
25
  describe('SessionHandler.readStateFromCookie', () => {
5
26
  it('should return default session with no cookies', async () => {
6
27
  const handler = new SessionHandler();
@@ -37,4 +58,93 @@ describe('SessionHandler.readStateFromCookie', () => {
37
58
  const state = await handler.readStateFromCookie('id_token=header.payload');
38
59
  assert.equal(state.user, undefined);
39
60
  });
61
+ it('should select the organization account from cookies', async () => {
62
+ const { handler } = stubbedHandler();
63
+ const state = await handler.readStateFromCookie('id_token=h.p; id_token_sign=s; id_token_org=org2; id_token_dep=dep1');
64
+ assert.equal(state.account?.type, 'organization');
65
+ assert.equal(state.account?.id, 'org2');
66
+ assert.equal(state.account?.department, 'dep1');
67
+ assert.equal(state.accountRole, 'user');
68
+ });
69
+ it('should build a user account when no org cookie', async () => {
70
+ const { handler } = stubbedHandler();
71
+ const state = await handler.readStateFromCookie('id_token=h.p; id_token_sign=s');
72
+ assert.equal(state.account?.type, 'user');
73
+ assert.equal(state.account?.id, 'user1');
74
+ assert.equal(state.accountRole, 'admin');
75
+ });
76
+ it('should accept an already-parsed cookies object without re-parsing', async () => {
77
+ const { handler, calls } = stubbedHandler();
78
+ const parsed = { id_token: 'h.p', id_token_sign: 's', id_token_org: 'org2', id_token_dep: 'dep1', i18n_lang: 'en', theme_dark: '1' };
79
+ const state = await handler.readStateFromCookie(parsed);
80
+ assert.equal(state.lang, 'en');
81
+ assert.equal(state.dark, true);
82
+ assert.equal(state.account?.type, 'organization');
83
+ assert.equal(state.account?.id, 'org2');
84
+ assert.equal(calls.count, 1);
85
+ });
86
+ it('should produce equivalent state from string and object cookie inputs', async () => {
87
+ const { handler } = stubbedHandler();
88
+ const fromString = await handler.readStateFromCookie('id_token=h.p; id_token_sign=s; i18n_lang=en');
89
+ const fromObject = await handler.readStateFromCookie({ id_token: 'h.p', id_token_sign: 's', i18n_lang: 'en' });
90
+ assert.deepEqual(fromString, fromObject);
91
+ });
92
+ it('should verify the token on every request', async () => {
93
+ const { handler, calls } = stubbedHandler();
94
+ const cookies = 'id_token=h.p; id_token_sign=s';
95
+ await handler.readStateFromCookie(cookies);
96
+ await handler.readStateFromCookie(cookies);
97
+ assert.equal(calls.count, 2);
98
+ });
99
+ it('should return a mutable state that callers can adjust', async () => {
100
+ const { handler } = stubbedHandler();
101
+ const state = await handler.readStateFromCookie('id_token=h.p; id_token_sign=s');
102
+ state.user.name = 'mutated'; // must not throw
103
+ state.user.organizations[0].role = 'other';
104
+ assert.equal(state.user.name, 'mutated');
105
+ assert.equal(state.user.organizations[0].role, 'other');
106
+ });
107
+ it('should decode without verifying in onlyDecode mode', async () => {
108
+ const handler = new SessionHandler();
109
+ const token = jwt.sign(structuredClone(testUser), 'secret');
110
+ const [header, payload, sign] = token.split('.');
111
+ const state = await handler.readStateFromCookie(`id_token=${header}.${payload}; id_token_sign=${sign}`, true);
112
+ assert.equal(state.user?.id, 'user1');
113
+ assert.equal(state.account?.type, 'user');
114
+ });
115
+ it('should reject and clear the session when the token is invalid', async () => {
116
+ const handler = new SessionHandler();
117
+ handler.verifyToken = async () => { throw Object.assign(new Error('jwt expired'), { name: 'TokenExpiredError' }); };
118
+ let cleared = false;
119
+ await assert.rejects(handler.readStateFromCookie('id_token=h.p; id_token_sign=s', undefined, () => { cleared = true; }), (err) => err.status === 401);
120
+ assert.equal(cleared, true);
121
+ });
122
+ it('should surface a 500 without clearing the session on a jwks error', async () => {
123
+ const handler = new SessionHandler();
124
+ handler.verifyToken = async () => { throw Object.assign(new Error('jwks down'), { name: 'JwksError' }); };
125
+ let cleared = false;
126
+ await assert.rejects(handler.readStateFromCookie('id_token=h.p; id_token_sign=s', undefined, () => { cleared = true; }), (err) => err.status === 500);
127
+ assert.equal(cleared, false);
128
+ });
129
+ });
130
+ describe('SessionHandler.verifyToken', () => {
131
+ it('should verify a token and cache the signing key per kid', async () => {
132
+ const { privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
133
+ const pem = publicKey.export({ format: 'pem', type: 'spki' });
134
+ const handler = new SessionHandler();
135
+ let getSigningKeyCalls = 0;
136
+ handler._jwksClient = {
137
+ getSigningKey: async () => {
138
+ getSigningKeyCalls++;
139
+ return { getPublicKey: () => pem };
140
+ }
141
+ };
142
+ const token = jwt.sign({ id: 'user1' }, privateKey, { algorithm: 'RS256', keyid: 'kid1' });
143
+ const payload1 = await handler.verifyToken(token);
144
+ assert.equal(payload1.id, 'user1');
145
+ const payload2 = await handler.verifyToken(token);
146
+ assert.equal(payload2.id, 'user1');
147
+ assert.equal(getSigningKeyCalls, 1);
148
+ });
40
149
  });
150
+ //# sourceMappingURL=session.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.test.js","sourceRoot":"","sources":["session.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it } from 'node:test'\nimport { strict as assert } from 'assert'\nimport { generateKeyPairSync } from 'node:crypto'\nimport jwt from 'jsonwebtoken'\nimport { SessionHandler } from './session.js'\n\nconst testUser = {\n email: 'test@example.com',\n id: 'user1',\n name: 'Test User',\n organizations: [\n { id: 'org1', name: 'Test Org', role: 'admin' },\n { id: 'org2', name: 'Other Org', role: 'user', department: 'dep1', departmentName: 'Department 1' }\n ]\n}\n\n// a handler whose token verification is stubbed, counts the number of verifications\nconst stubbedHandler = () => {\n const handler = new SessionHandler()\n const calls = { count: 0 }\n handler.verifyToken = async () => {\n calls.count++\n return structuredClone(testUser)\n }\n return { handler, calls }\n}\n\ndescribe('SessionHandler.readStateFromCookie', () => {\n it('should return default session with no cookies', async () => {\n const handler = new SessionHandler()\n const state = await handler.readStateFromCookie(undefined)\n assert.equal(state.lang, 'fr')\n assert.equal(state.user, undefined)\n assert.equal(state.dark, undefined)\n })\n\n it('should parse dark mode cookie', async () => {\n const handler = new SessionHandler()\n const state = await handler.readStateFromCookie('theme_dark=true')\n assert.equal(state.dark, true)\n })\n\n it('should parse i18n_lang cookie', async () => {\n const handler = new SessionHandler()\n const state = await handler.readStateFromCookie('i18n_lang=en')\n assert.equal(state.lang, 'en')\n })\n\n it('should use defaultLang when no i18n_lang cookie', async () => {\n const handler = new SessionHandler()\n handler.defaultLang = 'de'\n const state = await handler.readStateFromCookie('')\n assert.equal(state.lang, 'de')\n })\n\n it('should return early when no id_token', async () => {\n const handler = new SessionHandler()\n const state = await handler.readStateFromCookie('i18n_lang=en; theme_dark=1')\n assert.equal(state.lang, 'en')\n assert.equal(state.dark, true)\n assert.equal(state.user, undefined)\n })\n\n it('should return early when no id_token_sign and not onlyDecode', async () => {\n const handler = new SessionHandler()\n const state = await handler.readStateFromCookie('id_token=header.payload')\n assert.equal(state.user, undefined)\n })\n\n it('should select the organization account from cookies', async () => {\n const { handler } = stubbedHandler()\n const state = await handler.readStateFromCookie('id_token=h.p; id_token_sign=s; id_token_org=org2; id_token_dep=dep1')\n assert.equal(state.account?.type, 'organization')\n assert.equal(state.account?.id, 'org2')\n assert.equal(state.account?.department, 'dep1')\n assert.equal(state.accountRole, 'user')\n })\n\n it('should build a user account when no org cookie', async () => {\n const { handler } = stubbedHandler()\n const state = await handler.readStateFromCookie('id_token=h.p; id_token_sign=s')\n assert.equal(state.account?.type, 'user')\n assert.equal(state.account?.id, 'user1')\n assert.equal(state.accountRole, 'admin')\n })\n\n it('should accept an already-parsed cookies object without re-parsing', async () => {\n const { handler, calls } = stubbedHandler()\n const parsed = { id_token: 'h.p', id_token_sign: 's', id_token_org: 'org2', id_token_dep: 'dep1', i18n_lang: 'en', theme_dark: '1' }\n const state = await handler.readStateFromCookie(parsed)\n assert.equal(state.lang, 'en')\n assert.equal(state.dark, true)\n assert.equal(state.account?.type, 'organization')\n assert.equal(state.account?.id, 'org2')\n assert.equal(calls.count, 1)\n })\n\n it('should produce equivalent state from string and object cookie inputs', async () => {\n const { handler } = stubbedHandler()\n const fromString = await handler.readStateFromCookie('id_token=h.p; id_token_sign=s; i18n_lang=en')\n const fromObject = await handler.readStateFromCookie({ id_token: 'h.p', id_token_sign: 's', i18n_lang: 'en' })\n assert.deepEqual(fromString, fromObject)\n })\n\n it('should verify the token on every request', async () => {\n const { handler, calls } = stubbedHandler()\n const cookies = 'id_token=h.p; id_token_sign=s'\n await handler.readStateFromCookie(cookies)\n await handler.readStateFromCookie(cookies)\n assert.equal(calls.count, 2)\n })\n\n it('should return a mutable state that callers can adjust', async () => {\n const { handler } = stubbedHandler()\n const state = await handler.readStateFromCookie('id_token=h.p; id_token_sign=s')\n state.user!.name = 'mutated' // must not throw\n state.user!.organizations[0].role = 'other'\n assert.equal(state.user!.name, 'mutated')\n assert.equal(state.user!.organizations[0].role, 'other')\n })\n\n it('should decode without verifying in onlyDecode mode', async () => {\n const handler = new SessionHandler()\n const token = jwt.sign(structuredClone(testUser), 'secret')\n const [header, payload, sign] = token.split('.')\n const state = await handler.readStateFromCookie(`id_token=${header}.${payload}; id_token_sign=${sign}`, true)\n assert.equal(state.user?.id, 'user1')\n assert.equal(state.account?.type, 'user')\n })\n\n it('should reject and clear the session when the token is invalid', async () => {\n const handler = new SessionHandler()\n handler.verifyToken = async () => { throw Object.assign(new Error('jwt expired'), { name: 'TokenExpiredError' }) }\n let cleared = false\n await assert.rejects(\n handler.readStateFromCookie('id_token=h.p; id_token_sign=s', undefined, () => { cleared = true }),\n (err: any) => err.status === 401\n )\n assert.equal(cleared, true)\n })\n\n it('should surface a 500 without clearing the session on a jwks error', async () => {\n const handler = new SessionHandler()\n handler.verifyToken = async () => { throw Object.assign(new Error('jwks down'), { name: 'JwksError' }) }\n let cleared = false\n await assert.rejects(\n handler.readStateFromCookie('id_token=h.p; id_token_sign=s', undefined, () => { cleared = true }),\n (err: any) => err.status === 500\n )\n assert.equal(cleared, false)\n })\n})\n\ndescribe('SessionHandler.verifyToken', () => {\n it('should verify a token and cache the signing key per kid', async () => {\n const { privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })\n const pem = publicKey.export({ format: 'pem', type: 'spki' }) as string\n const handler = new SessionHandler()\n let getSigningKeyCalls = 0\n ;(handler as any)._jwksClient = {\n getSigningKey: async () => {\n getSigningKeyCalls++\n return { getPublicKey: () => pem }\n }\n }\n const token = jwt.sign({ id: 'user1' }, privateKey, { algorithm: 'RS256', keyid: 'kid1' })\n const payload1 = await handler.verifyToken(token)\n assert.equal(payload1.id, 'user1')\n const payload2 = await handler.verifyToken(token)\n assert.equal(payload2.id, 'user1')\n assert.equal(getSigningKeyCalls, 1)\n })\n})\n"]}
@@ -14,3 +14,4 @@ export async function load(url, context, nextLoad) {
14
14
  }
15
15
  return result;
16
16
  }
17
+ //# sourceMappingURL=test-module-hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-module-hooks.js","sourceRoot":"","sources":["test-module-hooks.ts"],"names":[],"mappings":"AAAA,cAAc;AAEd,4DAA4D;AAE5D,MAAM,MAAM,GAAG,2BAA2B,CAAA;AAE1C,MAAM,CAAC,KAAK,UAAU,IAAI,CAAE,GAAG,EAAE,OAAO,EAAE,QAAQ;IAChD,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;IAC3C,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,mBAAmB,EAAE,CAAC;QACxE,IAAI,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAA;QACrC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QACpC,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,GAAG,6EAA6E,GAAG,MAAM,CAAA;YAC/F,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAA;YACtD,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QAC9C,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["// @ts-nocheck\n\n// cf https://nodejs.org/api/module.html#customization-hooks\n\nconst regexp = /\\/\\/ @test:spy\\((.*)\\)\\n/g\n\nexport async function load (url, context, nextLoad) {\n const result = await nextLoad(url, context)\n if (result.format === 'module' || result.format === 'module-typescript') {\n let source = result.source.toString()\n const matches = source.match(regexp)\n if (matches) {\n source = 'import { emit as __emitTestSpy } from \"@data-fair/lib-node/test-spies.js\"\\n' + source\n source = source.replace(regexp, '__emitTestSpy($1)\\n')\n result.source = Buffer.from(source, 'utf-8')\n }\n }\n return result\n}\n"]}
package/test-spies.js CHANGED
@@ -40,3 +40,4 @@ export function emit(eventName, data) {
40
40
  p.resolve(data);
41
41
  }
42
42
  }
43
+ //# sourceMappingURL=test-spies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-spies.js","sourceRoot":"","sources":["test-spies.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,WAAW,MAAM,OAAO,CAAA;AAE/B,MAAM,KAAK,GAAG,WAAW,CAAC,YAAY,CAAC,CAAA;AAEvC,MAAM,UAAU,mBAAmB;IACjC,QAAQ,CAAC,0CAA0C,EAAE,EAAE,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;AACtF,CAAC;AAED,MAAM,EAAE,eAAe,EAAE,GAAG,KAAK,CAAA;AAEjC,MAAM,eAAe,GAAmE,EAAE,CAAA;AAC1F,IAAI,CAAC,GAAG,CAAC,CAAA;AAET,MAAM,UAAU,OAAO,CAAM,SAAiB,EAAE,OAAO,GAAG,IAAI;IAC5D,CAAC,EAAE,CAAA;IACH,MAAM,EAAE,GAAG,CAAC,CAAA;IAEZ,KAAK,CAAC,wBAAwB,SAAS,QAAQ,EAAE,cAAc,OAAO,KAAK,CAAC,CAAA;IAE5E,sBAAsB;IACtB,wEAAwE;IACxE,KAAK,CAAC,eAAe,GAAG,CAAC,CAAA;IACzB,MAAM,YAAY,GAAG,IAAI,KAAK,CAAC,gCAAgC,SAAS,EAAE,CAAC,CAAA;IAC3E,KAAK,CAAC,eAAe,GAAG,eAAe,CAAA;IACvC,KAAK,CAAC,iBAAiB,CAAC,YAAY,EAAE,OAAO,CAAC,CAAA;IAE9C,MAAM,CAAC,GAAG,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3C,eAAe,CAAC,SAAS,CAAC,GAAG,eAAe,CAAC,SAAS,CAAC,IAAI,EAAE,CAAA;QAC7D,eAAe,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAA;QAChD,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,CAAA;IACjD,CAAC,CAAC,CAAA;IACF,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE;QACb,KAAK,CAAC,wBAAwB,SAAS,QAAQ,EAAE,GAAG,CAAC,CAAA;QACrD,eAAe,CAAC,SAAS,CAAC,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;QAChF,IAAI,eAAe,CAAC,SAAS,CAAC,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,eAAe,CAAC,SAAS,CAAC,CAAA;IAChF,CAAC,CAAC,CAAA;IACF,OAAO,CAAC,CAAA;AACV,CAAC;AAED,MAAM,UAAU,IAAI,CAAE,SAAiB,EAAE,IAAS;IAChD,KAAK,CAAC,qBAAqB,SAAS,KAAK,eAAe,CAAC,SAAS,CAAC,EAAE,MAAM,aAAa,CAAC,CAAA;IACzF,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;QACxC,OAAO,CAAC,KAAK,CAAC,4BAA4B,SAAS,EAAE,CAAC,CAAA;QACtD,OAAM;IACR,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,eAAe,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3C,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACjB,CAAC;AACH,CAAC","sourcesContent":["import { register } from 'node:module'\nimport debugModule from 'debug'\n\nconst debug = debugModule('test-spies')\n\nexport function registerModuleHooks () {\n register('@data-fair/lib-node/test-module-hooks.js', { parentURL: import.meta.url })\n}\n\nconst { stackTraceLimit } = Error\n\nconst pendingPromises: Record<string, { resolve: (data: any) => void, id: number }[]> = {}\nlet i = 0\n\nexport function waitFor <T> (eventName: string, timeout = 2000) {\n i++\n const id = i\n\n debug(`Waiting for test spy ${eventName} (id=${id}, timeout: ${timeout}ms)`)\n\n // better stack traces\n // see https://github.com/axios/axios/issues/2387#issuecomment-652242713\n Error.stackTraceLimit = 0\n const timeoutError = new Error(`Timeout waiting for test spy ${eventName}`)\n Error.stackTraceLimit = stackTraceLimit\n Error.captureStackTrace(timeoutError, waitFor)\n\n const p = new Promise<T>((resolve, reject) => {\n pendingPromises[eventName] = pendingPromises[eventName] ?? []\n pendingPromises[eventName].push({ resolve, id })\n setTimeout(() => reject(timeoutError), timeout)\n })\n p.finally(() => {\n debug(`Cleaning up test spy ${eventName} (id=${id})`)\n pendingPromises[eventName] = pendingPromises[eventName].filter(p => p.id !== id)\n if (pendingPromises[eventName].length === 0) delete pendingPromises[eventName]\n })\n return p\n}\n\nexport function emit (eventName: string, data: any) {\n debug(`Emitting test spy ${eventName} (${pendingPromises[eventName]?.length} listeners)`)\n if (!pendingPromises[eventName]?.length) {\n console.debug(`No listener for test spy ${eventName}`)\n return\n }\n for (const p of pendingPromises[eventName]) {\n p.resolve(data)\n }\n}\n"]}
@@ -1,8 +1,43 @@
1
1
  import type { Db } from 'mongodb';
2
2
  import type { Debugger } from 'debug';
3
3
  import type { Locks } from './locks.js';
4
+ /**
5
+ * A migration to be applied to the service's database.
6
+ *
7
+ * Scripts live in `upgrade/<last-released-version>/<filename>.ts` under the
8
+ * service. The folder name MUST be the version of the last release at the
9
+ * time the script is authored — never an anticipated future version. On a
10
+ * working branch the eventual release number is unknown (the same change may
11
+ * ship as a minor, a major, or be backported), so the last known release is
12
+ * the only stable reference.
13
+ *
14
+ * The runner executes every script whose folder version satisfies
15
+ * `semver.gte(folder, dbVersion)`, where `dbVersion` is what was stored at
16
+ * the previous startup, then records the current `package.json` version.
17
+ * With folder = last-released, the script runs once on production upgrade
18
+ * (after the release bumps `package.json`), and re-runs on every staging
19
+ * deploy until the next release ships — this is expected.
20
+ *
21
+ * `exec` MUST be idempotent: re-running it must be a safe no-op.
22
+ */
4
23
  export interface UpgradeScript {
24
+ /** One short sentence; logged at run time. */
5
25
  description: string;
26
+ /** Idempotent migration body. */
6
27
  exec: (db: Db, debug: Debugger) => Promise<void>;
7
28
  }
29
+ /**
30
+ * Run pending upgrade scripts for the current service, then record the
31
+ * current `package.json` version in the `services` Mongo collection.
32
+ *
33
+ * Acquires the `upgrade` lock so only one process runs migrations at a time;
34
+ * other processes log a warning and continue without waiting.
35
+ *
36
+ * See the `UpgradeScript` interface for the folder-naming rule.
37
+ *
38
+ * @param db - Active Mongo connection (reused by the scripts).
39
+ * @param locks - Lock manager from `@data-fair/lib-node/locks`.
40
+ * @param basePath - Path to the service directory containing `upgrade/` and `package.json`. Defaults to `./`.
41
+ * @param isFresh - Optional callback returning true if the database is a brand-new install with no legacy data. When true, all historical scripts are skipped and only the current version is recorded.
42
+ */
8
43
  export default function (db: Db, locks: Locks, basePath?: string, isFresh?: () => Promise<boolean>): Promise<void>;
@@ -3,7 +3,20 @@ import path from 'path';
3
3
  import { readdirSync, readFileSync, statSync } from 'fs';
4
4
  import Debug from 'debug';
5
5
  const debug = Debug('upgrade');
6
- // chose the proper scripts to execute, then run them
6
+ /**
7
+ * Run pending upgrade scripts for the current service, then record the
8
+ * current `package.json` version in the `services` Mongo collection.
9
+ *
10
+ * Acquires the `upgrade` lock so only one process runs migrations at a time;
11
+ * other processes log a warning and continue without waiting.
12
+ *
13
+ * See the `UpgradeScript` interface for the folder-naming rule.
14
+ *
15
+ * @param db - Active Mongo connection (reused by the scripts).
16
+ * @param locks - Lock manager from `@data-fair/lib-node/locks`.
17
+ * @param basePath - Path to the service directory containing `upgrade/` and `package.json`. Defaults to `./`.
18
+ * @param isFresh - Optional callback returning true if the database is a brand-new install with no legacy data. When true, all historical scripts are skipped and only the current version is recorded.
19
+ */
7
20
  export default async function (db, locks, basePath = './', isFresh) {
8
21
  const ack = await locks.acquire('upgrade');
9
22
  if (!ack) {
@@ -93,3 +106,4 @@ async function listScripts(scriptsRoot) {
93
106
  };
94
107
  });
95
108
  }
109
+ //# sourceMappingURL=upgrade-scripts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade-scripts.js","sourceRoot":"","sources":["upgrade-scripts.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Db } from 'mongodb'\nimport type { Debugger } from 'debug'\nimport semver from 'semver'\nimport path from 'path'\nimport { readdirSync, readFileSync, statSync } from 'fs'\nimport Debug from 'debug'\nimport type { Locks } from './locks.js'\n\nconst debug = Debug('upgrade')\n\n/**\n * A migration to be applied to the service's database.\n *\n * Scripts live in `upgrade/<last-released-version>/<filename>.ts` under the\n * service. The folder name MUST be the version of the last release at the\n * time the script is authored — never an anticipated future version. On a\n * working branch the eventual release number is unknown (the same change may\n * ship as a minor, a major, or be backported), so the last known release is\n * the only stable reference.\n *\n * The runner executes every script whose folder version satisfies\n * `semver.gte(folder, dbVersion)`, where `dbVersion` is what was stored at\n * the previous startup, then records the current `package.json` version.\n * With folder = last-released, the script runs once on production upgrade\n * (after the release bumps `package.json`), and re-runs on every staging\n * deploy until the next release ships — this is expected.\n *\n * `exec` MUST be idempotent: re-running it must be a safe no-op.\n */\nexport interface UpgradeScript {\n /** One short sentence; logged at run time. */\n description: string\n /** Idempotent migration body. */\n exec: (db: Db, debug: Debugger) => Promise<void>\n}\n\n/**\n * Run pending upgrade scripts for the current service, then record the\n * current `package.json` version in the `services` Mongo collection.\n *\n * Acquires the `upgrade` lock so only one process runs migrations at a time;\n * other processes log a warning and continue without waiting.\n *\n * See the `UpgradeScript` interface for the folder-naming rule.\n *\n * @param db - Active Mongo connection (reused by the scripts).\n * @param locks - Lock manager from `@data-fair/lib-node/locks`.\n * @param basePath - Path to the service directory containing `upgrade/` and `package.json`. Defaults to `./`.\n * @param isFresh - Optional callback returning true if the database is a brand-new install with no legacy data. When true, all historical scripts are skipped and only the current version is recorded.\n */\nexport default async function (db: Db, locks: Locks, basePath = './', isFresh?: () => Promise<boolean>) {\n const ack = await locks.acquire('upgrade')\n if (!ack) {\n console.warn('upgrade scripts lock is already acquired, skip them')\n // IMPORTANT: this behaviour of running the process when the upgrade scripts are still running on another one\n // implies that they cannot be considered a pre-requisite..\n // if we want to consider the upgrade scripts as a pre-requisite we should implement a wait on all\n // containers for the scripts that are running in only 1 (while loop on \"acquire\" ?) and a healthcheck\n // are not considered \"up\" and the previous versions keep running in the mean time\n } else {\n try {\n await runScripts(db, basePath, isFresh)\n } finally {\n await locks.release('upgrade')\n }\n }\n}\n\nasync function runScripts (db: Db, basePath: string, isFresh?: () => Promise<boolean>) {\n const dir = path.resolve(basePath)\n // in the case of workspaces the parent package.json is the right place to look for name and versions\n const parentPjsonPath = path.join(dir, '../package.json')\n const pjsonPath = path.join(dir, 'package.json')\n let pjson\n try {\n pjson = JSON.parse(readFileSync(parentPjsonPath, 'utf8'))\n debug('read service info from ' + parentPjsonPath)\n } catch (err) {\n pjson = JSON.parse(readFileSync(pjsonPath, 'utf8'))\n debug('read service info from ' + pjsonPath)\n }\n\n const scriptsRoot = path.join(dir, 'upgrade')\n debug(`service=${pjson.name} version=${pjson.version}`)\n\n const services = db.collection<{ id: string, version: string }>('services')\n const service = await services.findOne({ id: pjson.name })\n let previousPersion = service?.version\n debug('list scripts from ' + scriptsRoot)\n let scripts = await listScripts(scriptsRoot)\n if (previousPersion) {\n debug(`current service version from database : ${previousPersion}`)\n } else {\n debug('no service version found in database, this is the first time the upgrade system is run')\n scripts = scripts.filter(scriptDef => scriptDef.version === 'init')\n if (isFresh) {\n if (await isFresh()) {\n debug('isFresh function returned true, this is a fresh install, do not run any script')\n } else {\n debug('isFresh function returned false, this is not a fresh install, run all init scripts')\n previousPersion = '0.0.0'\n }\n } else {\n debug('no isFresh function, it could be a fresh install, do not run any script')\n }\n }\n\n if (previousPersion) {\n for (const scriptDef of scripts) {\n if (semver.gte(scriptDef.version, previousPersion)) {\n for (const scriptName of scriptDef.names) {\n const script: UpgradeScript = (await import(path.join(scriptsRoot, scriptDef.version, scriptName))).default\n debug('apply script %s/%s : %s', scriptDef.version, scriptName, script.description)\n await script.exec(db, Debug(`upgrade:${scriptDef.version}:${scriptName}`))\n }\n }\n }\n }\n\n const coercedVersion = semver.coerce(pjson.version)\n if (!coercedVersion) throw new Error(`Invalid version number in package.json ${pjson.version}`)\n const newService = { id: pjson.name, version: coercedVersion.version }\n debug(`upgrade scripts are over, save current version number ${newService.version}`)\n await services.updateOne({ id: pjson.name }, { $set: newService }, { upsert: true })\n}\n\n// Walk the scripts directories\nasync function listScripts (scriptsRoot: string) {\n const dirs = readdirSync(scriptsRoot)\n .filter(dir => statSync(path.join(scriptsRoot, dir)).isDirectory())\n .sort()\n return dirs.map(dir => {\n return {\n version: dir,\n names: readdirSync(path.join(scriptsRoot, dir)).sort()\n }\n })\n}\n"]}
package/validation.js CHANGED
@@ -1 +1,2 @@
1
1
  export * from '@data-fair/lib-validation';
2
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["validation.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAA","sourcesContent":["export * from '@data-fair/lib-validation'\n"]}
package/ws-client.js CHANGED
@@ -137,3 +137,4 @@ export class DataFairWsClient extends WsClient {
137
137
  return event;
138
138
  }
139
139
  }
140
+ //# sourceMappingURL=ws-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-client.js","sourceRoot":"","sources":["ws-client.ts"],"names":[],"mappings":"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","sourcesContent":["// a wrapper around a websocket connection with auto reconnect\n// JSON parsing and channel subscriptions\n\nimport { type Message } from '@data-fair/lib-common-types/ws.js'\nimport { type Account } from '@data-fair/lib-common-types/session/index.js'\nimport EventEmitter from 'node:events'\nimport WebSocket from 'ws'\nimport Debug from 'debug'\n\nconst debug = Debug('ws')\n\nconst { stackTraceLimit } = Error\n\nclass WsClientError extends Error {\n constructor () {\n super()\n this.name = 'WsClientError'\n }\n}\n\ntype logFn = (msg: string, ...args: any[]) => void\n\nexport interface WsClientOpts {\n log?: { info: logFn, error: logFn, debug: logFn }\n url: string\n headers?: Record<string, string>\n apiKey?: string\n adminMode?: boolean\n account?: Account\n}\n\nexport type FullWsClientOpts = WsClientOpts & Required<Pick<WsClientOpts, 'log'>>\n\nexport class WsClient extends EventEmitter {\n private _channels: string[]\n private _ws: WebSocket | undefined\n opts: FullWsClientOpts\n\n constructor (opts: WsClientOpts) {\n super()\n this._channels = []\n this.opts = { log: console, ...opts }\n }\n\n private async _connect (): Promise<WebSocket> {\n return await new Promise((resolve, reject) => {\n const wsUrl = this.opts.url.replace('http://', 'ws://').replace('https://', 'wss://') + '/'\n debug(`connect Web Socket to ${wsUrl}`)\n const ws = new WebSocket(wsUrl, { headers: this.opts.headers ?? {} })\n this._ws = ws\n ws.on('error', (err: any) => {\n debug('WS encountered an error', err.message)\n this._reconnect()\n reject(err)\n })\n ws.once('open', () => {\n debug('WS is opened')\n resolve(ws)\n })\n ws.on('message', (msg: string) => {\n const message = JSON.parse(msg) as Message\n debug('received message', message)\n this.emit('message', message)\n })\n })\n }\n\n private async _reconnect () {\n if (!this._ws) return\n debug('reconnect')\n this._ws.terminate()\n await this._connect()\n for (const channel of this._channels) {\n await this.subscribe(channel, true)\n }\n }\n\n async subscribe (channel: string, force = false, timeout = 2000) {\n if (this._channels.includes(channel) && !force) return\n const ws = this._ws ?? await this._connect()\n\n // better stack traces\n // see https://github.com/axios/axios/issues/2387#issuecomment-652242713\n Error.stackTraceLimit = 0\n const errorContext = new WsClientError()\n Error.stackTraceLimit = stackTraceLimit\n Error.captureStackTrace(errorContext, this.subscribe)\n\n debug('subscribe to channel', channel)\n const subscribeMessage: any = { type: 'subscribe', channel }\n if (this.opts.apiKey) subscribeMessage.apiKey = this.opts.apiKey\n if (this.opts.adminMode && this.opts.account) subscribeMessage.account = JSON.stringify(this.opts.account)\n ws.send(JSON.stringify(subscribeMessage))\n const event = await this.waitFor(\n channel,\n (e: Message) => {\n return e.type === 'subscribe-confirm' || e.type === 'error'\n },\n timeout,\n true,\n true,\n errorContext\n )\n if (event.type === 'error') {\n const error = new Error(event.data)\n error.stack += '\\nSubscribe context:\\n' + errorContext.stack\n throw error\n }\n if (this._channels.includes(channel)) this._channels.push(channel)\n }\n\n async waitFor (channel: string, filter?: (message: Message) => boolean, timeout = 300000, skipSubscribe = false, fullMessage = false, _errorContext?: WsClientError): Promise<Message> {\n if (!skipSubscribe) await this.subscribe(channel)\n\n // better stack traces\n // see https://github.com/axios/axios/issues/2387#issuecomment-652242713\n let errorContext: WsClientError\n if (_errorContext) {\n errorContext = _errorContext\n } else {\n Error.stackTraceLimit = 0\n errorContext = new WsClientError()\n Error.stackTraceLimit = stackTraceLimit\n Error.captureStackTrace(errorContext, this.waitFor)\n }\n\n return await new Promise((resolve, reject) => {\n const _timeout = setTimeout(() => {\n this.off('message', messageCb)\n const error = new Error(`Timeout of ${timeout}ms exceeded`)\n error.stack += '\\nWaitFor context:\\n' + errorContext.stack\n reject(error)\n }, timeout)\n const messageCb = (message: any) => {\n if (message.channel === channel && (!filter || filter(fullMessage ? message : message.data))) {\n clearTimeout(_timeout)\n this.off('message', messageCb)\n resolve(fullMessage ? message : message.data)\n }\n }\n this.on('message', messageCb)\n })\n }\n\n close () {\n if (this._ws) this._ws.terminate()\n }\n}\n\nexport class DataFairWsClient extends WsClient {\n async waitForJournal (datasetId: string, eventType: string, timeout = 300000) {\n await this.opts.log.info(`wait for event \"${eventType}\" on dataset \"${datasetId}\"`)\n\n // better stack traces\n // see https://github.com/axios/axios/issues/2387#issuecomment-652242713\n Error.stackTraceLimit = 0\n const errorContext = new WsClientError()\n Error.stackTraceLimit = stackTraceLimit\n Error.captureStackTrace(errorContext, this.waitForJournal)\n\n const event = await this.waitFor(\n `datasets/${datasetId}/journal`,\n (e) => {\n return e.type === eventType || e.type === 'error'\n },\n timeout,\n false,\n false,\n errorContext\n )\n if (event.type === 'error') {\n const error = new Error(event.data)\n error.stack += '\\nWaitForJournal context:\\n' + errorContext.stack\n throw error\n }\n return event\n }\n}\n"]}
package/ws-emitter.js CHANGED
@@ -18,3 +18,4 @@ export const emit = async (channel, data) => {
18
18
  throw new Error('wsEmitter.init was not called before sending data');
19
19
  await messagesCollection.insertOne({ type: 'message', channel, data, date: new Date().toISOString() });
20
20
  };
21
+ //# sourceMappingURL=ws-emitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-emitter.js","sourceRoot":"","sources":["ws-emitter.ts"],"names":[],"mappings":"AAAA,wGAAwG;AAIxG,iEAAiE;AACjE,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAK,EAAE,EAAM,EAAE,EAAE;IACrD,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,CAAC,eAAe,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IACnF,IAAI,CAAC,UAAU;QAAE,MAAM,EAAE,CAAC,gBAAgB,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;IACpG,OAAO,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,CAAA;AACrC,CAAC,CAAA;AAED,IAAI,kBAA0C,CAAA;AAE9C,MAAM,CAAC,MAAM,IAAI,GAAG,KAAK,EAAE,EAAM,EAAE,EAAE;IACnC,IAAI,kBAAkB;QAAE,OAAM,CAAC,sBAAsB;IACrD,kBAAkB,GAAG,MAAM,sBAAsB,CAAC,EAAE,CAAC,CAAA;IACrD,MAAM,kBAAkB,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA;AACtD,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,IAAI,GAAG,KAAK,EAAE,OAAe,EAAE,IAAS,EAAE,EAAE;IACvD,IAAI,CAAC,kBAAkB;QAAE,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAA;IAC7F,MAAM,kBAAkB,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;AACxG,CAAC,CAAA","sourcesContent":["// write messages to a mongodb collection/queue for publication by ws-server.js and consumption by ws.js\n\nimport type { Db, Collection } from 'mongodb'\n\n// return a capped mongodb collection that acts as a simple queue\nexport const initMessagesCollection = async (db: Db) => {\n const collection = (await db.listCollections({ name: 'ws-messages' }).toArray())[0]\n if (!collection) await db.createCollection('ws-messages', { capped: true, size: 100000, max: 1000 })\n return db.collection('ws-messages')\n}\n\nlet messagesCollection: Collection | undefined\n\nexport const init = async (db: Db) => {\n if (messagesCollection) return // already initialized\n messagesCollection = await initMessagesCollection(db)\n await messagesCollection.insertOne({ type: 'init' })\n}\n\nexport const emit = async (channel: string, data: any) => {\n if (!messagesCollection) throw new Error('wsEmitter.init was not called before sending data')\n await messagesCollection.insertOne({ type: 'message', channel, data, date: new Date().toISOString() })\n}\n"]}