@camunda/ccma-api 0.3.0 → 0.5.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 (53) hide show
  1. package/dist/index.d.ts +8 -6
  2. package/dist/index.js +8 -0
  3. package/dist/index.js.map +2 -2
  4. package/dist/services/accounts/bug-report.service.d.ts +28 -0
  5. package/dist/services/accounts/bug-report.service.js +44 -0
  6. package/dist/services/accounts/bug-report.service.js.map +7 -0
  7. package/dist/services/accounts/hdp.service.d.ts +8 -1
  8. package/dist/services/accounts/hdp.service.js +33 -0
  9. package/dist/services/accounts/hdp.service.js.map +2 -2
  10. package/dist/services/accounts/orgs.service.d.ts +4 -16
  11. package/dist/services/accounts/orgs.service.js.map +2 -2
  12. package/dist/services/accounts/orgs.v2.service.d.ts +7 -0
  13. package/dist/services/accounts/orgs.v2.service.js +35 -0
  14. package/dist/services/accounts/orgs.v2.service.js.map +2 -2
  15. package/dist/services/audit-logger/audit-logger.system.service.d.ts +6 -0
  16. package/dist/services/audit-logger/audit-logger.system.service.js +3 -0
  17. package/dist/services/audit-logger/audit-logger.system.service.js.map +2 -2
  18. package/dist/services/console/console.admin.service.d.ts +24 -1
  19. package/dist/services/console/console.admin.service.js +72 -0
  20. package/dist/services/console/console.admin.service.js.map +2 -2
  21. package/dist/services/console/console.cluster.v2.service.d.ts +3 -3
  22. package/dist/services/console/console.cluster.v2.service.js +20 -7
  23. package/dist/services/console/console.cluster.v2.service.js.map +2 -2
  24. package/dist/services/console/console.clusterplan.service.d.ts +7 -0
  25. package/dist/services/console/console.clusterplan.service.js +33 -0
  26. package/dist/services/console/console.clusterplan.service.js.map +2 -2
  27. package/dist/services/console/console.hotbackups.service.d.ts +22 -0
  28. package/dist/services/console/console.hotbackups.service.js +85 -0
  29. package/dist/services/console/console.hotbackups.service.js.map +2 -2
  30. package/dist/services/console/console.hotbackups.service.spec.d.ts +1 -0
  31. package/dist/services/console/generation.service.d.ts +22 -1
  32. package/dist/services/console/generation.service.js +99 -0
  33. package/dist/services/console/generation.service.js.map +2 -2
  34. package/dist/services/hibob/hibob.service.d.ts +1 -1
  35. package/dist/services/hibob/hibob.service.js.map +1 -1
  36. package/dist/services/litellm/litellm.service.d.ts +30 -0
  37. package/dist/services/litellm/litellm.service.js +42 -0
  38. package/dist/services/litellm/litellm.service.js.map +2 -2
  39. package/dist/services/notifications/notifications.service.js +6 -1
  40. package/dist/services/notifications/notifications.service.js.map +2 -2
  41. package/dist/types/general.types.d.ts +1 -1
  42. package/dist/utils/api.d.ts +13 -0
  43. package/dist/utils/api.js +71 -11
  44. package/dist/utils/api.js.map +2 -2
  45. package/dist/utils/api.log-duration.spec.d.ts +1 -0
  46. package/dist/utils/api.request-headers.spec.d.ts +1 -0
  47. package/dist/utils/api.transient-retry.spec.d.ts +1 -0
  48. package/dist/utils/sse.js +7 -0
  49. package/dist/utils/sse.js.map +2 -2
  50. package/dist/utils/traceparent.d.ts +18 -0
  51. package/dist/utils/traceparent.js +30 -0
  52. package/dist/utils/traceparent.js.map +2 -2
  53. 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");
@@ -111,9 +141,11 @@ async function request(payload) {
111
141
  token = await payload.config.auth.getToken();
112
142
  }
113
143
  let dynamicHeaders = {};
114
- try {
115
- dynamicHeaders = payload.config.getDynamicHeaders?.() ?? {};
116
- } catch (_error) {
144
+ if (payload.metricsDetails?.target?.type !== "external") {
145
+ try {
146
+ dynamicHeaders = payload.config.getDynamicHeaders?.() ?? {};
147
+ } catch (_error) {
148
+ }
117
149
  }
118
150
  const headers = {
119
151
  ...dynamicHeaders,
@@ -211,7 +243,7 @@ async function request(payload) {
211
243
  const end = performance.now();
212
244
  const duration = end - start;
213
245
  if ([429].includes(fetchResponse.status)) {
214
- log(payload, "info", "rate limited");
246
+ log(payload, "info", "rate limited", { durationMs: Math.floor(duration) });
215
247
  sendMetrics(payload, "rate-limit-retry", {
216
248
  duration,
217
249
  statusCode: String(fetchResponse.status)
@@ -251,7 +283,9 @@ async function request(payload) {
251
283
  };
252
284
  return request(payload);
253
285
  }
254
- log(payload, "debug", `rate limited, no more retries`);
286
+ log(payload, "debug", `rate limited, no more retries`, {
287
+ durationMs: Math.floor(duration)
288
+ });
255
289
  }
256
290
  const response = {
257
291
  // eslint-disable-next-line id-length
@@ -265,7 +299,8 @@ async function request(payload) {
265
299
  const responseType = payload.responseType ?? type;
266
300
  if (response.ok) {
267
301
  log(payload, "debug", `request successful in ${Math.floor(duration)}ms`, {
268
- baseUrl: base
302
+ baseUrl: base,
303
+ durationMs: Math.floor(duration)
269
304
  });
270
305
  sendMetrics(payload, "success", {
271
306
  duration,
@@ -329,9 +364,19 @@ async function request(payload) {
329
364
  "debug",
330
365
  `request failed with status ${response.status}: ${statusText}`,
331
366
  {
332
- baseUrl: base
367
+ baseUrl: base,
368
+ durationMs: Math.floor(duration)
333
369
  }
334
370
  );
371
+ if (TRANSIENT_RETRY_STATUS.includes(response.status) && canRetryTransient(payload)) {
372
+ sendMetrics(payload, "transient-retry", {
373
+ duration,
374
+ statusCode: String(fetchResponse.status),
375
+ error: statusText
376
+ });
377
+ await scheduleTransientRetry(payload);
378
+ return request(payload);
379
+ }
335
380
  let type2 = "error";
336
381
  const noErrorStatusCodes = [];
337
382
  switch (payload.metricsDetails.target.name) {
@@ -376,10 +421,6 @@ async function request(payload) {
376
421
  }
377
422
  return response;
378
423
  } catch (error) {
379
- log(payload, "error", `request failed with no status`, {
380
- baseUrl: base,
381
- error
382
- });
383
424
  const duration = performance.now() - start;
384
425
  let errorMessage;
385
426
  if (error?.cause?.code) {
@@ -387,6 +428,25 @@ async function request(payload) {
387
428
  } else if (error?.message) {
388
429
  errorMessage = error?.message;
389
430
  }
431
+ if (canRetryTransient(payload)) {
432
+ log(payload, "debug", `request failed with no status, retrying`, {
433
+ baseUrl: base,
434
+ error,
435
+ durationMs: Math.floor(duration)
436
+ });
437
+ sendMetrics(payload, "transient-retry", {
438
+ statusCode: "fetch-error",
439
+ duration,
440
+ error: errorMessage
441
+ });
442
+ await scheduleTransientRetry(payload);
443
+ return request(payload);
444
+ }
445
+ log(payload, "error", `request failed with no status`, {
446
+ baseUrl: base,
447
+ error,
448
+ durationMs: Math.floor(duration)
449
+ });
390
450
  sendMetrics(payload, "error", {
391
451
  statusCode: "fetch-error",
392
452
  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' | 'blob' | '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 // Dynamic (e.g. correlation) headers first, then explicit per-request headers\n // override them, then auth/content-type below take final precedence.\n let dynamicHeaders: Record<string, string> = {}\n try {\n dynamicHeaders = payload.config.getDynamicHeaders?.() ?? {}\n } catch (_error) {\n // never let header evaluation break a request\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 // 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', { durationMs: Math.floor(duration) })\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 durationMs: Math.floor(duration),\n })\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 durationMs: Math.floor(duration),\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 durationMs: Math.floor(duration),\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 durationMs: Math.floor(duration),\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 durationMs: Math.floor(duration),\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 @@
1
+ export {};
package/dist/utils/sse.js CHANGED
@@ -148,6 +148,13 @@ function connectSse(options) {
148
148
  const headers = {
149
149
  accept: "text/event-stream"
150
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
+ }
151
158
  if (token) {
152
159
  headers.authorization = `Bearer ${token}`;
153
160
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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 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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",
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
6
  "names": []
7
7
  }
@@ -10,3 +10,21 @@
10
10
  export declare function generateTraceparent(): string;
11
11
  /** Convenience: the `{ traceparent }` header object for a config's getDynamicHeaders. */
12
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>;
@@ -9,8 +9,38 @@ function generateTraceparent() {
9
9
  function traceparentHeaders() {
10
10
  return { traceparent: generateTraceparent() };
11
11
  }
12
+ const SESSION_ID_HEADER = "x-cc-session-id";
13
+ const SESSION_ID_STORAGE_KEY = "cc-session-id";
14
+ let fallbackSessionId;
15
+ function getSessionId() {
16
+ try {
17
+ const storage = globalThis.sessionStorage;
18
+ if (storage) {
19
+ let id = storage.getItem(SESSION_ID_STORAGE_KEY);
20
+ if (!id) {
21
+ id = globalThis.crypto.randomUUID();
22
+ storage.setItem(SESSION_ID_STORAGE_KEY, id);
23
+ }
24
+ return id;
25
+ }
26
+ } catch {
27
+ }
28
+ if (!fallbackSessionId) {
29
+ fallbackSessionId = globalThis.crypto.randomUUID();
30
+ }
31
+ return fallbackSessionId;
32
+ }
33
+ function sessionHeaders() {
34
+ return { [SESSION_ID_HEADER]: getSessionId() };
35
+ }
36
+ function correlationHeaders() {
37
+ return { ...traceparentHeaders(), ...sessionHeaders() };
38
+ }
12
39
  export {
40
+ correlationHeaders,
13
41
  generateTraceparent,
42
+ getSessionId,
43
+ sessionHeaders,
14
44
  traceparentHeaders
15
45
  };
16
46
  //# sourceMappingURL=traceparent.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/traceparent.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\n/**\n * Generates a fresh W3C `traceparent` header value\n * (`00-<32 hex trace id>-<16 hex span id>-01`).\n *\n * Isomorphic: uses the Web Crypto API available on both the browser and Node\n * 18+ (`globalThis.crypto`). Intended for frontends to start a trace at the\n * request boundary so backends adopt it (see SessionMiddleware) \u2014 each call\n * mints a new trace, i.e. one trace per outgoing request.\n */\nexport function generateTraceparent(): string {\n const bytes = new Uint8Array(24)\n globalThis.crypto.getRandomValues(bytes)\n const hex = (slice: Uint8Array): string =>\n Array.from(slice, (byte) => byte.toString(16).padStart(2, '0')).join('')\n const traceId = hex(bytes.subarray(0, 16))\n const spanId = hex(bytes.subarray(16, 24))\n return `00-${traceId}-${spanId}-01`\n}\n\n/** Convenience: the `{ traceparent }` header object for a config's getDynamicHeaders. */\nexport function traceparentHeaders(): Record<string, string> {\n return { traceparent: generateTraceparent() }\n}\n"],
5
- "mappings": "AAeO,SAAS,sBAA8B;AAC5C,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,aAAW,OAAO,gBAAgB,KAAK;AACvC,QAAM,MAAM,CAAC,UACX,MAAM,KAAK,OAAO,CAAC,SAAS,KAAK,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AACzE,QAAM,UAAU,IAAI,MAAM,SAAS,GAAG,EAAE,CAAC;AACzC,QAAM,SAAS,IAAI,MAAM,SAAS,IAAI,EAAE,CAAC;AACzC,SAAO,MAAM,OAAO,IAAI,MAAM;AAChC;AAGO,SAAS,qBAA6C;AAC3D,SAAO,EAAE,aAAa,oBAAoB,EAAE;AAC9C;",
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\n/**\n * Generates a fresh W3C `traceparent` header value\n * (`00-<32 hex trace id>-<16 hex span id>-01`).\n *\n * Isomorphic: uses the Web Crypto API available on both the browser and Node\n * 18+ (`globalThis.crypto`). Intended for frontends to start a trace at the\n * request boundary so backends adopt it (see SessionMiddleware) \u2014 each call\n * mints a new trace, i.e. one trace per outgoing request.\n */\nexport function generateTraceparent(): string {\n const bytes = new Uint8Array(24)\n globalThis.crypto.getRandomValues(bytes)\n const hex = (slice: Uint8Array): string =>\n Array.from(slice, (byte) => byte.toString(16).padStart(2, '0')).join('')\n const traceId = hex(bytes.subarray(0, 16))\n const spanId = hex(bytes.subarray(16, 24))\n return `00-${traceId}-${spanId}-01`\n}\n\n/** Convenience: the `{ traceparent }` header object for a config's getDynamicHeaders. */\nexport function traceparentHeaders(): Record<string, string> {\n return { traceparent: generateTraceparent() }\n}\n\nconst SESSION_ID_HEADER = 'x-cc-session-id'\nconst SESSION_ID_STORAGE_KEY = 'cc-session-id'\n\nlet fallbackSessionId: string | undefined\n\n/**\n * A stable per-tab session id, minted once and cached in `sessionStorage` so it\n * survives reloads and in-tab navigation but is distinct per tab. Unlike a\n * traceparent (one per request), this groups every request of one browser\n * session under a single `sid` on the backend. Falls back to a per-process id\n * when `sessionStorage` is unavailable (SSR, tests, sandboxed iframe, privacy\n * mode) so it never throws.\n */\nexport function getSessionId(): string {\n try {\n const storage = globalThis.sessionStorage\n if (storage) {\n let id = storage.getItem(SESSION_ID_STORAGE_KEY)\n if (!id) {\n id = globalThis.crypto.randomUUID()\n storage.setItem(SESSION_ID_STORAGE_KEY, id)\n }\n return id\n }\n } catch {\n // sessionStorage access can throw (sandboxed iframe / disabled storage).\n }\n if (!fallbackSessionId) {\n fallbackSessionId = globalThis.crypto.randomUUID()\n }\n return fallbackSessionId\n}\n\n/** Convenience: the `{ 'x-cc-session-id' }` header object for the per-tab session. */\nexport function sessionHeaders(): Record<string, string> {\n return { [SESSION_ID_HEADER]: getSessionId() }\n}\n\n/**\n * Correlation headers for a frontend request: a fresh `traceparent` (one trace\n * per request, which the backend adopts) plus the stable per-tab\n * `x-cc-session-id`. Wire as a config's `getDynamicHeaders` so the backend can\n * both follow a single action (trace) and group a browser session (sid).\n */\nexport function correlationHeaders(): Record<string, string> {\n return { ...traceparentHeaders(), ...sessionHeaders() }\n}\n"],
5
+ "mappings": "AAeO,SAAS,sBAA8B;AAC5C,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,aAAW,OAAO,gBAAgB,KAAK;AACvC,QAAM,MAAM,CAAC,UACX,MAAM,KAAK,OAAO,CAAC,SAAS,KAAK,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AACzE,QAAM,UAAU,IAAI,MAAM,SAAS,GAAG,EAAE,CAAC;AACzC,QAAM,SAAS,IAAI,MAAM,SAAS,IAAI,EAAE,CAAC;AACzC,SAAO,MAAM,OAAO,IAAI,MAAM;AAChC;AAGO,SAAS,qBAA6C;AAC3D,SAAO,EAAE,aAAa,oBAAoB,EAAE;AAC9C;AAEA,MAAM,oBAAoB;AAC1B,MAAM,yBAAyB;AAE/B,IAAI;AAUG,SAAS,eAAuB;AACrC,MAAI;AACF,UAAM,UAAU,WAAW;AAC3B,QAAI,SAAS;AACX,UAAI,KAAK,QAAQ,QAAQ,sBAAsB;AAC/C,UAAI,CAAC,IAAI;AACP,aAAK,WAAW,OAAO,WAAW;AAClC,gBAAQ,QAAQ,wBAAwB,EAAE;AAAA,MAC5C;AACA,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AACA,MAAI,CAAC,mBAAmB;AACtB,wBAAoB,WAAW,OAAO,WAAW;AAAA,EACnD;AACA,SAAO;AACT;AAGO,SAAS,iBAAyC;AACvD,SAAO,EAAE,CAAC,iBAAiB,GAAG,aAAa,EAAE;AAC/C;AAQO,SAAS,qBAA6C;AAC3D,SAAO,EAAE,GAAG,mBAAmB,GAAG,GAAG,eAAe,EAAE;AACxD;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/ccma-api",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Centralized API service layer for Camunda Cloud Management Apps",
5
5
  "bugs": {
6
6
  "url": "https://github.com/camunda/camunda-cloud-management-apps/issues"
@@ -41,7 +41,7 @@
41
41
  }
42
42
  },
43
43
  "dependencies": {
44
- "@camunda/ccma-shared-types": "0.4.0"
44
+ "@camunda/ccma-shared-types": "0.5.0"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "node esbuild.mjs && tsc --emitDeclarationOnly --outDir dist",