@camunda/ccma-api 2.2.0 → 3.0.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.
@@ -1,4 +1,4 @@
1
- import type { ApprovalEntityType, ApprovalJobDto, ApprovalJobStatus, ApprovalSortField, ApprovalSortOrder, ApprovalStatusCountsDto, ApprovalUseCaseDto, ClusterBulkOperationPayloadDto, ClusterBulkOperationType, FeatureFlagOverridePayloadDto } from '@camunda/ccma-shared-types';
1
+ import type { ApprovalEntityType, ApprovalJobDto, ApprovalJobStatus, ApprovalSortField, ApprovalSortOrder, ApprovalStatusCountsDto, ApprovalUseCaseDto, ClusterBulkOperationPayloadDto, ClusterBulkOperationType } from '@camunda/ccma-shared-types';
2
2
  import type { RequestBaseParams } from '../../types/general.types.js';
3
3
  export interface ApprovalPageDto {
4
4
  approvals: ApprovalJobDto[];
@@ -199,18 +199,6 @@ export declare class ControlApprovalsService {
199
199
  * approving needs, and the reason is necessary.
200
200
  */
201
201
  static createExtendTrial(params: CreateApprovalRequest<ExtendTrialPayloadDto>): Promise<ApprovalJobDto>;
202
- /**
203
- * Opens a feature-flag-override request.
204
- *
205
- * Opening one needs the feature flag permission, the same one that
206
- * approving needs, and the reason is necessary. A stage override that names
207
- * `prod` needs the prod permission on top, to ask AND to approve.
208
- *
209
- * The payload holds a number of DAYS and not a date. Every override ends:
210
- * without a deadline the store becomes a second, invisible layer of
211
- * configuration.
212
- */
213
- static createFeatureFlagOverride(params: CreateApprovalRequest<FeatureFlagOverridePayloadDto>): Promise<ApprovalJobDto>;
214
202
  /**
215
203
  * The shared half of every call that answers one request.
216
204
  *
@@ -235,25 +235,6 @@ class ControlApprovalsService {
235
235
  );
236
236
  return approval;
237
237
  }
238
- /**
239
- * Opens a feature-flag-override request.
240
- *
241
- * Opening one needs the feature flag permission, the same one that
242
- * approving needs, and the reason is necessary. A stage override that names
243
- * `prod` needs the prod permission on top, to ask AND to approve.
244
- *
245
- * The payload holds a number of DAYS and not a date. Every override ends:
246
- * without a deadline the store becomes a second, invisible layer of
247
- * configuration.
248
- */
249
- static async createFeatureFlagOverride(params) {
250
- const { approval } = await ControlApprovalsService.create(
251
- params,
252
- "createFeatureFlagOverride",
253
- "api/approvals/feature-flag-override"
254
- );
255
- return approval;
256
- }
257
238
  /**
258
239
  * The shared half of every call that answers one request.
259
240
  *
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/services/control/control.approvals.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 ApprovalEntityType,\n ApprovalJobDto,\n ApprovalJobStatus,\n ApprovalSortField,\n ApprovalSortOrder,\n ApprovalStatusCountsDto,\n ApprovalUseCaseDto,\n ClusterBulkOperationPayloadDto,\n ClusterBulkOperationType,\n FeatureFlagOverridePayloadDto,\n} from '@camunda/ccma-shared-types'\nimport { CLUSTER_BULK_OPERATION_RULES } from '@camunda/ccma-shared-types'\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 * The four-eyes approval surface of control-backend.\n *\n * One service for the whole mechanism, and not one for each use case. The\n * generic routes (list, read, approve, reject, withdraw, run) carry no\n * knowledge of a use case, which is what lets one view in control-frontend\n * show every privileged action. Only the create calls are per use case,\n * because a payload can be validated only for the use case it belongs to.\n *\n * Every route answers 404 while the feature flag `control.approvals` is off on\n * the stage, and while the service reaches no stage at all.\n */\n\nconst BASE = { id: 'control' } as const\n\nexport interface ApprovalPageDto {\n approvals: ApprovalJobDto[]\n page: {\n limit: number\n offset: number\n /** True when the next page holds more. Never read the length instead. */\n hasMore: boolean\n }\n}\n\n/**\n * What narrows the queue. The list and the counts read the same fields.\n *\n * `entityType` and `entityId` are ONE filter, not two. control-backend answers\n * 400 for one without the other, so the type carries both or neither.\n */\nexport type ApprovalFacetParams = {\n type?: string\n /**\n * One organization, matched whole against `orgId` of the payload.\n *\n * It reads only the payload, so `entityType`/`entityId` is wider. That filter\n * reads the entity link, so it finds a request that touches the organization\n * through another field. The organization page of control-frontend asks the\n * entity-link filter, not this one. `orgId` is also not `q`: the free text\n * reads the whole payload as text.\n */\n orgId?: string\n /**\n * Free text. control-backend matches it in the reason, the person who\n * opened the request, the action and its label, and the payload that the\n * summary shows.\n */\n q?: string\n /**\n * The queue of one person: what they opened, or what waits for them.\n *\n * `actionable` is the rule of the Approve button asked in SQL \u2014 pending,\n * opened by somebody else, not approved by this person yet, and guarded by\n * permissions that they hold.\n */\n mine?: 'created' | 'actionable'\n} & (\n | {\n /**\n * The kind of entity a request must touch. Read with `entityId`.\n *\n * It reads the entity link of control-backend, so it is wider and truer\n * than `orgId`: a cluster-move request answers for its source\n * organization, its target organization AND the cluster. The organization\n * and cluster detail pages of control-frontend ask this.\n */\n entityType: ApprovalEntityType\n /** The id of the entity a request must touch. Read with `entityType`. */\n entityId: string\n }\n | { entityType?: never; entityId?: never }\n)\n\nexport interface ListApprovalsRequest extends RequestBaseParams {\n params?: ApprovalFacetParams & {\n status?: ApprovalJobStatus\n sort?: ApprovalSortField\n order?: ApprovalSortOrder\n limit?: number\n offset?: number\n }\n}\n\nexport interface CountApprovalsRequest extends RequestBaseParams {\n params?: ApprovalFacetParams\n}\n\nexport interface ApprovalIdRequest extends RequestBaseParams {\n params: { id: string }\n}\n\nexport interface RejectApprovalRequest extends RequestBaseParams {\n params: { id: string; reason: string }\n}\n\n/** A withdrawal says why, or says nothing. The reason is not demanded. */\nexport interface WithdrawApprovalRequest extends RequestBaseParams {\n params: { id: string; reason?: string }\n}\n\n/** The envelope that every create route of every use case shares. */\nexport interface CreateApprovalRequest<TPayload> extends RequestBaseParams {\n params: {\n payload: TPayload\n reason?: string\n /** More approvals than the floor of the use case. Never fewer. */\n requiredAcks?: number\n }\n}\n\nexport interface SendToExpiredTrialPayloadDto {\n orgId: string\n orgName?: string\n}\n\nexport interface ExtendTrialPayloadDto {\n orgId: string\n orgName?: string\n /**\n * Days of trial counted from the moment the action RUNS.\n *\n * Never a date. Accounts holds the new expiry inside a window that it\n * measures when the call arrives, and under approvals that is days after the\n * person chose the value. See `use-cases/extend-trial.ts` in\n * control-backend.\n */\n days: number\n}\n\nexport interface MoveClusterOrgPayloadDto {\n clusterId: string\n /** A copy, so a list shows a name that a person knows. Nothing reads it. */\n clusterName?: string\n targetOrgId: string\n /** A copy, for the same reason as `clusterName`. */\n targetOrgName?: string\n // The SOURCE organization is not here on purpose. control-backend resolves it\n // server-side from the cluster's current owner, so a caller cannot attach the\n // request to an organization the cluster never belonged to.\n}\n\n/**\n * The query string of a read, with the fields that carry no value dropped.\n *\n * An empty value is not the same as an absent one: `q=` would reach the schema\n * of control-backend, which holds the text at one character and answers 400.\n */\nfunction querySuffix(params: object = {}): string {\n const query = new URLSearchParams()\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== '') {\n query.set(key, String(value))\n }\n }\n return query.size > 0 ? `?${query.toString()}` : ''\n}\n\nfunction contextFor(\n name: string,\n url: string,\n normalizedUrl: string,\n type: 'get' | 'post',\n source: string,\n): RequestBaseServiceContext {\n return {\n method: {\n name: `ControlApprovalsService.${name}`,\n url,\n normalizedUrl,\n type,\n },\n source,\n target: { name: 'control', type: 'internal' },\n }\n}\n\nexport class ControlApprovalsService {\n /**\n * The registered privileged actions, with the rules that guard each one.\n *\n * A frontend reads this to run the same policy as the service, so it never\n * holds a copy of the registry. A new use case then needs no frontend\n * deploy.\n */\n public static async listUseCases(\n params: RequestBaseParams,\n ): Promise<ApprovalUseCaseDto[]> {\n const serviceContext = contextFor(\n 'listUseCases',\n 'api/approvals/use-cases',\n 'api/approvals/use-cases',\n 'get',\n params.config.metrics?.source || 'unset',\n )\n const response = await request<{ useCases: ApprovalUseCaseDto[] }>({\n config: params.config,\n base: BASE,\n url: serviceContext.method.url,\n method: 'get',\n metricsDetails: serviceContext,\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 return response.body!.useCases\n }\n\n /**\n * One page of the requests that the caller can read.\n *\n * It is not the whole table: control-backend hides a request whose use case\n * the caller cannot act on, unless the caller opened it.\n */\n public static async list(\n params: ListApprovalsRequest,\n ): Promise<ApprovalPageDto> {\n const suffix = querySuffix(params.params)\n const serviceContext = contextFor(\n 'list',\n `api/approvals${suffix}`,\n 'api/approvals',\n 'get',\n params.config.metrics?.source || 'unset',\n )\n const response = await request<ApprovalPageDto>({\n config: params.config,\n base: BASE,\n url: serviceContext.method.url,\n method: 'get',\n metricsDetails: serviceContext,\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 return response.body!\n }\n\n /**\n * How many requests each status holds, for the status filters of a queue.\n *\n * It is the same question as `list` without the status, so a filter that\n * shows a number never opens an empty page.\n */\n public static async counts(\n params: CountApprovalsRequest,\n ): Promise<ApprovalStatusCountsDto> {\n const suffix = querySuffix(params.params)\n const serviceContext = contextFor(\n 'counts',\n `api/approvals/counts${suffix}`,\n 'api/approvals/counts',\n 'get',\n params.config.metrics?.source || 'unset',\n )\n const response = await request<{ counts: ApprovalStatusCountsDto }>({\n config: params.config,\n base: BASE,\n url: serviceContext.method.url,\n method: 'get',\n metricsDetails: serviceContext,\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 return response.body!.counts\n }\n\n public static async get(params: ApprovalIdRequest): Promise<ApprovalJobDto> {\n return ControlApprovalsService.readOne(\n params,\n 'get',\n `api/approvals/${params.params.id}`,\n 'api/approvals/:id',\n 'get',\n )\n }\n\n /**\n * What a create route answers, plus whether it opened a NEW request.\n *\n * The route answers 201 for a fresh request and 200 for the one already\n * open for the same target \u2014 `create` deduplicates. A caller that must\n * tell the two apart (a floor-of-one use case, where 200 is the ordinary\n * path and not a failure) reads `created`.\n */\n public static async createMoveClusterOrg(\n params: CreateApprovalRequest<MoveClusterOrgPayloadDto>,\n ): Promise<{ approval: ApprovalJobDto; created: boolean }> {\n return ControlApprovalsService.create(\n params,\n 'createMoveClusterOrg',\n 'api/approvals/move-cluster-org',\n )\n }\n\n /**\n * Opens a cluster bulk operation of one type.\n *\n * Each type is its own use case with its own route, because each one has\n * its own permission. The floor of approvals is the same for every type,\n * and it depends on the stage: 2 on prod, 1 on dev, int and local. The\n * route is `api/approvals/<use case>`, as for every other use case. A 200\n * means that the same operation over the same clusters is already open.\n */\n public static async createClusterBulkOperation<\n T extends ClusterBulkOperationType,\n >(\n type: T,\n params: CreateApprovalRequest<ClusterBulkOperationPayloadDto<T>>,\n ): Promise<{ approval: ApprovalJobDto; created: boolean }> {\n const useCase = CLUSTER_BULK_OPERATION_RULES[type].useCase\n return ControlApprovalsService.create(\n params,\n 'createClusterBulkOperation',\n `api/approvals/${useCase}`,\n )\n }\n\n /** Adds the approval of the caller. The last one can run the action. */\n public static async acknowledge(\n params: ApprovalIdRequest,\n ): Promise<ApprovalJobDto> {\n return ControlApprovalsService.readOne(\n params,\n 'acknowledge',\n `api/approvals/${params.params.id}/ack`,\n 'api/approvals/:id/ack',\n 'post',\n )\n }\n\n /** Closes a request without running the action. The reason is necessary. */\n public static async reject(\n params: RejectApprovalRequest,\n ): Promise<ApprovalJobDto> {\n return ControlApprovalsService.readOne(\n params,\n 'reject',\n `api/approvals/${params.params.id}/reject`,\n 'api/approvals/:id/reject',\n 'post',\n { reason: params.params.reason },\n )\n }\n\n /**\n * Takes back a request. Only the person who opened it can.\n *\n * The reason is optional, and it lands in the same field that a rejection\n * writes. The creator cannot reject their own request, so this is where they\n * say why they closed it.\n */\n public static async withdraw(\n params: WithdrawApprovalRequest,\n ): Promise<ApprovalJobDto> {\n return ControlApprovalsService.readOne(\n params,\n 'withdraw',\n `api/approvals/${params.params.id}/withdraw`,\n 'api/approvals/:id/withdraw',\n 'post',\n params.params.reason ? { reason: params.params.reason } : undefined,\n )\n }\n\n /**\n * Runs a request that holds its approvals.\n *\n * Only a use case in the explicit mode needs this. An action that throws\n * leaves the request `failed`, and this call still answers 200 with it, so\n * read `status` and `executionError` of the answer.\n */\n public static async execute(\n params: ApprovalIdRequest,\n ): Promise<ApprovalJobDto> {\n return ControlApprovalsService.readOne(\n params,\n 'execute',\n `api/approvals/${params.params.id}/execute`,\n 'api/approvals/:id/execute',\n 'post',\n )\n }\n\n /** Opens a send-to-expired-trial request. Any signed-in Camundi can. */\n public static async createSendToExpiredTrial(\n params: CreateApprovalRequest<SendToExpiredTrialPayloadDto>,\n ): Promise<ApprovalJobDto> {\n const { approval } = await ControlApprovalsService.create(\n params,\n 'createSendToExpiredTrial',\n 'api/approvals/send-to-expired-trial',\n )\n return approval\n }\n\n /**\n * Opens an extend-trial request.\n *\n * Opening one needs `accounts:admin:extendTrial`, the same permission that\n * approving needs, and the reason is necessary.\n */\n public static async createExtendTrial(\n params: CreateApprovalRequest<ExtendTrialPayloadDto>,\n ): Promise<ApprovalJobDto> {\n const { approval } = await ControlApprovalsService.create(\n params,\n 'createExtendTrial',\n 'api/approvals/extend-trial',\n )\n return approval\n }\n\n /**\n * Opens a feature-flag-override request.\n *\n * Opening one needs the feature flag permission, the same one that\n * approving needs, and the reason is necessary. A stage override that names\n * `prod` needs the prod permission on top, to ask AND to approve.\n *\n * The payload holds a number of DAYS and not a date. Every override ends:\n * without a deadline the store becomes a second, invisible layer of\n * configuration.\n */\n public static async createFeatureFlagOverride(\n params: CreateApprovalRequest<FeatureFlagOverridePayloadDto>,\n ): Promise<ApprovalJobDto> {\n const { approval } = await ControlApprovalsService.create(\n params,\n 'createFeatureFlagOverride',\n 'api/approvals/feature-flag-override',\n )\n return approval\n }\n\n /**\n * The shared half of every call that answers one request.\n *\n * Six routes answer `{ approval }` and differ only in the verb and the path.\n * One private helper keeps the metrics context, the status check and the\n * unwrapping in one place.\n */\n private static async readOne(\n params: RequestBaseParams,\n name: string,\n url: string,\n normalizedUrl: string,\n method: 'get' | 'post',\n body?: Record<string, unknown>,\n ): Promise<ApprovalJobDto> {\n const serviceContext = contextFor(\n name,\n url,\n normalizedUrl,\n method,\n params.config.metrics?.source || 'unset',\n )\n const response = await request<{ approval: ApprovalJobDto }>({\n config: params.config,\n base: BASE,\n url,\n method,\n metricsDetails: serviceContext,\n body,\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 return response.body!.approval\n }\n\n /**\n * The shared half of every create route. See `approvalRequestBody`.\n *\n * The route answers 201 for a fresh request and 200 for the one already\n * open for the same target (`create` deduplicates). `created` carries that\n * difference. `createSendToExpiredTrial` and `createExtendTrial` do not\n * need it, so they read `approval` alone off this result.\n */\n private static async create<TPayload>(\n params: CreateApprovalRequest<TPayload>,\n name: string,\n url: string,\n ): Promise<{ approval: ApprovalJobDto; created: boolean }> {\n const serviceContext = contextFor(\n name,\n url,\n url,\n 'post',\n params.config.metrics?.source || 'unset',\n )\n const response = await request<{ approval: ApprovalJobDto }>({\n config: params.config,\n base: BASE,\n url,\n method: 'post',\n metricsDetails: serviceContext,\n body: {\n payload: params.params.payload,\n reason: params.params.reason,\n requiredAcks: params.params.requiredAcks,\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 return {\n approval: response.body!.approval,\n created: response.status === 201,\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 {\n ApprovalEntityType,\n ApprovalJobDto,\n ApprovalJobStatus,\n ApprovalSortField,\n ApprovalSortOrder,\n ApprovalStatusCountsDto,\n ApprovalUseCaseDto,\n ClusterBulkOperationPayloadDto,\n ClusterBulkOperationType,\n} from '@camunda/ccma-shared-types'\nimport { CLUSTER_BULK_OPERATION_RULES } from '@camunda/ccma-shared-types'\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 * The four-eyes approval surface of control-backend.\n *\n * One service for the whole mechanism, and not one for each use case. The\n * generic routes (list, read, approve, reject, withdraw, run) carry no\n * knowledge of a use case, which is what lets one view in control-frontend\n * show every privileged action. Only the create calls are per use case,\n * because a payload can be validated only for the use case it belongs to.\n *\n * Every route answers 404 while the feature flag `control.approvals` is off on\n * the stage, and while the service reaches no stage at all.\n */\n\nconst BASE = { id: 'control' } as const\n\nexport interface ApprovalPageDto {\n approvals: ApprovalJobDto[]\n page: {\n limit: number\n offset: number\n /** True when the next page holds more. Never read the length instead. */\n hasMore: boolean\n }\n}\n\n/**\n * What narrows the queue. The list and the counts read the same fields.\n *\n * `entityType` and `entityId` are ONE filter, not two. control-backend answers\n * 400 for one without the other, so the type carries both or neither.\n */\nexport type ApprovalFacetParams = {\n type?: string\n /**\n * One organization, matched whole against `orgId` of the payload.\n *\n * It reads only the payload, so `entityType`/`entityId` is wider. That filter\n * reads the entity link, so it finds a request that touches the organization\n * through another field. The organization page of control-frontend asks the\n * entity-link filter, not this one. `orgId` is also not `q`: the free text\n * reads the whole payload as text.\n */\n orgId?: string\n /**\n * Free text. control-backend matches it in the reason, the person who\n * opened the request, the action and its label, and the payload that the\n * summary shows.\n */\n q?: string\n /**\n * The queue of one person: what they opened, or what waits for them.\n *\n * `actionable` is the rule of the Approve button asked in SQL \u2014 pending,\n * opened by somebody else, not approved by this person yet, and guarded by\n * permissions that they hold.\n */\n mine?: 'created' | 'actionable'\n} & (\n | {\n /**\n * The kind of entity a request must touch. Read with `entityId`.\n *\n * It reads the entity link of control-backend, so it is wider and truer\n * than `orgId`: a cluster-move request answers for its source\n * organization, its target organization AND the cluster. The organization\n * and cluster detail pages of control-frontend ask this.\n */\n entityType: ApprovalEntityType\n /** The id of the entity a request must touch. Read with `entityType`. */\n entityId: string\n }\n | { entityType?: never; entityId?: never }\n)\n\nexport interface ListApprovalsRequest extends RequestBaseParams {\n params?: ApprovalFacetParams & {\n status?: ApprovalJobStatus\n sort?: ApprovalSortField\n order?: ApprovalSortOrder\n limit?: number\n offset?: number\n }\n}\n\nexport interface CountApprovalsRequest extends RequestBaseParams {\n params?: ApprovalFacetParams\n}\n\nexport interface ApprovalIdRequest extends RequestBaseParams {\n params: { id: string }\n}\n\nexport interface RejectApprovalRequest extends RequestBaseParams {\n params: { id: string; reason: string }\n}\n\n/** A withdrawal says why, or says nothing. The reason is not demanded. */\nexport interface WithdrawApprovalRequest extends RequestBaseParams {\n params: { id: string; reason?: string }\n}\n\n/** The envelope that every create route of every use case shares. */\nexport interface CreateApprovalRequest<TPayload> extends RequestBaseParams {\n params: {\n payload: TPayload\n reason?: string\n /** More approvals than the floor of the use case. Never fewer. */\n requiredAcks?: number\n }\n}\n\nexport interface SendToExpiredTrialPayloadDto {\n orgId: string\n orgName?: string\n}\n\nexport interface ExtendTrialPayloadDto {\n orgId: string\n orgName?: string\n /**\n * Days of trial counted from the moment the action RUNS.\n *\n * Never a date. Accounts holds the new expiry inside a window that it\n * measures when the call arrives, and under approvals that is days after the\n * person chose the value. See `use-cases/extend-trial.ts` in\n * control-backend.\n */\n days: number\n}\n\nexport interface MoveClusterOrgPayloadDto {\n clusterId: string\n /** A copy, so a list shows a name that a person knows. Nothing reads it. */\n clusterName?: string\n targetOrgId: string\n /** A copy, for the same reason as `clusterName`. */\n targetOrgName?: string\n // The SOURCE organization is not here on purpose. control-backend resolves it\n // server-side from the cluster's current owner, so a caller cannot attach the\n // request to an organization the cluster never belonged to.\n}\n\n/**\n * The query string of a read, with the fields that carry no value dropped.\n *\n * An empty value is not the same as an absent one: `q=` would reach the schema\n * of control-backend, which holds the text at one character and answers 400.\n */\nfunction querySuffix(params: object = {}): string {\n const query = new URLSearchParams()\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== '') {\n query.set(key, String(value))\n }\n }\n return query.size > 0 ? `?${query.toString()}` : ''\n}\n\nfunction contextFor(\n name: string,\n url: string,\n normalizedUrl: string,\n type: 'get' | 'post',\n source: string,\n): RequestBaseServiceContext {\n return {\n method: {\n name: `ControlApprovalsService.${name}`,\n url,\n normalizedUrl,\n type,\n },\n source,\n target: { name: 'control', type: 'internal' },\n }\n}\n\nexport class ControlApprovalsService {\n /**\n * The registered privileged actions, with the rules that guard each one.\n *\n * A frontend reads this to run the same policy as the service, so it never\n * holds a copy of the registry. A new use case then needs no frontend\n * deploy.\n */\n public static async listUseCases(\n params: RequestBaseParams,\n ): Promise<ApprovalUseCaseDto[]> {\n const serviceContext = contextFor(\n 'listUseCases',\n 'api/approvals/use-cases',\n 'api/approvals/use-cases',\n 'get',\n params.config.metrics?.source || 'unset',\n )\n const response = await request<{ useCases: ApprovalUseCaseDto[] }>({\n config: params.config,\n base: BASE,\n url: serviceContext.method.url,\n method: 'get',\n metricsDetails: serviceContext,\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 return response.body!.useCases\n }\n\n /**\n * One page of the requests that the caller can read.\n *\n * It is not the whole table: control-backend hides a request whose use case\n * the caller cannot act on, unless the caller opened it.\n */\n public static async list(\n params: ListApprovalsRequest,\n ): Promise<ApprovalPageDto> {\n const suffix = querySuffix(params.params)\n const serviceContext = contextFor(\n 'list',\n `api/approvals${suffix}`,\n 'api/approvals',\n 'get',\n params.config.metrics?.source || 'unset',\n )\n const response = await request<ApprovalPageDto>({\n config: params.config,\n base: BASE,\n url: serviceContext.method.url,\n method: 'get',\n metricsDetails: serviceContext,\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 return response.body!\n }\n\n /**\n * How many requests each status holds, for the status filters of a queue.\n *\n * It is the same question as `list` without the status, so a filter that\n * shows a number never opens an empty page.\n */\n public static async counts(\n params: CountApprovalsRequest,\n ): Promise<ApprovalStatusCountsDto> {\n const suffix = querySuffix(params.params)\n const serviceContext = contextFor(\n 'counts',\n `api/approvals/counts${suffix}`,\n 'api/approvals/counts',\n 'get',\n params.config.metrics?.source || 'unset',\n )\n const response = await request<{ counts: ApprovalStatusCountsDto }>({\n config: params.config,\n base: BASE,\n url: serviceContext.method.url,\n method: 'get',\n metricsDetails: serviceContext,\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 return response.body!.counts\n }\n\n public static async get(params: ApprovalIdRequest): Promise<ApprovalJobDto> {\n return ControlApprovalsService.readOne(\n params,\n 'get',\n `api/approvals/${params.params.id}`,\n 'api/approvals/:id',\n 'get',\n )\n }\n\n /**\n * What a create route answers, plus whether it opened a NEW request.\n *\n * The route answers 201 for a fresh request and 200 for the one already\n * open for the same target \u2014 `create` deduplicates. A caller that must\n * tell the two apart (a floor-of-one use case, where 200 is the ordinary\n * path and not a failure) reads `created`.\n */\n public static async createMoveClusterOrg(\n params: CreateApprovalRequest<MoveClusterOrgPayloadDto>,\n ): Promise<{ approval: ApprovalJobDto; created: boolean }> {\n return ControlApprovalsService.create(\n params,\n 'createMoveClusterOrg',\n 'api/approvals/move-cluster-org',\n )\n }\n\n /**\n * Opens a cluster bulk operation of one type.\n *\n * Each type is its own use case with its own route, because each one has\n * its own permission. The floor of approvals is the same for every type,\n * and it depends on the stage: 2 on prod, 1 on dev, int and local. The\n * route is `api/approvals/<use case>`, as for every other use case. A 200\n * means that the same operation over the same clusters is already open.\n */\n public static async createClusterBulkOperation<\n T extends ClusterBulkOperationType,\n >(\n type: T,\n params: CreateApprovalRequest<ClusterBulkOperationPayloadDto<T>>,\n ): Promise<{ approval: ApprovalJobDto; created: boolean }> {\n const useCase = CLUSTER_BULK_OPERATION_RULES[type].useCase\n return ControlApprovalsService.create(\n params,\n 'createClusterBulkOperation',\n `api/approvals/${useCase}`,\n )\n }\n\n /** Adds the approval of the caller. The last one can run the action. */\n public static async acknowledge(\n params: ApprovalIdRequest,\n ): Promise<ApprovalJobDto> {\n return ControlApprovalsService.readOne(\n params,\n 'acknowledge',\n `api/approvals/${params.params.id}/ack`,\n 'api/approvals/:id/ack',\n 'post',\n )\n }\n\n /** Closes a request without running the action. The reason is necessary. */\n public static async reject(\n params: RejectApprovalRequest,\n ): Promise<ApprovalJobDto> {\n return ControlApprovalsService.readOne(\n params,\n 'reject',\n `api/approvals/${params.params.id}/reject`,\n 'api/approvals/:id/reject',\n 'post',\n { reason: params.params.reason },\n )\n }\n\n /**\n * Takes back a request. Only the person who opened it can.\n *\n * The reason is optional, and it lands in the same field that a rejection\n * writes. The creator cannot reject their own request, so this is where they\n * say why they closed it.\n */\n public static async withdraw(\n params: WithdrawApprovalRequest,\n ): Promise<ApprovalJobDto> {\n return ControlApprovalsService.readOne(\n params,\n 'withdraw',\n `api/approvals/${params.params.id}/withdraw`,\n 'api/approvals/:id/withdraw',\n 'post',\n params.params.reason ? { reason: params.params.reason } : undefined,\n )\n }\n\n /**\n * Runs a request that holds its approvals.\n *\n * Only a use case in the explicit mode needs this. An action that throws\n * leaves the request `failed`, and this call still answers 200 with it, so\n * read `status` and `executionError` of the answer.\n */\n public static async execute(\n params: ApprovalIdRequest,\n ): Promise<ApprovalJobDto> {\n return ControlApprovalsService.readOne(\n params,\n 'execute',\n `api/approvals/${params.params.id}/execute`,\n 'api/approvals/:id/execute',\n 'post',\n )\n }\n\n /** Opens a send-to-expired-trial request. Any signed-in Camundi can. */\n public static async createSendToExpiredTrial(\n params: CreateApprovalRequest<SendToExpiredTrialPayloadDto>,\n ): Promise<ApprovalJobDto> {\n const { approval } = await ControlApprovalsService.create(\n params,\n 'createSendToExpiredTrial',\n 'api/approvals/send-to-expired-trial',\n )\n return approval\n }\n\n /**\n * Opens an extend-trial request.\n *\n * Opening one needs `accounts:admin:extendTrial`, the same permission that\n * approving needs, and the reason is necessary.\n */\n public static async createExtendTrial(\n params: CreateApprovalRequest<ExtendTrialPayloadDto>,\n ): Promise<ApprovalJobDto> {\n const { approval } = await ControlApprovalsService.create(\n params,\n 'createExtendTrial',\n 'api/approvals/extend-trial',\n )\n return approval\n }\n\n /**\n * The shared half of every call that answers one request.\n *\n * Six routes answer `{ approval }` and differ only in the verb and the path.\n * One private helper keeps the metrics context, the status check and the\n * unwrapping in one place.\n */\n private static async readOne(\n params: RequestBaseParams,\n name: string,\n url: string,\n normalizedUrl: string,\n method: 'get' | 'post',\n body?: Record<string, unknown>,\n ): Promise<ApprovalJobDto> {\n const serviceContext = contextFor(\n name,\n url,\n normalizedUrl,\n method,\n params.config.metrics?.source || 'unset',\n )\n const response = await request<{ approval: ApprovalJobDto }>({\n config: params.config,\n base: BASE,\n url,\n method,\n metricsDetails: serviceContext,\n body,\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 return response.body!.approval\n }\n\n /**\n * The shared half of every create route. See `approvalRequestBody`.\n *\n * The route answers 201 for a fresh request and 200 for the one already\n * open for the same target (`create` deduplicates). `created` carries that\n * difference. `createSendToExpiredTrial` and `createExtendTrial` do not\n * need it, so they read `approval` alone off this result.\n */\n private static async create<TPayload>(\n params: CreateApprovalRequest<TPayload>,\n name: string,\n url: string,\n ): Promise<{ approval: ApprovalJobDto; created: boolean }> {\n const serviceContext = contextFor(\n name,\n url,\n url,\n 'post',\n params.config.metrics?.source || 'unset',\n )\n const response = await request<{ approval: ApprovalJobDto }>({\n config: params.config,\n base: BASE,\n url,\n method: 'post',\n metricsDetails: serviceContext,\n body: {\n payload: params.params.payload,\n reason: params.params.reason,\n requiredAcks: params.params.requiredAcks,\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 return {\n approval: response.body!.approval,\n created: response.status === 201,\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- import type { ExternalOrganizationDto, Features } from '@camunda/ccma-shared-types';
1
+ import type { ExternalClickwrapConfigDto, ExternalOrganizationDto, Features } from '@camunda/ccma-shared-types';
2
2
  import type { RequestBaseParams } from '../../types/general.types.js';
3
3
  export type UserThemePreference = 'light' | 'dark' | 'system';
4
4
  export interface UpdateUserThemeParams extends RequestBaseParams {
@@ -38,6 +38,15 @@ export declare class ExternalAccountsService {
38
38
  * token alone.
39
39
  */
40
40
  static getFeatureFlags(params: GetFeatureFlagsParams): Promise<Features>;
41
+ /**
42
+ * The clickwrap config for the opt-in to alpha features
43
+ * (`GET external/clickwrap-config`). The deployed accounts-backend owns the
44
+ * PactSafe identifiers, thus a host does not hold a copy.
45
+ *
46
+ * A field is absent when the backend has no value for it. A host with no
47
+ * `accessId` shows no opt-in.
48
+ */
49
+ static getClickwrapConfig(params: RequestBaseParams): Promise<ExternalClickwrapConfigDto>;
41
50
  /**
42
51
  * Persist the user's theme preference (`PATCH external/user/theme`). The
43
52
  * backend stores it on the user profile and reflects it in the next access
@@ -78,6 +78,45 @@ class ExternalAccountsService {
78
78
  }
79
79
  return response.body;
80
80
  }
81
+ /**
82
+ * The clickwrap config for the opt-in to alpha features
83
+ * (`GET external/clickwrap-config`). The deployed accounts-backend owns the
84
+ * PactSafe identifiers, thus a host does not hold a copy.
85
+ *
86
+ * A field is absent when the backend has no value for it. A host with no
87
+ * `accessId` shows no opt-in.
88
+ */
89
+ static async getClickwrapConfig(params) {
90
+ const serviceContext = {
91
+ method: {
92
+ name: "ExternalAccountsService.getClickwrapConfig",
93
+ url: "external/clickwrap-config",
94
+ normalizedUrl: "external/clickwrap-config",
95
+ type: "get"
96
+ },
97
+ source: params.config.metrics?.source || "unset",
98
+ target: {
99
+ name: "accounts",
100
+ type: "internal"
101
+ }
102
+ };
103
+ const response = await request({
104
+ config: params.config,
105
+ base: { id: "accounts" },
106
+ url: serviceContext.method.url,
107
+ method: serviceContext.method.type,
108
+ metricsDetails: serviceContext,
109
+ responseType: "json"
110
+ });
111
+ if (!response.ok) {
112
+ throw new HttpError(
113
+ `${serviceContext.method.name}: ${response.status} ${response.statusText}`,
114
+ response.status,
115
+ response.statusText
116
+ );
117
+ }
118
+ return response.body ?? {};
119
+ }
81
120
  /**
82
121
  * Persist the user's theme preference (`PATCH external/user/theme`). The
83
122
  * backend stores it on the user profile and reflects it in the next access
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/services/external/external.accounts.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 ExternalOrganizationDto,\n Features,\n} from '@camunda/ccma-shared-types'\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\nexport type UserThemePreference = 'light' | 'dark' | 'system'\n\nexport interface UpdateUserThemeParams extends RequestBaseParams {\n theme: UserThemePreference\n}\n\nexport interface GetFeatureFlagsParams extends RequestBaseParams {\n params?: {\n /**\n * The organization to evaluate the org-scoped flags for. Omit it before\n * the user selects an organization. The caller must be a member, because\n * the backend answers 403 for every other organization.\n */\n orgId?: string\n }\n}\n\n/**\n * Accounts backend **external** (CORS-enabled) endpoints used to build webapp\n * navigation. Distinct from `OrgsService`/`UsersService`, which target the\n * internal accounts API with admin-scoped DTOs. Use this from any Camunda\n * webapp (console, modeler, operate, \u2026); use the internal services from\n * console/accounts' own admin surfaces.\n */\nexport class ExternalAccountsService {\n /**\n * Organizations the current user belongs to\n * (`GET external/organizations/my`). Source for the org breadcrumb and the\n * organization switcher.\n */\n public static async getMyOrganizations(\n params: RequestBaseParams,\n ): Promise<ExternalOrganizationDto[]> {\n const serviceContext: RequestBaseServiceContext = {\n method: {\n name: 'ExternalAccountsService.getMyOrganizations',\n url: 'external/organizations/my',\n normalizedUrl: 'external/organizations/my',\n type: 'get',\n },\n source: params.config.metrics?.source || 'unset',\n target: {\n name: 'accounts',\n type: 'internal',\n },\n }\n\n const response = await request<ExternalOrganizationDto[]>({\n ...params,\n base: { id: 'accounts' },\n url: serviceContext.method.url,\n method: serviceContext.method.type,\n metricsDetails: serviceContext,\n responseType: 'json',\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 /**\n * The evaluated feature flags for the current user\n * (`GET external/feature-flags`). The deployed accounts-backend decides the\n * values, thus a host does not hold a copy of the rules.\n *\n * The stage of the flags is the stage of the backend that answers, not the\n * stage that the client thinks it runs on. The email comes from the access\n * token alone.\n */\n public static async getFeatureFlags(\n params: GetFeatureFlagsParams,\n ): Promise<Features> {\n const orgId = params.params?.orgId\n const serviceContext: RequestBaseServiceContext = {\n method: {\n name: 'ExternalAccountsService.getFeatureFlags',\n url: orgId\n ? `external/feature-flags?orgId=${encodeURIComponent(orgId)}`\n : 'external/feature-flags',\n normalizedUrl: 'external/feature-flags',\n type: 'get',\n },\n source: params.config.metrics?.source || 'unset',\n target: {\n name: 'accounts',\n type: 'internal',\n },\n }\n\n const response = await request<Features>({\n config: params.config,\n base: { id: 'accounts' },\n url: serviceContext.method.url,\n method: serviceContext.method.type,\n metricsDetails: serviceContext,\n responseType: 'json',\n })\n\n if (!response.ok || !response.body) {\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 /**\n * Persist the user's theme preference (`PATCH external/user/theme`). The\n * backend stores it on the user profile and reflects it in the next access\n * token's `https://camunda.com/settings.theme` claim. Response is empty.\n */\n public static async updateUserTheme(\n params: UpdateUserThemeParams,\n ): Promise<void> {\n const serviceContext: RequestBaseServiceContext = {\n method: {\n name: 'ExternalAccountsService.updateUserTheme',\n url: 'external/user/theme',\n normalizedUrl: 'external/user/theme',\n type: 'patch',\n },\n source: params.config.metrics?.source || 'unset',\n target: {\n name: 'accounts',\n type: 'internal',\n },\n }\n\n const response = await request({\n config: params.config,\n base: { id: 'accounts' },\n url: serviceContext.method.url,\n method: serviceContext.method.type,\n metricsDetails: serviceContext,\n body: { theme: params.theme },\n responseType: 'none',\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}\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 ExternalClickwrapConfigDto,\n ExternalOrganizationDto,\n Features,\n} from '@camunda/ccma-shared-types'\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\nexport type UserThemePreference = 'light' | 'dark' | 'system'\n\nexport interface UpdateUserThemeParams extends RequestBaseParams {\n theme: UserThemePreference\n}\n\nexport interface GetFeatureFlagsParams extends RequestBaseParams {\n params?: {\n /**\n * The organization to evaluate the org-scoped flags for. Omit it before\n * the user selects an organization. The caller must be a member, because\n * the backend answers 403 for every other organization.\n */\n orgId?: string\n }\n}\n\n/**\n * Accounts backend **external** (CORS-enabled) endpoints used to build webapp\n * navigation. Distinct from `OrgsService`/`UsersService`, which target the\n * internal accounts API with admin-scoped DTOs. Use this from any Camunda\n * webapp (console, modeler, operate, \u2026); use the internal services from\n * console/accounts' own admin surfaces.\n */\nexport class ExternalAccountsService {\n /**\n * Organizations the current user belongs to\n * (`GET external/organizations/my`). Source for the org breadcrumb and the\n * organization switcher.\n */\n public static async getMyOrganizations(\n params: RequestBaseParams,\n ): Promise<ExternalOrganizationDto[]> {\n const serviceContext: RequestBaseServiceContext = {\n method: {\n name: 'ExternalAccountsService.getMyOrganizations',\n url: 'external/organizations/my',\n normalizedUrl: 'external/organizations/my',\n type: 'get',\n },\n source: params.config.metrics?.source || 'unset',\n target: {\n name: 'accounts',\n type: 'internal',\n },\n }\n\n const response = await request<ExternalOrganizationDto[]>({\n ...params,\n base: { id: 'accounts' },\n url: serviceContext.method.url,\n method: serviceContext.method.type,\n metricsDetails: serviceContext,\n responseType: 'json',\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 /**\n * The evaluated feature flags for the current user\n * (`GET external/feature-flags`). The deployed accounts-backend decides the\n * values, thus a host does not hold a copy of the rules.\n *\n * The stage of the flags is the stage of the backend that answers, not the\n * stage that the client thinks it runs on. The email comes from the access\n * token alone.\n */\n public static async getFeatureFlags(\n params: GetFeatureFlagsParams,\n ): Promise<Features> {\n const orgId = params.params?.orgId\n const serviceContext: RequestBaseServiceContext = {\n method: {\n name: 'ExternalAccountsService.getFeatureFlags',\n url: orgId\n ? `external/feature-flags?orgId=${encodeURIComponent(orgId)}`\n : 'external/feature-flags',\n normalizedUrl: 'external/feature-flags',\n type: 'get',\n },\n source: params.config.metrics?.source || 'unset',\n target: {\n name: 'accounts',\n type: 'internal',\n },\n }\n\n const response = await request<Features>({\n config: params.config,\n base: { id: 'accounts' },\n url: serviceContext.method.url,\n method: serviceContext.method.type,\n metricsDetails: serviceContext,\n responseType: 'json',\n })\n\n if (!response.ok || !response.body) {\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 /**\n * The clickwrap config for the opt-in to alpha features\n * (`GET external/clickwrap-config`). The deployed accounts-backend owns the\n * PactSafe identifiers, thus a host does not hold a copy.\n *\n * A field is absent when the backend has no value for it. A host with no\n * `accessId` shows no opt-in.\n */\n public static async getClickwrapConfig(\n params: RequestBaseParams,\n ): Promise<ExternalClickwrapConfigDto> {\n const serviceContext: RequestBaseServiceContext = {\n method: {\n name: 'ExternalAccountsService.getClickwrapConfig',\n url: 'external/clickwrap-config',\n normalizedUrl: 'external/clickwrap-config',\n type: 'get',\n },\n source: params.config.metrics?.source || 'unset',\n target: {\n name: 'accounts',\n type: 'internal',\n },\n }\n\n const response = await request<ExternalClickwrapConfigDto>({\n config: params.config,\n base: { id: 'accounts' },\n url: serviceContext.method.url,\n method: serviceContext.method.type,\n metricsDetails: serviceContext,\n responseType: 'json',\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 /**\n * Persist the user's theme preference (`PATCH external/user/theme`). The\n * backend stores it on the user profile and reflects it in the next access\n * token's `https://camunda.com/settings.theme` claim. Response is empty.\n */\n public static async updateUserTheme(\n params: UpdateUserThemeParams,\n ): Promise<void> {\n const serviceContext: RequestBaseServiceContext = {\n method: {\n name: 'ExternalAccountsService.updateUserTheme',\n url: 'external/user/theme',\n normalizedUrl: 'external/user/theme',\n type: 'patch',\n },\n source: params.config.metrics?.source || 'unset',\n target: {\n name: 'accounts',\n type: 'internal',\n },\n }\n\n const response = await request({\n config: params.config,\n base: { id: 'accounts' },\n url: serviceContext.method.url,\n method: serviceContext.method.type,\n metricsDetails: serviceContext,\n body: { theme: params.theme },\n responseType: 'none',\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}\n"],
5
+ "mappings": "AAeA,SAAS,eAAe;AACxB,SAAS,iBAAiB;AA0BnB,MAAM,wBAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMnC,aAAoB,mBAClB,QACoC;AACpC,UAAM,iBAA4C;AAAA,MAChD,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,KAAK;AAAA,QACL,eAAe;AAAA,QACf,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,WAAW,MAAM,QAAmC;AAAA,MACxD,GAAG;AAAA,MACH,MAAM,EAAE,IAAI,WAAW;AAAA,MACvB,KAAK,eAAe,OAAO;AAAA,MAC3B,QAAQ,eAAe,OAAO;AAAA,MAC9B,gBAAgB;AAAA,MAChB,cAAc;AAAA,IAChB,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,QAAQ,CAAC;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,aAAoB,gBAClB,QACmB;AACnB,UAAM,QAAQ,OAAO,QAAQ;AAC7B,UAAM,iBAA4C;AAAA,MAChD,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,KAAK,QACD,gCAAgC,mBAAmB,KAAK,CAAC,KACzD;AAAA,QACJ,eAAe;AAAA,QACf,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,WAAW,MAAM,QAAkB;AAAA,MACvC,QAAQ,OAAO;AAAA,MACf,MAAM,EAAE,IAAI,WAAW;AAAA,MACvB,KAAK,eAAe,OAAO;AAAA,MAC3B,QAAQ,eAAe,OAAO;AAAA,MAC9B,gBAAgB;AAAA,MAChB,cAAc;AAAA,IAChB,CAAC;AAED,QAAI,CAAC,SAAS,MAAM,CAAC,SAAS,MAAM;AAClC,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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,aAAoB,mBAClB,QACqC;AACrC,UAAM,iBAA4C;AAAA,MAChD,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,KAAK;AAAA,QACL,eAAe;AAAA,QACf,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,WAAW,MAAM,QAAoC;AAAA,MACzD,QAAQ,OAAO;AAAA,MACf,MAAM,EAAE,IAAI,WAAW;AAAA,MACvB,KAAK,eAAe,OAAO;AAAA,MAC3B,QAAQ,eAAe,OAAO;AAAA,MAC9B,gBAAgB;AAAA,MAChB,cAAc;AAAA,IAChB,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,QAAQ,CAAC;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,aAAoB,gBAClB,QACe;AACf,UAAM,iBAA4C;AAAA,MAChD,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,KAAK;AAAA,QACL,eAAe;AAAA,QACf,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,WAAW,MAAM,QAAQ;AAAA,MAC7B,QAAQ,OAAO;AAAA,MACf,MAAM,EAAE,IAAI,WAAW;AAAA,MACvB,KAAK,eAAe,OAAO;AAAA,MAC3B,QAAQ,eAAe,OAAO;AAAA,MAC9B,gBAAgB;AAAA,MAChB,MAAM,EAAE,OAAO,OAAO,MAAM;AAAA,MAC5B,cAAc;AAAA,IAChB,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;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/ccma-api",
3
- "version": "2.2.0",
3
+ "version": "3.0.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": "4.0.0"
44
+ "@camunda/ccma-shared-types": "5.0.0"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "node esbuild.mjs && tsc --emitDeclarationOnly --outDir dist",
@@ -1,13 +0,0 @@
1
- import type { FeatureFlagsDto } from '@camunda/ccma-shared-types';
2
- import type { RequestBaseParams } from '../../types/general.types.js';
3
- export declare class ControlFeatureFlagsService {
4
- /**
5
- * Every flag, with the value that the code gives and the override that is
6
- * stored for it.
7
- *
8
- * The answer carries BOTH values. A view that showed only the effective one
9
- * would hide the drift between the source and the store, which is the one
10
- * thing an override makes easy to lose.
11
- */
12
- static list(params: RequestBaseParams): Promise<FeatureFlagsDto>;
13
- }
@@ -1,52 +0,0 @@
1
- import { request } from "../../utils/api.js";
2
- import { HttpError } from "../../utils/errors.js";
3
- const BASE = { id: "control" };
4
- class ControlFeatureFlagsService {
5
- /**
6
- * Every flag, with the value that the code gives and the override that is
7
- * stored for it.
8
- *
9
- * The answer carries BOTH values. A view that showed only the effective one
10
- * would hide the drift between the source and the store, which is the one
11
- * thing an override makes easy to lose.
12
- */
13
- static async list(params) {
14
- const serviceContext = {
15
- method: {
16
- name: "ControlFeatureFlagsService.list",
17
- url: "api/feature-flags",
18
- normalizedUrl: "api/feature-flags",
19
- type: "get"
20
- },
21
- source: params.config.metrics?.source || "unset",
22
- target: { name: "control", type: "internal" }
23
- };
24
- const response = await request({
25
- // `...params` first, so `cacheControl` and `onCachedResponse` reach the
26
- // request. The parameter type extends `RequestBaseParams`, thus a
27
- // caller may pass them, and `request` reads `onCachedResponse` for its
28
- // stale-while-revalidate path. `config` alone advertised two options in
29
- // the type and dropped them both. The named fields below are written
30
- // after, so they still win. `control.annotations.service.ts` does the
31
- // same, for the same reason.
32
- ...params,
33
- config: params.config,
34
- base: BASE,
35
- url: serviceContext.method.url,
36
- method: "get",
37
- metricsDetails: serviceContext
38
- });
39
- if (!response.ok || !response.body) {
40
- throw new HttpError(
41
- `${serviceContext.method.name}: ${response.status} ${response.statusText}`,
42
- response.status,
43
- response.statusText
44
- );
45
- }
46
- return response.body;
47
- }
48
- }
49
- export {
50
- ControlFeatureFlagsService
51
- };
52
- //# sourceMappingURL=control.feature-flags.service.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/services/control/control.feature-flags.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 { FeatureFlagsDto } from '@camunda/ccma-shared-types'\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 * The feature flag surface of control-backend.\n *\n * It READS, and it holds no write. An override is written by the action of an\n * approved request, thus the write lives on `ControlApprovalsService` with\n * the other create routes. A service with a write here would look like a way\n * around the four-eyes rule, and there is none.\n *\n * The route answers 404 while the feature flag `control.approvals` is off on\n * the stage.\n */\n\nconst BASE = { id: 'control' } as const\n\nexport class ControlFeatureFlagsService {\n /**\n * Every flag, with the value that the code gives and the override that is\n * stored for it.\n *\n * The answer carries BOTH values. A view that showed only the effective one\n * would hide the drift between the source and the store, which is the one\n * thing an override makes easy to lose.\n */\n public static async list(\n params: RequestBaseParams,\n ): Promise<FeatureFlagsDto> {\n const serviceContext: RequestBaseServiceContext = {\n method: {\n name: 'ControlFeatureFlagsService.list',\n url: 'api/feature-flags',\n normalizedUrl: 'api/feature-flags',\n type: 'get',\n },\n source: params.config.metrics?.source || 'unset',\n target: { name: 'control', type: 'internal' },\n }\n\n const response = await request<FeatureFlagsDto>({\n // `...params` first, so `cacheControl` and `onCachedResponse` reach the\n // request. The parameter type extends `RequestBaseParams`, thus a\n // caller may pass them, and `request` reads `onCachedResponse` for its\n // stale-while-revalidate path. `config` alone advertised two options in\n // the type and dropped them both. The named fields below are written\n // after, so they still win. `control.annotations.service.ts` does the\n // same, for the same reason.\n ...params,\n config: params.config,\n base: BASE,\n url: serviceContext.method.url,\n method: 'get',\n metricsDetails: serviceContext,\n })\n\n if (!response.ok || !response.body) {\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": "AAWA,SAAS,eAAe;AACxB,SAAS,iBAAiB;AAc1B,MAAM,OAAO,EAAE,IAAI,UAAU;AAEtB,MAAM,2BAA2B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAStC,aAAoB,KAClB,QAC0B;AAC1B,UAAM,iBAA4C;AAAA,MAChD,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,KAAK;AAAA,QACL,eAAe;AAAA,QACf,MAAM;AAAA,MACR;AAAA,MACA,QAAQ,OAAO,OAAO,SAAS,UAAU;AAAA,MACzC,QAAQ,EAAE,MAAM,WAAW,MAAM,WAAW;AAAA,IAC9C;AAEA,UAAM,WAAW,MAAM,QAAyB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQ9C,GAAG;AAAA,MACH,QAAQ,OAAO;AAAA,MACf,MAAM;AAAA,MACN,KAAK,eAAe,OAAO;AAAA,MAC3B,QAAQ;AAAA,MACR,gBAAgB;AAAA,IAClB,CAAC;AAED,QAAI,CAAC,SAAS,MAAM,CAAC,SAAS,MAAM;AAClC,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": []
7
- }