@camunda/ccma-api 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/index.d.ts +31 -7
  2. package/dist/index.js +56 -2
  3. package/dist/index.js.map +2 -2
  4. package/dist/services/accounts/admin-tools.service.d.ts +27 -0
  5. package/dist/services/accounts/admin-tools.service.js +70 -0
  6. package/dist/services/accounts/admin-tools.service.js.map +7 -0
  7. package/dist/services/accounts/connections.service.d.ts +85 -0
  8. package/dist/services/accounts/connections.service.js +150 -0
  9. package/dist/services/accounts/connections.service.js.map +2 -2
  10. package/dist/services/accounts/early-access-features.service.d.ts +59 -0
  11. package/dist/services/accounts/early-access-features.service.js +142 -0
  12. package/dist/services/accounts/early-access-features.service.js.map +2 -2
  13. package/dist/services/accounts/invites.service.d.ts +28 -0
  14. package/dist/services/accounts/invites.service.js +53 -0
  15. package/dist/services/accounts/invites.service.js.map +7 -0
  16. package/dist/services/accounts/orgs.service.d.ts +42 -0
  17. package/dist/services/accounts/orgs.service.js +123 -0
  18. package/dist/services/accounts/orgs.service.js.map +2 -2
  19. package/dist/services/accounts/orgs.v2.service.d.ts +6 -0
  20. package/dist/services/accounts/orgs.v2.service.js +34 -0
  21. package/dist/services/accounts/orgs.v2.service.js.map +2 -2
  22. package/dist/services/accounts/pdf-watermark.service.d.ts +93 -0
  23. package/dist/services/accounts/pdf-watermark.service.js +147 -0
  24. package/dist/services/accounts/pdf-watermark.service.js.map +7 -0
  25. package/dist/services/accounts/release-train.service.d.ts +25 -0
  26. package/dist/services/accounts/release-train.service.js +46 -0
  27. package/dist/services/accounts/release-train.service.js.map +7 -0
  28. package/dist/services/accounts/salesplan.service.d.ts +45 -0
  29. package/dist/services/accounts/salesplan.service.js +185 -0
  30. package/dist/services/accounts/salesplan.service.js.map +2 -2
  31. package/dist/services/accounts/token-issuer.service.d.ts +31 -0
  32. package/dist/services/accounts/token-issuer.service.js +63 -0
  33. package/dist/services/accounts/token-issuer.service.js.map +7 -0
  34. package/dist/services/accounts/users.service.d.ts +59 -0
  35. package/dist/services/accounts/users.service.js +296 -4
  36. package/dist/services/accounts/users.service.js.map +2 -2
  37. package/dist/services/console/console.admin.service.d.ts +21 -1
  38. package/dist/services/console/console.admin.service.js +75 -0
  39. package/dist/services/console/console.admin.service.js.map +2 -2
  40. package/dist/services/console/console.channel.service.d.ts +49 -0
  41. package/dist/services/console/console.channel.service.js +123 -0
  42. package/dist/services/console/console.channel.service.js.map +7 -0
  43. package/dist/services/console/console.cluster.service.d.ts +23 -11
  44. package/dist/services/console/console.cluster.service.js +36 -1
  45. package/dist/services/console/console.cluster.service.js.map +2 -2
  46. package/dist/services/console/console.cluster.v2.service.d.ts +18 -1
  47. package/dist/services/console/console.cluster.v2.service.js +95 -0
  48. package/dist/services/console/console.cluster.v2.service.js.map +2 -2
  49. package/dist/services/console/console.clusterplan.service.d.ts +21 -0
  50. package/dist/services/console/console.clusterplan.service.js +92 -0
  51. package/dist/services/console/console.clusterplan.service.js.map +2 -2
  52. package/dist/services/console/console.clusterplantype.service.d.ts +54 -0
  53. package/dist/services/console/console.clusterplantype.service.js +152 -0
  54. package/dist/services/console/console.clusterplantype.service.js.map +7 -0
  55. package/dist/services/console/console.k8scontext.service.d.ts +10 -0
  56. package/dist/services/console/console.k8scontext.service.js +41 -0
  57. package/dist/services/console/console.k8scontext.service.js.map +7 -0
  58. package/dist/services/console/console.storybook-config.service.d.ts +10 -0
  59. package/dist/services/console/console.storybook-config.service.js +40 -0
  60. package/dist/services/console/console.storybook-config.service.js.map +7 -0
  61. package/dist/services/console/generation.service.d.ts +45 -0
  62. package/dist/services/console/generation.service.js +143 -0
  63. package/dist/services/console/generation.service.js.map +2 -2
  64. package/dist/services/external/external.accounts.service.d.ts +27 -0
  65. package/dist/services/external/external.accounts.service.js +80 -0
  66. package/dist/services/external/external.accounts.service.js.map +7 -0
  67. package/dist/services/external/external.console.service.d.ts +37 -0
  68. package/dist/services/external/external.console.service.js +81 -0
  69. package/dist/services/external/external.console.service.js.map +7 -0
  70. package/dist/services/external/external.notifications.service.d.ts +78 -0
  71. package/dist/services/external/external.notifications.service.js +220 -0
  72. package/dist/services/external/external.notifications.service.js.map +7 -0
  73. package/dist/services/external/external.services.spec.d.ts +1 -0
  74. package/dist/services/notifications/notifications.service.d.ts +45 -1
  75. package/dist/services/notifications/notifications.service.js +168 -1
  76. package/dist/services/notifications/notifications.service.js.map +2 -2
  77. package/dist/types/general.types.d.ts +9 -1
  78. package/dist/utils/api.d.ts +14 -1
  79. package/dist/utils/api.js +71 -6
  80. package/dist/utils/api.js.map +2 -2
  81. package/dist/utils/api.request-headers.spec.d.ts +1 -0
  82. package/dist/utils/api.transient-retry.spec.d.ts +1 -0
  83. package/dist/utils/sse.d.ts +51 -0
  84. package/dist/utils/sse.js +254 -0
  85. package/dist/utils/sse.js.map +7 -0
  86. package/dist/utils/sse.spec.d.ts +1 -0
  87. package/dist/utils/traceparent.d.ts +30 -0
  88. package/dist/utils/traceparent.js +46 -0
  89. package/dist/utils/traceparent.js.map +7 -0
  90. package/dist/utils/traceparent.spec.d.ts +1 -0
  91. package/package.json +2 -2
package/dist/utils/api.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { getEnvParam } from "./environments.js";
2
2
  import { frontendCache } from "./frontendCache.js";
3
+ const TRANSIENT_RETRY_STATUS = [502, 503, 504];
4
+ const MAX_TRANSIENT_RETRIES = 3;
3
5
  function sendMetrics(payload, type, options) {
4
6
  if (payload.config.metrics) {
5
7
  if (payload.config.metrics.types.includes(type)) {
@@ -97,6 +99,34 @@ function rateLimitWaitingTime(attempt) {
97
99
  const waitingTime = 2 ** attempt * 1e3 + noise;
98
100
  return Math.floor(waitingTime);
99
101
  }
102
+ function transientRetryWaitingTime(attempt) {
103
+ const noise = Math.random() * 100;
104
+ return Math.floor(2 ** attempt * 250 + noise);
105
+ }
106
+ function canRetryTransient(payload) {
107
+ if (!payload.retryTransientErrors) {
108
+ return false;
109
+ }
110
+ const remaining = payload.transientRetry?.remaining ?? MAX_TRANSIENT_RETRIES;
111
+ return remaining > 0;
112
+ }
113
+ async function scheduleTransientRetry(payload) {
114
+ const attempts = payload.transientRetry?.attempts ?? 1;
115
+ const remaining = payload.transientRetry?.remaining ?? MAX_TRANSIENT_RETRIES;
116
+ const waitingTime = transientRetryWaitingTime(attempts);
117
+ log(
118
+ payload,
119
+ "debug",
120
+ `transient error, attempt ${attempts}, remaining ${remaining}, waiting ${waitingTime}ms`
121
+ );
122
+ await new Promise((resolve) => {
123
+ setTimeout(resolve, waitingTime);
124
+ });
125
+ payload.transientRetry = {
126
+ attempts: attempts + 1,
127
+ remaining: remaining - 1
128
+ };
129
+ }
100
130
  async function request(payload) {
101
131
  log(payload, "debug", "starting request");
102
132
  sendMetrics(payload, "request");
@@ -110,7 +140,17 @@ async function request(payload) {
110
140
  if (payload.config.auth) {
111
141
  token = await payload.config.auth.getToken();
112
142
  }
113
- const headers = payload.headers ?? {};
143
+ let dynamicHeaders = {};
144
+ if (payload.metricsDetails?.target?.type !== "external") {
145
+ try {
146
+ dynamicHeaders = payload.config.getDynamicHeaders?.() ?? {};
147
+ } catch (_error) {
148
+ }
149
+ }
150
+ const headers = {
151
+ ...dynamicHeaders,
152
+ ...payload.headers ?? {}
153
+ };
114
154
  if (token) {
115
155
  headers.Authorization = `Bearer ${token}`;
116
156
  }
@@ -296,10 +336,13 @@ async function request(payload) {
296
336
  case "text":
297
337
  response.body = await fetchResponse.text();
298
338
  break;
339
+ case "blob":
340
+ response.body = await fetchResponse.blob();
341
+ break;
299
342
  default:
300
343
  response.body = await fetchResponse.text();
301
344
  }
302
- if (payload.method === "get" && payload.config.frontendCaching?.enabled && response.body !== void 0) {
345
+ if (payload.method === "get" && payload.config.frontendCaching?.enabled && response.body !== void 0 && responseType !== "blob") {
303
346
  try {
304
347
  await frontendCache.set("get", url, response.body);
305
348
  log(payload, "debug", "cache store successful");
@@ -321,6 +364,15 @@ async function request(payload) {
321
364
  baseUrl: base
322
365
  }
323
366
  );
367
+ if (TRANSIENT_RETRY_STATUS.includes(response.status) && canRetryTransient(payload)) {
368
+ sendMetrics(payload, "transient-retry", {
369
+ duration,
370
+ statusCode: String(fetchResponse.status),
371
+ error: statusText
372
+ });
373
+ await scheduleTransientRetry(payload);
374
+ return request(payload);
375
+ }
324
376
  let type2 = "error";
325
377
  const noErrorStatusCodes = [];
326
378
  switch (payload.metricsDetails.target.name) {
@@ -365,10 +417,6 @@ async function request(payload) {
365
417
  }
366
418
  return response;
367
419
  } catch (error) {
368
- log(payload, "error", `request failed with no status`, {
369
- baseUrl: base,
370
- error
371
- });
372
420
  const duration = performance.now() - start;
373
421
  let errorMessage;
374
422
  if (error?.cause?.code) {
@@ -376,6 +424,23 @@ async function request(payload) {
376
424
  } else if (error?.message) {
377
425
  errorMessage = error?.message;
378
426
  }
427
+ if (canRetryTransient(payload)) {
428
+ log(payload, "debug", `request failed with no status, retrying`, {
429
+ baseUrl: base,
430
+ error
431
+ });
432
+ sendMetrics(payload, "transient-retry", {
433
+ statusCode: "fetch-error",
434
+ duration,
435
+ error: errorMessage
436
+ });
437
+ await scheduleTransientRetry(payload);
438
+ return request(payload);
439
+ }
440
+ log(payload, "error", `request failed with no status`, {
441
+ baseUrl: base,
442
+ error
443
+ });
379
444
  sendMetrics(payload, "error", {
380
445
  statusCode: "fetch-error",
381
446
  duration,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/api.ts"],
4
- "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\nimport type {\n RequestBaseCustomMetricsDetailsType,\n RequestBaseMethodType,\n RequestBaseParams,\n RequestBaseServiceContext,\n} from '../types/general.types.js'\nimport { getEnvParam, type ServiceName } from './environments.js'\nimport { frontendCache } from './frontendCache.js'\n\nexport interface RequestPayload extends RequestBaseParams {\n base?: {\n id: ServiceName\n }\n url: string\n method: RequestBaseMethodType\n headers?: { [key: string]: string }\n type?: 'json' | 'text' | 'form' | 'none'\n responseType?: 'json' | 'text' | 'none'\n retry?: {\n attempts: number\n remaining: number\n }\n body?: any\n metricsDetails: RequestBaseServiceContext\n}\n\nexport interface Response<T> {\n body?: T\n status: number\n statusText?: string\n ok: boolean\n meta: {\n duration: number\n }\n}\n\nfunction sendMetrics(\n payload: RequestPayload,\n type: RequestBaseCustomMetricsDetailsType,\n options?: {\n duration?: number\n statusCode?: string\n error?: string\n },\n) {\n if (payload.config.metrics) {\n if (payload.config.metrics.types.includes(type)) {\n let durationBucket: string | undefined\n if (options?.duration) {\n if (options.duration < 100) {\n durationBucket = '0 | <100ms'\n } else if (options.duration < 200) {\n durationBucket = '1 | <200ms'\n } else if (options.duration < 500) {\n durationBucket = '2 | <500ms'\n } else if (options.duration < 1000) {\n durationBucket = '3 | <1s'\n } else {\n durationBucket = '4 | >=1s'\n }\n }\n\n // only add normalized errors as labels\n let error = 'other'\n if (options?.error) {\n try {\n const errorMessage = options.error\n if (errorMessage.startsWith('<!DOCTYPE html>')) {\n error = 'html-response'\n } else if (errorMessage.includes('UND_ERR_CONNECT_TIMEOUT')) {\n error = 'connect-timeout'\n }\n } catch (_error) {\n // ignore it\n }\n } else {\n error = 'unset'\n }\n\n const metrics: RequestBaseServiceContext = {\n ...payload.metricsDetails,\n eventType: type,\n durationBucket,\n statusCode: options?.statusCode,\n error,\n }\n try {\n payload.config.metrics.callback(metrics)\n } catch (metricsCallbackError) {\n log(payload, 'debug', `Failed to send metrics`, {\n error: metricsCallbackError,\n metrics,\n })\n }\n }\n }\n}\n\nexport function log(\n payload: RequestPayload,\n type: 'debug' | 'info' | 'error',\n message: string,\n additionalData?: any,\n) {\n if (payload.config.logger) {\n const messageParts: string[] = []\n messageParts.push(payload.metricsDetails?.method?.name)\n messageParts.push(\n `${payload.metricsDetails?.target?.name} (${payload.metricsDetails?.target?.type})`,\n )\n messageParts.push(message)\n let body: any\n if (payload.body) {\n body = JSON.parse(JSON.stringify(payload.body))\n for (const key of Object.keys(body)) {\n if (\n key.toLowerCase().includes('password') ||\n key.toLowerCase().includes('secret') ||\n key.toLowerCase().includes('token')\n ) {\n body[key] = '********'\n }\n }\n }\n\n let fullUrl = 'unset'\n\n if (payload.base) {\n try {\n const base = getEnvParam(payload.base.id, payload.config)\n fullUrl = base ? `${base}/${payload.url}` : payload.url\n } catch (_error) {\n fullUrl = `failed to get base url for ${payload.base.id}`\n }\n }\n\n let url = `${payload.url}`.toLowerCase()\n if (url.includes('secret') || url.includes('password')) {\n url = url.split('?')[0]\n }\n\n const data = {\n base: payload.base,\n method: payload.method,\n url,\n fullUrl,\n retry: payload.retry,\n body,\n context: payload.metricsDetails,\n ...additionalData,\n }\n payload.config.logger[type](messageParts.join(' > '), { data })\n }\n}\n\nfunction rateLimitWaitingTime(attempt: number): number {\n const noise = Math.random() * 100\n const waitingTime = 2 ** attempt * 1000 + noise\n return Math.floor(waitingTime)\n}\n\nexport async function request<T>(\n payload: RequestPayload,\n): Promise<Response<T>> {\n log(payload, 'debug', 'starting request')\n sendMetrics(payload, 'request')\n // configure frontend cache if enabled (browser only)\n try {\n if (payload.config.frontendCaching?.enabled) {\n frontendCache.configure(payload.config.frontendCaching)\n }\n } catch (_cacheConfigError) {\n // ignore configuration errors silently\n }\n let token\n if (payload.config.auth) {\n token = await payload.config.auth.getToken()\n }\n const headers: { [key: string]: string } = payload.headers ?? {}\n if (token) {\n headers.Authorization = `Bearer ${token}`\n }\n const type = payload.type ?? 'json'\n let body\n if (type) {\n switch (type) {\n case 'json':\n headers['Content-Type'] = 'application/json'\n body = payload.body ? JSON.stringify(payload.body) : undefined\n break\n case 'text':\n headers['Content-Type'] = 'text/plain'\n body = payload.body ? JSON.stringify(payload.body) : undefined\n break\n case 'form':\n if (payload.body) {\n const formData = new URLSearchParams()\n Object.keys(payload.body).forEach((key) => {\n formData.append(key, payload.body[key])\n })\n body = formData\n }\n break\n }\n }\n let base\n if (payload.base) {\n try {\n base = getEnvParam(payload.base.id, payload.config)\n } catch (error) {\n log(payload, 'error', `Failed to get base url for ${payload.base.id}`, {\n error,\n })\n throw error\n }\n }\n const url = base ? `${base}/${payload.url}` : payload.url\n\n // Attempt cache read for GET requests (stale-while-revalidate when callback supplied)\n let serveCachedAndReturnAfterNetwork = false\n if (payload.method === 'get' && payload.config.frontendCaching?.enabled) {\n try {\n const cached = await frontendCache.get<T>('get', url)\n if (cached) {\n const fresh = frontendCache.isFresh(cached)\n if (payload.onCachedResponse) {\n payload.onCachedResponse(cached.payload, { fresh, source: 'cache' })\n if (!fresh) {\n log(payload, 'debug', 'cache stale, refreshing in background')\n serveCachedAndReturnAfterNetwork = true\n } else {\n log(payload, 'debug', 'cache fresh, returning early')\n sendMetrics(payload, 'success', { duration: 0, statusCode: '200' })\n return {\n // eslint-disable-next-line id-length\n ok: true,\n status: 200,\n statusText: 'cached',\n body: cached.payload,\n meta: { duration: 0 },\n }\n }\n } else if (fresh) {\n // no callback but fresh -> return directly\n log(payload, 'debug', 'cache hit, returning cached response')\n sendMetrics(payload, 'success', { duration: 0, statusCode: '200' })\n return {\n // eslint-disable-next-line id-length\n ok: true,\n status: 200,\n statusText: 'cached',\n body: cached.payload,\n meta: { duration: 0 },\n }\n } else {\n log(payload, 'debug', 'cache stale, proceeding with network request')\n }\n }\n } catch (_cacheGetError) {\n // ignore cache errors\n }\n }\n const start = performance.now()\n const proxyAgent = payload.config.proxyAgent ?? {}\n try {\n const fetchResponse = await fetch(url, {\n signal: payload.config.timeout\n ? AbortSignal.timeout(payload.config.timeout)\n : undefined,\n method: payload.method.toUpperCase(),\n headers,\n body,\n ...proxyAgent,\n })\n const end = performance.now()\n const duration = end - start\n\n if ([429].includes(fetchResponse.status)) {\n // rate limiting, retry\n log(payload, 'info', 'rate limited')\n sendMetrics(payload, 'rate-limit-retry', {\n duration,\n statusCode: String(fetchResponse.status),\n })\n const remaining = payload.retry?.remaining ?? 8\n const attempts = payload.retry?.attempts ?? 1\n if (remaining > 0) {\n let waitingTime = rateLimitWaitingTime(attempts)\n if (fetchResponse.headers.has('x-ratelimit-reset')) {\n const rateLimitHeader = fetchResponse.headers.get('x-ratelimit-reset')\n log(\n payload,\n 'debug',\n `rate limited, found x-ratelimit-reset header: ${rateLimitHeader}`,\n )\n const rateLimitHeaderNumber = Number(rateLimitHeader)\n if (Number.isNaN(rateLimitHeaderNumber)) {\n waitingTime = Date.parse(rateLimitHeader ?? '0') - Date.now()\n } else {\n waitingTime = rateLimitHeaderNumber - Date.now()\n }\n }\n if (waitingTime < 0 || waitingTime > 180000) {\n waitingTime = rateLimitWaitingTime(attempts)\n }\n log(\n payload,\n 'debug',\n `rate limited, attempt ${attempts}, remaining ${remaining}, waiting ${waitingTime}ms`,\n )\n await new Promise((resolve) => {\n setTimeout(resolve, waitingTime)\n })\n // eslint-disable-next-line require-atomic-updates\n payload.retry = {\n attempts: attempts + 1,\n remaining: remaining - 1,\n }\n return request(payload)\n }\n log(payload, 'debug', `rate limited, no more retries`)\n }\n\n const response: Response<T> = {\n // eslint-disable-next-line id-length\n ok: fetchResponse.ok,\n status: fetchResponse.status,\n statusText: fetchResponse.statusText,\n meta: {\n duration,\n },\n }\n\n const responseType = payload.responseType ?? type\n\n if (response.ok) {\n log(payload, 'debug', `request successful in ${Math.floor(duration)}ms`, {\n baseUrl: base,\n })\n sendMetrics(payload, 'success', {\n duration,\n statusCode: String(fetchResponse.status),\n })\n switch (responseType) {\n case 'json':\n try {\n response.body = await fetchResponse.json()\n } catch (_error) {\n log(\n payload,\n 'debug',\n `Failed to parse json body. Returning empty object.`,\n {\n baseUrl: base,\n },\n )\n response.body = {} as T\n try {\n const responseBodyText = await fetchResponse.text()\n log(payload, 'debug', `Response body text`, {\n baseUrl: base,\n responseBodyText,\n status: fetchResponse.status,\n })\n } catch (textError) {\n // this is fine\n log(payload, 'debug', `Failed to parse response body text.`, {\n baseUrl: base,\n status: fetchResponse.status,\n error: textError,\n })\n }\n }\n break\n case 'text':\n response.body = (await fetchResponse.text()) as T\n break\n default:\n response.body = (await fetchResponse.text()) as T\n }\n // store in cache after parsing\n if (\n payload.method === 'get' &&\n payload.config.frontendCaching?.enabled &&\n response.body !== undefined\n ) {\n try {\n await frontendCache.set('get', url, response.body as any)\n log(payload, 'debug', 'cache store successful')\n } catch (_cacheSetError) {\n // ignore cache set error\n }\n }\n } else {\n let statusText = fetchResponse.statusText\n try {\n statusText = await fetchResponse.text()\n } catch (_error) {\n // this is fine\n }\n response.statusText = statusText\n log(\n payload,\n 'debug',\n `request failed with status ${response.status}: ${statusText}`,\n {\n baseUrl: base,\n },\n )\n\n // Special Handling for dedicated target systems to not count special status codes as errors\n // eslint-disable-next-line no-shadow\n let type: RequestBaseCustomMetricsDetailsType = 'error'\n const noErrorStatusCodes: number[] = []\n switch (payload.metricsDetails.target.name) {\n case 'auth0':\n noErrorStatusCodes.push(...[400, 404, 409])\n break\n case 'accounts':\n case 'console':\n noErrorStatusCodes.push(...[404, 409])\n if (payload.metricsDetails.method.name === 'createTemporaryCluster') {\n // case: cannot create temporary cluster\n noErrorStatusCodes.push(...[400, 403, 412])\n }\n if (\n [\n 'ConsoleM2MService.verifyZeebeClient',\n 'ConsoleM2MService.verifyApiClient',\n ].includes(payload.metricsDetails.method.name)\n ) {\n // case: api/zeebe client credentials are invalid\n noErrorStatusCodes.push(...[401])\n }\n if (\n payload.metricsDetails.method.name ===\n 'OrgsService.createAndReturnSocialLoginUser'\n ) {\n noErrorStatusCodes.push(\n ...[\n // onboard user, user already has an org\n 400,\n // existing extSSO-connection\n 412,\n ],\n )\n }\n break\n }\n\n if (noErrorStatusCodes.includes(response.status)) {\n type = 'success'\n }\n\n sendMetrics(payload, type, {\n duration,\n statusCode: String(fetchResponse.status),\n error: statusText,\n })\n }\n if (serveCachedAndReturnAfterNetwork && response.ok) {\n // when we served stale via callback earlier, we now also return the fresh response\n log(payload, 'debug', 'stale cache updated, returning new data')\n }\n return response\n } catch (error: any) {\n log(payload, 'error', `request failed with no status`, {\n baseUrl: base,\n error,\n })\n const duration = performance.now() - start\n let errorMessage: string | undefined\n if (error?.cause?.code) {\n errorMessage = error?.cause?.code\n } else if (error?.message) {\n errorMessage = error?.message\n }\n sendMetrics(payload, 'error', {\n statusCode: 'fetch-error',\n duration,\n error: errorMessage,\n })\n const response: Response<T> = {\n // eslint-disable-next-line id-length\n ok: false,\n status: 500,\n statusText: 'fetch error',\n meta: {\n duration,\n },\n }\n return response\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\nimport type {\n RequestBaseCustomMetricsDetailsType,\n RequestBaseMethodType,\n RequestBaseParams,\n RequestBaseServiceContext,\n} from '../types/general.types.js'\nimport { getEnvParam, type ServiceName } from './environments.js'\nimport { frontendCache } from './frontendCache.js'\n\nexport interface RequestPayload extends RequestBaseParams {\n base?: {\n id: ServiceName\n }\n url: string\n method: RequestBaseMethodType\n headers?: { [key: string]: string }\n type?: 'json' | 'text' | 'form' | 'none'\n responseType?: 'json' | 'text' | 'blob' | 'none'\n retry?: {\n attempts: number\n remaining: number\n }\n /**\n * Opt-in: retry transient gateway failures (502/503/504 and network/fetch\n * errors) with backoff, mirroring the 429 retry. Retried attempts emit a\n * `transient-retry` metric instead of `error`, so a healed blip (e.g. an\n * envoy connection reset during a pod restart) doesn't count toward the\n * internal error-rate alert. Only enable for idempotent-safe calls \u2014 a\n * retried POST may execute twice.\n */\n retryTransientErrors?: boolean\n transientRetry?: {\n attempts: number\n remaining: number\n }\n body?: any\n metricsDetails: RequestBaseServiceContext\n}\n\nconst TRANSIENT_RETRY_STATUS = [502, 503, 504]\nconst MAX_TRANSIENT_RETRIES = 3\n\nexport interface Response<T> {\n body?: T\n status: number\n statusText?: string\n ok: boolean\n meta: {\n duration: number\n }\n}\n\nfunction sendMetrics(\n payload: RequestPayload,\n type: RequestBaseCustomMetricsDetailsType,\n options?: {\n duration?: number\n statusCode?: string\n error?: string\n },\n) {\n if (payload.config.metrics) {\n if (payload.config.metrics.types.includes(type)) {\n let durationBucket: string | undefined\n if (options?.duration) {\n if (options.duration < 100) {\n durationBucket = '0 | <100ms'\n } else if (options.duration < 200) {\n durationBucket = '1 | <200ms'\n } else if (options.duration < 500) {\n durationBucket = '2 | <500ms'\n } else if (options.duration < 1000) {\n durationBucket = '3 | <1s'\n } else {\n durationBucket = '4 | >=1s'\n }\n }\n\n // only add normalized errors as labels\n let error = 'other'\n if (options?.error) {\n try {\n const errorMessage = options.error\n if (errorMessage.startsWith('<!DOCTYPE html>')) {\n error = 'html-response'\n } else if (errorMessage.includes('UND_ERR_CONNECT_TIMEOUT')) {\n error = 'connect-timeout'\n }\n } catch (_error) {\n // ignore it\n }\n } else {\n error = 'unset'\n }\n\n const metrics: RequestBaseServiceContext = {\n ...payload.metricsDetails,\n eventType: type,\n durationBucket,\n statusCode: options?.statusCode,\n error,\n }\n try {\n payload.config.metrics.callback(metrics)\n } catch (metricsCallbackError) {\n log(payload, 'debug', `Failed to send metrics`, {\n error: metricsCallbackError,\n metrics,\n })\n }\n }\n }\n}\n\nexport function log(\n payload: RequestPayload,\n type: 'debug' | 'info' | 'error',\n message: string,\n additionalData?: any,\n) {\n if (payload.config.logger) {\n const messageParts: string[] = []\n messageParts.push(payload.metricsDetails?.method?.name)\n messageParts.push(\n `${payload.metricsDetails?.target?.name} (${payload.metricsDetails?.target?.type})`,\n )\n messageParts.push(message)\n let body: any\n if (payload.body) {\n body = JSON.parse(JSON.stringify(payload.body))\n for (const key of Object.keys(body)) {\n if (\n key.toLowerCase().includes('password') ||\n key.toLowerCase().includes('secret') ||\n key.toLowerCase().includes('token')\n ) {\n body[key] = '********'\n }\n }\n }\n\n let fullUrl = 'unset'\n\n if (payload.base) {\n try {\n const base = getEnvParam(payload.base.id, payload.config)\n fullUrl = base ? `${base}/${payload.url}` : payload.url\n } catch (_error) {\n fullUrl = `failed to get base url for ${payload.base.id}`\n }\n }\n\n let url = `${payload.url}`.toLowerCase()\n if (url.includes('secret') || url.includes('password')) {\n url = url.split('?')[0]\n }\n\n const data = {\n base: payload.base,\n method: payload.method,\n url,\n fullUrl,\n retry: payload.retry,\n body,\n context: payload.metricsDetails,\n ...additionalData,\n }\n payload.config.logger[type](messageParts.join(' > '), { data })\n }\n}\n\nfunction rateLimitWaitingTime(attempt: number): number {\n const noise = Math.random() * 100\n const waitingTime = 2 ** attempt * 1000 + noise\n return Math.floor(waitingTime)\n}\n\nfunction transientRetryWaitingTime(attempt: number): number {\n const noise = Math.random() * 100\n // 500ms, 1s, 2s (+jitter) \u2014 bounded so a fire-and-forget call heals fast.\n return Math.floor(2 ** attempt * 250 + noise)\n}\n\nfunction canRetryTransient(payload: RequestPayload): boolean {\n if (!payload.retryTransientErrors) {\n return false\n }\n const remaining = payload.transientRetry?.remaining ?? MAX_TRANSIENT_RETRIES\n return remaining > 0\n}\n\nasync function scheduleTransientRetry(payload: RequestPayload): Promise<void> {\n const attempts = payload.transientRetry?.attempts ?? 1\n const remaining = payload.transientRetry?.remaining ?? MAX_TRANSIENT_RETRIES\n const waitingTime = transientRetryWaitingTime(attempts)\n log(\n payload,\n 'debug',\n `transient error, attempt ${attempts}, remaining ${remaining}, waiting ${waitingTime}ms`,\n )\n await new Promise((resolve) => {\n setTimeout(resolve, waitingTime)\n })\n // eslint-disable-next-line require-atomic-updates\n payload.transientRetry = {\n attempts: attempts + 1,\n remaining: remaining - 1,\n }\n}\n\nexport async function request<T>(\n payload: RequestPayload,\n): Promise<Response<T>> {\n log(payload, 'debug', 'starting request')\n sendMetrics(payload, 'request')\n // configure frontend cache if enabled (browser only)\n try {\n if (payload.config.frontendCaching?.enabled) {\n frontendCache.configure(payload.config.frontendCaching)\n }\n } catch (_cacheConfigError) {\n // ignore configuration errors silently\n }\n let token\n if (payload.config.auth) {\n token = await payload.config.auth.getToken()\n }\n // Dynamic (e.g. correlation) headers first, then explicit per-request headers\n // override them, then auth/content-type below take final precedence.\n //\n // Correlation headers (traceparent, x-cc-session-id) identify our internal\n // request/session, so they must only ever reach Camunda-owned systems. Every\n // third-party target is marked `type: 'external'`, so we gate on that here:\n // external targets never receive dynamic headers even if the caller's config\n // sets getDynamicHeaders. This makes propagation safe-by-default and removes\n // the footgun of a shared config that is used for both internal and\n // third-party calls (e.g. BASIC_API_CONFIG \u2192 token-issuer vs Slack).\n let dynamicHeaders: Record<string, string> = {}\n if (payload.metricsDetails?.target?.type !== 'external') {\n try {\n dynamicHeaders = payload.config.getDynamicHeaders?.() ?? {}\n } catch (_error) {\n // never let header evaluation break a request\n }\n }\n const headers: { [key: string]: string } = {\n ...dynamicHeaders,\n ...(payload.headers ?? {}),\n }\n if (token) {\n headers.Authorization = `Bearer ${token}`\n }\n const type = payload.type ?? 'json'\n let body\n if (type) {\n switch (type) {\n case 'json':\n headers['Content-Type'] = 'application/json'\n body = payload.body ? JSON.stringify(payload.body) : undefined\n break\n case 'text':\n headers['Content-Type'] = 'text/plain'\n body = payload.body ? JSON.stringify(payload.body) : undefined\n break\n case 'form':\n if (payload.body) {\n const formData = new URLSearchParams()\n Object.keys(payload.body).forEach((key) => {\n formData.append(key, payload.body[key])\n })\n body = formData\n }\n break\n }\n }\n let base\n if (payload.base) {\n try {\n base = getEnvParam(payload.base.id, payload.config)\n } catch (error) {\n log(payload, 'error', `Failed to get base url for ${payload.base.id}`, {\n error,\n })\n throw error\n }\n }\n const url = base ? `${base}/${payload.url}` : payload.url\n\n // Attempt cache read for GET requests (stale-while-revalidate when callback supplied)\n let serveCachedAndReturnAfterNetwork = false\n if (payload.method === 'get' && payload.config.frontendCaching?.enabled) {\n try {\n const cached = await frontendCache.get<T>('get', url)\n if (cached) {\n const fresh = frontendCache.isFresh(cached)\n if (payload.onCachedResponse) {\n payload.onCachedResponse(cached.payload, { fresh, source: 'cache' })\n if (!fresh) {\n log(payload, 'debug', 'cache stale, refreshing in background')\n serveCachedAndReturnAfterNetwork = true\n } else {\n log(payload, 'debug', 'cache fresh, returning early')\n sendMetrics(payload, 'success', { duration: 0, statusCode: '200' })\n return {\n // eslint-disable-next-line id-length\n ok: true,\n status: 200,\n statusText: 'cached',\n body: cached.payload,\n meta: { duration: 0 },\n }\n }\n } else if (fresh) {\n // no callback but fresh -> return directly\n log(payload, 'debug', 'cache hit, returning cached response')\n sendMetrics(payload, 'success', { duration: 0, statusCode: '200' })\n return {\n // eslint-disable-next-line id-length\n ok: true,\n status: 200,\n statusText: 'cached',\n body: cached.payload,\n meta: { duration: 0 },\n }\n } else {\n log(payload, 'debug', 'cache stale, proceeding with network request')\n }\n }\n } catch (_cacheGetError) {\n // ignore cache errors\n }\n }\n const start = performance.now()\n const proxyAgent = payload.config.proxyAgent ?? {}\n try {\n const fetchResponse = await fetch(url, {\n signal: payload.config.timeout\n ? AbortSignal.timeout(payload.config.timeout)\n : undefined,\n method: payload.method.toUpperCase(),\n headers,\n body,\n ...proxyAgent,\n })\n const end = performance.now()\n const duration = end - start\n\n if ([429].includes(fetchResponse.status)) {\n // rate limiting, retry\n log(payload, 'info', 'rate limited')\n sendMetrics(payload, 'rate-limit-retry', {\n duration,\n statusCode: String(fetchResponse.status),\n })\n const remaining = payload.retry?.remaining ?? 8\n const attempts = payload.retry?.attempts ?? 1\n if (remaining > 0) {\n let waitingTime = rateLimitWaitingTime(attempts)\n if (fetchResponse.headers.has('x-ratelimit-reset')) {\n const rateLimitHeader = fetchResponse.headers.get('x-ratelimit-reset')\n log(\n payload,\n 'debug',\n `rate limited, found x-ratelimit-reset header: ${rateLimitHeader}`,\n )\n const rateLimitHeaderNumber = Number(rateLimitHeader)\n if (Number.isNaN(rateLimitHeaderNumber)) {\n waitingTime = Date.parse(rateLimitHeader ?? '0') - Date.now()\n } else {\n waitingTime = rateLimitHeaderNumber - Date.now()\n }\n }\n if (waitingTime < 0 || waitingTime > 180000) {\n waitingTime = rateLimitWaitingTime(attempts)\n }\n log(\n payload,\n 'debug',\n `rate limited, attempt ${attempts}, remaining ${remaining}, waiting ${waitingTime}ms`,\n )\n await new Promise((resolve) => {\n setTimeout(resolve, waitingTime)\n })\n // eslint-disable-next-line require-atomic-updates\n payload.retry = {\n attempts: attempts + 1,\n remaining: remaining - 1,\n }\n return request(payload)\n }\n log(payload, 'debug', `rate limited, no more retries`)\n }\n\n const response: Response<T> = {\n // eslint-disable-next-line id-length\n ok: fetchResponse.ok,\n status: fetchResponse.status,\n statusText: fetchResponse.statusText,\n meta: {\n duration,\n },\n }\n\n const responseType = payload.responseType ?? type\n\n if (response.ok) {\n log(payload, 'debug', `request successful in ${Math.floor(duration)}ms`, {\n baseUrl: base,\n })\n sendMetrics(payload, 'success', {\n duration,\n statusCode: String(fetchResponse.status),\n })\n switch (responseType) {\n case 'json':\n try {\n response.body = await fetchResponse.json()\n } catch (_error) {\n log(\n payload,\n 'debug',\n `Failed to parse json body. Returning empty object.`,\n {\n baseUrl: base,\n },\n )\n response.body = {} as T\n try {\n const responseBodyText = await fetchResponse.text()\n log(payload, 'debug', `Response body text`, {\n baseUrl: base,\n responseBodyText,\n status: fetchResponse.status,\n })\n } catch (textError) {\n // this is fine\n log(payload, 'debug', `Failed to parse response body text.`, {\n baseUrl: base,\n status: fetchResponse.status,\n error: textError,\n })\n }\n }\n break\n case 'text':\n response.body = (await fetchResponse.text()) as T\n break\n case 'blob':\n // Binary payloads (file downloads, e.g. database dumps). Not cached.\n response.body = (await fetchResponse.blob()) as T\n break\n default:\n response.body = (await fetchResponse.text()) as T\n }\n // store in cache after parsing\n if (\n payload.method === 'get' &&\n payload.config.frontendCaching?.enabled &&\n response.body !== undefined &&\n responseType !== 'blob'\n ) {\n try {\n await frontendCache.set('get', url, response.body as any)\n log(payload, 'debug', 'cache store successful')\n } catch (_cacheSetError) {\n // ignore cache set error\n }\n }\n } else {\n let statusText = fetchResponse.statusText\n try {\n statusText = await fetchResponse.text()\n } catch (_error) {\n // this is fine\n }\n response.statusText = statusText\n log(\n payload,\n 'debug',\n `request failed with status ${response.status}: ${statusText}`,\n {\n baseUrl: base,\n },\n )\n\n // Transient gateway failure (e.g. envoy \"connection termination\" 503\n // during a pod restart). When opted in, retry with backoff and record it\n // as `transient-retry` rather than `error`, so a healed blip stays off\n // the internal error-rate alert.\n if (\n TRANSIENT_RETRY_STATUS.includes(response.status) &&\n canRetryTransient(payload)\n ) {\n sendMetrics(payload, 'transient-retry', {\n duration,\n statusCode: String(fetchResponse.status),\n error: statusText,\n })\n await scheduleTransientRetry(payload)\n return request(payload)\n }\n\n // Special Handling for dedicated target systems to not count special status codes as errors\n // eslint-disable-next-line no-shadow\n let type: RequestBaseCustomMetricsDetailsType = 'error'\n const noErrorStatusCodes: number[] = []\n switch (payload.metricsDetails.target.name) {\n case 'auth0':\n noErrorStatusCodes.push(...[400, 404, 409])\n break\n case 'accounts':\n case 'console':\n noErrorStatusCodes.push(...[404, 409])\n if (payload.metricsDetails.method.name === 'createTemporaryCluster') {\n // case: cannot create temporary cluster\n noErrorStatusCodes.push(...[400, 403, 412])\n }\n if (\n [\n 'ConsoleM2MService.verifyZeebeClient',\n 'ConsoleM2MService.verifyApiClient',\n ].includes(payload.metricsDetails.method.name)\n ) {\n // case: api/zeebe client credentials are invalid\n noErrorStatusCodes.push(...[401])\n }\n if (\n payload.metricsDetails.method.name ===\n 'OrgsService.createAndReturnSocialLoginUser'\n ) {\n noErrorStatusCodes.push(\n ...[\n // onboard user, user already has an org\n 400,\n // existing extSSO-connection\n 412,\n ],\n )\n }\n break\n }\n\n if (noErrorStatusCodes.includes(response.status)) {\n type = 'success'\n }\n\n sendMetrics(payload, type, {\n duration,\n statusCode: String(fetchResponse.status),\n error: statusText,\n })\n }\n if (serveCachedAndReturnAfterNetwork && response.ok) {\n // when we served stale via callback earlier, we now also return the fresh response\n log(payload, 'debug', 'stale cache updated, returning new data')\n }\n return response\n } catch (error: any) {\n const duration = performance.now() - start\n let errorMessage: string | undefined\n if (error?.cause?.code) {\n errorMessage = error?.cause?.code\n } else if (error?.message) {\n errorMessage = error?.message\n }\n // Network/fetch error with no HTTP status (e.g. connection reset, timeout).\n // Retry-on-opt-in before treating it as an error (see else branch above).\n if (canRetryTransient(payload)) {\n log(payload, 'debug', `request failed with no status, retrying`, {\n baseUrl: base,\n error,\n })\n sendMetrics(payload, 'transient-retry', {\n statusCode: 'fetch-error',\n duration,\n error: errorMessage,\n })\n await scheduleTransientRetry(payload)\n return request(payload)\n }\n log(payload, 'error', `request failed with no status`, {\n baseUrl: base,\n error,\n })\n sendMetrics(payload, 'error', {\n statusCode: 'fetch-error',\n duration,\n error: errorMessage,\n })\n const response: Response<T> = {\n // eslint-disable-next-line id-length\n ok: false,\n status: 500,\n statusText: 'fetch error',\n meta: {\n duration,\n },\n }\n return response\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["type"]
7
7
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ import type { RequestBaseParamsConfig } from '../types/general.types.js';
2
+ /**
3
+ * Dependency-free Server-Sent Events client built on `fetch` + `ReadableStream`.
4
+ *
5
+ * Why not the native `EventSource`? It cannot send an `Authorization` header,
6
+ * which every Camunda stream requires. This helper reuses the same
7
+ * `config.auth.getToken()` contract as `request()`, so it always sends a fresh
8
+ * bearer token — including on every reconnect, which is how token rotation is
9
+ * handled (no separate refresh timer needed on the consumer side).
10
+ *
11
+ * It is isomorphic (browser + Node 22+), auto-reconnects with exponential
12
+ * backoff + full jitter, honours `Last-Event-ID`, and aborts+reconnects when
13
+ * the stream goes silent past `heartbeatTimeoutMs` (comment/keep-alive frames
14
+ * count as activity, so a well-behaved server keeps the connection pinned).
15
+ */
16
+ export type SseConnectionState = 'connecting' | 'open' | 'reconnecting' | 'closed';
17
+ export interface SseMessage {
18
+ /** Concatenated `data:` lines (newline-joined), excluding the trailing newline. */
19
+ data: string;
20
+ /** The `event:` field, if the server set one. */
21
+ event?: string;
22
+ /** The `id:` field, if the server set one. Echoed back as `Last-Event-ID`. */
23
+ id?: string;
24
+ }
25
+ export interface SseConnectionOptions {
26
+ /** Absolute URL of the event stream. */
27
+ url: string;
28
+ /** Reuses `auth.getToken` (called per (re)connect) and `logger`. */
29
+ config: RequestBaseParamsConfig;
30
+ /** Called for each complete, non-empty SSE event (comment-only frames excluded). */
31
+ onMessage: (message: SseMessage) => void;
32
+ /** Observability hook for connection lifecycle transitions. */
33
+ onStateChange?: (state: SseConnectionState) => void;
34
+ /**
35
+ * Abort + reconnect when no bytes (including comments / keep-alives) arrive
36
+ * for this long. Default 120_000 ms.
37
+ */
38
+ heartbeatTimeoutMs?: number;
39
+ /** Backoff tuning. Defaults: base 1_000 ms, max 30_000 ms, full jitter. */
40
+ reconnect?: {
41
+ baseDelayMs?: number;
42
+ maxDelayMs?: number;
43
+ };
44
+ }
45
+ export interface SseConnection {
46
+ /** Idempotent. Aborts any in-flight fetch, cancels timers, emits no further events. */
47
+ close: () => void;
48
+ /** Current lifecycle state. */
49
+ readonly state: SseConnectionState;
50
+ }
51
+ export declare function connectSse(options: SseConnectionOptions): SseConnection;
@@ -0,0 +1,254 @@
1
+ const DEFAULT_HEARTBEAT_TIMEOUT_MS = 12e4;
2
+ const DEFAULT_BASE_DELAY_MS = 1e3;
3
+ const DEFAULT_MAX_DELAY_MS = 3e4;
4
+ class SseParser {
5
+ constructor(onMessage, lastEventId) {
6
+ this.onMessage = onMessage;
7
+ this.buffer = "";
8
+ this.dataLines = [];
9
+ this.lastEventId = lastEventId;
10
+ }
11
+ getLastEventId() {
12
+ return this.lastEventId;
13
+ }
14
+ /** Feed a decoded text chunk. Returns nothing; dispatches via `onMessage`. */
15
+ push(chunk) {
16
+ this.buffer += chunk;
17
+ this.buffer = this.buffer.replace(/\r\n?/g, "\n");
18
+ let newlineIndex = this.buffer.indexOf("\n");
19
+ while (newlineIndex !== -1) {
20
+ const line = this.buffer.slice(0, newlineIndex);
21
+ this.buffer = this.buffer.slice(newlineIndex + 1);
22
+ this.handleLine(line);
23
+ newlineIndex = this.buffer.indexOf("\n");
24
+ }
25
+ }
26
+ handleLine(line) {
27
+ if (line === "") {
28
+ this.dispatch();
29
+ return;
30
+ }
31
+ if (line.startsWith(":")) {
32
+ return;
33
+ }
34
+ const colonIndex = line.indexOf(":");
35
+ const field = colonIndex === -1 ? line : line.slice(0, colonIndex);
36
+ let value = colonIndex === -1 ? "" : line.slice(colonIndex + 1);
37
+ if (value.startsWith(" ")) {
38
+ value = value.slice(1);
39
+ }
40
+ switch (field) {
41
+ case "data":
42
+ this.dataLines.push(value);
43
+ break;
44
+ case "event":
45
+ this.eventName = value;
46
+ break;
47
+ case "id":
48
+ if (!value.includes("\0")) {
49
+ this.lastEventId = value;
50
+ }
51
+ break;
52
+ // `retry:` and unknown fields are intentionally ignored.
53
+ default:
54
+ break;
55
+ }
56
+ }
57
+ dispatch() {
58
+ if (this.dataLines.length === 0 && this.eventName === void 0) {
59
+ this.reset();
60
+ return;
61
+ }
62
+ this.onMessage({
63
+ data: this.dataLines.join("\n"),
64
+ event: this.eventName,
65
+ id: this.lastEventId
66
+ });
67
+ this.reset();
68
+ }
69
+ reset() {
70
+ this.dataLines = [];
71
+ this.eventName = void 0;
72
+ }
73
+ }
74
+ function computeBackoff(attempt, baseDelayMs, maxDelayMs) {
75
+ const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
76
+ return Math.floor(Math.random() * exponential);
77
+ }
78
+ function connectSse(options) {
79
+ const heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
80
+ const baseDelayMs = options.reconnect?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
81
+ const maxDelayMs = options.reconnect?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
82
+ const logger = options.config.logger;
83
+ let state = "connecting";
84
+ let closed = false;
85
+ let attempt = 0;
86
+ let lastEventId;
87
+ let controller;
88
+ let heartbeatTimer;
89
+ let reconnectTimer;
90
+ let authRetryUsed = false;
91
+ const setState = (next) => {
92
+ if (state === next || closed) {
93
+ return;
94
+ }
95
+ state = next;
96
+ try {
97
+ options.onStateChange?.(next);
98
+ } catch (error) {
99
+ logger?.debug("connectSse onStateChange callback threw", { error });
100
+ }
101
+ };
102
+ const clearHeartbeat = () => {
103
+ if (heartbeatTimer !== void 0) {
104
+ clearTimeout(heartbeatTimer);
105
+ heartbeatTimer = void 0;
106
+ }
107
+ };
108
+ const armHeartbeat = () => {
109
+ clearHeartbeat();
110
+ heartbeatTimer = setTimeout(() => {
111
+ logger?.debug("connectSse heartbeat timeout, reconnecting", {
112
+ url: options.url
113
+ });
114
+ controller?.abort();
115
+ }, heartbeatTimeoutMs);
116
+ };
117
+ const scheduleReconnect = () => {
118
+ if (closed) {
119
+ return;
120
+ }
121
+ if (reconnectTimer !== void 0) {
122
+ return;
123
+ }
124
+ setState("reconnecting");
125
+ const delay = computeBackoff(attempt, baseDelayMs, maxDelayMs);
126
+ attempt += 1;
127
+ reconnectTimer = setTimeout(() => {
128
+ reconnectTimer = void 0;
129
+ void run();
130
+ }, delay);
131
+ };
132
+ const run = async () => {
133
+ if (closed) {
134
+ return;
135
+ }
136
+ controller = new AbortController();
137
+ let token;
138
+ try {
139
+ token = options.config.auth ? await options.config.auth.getToken() : void 0;
140
+ } catch (error) {
141
+ logger?.debug("connectSse failed to get token", { error });
142
+ scheduleReconnect();
143
+ return;
144
+ }
145
+ if (closed) {
146
+ return;
147
+ }
148
+ const headers = {
149
+ accept: "text/event-stream"
150
+ };
151
+ if (options.config.getDynamicHeaders) {
152
+ try {
153
+ Object.assign(headers, options.config.getDynamicHeaders());
154
+ } catch (error) {
155
+ logger?.debug("connectSse getDynamicHeaders threw", { error });
156
+ }
157
+ }
158
+ if (token) {
159
+ headers.authorization = `Bearer ${token}`;
160
+ }
161
+ if (lastEventId !== void 0) {
162
+ headers["last-event-id"] = lastEventId;
163
+ }
164
+ const parser = new SseParser(options.onMessage, lastEventId);
165
+ try {
166
+ const response = await fetch(options.url, {
167
+ method: "GET",
168
+ headers,
169
+ signal: controller.signal,
170
+ ...options.config.proxyAgent ?? {}
171
+ });
172
+ if (!response.ok) {
173
+ if ((response.status === 401 || response.status === 403) && !authRetryUsed) {
174
+ authRetryUsed = true;
175
+ logger?.debug(
176
+ "connectSse auth error, retrying once with fresh token",
177
+ {
178
+ status: response.status
179
+ }
180
+ );
181
+ setState("reconnecting");
182
+ void run();
183
+ return;
184
+ }
185
+ logger?.debug("connectSse non-ok response", { status: response.status });
186
+ scheduleReconnect();
187
+ return;
188
+ }
189
+ if (!response.body) {
190
+ logger?.debug("connectSse response has no body");
191
+ scheduleReconnect();
192
+ return;
193
+ }
194
+ attempt = 0;
195
+ authRetryUsed = false;
196
+ setState("open");
197
+ armHeartbeat();
198
+ const reader = response.body.getReader();
199
+ const decoder = new TextDecoder();
200
+ while (!closed) {
201
+ const { done, value } = await reader.read();
202
+ if (done) {
203
+ break;
204
+ }
205
+ armHeartbeat();
206
+ parser.push(decoder.decode(value, { stream: true }));
207
+ lastEventId = parser.getLastEventId();
208
+ }
209
+ if (!closed) {
210
+ scheduleReconnect();
211
+ }
212
+ } catch (error) {
213
+ if (closed) {
214
+ return;
215
+ }
216
+ logger?.debug("connectSse stream error, reconnecting", { error });
217
+ scheduleReconnect();
218
+ } finally {
219
+ clearHeartbeat();
220
+ }
221
+ };
222
+ const close = () => {
223
+ if (closed) {
224
+ return;
225
+ }
226
+ closed = true;
227
+ clearHeartbeat();
228
+ if (reconnectTimer !== void 0) {
229
+ clearTimeout(reconnectTimer);
230
+ reconnectTimer = void 0;
231
+ }
232
+ controller?.abort();
233
+ setStateClosed();
234
+ };
235
+ function setStateClosed() {
236
+ state = "closed";
237
+ try {
238
+ options.onStateChange?.("closed");
239
+ } catch (error) {
240
+ logger?.debug("connectSse onStateChange callback threw", { error });
241
+ }
242
+ }
243
+ void run();
244
+ return {
245
+ close,
246
+ get state() {
247
+ return state;
248
+ }
249
+ };
250
+ }
251
+ export {
252
+ connectSse
253
+ };
254
+ //# sourceMappingURL=sse.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/utils/sse.ts"],
4
+ "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\nimport type { RequestBaseParamsConfig } from '../types/general.types.js'\n\n/**\n * Dependency-free Server-Sent Events client built on `fetch` + `ReadableStream`.\n *\n * Why not the native `EventSource`? It cannot send an `Authorization` header,\n * which every Camunda stream requires. This helper reuses the same\n * `config.auth.getToken()` contract as `request()`, so it always sends a fresh\n * bearer token \u2014 including on every reconnect, which is how token rotation is\n * handled (no separate refresh timer needed on the consumer side).\n *\n * It is isomorphic (browser + Node 22+), auto-reconnects with exponential\n * backoff + full jitter, honours `Last-Event-ID`, and aborts+reconnects when\n * the stream goes silent past `heartbeatTimeoutMs` (comment/keep-alive frames\n * count as activity, so a well-behaved server keeps the connection pinned).\n */\n\nexport type SseConnectionState =\n | 'connecting'\n | 'open'\n | 'reconnecting'\n | 'closed'\n\nexport interface SseMessage {\n /** Concatenated `data:` lines (newline-joined), excluding the trailing newline. */\n data: string\n /** The `event:` field, if the server set one. */\n event?: string\n /** The `id:` field, if the server set one. Echoed back as `Last-Event-ID`. */\n id?: string\n}\n\nexport interface SseConnectionOptions {\n /** Absolute URL of the event stream. */\n url: string\n /** Reuses `auth.getToken` (called per (re)connect) and `logger`. */\n config: RequestBaseParamsConfig\n /** Called for each complete, non-empty SSE event (comment-only frames excluded). */\n onMessage: (message: SseMessage) => void\n /** Observability hook for connection lifecycle transitions. */\n onStateChange?: (state: SseConnectionState) => void\n /**\n * Abort + reconnect when no bytes (including comments / keep-alives) arrive\n * for this long. Default 120_000 ms.\n */\n heartbeatTimeoutMs?: number\n /** Backoff tuning. Defaults: base 1_000 ms, max 30_000 ms, full jitter. */\n reconnect?: {\n baseDelayMs?: number\n maxDelayMs?: number\n }\n}\n\nexport interface SseConnection {\n /** Idempotent. Aborts any in-flight fetch, cancels timers, emits no further events. */\n close: () => void\n /** Current lifecycle state. */\n readonly state: SseConnectionState\n}\n\nconst DEFAULT_HEARTBEAT_TIMEOUT_MS = 120_000\nconst DEFAULT_BASE_DELAY_MS = 1_000\nconst DEFAULT_MAX_DELAY_MS = 30_000\n\n/**\n * Incrementally parses a `text/event-stream` byte stream into complete events.\n * Events are separated by a blank line; `data:` lines accumulate; `event:`/`id:`\n * set the corresponding fields; lines starting with `:` are comments (ignored,\n * but still count as stream activity for the heartbeat).\n */\nclass SseParser {\n private buffer = ''\n private dataLines: string[] = []\n private eventName: string | undefined\n private lastEventId: string | undefined\n\n constructor(\n private readonly onMessage: (message: SseMessage) => void,\n /** Seed used to resume: exposed back so the connection can send Last-Event-ID. */\n lastEventId: string | undefined,\n ) {\n this.lastEventId = lastEventId\n }\n\n getLastEventId(): string | undefined {\n return this.lastEventId\n }\n\n /** Feed a decoded text chunk. Returns nothing; dispatches via `onMessage`. */\n push(chunk: string): void {\n this.buffer += chunk\n // Normalize CRLF / CR to LF so line splitting is uniform.\n this.buffer = this.buffer.replace(/\\r\\n?/g, '\\n')\n\n let newlineIndex = this.buffer.indexOf('\\n')\n while (newlineIndex !== -1) {\n const line = this.buffer.slice(0, newlineIndex)\n this.buffer = this.buffer.slice(newlineIndex + 1)\n this.handleLine(line)\n newlineIndex = this.buffer.indexOf('\\n')\n }\n }\n\n private handleLine(line: string): void {\n // Blank line: dispatch the accumulated event.\n if (line === '') {\n this.dispatch()\n return\n }\n\n // Comment line (keep-alive) \u2014 ignored.\n if (line.startsWith(':')) {\n return\n }\n\n const colonIndex = line.indexOf(':')\n const field = colonIndex === -1 ? line : line.slice(0, colonIndex)\n let value = colonIndex === -1 ? '' : line.slice(colonIndex + 1)\n // A single leading space after the colon is stripped, per the spec.\n if (value.startsWith(' ')) {\n value = value.slice(1)\n }\n\n switch (field) {\n case 'data':\n this.dataLines.push(value)\n break\n case 'event':\n this.eventName = value\n break\n case 'id':\n // Per spec, an id containing NUL is ignored; otherwise it updates the\n // last-event-id used on reconnect.\n if (!value.includes('\\u0000')) {\n this.lastEventId = value\n }\n break\n // `retry:` and unknown fields are intentionally ignored.\n default:\n break\n }\n }\n\n private dispatch(): void {\n if (this.dataLines.length === 0 && this.eventName === undefined) {\n // Nothing to dispatch (e.g. stray blank line).\n this.reset()\n return\n }\n\n this.onMessage({\n data: this.dataLines.join('\\n'),\n event: this.eventName,\n id: this.lastEventId,\n })\n this.reset()\n }\n\n private reset(): void {\n this.dataLines = []\n this.eventName = undefined\n }\n}\n\nfunction computeBackoff(\n attempt: number,\n baseDelayMs: number,\n maxDelayMs: number,\n): number {\n const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt)\n // Full jitter: pick uniformly in [0, exponential].\n return Math.floor(Math.random() * exponential)\n}\n\nexport function connectSse(options: SseConnectionOptions): SseConnection {\n const heartbeatTimeoutMs =\n options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS\n const baseDelayMs = options.reconnect?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS\n const maxDelayMs = options.reconnect?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS\n const logger = options.config.logger\n\n let state: SseConnectionState = 'connecting'\n let closed = false\n let attempt = 0\n let lastEventId: string | undefined\n let controller: AbortController | undefined\n let heartbeatTimer: ReturnType<typeof setTimeout> | undefined\n let reconnectTimer: ReturnType<typeof setTimeout> | undefined\n // Once we've seen a 401/403 we allow exactly one immediate fresh-token retry\n // before falling back to normal backoff, so a transient auth blip self-heals\n // without hammering the token endpoint.\n let authRetryUsed = false\n\n const setState = (next: SseConnectionState): void => {\n if (state === next || closed) {\n return\n }\n state = next\n try {\n options.onStateChange?.(next)\n } catch (error) {\n logger?.debug('connectSse onStateChange callback threw', { error })\n }\n }\n\n const clearHeartbeat = (): void => {\n if (heartbeatTimer !== undefined) {\n clearTimeout(heartbeatTimer)\n heartbeatTimer = undefined\n }\n }\n\n const armHeartbeat = (): void => {\n clearHeartbeat()\n heartbeatTimer = setTimeout(() => {\n logger?.debug('connectSse heartbeat timeout, reconnecting', {\n url: options.url,\n })\n // Abort the current fetch; the read loop's catch schedules the reconnect.\n controller?.abort()\n }, heartbeatTimeoutMs)\n }\n\n const scheduleReconnect = (): void => {\n if (closed) {\n return\n }\n // Guard against stacking timers: multiple error paths (heartbeat abort +\n // stream end, etc.) can each call scheduleReconnect before the pending\n // timer fires. Without this, overlapping timers spawn concurrent run()\n // loops (and concurrent fetches).\n if (reconnectTimer !== undefined) {\n return\n }\n setState('reconnecting')\n const delay = computeBackoff(attempt, baseDelayMs, maxDelayMs)\n attempt += 1\n reconnectTimer = setTimeout(() => {\n reconnectTimer = undefined\n void run()\n }, delay)\n }\n\n const run = async (): Promise<void> => {\n if (closed) {\n return\n }\n\n controller = new AbortController()\n\n let token: string | undefined\n try {\n token = options.config.auth\n ? await options.config.auth.getToken()\n : undefined\n } catch (error) {\n logger?.debug('connectSse failed to get token', { error })\n scheduleReconnect()\n return\n }\n\n if (closed) {\n return\n }\n\n const headers: Record<string, string> = {\n accept: 'text/event-stream',\n }\n // Correlation headers (traceparent, x-cc-session-id) so the SSE connection\n // is traceable and grouped into the session, mirroring request(). A\n // throwing getDynamicHeaders is swallowed \u2014 the stream still connects.\n if (options.config.getDynamicHeaders) {\n try {\n Object.assign(headers, options.config.getDynamicHeaders())\n } catch (error) {\n logger?.debug('connectSse getDynamicHeaders threw', { error })\n }\n }\n if (token) {\n headers.authorization = `Bearer ${token}`\n }\n if (lastEventId !== undefined) {\n headers['last-event-id'] = lastEventId\n }\n\n const parser = new SseParser(options.onMessage, lastEventId)\n\n try {\n const response = await fetch(options.url, {\n method: 'GET',\n headers,\n signal: controller.signal,\n ...(options.config.proxyAgent ?? {}),\n })\n\n if (!response.ok) {\n // Auth blip: retry once immediately with a freshly minted token before\n // falling back to backoff.\n if (\n (response.status === 401 || response.status === 403) &&\n !authRetryUsed\n ) {\n authRetryUsed = true\n logger?.debug(\n 'connectSse auth error, retrying once with fresh token',\n {\n status: response.status,\n },\n )\n setState('reconnecting')\n void run()\n return\n }\n logger?.debug('connectSse non-ok response', { status: response.status })\n scheduleReconnect()\n return\n }\n\n if (!response.body) {\n logger?.debug('connectSse response has no body')\n scheduleReconnect()\n return\n }\n\n // Connected successfully: reset backoff + auth-retry budget.\n attempt = 0\n authRetryUsed = false\n setState('open')\n armHeartbeat()\n\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n\n while (!closed) {\n const { done, value } = await reader.read()\n if (done) {\n break\n }\n // Any bytes (incl. comments) count as liveness.\n armHeartbeat()\n parser.push(decoder.decode(value, { stream: true }))\n lastEventId = parser.getLastEventId()\n }\n\n // Stream ended cleanly (server closed): reconnect unless we're closing.\n if (!closed) {\n scheduleReconnect()\n }\n } catch (error) {\n if (closed) {\n return\n }\n // AbortError from close() is handled by the `closed` guard above; an\n // AbortError from the heartbeat means we intentionally dropped a silent\n // connection and should reconnect.\n logger?.debug('connectSse stream error, reconnecting', { error })\n scheduleReconnect()\n } finally {\n clearHeartbeat()\n }\n }\n\n const close = (): void => {\n if (closed) {\n return\n }\n closed = true\n clearHeartbeat()\n if (reconnectTimer !== undefined) {\n clearTimeout(reconnectTimer)\n reconnectTimer = undefined\n }\n controller?.abort()\n setStateClosed()\n }\n\n // `setState` short-circuits once `closed` is true, so set the terminal state\n // through a dedicated path.\n function setStateClosed(): void {\n state = 'closed'\n try {\n options.onStateChange?.('closed')\n } catch (error) {\n logger?.debug('connectSse onStateChange callback threw', { error })\n }\n }\n\n void run()\n\n return {\n close,\n get state() {\n return state\n },\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Generates a fresh W3C `traceparent` header value
3
+ * (`00-<32 hex trace id>-<16 hex span id>-01`).
4
+ *
5
+ * Isomorphic: uses the Web Crypto API available on both the browser and Node
6
+ * 18+ (`globalThis.crypto`). Intended for frontends to start a trace at the
7
+ * request boundary so backends adopt it (see SessionMiddleware) — each call
8
+ * mints a new trace, i.e. one trace per outgoing request.
9
+ */
10
+ export declare function generateTraceparent(): string;
11
+ /** Convenience: the `{ traceparent }` header object for a config's getDynamicHeaders. */
12
+ export declare function traceparentHeaders(): Record<string, string>;
13
+ /**
14
+ * A stable per-tab session id, minted once and cached in `sessionStorage` so it
15
+ * survives reloads and in-tab navigation but is distinct per tab. Unlike a
16
+ * traceparent (one per request), this groups every request of one browser
17
+ * session under a single `sid` on the backend. Falls back to a per-process id
18
+ * when `sessionStorage` is unavailable (SSR, tests, sandboxed iframe, privacy
19
+ * mode) so it never throws.
20
+ */
21
+ export declare function getSessionId(): string;
22
+ /** Convenience: the `{ 'x-cc-session-id' }` header object for the per-tab session. */
23
+ export declare function sessionHeaders(): Record<string, string>;
24
+ /**
25
+ * Correlation headers for a frontend request: a fresh `traceparent` (one trace
26
+ * per request, which the backend adopts) plus the stable per-tab
27
+ * `x-cc-session-id`. Wire as a config's `getDynamicHeaders` so the backend can
28
+ * both follow a single action (trace) and group a browser session (sid).
29
+ */
30
+ export declare function correlationHeaders(): Record<string, string>;