Compare commits

..

42 Commits

Author SHA1 Message Date
Owen Schwartz d2d30cd82e New translations en-us.json (Norwegian Bokmal) 2026-02-14 12:34:30 -08:00
Owen Schwartz 4613d90eaf New translations en-us.json (Chinese Simplified) 2026-02-14 12:34:29 -08:00
Owen Schwartz 4b3a172720 New translations en-us.json (Turkish) 2026-02-14 12:34:27 -08:00
Owen Schwartz 715654dd96 New translations en-us.json (Russian) 2026-02-14 12:34:26 -08:00
Owen Schwartz fb1a9f5849 New translations en-us.json (Portuguese) 2026-02-14 12:34:25 -08:00
Owen Schwartz b783dc62c0 New translations en-us.json (Polish) 2026-02-14 12:34:23 -08:00
Owen Schwartz 83b5e20b1b New translations en-us.json (Dutch) 2026-02-14 12:34:22 -08:00
Owen Schwartz ab2ec7129c New translations en-us.json (Korean) 2026-02-14 12:34:21 -08:00
Owen Schwartz 9d31147750 New translations en-us.json (Italian) 2026-02-14 12:34:20 -08:00
Owen Schwartz 38ae15bad5 New translations en-us.json (German) 2026-02-14 12:34:18 -08:00
Owen Schwartz 2d52748fbc New translations en-us.json (Czech) 2026-02-14 12:34:17 -08:00
Owen Schwartz 5e14b57f90 New translations en-us.json (Bulgarian) 2026-02-14 12:34:16 -08:00
Owen Schwartz a2528fe8f9 New translations en-us.json (Spanish) 2026-02-14 12:34:14 -08:00
Owen Schwartz 6068d8b554 New translations en-us.json (French) 2026-02-14 12:34:13 -08:00
Owen Schwartz 4ca54a6c5b New translations en-us.json (Norwegian Bokmal) 2026-02-13 18:31:27 -08:00
Owen Schwartz 274faded45 New translations en-us.json (Chinese Simplified) 2026-02-13 18:31:26 -08:00
Owen Schwartz 7eebd77aa3 New translations en-us.json (Turkish) 2026-02-13 18:31:25 -08:00
Owen Schwartz 99278f9961 New translations en-us.json (Russian) 2026-02-13 18:31:23 -08:00
Owen Schwartz ceb9cd38eb New translations en-us.json (Portuguese) 2026-02-13 18:31:22 -08:00
Owen Schwartz 687eda132a New translations en-us.json (Polish) 2026-02-13 18:31:21 -08:00
Owen Schwartz 3ebd7bee63 New translations en-us.json (Dutch) 2026-02-13 18:31:19 -08:00
Owen Schwartz 4584d9ddb1 New translations en-us.json (Korean) 2026-02-13 18:31:18 -08:00
Owen Schwartz 97a0c65137 New translations en-us.json (Italian) 2026-02-13 18:31:17 -08:00
Owen Schwartz f28925347c New translations en-us.json (German) 2026-02-13 18:31:15 -08:00
Owen Schwartz 220e6c0aca New translations en-us.json (Czech) 2026-02-13 18:31:14 -08:00
Owen Schwartz dd56706624 New translations en-us.json (Bulgarian) 2026-02-13 18:31:12 -08:00
Owen Schwartz 597ef725c4 New translations en-us.json (Spanish) 2026-02-13 18:31:10 -08:00
Owen Schwartz 39e7655327 New translations en-us.json (French) 2026-02-13 18:31:09 -08:00
Owen Schwartz 829e324217 New translations en-us.json (Norwegian Bokmal) 2026-02-13 17:18:44 -08:00
Owen Schwartz 59ce52ce83 New translations en-us.json (Chinese Simplified) 2026-02-13 17:18:43 -08:00
Owen Schwartz 43f602a005 New translations en-us.json (Turkish) 2026-02-13 17:18:41 -08:00
Owen Schwartz c4643d9bcb New translations en-us.json (Russian) 2026-02-13 17:18:40 -08:00
Owen Schwartz ea5fa998c1 New translations en-us.json (Portuguese) 2026-02-13 17:18:38 -08:00
Owen Schwartz 9258ffda77 New translations en-us.json (Polish) 2026-02-13 17:18:37 -08:00
Owen Schwartz 103b89a171 New translations en-us.json (Dutch) 2026-02-13 17:18:35 -08:00
Owen Schwartz 336d94bc43 New translations en-us.json (Korean) 2026-02-13 17:18:34 -08:00
Owen Schwartz f1dfac1192 New translations en-us.json (Italian) 2026-02-13 17:18:33 -08:00
Owen Schwartz 93f09b9c1b New translations en-us.json (German) 2026-02-13 17:18:31 -08:00
Owen Schwartz e2d940e477 New translations en-us.json (Czech) 2026-02-13 17:18:30 -08:00
Owen Schwartz 2a99bdadd8 New translations en-us.json (Bulgarian) 2026-02-13 17:18:29 -08:00
Owen Schwartz b2af60055b New translations en-us.json (Spanish) 2026-02-13 17:18:27 -08:00
Owen Schwartz 540bafb730 New translations en-us.json (French) 2026-02-13 17:18:26 -08:00
20 changed files with 168 additions and 60 deletions
+35 -4
View File
@@ -525,10 +525,41 @@ jobs:
VERIFIED_INDEX_KEYLESS=false VERIFIED_INDEX_KEYLESS=false
fi fi
# Check if verification succeeded # If index verification fails, attempt to verify child platform manifests
if [ "${VERIFIED_INDEX}" != "true" ] && [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then if [ "${VERIFIED_INDEX}" != "true" ] || [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then
echo "⚠️ WARNING: Verification not available for ${BASE_IMAGE}:${IMAGE_TAG}" echo "Index verification not available; attempting child manifest verification for ${BASE_IMAGE}:${IMAGE_TAG}"
echo "This may be due to registry propagation delays. Continuing anyway." CHILD_VERIFIED=false
for ARCH in arm64 amd64; do
CHILD_TAG="${IMAGE_TAG}-${ARCH}"
echo "Resolving child digest for ${BASE_IMAGE}:${CHILD_TAG}"
CHILD_DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${CHILD_TAG} | jq -r '.Digest' || true)"
if [ -n "${CHILD_DIGEST}" ] && [ "${CHILD_DIGEST}" != "null" ]; then
CHILD_REF="${BASE_IMAGE}@${CHILD_DIGEST}"
echo "==> cosign verify (public key) child ${CHILD_REF}"
if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${CHILD_REF}' -o text"; then
CHILD_VERIFIED=true
echo "Public key verification succeeded for child ${CHILD_REF}"
else
echo "Public key verification failed for child ${CHILD_REF}"
fi
echo "==> cosign verify (keyless policy) child ${CHILD_REF}"
if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${CHILD_REF}' -o text"; then
CHILD_VERIFIED=true
echo "Keyless verification succeeded for child ${CHILD_REF}"
else
echo "Keyless verification failed for child ${CHILD_REF}"
fi
else
echo "No child digest found for ${BASE_IMAGE}:${CHILD_TAG}; skipping"
fi
done
if [ "${CHILD_VERIFIED}" != "true" ]; then
echo "Failed to verify index and no child manifests verified for ${BASE_IMAGE}:${IMAGE_TAG}"
exit 1
fi
fi fi
) || TAG_FAILED=true ) || TAG_FAILED=true
+7 -1
View File
@@ -461,6 +461,8 @@
"filterByApprovalState": "Филтрирайте по състояние на одобрение", "filterByApprovalState": "Филтрирайте по състояние на одобрение",
"approvalListEmpty": "Няма одобрения", "approvalListEmpty": "Няма одобрения",
"approvalState": "Състояние на одобрение", "approvalState": "Състояние на одобрение",
"approvalLoadMore": "Load more",
"loadingApprovals": "Loading Approvals",
"approve": "Одобряване", "approve": "Одобряване",
"approved": "Одобрен", "approved": "Одобрен",
"denied": "Отказан", "denied": "Отказан",
@@ -1169,7 +1171,8 @@
"actionViewLogs": "Преглед на дневници", "actionViewLogs": "Преглед на дневници",
"noneSelected": "Нищо не е избрано", "noneSelected": "Нищо не е избрано",
"orgNotFound2": "Няма намерени организации.", "orgNotFound2": "Няма намерени организации.",
"searchProgress": "Търсене...", "searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"create": "Създаване", "create": "Създаване",
"orgs": "Организации", "orgs": "Организации",
"loginError": "Възникна неочаквана грешка. Моля, опитайте отново.", "loginError": "Възникна неочаквана грешка. Моля, опитайте отново.",
@@ -1916,6 +1919,9 @@
"authPageBrandingQuestionRemove": "Сигурни ли сте, че искате да премахнете брандинга за страниците за автентификация?", "authPageBrandingQuestionRemove": "Сигурни ли сте, че искате да премахнете брандинга за страниците за автентификация?",
"authPageBrandingDeleteConfirm": "Потвърждение на изтриване на брандинга.", "authPageBrandingDeleteConfirm": "Потвърждение на изтриване на брандинга.",
"brandingLogoURL": "URL адрес на логото.", "brandingLogoURL": "URL адрес на логото.",
"brandingLogoURLOrPath": "Logo URL or Path",
"brandingLogoPathDescription": "Enter a URL or a local path.",
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
"brandingPrimaryColor": "Основен цвят.", "brandingPrimaryColor": "Основен цвят.",
"brandingLogoWidth": "Ширина (px).", "brandingLogoWidth": "Ширина (px).",
"brandingLogoHeight": "Височина (px).", "brandingLogoHeight": "Височина (px).",
+7 -1
View File
@@ -461,6 +461,8 @@
"filterByApprovalState": "Filtrovat podle státu schválení", "filterByApprovalState": "Filtrovat podle státu schválení",
"approvalListEmpty": "Žádná schválení", "approvalListEmpty": "Žádná schválení",
"approvalState": "Země schválení", "approvalState": "Země schválení",
"approvalLoadMore": "Load more",
"loadingApprovals": "Loading Approvals",
"approve": "Schválit", "approve": "Schválit",
"approved": "Schváleno", "approved": "Schváleno",
"denied": "Zamítnuto", "denied": "Zamítnuto",
@@ -1169,7 +1171,8 @@
"actionViewLogs": "Zobrazit logy", "actionViewLogs": "Zobrazit logy",
"noneSelected": "Není vybráno", "noneSelected": "Není vybráno",
"orgNotFound2": "Nebyly nalezeny žádné organizace.", "orgNotFound2": "Nebyly nalezeny žádné organizace.",
"searchProgress": "Hledat...", "searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"create": "Vytvořit", "create": "Vytvořit",
"orgs": "Organizace", "orgs": "Organizace",
"loginError": "Došlo k neočekávané chybě. Zkuste to prosím znovu.", "loginError": "Došlo k neočekávané chybě. Zkuste to prosím znovu.",
@@ -1916,6 +1919,9 @@
"authPageBrandingQuestionRemove": "Jste si jisti, že chcete odstranit branding autentizačních stránek?", "authPageBrandingQuestionRemove": "Jste si jisti, že chcete odstranit branding autentizačních stránek?",
"authPageBrandingDeleteConfirm": "Potvrzení odstranění brandingu", "authPageBrandingDeleteConfirm": "Potvrzení odstranění brandingu",
"brandingLogoURL": "URL loga", "brandingLogoURL": "URL loga",
"brandingLogoURLOrPath": "Logo URL or Path",
"brandingLogoPathDescription": "Enter a URL or a local path.",
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
"brandingPrimaryColor": "Primární barva", "brandingPrimaryColor": "Primární barva",
"brandingLogoWidth": "Šířka (px)", "brandingLogoWidth": "Šířka (px)",
"brandingLogoHeight": "Výška (px)", "brandingLogoHeight": "Výška (px)",
+7 -1
View File
@@ -461,6 +461,8 @@
"filterByApprovalState": "Filtern nach Genehmigungsstatus", "filterByApprovalState": "Filtern nach Genehmigungsstatus",
"approvalListEmpty": "Keine Genehmigungen", "approvalListEmpty": "Keine Genehmigungen",
"approvalState": "Genehmigungsstatus", "approvalState": "Genehmigungsstatus",
"approvalLoadMore": "Load more",
"loadingApprovals": "Loading Approvals",
"approve": "Bestätigen", "approve": "Bestätigen",
"approved": "Genehmigt", "approved": "Genehmigt",
"denied": "Verweigert", "denied": "Verweigert",
@@ -1169,7 +1171,8 @@
"actionViewLogs": "Logs anzeigen", "actionViewLogs": "Logs anzeigen",
"noneSelected": "Keine ausgewählt", "noneSelected": "Keine ausgewählt",
"orgNotFound2": "Keine Organisationen gefunden.", "orgNotFound2": "Keine Organisationen gefunden.",
"searchProgress": "Suche...", "searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"create": "Erstellen", "create": "Erstellen",
"orgs": "Organisationen", "orgs": "Organisationen",
"loginError": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.", "loginError": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
@@ -1916,6 +1919,9 @@
"authPageBrandingQuestionRemove": "Sind Sie sicher, dass Sie das Branding für Authentifizierungsseiten entfernen möchten?", "authPageBrandingQuestionRemove": "Sind Sie sicher, dass Sie das Branding für Authentifizierungsseiten entfernen möchten?",
"authPageBrandingDeleteConfirm": "Branding löschen bestätigen", "authPageBrandingDeleteConfirm": "Branding löschen bestätigen",
"brandingLogoURL": "Logo URL", "brandingLogoURL": "Logo URL",
"brandingLogoURLOrPath": "Logo URL or Path",
"brandingLogoPathDescription": "Enter a URL or a local path.",
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
"brandingPrimaryColor": "Primär-Farbe", "brandingPrimaryColor": "Primär-Farbe",
"brandingLogoWidth": "Breite (px)", "brandingLogoWidth": "Breite (px)",
"brandingLogoHeight": "Höhe (px)", "brandingLogoHeight": "Höhe (px)",
+7 -1
View File
@@ -461,6 +461,8 @@
"filterByApprovalState": "Filtrar por estado de aprobación", "filterByApprovalState": "Filtrar por estado de aprobación",
"approvalListEmpty": "No hay aprobaciones", "approvalListEmpty": "No hay aprobaciones",
"approvalState": "Estado de aprobación", "approvalState": "Estado de aprobación",
"approvalLoadMore": "Load more",
"loadingApprovals": "Loading Approvals",
"approve": "Aprobar", "approve": "Aprobar",
"approved": "Aprobado", "approved": "Aprobado",
"denied": "Denegado", "denied": "Denegado",
@@ -1169,7 +1171,8 @@
"actionViewLogs": "Ver registros", "actionViewLogs": "Ver registros",
"noneSelected": "Ninguno seleccionado", "noneSelected": "Ninguno seleccionado",
"orgNotFound2": "No se encontraron organizaciones.", "orgNotFound2": "No se encontraron organizaciones.",
"searchProgress": "Buscar...", "searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"create": "Crear", "create": "Crear",
"orgs": "Organizaciones", "orgs": "Organizaciones",
"loginError": "Ocurrió un error inesperado. Por favor, inténtelo de nuevo.", "loginError": "Ocurrió un error inesperado. Por favor, inténtelo de nuevo.",
@@ -1916,6 +1919,9 @@
"authPageBrandingQuestionRemove": "¿Está seguro de que desea eliminar la marca de las páginas de autenticación?", "authPageBrandingQuestionRemove": "¿Está seguro de que desea eliminar la marca de las páginas de autenticación?",
"authPageBrandingDeleteConfirm": "Confirmar eliminación de la marca", "authPageBrandingDeleteConfirm": "Confirmar eliminación de la marca",
"brandingLogoURL": "URL del logotipo", "brandingLogoURL": "URL del logotipo",
"brandingLogoURLOrPath": "Logo URL or Path",
"brandingLogoPathDescription": "Enter a URL or a local path.",
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
"brandingPrimaryColor": "Color primario", "brandingPrimaryColor": "Color primario",
"brandingLogoWidth": "Ancho (px)", "brandingLogoWidth": "Ancho (px)",
"brandingLogoHeight": "Altura (px)", "brandingLogoHeight": "Altura (px)",
+7 -1
View File
@@ -461,6 +461,8 @@
"filterByApprovalState": "Filtrer par État d'Approbation", "filterByApprovalState": "Filtrer par État d'Approbation",
"approvalListEmpty": "Aucune approbation", "approvalListEmpty": "Aucune approbation",
"approvalState": "État d'approbation", "approvalState": "État d'approbation",
"approvalLoadMore": "Load more",
"loadingApprovals": "Loading Approvals",
"approve": "Approuver", "approve": "Approuver",
"approved": "Approuvé", "approved": "Approuvé",
"denied": "Refusé", "denied": "Refusé",
@@ -1169,7 +1171,8 @@
"actionViewLogs": "Voir les logs", "actionViewLogs": "Voir les logs",
"noneSelected": "Aucune sélection", "noneSelected": "Aucune sélection",
"orgNotFound2": "Aucune organisation trouvée.", "orgNotFound2": "Aucune organisation trouvée.",
"searchProgress": "Rechercher...", "searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"create": "Créer", "create": "Créer",
"orgs": "Organisations", "orgs": "Organisations",
"loginError": "Une erreur inattendue s'est produite. Veuillez réessayer.", "loginError": "Une erreur inattendue s'est produite. Veuillez réessayer.",
@@ -1916,6 +1919,9 @@
"authPageBrandingQuestionRemove": "Êtes-vous sûr de vouloir supprimer la marque des pages d'authentification ?", "authPageBrandingQuestionRemove": "Êtes-vous sûr de vouloir supprimer la marque des pages d'authentification ?",
"authPageBrandingDeleteConfirm": "Confirmer la suppression de la marque", "authPageBrandingDeleteConfirm": "Confirmer la suppression de la marque",
"brandingLogoURL": "URL du logo", "brandingLogoURL": "URL du logo",
"brandingLogoURLOrPath": "Logo URL or Path",
"brandingLogoPathDescription": "Enter a URL or a local path.",
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
"brandingPrimaryColor": "Couleur principale", "brandingPrimaryColor": "Couleur principale",
"brandingLogoWidth": "Largeur (px)", "brandingLogoWidth": "Largeur (px)",
"brandingLogoHeight": "Hauteur (px)", "brandingLogoHeight": "Hauteur (px)",
+7 -1
View File
@@ -461,6 +461,8 @@
"filterByApprovalState": "Filtra Per Stato Di Approvazione", "filterByApprovalState": "Filtra Per Stato Di Approvazione",
"approvalListEmpty": "Nessuna approvazione", "approvalListEmpty": "Nessuna approvazione",
"approvalState": "Stato Di Approvazione", "approvalState": "Stato Di Approvazione",
"approvalLoadMore": "Load more",
"loadingApprovals": "Loading Approvals",
"approve": "Approva", "approve": "Approva",
"approved": "Approvato", "approved": "Approvato",
"denied": "Negato", "denied": "Negato",
@@ -1169,7 +1171,8 @@
"actionViewLogs": "Visualizza Log", "actionViewLogs": "Visualizza Log",
"noneSelected": "Nessuna selezione", "noneSelected": "Nessuna selezione",
"orgNotFound2": "Nessuna organizzazione trovata.", "orgNotFound2": "Nessuna organizzazione trovata.",
"searchProgress": "Ricerca...", "searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"create": "Crea", "create": "Crea",
"orgs": "Organizzazioni", "orgs": "Organizzazioni",
"loginError": "Si è verificato un errore imprevisto. Riprova.", "loginError": "Si è verificato un errore imprevisto. Riprova.",
@@ -1916,6 +1919,9 @@
"authPageBrandingQuestionRemove": "Sei sicuro di voler rimuovere il branding per le pagine di autenticazione?", "authPageBrandingQuestionRemove": "Sei sicuro di voler rimuovere il branding per le pagine di autenticazione?",
"authPageBrandingDeleteConfirm": "Conferma Eliminazione Branding", "authPageBrandingDeleteConfirm": "Conferma Eliminazione Branding",
"brandingLogoURL": "URL Logo", "brandingLogoURL": "URL Logo",
"brandingLogoURLOrPath": "Logo URL or Path",
"brandingLogoPathDescription": "Enter a URL or a local path.",
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
"brandingPrimaryColor": "Colore Primario", "brandingPrimaryColor": "Colore Primario",
"brandingLogoWidth": "Larghezza (px)", "brandingLogoWidth": "Larghezza (px)",
"brandingLogoHeight": "Altezza (px)", "brandingLogoHeight": "Altezza (px)",
+7 -1
View File
@@ -461,6 +461,8 @@
"filterByApprovalState": "승인 상태로 필터링", "filterByApprovalState": "승인 상태로 필터링",
"approvalListEmpty": "승인이 없습니다.", "approvalListEmpty": "승인이 없습니다.",
"approvalState": "승인 상태", "approvalState": "승인 상태",
"approvalLoadMore": "Load more",
"loadingApprovals": "Loading Approvals",
"approve": "승인", "approve": "승인",
"approved": "승인됨", "approved": "승인됨",
"denied": "거부됨", "denied": "거부됨",
@@ -1169,7 +1171,8 @@
"actionViewLogs": "로그 보기", "actionViewLogs": "로그 보기",
"noneSelected": "선택된 항목 없음", "noneSelected": "선택된 항목 없음",
"orgNotFound2": "조직이 없습니다.", "orgNotFound2": "조직이 없습니다.",
"searchProgress": "검색...", "searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"create": "생성", "create": "생성",
"orgs": "조직", "orgs": "조직",
"loginError": "예기치 않은 오류가 발생했습니다. 다시 시도해주세요.", "loginError": "예기치 않은 오류가 발생했습니다. 다시 시도해주세요.",
@@ -1916,6 +1919,9 @@
"authPageBrandingQuestionRemove": "인증 페이지의 브랜딩을 제거하시겠습니까?", "authPageBrandingQuestionRemove": "인증 페이지의 브랜딩을 제거하시겠습니까?",
"authPageBrandingDeleteConfirm": "브랜딩 삭제 확인", "authPageBrandingDeleteConfirm": "브랜딩 삭제 확인",
"brandingLogoURL": "로고 URL", "brandingLogoURL": "로고 URL",
"brandingLogoURLOrPath": "Logo URL or Path",
"brandingLogoPathDescription": "Enter a URL or a local path.",
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
"brandingPrimaryColor": "기본 색상", "brandingPrimaryColor": "기본 색상",
"brandingLogoWidth": "너비(px)", "brandingLogoWidth": "너비(px)",
"brandingLogoHeight": "높이(px)", "brandingLogoHeight": "높이(px)",
+7 -1
View File
@@ -461,6 +461,8 @@
"filterByApprovalState": "Filtrer etter godkjenningsstatus", "filterByApprovalState": "Filtrer etter godkjenningsstatus",
"approvalListEmpty": "Ingen godkjenninger", "approvalListEmpty": "Ingen godkjenninger",
"approvalState": "Godkjennings tilstand", "approvalState": "Godkjennings tilstand",
"approvalLoadMore": "Load more",
"loadingApprovals": "Loading Approvals",
"approve": "Godkjenn", "approve": "Godkjenn",
"approved": "Godkjent", "approved": "Godkjent",
"denied": "Avvist", "denied": "Avvist",
@@ -1169,7 +1171,8 @@
"actionViewLogs": "Vis logger", "actionViewLogs": "Vis logger",
"noneSelected": "Ingen valgt", "noneSelected": "Ingen valgt",
"orgNotFound2": "Ingen organisasjoner funnet.", "orgNotFound2": "Ingen organisasjoner funnet.",
"searchProgress": "Søker...", "searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"create": "Opprett", "create": "Opprett",
"orgs": "Organisasjoner", "orgs": "Organisasjoner",
"loginError": "En uventet feil oppstod. Vennligst prøv igjen.", "loginError": "En uventet feil oppstod. Vennligst prøv igjen.",
@@ -1916,6 +1919,9 @@
"authPageBrandingQuestionRemove": "Er du sikker på at du vil fjerne merkevarebyggingen for autentiseringssider?", "authPageBrandingQuestionRemove": "Er du sikker på at du vil fjerne merkevarebyggingen for autentiseringssider?",
"authPageBrandingDeleteConfirm": "Bekreft sletting av merkevarebygging", "authPageBrandingDeleteConfirm": "Bekreft sletting av merkevarebygging",
"brandingLogoURL": "Logo URL", "brandingLogoURL": "Logo URL",
"brandingLogoURLOrPath": "Logo URL or Path",
"brandingLogoPathDescription": "Enter a URL or a local path.",
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
"brandingPrimaryColor": "Primærfarge", "brandingPrimaryColor": "Primærfarge",
"brandingLogoWidth": "Bredde (px)", "brandingLogoWidth": "Bredde (px)",
"brandingLogoHeight": "Høyde (px)", "brandingLogoHeight": "Høyde (px)",
+7 -1
View File
@@ -461,6 +461,8 @@
"filterByApprovalState": "Filter op goedkeuringsstatus", "filterByApprovalState": "Filter op goedkeuringsstatus",
"approvalListEmpty": "Geen goedkeuringen", "approvalListEmpty": "Geen goedkeuringen",
"approvalState": "Goedkeuring status", "approvalState": "Goedkeuring status",
"approvalLoadMore": "Load more",
"loadingApprovals": "Loading Approvals",
"approve": "Goedkeuren", "approve": "Goedkeuren",
"approved": "Goedgekeurd", "approved": "Goedgekeurd",
"denied": "Geweigerd", "denied": "Geweigerd",
@@ -1169,7 +1171,8 @@
"actionViewLogs": "Logboeken bekijken", "actionViewLogs": "Logboeken bekijken",
"noneSelected": "Niet geselecteerd", "noneSelected": "Niet geselecteerd",
"orgNotFound2": "Geen organisaties gevonden.", "orgNotFound2": "Geen organisaties gevonden.",
"searchProgress": "Zoeken...", "searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"create": "Aanmaken", "create": "Aanmaken",
"orgs": "Organisaties", "orgs": "Organisaties",
"loginError": "Er is een onverwachte fout opgetreden. Probeer het opnieuw.", "loginError": "Er is een onverwachte fout opgetreden. Probeer het opnieuw.",
@@ -1916,6 +1919,9 @@
"authPageBrandingQuestionRemove": "Weet u zeker dat u de branding voor Auth-pagina's wilt verwijderen?", "authPageBrandingQuestionRemove": "Weet u zeker dat u de branding voor Auth-pagina's wilt verwijderen?",
"authPageBrandingDeleteConfirm": "Bevestig verwijder Branding", "authPageBrandingDeleteConfirm": "Bevestig verwijder Branding",
"brandingLogoURL": "Het logo-URL", "brandingLogoURL": "Het logo-URL",
"brandingLogoURLOrPath": "Logo URL or Path",
"brandingLogoPathDescription": "Enter a URL or a local path.",
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
"brandingPrimaryColor": "Primaire kleur", "brandingPrimaryColor": "Primaire kleur",
"brandingLogoWidth": "Breedte (px)", "brandingLogoWidth": "Breedte (px)",
"brandingLogoHeight": "Hoogte (px)", "brandingLogoHeight": "Hoogte (px)",
+7 -1
View File
@@ -461,6 +461,8 @@
"filterByApprovalState": "Filtruj według państwa zatwierdzenia", "filterByApprovalState": "Filtruj według państwa zatwierdzenia",
"approvalListEmpty": "Brak zatwierdzeń", "approvalListEmpty": "Brak zatwierdzeń",
"approvalState": "Państwo zatwierdzające", "approvalState": "Państwo zatwierdzające",
"approvalLoadMore": "Load more",
"loadingApprovals": "Loading Approvals",
"approve": "Zatwierdź", "approve": "Zatwierdź",
"approved": "Zatwierdzone", "approved": "Zatwierdzone",
"denied": "Odmowa", "denied": "Odmowa",
@@ -1169,7 +1171,8 @@
"actionViewLogs": "Zobacz dzienniki", "actionViewLogs": "Zobacz dzienniki",
"noneSelected": "Nie wybrano", "noneSelected": "Nie wybrano",
"orgNotFound2": "Nie znaleziono organizacji.", "orgNotFound2": "Nie znaleziono organizacji.",
"searchProgress": "Szukaj...", "searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"create": "Utwórz", "create": "Utwórz",
"orgs": "Organizacje", "orgs": "Organizacje",
"loginError": "Wystąpił nieoczekiwany błąd. Spróbuj ponownie.", "loginError": "Wystąpił nieoczekiwany błąd. Spróbuj ponownie.",
@@ -1916,6 +1919,9 @@
"authPageBrandingQuestionRemove": "Czy na pewno chcesz usunąć branding dla stron uwierzytelniania?", "authPageBrandingQuestionRemove": "Czy na pewno chcesz usunąć branding dla stron uwierzytelniania?",
"authPageBrandingDeleteConfirm": "Potwierdź usunięcie brandingu", "authPageBrandingDeleteConfirm": "Potwierdź usunięcie brandingu",
"brandingLogoURL": "URL logo", "brandingLogoURL": "URL logo",
"brandingLogoURLOrPath": "Logo URL or Path",
"brandingLogoPathDescription": "Enter a URL or a local path.",
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
"brandingPrimaryColor": "Główny kolor", "brandingPrimaryColor": "Główny kolor",
"brandingLogoWidth": "Szerokość (piksele)", "brandingLogoWidth": "Szerokość (piksele)",
"brandingLogoHeight": "Wysokość (piksele)", "brandingLogoHeight": "Wysokość (piksele)",
+7 -1
View File
@@ -461,6 +461,8 @@
"filterByApprovalState": "Filtrar por estado de aprovação", "filterByApprovalState": "Filtrar por estado de aprovação",
"approvalListEmpty": "Sem aprovações", "approvalListEmpty": "Sem aprovações",
"approvalState": "Estado de aprovação", "approvalState": "Estado de aprovação",
"approvalLoadMore": "Load more",
"loadingApprovals": "Loading Approvals",
"approve": "Aprovar", "approve": "Aprovar",
"approved": "Aceito", "approved": "Aceito",
"denied": "Negado", "denied": "Negado",
@@ -1169,7 +1171,8 @@
"actionViewLogs": "Visualizar registros", "actionViewLogs": "Visualizar registros",
"noneSelected": "Nenhum selecionado", "noneSelected": "Nenhum selecionado",
"orgNotFound2": "Nenhuma organização encontrada.", "orgNotFound2": "Nenhuma organização encontrada.",
"searchProgress": "Pesquisar...", "searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"create": "Criar", "create": "Criar",
"orgs": "Organizações", "orgs": "Organizações",
"loginError": "Ocorreu um erro inesperado. Por favor, tente novamente.", "loginError": "Ocorreu um erro inesperado. Por favor, tente novamente.",
@@ -1916,6 +1919,9 @@
"authPageBrandingQuestionRemove": "Tem certeza de que deseja remover a marcação das Páginas de Autenticação?", "authPageBrandingQuestionRemove": "Tem certeza de que deseja remover a marcação das Páginas de Autenticação?",
"authPageBrandingDeleteConfirm": "Confirmar Exclusão de Marca", "authPageBrandingDeleteConfirm": "Confirmar Exclusão de Marca",
"brandingLogoURL": "URL do Logo", "brandingLogoURL": "URL do Logo",
"brandingLogoURLOrPath": "Logo URL or Path",
"brandingLogoPathDescription": "Enter a URL or a local path.",
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
"brandingPrimaryColor": "Cor Primária", "brandingPrimaryColor": "Cor Primária",
"brandingLogoWidth": "Largura (px)", "brandingLogoWidth": "Largura (px)",
"brandingLogoHeight": "Altura (px)", "brandingLogoHeight": "Altura (px)",
+7 -1
View File
@@ -461,6 +461,8 @@
"filterByApprovalState": "Фильтр по состоянию утверждения", "filterByApprovalState": "Фильтр по состоянию утверждения",
"approvalListEmpty": "Нет утверждений", "approvalListEmpty": "Нет утверждений",
"approvalState": "Состояние одобрения", "approvalState": "Состояние одобрения",
"approvalLoadMore": "Load more",
"loadingApprovals": "Loading Approvals",
"approve": "Одобрить", "approve": "Одобрить",
"approved": "Одобрено", "approved": "Одобрено",
"denied": "Отказано", "denied": "Отказано",
@@ -1169,7 +1171,8 @@
"actionViewLogs": "Просмотр журналов", "actionViewLogs": "Просмотр журналов",
"noneSelected": "Ничего не выбрано", "noneSelected": "Ничего не выбрано",
"orgNotFound2": "Организации не найдены.", "orgNotFound2": "Организации не найдены.",
"searchProgress": "Поиск...", "searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"create": "Создать", "create": "Создать",
"orgs": "Организации", "orgs": "Организации",
"loginError": "Произошла непредвиденная ошибка. Пожалуйста, попробуйте еще раз.", "loginError": "Произошла непредвиденная ошибка. Пожалуйста, попробуйте еще раз.",
@@ -1916,6 +1919,9 @@
"authPageBrandingQuestionRemove": "Вы уверены, что хотите удалить брендирование для страниц аутентификации?", "authPageBrandingQuestionRemove": "Вы уверены, что хотите удалить брендирование для страниц аутентификации?",
"authPageBrandingDeleteConfirm": "Подтвердить удаление брендирования", "authPageBrandingDeleteConfirm": "Подтвердить удаление брендирования",
"brandingLogoURL": "URL логотипа", "brandingLogoURL": "URL логотипа",
"brandingLogoURLOrPath": "Logo URL or Path",
"brandingLogoPathDescription": "Enter a URL or a local path.",
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
"brandingPrimaryColor": "Основной цвет", "brandingPrimaryColor": "Основной цвет",
"brandingLogoWidth": "Ширина (px)", "brandingLogoWidth": "Ширина (px)",
"brandingLogoHeight": "Высота (px)", "brandingLogoHeight": "Высота (px)",
+7 -1
View File
@@ -461,6 +461,8 @@
"filterByApprovalState": "Onay Durumuna Göre Filtrele", "filterByApprovalState": "Onay Durumuna Göre Filtrele",
"approvalListEmpty": "Onay yok", "approvalListEmpty": "Onay yok",
"approvalState": "Onay Durumu", "approvalState": "Onay Durumu",
"approvalLoadMore": "Load more",
"loadingApprovals": "Loading Approvals",
"approve": "Onayla", "approve": "Onayla",
"approved": "Onaylandı", "approved": "Onaylandı",
"denied": "Reddedildi", "denied": "Reddedildi",
@@ -1169,7 +1171,8 @@
"actionViewLogs": "Kayıtları Görüntüle", "actionViewLogs": "Kayıtları Görüntüle",
"noneSelected": "Hiçbiri seçili değil", "noneSelected": "Hiçbiri seçili değil",
"orgNotFound2": "Hiçbir organizasyon bulunamadı.", "orgNotFound2": "Hiçbir organizasyon bulunamadı.",
"searchProgress": "Ara...", "searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"create": "Oluştur", "create": "Oluştur",
"orgs": "Organizasyonlar", "orgs": "Organizasyonlar",
"loginError": "Beklenmeyen bir hata oluştu. Lütfen tekrar deneyin.", "loginError": "Beklenmeyen bir hata oluştu. Lütfen tekrar deneyin.",
@@ -1916,6 +1919,9 @@
"authPageBrandingQuestionRemove": "Kimlik Sayfaları için markayı kaldırmak istediğinizden emin misiniz?", "authPageBrandingQuestionRemove": "Kimlik Sayfaları için markayı kaldırmak istediğinizden emin misiniz?",
"authPageBrandingDeleteConfirm": "Markayı Silmeyi Onayla", "authPageBrandingDeleteConfirm": "Markayı Silmeyi Onayla",
"brandingLogoURL": "Logo URL", "brandingLogoURL": "Logo URL",
"brandingLogoURLOrPath": "Logo URL or Path",
"brandingLogoPathDescription": "Enter a URL or a local path.",
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
"brandingPrimaryColor": "Ana Renk", "brandingPrimaryColor": "Ana Renk",
"brandingLogoWidth": "Genişlik (px)", "brandingLogoWidth": "Genişlik (px)",
"brandingLogoHeight": "Yükseklik (px)", "brandingLogoHeight": "Yükseklik (px)",
+7 -1
View File
@@ -461,6 +461,8 @@
"filterByApprovalState": "按批准状态过滤", "filterByApprovalState": "按批准状态过滤",
"approvalListEmpty": "无批准", "approvalListEmpty": "无批准",
"approvalState": "审批状态", "approvalState": "审批状态",
"approvalLoadMore": "Load more",
"loadingApprovals": "Loading Approvals",
"approve": "批准", "approve": "批准",
"approved": "已批准", "approved": "已批准",
"denied": "被拒绝", "denied": "被拒绝",
@@ -1169,7 +1171,8 @@
"actionViewLogs": "查看日志", "actionViewLogs": "查看日志",
"noneSelected": "未选择", "noneSelected": "未选择",
"orgNotFound2": "未找到组织。", "orgNotFound2": "未找到组织。",
"searchProgress": "搜索中...", "searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"create": "创建", "create": "创建",
"orgs": "组织", "orgs": "组织",
"loginError": "发生意外错误。请重试。", "loginError": "发生意外错误。请重试。",
@@ -1916,6 +1919,9 @@
"authPageBrandingQuestionRemove": "您确定要移除授权页面的品牌吗?", "authPageBrandingQuestionRemove": "您确定要移除授权页面的品牌吗?",
"authPageBrandingDeleteConfirm": "确认删除品牌", "authPageBrandingDeleteConfirm": "确认删除品牌",
"brandingLogoURL": "Logo URL", "brandingLogoURL": "Logo URL",
"brandingLogoURLOrPath": "Logo URL or Path",
"brandingLogoPathDescription": "Enter a URL or a local path.",
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
"brandingPrimaryColor": "主要颜色", "brandingPrimaryColor": "主要颜色",
"brandingLogoWidth": "宽度(px", "brandingLogoWidth": "宽度(px",
"brandingLogoHeight": "高度(px", "brandingLogoHeight": "高度(px",
+4 -4
View File
@@ -15,10 +15,10 @@ export const sandboxLimitSet: LimitSet = {
}; };
export const freeLimitSet: LimitSet = { export const freeLimitSet: LimitSet = {
[FeatureId.SITES]: { value: 5, description: "Basic limit" }, [FeatureId.USERS]: { value: 5, description: "Starter limit" },
[FeatureId.USERS]: { value: 5, description: "Basic limit" }, [FeatureId.SITES]: { value: 5, description: "Starter limit" },
[FeatureId.DOMAINS]: { value: 5, description: "Basic limit" }, [FeatureId.DOMAINS]: { value: 5, description: "Starter limit" },
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Basic limit" }, [FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Starter limit" },
}; };
export const tier1LimitSet: LimitSet = { export const tier1LimitSet: LimitSet = {
@@ -28,7 +28,6 @@ import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types";
import { isSubscribed } from "#private/lib/isSubscribed"; import { isSubscribed } from "#private/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import privateConfig from "#private/lib/config"; import privateConfig from "#private/lib/config";
import { build } from "@server/build";
const paramsSchema = z.strictObject({ orgId: z.string().nonempty() }); const paramsSchema = z.strictObject({ orgId: z.string().nonempty() });
@@ -123,14 +122,12 @@ export async function createOrgOidcIdp(
let { autoProvision } = parsedBody.data; let { autoProvision } = parsedBody.data;
if (build == "saas") { // this is not paywalled with a ee license because this whole endpoint is restricted const subscribed = await isSubscribed(
const subscribed = await isSubscribed( orgId,
orgId, tierMatrix.deviceApprovals
tierMatrix.deviceApprovals );
); if (!subscribed) {
if (!subscribed) { autoProvision = false;
autoProvision = false;
}
} }
const key = config.getRawConfig().server.secret!; const key = config.getRawConfig().server.secret!;
@@ -27,7 +27,6 @@ import config from "@server/lib/config";
import { isSubscribed } from "#private/lib/isSubscribed"; import { isSubscribed } from "#private/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import privateConfig from "#private/lib/config"; import privateConfig from "#private/lib/config";
import { build } from "@server/build";
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -128,15 +127,12 @@ export async function updateOrgOidcIdp(
let { autoProvision } = parsedBody.data; let { autoProvision } = parsedBody.data;
if (build == "saas") { const subscribed = await isSubscribed(
// this is not paywalled with a ee license because this whole endpoint is restricted orgId,
const subscribed = await isSubscribed( tierMatrix.deviceApprovals
orgId, );
tierMatrix.deviceApprovals if (!subscribed) {
); autoProvision = false;
if (!subscribed) {
autoProvision = false;
}
} }
// Check if IDP exists and is of type OIDC // Check if IDP exists and is of type OIDC
+2 -2
View File
@@ -797,7 +797,7 @@ async function notAllowed(
) { ) {
let loginPage: LoginPage | null = null; let loginPage: LoginPage | null = null;
if (orgId) { if (orgId) {
const subscribed = await isSubscribed( // this is fine because the org login page is only a saas feature const subscribed = await isSubscribed(
orgId, orgId,
tierMatrix.loginPageDomain tierMatrix.loginPageDomain
); );
@@ -854,7 +854,7 @@ async function headerAuthChallenged(
) { ) {
let loginPage: LoginPage | null = null; let loginPage: LoginPage | null = null;
if (orgId) { if (orgId) {
const subscribed = await isSubscribed(orgId, tierMatrix.loginPageDomain); // this is fine because the org login page is only a saas feature const subscribed = await isSubscribed(orgId, tierMatrix.loginPageDomain);
if (subscribed) { if (subscribed) {
loginPage = await getOrgLoginPage(orgId); loginPage = await getOrgLoginPage(orgId);
} }
@@ -61,7 +61,7 @@ import {
import { FeatureId } from "@server/lib/billing/features"; import { FeatureId } from "@server/lib/billing/features";
// Plan tier definitions matching the mockup // Plan tier definitions matching the mockup
type PlanId = "basic" | "home" | "team" | "business" | "enterprise"; type PlanId = "starter" | "home" | "team" | "business" | "enterprise";
type PlanOption = { type PlanOption = {
id: PlanId; id: PlanId;
@@ -73,8 +73,8 @@ type PlanOption = {
const planOptions: PlanOption[] = [ const planOptions: PlanOption[] = [
{ {
id: "basic", id: "starter",
name: "Basic", name: "Starter",
price: "Free", price: "Free",
tierType: null tierType: null
}, },
@@ -109,10 +109,10 @@ const planOptions: PlanOption[] = [
// Tier limits mapping derived from limit sets // Tier limits mapping derived from limit sets
const tierLimits: Record< const tierLimits: Record<
Tier | "basic", Tier | "starter",
{ users: number; sites: number; domains: number; remoteNodes: number } { users: number; sites: number; domains: number; remoteNodes: number }
> = { > = {
basic: { starter: {
users: freeLimitSet[FeatureId.USERS]?.value ?? 0, users: freeLimitSet[FeatureId.USERS]?.value ?? 0,
sites: freeLimitSet[FeatureId.SITES]?.value ?? 0, sites: freeLimitSet[FeatureId.SITES]?.value ?? 0,
domains: freeLimitSet[FeatureId.DOMAINS]?.value ?? 0, domains: freeLimitSet[FeatureId.DOMAINS]?.value ?? 0,
@@ -183,7 +183,7 @@ export default function BillingPage() {
// Confirmation dialog state // Confirmation dialog state
const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [pendingTier, setPendingTier] = useState<{ const [pendingTier, setPendingTier] = useState<{
tier: Tier | "basic"; tier: Tier | "starter";
action: "upgrade" | "downgrade"; action: "upgrade" | "downgrade";
planName: string; planName: string;
price: string; price: string;
@@ -402,8 +402,8 @@ export default function BillingPage() {
pendingTier.action === "upgrade" || pendingTier.action === "upgrade" ||
pendingTier.action === "downgrade" pendingTier.action === "downgrade"
) { ) {
// If downgrading to basic (free tier), go to Stripe portal // If downgrading to starter (free tier), go to Stripe portal
if (pendingTier.tier === "basic") { if (pendingTier.tier === "starter") {
handleModifySubscription(); handleModifySubscription();
} else if (hasSubscription) { } else if (hasSubscription) {
handleChangeTier(pendingTier.tier); handleChangeTier(pendingTier.tier);
@@ -417,7 +417,7 @@ export default function BillingPage() {
}; };
const showTierConfirmation = ( const showTierConfirmation = (
tier: Tier | "basic", tier: Tier | "starter",
action: "upgrade" | "downgrade", action: "upgrade" | "downgrade",
planName: string, planName: string,
price: string price: string
@@ -432,9 +432,9 @@ export default function BillingPage() {
// Get current plan ID from tier // Get current plan ID from tier
const getCurrentPlanId = (): PlanId => { const getCurrentPlanId = (): PlanId => {
if (!hasSubscription || !currentTier) return "basic"; if (!hasSubscription || !currentTier) return "starter";
const plan = planOptions.find((p) => p.tierType === currentTier); const plan = planOptions.find((p) => p.tierType === currentTier);
return plan?.id || "basic"; return plan?.id || "starter";
}; };
const currentPlanId = getCurrentPlanId(); const currentPlanId = getCurrentPlanId();
@@ -451,8 +451,8 @@ export default function BillingPage() {
} }
if (plan.id === currentPlanId) { if (plan.id === currentPlanId) {
// If it's the basic plan (basic with no subscription), show as current but disabled // If it's the starter plan (starter with no subscription), show as current but disabled
if (plan.id === "basic" && !hasSubscription) { if (plan.id === "starter" && !hasSubscription) {
return { return {
label: "Current Plan", label: "Current Plan",
action: () => {}, action: () => {},
@@ -484,10 +484,10 @@ export default function BillingPage() {
plan.name, plan.name,
plan.price + (" " + plan.priceDetail || "") plan.price + (" " + plan.priceDetail || "")
); );
} else if (plan.id === "basic") { } else if (plan.id === "starter") {
// Show confirmation for downgrading to basic (free tier) // Show confirmation for downgrading to starter (free tier)
showTierConfirmation( showTierConfirmation(
"basic", "starter",
"downgrade", "downgrade",
plan.name, plan.name,
plan.price plan.price
@@ -566,7 +566,7 @@ export default function BillingPage() {
}; };
// Check if downgrading to a tier would violate current usage limits // Check if downgrading to a tier would violate current usage limits
const checkLimitViolations = (targetTier: Tier | "basic"): Array<{ const checkLimitViolations = (targetTier: Tier | "starter"): Array<{
feature: string; feature: string;
currentUsage: number; currentUsage: number;
newLimit: number; newLimit: number;