@camunda/ccma-shared-types 0.5.0 → 0.6.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.
- package/dist/_tests/utils/failoverKeyPolicy.utils.spec.d.ts +1 -0
- package/dist/constants/misc.const.d.ts +75 -55
- package/dist/constants/misc.const.js +12 -82
- package/dist/constants/misc.const.js.map +2 -2
- package/dist/dtos/CommonCluster.dto.d.ts +24 -1
- package/dist/dtos/ControlV2.dto.d.ts +17 -3
- package/dist/dtos/SalesPlan.dto.d.ts +11 -0
- package/dist/dtos/SalesPlan.dto.js +14 -0
- package/dist/dtos/SalesPlan.dto.js.map +2 -2
- package/dist/enums/AuditEntity.enum.d.ts +2 -0
- package/dist/enums/AuditEntity.enum.js +2 -0
- package/dist/enums/AuditEntity.enum.js.map +2 -2
- package/dist/enums/ConsoleApiClientPermission.enum.d.ts +6 -1
- package/dist/enums/ConsoleApiClientPermission.enum.js +4 -0
- package/dist/enums/ConsoleApiClientPermission.enum.js.map +2 -2
- package/dist/enums/ErrorType.enum.d.ts +1 -0
- package/dist/enums/ErrorType.enum.js +1 -0
- package/dist/enums/ErrorType.enum.js.map +2 -2
- package/dist/index.d.ts +11 -5
- package/dist/index.js +49 -3
- package/dist/index.js.map +2 -2
- package/dist/types/ClusterPlanModification.type.d.ts +2 -0
- package/dist/types/HdpTenantUsage.type.d.ts +32 -0
- package/dist/types/TrialManagement.types.d.ts +63 -0
- package/dist/types/TrialManagement.types.js +1 -0
- package/dist/types/TrialManagement.types.js.map +7 -0
- package/dist/utils/cloudProvider.utils.d.ts +18 -0
- package/dist/utils/cloudProvider.utils.js +23 -0
- package/dist/utils/cloudProvider.utils.js.map +7 -0
- package/dist/utils/cronSchedule.utils.d.ts +42 -0
- package/dist/utils/cronSchedule.utils.js +161 -0
- package/dist/utils/cronSchedule.utils.js.map +7 -0
- package/dist/utils/cronSchedule.utils.spec.d.ts +1 -0
- package/dist/utils/enterpriseTerm.utils.d.ts +78 -0
- package/dist/utils/enterpriseTerm.utils.js +69 -0
- package/dist/utils/enterpriseTerm.utils.js.map +7 -0
- package/dist/utils/enterpriseTerm.utils.spec.d.ts +1 -0
- package/dist/utils/failoverKeyPolicy.utils.d.ts +26 -0
- package/dist/utils/failoverKeyPolicy.utils.js +15 -0
- package/dist/utils/failoverKeyPolicy.utils.js.map +7 -0
- package/dist/utils/string-validation.utils.d.ts +17 -0
- package/dist/utils/string-validation.utils.js +8 -1
- package/dist/utils/string-validation.utils.js.map +2 -2
- package/dist/utils/versionCheck.utils.d.ts +12 -0
- package/dist/utils/versionCheck.utils.js +37 -1
- package/dist/utils/versionCheck.utils.js.map +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { HOTBACKUP_MIN_CREATE_PERIOD } from "../constants/misc.const.js";
|
|
2
|
+
const BACKUP_SCHEDULE_MIN_INTERVAL_MINUTES = HOTBACKUP_MIN_CREATE_PERIOD / (60 * 1e3);
|
|
3
|
+
const MINUTES_PER_DAY = 24 * 60;
|
|
4
|
+
const MONTH_NAMES = [
|
|
5
|
+
"JAN",
|
|
6
|
+
"FEB",
|
|
7
|
+
"MAR",
|
|
8
|
+
"APR",
|
|
9
|
+
"MAY",
|
|
10
|
+
"JUN",
|
|
11
|
+
"JUL",
|
|
12
|
+
"AUG",
|
|
13
|
+
"SEP",
|
|
14
|
+
"OCT",
|
|
15
|
+
"NOV",
|
|
16
|
+
"DEC"
|
|
17
|
+
];
|
|
18
|
+
const DAY_NAMES = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
|
|
19
|
+
const CRON_FIELDS = [
|
|
20
|
+
{ name: "minute", min: 0, max: 59 },
|
|
21
|
+
{ name: "hour", min: 0, max: 23 },
|
|
22
|
+
{ name: "day of month", min: 1, max: 31 },
|
|
23
|
+
{ name: "month", min: 1, max: 12, names: MONTH_NAMES },
|
|
24
|
+
{ name: "day of week", min: 0, max: 6, maxInput: 7, names: DAY_NAMES }
|
|
25
|
+
];
|
|
26
|
+
function validateCronSchedule(schedule, options = {}) {
|
|
27
|
+
const minIntervalMinutes = options.minIntervalMinutes ?? BACKUP_SCHEDULE_MIN_INTERVAL_MINUTES;
|
|
28
|
+
if (typeof schedule !== "string" || schedule.trim().length === 0) {
|
|
29
|
+
return { valid: false, reason: "Schedule must be a non-empty cron string" };
|
|
30
|
+
}
|
|
31
|
+
const fields = schedule.trim().split(/\s+/u);
|
|
32
|
+
if (fields.length !== CRON_FIELDS.length) {
|
|
33
|
+
return {
|
|
34
|
+
valid: false,
|
|
35
|
+
reason: `Schedule must be a cron expression with ${CRON_FIELDS.length} fields (minute hour day-of-month month day-of-week), got ${fields.length}`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const values = [];
|
|
39
|
+
for (const [index, spec] of CRON_FIELDS.entries()) {
|
|
40
|
+
const parsed = parseField(fields[index], spec);
|
|
41
|
+
if ("reason" in parsed) {
|
|
42
|
+
return { valid: false, reason: parsed.reason };
|
|
43
|
+
}
|
|
44
|
+
values.push(parsed.values);
|
|
45
|
+
}
|
|
46
|
+
const [minutes, hours] = values;
|
|
47
|
+
const gap = smallestIntervalMinutes(minutes, hours);
|
|
48
|
+
if (gap < minIntervalMinutes) {
|
|
49
|
+
return {
|
|
50
|
+
valid: false,
|
|
51
|
+
reason: `Schedule runs every ${gap} minute(s); backups can be scheduled at most once every ${minIntervalMinutes} minutes`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { valid: true, minIntervalMinutes: gap };
|
|
55
|
+
}
|
|
56
|
+
function smallestIntervalMinutes(minutes, hours) {
|
|
57
|
+
let smallest = MINUTES_PER_DAY;
|
|
58
|
+
for (let index = 1; index < minutes.length; index++) {
|
|
59
|
+
smallest = Math.min(smallest, minutes[index] - minutes[index - 1]);
|
|
60
|
+
}
|
|
61
|
+
const firstMinute = minutes[0];
|
|
62
|
+
const lastMinute = minutes[minutes.length - 1];
|
|
63
|
+
let smallestHourGap = 24;
|
|
64
|
+
for (let index = 1; index < hours.length; index++) {
|
|
65
|
+
smallestHourGap = Math.min(smallestHourGap, hours[index] - hours[index - 1]);
|
|
66
|
+
}
|
|
67
|
+
if (hours.length > 1) {
|
|
68
|
+
smallestHourGap = Math.min(
|
|
69
|
+
smallestHourGap,
|
|
70
|
+
24 - hours[hours.length - 1] + hours[0]
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return Math.min(
|
|
74
|
+
smallest,
|
|
75
|
+
smallestHourGap * 60 - lastMinute + firstMinute,
|
|
76
|
+
MINUTES_PER_DAY
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
function parseField(field, spec) {
|
|
80
|
+
const collected = /* @__PURE__ */ new Set();
|
|
81
|
+
for (const part of field.split(",")) {
|
|
82
|
+
const parsed = parsePart(part, spec);
|
|
83
|
+
if ("reason" in parsed) {
|
|
84
|
+
return parsed;
|
|
85
|
+
}
|
|
86
|
+
for (const value of parsed.values) {
|
|
87
|
+
collected.add(value);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (collected.size === 0) {
|
|
91
|
+
return { reason: `Invalid ${spec.name} field: "${field}"` };
|
|
92
|
+
}
|
|
93
|
+
return { values: Array.from(collected).sort((a, b) => a - b) };
|
|
94
|
+
}
|
|
95
|
+
function parsePart(part, spec) {
|
|
96
|
+
const invalid = { reason: `Invalid ${spec.name} field: "${part}"` };
|
|
97
|
+
const [rangePart, stepPart, ...rest] = part.split("/");
|
|
98
|
+
if (rest.length > 0 || rangePart === void 0) {
|
|
99
|
+
return invalid;
|
|
100
|
+
}
|
|
101
|
+
const maxInput = spec.maxInput ?? spec.max;
|
|
102
|
+
let step = 1;
|
|
103
|
+
if (stepPart !== void 0) {
|
|
104
|
+
if (!/^\d+$/u.test(stepPart)) {
|
|
105
|
+
return invalid;
|
|
106
|
+
}
|
|
107
|
+
step = Number(stepPart);
|
|
108
|
+
if (step < 1) {
|
|
109
|
+
return invalid;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
let from;
|
|
113
|
+
let to;
|
|
114
|
+
if (rangePart === "*") {
|
|
115
|
+
from = spec.min;
|
|
116
|
+
to = spec.max;
|
|
117
|
+
} else if (rangePart.includes("-")) {
|
|
118
|
+
const bounds = rangePart.split("-");
|
|
119
|
+
if (bounds.length !== 2) {
|
|
120
|
+
return invalid;
|
|
121
|
+
}
|
|
122
|
+
const start = toValue(bounds[0], spec);
|
|
123
|
+
const end = toValue(bounds[1], spec);
|
|
124
|
+
if (start === void 0 || end === void 0 || start > end) {
|
|
125
|
+
return invalid;
|
|
126
|
+
}
|
|
127
|
+
from = start;
|
|
128
|
+
to = end;
|
|
129
|
+
} else {
|
|
130
|
+
const single = toValue(rangePart, spec);
|
|
131
|
+
if (single === void 0) {
|
|
132
|
+
return invalid;
|
|
133
|
+
}
|
|
134
|
+
from = single;
|
|
135
|
+
to = stepPart === void 0 ? single : maxInput;
|
|
136
|
+
}
|
|
137
|
+
const values = [];
|
|
138
|
+
for (let value = from; value <= to; value += step) {
|
|
139
|
+
values.push(value > spec.max ? spec.min : value);
|
|
140
|
+
}
|
|
141
|
+
return { values };
|
|
142
|
+
}
|
|
143
|
+
function toValue(raw, spec) {
|
|
144
|
+
const trimmed = raw.trim();
|
|
145
|
+
let value;
|
|
146
|
+
if (/^\d+$/u.test(trimmed)) {
|
|
147
|
+
value = Number(trimmed);
|
|
148
|
+
} else {
|
|
149
|
+
const nameIndex = spec.names?.indexOf(trimmed.toUpperCase()) ?? -1;
|
|
150
|
+
if (nameIndex < 0) {
|
|
151
|
+
return void 0;
|
|
152
|
+
}
|
|
153
|
+
value = nameIndex + spec.min;
|
|
154
|
+
}
|
|
155
|
+
return value >= spec.min && value <= (spec.maxInput ?? spec.max) ? value : void 0;
|
|
156
|
+
}
|
|
157
|
+
export {
|
|
158
|
+
BACKUP_SCHEDULE_MIN_INTERVAL_MINUTES,
|
|
159
|
+
validateCronSchedule
|
|
160
|
+
};
|
|
161
|
+
//# sourceMappingURL=cronSchedule.utils.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/utils/cronSchedule.utils.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 { HOTBACKUP_MIN_CREATE_PERIOD } from '../constants/misc.const.js'\n\n/**\n * Smallest gap we allow between two runs of a backup schedule, in minutes.\n * Same 15-minute floor the manual (on-demand) create throttle uses \u2014 a\n * schedule must not be able to produce backups faster than a customer could\n * trigger them by hand.\n */\nexport const BACKUP_SCHEDULE_MIN_INTERVAL_MINUTES =\n HOTBACKUP_MIN_CREATE_PERIOD / (60 * 1000)\n\n/**\n * Gap reported for a schedule that fires at most once per day \u2014 used as the\n * \"no further constraint\" ceiling so callers never see `Infinity`.\n */\nconst MINUTES_PER_DAY = 24 * 60\n\nconst MONTH_NAMES = [\n 'JAN',\n 'FEB',\n 'MAR',\n 'APR',\n 'MAY',\n 'JUN',\n 'JUL',\n 'AUG',\n 'SEP',\n 'OCT',\n 'NOV',\n 'DEC',\n]\n\nconst DAY_NAMES = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']\n\ninterface CronFieldSpec {\n name: string\n min: number\n /** Largest value the field can produce, after alias normalization. */\n max: number\n /**\n * Largest value accepted on input, when the field has an out-of-range alias:\n * cron's `7` for Sunday, which is normalized to `0`. Defaults to `max`.\n */\n maxInput?: number\n names?: string[]\n}\n\nconst CRON_FIELDS: CronFieldSpec[] = [\n { name: 'minute', min: 0, max: 59 },\n { name: 'hour', min: 0, max: 23 },\n { name: 'day of month', min: 1, max: 31 },\n { name: 'month', min: 1, max: 12, names: MONTH_NAMES },\n { name: 'day of week', min: 0, max: 6, maxInput: 7, names: DAY_NAMES },\n]\n\nexport interface CronScheduleValidationSuccess {\n valid: true\n /**\n * Smallest gap between two consecutive runs, in minutes. Capped at one day:\n * day-of-month / day-of-week restrictions only ever push runs further apart,\n * so they are deliberately not taken into account.\n */\n minIntervalMinutes: number\n}\n\nexport interface CronScheduleValidationFailure {\n valid: false\n /** Human-readable, safe to hand straight back to an API caller. */\n reason: string\n}\n\nexport type CronScheduleValidationResult =\n | CronScheduleValidationSuccess\n | CronScheduleValidationFailure\n\n/**\n * Validates a standard five-field cron expression (`minute hour day-of-month\n * month day-of-week`) \u2014 the syntax the operator's `BackupSchedule` CR expects \u2014\n * and computes how often it fires.\n *\n * Kept dependency-free on purpose: console-backend has no cron library, and the\n * K8s API would otherwise be the first thing to see (and reject) a malformed\n * expression, turning a customer typo into a 500.\n *\n * Supported per field: `*`, single values, lists (`0,30`), ranges (`1-5`) and\n * steps \u2014 `0-30/10`, `5/30`, or `*` followed by `/15`. No whitespace is allowed\n * around the `/` (fields are split on whitespace first, so a spaced-out step\n * reads as extra fields). Month and day-of-week additionally accept their\n * three-letter names. Seconds (a sixth field) and non-standard macros\n * (`@daily`, `L`, `W`, `#`) are rejected \u2014 the operator does not support them\n * either.\n */\nexport function validateCronSchedule(\n schedule: string,\n options: { minIntervalMinutes?: number } = {},\n): CronScheduleValidationResult {\n const minIntervalMinutes =\n options.minIntervalMinutes ?? BACKUP_SCHEDULE_MIN_INTERVAL_MINUTES\n\n if (typeof schedule !== 'string' || schedule.trim().length === 0) {\n return { valid: false, reason: 'Schedule must be a non-empty cron string' }\n }\n\n const fields = schedule.trim().split(/\\s+/u)\n if (fields.length !== CRON_FIELDS.length) {\n return {\n valid: false,\n reason: `Schedule must be a cron expression with ${CRON_FIELDS.length} fields (minute hour day-of-month month day-of-week), got ${fields.length}`,\n }\n }\n\n const values: number[][] = []\n for (const [index, spec] of CRON_FIELDS.entries()) {\n const parsed = parseField(fields[index], spec)\n if ('reason' in parsed) {\n return { valid: false, reason: parsed.reason }\n }\n values.push(parsed.values)\n }\n\n const [minutes, hours] = values\n const gap = smallestIntervalMinutes(minutes, hours)\n\n if (gap < minIntervalMinutes) {\n return {\n valid: false,\n reason: `Schedule runs every ${gap} minute(s); backups can be scheduled at most once every ${minIntervalMinutes} minutes`,\n }\n }\n\n return { valid: true, minIntervalMinutes: gap }\n}\n\n/**\n * Smallest gap between two runs, derived from the minute and hour fields only.\n * Ignoring the day fields under-estimates the gap for schedules that skip days,\n * which keeps the check conservative: it can never wave through a schedule that\n * fires more often than the computed value.\n */\nfunction smallestIntervalMinutes(minutes: number[], hours: number[]): number {\n let smallest = MINUTES_PER_DAY\n\n for (let index = 1; index < minutes.length; index++) {\n smallest = Math.min(smallest, minutes[index] - minutes[index - 1])\n }\n\n const firstMinute = minutes[0]\n const lastMinute = minutes[minutes.length - 1]\n\n // Gap from the last run of one active hour to the first run of the next one.\n // With a single active hour the next run is a day later.\n let smallestHourGap = 24\n for (let index = 1; index < hours.length; index++) {\n smallestHourGap = Math.min(smallestHourGap, hours[index] - hours[index - 1])\n }\n if (hours.length > 1) {\n // Wrap-around: last active hour of the day to the first one of the next.\n smallestHourGap = Math.min(\n smallestHourGap,\n 24 - hours[hours.length - 1] + hours[0],\n )\n }\n\n return Math.min(\n smallest,\n smallestHourGap * 60 - lastMinute + firstMinute,\n MINUTES_PER_DAY,\n )\n}\n\nfunction parseField(\n field: string,\n spec: CronFieldSpec,\n): { values: number[] } | { reason: string } {\n const collected = new Set<number>()\n\n for (const part of field.split(',')) {\n const parsed = parsePart(part, spec)\n if ('reason' in parsed) {\n return parsed\n }\n for (const value of parsed.values) {\n collected.add(value)\n }\n }\n\n if (collected.size === 0) {\n return { reason: `Invalid ${spec.name} field: \"${field}\"` }\n }\n\n return { values: Array.from(collected).sort((a, b) => a - b) }\n}\n\nfunction parsePart(\n part: string,\n spec: CronFieldSpec,\n): { values: number[] } | { reason: string } {\n const invalid = { reason: `Invalid ${spec.name} field: \"${part}\"` }\n const [rangePart, stepPart, ...rest] = part.split('/')\n if (rest.length > 0 || rangePart === undefined) {\n return invalid\n }\n\n const maxInput = spec.maxInput ?? spec.max\n\n let step = 1\n if (stepPart !== undefined) {\n if (!/^\\d+$/u.test(stepPart)) {\n return invalid\n }\n step = Number(stepPart)\n if (step < 1) {\n return invalid\n }\n }\n\n let from: number\n let to: number\n if (rangePart === '*') {\n from = spec.min\n to = spec.max\n } else if (rangePart.includes('-')) {\n const bounds = rangePart.split('-')\n if (bounds.length !== 2) {\n return invalid\n }\n const start = toValue(bounds[0], spec)\n const end = toValue(bounds[1], spec)\n if (start === undefined || end === undefined || start > end) {\n return invalid\n }\n from = start\n to = end\n } else {\n const single = toValue(rangePart, spec)\n if (single === undefined) {\n return invalid\n }\n // `5/15` means \"from 5 to the field maximum, every 15\" \u2014 a bare value\n // without a step is just itself.\n from = single\n to = stepPart === undefined ? single : maxInput\n }\n\n const values: number[] = []\n for (let value = from; value <= to; value += step) {\n // Normalize the alias (day-of-week `7` -> `0`) only here, so a range like\n // `5-7` (Fri\u2013Sun) still parses as an ascending range.\n values.push(value > spec.max ? spec.min : value)\n }\n return { values }\n}\n\nfunction toValue(raw: string, spec: CronFieldSpec): number | undefined {\n const trimmed = raw.trim()\n let value: number\n\n if (/^\\d+$/u.test(trimmed)) {\n value = Number(trimmed)\n } else {\n const nameIndex = spec.names?.indexOf(trimmed.toUpperCase()) ?? -1\n if (nameIndex < 0) {\n return undefined\n }\n value = nameIndex + spec.min\n }\n\n return value >= spec.min && value <= (spec.maxInput ?? spec.max)\n ? value\n : undefined\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The enterprise contract period a usage figure belongs to.
|
|
3
|
+
*
|
|
4
|
+
* An enterprise contract starts at `enterpriseStartDate` and renews every
|
|
5
|
+
* `enterpriseDurationInMonth` months. Usage is measured against the *current*
|
|
6
|
+
* period's included quota, so anything summed or peaked over a wider window
|
|
7
|
+
* bills a previous term's usage against this term's inclusions.
|
|
8
|
+
*
|
|
9
|
+
* Extracted from c4's billing tab so that surface, control-frontend and
|
|
10
|
+
* accounts-frontend cannot disagree about which days a figure covers — three
|
|
11
|
+
* copies of this arithmetic is how the same metric ends up with three different
|
|
12
|
+
* values depending on where a Camundi reads it.
|
|
13
|
+
*/
|
|
14
|
+
export interface EnterpriseTermWindow {
|
|
15
|
+
/** First day of the current period. Keeps the wall-clock time of the start date. */
|
|
16
|
+
periodStart: Date;
|
|
17
|
+
/** Last day of the current period (inclusive). */
|
|
18
|
+
periodEnd: Date;
|
|
19
|
+
/** How many full periods have elapsed since the contract started. */
|
|
20
|
+
completedPeriods: number;
|
|
21
|
+
/** First day of the next period. */
|
|
22
|
+
nextRenewal: Date;
|
|
23
|
+
/** Months elapsed at `periodStart`, i.e. the first month index of this period. */
|
|
24
|
+
startMonth: number;
|
|
25
|
+
/** Last month index of this period. */
|
|
26
|
+
endMonth: number;
|
|
27
|
+
}
|
|
28
|
+
/** Default contract length when the org carries no explicit duration. */
|
|
29
|
+
export declare const DEFAULT_ENTERPRISE_DURATION_IN_MONTHS = 12;
|
|
30
|
+
/**
|
|
31
|
+
* Add months, clamping to the end of the target month rather than rolling into
|
|
32
|
+
* the next one — `addMonthsClamped(Jan 31, 1)` is Feb 28/29, not Mar 2/3.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately local-time (`getMonth`/`setMonth`), matching how contract dates
|
|
35
|
+
* are compared elsewhere; only the day-bucket comparison against HDP rows
|
|
36
|
+
* switches to UTC, via `toUtcDayStartMs`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function addMonthsClamped(date: Date, months: number): Date;
|
|
39
|
+
/** Whole months between two dates, ignoring the day of month. */
|
|
40
|
+
export declare function diffInWholeMonths(start: Date, end: Date): number;
|
|
41
|
+
/**
|
|
42
|
+
* The UTC day `date` falls on, as epoch millis.
|
|
43
|
+
*
|
|
44
|
+
* HDP buckets usage per UTC day (a BigQuery DATE column, so every row's ts is
|
|
45
|
+
* UTC midnight), whereas a contract bound keeps the wall-clock time it was
|
|
46
|
+
* created at. Comparing the two directly drops the term's first day whenever the
|
|
47
|
+
* plan was created after midnight, so both sides collapse to a UTC day first.
|
|
48
|
+
*/
|
|
49
|
+
export declare function toUtcDayStartMs(date: Date): number;
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the contract period that `now` falls in.
|
|
52
|
+
*
|
|
53
|
+
* `durationInMonths` falls back to 12 for anything non-positive or absent,
|
|
54
|
+
* matching what the billing surfaces already assumed. `null` is accepted as well
|
|
55
|
+
* as `undefined` because the accounts MobX model stores the field as
|
|
56
|
+
* `types.maybeNull(types.number)` while the DTO has it optional — callers
|
|
57
|
+
* shouldn't have to normalise that difference at every call site.
|
|
58
|
+
*
|
|
59
|
+
* A contract dated in the future resolves to its first period — nothing has been
|
|
60
|
+
* used before it starts, so there is no earlier window to report on.
|
|
61
|
+
*/
|
|
62
|
+
export declare function getCurrentEnterpriseTerm(options: {
|
|
63
|
+
startDate: Date;
|
|
64
|
+
durationInMonths?: number | null;
|
|
65
|
+
now?: Date;
|
|
66
|
+
}): EnterpriseTermWindow;
|
|
67
|
+
/**
|
|
68
|
+
* Peak value among the points inside `term`, or 0 when none fall in it.
|
|
69
|
+
*
|
|
70
|
+
* For a gauge like tenants the org's figure for a period is its highest daily
|
|
71
|
+
* reading, never a sum — and the reading has to come from *this* period. Both
|
|
72
|
+
* ends are inclusive and compared as UTC days, so the term's first and last day
|
|
73
|
+
* count regardless of the time of day the contract was created at.
|
|
74
|
+
*/
|
|
75
|
+
export declare function peakWithinTerm(points: readonly {
|
|
76
|
+
ts: number;
|
|
77
|
+
value: number;
|
|
78
|
+
}[], term: Pick<EnterpriseTermWindow, 'periodStart' | 'periodEnd'>): number;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2
|
+
const DEFAULT_ENTERPRISE_DURATION_IN_MONTHS = 12;
|
|
3
|
+
function addMonthsClamped(date, months) {
|
|
4
|
+
const next = new Date(date.getTime());
|
|
5
|
+
const originalDay = next.getDate();
|
|
6
|
+
next.setMonth(next.getMonth() + months);
|
|
7
|
+
if (next.getDate() !== originalDay) {
|
|
8
|
+
next.setDate(0);
|
|
9
|
+
}
|
|
10
|
+
return next;
|
|
11
|
+
}
|
|
12
|
+
function diffInWholeMonths(start, end) {
|
|
13
|
+
return (end.getFullYear() - start.getFullYear()) * 12 + (end.getMonth() - start.getMonth());
|
|
14
|
+
}
|
|
15
|
+
function toUtcDayStartMs(date) {
|
|
16
|
+
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
17
|
+
}
|
|
18
|
+
function getCurrentEnterpriseTerm(options) {
|
|
19
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
20
|
+
const durationInMonths = typeof options.durationInMonths === "number" && options.durationInMonths > 0 ? options.durationInMonths : DEFAULT_ENTERPRISE_DURATION_IN_MONTHS;
|
|
21
|
+
let completedPeriods = Math.max(
|
|
22
|
+
0,
|
|
23
|
+
Math.floor(
|
|
24
|
+
Math.abs(diffInWholeMonths(options.startDate, now)) / durationInMonths
|
|
25
|
+
)
|
|
26
|
+
);
|
|
27
|
+
const nowDay = toUtcDayStartMs(now);
|
|
28
|
+
const periodStartDay = (periods) => toUtcDayStartMs(
|
|
29
|
+
addMonthsClamped(options.startDate, periods * durationInMonths)
|
|
30
|
+
);
|
|
31
|
+
while (completedPeriods > 0 && periodStartDay(completedPeriods) > nowDay) {
|
|
32
|
+
completedPeriods -= 1;
|
|
33
|
+
}
|
|
34
|
+
while (periodStartDay(completedPeriods + 1) <= nowDay) {
|
|
35
|
+
completedPeriods += 1;
|
|
36
|
+
}
|
|
37
|
+
const periodStart = addMonthsClamped(
|
|
38
|
+
options.startDate,
|
|
39
|
+
completedPeriods * durationInMonths
|
|
40
|
+
);
|
|
41
|
+
const nextRenewal = addMonthsClamped(periodStart, durationInMonths);
|
|
42
|
+
const periodEnd = new Date(nextRenewal.getTime() - DAY_MS);
|
|
43
|
+
const startMonth = completedPeriods * durationInMonths;
|
|
44
|
+
return {
|
|
45
|
+
periodStart,
|
|
46
|
+
periodEnd,
|
|
47
|
+
completedPeriods,
|
|
48
|
+
nextRenewal,
|
|
49
|
+
startMonth,
|
|
50
|
+
endMonth: startMonth + (durationInMonths - 1)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function peakWithinTerm(points, term) {
|
|
54
|
+
const from = toUtcDayStartMs(term.periodStart);
|
|
55
|
+
const to = toUtcDayStartMs(term.periodEnd);
|
|
56
|
+
return points.reduce(
|
|
57
|
+
(peak, point) => point.ts >= from && point.ts <= to ? Math.max(peak, point.value) : peak,
|
|
58
|
+
0
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
DEFAULT_ENTERPRISE_DURATION_IN_MONTHS,
|
|
63
|
+
addMonthsClamped,
|
|
64
|
+
diffInWholeMonths,
|
|
65
|
+
getCurrentEnterpriseTerm,
|
|
66
|
+
peakWithinTerm,
|
|
67
|
+
toUtcDayStartMs
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=enterpriseTerm.utils.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/utils/enterpriseTerm.utils.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 * The enterprise contract period a usage figure belongs to.\n *\n * An enterprise contract starts at `enterpriseStartDate` and renews every\n * `enterpriseDurationInMonth` months. Usage is measured against the *current*\n * period's included quota, so anything summed or peaked over a wider window\n * bills a previous term's usage against this term's inclusions.\n *\n * Extracted from c4's billing tab so that surface, control-frontend and\n * accounts-frontend cannot disagree about which days a figure covers \u2014 three\n * copies of this arithmetic is how the same metric ends up with three different\n * values depending on where a Camundi reads it.\n */\nexport interface EnterpriseTermWindow {\n /** First day of the current period. Keeps the wall-clock time of the start date. */\n periodStart: Date\n /** Last day of the current period (inclusive). */\n periodEnd: Date\n /** How many full periods have elapsed since the contract started. */\n completedPeriods: number\n /** First day of the next period. */\n nextRenewal: Date\n /** Months elapsed at `periodStart`, i.e. the first month index of this period. */\n startMonth: number\n /** Last month index of this period. */\n endMonth: number\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\n/** Default contract length when the org carries no explicit duration. */\nexport const DEFAULT_ENTERPRISE_DURATION_IN_MONTHS = 12\n\n/**\n * Add months, clamping to the end of the target month rather than rolling into\n * the next one \u2014 `addMonthsClamped(Jan 31, 1)` is Feb 28/29, not Mar 2/3.\n *\n * Deliberately local-time (`getMonth`/`setMonth`), matching how contract dates\n * are compared elsewhere; only the day-bucket comparison against HDP rows\n * switches to UTC, via `toUtcDayStartMs`.\n */\nexport function addMonthsClamped(date: Date, months: number): Date {\n const next = new Date(date.getTime())\n const originalDay = next.getDate()\n next.setMonth(next.getMonth() + months)\n\n if (next.getDate() !== originalDay) {\n next.setDate(0)\n }\n\n return next\n}\n\n/** Whole months between two dates, ignoring the day of month. */\nexport function diffInWholeMonths(start: Date, end: Date): number {\n return (\n (end.getFullYear() - start.getFullYear()) * 12 +\n (end.getMonth() - start.getMonth())\n )\n}\n\n/**\n * The UTC day `date` falls on, as epoch millis.\n *\n * HDP buckets usage per UTC day (a BigQuery DATE column, so every row's ts is\n * UTC midnight), whereas a contract bound keeps the wall-clock time it was\n * created at. Comparing the two directly drops the term's first day whenever the\n * plan was created after midnight, so both sides collapse to a UTC day first.\n */\nexport function toUtcDayStartMs(date: Date): number {\n return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())\n}\n\n/**\n * Resolve the contract period that `now` falls in.\n *\n * `durationInMonths` falls back to 12 for anything non-positive or absent,\n * matching what the billing surfaces already assumed. `null` is accepted as well\n * as `undefined` because the accounts MobX model stores the field as\n * `types.maybeNull(types.number)` while the DTO has it optional \u2014 callers\n * shouldn't have to normalise that difference at every call site.\n *\n * A contract dated in the future resolves to its first period \u2014 nothing has been\n * used before it starts, so there is no earlier window to report on.\n */\nexport function getCurrentEnterpriseTerm(options: {\n startDate: Date\n durationInMonths?: number | null\n now?: Date\n}): EnterpriseTermWindow {\n const now = options.now ?? new Date()\n const durationInMonths =\n typeof options.durationInMonths === 'number' && options.durationInMonths > 0\n ? options.durationInMonths\n : DEFAULT_ENTERPRISE_DURATION_IN_MONTHS\n\n // `diffInWholeMonths` ignores the day of month, so this is only an estimate:\n // for a contract starting the 15th, `now` on the 1st of an anniversary month\n // counts a full extra month and the estimate overshoots into a period that\n // hasn't started yet. `abs` keeps a future-dated contract off a negative index.\n let completedPeriods = Math.max(\n 0,\n Math.floor(\n Math.abs(diffInWholeMonths(options.startDate, now)) / durationInMonths,\n ),\n )\n\n // Correct the estimate against the real dates until `now` actually sits in the\n // period. Compared as UTC days so the renewal day belongs to the new period\n // rather than to neither \u2014 the same day granularity `peakWithinTerm` uses.\n // Both loops are bounded: the day-of-month error is under one month, so the\n // estimate is at most one period out either way.\n const nowDay = toUtcDayStartMs(now)\n const periodStartDay = (periods: number): number =>\n toUtcDayStartMs(\n addMonthsClamped(options.startDate, periods * durationInMonths),\n )\n\n while (completedPeriods > 0 && periodStartDay(completedPeriods) > nowDay) {\n completedPeriods -= 1\n }\n while (periodStartDay(completedPeriods + 1) <= nowDay) {\n completedPeriods += 1\n }\n\n const periodStart = addMonthsClamped(\n options.startDate,\n completedPeriods * durationInMonths,\n )\n const nextRenewal = addMonthsClamped(periodStart, durationInMonths)\n const periodEnd = new Date(nextRenewal.getTime() - DAY_MS)\n const startMonth = completedPeriods * durationInMonths\n\n return {\n periodStart,\n periodEnd,\n completedPeriods,\n nextRenewal,\n startMonth,\n endMonth: startMonth + (durationInMonths - 1),\n }\n}\n\n/**\n * Peak value among the points inside `term`, or 0 when none fall in it.\n *\n * For a gauge like tenants the org's figure for a period is its highest daily\n * reading, never a sum \u2014 and the reading has to come from *this* period. Both\n * ends are inclusive and compared as UTC days, so the term's first and last day\n * count regardless of the time of day the contract was created at.\n */\nexport function peakWithinTerm(\n points: readonly { ts: number; value: number }[],\n term: Pick<EnterpriseTermWindow, 'periodStart' | 'periodEnd'>,\n): number {\n const from = toUtcDayStartMs(term.periodStart)\n const to = toUtcDayStartMs(term.periodEnd)\n\n return points.reduce(\n (peak, point) =>\n point.ts >= from && point.ts <= to ? Math.max(peak, point.value) : peak,\n 0,\n )\n}\n"],
|
|
5
|
+
"mappings": "AAkCA,MAAM,SAAS,KAAK,KAAK,KAAK;AAGvB,MAAM,wCAAwC;AAU9C,SAAS,iBAAiB,MAAY,QAAsB;AACjE,QAAM,OAAO,IAAI,KAAK,KAAK,QAAQ,CAAC;AACpC,QAAM,cAAc,KAAK,QAAQ;AACjC,OAAK,SAAS,KAAK,SAAS,IAAI,MAAM;AAEtC,MAAI,KAAK,QAAQ,MAAM,aAAa;AAClC,SAAK,QAAQ,CAAC;AAAA,EAChB;AAEA,SAAO;AACT;AAGO,SAAS,kBAAkB,OAAa,KAAmB;AAChE,UACG,IAAI,YAAY,IAAI,MAAM,YAAY,KAAK,MAC3C,IAAI,SAAS,IAAI,MAAM,SAAS;AAErC;AAUO,SAAS,gBAAgB,MAAoB;AAClD,SAAO,KAAK,IAAI,KAAK,eAAe,GAAG,KAAK,YAAY,GAAG,KAAK,WAAW,CAAC;AAC9E;AAcO,SAAS,yBAAyB,SAIhB;AACvB,QAAM,MAAM,QAAQ,OAAO,oBAAI,KAAK;AACpC,QAAM,mBACJ,OAAO,QAAQ,qBAAqB,YAAY,QAAQ,mBAAmB,IACvE,QAAQ,mBACR;AAMN,MAAI,mBAAmB,KAAK;AAAA,IAC1B;AAAA,IACA,KAAK;AAAA,MACH,KAAK,IAAI,kBAAkB,QAAQ,WAAW,GAAG,CAAC,IAAI;AAAA,IACxD;AAAA,EACF;AAOA,QAAM,SAAS,gBAAgB,GAAG;AAClC,QAAM,iBAAiB,CAAC,YACtB;AAAA,IACE,iBAAiB,QAAQ,WAAW,UAAU,gBAAgB;AAAA,EAChE;AAEF,SAAO,mBAAmB,KAAK,eAAe,gBAAgB,IAAI,QAAQ;AACxE,wBAAoB;AAAA,EACtB;AACA,SAAO,eAAe,mBAAmB,CAAC,KAAK,QAAQ;AACrD,wBAAoB;AAAA,EACtB;AAEA,QAAM,cAAc;AAAA,IAClB,QAAQ;AAAA,IACR,mBAAmB;AAAA,EACrB;AACA,QAAM,cAAc,iBAAiB,aAAa,gBAAgB;AAClE,QAAM,YAAY,IAAI,KAAK,YAAY,QAAQ,IAAI,MAAM;AACzD,QAAM,aAAa,mBAAmB;AAEtC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,cAAc,mBAAmB;AAAA,EAC7C;AACF;AAUO,SAAS,eACd,QACA,MACQ;AACR,QAAM,OAAO,gBAAgB,KAAK,WAAW;AAC7C,QAAM,KAAK,gBAAgB,KAAK,SAAS;AAEzC,SAAO,OAAO;AAAA,IACZ,CAAC,MAAM,UACL,MAAM,MAAM,QAAQ,MAAM,MAAM,KAAK,KAAK,IAAI,MAAM,MAAM,KAAK,IAAI;AAAA,IACrE;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three IAM principals that need access to a BYOK cluster's KMS keys after
|
|
3
|
+
* a failover. All three live in the failover region's account — the same
|
|
4
|
+
* account as the failover cluster's own role.
|
|
5
|
+
*/
|
|
6
|
+
export interface FailoverKeyPolicyPrincipals {
|
|
7
|
+
/** Reads the source cluster's secondary bucket during the BucketClone. */
|
|
8
|
+
cloneRole: string;
|
|
9
|
+
/** The failover cluster's own access to its new primary bucket. */
|
|
10
|
+
clusterRole: string;
|
|
11
|
+
/** Replicates the new primary bucket back out to its secondary. */
|
|
12
|
+
replicationRole: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Derive the three principals a customer must add to both KMS key policies
|
|
16
|
+
* after a failover, from the failover cluster's own role ARN and the stableId
|
|
17
|
+
* carried over from its source.
|
|
18
|
+
*
|
|
19
|
+
* The clone role is pre-staged as `clone-backup-<stableId>`, and the
|
|
20
|
+
* replication role is the cluster role with a `-replication` suffix — so the
|
|
21
|
+
* account parsed out of the cluster role ARN is all that is needed.
|
|
22
|
+
*
|
|
23
|
+
* Returns `undefined` when either input is missing or the role ARN is not in
|
|
24
|
+
* the expected shape, so callers render nothing rather than a wrong ARN.
|
|
25
|
+
*/
|
|
26
|
+
export declare function deriveFailoverKeyPolicyPrincipals(clusterRoleArn: string | undefined, stableId: string | undefined): FailoverKeyPolicyPrincipals | undefined;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const CLUSTER_ROLE_ARN = /^arn:aws:iam::(?<account>\d{12}):role\/c8-cluster\/c8-apps-(?<uuid>[0-9a-f-]{36})$/u;
|
|
2
|
+
function deriveFailoverKeyPolicyPrincipals(clusterRoleArn, stableId) {
|
|
3
|
+
if (!clusterRoleArn || !stableId) return void 0;
|
|
4
|
+
const account = CLUSTER_ROLE_ARN.exec(clusterRoleArn)?.groups?.account;
|
|
5
|
+
if (!account) return void 0;
|
|
6
|
+
return {
|
|
7
|
+
cloneRole: `arn:aws:iam::${account}:role/c8-cluster/clone-backup-${stableId}`,
|
|
8
|
+
clusterRole: clusterRoleArn,
|
|
9
|
+
replicationRole: `${clusterRoleArn}-replication`
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
deriveFailoverKeyPolicyPrincipals
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=failoverKeyPolicy.utils.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/utils/failoverKeyPolicy.utils.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 * The cluster's own IAM role ARN, surfaced by the operator on the ZeebeCluster\n * `status.accountId` field (the name is historical \u2014 the value is a role ARN,\n * not a bare account id):\n *\n * arn:aws:iam::<account>:role/c8-cluster/c8-apps-<clusterUuid>\n */\nconst CLUSTER_ROLE_ARN =\n /^arn:aws:iam::(?<account>\\d{12}):role\\/c8-cluster\\/c8-apps-(?<uuid>[0-9a-f-]{36})$/u\n\n/**\n * The three IAM principals that need access to a BYOK cluster's KMS keys after\n * a failover. All three live in the failover region's account \u2014 the same\n * account as the failover cluster's own role.\n */\nexport interface FailoverKeyPolicyPrincipals {\n /** Reads the source cluster's secondary bucket during the BucketClone. */\n cloneRole: string\n /** The failover cluster's own access to its new primary bucket. */\n clusterRole: string\n /** Replicates the new primary bucket back out to its secondary. */\n replicationRole: string\n}\n\n/**\n * Derive the three principals a customer must add to both KMS key policies\n * after a failover, from the failover cluster's own role ARN and the stableId\n * carried over from its source.\n *\n * The clone role is pre-staged as `clone-backup-<stableId>`, and the\n * replication role is the cluster role with a `-replication` suffix \u2014 so the\n * account parsed out of the cluster role ARN is all that is needed.\n *\n * Returns `undefined` when either input is missing or the role ARN is not in\n * the expected shape, so callers render nothing rather than a wrong ARN.\n */\nexport function deriveFailoverKeyPolicyPrincipals(\n clusterRoleArn: string | undefined,\n stableId: string | undefined,\n): FailoverKeyPolicyPrincipals | undefined {\n if (!clusterRoleArn || !stableId) return undefined\n\n const account = CLUSTER_ROLE_ARN.exec(clusterRoleArn)?.groups?.account\n if (!account) return undefined\n\n return {\n cloneRole: `arn:aws:iam::${account}:role/c8-cluster/clone-backup-${stableId}`,\n clusterRole: clusterRoleArn,\n replicationRole: `${clusterRoleArn}-replication`,\n }\n}\n"],
|
|
5
|
+
"mappings": "AAaA,MAAM,mBACJ;AA4BK,SAAS,kCACd,gBACA,UACyC;AACzC,MAAI,CAAC,kBAAkB,CAAC,SAAU,QAAO;AAEzC,QAAM,UAAU,iBAAiB,KAAK,cAAc,GAAG,QAAQ;AAC/D,MAAI,CAAC,QAAS,QAAO;AAErB,SAAO;AAAA,IACL,WAAW,gBAAgB,OAAO,iCAAiC,QAAQ;AAAA,IAC3E,aAAa;AAAA,IACb,iBAAiB,GAAG,cAAc;AAAA,EACpC;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -28,6 +28,23 @@ export declare function isValidClientName(name: string): boolean;
|
|
|
28
28
|
* @returns true if the CIDR is valid, false otherwise
|
|
29
29
|
*/
|
|
30
30
|
export declare function isValidCIDR(cidr: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Normalizes a comma-separated list of IPv4 addresses and/or CIDR notations by
|
|
33
|
+
* stripping every whitespace character.
|
|
34
|
+
*
|
|
35
|
+
* Whitespace is never meaningful inside a CIDR list, but `isValidCIDR` trims
|
|
36
|
+
* before validating, so a value such as `"10.0.0.1 "` passes validation and is
|
|
37
|
+
* then persisted verbatim — where the trailing space later breaks the cluster
|
|
38
|
+
* controller. Normalizing on the way in keeps the stored value clean.
|
|
39
|
+
*
|
|
40
|
+
* Examples:
|
|
41
|
+
* - "10.0.0.1 " -> "10.0.0.1"
|
|
42
|
+
* - "192.168.1.1, 10.0.0.0/8" -> "192.168.1.1,10.0.0.0/8"
|
|
43
|
+
*
|
|
44
|
+
* @param cidrList - A comma-separated list of IPv4 addresses or CIDR notations
|
|
45
|
+
* @returns The list with all whitespace removed
|
|
46
|
+
*/
|
|
47
|
+
export declare function normalizeCIDRList(cidrList: string): string;
|
|
31
48
|
/**
|
|
32
49
|
* Validates a comma-separated list of IPv4 addresses and/or CIDR notations.
|
|
33
50
|
*
|
|
@@ -128,6 +128,12 @@ function isValidCIDR(cidr) {
|
|
|
128
128
|
}
|
|
129
129
|
return true;
|
|
130
130
|
}
|
|
131
|
+
function normalizeCIDRList(cidrList) {
|
|
132
|
+
if (typeof cidrList !== "string") {
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
135
|
+
return cidrList.replace(/\s/gu, "");
|
|
136
|
+
}
|
|
131
137
|
function isValidCIDRList(cidrList) {
|
|
132
138
|
if (typeof cidrList !== "string") {
|
|
133
139
|
return false;
|
|
@@ -148,6 +154,7 @@ export {
|
|
|
148
154
|
isValidCIDRList,
|
|
149
155
|
isValidClientName,
|
|
150
156
|
isValidHostname,
|
|
151
|
-
isValidPrivateLinkRegion
|
|
157
|
+
isValidPrivateLinkRegion,
|
|
158
|
+
normalizeCIDRList
|
|
152
159
|
};
|
|
153
160
|
//# sourceMappingURL=string-validation.utils.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/utils/string-validation.utils.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 { PRIVATE_LINK_REGIONS } from '../constants/privateLinkRegions.const.js'\n\nconst AWS_ARN_PARTITIONS = new Set(['aws', 'aws-cn', 'aws-us-gov'])\n\nfunction isAlphaNumericOrDash(value: string): boolean {\n for (const char of value) {\n const isLowercaseLetter = char >= 'a' && char <= 'z'\n const isUppercaseLetter = char >= 'A' && char <= 'Z'\n const isDigit = char >= '0' && char <= '9'\n\n if (!isLowercaseLetter && !isUppercaseLetter && !isDigit && char !== '-') {\n return false\n }\n }\n\n return true\n}\n\nfunction isDigitsOnly(value: string): boolean {\n for (const char of value) {\n if (char < '0' || char > '9') {\n return false\n }\n }\n\n return true\n}\n\nexport function isValidHostname(value: string) {\n if (typeof value !== 'string') {\n return false\n }\n\n const validHostnameChars = /^[a-zA-Z0-9-.]{1,253}\\.?$/gu\n if (!validHostnameChars.test(value)) {\n return false\n }\n\n if (value.endsWith('.')) {\n // eslint-disable-next-line no-param-reassign\n value = value.slice(0, value.length - 1)\n }\n\n if (value.length > 253) {\n return false\n }\n\n const labels = value.split('.')\n\n const isValid = labels.every((label) => {\n // eslint-disable-next-line prefer-named-capture-group\n const validLabelChars = /^([a-zA-Z0-9-]+)$/gu\n\n const validLabel =\n validLabelChars.test(label) &&\n label.length < 64 &&\n !label.startsWith('-') &&\n !label.endsWith('-')\n\n return validLabel\n })\n\n return isValid\n}\n\nexport function isValidAwsArn(value: string): boolean {\n if (typeof value !== 'string') {\n return false\n }\n\n if (value.length === 0 || value.includes('*')) {\n return false\n }\n\n const segments = value.split(':')\n\n if (segments.length < 6) {\n return false\n }\n\n const [prefix, partition, service, region, accountId, ...resourceParts] =\n segments\n const resource = resourceParts.join(':')\n\n if (prefix !== 'arn' || !AWS_ARN_PARTITIONS.has(partition)) {\n return false\n }\n\n if (!service || !isAlphaNumericOrDash(service)) {\n return false\n }\n\n if (service === 'iam' && accountId.length === 0) {\n return false\n }\n\n if (region && !isAlphaNumericOrDash(region)) {\n return false\n }\n\n if (accountId && (!isDigitsOnly(accountId) || accountId.length !== 12)) {\n return false\n }\n\n return resource.length > 0\n}\n\nexport function isValidPrivateLinkRegion(value: string): boolean {\n if (typeof value !== 'string') {\n return false\n }\n\n return PRIVATE_LINK_REGIONS.includes(value)\n}\n\n/**\n * Regular expression to validate client names.\n * Allows only letters (a-z, A-Z), digits (0-9), dashes (-), and underscores (_).\n * This matches the frontend validation in console-frontend/src/models/validation/input.ts\n */\nconst CLIENT_NAME_REGEX = /^[a-zA-Z0-9_-]+$/u\n\n/**\n * Validates that a client name contains only allowed characters:\n * letters, digits, dashes, and underscores.\n *\n * @param name - The client name to validate\n * @returns true if the name is valid, false otherwise\n */\nexport function isValidClientName(name: string): boolean {\n if (typeof name !== 'string') {\n return false\n }\n\n if (name.trim().length === 0) {\n return false\n }\n\n return CLIENT_NAME_REGEX.test(name)\n}\n\n/**\n * Validates a single IPv4 address or CIDR notation.\n *\n * Valid examples:\n * - 192.168.1.1\n * - 10.0.0.0/8\n * - 0.0.0.0/0\n * - 255.255.255.255/32\n *\n * Invalid examples:\n * - 999.999.999.999 (octets must be 0-255)\n * - 1.2.3.4/55 (prefix must be 0-32)\n * - 256.1.1.1 (octets must be 0-255)\n * - 1.2.3 (must have 4 octets)\n *\n * @param cidr - The IPv4 address or CIDR notation to validate\n * @returns true if the CIDR is valid, false otherwise\n */\nexport function isValidCIDR(cidr: string): boolean {\n if (typeof cidr !== 'string') {\n return false\n }\n\n const trimmed = cidr.trim()\n\n if (trimmed.length === 0) {\n return false\n }\n\n // Split IP and prefix\n const parts = trimmed.split('/')\n\n // Must have either IP only or IP/prefix\n if (parts.length > 2) {\n return false\n }\n\n const ip = parts[0]\n const prefix = parts[1]\n\n // Validate IP address\n const octets = ip.split('.')\n\n // Must have exactly 4 octets\n if (octets.length !== 4) {\n return false\n }\n\n // Validate each octet\n for (const octet of octets) {\n // Check if it's a valid number\n if (!/^\\d+$/u.test(octet)) {\n return false\n }\n\n const num = parseInt(octet, 10)\n\n // Octet must be 0-255\n if (num < 0 || num > 255) {\n return false\n }\n\n // Leading zeros are not allowed (except for \"0\" itself)\n if (octet.length > 1 && octet.startsWith('0')) {\n return false\n }\n }\n\n // If prefix is present, validate it\n if (prefix !== undefined) {\n if (!/^\\d+$/u.test(prefix)) {\n return false\n }\n\n const prefixNum = parseInt(prefix, 10)\n\n // Prefix must be 0-32 for IPv4\n if (prefixNum < 0 || prefixNum > 32) {\n return false\n }\n }\n\n return true\n}\n\n/**\n * Validates a comma-separated list of IPv4 addresses and/or CIDR notations.\n *\n * Valid examples:\n * - \"192.168.1.1\"\n * - \"192.168.1.1,10.0.0.0/8\"\n * - \"192.168.1.1, 10.0.0.0/8\" (spaces are trimmed)\n *\n * Invalid examples:\n * - \"192.168.1.1,999.999.999.999\" (second entry is invalid)\n * - \"1.2.3.4/55\" (prefix is invalid)\n *\n * @param cidrList - A comma-separated list of IPv4 addresses or CIDR notations\n * @returns true if all entries in the list are valid, false otherwise\n */\nexport function isValidCIDRList(cidrList: string): boolean {\n if (typeof cidrList !== 'string') {\n return false\n }\n\n const trimmed = cidrList.trim()\n\n if (trimmed.length === 0) {\n return false\n }\n\n const entries = trimmed.split(',')\n\n // At least one entry must be present\n if (entries.length === 0) {\n return false\n }\n\n // All entries must be valid CIDRs\n return entries.every((entry) => isValidCIDR(entry))\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 { PRIVATE_LINK_REGIONS } from '../constants/privateLinkRegions.const.js'\n\nconst AWS_ARN_PARTITIONS = new Set(['aws', 'aws-cn', 'aws-us-gov'])\n\nfunction isAlphaNumericOrDash(value: string): boolean {\n for (const char of value) {\n const isLowercaseLetter = char >= 'a' && char <= 'z'\n const isUppercaseLetter = char >= 'A' && char <= 'Z'\n const isDigit = char >= '0' && char <= '9'\n\n if (!isLowercaseLetter && !isUppercaseLetter && !isDigit && char !== '-') {\n return false\n }\n }\n\n return true\n}\n\nfunction isDigitsOnly(value: string): boolean {\n for (const char of value) {\n if (char < '0' || char > '9') {\n return false\n }\n }\n\n return true\n}\n\nexport function isValidHostname(value: string) {\n if (typeof value !== 'string') {\n return false\n }\n\n const validHostnameChars = /^[a-zA-Z0-9-.]{1,253}\\.?$/gu\n if (!validHostnameChars.test(value)) {\n return false\n }\n\n if (value.endsWith('.')) {\n // eslint-disable-next-line no-param-reassign\n value = value.slice(0, value.length - 1)\n }\n\n if (value.length > 253) {\n return false\n }\n\n const labels = value.split('.')\n\n const isValid = labels.every((label) => {\n // eslint-disable-next-line prefer-named-capture-group\n const validLabelChars = /^([a-zA-Z0-9-]+)$/gu\n\n const validLabel =\n validLabelChars.test(label) &&\n label.length < 64 &&\n !label.startsWith('-') &&\n !label.endsWith('-')\n\n return validLabel\n })\n\n return isValid\n}\n\nexport function isValidAwsArn(value: string): boolean {\n if (typeof value !== 'string') {\n return false\n }\n\n if (value.length === 0 || value.includes('*')) {\n return false\n }\n\n const segments = value.split(':')\n\n if (segments.length < 6) {\n return false\n }\n\n const [prefix, partition, service, region, accountId, ...resourceParts] =\n segments\n const resource = resourceParts.join(':')\n\n if (prefix !== 'arn' || !AWS_ARN_PARTITIONS.has(partition)) {\n return false\n }\n\n if (!service || !isAlphaNumericOrDash(service)) {\n return false\n }\n\n if (service === 'iam' && accountId.length === 0) {\n return false\n }\n\n if (region && !isAlphaNumericOrDash(region)) {\n return false\n }\n\n if (accountId && (!isDigitsOnly(accountId) || accountId.length !== 12)) {\n return false\n }\n\n return resource.length > 0\n}\n\nexport function isValidPrivateLinkRegion(value: string): boolean {\n if (typeof value !== 'string') {\n return false\n }\n\n return PRIVATE_LINK_REGIONS.includes(value)\n}\n\n/**\n * Regular expression to validate client names.\n * Allows only letters (a-z, A-Z), digits (0-9), dashes (-), and underscores (_).\n * This matches the frontend validation in console-frontend/src/models/validation/input.ts\n */\nconst CLIENT_NAME_REGEX = /^[a-zA-Z0-9_-]+$/u\n\n/**\n * Validates that a client name contains only allowed characters:\n * letters, digits, dashes, and underscores.\n *\n * @param name - The client name to validate\n * @returns true if the name is valid, false otherwise\n */\nexport function isValidClientName(name: string): boolean {\n if (typeof name !== 'string') {\n return false\n }\n\n if (name.trim().length === 0) {\n return false\n }\n\n return CLIENT_NAME_REGEX.test(name)\n}\n\n/**\n * Validates a single IPv4 address or CIDR notation.\n *\n * Valid examples:\n * - 192.168.1.1\n * - 10.0.0.0/8\n * - 0.0.0.0/0\n * - 255.255.255.255/32\n *\n * Invalid examples:\n * - 999.999.999.999 (octets must be 0-255)\n * - 1.2.3.4/55 (prefix must be 0-32)\n * - 256.1.1.1 (octets must be 0-255)\n * - 1.2.3 (must have 4 octets)\n *\n * @param cidr - The IPv4 address or CIDR notation to validate\n * @returns true if the CIDR is valid, false otherwise\n */\nexport function isValidCIDR(cidr: string): boolean {\n if (typeof cidr !== 'string') {\n return false\n }\n\n const trimmed = cidr.trim()\n\n if (trimmed.length === 0) {\n return false\n }\n\n // Split IP and prefix\n const parts = trimmed.split('/')\n\n // Must have either IP only or IP/prefix\n if (parts.length > 2) {\n return false\n }\n\n const ip = parts[0]\n const prefix = parts[1]\n\n // Validate IP address\n const octets = ip.split('.')\n\n // Must have exactly 4 octets\n if (octets.length !== 4) {\n return false\n }\n\n // Validate each octet\n for (const octet of octets) {\n // Check if it's a valid number\n if (!/^\\d+$/u.test(octet)) {\n return false\n }\n\n const num = parseInt(octet, 10)\n\n // Octet must be 0-255\n if (num < 0 || num > 255) {\n return false\n }\n\n // Leading zeros are not allowed (except for \"0\" itself)\n if (octet.length > 1 && octet.startsWith('0')) {\n return false\n }\n }\n\n // If prefix is present, validate it\n if (prefix !== undefined) {\n if (!/^\\d+$/u.test(prefix)) {\n return false\n }\n\n const prefixNum = parseInt(prefix, 10)\n\n // Prefix must be 0-32 for IPv4\n if (prefixNum < 0 || prefixNum > 32) {\n return false\n }\n }\n\n return true\n}\n\n/**\n * Normalizes a comma-separated list of IPv4 addresses and/or CIDR notations by\n * stripping every whitespace character.\n *\n * Whitespace is never meaningful inside a CIDR list, but `isValidCIDR` trims\n * before validating, so a value such as `\"10.0.0.1 \"` passes validation and is\n * then persisted verbatim \u2014 where the trailing space later breaks the cluster\n * controller. Normalizing on the way in keeps the stored value clean.\n *\n * Examples:\n * - \"10.0.0.1 \" -> \"10.0.0.1\"\n * - \"192.168.1.1, 10.0.0.0/8\" -> \"192.168.1.1,10.0.0.0/8\"\n *\n * @param cidrList - A comma-separated list of IPv4 addresses or CIDR notations\n * @returns The list with all whitespace removed\n */\nexport function normalizeCIDRList(cidrList: string): string {\n if (typeof cidrList !== 'string') {\n return ''\n }\n\n return cidrList.replace(/\\s/gu, '')\n}\n\n/**\n * Validates a comma-separated list of IPv4 addresses and/or CIDR notations.\n *\n * Valid examples:\n * - \"192.168.1.1\"\n * - \"192.168.1.1,10.0.0.0/8\"\n * - \"192.168.1.1, 10.0.0.0/8\" (spaces are trimmed)\n *\n * Invalid examples:\n * - \"192.168.1.1,999.999.999.999\" (second entry is invalid)\n * - \"1.2.3.4/55\" (prefix is invalid)\n *\n * @param cidrList - A comma-separated list of IPv4 addresses or CIDR notations\n * @returns true if all entries in the list are valid, false otherwise\n */\nexport function isValidCIDRList(cidrList: string): boolean {\n if (typeof cidrList !== 'string') {\n return false\n }\n\n const trimmed = cidrList.trim()\n\n if (trimmed.length === 0) {\n return false\n }\n\n const entries = trimmed.split(',')\n\n // At least one entry must be present\n if (entries.length === 0) {\n return false\n }\n\n // All entries must be valid CIDRs\n return entries.every((entry) => isValidCIDR(entry))\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -13,3 +13,15 @@ export declare const checkVersion: (params: {
|
|
|
13
13
|
versionToCompareTo: string;
|
|
14
14
|
trueOnInvalidVersion?: boolean;
|
|
15
15
|
}) => boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Version gate for the App Integrations exporter toggle (#8536). Shared by the
|
|
18
|
+
* c4 settings tile and the console-backend update guard so both stay in
|
|
19
|
+
* lockstep — the fix landed as patches on the 8.9 stream (zeebe >=8.9.14 AND
|
|
20
|
+
* connectorBridge >=8.9.7), everything from 8.10.0-alpha4 on already carries
|
|
21
|
+
* both fixes. A plain zeebe >=8.9.14 check would wrongly match
|
|
22
|
+
* 8.10.0-alpha1/2/3, which predate the fix.
|
|
23
|
+
*/
|
|
24
|
+
export declare const isAppIntegrationsVersionSupported: (versions: {
|
|
25
|
+
zeebe?: string;
|
|
26
|
+
connectorBridge?: string;
|
|
27
|
+
}) => boolean;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import * as semver from "semver";
|
|
2
|
+
import {
|
|
3
|
+
APP_INTEGRATIONS_89_STREAM_CONNECTOR_BRIDGE_MIN_VERSION,
|
|
4
|
+
APP_INTEGRATIONS_89_STREAM_ZEEBE_MIN_VERSION,
|
|
5
|
+
APP_INTEGRATIONS_MIN_VERSION
|
|
6
|
+
} from "../constants/misc.const.js";
|
|
2
7
|
var VersionCheckOperator = /* @__PURE__ */ ((VersionCheckOperator2) => {
|
|
3
8
|
VersionCheckOperator2[VersionCheckOperator2["LessThan"] = 0] = "LessThan";
|
|
4
9
|
VersionCheckOperator2[VersionCheckOperator2["LessThanOrEqual"] = 1] = "LessThanOrEqual";
|
|
@@ -88,8 +93,39 @@ const checkVersion = (params) => {
|
|
|
88
93
|
return false;
|
|
89
94
|
}
|
|
90
95
|
};
|
|
96
|
+
const isAppIntegrationsVersionSupported = (versions) => {
|
|
97
|
+
if (!versions.zeebe) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const isIn89Stream = checkVersion({
|
|
101
|
+
possiblyVersionOrDockerPath: versions.zeebe,
|
|
102
|
+
trueForSnapshot: "compare",
|
|
103
|
+
operator: 5 /* SameMinor */,
|
|
104
|
+
versionToCompareTo: "8.9.0"
|
|
105
|
+
});
|
|
106
|
+
if (!isIn89Stream) {
|
|
107
|
+
return checkVersion({
|
|
108
|
+
possiblyVersionOrDockerPath: versions.zeebe,
|
|
109
|
+
trueForSnapshot: true,
|
|
110
|
+
operator: 3 /* GreaterThanOrEqual */,
|
|
111
|
+
versionToCompareTo: APP_INTEGRATIONS_MIN_VERSION
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return checkVersion({
|
|
115
|
+
possiblyVersionOrDockerPath: versions.zeebe,
|
|
116
|
+
trueForSnapshot: true,
|
|
117
|
+
operator: 3 /* GreaterThanOrEqual */,
|
|
118
|
+
versionToCompareTo: APP_INTEGRATIONS_89_STREAM_ZEEBE_MIN_VERSION
|
|
119
|
+
}) && checkVersion({
|
|
120
|
+
possiblyVersionOrDockerPath: versions.connectorBridge,
|
|
121
|
+
trueForSnapshot: true,
|
|
122
|
+
operator: 3 /* GreaterThanOrEqual */,
|
|
123
|
+
versionToCompareTo: APP_INTEGRATIONS_89_STREAM_CONNECTOR_BRIDGE_MIN_VERSION
|
|
124
|
+
});
|
|
125
|
+
};
|
|
91
126
|
export {
|
|
92
127
|
VersionCheckOperator,
|
|
93
|
-
checkVersion
|
|
128
|
+
checkVersion,
|
|
129
|
+
isAppIntegrationsVersionSupported
|
|
94
130
|
};
|
|
95
131
|
//# sourceMappingURL=versionCheck.utils.js.map
|