@camunda/ccma-api 0.5.0 → 0.5.2

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 (56) hide show
  1. package/dist/index.d.ts +11 -3
  2. package/dist/index.js +14 -0
  3. package/dist/index.js.map +2 -2
  4. package/dist/services/accounts/hdp.service.d.ts +14 -1
  5. package/dist/services/accounts/hdp.service.js +41 -0
  6. package/dist/services/accounts/hdp.service.js.map +2 -2
  7. package/dist/services/accounts/orgs.service.d.ts +28 -0
  8. package/dist/services/accounts/orgs.service.js +41 -1
  9. package/dist/services/accounts/orgs.service.js.map +2 -2
  10. package/dist/services/accounts/orgs.service.spec.d.ts +1 -0
  11. package/dist/services/accounts/queued-operations.service.d.ts +59 -0
  12. package/dist/services/accounts/queued-operations.service.js +49 -0
  13. package/dist/services/accounts/queued-operations.service.js.map +7 -0
  14. package/dist/services/accounts/telemetry-ingest.service.d.ts +32 -0
  15. package/dist/services/accounts/telemetry-ingest.service.js +41 -0
  16. package/dist/services/accounts/telemetry-ingest.service.js.map +7 -0
  17. package/dist/services/accounts/trial-management.service.d.ts +27 -0
  18. package/dist/services/accounts/trial-management.service.js +101 -0
  19. package/dist/services/accounts/trial-management.service.js.map +7 -0
  20. package/dist/services/audit-logger/audit-logger.service.js +10 -1
  21. package/dist/services/audit-logger/audit-logger.service.js.map +2 -2
  22. package/dist/services/audit-logger/audit-logger.service.spec.d.ts +1 -0
  23. package/dist/services/auth0/auth0.service.d.ts +7 -0
  24. package/dist/services/auth0/auth0.service.js +2 -1
  25. package/dist/services/auth0/auth0.service.js.map +2 -2
  26. package/dist/services/auth0/auth0.service.spec.d.ts +1 -0
  27. package/dist/services/console/console.admin.service.js +13 -2
  28. package/dist/services/console/console.admin.service.js.map +2 -2
  29. package/dist/services/console/console.admin.service.spec.d.ts +1 -0
  30. package/dist/services/console/console.cluster.service.d.ts +9 -0
  31. package/dist/services/console/console.cluster.service.js +7 -0
  32. package/dist/services/console/console.cluster.service.js.map +2 -2
  33. package/dist/services/console/generation.service.d.ts +21 -1
  34. package/dist/services/console/generation.service.js +9 -0
  35. package/dist/services/console/generation.service.js.map +2 -2
  36. package/dist/services/github/github.service.d.ts +1 -0
  37. package/dist/services/github/github.service.js +32 -22
  38. package/dist/services/github/github.service.js.map +2 -2
  39. package/dist/services/github/github.service.spec.d.ts +1 -0
  40. package/dist/services/thanos/thanos.service.d.ts +49 -0
  41. package/dist/services/thanos/thanos.service.js +60 -0
  42. package/dist/services/thanos/thanos.service.js.map +7 -0
  43. package/dist/services/thanos/thanos.service.spec.d.ts +1 -0
  44. package/dist/services/token-issuer/token-issuer.jwks.service.js +5 -1
  45. package/dist/services/token-issuer/token-issuer.jwks.service.js.map +2 -2
  46. package/dist/services/token-issuer/token-issuer.jwks.service.spec.d.ts +1 -0
  47. package/dist/utils/api.js +8 -0
  48. package/dist/utils/api.js.map +2 -2
  49. package/dist/utils/cache.d.ts +24 -0
  50. package/dist/utils/cache.js +41 -0
  51. package/dist/utils/cache.js.map +2 -2
  52. package/dist/utils/cache.spec.d.ts +1 -0
  53. package/dist/utils/traceparent.d.ts +21 -1
  54. package/dist/utils/traceparent.js +14 -0
  55. package/dist/utils/traceparent.js.map +2 -2
  56. package/package.json +2 -2
@@ -0,0 +1,49 @@
1
+ import type { RequestBaseParams } from '../../types/general.types.js';
2
+ /**
3
+ * Thanos' Prometheus-compatible HTTP query API.
4
+ *
5
+ * Console-backend used to call this with a bare `fetch()`, which is why its
6
+ * failure line had to hand-roll status extraction and body truncation — and got
7
+ * both wrong (it logged the `Response` object, which serialises to `{}`).
8
+ * Going through `request()` means the status, the capped error body, the
9
+ * `ccma` request metrics and the `error.*` log shape all come from the same
10
+ * place as every other outbound call.
11
+ */
12
+ /** Endpoints of the query API we call. */
13
+ export declare enum ThanosApiEndpoint {
14
+ QUERY_RANGE = "query_range"
15
+ }
16
+ export interface ThanosQueryResult {
17
+ metric: any;
18
+ values?: any[];
19
+ value?: any[];
20
+ }
21
+ export interface ThanosQueryResponse {
22
+ status: string;
23
+ data?: {
24
+ resultType?: string;
25
+ result?: ThanosQueryResult[];
26
+ };
27
+ }
28
+ export interface ThanosQueryRequest extends RequestBaseParams {
29
+ options: {
30
+ /** Host of the per-k8s-context Thanos query endpoint, without a scheme. */
31
+ thanosEndpoint: string;
32
+ endpoint: ThanosApiEndpoint | string;
33
+ /** Basic-auth credentials for the endpoint. */
34
+ username: string;
35
+ password: string;
36
+ queryParameter?: URLSearchParams;
37
+ };
38
+ }
39
+ export declare class ThanosService {
40
+ /**
41
+ * Run one query against a single Thanos endpoint.
42
+ *
43
+ * Throws `HttpError` on a non-2xx, per this package's convention. The status
44
+ * and the truncated response body ride along on it (`status`, `message`), so
45
+ * a caller that logs the caught error records what actually went wrong
46
+ * without reading the body itself.
47
+ */
48
+ static query(params: ThanosQueryRequest): Promise<ThanosQueryResponse>;
49
+ }
@@ -0,0 +1,60 @@
1
+ import { request } from "../../utils/api.js";
2
+ import { HttpError } from "../../utils/errors.js";
3
+ var ThanosApiEndpoint = /* @__PURE__ */ ((ThanosApiEndpoint2) => {
4
+ ThanosApiEndpoint2["QUERY_RANGE"] = "query_range";
5
+ return ThanosApiEndpoint2;
6
+ })(ThanosApiEndpoint || {});
7
+ class ThanosService {
8
+ /**
9
+ * Run one query against a single Thanos endpoint.
10
+ *
11
+ * Throws `HttpError` on a non-2xx, per this package's convention. The status
12
+ * and the truncated response body ride along on it (`status`, `message`), so
13
+ * a caller that logs the caught error records what actually went wrong
14
+ * without reading the body itself.
15
+ */
16
+ static async query(params) {
17
+ const queryParams = params.options.queryParameter ? `?${params.options.queryParameter.toString()}` : "";
18
+ const serviceContext = {
19
+ method: {
20
+ name: `ThanosService.query`,
21
+ url: `https://${params.options.thanosEndpoint}/api/v1/${params.options.endpoint}${queryParams}`,
22
+ // The endpoint host is per-k8s-context, so it stays out of the
23
+ // normalized URL — otherwise every region is its own metric series.
24
+ normalizedUrl: `https://:thanosEndpoint/api/v1/${params.options.endpoint}`,
25
+ type: "get"
26
+ },
27
+ source: params.config.metrics?.source || "unset",
28
+ target: {
29
+ name: "thanos",
30
+ type: "external"
31
+ }
32
+ };
33
+ const basicAuth = Buffer.from(
34
+ `${params.options.username}:${params.options.password}`
35
+ ).toString("base64");
36
+ const response = await request({
37
+ config: params.config,
38
+ url: serviceContext.method.url,
39
+ method: serviceContext.method.type,
40
+ metricsDetails: serviceContext,
41
+ type: "json",
42
+ headers: {
43
+ Authorization: `Basic ${basicAuth}`
44
+ }
45
+ });
46
+ if (!response.ok) {
47
+ throw new HttpError(
48
+ `${serviceContext.method.name}: ${response.status} ${response.statusText}`,
49
+ response.status,
50
+ response.statusText
51
+ );
52
+ }
53
+ return response.body;
54
+ }
55
+ }
56
+ export {
57
+ ThanosApiEndpoint,
58
+ ThanosService
59
+ };
60
+ //# sourceMappingURL=thanos.service.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/services/thanos/thanos.service.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 RequestBaseParams,\n RequestBaseServiceContext,\n} from '../../types/general.types.js'\nimport { request } from '../../utils/api.js'\nimport { HttpError } from '../../utils/errors.js'\n\n/**\n * Thanos' Prometheus-compatible HTTP query API.\n *\n * Console-backend used to call this with a bare `fetch()`, which is why its\n * failure line had to hand-roll status extraction and body truncation \u2014 and got\n * both wrong (it logged the `Response` object, which serialises to `{}`).\n * Going through `request()` means the status, the capped error body, the\n * `ccma` request metrics and the `error.*` log shape all come from the same\n * place as every other outbound call.\n */\n\n/** Endpoints of the query API we call. */\nexport enum ThanosApiEndpoint {\n QUERY_RANGE = 'query_range',\n}\n\nexport interface ThanosQueryResult {\n metric: any\n values?: any[]\n value?: any[]\n}\n\nexport interface ThanosQueryResponse {\n status: string\n data?: {\n resultType?: string\n result?: ThanosQueryResult[]\n }\n}\n\nexport interface ThanosQueryRequest extends RequestBaseParams {\n options: {\n /** Host of the per-k8s-context Thanos query endpoint, without a scheme. */\n thanosEndpoint: string\n endpoint: ThanosApiEndpoint | string\n /** Basic-auth credentials for the endpoint. */\n username: string\n password: string\n queryParameter?: URLSearchParams\n }\n}\n\nexport class ThanosService {\n /**\n * Run one query against a single Thanos endpoint.\n *\n * Throws `HttpError` on a non-2xx, per this package's convention. The status\n * and the truncated response body ride along on it (`status`, `message`), so\n * a caller that logs the caught error records what actually went wrong\n * without reading the body itself.\n */\n public static async query(\n params: ThanosQueryRequest,\n ): Promise<ThanosQueryResponse> {\n const queryParams = params.options.queryParameter\n ? `?${params.options.queryParameter.toString()}`\n : ''\n const serviceContext: RequestBaseServiceContext = {\n method: {\n name: `ThanosService.query`,\n url: `https://${params.options.thanosEndpoint}/api/v1/${params.options.endpoint}${queryParams}`,\n // The endpoint host is per-k8s-context, so it stays out of the\n // normalized URL \u2014 otherwise every region is its own metric series.\n normalizedUrl: `https://:thanosEndpoint/api/v1/${params.options.endpoint}`,\n type: 'get',\n },\n source: params.config.metrics?.source || 'unset',\n target: {\n name: 'thanos',\n type: 'external',\n },\n }\n\n const basicAuth = Buffer.from(\n `${params.options.username}:${params.options.password}`,\n ).toString('base64')\n\n const response = await request<ThanosQueryResponse>({\n config: params.config,\n url: serviceContext.method.url,\n method: serviceContext.method.type,\n metricsDetails: serviceContext,\n type: 'json',\n headers: {\n Authorization: `Basic ${basicAuth}`,\n },\n })\n\n if (!response.ok) {\n throw new HttpError(\n `${serviceContext.method.name}: ${response.status} ${response.statusText}`,\n response.status,\n response.statusText,\n )\n }\n\n return response.body!\n }\n}\n"],
5
+ "mappings": "AAUA,SAAS,eAAe;AACxB,SAAS,iBAAiB;AAcnB,IAAK,oBAAL,kBAAKA,uBAAL;AACL,EAAAA,mBAAA,iBAAc;AADJ,SAAAA;AAAA,GAAA;AA8BL,MAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASzB,aAAoB,MAClB,QAC8B;AAC9B,UAAM,cAAc,OAAO,QAAQ,iBAC/B,IAAI,OAAO,QAAQ,eAAe,SAAS,CAAC,KAC5C;AACJ,UAAM,iBAA4C;AAAA,MAChD,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,KAAK,WAAW,OAAO,QAAQ,cAAc,WAAW,OAAO,QAAQ,QAAQ,GAAG,WAAW;AAAA;AAAA;AAAA,QAG7F,eAAe,kCAAkC,OAAO,QAAQ,QAAQ;AAAA,QACxE,MAAM;AAAA,MACR;AAAA,MACA,QAAQ,OAAO,OAAO,SAAS,UAAU;AAAA,MACzC,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAEA,UAAM,YAAY,OAAO;AAAA,MACvB,GAAG,OAAO,QAAQ,QAAQ,IAAI,OAAO,QAAQ,QAAQ;AAAA,IACvD,EAAE,SAAS,QAAQ;AAEnB,UAAM,WAAW,MAAM,QAA6B;AAAA,MAClD,QAAQ,OAAO;AAAA,MACf,KAAK,eAAe,OAAO;AAAA,MAC3B,QAAQ,eAAe,OAAO;AAAA,MAC9B,gBAAgB;AAAA,MAChB,MAAM;AAAA,MACN,SAAS;AAAA,QACP,eAAe,SAAS,SAAS;AAAA,MACnC;AAAA,IACF,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI;AAAA,QACR,GAAG,eAAe,OAAO,IAAI,KAAK,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,QACxE,SAAS;AAAA,QACT,SAAS;AAAA,MACX;AAAA,IACF;AAEA,WAAO,SAAS;AAAA,EAClB;AACF;",
6
+ "names": ["ThanosApiEndpoint"]
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -23,7 +23,11 @@ class TokenIssuerJwksService {
23
23
  config: params.config,
24
24
  url: serviceContext.method.url,
25
25
  method: serviceContext.method.type,
26
- metricsDetails: serviceContext
26
+ metricsDetails: serviceContext,
27
+ // A read-only GET against an internal service, so retrying is free of
28
+ // side effects — and this feeds AuthChecker, so a brief token-issuer blip
29
+ // otherwise turns straight into failed auth for real requests.
30
+ retryTransientErrors: true
27
31
  };
28
32
  const response = await request(requestConfig);
29
33
  if (!response.ok) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/services/token-issuer/token-issuer.jwks.service.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 { MINUTE } from '@camunda/ccma-shared-types'\nimport type {\n RequestBaseParams,\n RequestBaseServiceContext,\n} from '../../types/general.types.js'\nimport { log, request } from '../../utils/api.js'\nimport { Cache } from '../../utils/cache.js'\nimport { HttpError } from '../../utils/errors.js'\n\nconst CACHE_KEY = 'jwks'\nconst keysCache = new Cache<string, JwksResponse>(60 * MINUTE)\n\nexport interface JwksRequest extends RequestBaseParams {\n params: {\n uri: string\n }\n}\n\nexport interface JwksResponse {\n keys: any\n}\n\nexport class TokenIssuerJwksService {\n public static async get(params: JwksRequest): Promise<JwksResponse> {\n const serviceContext: RequestBaseServiceContext = {\n method: {\n name: `TokenIssuerJwksService.get`,\n url: params.params.uri,\n normalizedUrl: params.params.uri,\n type: 'get',\n },\n source: params.config.metrics?.source || 'unset',\n target: {\n name: 'token-issuer',\n type: 'internal',\n },\n }\n const requestConfig = {\n config: params.config,\n url: serviceContext.method.url,\n method: serviceContext.method.type,\n metricsDetails: serviceContext,\n }\n const response = await request<JwksResponse>(requestConfig)\n if (!response.ok) {\n const fromCache = keysCache.get(CACHE_KEY)\n if (fromCache && TokenIssuerJwksService.isValidKeysObject(fromCache)) {\n return fromCache\n }\n throw new HttpError(\n `${serviceContext.method.name}: ${response.status} ${response.statusText}`,\n response.status,\n response.statusText,\n )\n }\n\n if (TokenIssuerJwksService.isValidKeysObject(response.body)) {\n // valid response\n log(requestConfig, 'debug', 'jwks response valid, adding cache entry', {\n response,\n })\n keysCache.set(CACHE_KEY, response.body!)\n if (requestConfig.config.stage === 'dev') {\n const fromCache = keysCache.get(CACHE_KEY)\n log(requestConfig, 'debug', 'jwks value in cache', {\n response,\n fromCache,\n })\n }\n } else {\n log(requestConfig, 'debug', 'jwks response invalid', { response })\n // invalid response\n const fromCache = keysCache.get(CACHE_KEY)\n log(requestConfig, 'debug', 'checking cached entry', {\n response,\n fromCache,\n })\n if (fromCache && TokenIssuerJwksService.isValidKeysObject(fromCache)) {\n log(requestConfig, 'debug', 'using jwks from local cache', {\n response,\n fromCache,\n })\n return fromCache\n }\n }\n return response.body!\n }\n\n private static isValidKeysObject(response: any): boolean {\n return response?.keys && response?.keys.length > 0\n }\n}\n"],
5
- "mappings": "AAMA,SAAS,cAAc;AAKvB,SAAS,KAAK,eAAe;AAC7B,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAE1B,MAAM,YAAY;AAClB,MAAM,YAAY,IAAI,MAA4B,KAAK,MAAM;AAYtD,MAAM,uBAAuB;AAAA,EAClC,aAAoB,IAAI,QAA4C;AAClE,UAAM,iBAA4C;AAAA,MAChD,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,KAAK,OAAO,OAAO;AAAA,QACnB,eAAe,OAAO,OAAO;AAAA,QAC7B,MAAM;AAAA,MACR;AAAA,MACA,QAAQ,OAAO,OAAO,SAAS,UAAU;AAAA,MACzC,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AACA,UAAM,gBAAgB;AAAA,MACpB,QAAQ,OAAO;AAAA,MACf,KAAK,eAAe,OAAO;AAAA,MAC3B,QAAQ,eAAe,OAAO;AAAA,MAC9B,gBAAgB;AAAA,IAClB;AACA,UAAM,WAAW,MAAM,QAAsB,aAAa;AAC1D,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,YAAY,UAAU,IAAI,SAAS;AACzC,UAAI,aAAa,uBAAuB,kBAAkB,SAAS,GAAG;AACpE,eAAO;AAAA,MACT;AACA,YAAM,IAAI;AAAA,QACR,GAAG,eAAe,OAAO,IAAI,KAAK,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,QACxE,SAAS;AAAA,QACT,SAAS;AAAA,MACX;AAAA,IACF;AAEA,QAAI,uBAAuB,kBAAkB,SAAS,IAAI,GAAG;AAE3D,UAAI,eAAe,SAAS,2CAA2C;AAAA,QACrE;AAAA,MACF,CAAC;AACD,gBAAU,IAAI,WAAW,SAAS,IAAK;AACvC,UAAI,cAAc,OAAO,UAAU,OAAO;AACxC,cAAM,YAAY,UAAU,IAAI,SAAS;AACzC,YAAI,eAAe,SAAS,uBAAuB;AAAA,UACjD;AAAA,UACA;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,OAAO;AACL,UAAI,eAAe,SAAS,yBAAyB,EAAE,SAAS,CAAC;AAEjE,YAAM,YAAY,UAAU,IAAI,SAAS;AACzC,UAAI,eAAe,SAAS,yBAAyB;AAAA,QACnD;AAAA,QACA;AAAA,MACF,CAAC;AACD,UAAI,aAAa,uBAAuB,kBAAkB,SAAS,GAAG;AACpE,YAAI,eAAe,SAAS,+BAA+B;AAAA,UACzD;AAAA,UACA;AAAA,QACF,CAAC;AACD,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO,SAAS;AAAA,EAClB;AAAA,EAEA,OAAe,kBAAkB,UAAwB;AACvD,WAAO,UAAU,QAAQ,UAAU,KAAK,SAAS;AAAA,EACnD;AACF;",
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 { MINUTE } from '@camunda/ccma-shared-types'\nimport type {\n RequestBaseParams,\n RequestBaseServiceContext,\n} from '../../types/general.types.js'\nimport { log, request } from '../../utils/api.js'\nimport { Cache } from '../../utils/cache.js'\nimport { HttpError } from '../../utils/errors.js'\n\nconst CACHE_KEY = 'jwks'\nconst keysCache = new Cache<string, JwksResponse>(60 * MINUTE)\n\nexport interface JwksRequest extends RequestBaseParams {\n params: {\n uri: string\n }\n}\n\nexport interface JwksResponse {\n keys: any\n}\n\nexport class TokenIssuerJwksService {\n public static async get(params: JwksRequest): Promise<JwksResponse> {\n const serviceContext: RequestBaseServiceContext = {\n method: {\n name: `TokenIssuerJwksService.get`,\n url: params.params.uri,\n normalizedUrl: params.params.uri,\n type: 'get',\n },\n source: params.config.metrics?.source || 'unset',\n target: {\n name: 'token-issuer',\n type: 'internal',\n },\n }\n const requestConfig = {\n config: params.config,\n url: serviceContext.method.url,\n method: serviceContext.method.type,\n metricsDetails: serviceContext,\n // A read-only GET against an internal service, so retrying is free of\n // side effects \u2014 and this feeds AuthChecker, so a brief token-issuer blip\n // otherwise turns straight into failed auth for real requests.\n retryTransientErrors: true,\n }\n const response = await request<JwksResponse>(requestConfig)\n if (!response.ok) {\n const fromCache = keysCache.get(CACHE_KEY)\n if (fromCache && TokenIssuerJwksService.isValidKeysObject(fromCache)) {\n return fromCache\n }\n throw new HttpError(\n `${serviceContext.method.name}: ${response.status} ${response.statusText}`,\n response.status,\n response.statusText,\n )\n }\n\n if (TokenIssuerJwksService.isValidKeysObject(response.body)) {\n // valid response\n log(requestConfig, 'debug', 'jwks response valid, adding cache entry', {\n response,\n })\n keysCache.set(CACHE_KEY, response.body!)\n if (requestConfig.config.stage === 'dev') {\n const fromCache = keysCache.get(CACHE_KEY)\n log(requestConfig, 'debug', 'jwks value in cache', {\n response,\n fromCache,\n })\n }\n } else {\n log(requestConfig, 'debug', 'jwks response invalid', { response })\n // invalid response\n const fromCache = keysCache.get(CACHE_KEY)\n log(requestConfig, 'debug', 'checking cached entry', {\n response,\n fromCache,\n })\n if (fromCache && TokenIssuerJwksService.isValidKeysObject(fromCache)) {\n log(requestConfig, 'debug', 'using jwks from local cache', {\n response,\n fromCache,\n })\n return fromCache\n }\n }\n return response.body!\n }\n\n private static isValidKeysObject(response: any): boolean {\n return response?.keys && response?.keys.length > 0\n }\n}\n"],
5
+ "mappings": "AAMA,SAAS,cAAc;AAKvB,SAAS,KAAK,eAAe;AAC7B,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAE1B,MAAM,YAAY;AAClB,MAAM,YAAY,IAAI,MAA4B,KAAK,MAAM;AAYtD,MAAM,uBAAuB;AAAA,EAClC,aAAoB,IAAI,QAA4C;AAClE,UAAM,iBAA4C;AAAA,MAChD,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,KAAK,OAAO,OAAO;AAAA,QACnB,eAAe,OAAO,OAAO;AAAA,QAC7B,MAAM;AAAA,MACR;AAAA,MACA,QAAQ,OAAO,OAAO,SAAS,UAAU;AAAA,MACzC,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AACA,UAAM,gBAAgB;AAAA,MACpB,QAAQ,OAAO;AAAA,MACf,KAAK,eAAe,OAAO;AAAA,MAC3B,QAAQ,eAAe,OAAO;AAAA,MAC9B,gBAAgB;AAAA;AAAA;AAAA;AAAA,MAIhB,sBAAsB;AAAA,IACxB;AACA,UAAM,WAAW,MAAM,QAAsB,aAAa;AAC1D,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,YAAY,UAAU,IAAI,SAAS;AACzC,UAAI,aAAa,uBAAuB,kBAAkB,SAAS,GAAG;AACpE,eAAO;AAAA,MACT;AACA,YAAM,IAAI;AAAA,QACR,GAAG,eAAe,OAAO,IAAI,KAAK,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,QACxE,SAAS;AAAA,QACT,SAAS;AAAA,MACX;AAAA,IACF;AAEA,QAAI,uBAAuB,kBAAkB,SAAS,IAAI,GAAG;AAE3D,UAAI,eAAe,SAAS,2CAA2C;AAAA,QACrE;AAAA,MACF,CAAC;AACD,gBAAU,IAAI,WAAW,SAAS,IAAK;AACvC,UAAI,cAAc,OAAO,UAAU,OAAO;AACxC,cAAM,YAAY,UAAU,IAAI,SAAS;AACzC,YAAI,eAAe,SAAS,uBAAuB;AAAA,UACjD;AAAA,UACA;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,OAAO;AACL,UAAI,eAAe,SAAS,yBAAyB,EAAE,SAAS,CAAC;AAEjE,YAAM,YAAY,UAAU,IAAI,SAAS;AACzC,UAAI,eAAe,SAAS,yBAAyB;AAAA,QACnD;AAAA,QACA;AAAA,MACF,CAAC;AACD,UAAI,aAAa,uBAAuB,kBAAkB,SAAS,GAAG;AACpE,YAAI,eAAe,SAAS,+BAA+B;AAAA,UACzD;AAAA,UACA;AAAA,QACF,CAAC;AACD,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO,SAAS;AAAA,EAClB;AAAA,EAEA,OAAe,kBAAkB,UAAwB;AACvD,WAAO,UAAU,QAAQ,UAAU,KAAK,SAAS;AAAA,EACnD;AACF;",
6
6
  "names": []
7
7
  }
package/dist/utils/api.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { getEnvParam } from "./environments.js";
2
2
  import { frontendCache } from "./frontendCache.js";
3
3
  const TRANSIENT_RETRY_STATUS = [502, 503, 504];
4
+ const MAX_ERROR_BODY_LENGTH = 500;
5
+ const ERROR_BODY_TRUNCATION_MARKER = "\u2026 [truncated]";
4
6
  const MAX_TRANSIENT_RETRIES = 3;
5
7
  function sendMetrics(payload, type, options) {
6
8
  if (payload.config.metrics) {
@@ -358,6 +360,12 @@ async function request(payload) {
358
360
  statusText = await fetchResponse.text();
359
361
  } catch (_error) {
360
362
  }
363
+ if (statusText.length > MAX_ERROR_BODY_LENGTH) {
364
+ statusText = `${statusText.slice(
365
+ 0,
366
+ MAX_ERROR_BODY_LENGTH - ERROR_BODY_TRUNCATION_MARKER.length
367
+ )}${ERROR_BODY_TRUNCATION_MARKER}`;
368
+ }
361
369
  response.statusText = statusText;
362
370
  log(
363
371
  payload,
@@ -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 /**\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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",
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]\n/**\n * Cap on the error body kept as `statusText`. It ends up in the thrown error's\n * message, the log line, and the `ccma.errors` payload, and some upstreams\n * (LiteLLM, GCP load balancers) answer failures with a full HTML page \u2014 one\n * such response is kilobytes of markup standing in for a reason phrase. The\n * head is what carries the actual reason.\n */\nconst MAX_ERROR_BODY_LENGTH = 500\n/** Appended in place of what was cut, and counted against the cap above. */\nconst ERROR_BODY_TRUNCATION_MARKER = '\u2026 [truncated]'\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 if (statusText.length > MAX_ERROR_BODY_LENGTH) {\n // The marker is part of the budget, so the stored value never exceeds\n // MAX_ERROR_BODY_LENGTH \u2014 appending it after slicing to the full cap\n // would put the result over the bound the constant advertises.\n statusText = `${statusText.slice(\n 0,\n MAX_ERROR_BODY_LENGTH - ERROR_BODY_TRUNCATION_MARKER.length,\n )}${ERROR_BODY_TRUNCATION_MARKER}`\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
  }
@@ -1,7 +1,31 @@
1
1
  export declare class Cache<K, V> {
2
2
  private expirationInMilliseconds;
3
3
  private data;
4
+ /**
5
+ * Loads that have started but not finished, keyed the same as `data`.
6
+ * `get`/`set` never touch this — only `getOrLoad` does — so the plain
7
+ * cache-aside callers are unaffected.
8
+ */
9
+ private inFlight;
4
10
  constructor(expirationInMilliseconds: number);
5
11
  set(key: K, value: V): void;
6
12
  get(key: K): V | undefined;
13
+ /**
14
+ * Cache-aside with single-flight: concurrent callers that miss the same key
15
+ * wait on one `load()` instead of each running their own.
16
+ *
17
+ * Without this, a cold key multiplies the upstream request count by the
18
+ * number of waiting callers. That is what made `GET
19
+ * /api/releases/release-train/:trainName` return 500s: Control asks for
20
+ * several trains at once, every lookup shares one cache key, and each
21
+ * `listMajorMinorReleases` fans out 20 parallel GitHub pages — so three
22
+ * simultaneous cache misses became ~120 concurrent GitHub calls and tripped
23
+ * GitHub's *secondary* (concurrency) rate limit.
24
+ *
25
+ * A rejected load is removed from the in-flight map and not stored, so the
26
+ * next caller retries rather than inheriting a wedged key.
27
+ */
28
+ getOrLoad(key: K, load: () => Promise<V>, options?: {
29
+ ignoreCache?: boolean;
30
+ }): Promise<V>;
7
31
  }
@@ -2,6 +2,12 @@ class Cache {
2
2
  constructor(expirationInMilliseconds) {
3
3
  this.expirationInMilliseconds = expirationInMilliseconds;
4
4
  this.data = /* @__PURE__ */ new Map();
5
+ /**
6
+ * Loads that have started but not finished, keyed the same as `data`.
7
+ * `get`/`set` never touch this — only `getOrLoad` does — so the plain
8
+ * cache-aside callers are unaffected.
9
+ */
10
+ this.inFlight = /* @__PURE__ */ new Map();
5
11
  }
6
12
  set(key, value) {
7
13
  this.data.set(key, {
@@ -18,6 +24,41 @@ class Cache {
18
24
  }
19
25
  return void 0;
20
26
  }
27
+ /**
28
+ * Cache-aside with single-flight: concurrent callers that miss the same key
29
+ * wait on one `load()` instead of each running their own.
30
+ *
31
+ * Without this, a cold key multiplies the upstream request count by the
32
+ * number of waiting callers. That is what made `GET
33
+ * /api/releases/release-train/:trainName` return 500s: Control asks for
34
+ * several trains at once, every lookup shares one cache key, and each
35
+ * `listMajorMinorReleases` fans out 20 parallel GitHub pages — so three
36
+ * simultaneous cache misses became ~120 concurrent GitHub calls and tripped
37
+ * GitHub's *secondary* (concurrency) rate limit.
38
+ *
39
+ * A rejected load is removed from the in-flight map and not stored, so the
40
+ * next caller retries rather than inheriting a wedged key.
41
+ */
42
+ getOrLoad(key, load, options) {
43
+ if (options?.ignoreCache !== true) {
44
+ const cached = this.get(key);
45
+ if (cached !== void 0) {
46
+ return Promise.resolve(cached);
47
+ }
48
+ }
49
+ const pending = this.inFlight.get(key);
50
+ if (pending) {
51
+ return pending;
52
+ }
53
+ const loading = load().then((value) => {
54
+ this.set(key, value);
55
+ return value;
56
+ }).finally(() => {
57
+ this.inFlight.delete(key);
58
+ });
59
+ this.inFlight.set(key, loading);
60
+ return loading;
61
+ }
21
62
  }
22
63
  export {
23
64
  Cache
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/cache.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\nexport class Cache<K, V> {\n private data: Map<K, { value: V; expiresAt: number }> = new Map()\n\n constructor(private expirationInMilliseconds: number) {}\n\n public set(key: K, value: V) {\n this.data.set(key, {\n value,\n expiresAt: Date.now() + this.expirationInMilliseconds,\n })\n }\n public get(key: K): V | undefined {\n const cached = this.data.get(key)\n if (cached && cached.expiresAt > Date.now()) {\n return cached.value\n } else if (cached) {\n this.data.delete(key)\n }\n return undefined\n }\n}\n"],
5
- "mappings": "AAMO,MAAM,MAAY;AAAA,EAGvB,YAAoB,0BAAkC;AAAlC;AAFpB,SAAQ,OAAgD,oBAAI,IAAI;AAAA,EAET;AAAA,EAEhD,IAAI,KAAQ,OAAU;AAC3B,SAAK,KAAK,IAAI,KAAK;AAAA,MACjB;AAAA,MACA,WAAW,KAAK,IAAI,IAAI,KAAK;AAAA,IAC/B,CAAC;AAAA,EACH;AAAA,EACO,IAAI,KAAuB;AAChC,UAAM,SAAS,KAAK,KAAK,IAAI,GAAG;AAChC,QAAI,UAAU,OAAO,YAAY,KAAK,IAAI,GAAG;AAC3C,aAAO,OAAO;AAAA,IAChB,WAAW,QAAQ;AACjB,WAAK,KAAK,OAAO,GAAG;AAAA,IACtB;AACA,WAAO;AAAA,EACT;AACF;",
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\nexport class Cache<K, V> {\n private data: Map<K, { value: V; expiresAt: number }> = new Map()\n /**\n * Loads that have started but not finished, keyed the same as `data`.\n * `get`/`set` never touch this \u2014 only `getOrLoad` does \u2014 so the plain\n * cache-aside callers are unaffected.\n */\n private inFlight: Map<K, Promise<V>> = new Map()\n\n constructor(private expirationInMilliseconds: number) {}\n\n public set(key: K, value: V) {\n this.data.set(key, {\n value,\n expiresAt: Date.now() + this.expirationInMilliseconds,\n })\n }\n public get(key: K): V | undefined {\n const cached = this.data.get(key)\n if (cached && cached.expiresAt > Date.now()) {\n return cached.value\n } else if (cached) {\n this.data.delete(key)\n }\n return undefined\n }\n\n /**\n * Cache-aside with single-flight: concurrent callers that miss the same key\n * wait on one `load()` instead of each running their own.\n *\n * Without this, a cold key multiplies the upstream request count by the\n * number of waiting callers. That is what made `GET\n * /api/releases/release-train/:trainName` return 500s: Control asks for\n * several trains at once, every lookup shares one cache key, and each\n * `listMajorMinorReleases` fans out 20 parallel GitHub pages \u2014 so three\n * simultaneous cache misses became ~120 concurrent GitHub calls and tripped\n * GitHub's *secondary* (concurrency) rate limit.\n *\n * A rejected load is removed from the in-flight map and not stored, so the\n * next caller retries rather than inheriting a wedged key.\n */\n public getOrLoad(\n key: K,\n load: () => Promise<V>,\n options?: { ignoreCache?: boolean },\n ): Promise<V> {\n if (options?.ignoreCache !== true) {\n const cached = this.get(key)\n if (cached !== undefined) {\n return Promise.resolve(cached)\n }\n }\n\n const pending = this.inFlight.get(key)\n if (pending) {\n return pending\n }\n\n const loading = load()\n .then((value) => {\n this.set(key, value)\n return value\n })\n .finally(() => {\n this.inFlight.delete(key)\n })\n this.inFlight.set(key, loading)\n return loading\n }\n}\n"],
5
+ "mappings": "AAMO,MAAM,MAAY;AAAA,EASvB,YAAoB,0BAAkC;AAAlC;AARpB,SAAQ,OAAgD,oBAAI,IAAI;AAMhE;AAAA;AAAA;AAAA;AAAA;AAAA,SAAQ,WAA+B,oBAAI,IAAI;AAAA,EAEQ;AAAA,EAEhD,IAAI,KAAQ,OAAU;AAC3B,SAAK,KAAK,IAAI,KAAK;AAAA,MACjB;AAAA,MACA,WAAW,KAAK,IAAI,IAAI,KAAK;AAAA,IAC/B,CAAC;AAAA,EACH;AAAA,EACO,IAAI,KAAuB;AAChC,UAAM,SAAS,KAAK,KAAK,IAAI,GAAG;AAChC,QAAI,UAAU,OAAO,YAAY,KAAK,IAAI,GAAG;AAC3C,aAAO,OAAO;AAAA,IAChB,WAAW,QAAQ;AACjB,WAAK,KAAK,OAAO,GAAG;AAAA,IACtB;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBO,UACL,KACA,MACA,SACY;AACZ,QAAI,SAAS,gBAAgB,MAAM;AACjC,YAAM,SAAS,KAAK,IAAI,GAAG;AAC3B,UAAI,WAAW,QAAW;AACxB,eAAO,QAAQ,QAAQ,MAAM;AAAA,MAC/B;AAAA,IACF;AAEA,UAAM,UAAU,KAAK,SAAS,IAAI,GAAG;AACrC,QAAI,SAAS;AACX,aAAO;AAAA,IACT;AAEA,UAAM,UAAU,KAAK,EAClB,KAAK,CAAC,UAAU;AACf,WAAK,IAAI,KAAK,KAAK;AACnB,aAAO;AAAA,IACT,CAAC,EACA,QAAQ,MAAM;AACb,WAAK,SAAS,OAAO,GAAG;AAAA,IAC1B,CAAC;AACH,SAAK,SAAS,IAAI,KAAK,OAAO;AAC9B,WAAO;AAAA,EACT;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,26 @@
1
+ /**
2
+ * Lets a frontend that runs an OpenTelemetry tracer take over minting the
3
+ * `traceparent` this module sends, so the header carries the id of a span that
4
+ * is actually exported instead of a random one that is not.
5
+ *
6
+ * Without this, a frontend with a tracer would have two writers competing for
7
+ * the header: the tracer's own propagator and `generateTraceparent`'s RNG. The
8
+ * backend would adopt whichever won, and the span the browser exported would
9
+ * never line up with the backend's — the browser's parent id would point at
10
+ * nothing. This mirrors what the backend already does in
11
+ * `SessionUtils.getPropagationHeaders()` ("prefer the active OTel span").
12
+ *
13
+ * The provider may return `undefined` (no active span right now), in which case
14
+ * a fresh random traceparent is generated as before. A throwing provider is
15
+ * treated the same way — correlation is best-effort and must never break a
16
+ * request. Pass `undefined` to uninstall.
17
+ */
18
+ export declare function setTraceparentProvider(provider: (() => string | undefined) | undefined): void;
1
19
  /**
2
20
  * Generates a fresh W3C `traceparent` header value
3
- * (`00-<32 hex trace id>-<16 hex span id>-01`).
21
+ * (`00-<32 hex trace id>-<16 hex span id>-01`), or returns the one supplied by
22
+ * an installed provider (`setTraceparentProvider`) when it has an active trace
23
+ * context to offer.
4
24
  *
5
25
  * Isomorphic: uses the Web Crypto API available on both the browser and Node
6
26
  * 18+ (`globalThis.crypto`). Intended for frontends to start a trace at the
@@ -1,4 +1,17 @@
1
+ let activeTraceparentProvider;
2
+ function setTraceparentProvider(provider) {
3
+ activeTraceparentProvider = provider;
4
+ }
1
5
  function generateTraceparent() {
6
+ if (activeTraceparentProvider) {
7
+ try {
8
+ const provided = activeTraceparentProvider();
9
+ if (provided) {
10
+ return provided;
11
+ }
12
+ } catch {
13
+ }
14
+ }
2
15
  const bytes = new Uint8Array(24);
3
16
  globalThis.crypto.getRandomValues(bytes);
4
17
  const hex = (slice) => Array.from(slice, (byte) => byte.toString(16).padStart(2, "0")).join("");
@@ -41,6 +54,7 @@ export {
41
54
  generateTraceparent,
42
55
  getSessionId,
43
56
  sessionHeaders,
57
+ setTraceparentProvider,
44
58
  traceparentHeaders
45
59
  };
46
60
  //# 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\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;",
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 * Optional source of the traceparent to send, installed by a caller that owns\n * a real trace context (see `setTraceparentProvider`). Module-private, and\n * unset in every process that does not install one \u2014 which is every consumer\n * except control-frontend's telemetry module today.\n */\nlet activeTraceparentProvider: (() => string | undefined) | undefined\n\n/**\n * Lets a frontend that runs an OpenTelemetry tracer take over minting the\n * `traceparent` this module sends, so the header carries the id of a span that\n * is actually exported instead of a random one that is not.\n *\n * Without this, a frontend with a tracer would have two writers competing for\n * the header: the tracer's own propagator and `generateTraceparent`'s RNG. The\n * backend would adopt whichever won, and the span the browser exported would\n * never line up with the backend's \u2014 the browser's parent id would point at\n * nothing. This mirrors what the backend already does in\n * `SessionUtils.getPropagationHeaders()` (\"prefer the active OTel span\").\n *\n * The provider may return `undefined` (no active span right now), in which case\n * a fresh random traceparent is generated as before. A throwing provider is\n * treated the same way \u2014 correlation is best-effort and must never break a\n * request. Pass `undefined` to uninstall.\n */\nexport function setTraceparentProvider(\n provider: (() => string | undefined) | undefined,\n): void {\n activeTraceparentProvider = provider\n}\n\n/**\n * Generates a fresh W3C `traceparent` header value\n * (`00-<32 hex trace id>-<16 hex span id>-01`), or returns the one supplied by\n * an installed provider (`setTraceparentProvider`) when it has an active trace\n * context to offer.\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 if (activeTraceparentProvider) {\n try {\n const provided = activeTraceparentProvider()\n if (provided) {\n return provided\n }\n } catch {\n // A broken provider must never break a request; fall through to random.\n }\n }\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": "AAYA,IAAI;AAmBG,SAAS,uBACd,UACM;AACN,8BAA4B;AAC9B;AAaO,SAAS,sBAA8B;AAC5C,MAAI,2BAA2B;AAC7B,QAAI;AACF,YAAM,WAAW,0BAA0B;AAC3C,UAAI,UAAU;AACZ,eAAO;AAAA,MACT;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AACA,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.5.0",
3
+ "version": "0.5.2",
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.5.0"
44
+ "@camunda/ccma-shared-types": "0.6.0"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "node esbuild.mjs && tsc --emitDeclarationOnly --outDir dist",