Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
238a8bf81c | ||
|
|
bef83d80a1 | ||
|
|
8ed1ff152b | ||
|
|
d2c8bbfcb2 | ||
|
|
da1be650ea | ||
|
|
5cb5a866ca | ||
|
|
83e5319661 | ||
|
|
ed58d90ab8 | ||
|
|
4ee54243f3 | ||
|
|
350c0b796e | ||
|
|
663bb32b2c | ||
|
|
b86e963c9d |
@@ -1,16 +1,34 @@
|
||||
# CI routing infra. Editing the top-level router (build.yml) or this filter
|
||||
# config re-runs every area's jobs, so every job-gating filter below includes
|
||||
# *ci. That makes a change to how jobs are dispatched actually exercise those
|
||||
# jobs (self-testing), instead of a router edit only matching the project filter.
|
||||
ci: &ci
|
||||
- .github/workflows/build.yml
|
||||
- .github/config/.files.yaml
|
||||
|
||||
build: &build
|
||||
- *ci
|
||||
- build.gradle
|
||||
- app/(common|core|proprietary|saas)/build.gradle
|
||||
- Taskfile.yml
|
||||
- .taskfiles/backend.yml
|
||||
- .github/workflows/check-licence.yml
|
||||
|
||||
openapi: &openapi
|
||||
- *ci
|
||||
- *build
|
||||
- app/(common|core|proprietary|saas)/src/main/java/**
|
||||
- .github/workflows/check-openapi.yml
|
||||
|
||||
docker-base: &docker-base
|
||||
- docker/base/Dockerfile
|
||||
|
||||
# Dockerfiles only (base + embedded + unoserver). Gates the slow multi-arch
|
||||
# (arm64) leg of the PR docker test build: arm64 is only rebuilt when a
|
||||
# Dockerfile itself changes, not on every code PR.
|
||||
dockerfiles: &dockerfiles
|
||||
- docker/**/Dockerfile*
|
||||
|
||||
docker: &docker
|
||||
- docker/embedded/Dockerfile
|
||||
- docker/embedded/Dockerfile.fat
|
||||
@@ -23,13 +41,11 @@ docker: &docker
|
||||
- *docker-base
|
||||
|
||||
project: &project
|
||||
- *ci
|
||||
- app/(common|core|proprietary|saas)/src/(main|test)/java/**
|
||||
- *build
|
||||
- "app/(common|core|proprietary|saas)/src/(main|test)/resources/**/!(messages_*.properties|*.md)*"
|
||||
- exampleYmlFiles/**
|
||||
- gradle/**
|
||||
- libs/**
|
||||
- "testing/**/!(requirements*.txt|requirements*.in)*"
|
||||
- *docker
|
||||
- *docker-base
|
||||
- gradle.properties
|
||||
@@ -45,8 +61,11 @@ project: &project
|
||||
- .taskfiles/docker.yml
|
||||
- scripts/db-migration/**
|
||||
- .github/workflows/db-migration-test.yml
|
||||
- .github/workflows/docker-compose-tests.yml
|
||||
- .github/workflows/test-build-docker.yml
|
||||
|
||||
frontend: &frontend
|
||||
- *ci
|
||||
- frontend/**
|
||||
- .github/workflows/testdriver.yml
|
||||
- testing/**
|
||||
@@ -63,10 +82,14 @@ frontend: &frontend
|
||||
- Taskfile.yml
|
||||
- .taskfiles/frontend.yml
|
||||
- .taskfiles/e2e.yml
|
||||
- .github/workflows/frontend-validation.yml
|
||||
- .github/workflows/e2e-stubbed.yml
|
||||
- .github/workflows/e2e-live.yml
|
||||
|
||||
# Files that affect the Tauri desktop bundle. Gate the multi-OS Tauri build
|
||||
# job on changes to any of these.
|
||||
tauri: &tauri
|
||||
- *ci
|
||||
- frontend/editor/src-tauri/**
|
||||
- frontend/editor/src/desktop/**
|
||||
- frontend/editor/tsconfig.desktop.vite.json
|
||||
@@ -81,6 +104,7 @@ tauri: &tauri
|
||||
# the engine validation job on changes to engine sources or to the Java
|
||||
# tool surfaces it generates models from.
|
||||
engine: &engine
|
||||
- *ci
|
||||
- engine/**
|
||||
- app/(common|core|proprietary|saas)/src/main/java/**
|
||||
- .github/workflows/ai-engine.yml
|
||||
@@ -93,6 +117,7 @@ engine: &engine
|
||||
# tasks that drive generation. Deliberately excludes the broad frontend/docker/
|
||||
# testing globs, so a CSS-only PR does not boot the backend to rebuild the spec.
|
||||
generated-models: &generated-models
|
||||
- *ci
|
||||
- *openapi
|
||||
- frontend/editor/scripts/generate-tool-api-types.mts
|
||||
- frontend/editor/src/core/types/toolApiTypes.ts
|
||||
@@ -115,6 +140,7 @@ licenses-backend: &licenses-backend
|
||||
# Files that can affect premium / enterprise behaviour. Gate the enterprise
|
||||
# Playwright job on changes to any of these on PRs.
|
||||
proprietary: &proprietary
|
||||
- *ci
|
||||
- app/proprietary/**
|
||||
- frontend/editor/src/proprietary/**
|
||||
- frontend/editor/src/core/tests/enterprise/**
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
check-pr:
|
||||
if: (github.event_name == 'pull_request' && github.event.action != 'closed') || github.event_name == 'workflow_dispatch'
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
outputs:
|
||||
should_deploy: ${{ steps.decide.outputs.should_deploy }}
|
||||
is_fork: ${{ steps.resolve.outputs.is_fork }}
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
|
||||
deploy-v2-pr:
|
||||
needs: [pick, check-pr]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
if: needs.check-pr.outputs.should_deploy == 'true' && (needs.check-pr.outputs.is_fork == 'false' || needs.check-pr.outputs.allow_fork == 'true')
|
||||
# Concurrency control - only one deployment per PR at a time
|
||||
concurrency:
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' }}
|
||||
USE_DEPOT: ${{ needs.pick.outputs.use_depot == 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
# Single source of truth for whether this preview embeds the admin portal:
|
||||
# drives the image build-arg and the deployment comment.
|
||||
@@ -297,7 +297,6 @@ jobs:
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/storage:/storage:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||
POLICIES_ENABLED: "true"
|
||||
STIRLING_BILLING_ACCOUNT_LINK_ENABLED: "true"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SECURITY_INITIALLOGIN_USERNAME: "${{ secrets.TEST_LOGIN_USERNAME }}"
|
||||
@@ -476,7 +475,7 @@ jobs:
|
||||
cleanup-v2-deployment:
|
||||
if: github.event.action == 'closed'
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
check-comment:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
issues: write
|
||||
if: |
|
||||
@@ -180,13 +180,13 @@ jobs:
|
||||
|
||||
deploy-pr:
|
||||
needs: [pick, check-comment]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' }}
|
||||
USE_DEPOT: ${{ needs.pick.outputs.use_depot == 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
|
||||
steps:
|
||||
@@ -511,7 +511,7 @@ jobs:
|
||||
handle-label-commands:
|
||||
if: ${{ github.event.issue.pull_request != null }}
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
|
||||
@@ -5,6 +5,11 @@ name: _runner-pick
|
||||
# can pick a runner class without each one duplicating the 200-char gate
|
||||
# expression in their own `runs-on:`.
|
||||
#
|
||||
# It also owns the single Depot kill-switch (use_depot). Depot is currently
|
||||
# disabled repo-wide; downstream jobs gate their Depot runner/build usage on
|
||||
# use_depot so nothing has to be deleted to turn Depot off. Flip DEPOT_ENABLED
|
||||
# in the decide step to switch Depot back on.
|
||||
#
|
||||
# Caller pattern:
|
||||
#
|
||||
# jobs:
|
||||
@@ -13,12 +18,15 @@ name: _runner-pick
|
||||
#
|
||||
# real-work:
|
||||
# needs: pick
|
||||
# runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-8' }}
|
||||
# runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-8' || 'ubuntu-latest' }}
|
||||
# steps: [...]
|
||||
#
|
||||
# Output:
|
||||
# is_fork: "true" when the trigger is a pull_request from a fork or an
|
||||
# untrusted author_association, "false" otherwise.
|
||||
# Outputs:
|
||||
# is_fork: "true" when the trigger is a pull_request from a fork or an
|
||||
# untrusted author_association, "false" otherwise. Use this for
|
||||
# trust gating (skipping secret-dependent jobs on forks).
|
||||
# use_depot: "true" when downstream jobs should use Depot runners/builders.
|
||||
# Currently forced "false" (Depot disabled repo-wide).
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -26,6 +34,9 @@ on:
|
||||
is_fork:
|
||||
description: '"true" if the trigger is an untrusted fork PR.'
|
||||
value: ${{ jobs.pick.outputs.is_fork }}
|
||||
use_depot:
|
||||
description: '"true" when downstream jobs should use Depot. Currently forced off.'
|
||||
value: ${{ jobs.pick.outputs.use_depot }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -36,6 +47,7 @@ jobs:
|
||||
timeout-minutes: 1
|
||||
outputs:
|
||||
is_fork: ${{ steps.decide.outputs.is_fork }}
|
||||
use_depot: ${{ steps.decide.outputs.use_depot }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
@@ -50,21 +62,33 @@ jobs:
|
||||
AUTHOR_ASSOC: ${{ github.event.pull_request.author_association }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
# Depot kill-switch. Depot is disabled repo-wide: no job uses Depot
|
||||
# runners or the Depot build actions while this is false. All the
|
||||
# Depot wiring is left in place - set DEPOT_ENABLED=true to switch it
|
||||
# back on (it then activates on trusted, non-fork triggers as before).
|
||||
DEPOT_ENABLED=false
|
||||
|
||||
if [ -z "${PR_NUMBER:-}" ]; then
|
||||
# Not a pull_request event at all (push, schedule, workflow_dispatch,
|
||||
# workflow_call from a non-PR trigger) -> trusted by default.
|
||||
echo "is_fork=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
is_fork=false
|
||||
elif [ "${HEAD_REPO_FORK}" = "true" ]; then
|
||||
is_fork=true
|
||||
else
|
||||
case "${AUTHOR_ASSOC}" in
|
||||
OWNER|MEMBER|COLLABORATOR) is_fork=false ;;
|
||||
*) is_fork=true ;;
|
||||
esac
|
||||
fi
|
||||
if [ "${HEAD_REPO_FORK}" = "true" ]; then
|
||||
echo "is_fork=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
|
||||
# Depot only ever ran on trusted triggers, so gate it on both the
|
||||
# kill-switch and is_fork.
|
||||
if [ "${DEPOT_ENABLED}" = "true" ] && [ "${is_fork}" = "false" ]; then
|
||||
use_depot=true
|
||||
else
|
||||
use_depot=false
|
||||
fi
|
||||
case "${AUTHOR_ASSOC}" in
|
||||
OWNER|MEMBER|COLLABORATOR)
|
||||
echo "is_fork=false" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
*)
|
||||
echo "is_fork=true" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "is_fork=${is_fork}" >> "$GITHUB_OUTPUT"
|
||||
echo "use_depot=${use_depot}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
build:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-8' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-8' || 'ubuntu-latest' }}
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
strategy:
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
# (nor DEPOT_TOKEN), so the suite can't boot premium and would fail. See the
|
||||
# header comment. GitHub reports the skipped reusable workflow as success.
|
||||
if: needs.pick.outputs.is_fork != 'true'
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
PREMIUM_KEY: ${{ secrets.PREMIUM_KEY_ENTERPRISE }}
|
||||
|
||||
@@ -41,6 +41,7 @@ jobs:
|
||||
openapi: ${{ steps.changes.outputs.openapi }}
|
||||
frontend: ${{ steps.changes.outputs.frontend }}
|
||||
docker-base: ${{ steps.changes.outputs.docker-base }}
|
||||
dockerfiles: ${{ steps.changes.outputs.dockerfiles }}
|
||||
tauri: ${{ steps.changes.outputs.tauri }}
|
||||
engine: ${{ steps.changes.outputs.engine }}
|
||||
generated-models: ${{ steps.changes.outputs.generated-models }}
|
||||
@@ -153,6 +154,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
docker-base-changed: ${{ needs.files-changed.outputs.docker-base }}
|
||||
dockerfiles-changed: ${{ needs.files-changed.outputs.dockerfiles }}
|
||||
|
||||
tauri-build:
|
||||
if: needs.files-changed.outputs.tauri == 'true'
|
||||
@@ -162,6 +164,13 @@ jobs:
|
||||
pull-requests: write
|
||||
uses: ./.github/workflows/tauri-build.yml
|
||||
secrets: inherit
|
||||
# PR smoke build: Linux only (fastest + cheapest to compile), unsigned,
|
||||
# deb-only, no AppImage. The full signed multi-OS matrix runs on release;
|
||||
# nightly still warms the Rust cache with all-OS defaults.
|
||||
with:
|
||||
platform: linux
|
||||
sign: false
|
||||
minimal: true
|
||||
|
||||
ai-engine:
|
||||
if: needs.files-changed.outputs.engine == 'true'
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
check-generate-openapi-docs:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
aggregate:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
migration-test:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-8' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-8' || 'ubuntu-latest' }}
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
deploy-v2-on-push:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
concurrency:
|
||||
group: deploy-v2-push-V2
|
||||
cancel-in-progress: true
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
contents: read
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' }}
|
||||
USE_DEPOT: ${{ needs.pick.outputs.use_depot == 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
|
||||
steps:
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
docker-compose-tests:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '4') }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '4') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
playwright-e2e-live:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
playwright-e2e:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
files-changed:
|
||||
name: detect what files changed
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
licenses-frontend: ${{ steps.changes.outputs.licenses-frontend }}
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
if: needs.files-changed.outputs.licenses-frontend == 'true'
|
||||
name: Generate Frontend License Report
|
||||
needs: [pick, files-changed]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
@@ -320,7 +320,7 @@ jobs:
|
||||
if: needs.files-changed.outputs.licenses-backend == 'true'
|
||||
needs: [pick, files-changed]
|
||||
name: Generate Backend License Report
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
frontend-validation:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
determine-matrix:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
|
||||
build-jars:
|
||||
needs: [pick, determine-matrix]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
strategy:
|
||||
@@ -639,7 +639,7 @@ jobs:
|
||||
|
||||
collect-and-release:
|
||||
needs: [pick, determine-matrix, build, build-jars]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
playwright-all-browsers:
|
||||
name: Playwright (chromium + firefox + webkit)
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
push:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
|
||||
@@ -21,6 +21,11 @@ on:
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
minimal:
|
||||
description: "Fast smoke build: Linux deb only, skip rpm and the flaky AppImage pass. Used by PR builds."
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
@@ -38,6 +43,11 @@ on:
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
minimal:
|
||||
description: "Fast smoke build: Linux deb only, skip rpm and the flaky AppImage pass."
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -386,10 +396,10 @@ jobs:
|
||||
with:
|
||||
projectPath: ./frontend/editor
|
||||
tauriScript: npx tauri
|
||||
# Linux: build deb+rpm only here. AppImage runs in its own
|
||||
# continue-on-error step below so its persistent linuxdeploy
|
||||
# failure (#6127 onwards) does not tank deb/rpm uploads.
|
||||
args: ${{ matrix.platform == 'ubuntu-22.04' && '--bundles deb,rpm' || matrix.args }}
|
||||
# Linux: build deb+rpm only here (deb-only on minimal smoke builds).
|
||||
# AppImage runs in its own continue-on-error step below so its
|
||||
# persistent linuxdeploy failure (#6127 onwards) does not tank uploads.
|
||||
args: ${{ matrix.platform == 'ubuntu-22.04' && (inputs.minimal && '--bundles deb' || '--bundles deb,rpm') || matrix.args }}
|
||||
|
||||
- name: Build Tauri app (unsigned)
|
||||
if: ${{ !inputs.sign }}
|
||||
@@ -406,15 +416,16 @@ jobs:
|
||||
with:
|
||||
projectPath: ./frontend/editor
|
||||
tauriScript: npx tauri
|
||||
# Linux: build deb+rpm only here. AppImage runs in its own
|
||||
# continue-on-error step below so its persistent linuxdeploy
|
||||
# failure (#6127 onwards) does not tank deb/rpm uploads.
|
||||
args: ${{ matrix.platform == 'ubuntu-22.04' && '--bundles deb,rpm' || matrix.args }}
|
||||
# Linux: build deb+rpm only here (deb-only on minimal smoke builds).
|
||||
# AppImage runs in its own continue-on-error step below so its
|
||||
# persistent linuxdeploy failure (#6127 onwards) does not tank uploads.
|
||||
args: ${{ matrix.platform == 'ubuntu-22.04' && (inputs.minimal && '--bundles deb' || '--bundles deb,rpm') || matrix.args }}
|
||||
|
||||
# AppImage is decoupled so its linuxdeploy run gets a fresh process
|
||||
# (rpm scratch state torn down) and its failure can't tank deb/rpm.
|
||||
# Skipped on minimal smoke builds (flaky + slow, deb is enough to verify).
|
||||
- name: Build Tauri app (Linux AppImage)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
if: matrix.platform == 'ubuntu-22.04' && !inputs.minimal
|
||||
continue-on-error: true
|
||||
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
||||
env:
|
||||
|
||||
@@ -12,6 +12,11 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: "false"
|
||||
dockerfiles-changed:
|
||||
description: "Whether any Dockerfile changed (forwarded from files-changed). Gates the slow arm64 build leg."
|
||||
required: false
|
||||
type: string
|
||||
default: "false"
|
||||
depot_cores:
|
||||
description: "Depot runner vCPU count (used in runs-on). Override for benchmarking."
|
||||
required: false
|
||||
@@ -41,12 +46,12 @@ jobs:
|
||||
# `task backend:build:ci` produce equivalent JARs (verify before wiring).
|
||||
test-build-docker-images:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' && inputs.docker-base-changed != 'true' }}
|
||||
USE_DEPOT: ${{ needs.pick.outputs.use_depot == 'true' && inputs.docker-base-changed != 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -146,13 +151,22 @@ jobs:
|
||||
# GITHUB_EVENT_NAME is already provided by the runner.
|
||||
env:
|
||||
DOCKER_BASE_CHANGED: ${{ inputs.docker-base-changed }}
|
||||
DOCKERFILES_CHANGED: ${{ inputs.dockerfiles-changed }}
|
||||
run: |
|
||||
if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && [ "$DOCKER_BASE_CHANGED" = "true" ]; then
|
||||
# Base Dockerfile changed: build against the locally-built base,
|
||||
# which only exists for amd64.
|
||||
echo "base_image=stirling-pdf-base:pr-test" >> "$GITHUB_OUTPUT"
|
||||
echo "platforms=linux/amd64" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
elif [ "$DOCKERFILES_CHANGED" = "true" ]; then
|
||||
# A Dockerfile changed: also verify the arm64 build (slow QEMU leg).
|
||||
echo "base_image=stirlingtools/stirling-pdf-base:latest" >> "$GITHUB_OUTPUT"
|
||||
echo "platforms=linux/amd64,linux/arm64/v8" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# No Dockerfile change: amd64 only. arm64 is exercised on the base
|
||||
# image publish and on release, not on every code PR.
|
||||
echo "base_image=stirlingtools/stirling-pdf-base:latest" >> "$GITHUB_OUTPUT"
|
||||
echo "platforms=linux/amd64" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Base-changed PRs build the embedded image with the local docker driver
|
||||
@@ -214,12 +228,12 @@ jobs:
|
||||
|
||||
test-build-unoserver-image:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' && inputs.docker-base-changed != 'true' }}
|
||||
USE_DEPOT: ${{ needs.pick.outputs.use_depot == 'true' && inputs.docker-base-changed != 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
|
||||
@@ -26,12 +26,12 @@ jobs:
|
||||
deploy:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' }}
|
||||
USE_DEPOT: ${{ needs.pick.outputs.use_depot == 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
if: always()
|
||||
name: detect what files changed
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
frontend: ${{ steps.changes.outputs.frontend }}
|
||||
@@ -175,7 +175,7 @@ jobs:
|
||||
test:
|
||||
if: needs.files-changed.outputs.frontend == 'true'
|
||||
needs: [pick, deploy, files-changed]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
|
||||
cleanup:
|
||||
needs: [pick, deploy, test]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ${{ needs.pick.outputs.use_depot == 'true' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }}
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
|
||||
@@ -26,7 +26,6 @@ tasks:
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
|
||||
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}'
|
||||
POLICIES_ENABLED: '{{.POLICIES_ENABLED}}'
|
||||
|
||||
dev:proprietary:
|
||||
desc: "Start backend dev server in proprietary mode"
|
||||
@@ -41,13 +40,12 @@ tasks:
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED | default "false"}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS | default "120"}}'
|
||||
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN | default ""}}'
|
||||
POLICIES_ENABLED: '{{.POLICIES_ENABLED | default ""}}'
|
||||
env:
|
||||
SERVER_PORT: '{{.PORT}}'
|
||||
cmds:
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{if .POLICIES_ENABLED}}POLICIES_ENABLED={{.POLICIES_ENABLED}} {{end}}cmd /c ".\gradlew.bat :stirling-pdf:bootRun"'
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}cmd /c ".\gradlew.bat :stirling-pdf:bootRun"'
|
||||
platforms: [windows]
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{if .POLICIES_ENABLED}}POLICIES_ENABLED={{.POLICIES_ENABLED}} {{end}}./gradlew :stirling-pdf:bootRun'
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}./gradlew :stirling-pdf:bootRun'
|
||||
platforms: [linux, darwin]
|
||||
|
||||
dev:bundled:
|
||||
|
||||
+14
-3
@@ -203,6 +203,19 @@ tasks:
|
||||
cmds:
|
||||
- task: lint:eslint
|
||||
- task: lint:dpdm
|
||||
- task: lint:colors
|
||||
|
||||
lint:colors:
|
||||
desc: "Enforce theme tokens — colours in core/theme route through the palette"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- node editor/scripts/lint/theme-lint.mjs
|
||||
|
||||
contrast:
|
||||
desc: "Report low-contrast theme token pairs (warning only, never blocks)"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- node editor/scripts/lint/theme-lint.mjs contrast
|
||||
|
||||
lint:eslint:
|
||||
desc: "Run ESLint linting"
|
||||
@@ -249,10 +262,8 @@ tasks:
|
||||
|
||||
typecheck:_run:
|
||||
internal: true
|
||||
env:
|
||||
CI: '{{ .CI | default "false" }}'
|
||||
cmds:
|
||||
- '{{ if eq .CI "true" }}npx tsc{{ else }}npx tsgo{{ end }} --noEmit --project {{.PROJECT}}'
|
||||
- 'npx tsc --noEmit --project {{.PROJECT}}'
|
||||
|
||||
typecheck:core:
|
||||
desc: "Typecheck core build variant"
|
||||
|
||||
@@ -155,6 +155,8 @@ The project structure is defined in `engine/pyproject.toml`. Any new dependencie
|
||||
|
||||
For a broader explanation of the frontend layering and override architecture, read @frontend/editor/DeveloperGuide.md
|
||||
|
||||
Before touching colours or theming (tokens, dark mode, accent colours), read @frontend/editor/src/core/theme/README.md — it explains the palette/`--c-*` token system and the rule that literal colours live only in `primitives.css`.
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT - Use @app/* for all imports
|
||||
import { AppLayout } from "@app/components/AppLayout";
|
||||
|
||||
@@ -90,7 +90,6 @@ tasks:
|
||||
vars:
|
||||
PORT: '{{.BACKEND_PORT}}'
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
POLICIES_ENABLED: "true"
|
||||
- task: frontend:dev:proprietary
|
||||
vars:
|
||||
PORT: '{{.EDITOR_PORT}}'
|
||||
|
||||
@@ -206,11 +206,6 @@ public class ApplicationProperties {
|
||||
|
||||
@Data
|
||||
public static class Policies {
|
||||
/**
|
||||
* Master switch for the policy + sources subsystem (the PAYG-metered automation surface).
|
||||
*/
|
||||
private boolean enabled = false;
|
||||
|
||||
/**
|
||||
* Absolute directories that policy folder input sources and output sinks may read from or
|
||||
* write to. Empty (the default) disables folder access entirely, so a policy can never be
|
||||
|
||||
+8
-1
@@ -35,6 +35,7 @@ import stirling.software.proprietary.billing.ContentHasher;
|
||||
import stirling.software.proprietary.billing.DocumentUnitCalculator;
|
||||
import stirling.software.proprietary.billing.DocumentUnitCalculator.FileSize;
|
||||
import stirling.software.proprietary.billing.UnitCalcPolicy;
|
||||
import stirling.software.proprietary.policy.controller.PolicyRunRoutes;
|
||||
import stirling.software.proprietary.security.model.ApiKeyAuthenticationToken;
|
||||
|
||||
/**
|
||||
@@ -84,7 +85,13 @@ public class InstanceEntitlementInterceptor implements HandlerInterceptor {
|
||||
instanceof ApiKeyAuthenticationToken;
|
||||
BillingCategory category = BillableOperationClassifier.categorize(request, apiKey);
|
||||
request.setAttribute(ATTR_CATEGORY, category);
|
||||
decision = gate.evaluate(category != BillingCategory.BYPASSED);
|
||||
// A policy run kicks off billable automation, so block it up front when unentitled
|
||||
// rather than after its first tool. It carries no automation header itself (category
|
||||
// BYPASSED), so it's gated here but metered only via its dispatched sub-steps - keeping
|
||||
// the BYPASSED meter category avoids double-counting.
|
||||
boolean billable =
|
||||
category != BillingCategory.BYPASSED || PolicyRunRoutes.matches(request);
|
||||
decision = gate.evaluate(billable);
|
||||
} catch (RuntimeException e) {
|
||||
// Fail open: an inability to resolve entitlement (e.g. a DB or SaaS blip) must never
|
||||
// turn into a hard block on billable work.
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package stirling.software.proprietary.integration.crypto;
|
||||
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Converter;
|
||||
|
||||
/**
|
||||
* For columns that were once whole-blob encrypted but no longer hold secrets.
|
||||
*
|
||||
* <p>Writes plaintext, so the value is readable by any instance regardless of the per-installation
|
||||
* encryption key. Any value that isn't our ciphertext (already-plaintext JSON, or ciphertext from a
|
||||
* key we don't hold) is returned as-is; the latter is the caller's to reject.
|
||||
*/
|
||||
@Converter
|
||||
public class LegacyDecryptStringConverter implements AttributeConverter<String, String> {
|
||||
|
||||
@Override
|
||||
public String convertToDatabaseColumn(String attribute) {
|
||||
return attribute;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertToEntityAttribute(String dbData) {
|
||||
// Plaintext JSON can never be our Base64 ciphertext ('{' is not in the Base64 alphabet), so
|
||||
// skip the decrypt attempt for it.
|
||||
if (dbData == null || dbData.stripLeading().startsWith("{")) {
|
||||
return dbData;
|
||||
}
|
||||
try {
|
||||
return CredentialEncryption.decrypt(dbData);
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
// Legacy ciphertext we can't read (key we don't hold) - the caller's to reject.
|
||||
return dbData;
|
||||
}
|
||||
}
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
package stirling.software.proprietary.integration.crypto;
|
||||
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Converter;
|
||||
|
||||
/**
|
||||
* {@link EncryptedStringConverter} for columns that held plaintext before encryption shipped:
|
||||
* writes are always encrypted, but a stored value that is not valid ciphertext is returned as-is,
|
||||
* so pre-encryption rows keep loading and become encrypted on their next save. The discrimination
|
||||
* is exact for JSON payloads, which can never be mistaken for ciphertext ('{' is not in the Base64
|
||||
* alphabet). The trade-off is that a genuinely corrupted ciphertext surfaces as garbage to the
|
||||
* caller's parser instead of failing here.
|
||||
*/
|
||||
@Converter
|
||||
public class LenientEncryptedStringConverter implements AttributeConverter<String, String> {
|
||||
|
||||
@Override
|
||||
public String convertToDatabaseColumn(String attribute) {
|
||||
return CredentialEncryption.encrypt(attribute);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertToEntityAttribute(String dbData) {
|
||||
try {
|
||||
return CredentialEncryption.decrypt(dbData);
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
// Not ciphertext: legacy plaintext from before encryption shipped.
|
||||
return dbData;
|
||||
}
|
||||
}
|
||||
}
|
||||
-2
@@ -6,7 +6,6 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -30,7 +29,6 @@ import stirling.software.proprietary.policy.source.SourceStore;
|
||||
* defended: an operator who roots an allowlist on a symlink to a sensitive location is trusted.
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class FolderAccessGuard {
|
||||
|
||||
public static final String FOLDER_TYPE = "folder";
|
||||
|
||||
-2
@@ -3,7 +3,6 @@ package stirling.software.proprietary.policy.config;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -23,7 +22,6 @@ import stirling.software.proprietary.policy.store.PolicyStore;
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class PolicyAccessGuard {
|
||||
|
||||
private final UserServiceInterface userService;
|
||||
|
||||
-2
@@ -7,7 +7,6 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -85,7 +84,6 @@ import stirling.software.proprietary.util.SecretMasker;
|
||||
@Hidden
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "Policies", description = "Run tool pipelines on the backend")
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class PolicyController {
|
||||
|
||||
private final PolicyRunner policyRunner;
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package stirling.software.proprietary.policy.controller;
|
||||
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* Policy execute-route namespace under {@code /api/v1/policies} - the paths that actually run an
|
||||
* automation ({@code /run}, {@code /run/stream}, {@code /{id}/run}, {@code /{id}/trigger}).
|
||||
*
|
||||
* <p>Single source of truth for both PAYG entitlement gates, so a caller without billing is blocked
|
||||
* at the start of a run rather than partway through: the saas {@code EntitlementGuard} gates these
|
||||
* on {@code FeatureGate.AUTOMATION}, and the self-hosted account-link {@code
|
||||
* InstanceEntitlementInterceptor} treats them as billable. Read/list policy endpoints are
|
||||
* deliberately excluded so the UI can still show policies and prompt on use.
|
||||
*
|
||||
* <p>This is the sole gate between an unentitled caller and a billable run, so the match is exact
|
||||
* (not a loose suffix) and segment-anchored. {@code PolicyRunRoutesTest} asserts it against every
|
||||
* mapping on {@code PolicyController}, so a new execute route that isn't classified here fails the
|
||||
* build rather than silently running for free.
|
||||
*/
|
||||
public final class PolicyRunRoutes {
|
||||
|
||||
private static final String BASE = "/api/v1/policies";
|
||||
|
||||
private PolicyRunRoutes() {}
|
||||
|
||||
/**
|
||||
* True when the request resolved to a policy execute endpoint. Prefers the matched route
|
||||
* pattern (context-path independent, set by Spring MVC) and falls back to the raw request URI.
|
||||
*/
|
||||
public static boolean matches(HttpServletRequest request) {
|
||||
Object pattern = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
|
||||
String path = pattern instanceof String s ? s : request.getRequestURI();
|
||||
String rel = relativeToBase(path);
|
||||
return rel != null && isExecuteRoute(rel);
|
||||
}
|
||||
|
||||
/**
|
||||
* The path relative to {@code /api/v1/policies}, or null if the request isn't under that base.
|
||||
* Segment-anchored (the char after the base must be {@code /} or end-of-string) so a sibling
|
||||
* like {@code /api/v1/policies-x/...} never matches; tolerates a leading context path.
|
||||
*/
|
||||
private static String relativeToBase(String path) {
|
||||
if (path == null) {
|
||||
return null;
|
||||
}
|
||||
int base = path.indexOf(BASE);
|
||||
if (base < 0) {
|
||||
return null;
|
||||
}
|
||||
int end = base + BASE.length();
|
||||
if (end < path.length() && path.charAt(end) != '/') {
|
||||
return null;
|
||||
}
|
||||
return path.substring(end);
|
||||
}
|
||||
|
||||
/**
|
||||
* The execute routes only: {@code /run}, {@code /run/stream}, and the single-segment {@code
|
||||
* /{id}/run} / {@code /{id}/trigger} (template or concrete id). Read/list/CRUD routes - {@code
|
||||
* /run/{runId}}, {@code /runs}, {@code /overview}, {@code /triggers}, {@code /{id}}, {@code
|
||||
* /order}, {@code /{id}/processed-history}, the base list/create - are all excluded.
|
||||
*/
|
||||
private static boolean isExecuteRoute(String rel) {
|
||||
return rel.equals("/run")
|
||||
|| rel.equals("/run/stream")
|
||||
|| isSingleIdRoute(rel, "run")
|
||||
|| isSingleIdRoute(rel, "trigger");
|
||||
}
|
||||
|
||||
/**
|
||||
* True for exactly {@code /{oneSegment}/<verb>} (the id being a template or a concrete value).
|
||||
*/
|
||||
private static boolean isSingleIdRoute(String rel, String verb) {
|
||||
String suffix = "/" + verb;
|
||||
if (!rel.endsWith(suffix)) {
|
||||
return false;
|
||||
}
|
||||
String idSegment = rel.substring(0, rel.length() - suffix.length());
|
||||
return idSegment.length() > 1
|
||||
&& idSegment.charAt(0) == '/'
|
||||
&& idSegment.indexOf('/', 1) < 0;
|
||||
}
|
||||
}
|
||||
-2
@@ -9,7 +9,6 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -57,7 +56,6 @@ import stirling.software.proprietary.service.DownstreamEntitlementError;
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class PolicyEngine {
|
||||
|
||||
// Admission weight for one run. Weighted heavy: a run chains many tools and holds intermediate
|
||||
|
||||
-2
@@ -9,7 +9,6 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
@@ -29,7 +28,6 @@ import stirling.software.proprietary.policy.model.PolicyRun;
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class PolicyRunRegistry {
|
||||
|
||||
private final Map<String, PolicyRun> runs = new ConcurrentHashMap<>();
|
||||
|
||||
-2
@@ -5,7 +5,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -35,7 +34,6 @@ import stirling.software.proprietary.policy.source.SourceStore;
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class PolicyRunner {
|
||||
|
||||
private final PolicyEngine policyEngine;
|
||||
|
||||
-2
@@ -2,7 +2,6 @@ package stirling.software.proprietary.policy.engine;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -25,7 +24,6 @@ import stirling.software.proprietary.policy.trigger.PolicyTrigger;
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class PolicyValidator {
|
||||
|
||||
private final List<PolicyTrigger> triggers;
|
||||
|
||||
-2
@@ -14,7 +14,6 @@ import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -42,7 +41,6 @@ import stirling.software.proprietary.policy.model.PolicyInputs;
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class FolderInputSource implements InputSource {
|
||||
|
||||
private static final String TYPE = FolderAccessGuard.FOLDER_TYPE;
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.core.io.AbstractResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -49,7 +48,6 @@ import software.amazon.awssdk.services.s3.model.S3Object;
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class S3InputSource implements InputSource {
|
||||
|
||||
private static final String TYPE = "s3";
|
||||
|
||||
-2
@@ -7,7 +7,6 @@ import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
@@ -24,7 +23,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class JpaProcessedLedger implements ProcessedLedger {
|
||||
|
||||
private static final int STAMP_CHUNK = 500;
|
||||
|
||||
-2
@@ -15,7 +15,6 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.MediaTypeFactory;
|
||||
@@ -41,7 +40,6 @@ import stirling.software.proprietary.policy.model.OutputSpec;
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class FolderOutputSink implements PolicyOutputSink {
|
||||
|
||||
static final String TYPE = FolderAccessGuard.FOLDER_TYPE;
|
||||
|
||||
-2
@@ -5,7 +5,6 @@ import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.MediaTypeFactory;
|
||||
@@ -23,7 +22,6 @@ import stirling.software.proprietary.policy.model.OutputSpec;
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class InlineOutputSink implements PolicyOutputSink {
|
||||
|
||||
private static final String TYPE = "inline";
|
||||
|
||||
-2
@@ -13,7 +13,6 @@ import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.MediaTypeFactory;
|
||||
@@ -54,7 +53,6 @@ import software.amazon.awssdk.services.s3.model.S3Exception;
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class S3OutputSink implements PolicyOutputSink {
|
||||
|
||||
private static final String TYPE = "s3";
|
||||
|
||||
-2
@@ -5,7 +5,6 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -29,7 +28,6 @@ import stirling.software.proprietary.policy.store.PolicyStore;
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class PolicyOverviewService {
|
||||
|
||||
private final PolicyStore policyStore;
|
||||
|
||||
+2
-4
@@ -4,7 +4,6 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -32,8 +31,8 @@ import tools.jackson.databind.ObjectMapper;
|
||||
* One-time, idempotent extraction of legacy embedded S3 credentials into stored connections:
|
||||
* sources and policy outputs written before connections shipped carry bucket/credentials in their
|
||||
* own options; this rewrites each to reference a (deduplicated) S3 {@link IntegrationConfig} and
|
||||
* keeps only per-use options (prefix, mode). MUST be programmatic - the option JSON is encrypted at
|
||||
* the application layer, so no SQL migration can read it.
|
||||
* keeps only per-use options (prefix, mode). MUST be programmatic - it parses and rewrites the
|
||||
* option JSON (and decrypts any legacy ciphertext row on read), which no SQL migration can do.
|
||||
*
|
||||
* <p>Idempotent by construction: rewritten rows no longer embed credentials, so re-runs find
|
||||
* nothing to do. Connections are deduplicated against both this run's extractions and existing S3
|
||||
@@ -44,7 +43,6 @@ import tools.jackson.databind.ObjectMapper;
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class EmbeddedS3CredentialMigration {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
-2
@@ -4,7 +4,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -23,7 +22,6 @@ import stirling.software.proprietary.policy.store.PolicyStore;
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class PolicyS3ConnectionUsageCheck implements IntegrationConfigUsageCheck {
|
||||
|
||||
private final SourceStore sourceStore;
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
@@ -32,7 +31,6 @@ import software.amazon.awssdk.services.s3.S3Configuration;
|
||||
* users rather than the operator.
|
||||
*/
|
||||
@Service
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class S3ConnectionPool {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
-2
@@ -3,7 +3,6 @@ package stirling.software.proprietary.policy.s3;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
@@ -39,7 +38,6 @@ import tools.jackson.databind.ObjectMapper;
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class S3ConnectionResolver {
|
||||
|
||||
static final String CONNECTION_ID_OPTION = "connectionId";
|
||||
|
||||
-2
@@ -3,7 +3,6 @@ package stirling.software.proprietary.policy.s3;
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -21,7 +20,6 @@ import stirling.software.proprietary.integration.service.IntegrationConfigValida
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class S3IntegrationValidator implements IntegrationConfigValidator {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
-2
@@ -10,7 +10,6 @@ import java.util.function.IntSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -24,7 +23,6 @@ import org.springframework.stereotype.Service;
|
||||
* table stays bounded (~one row per source per active hour, for at most 30 days).
|
||||
*/
|
||||
@Service
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class JpaSourceDocCounter implements SourceDocCounter {
|
||||
|
||||
private final SourceDocCountRepository countRepository;
|
||||
|
||||
+23
-7
@@ -4,10 +4,10 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@@ -15,9 +15,9 @@ import tools.jackson.databind.ObjectMapper;
|
||||
* Durable {@link SourceStore} backed by JPA; the runtime store. Sources are persisted as JSON via
|
||||
* {@link SourceEntity}, with scalar columns kept in sync for querying.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class JpaSourceStore implements SourceStore {
|
||||
|
||||
private final SourceRepository repository;
|
||||
@@ -53,17 +53,20 @@ public class JpaSourceStore implements SourceStore {
|
||||
|
||||
@Override
|
||||
public Optional<Source> get(String id) {
|
||||
return repository.findById(id).map(this::toSource);
|
||||
return repository.findById(id).flatMap(this::toSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Source> all() {
|
||||
return repository.findAll().stream().map(this::toSource).toList();
|
||||
return repository.findAll().stream().map(this::toSource).flatMap(Optional::stream).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Source> findByTeam(Long teamId) {
|
||||
return repository.findByTeam(teamId).stream().map(this::toSource).toList();
|
||||
return repository.findByTeam(teamId).stream()
|
||||
.map(this::toSource)
|
||||
.flatMap(Optional::stream)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,7 +78,20 @@ public class JpaSourceStore implements SourceStore {
|
||||
return true;
|
||||
}
|
||||
|
||||
private Source toSource(SourceEntity entity) {
|
||||
return objectMapper.readValue(entity.getSourceJson(), Source.class);
|
||||
// Skip (don't fail) rows whose JSON can't be read - e.g. written by another app version/key.
|
||||
// One unreadable row must never abort a bulk read or crash startup.
|
||||
private Optional<Source> toSource(SourceEntity entity) {
|
||||
try {
|
||||
return Optional.of(objectMapper.readValue(entity.getSourceJson(), Source.class));
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Skipping unreadable policy source id={} name={}: stored JSON could not be"
|
||||
+ " parsed ({}). Likely written by a different app version or"
|
||||
+ " encryption key.",
|
||||
entity.getId(),
|
||||
entity.getName(),
|
||||
e.getMessage());
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -3,7 +3,6 @@ package stirling.software.proprietary.policy.source;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -20,7 +19,6 @@ import stirling.software.proprietary.policy.config.PolicyManagementAuthority;
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class SourceAccessGuard {
|
||||
|
||||
private final UserServiceInterface userService;
|
||||
|
||||
-2
@@ -3,7 +3,6 @@ package stirling.software.proprietary.policy.source;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -43,7 +42,6 @@ import stirling.software.proprietary.util.SecretMasker;
|
||||
@Hidden
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "Sources", description = "Reusable policy input connections")
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class SourceController {
|
||||
|
||||
private final SourceStore sourceStore;
|
||||
|
||||
+4
-4
@@ -12,7 +12,7 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import stirling.software.proprietary.integration.crypto.LenientEncryptedStringConverter;
|
||||
import stirling.software.proprietary.integration.crypto.LegacyDecryptStringConverter;
|
||||
|
||||
/**
|
||||
* JPA row for a {@link Source}. The whole source lives as JSON in {@code sourceJson} (authoritative
|
||||
@@ -48,9 +48,9 @@ public class SourceEntity implements Serializable {
|
||||
@Column(name = "enabled")
|
||||
private boolean enabled;
|
||||
|
||||
// Encrypted at rest: source options carry user-supplied credentials (e.g. an S3 secret
|
||||
// access key). Lenient so rows written before encryption shipped still load.
|
||||
@Convert(converter = LenientEncryptedStringConverter.class)
|
||||
// Plaintext at rest: the S3 credentials that used to live here now sit in a referenced
|
||||
// IntegrationConfig connection (still encrypted). Decrypts legacy ciphertext on read.
|
||||
@Convert(converter = LegacyDecryptStringConverter.class)
|
||||
@Column(name = "source_json", columnDefinition = "text")
|
||||
private String sourceJson;
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -24,7 +23,6 @@ import stirling.software.proprietary.util.SecretMasker;
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class SourceOverviewService {
|
||||
|
||||
private final SourceStore sourceStore;
|
||||
|
||||
+26
-7
@@ -5,11 +5,11 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.policy.model.Policy;
|
||||
|
||||
@@ -19,9 +19,9 @@ import tools.jackson.databind.ObjectMapper;
|
||||
* Durable {@link PolicyStore} backed by JPA; the runtime store. Policies are persisted as JSON via
|
||||
* {@link PolicyEntity}, with scalar columns kept in sync for querying.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class JpaPolicyStore implements PolicyStore {
|
||||
|
||||
private final PolicyRepository repository;
|
||||
@@ -98,23 +98,30 @@ public class JpaPolicyStore implements PolicyStore {
|
||||
|
||||
@Override
|
||||
public Optional<Policy> get(String id) {
|
||||
return repository.findById(id).map(this::toPolicy);
|
||||
return repository.findById(id).flatMap(this::toPolicy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Policy> all() {
|
||||
return repository.findAllOrdered().stream().map(this::toPolicy).toList();
|
||||
return repository.findAllOrdered().stream()
|
||||
.map(this::toPolicy)
|
||||
.flatMap(Optional::stream)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Policy> findByTeam(Long teamId) {
|
||||
return repository.findByTeam(teamId).stream().map(this::toPolicy).toList();
|
||||
return repository.findByTeam(teamId).stream()
|
||||
.map(this::toPolicy)
|
||||
.flatMap(Optional::stream)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Policy> findByTriggerType(String triggerType) {
|
||||
return repository.findByTriggerTypeAndEnabledTrue(triggerType).stream()
|
||||
.map(this::toPolicy)
|
||||
.flatMap(Optional::stream)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -127,7 +134,19 @@ public class JpaPolicyStore implements PolicyStore {
|
||||
return true;
|
||||
}
|
||||
|
||||
private Policy toPolicy(PolicyEntity entity) {
|
||||
return objectMapper.readValue(entity.getPolicyJson(), Policy.class);
|
||||
// Skip (don't fail) rows whose JSON can't be read - e.g. written by another app version/key.
|
||||
// One unreadable row must never abort a bulk read or crash startup.
|
||||
private Optional<Policy> toPolicy(PolicyEntity entity) {
|
||||
try {
|
||||
return Optional.of(objectMapper.readValue(entity.getPolicyJson(), Policy.class));
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Skipping unreadable policy id={} name={}: stored JSON could not be parsed"
|
||||
+ " ({}). Likely written by a different app version or encryption key.",
|
||||
entity.getId(),
|
||||
entity.getName(),
|
||||
e.getMessage());
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -12,7 +12,7 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import stirling.software.proprietary.integration.crypto.LenientEncryptedStringConverter;
|
||||
import stirling.software.proprietary.integration.crypto.LegacyDecryptStringConverter;
|
||||
|
||||
/**
|
||||
* JPA row for a {@link stirling.software.proprietary.policy.model.Policy}. The whole policy lives
|
||||
@@ -58,9 +58,9 @@ public class PolicyEntity implements Serializable {
|
||||
@Column(name = "sort_order")
|
||||
private Integer sortOrder;
|
||||
|
||||
// Encrypted at rest: output options carry user-supplied credentials (e.g. an S3 secret
|
||||
// access key). Lenient so rows written before encryption shipped still load.
|
||||
@Convert(converter = LenientEncryptedStringConverter.class)
|
||||
// Plaintext at rest: the S3 credentials that used to live here now sit in a referenced
|
||||
// IntegrationConfig connection (still encrypted). Decrypts legacy ciphertext on read.
|
||||
@Convert(converter = LegacyDecryptStringConverter.class)
|
||||
@Column(name = "policy_json", columnDefinition = "text")
|
||||
private String policyJson;
|
||||
}
|
||||
|
||||
-2
@@ -20,7 +20,6 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -50,7 +49,6 @@ import stirling.software.proprietary.policy.store.PolicyStore;
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class FolderWatchTrigger implements PolicyTrigger {
|
||||
|
||||
private static final String TYPE = "folder-watch";
|
||||
|
||||
-2
@@ -2,7 +2,6 @@ package stirling.software.proprietary.policy.trigger;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -13,7 +12,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class PolicyTriggerManager implements SmartLifecycle {
|
||||
|
||||
private final List<PolicyTrigger> triggers;
|
||||
|
||||
-2
@@ -10,7 +10,6 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -32,7 +31,6 @@ import tools.jackson.databind.ObjectMapper;
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBooleanProperty(name = "policies.enabled")
|
||||
public class ScheduleTrigger implements PolicyTrigger {
|
||||
|
||||
private static final String TYPE = "schedule";
|
||||
|
||||
+37
@@ -155,6 +155,43 @@ class InstanceEntitlementInterceptorTest {
|
||||
verifyNoInteractions(entitlementCache);
|
||||
}
|
||||
|
||||
@Test
|
||||
void gatesPolicyRunUpFrontEvenWithoutAutomationHeader() throws Exception {
|
||||
// The policy /run call carries no automation header, but must be blocked up front (not
|
||||
// after its first tool) when the instance is unlinked.
|
||||
when(gate.evaluate(anyBoolean()))
|
||||
.thenReturn(GateDecision.block(GateDecision.Reason.NOT_LINKED));
|
||||
|
||||
InstanceEntitlementInterceptor interceptor = interceptor();
|
||||
MockHttpServletRequest req =
|
||||
new MockHttpServletRequest("POST", "/api/v1/policies/pol-1/run");
|
||||
MockHttpServletResponse resp = new MockHttpServletResponse();
|
||||
|
||||
assertFalse(interceptor.preHandle(req, resp, new Object()));
|
||||
assertEquals(HttpStatus.PAYMENT_REQUIRED.value(), resp.getStatus());
|
||||
assertTrue(resp.getContentAsString().contains("ACCOUNT_LINK_REQUIRED"));
|
||||
verify(gate).evaluate(true); // gated as billable despite no automation header
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotMeterThePolicyRunEndpointItself() throws Exception {
|
||||
// Gated up front, but metered only via its dispatched tool sub-steps (category BYPASSED
|
||||
// here), so the /run request itself never accrues usage.
|
||||
when(gate.evaluate(anyBoolean()))
|
||||
.thenReturn(GateDecision.allow(GateDecision.Reason.ENTITLED));
|
||||
UsageMeterService meter = mock(UsageMeterService.class);
|
||||
when(meterProvider.getIfAvailable()).thenReturn(meter);
|
||||
|
||||
InstanceEntitlementInterceptor interceptor = interceptor();
|
||||
MockHttpServletRequest req =
|
||||
new MockHttpServletRequest("POST", "/api/v1/policies/pol-1/run");
|
||||
MockHttpServletResponse resp = new MockHttpServletResponse();
|
||||
interceptor.preHandle(req, resp, new Object());
|
||||
interceptor.afterCompletion(req, resp, new Object(), null);
|
||||
|
||||
verifyNoInteractions(meter);
|
||||
}
|
||||
|
||||
private static InstanceEntitlement entitled(UnitCalcPolicy policy, LocalDateTime period) {
|
||||
return new InstanceEntitlement(
|
||||
true, 0, 0, 100L, EntitlementState.OK, policy, period, period.plusMonths(1));
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package stirling.software.proprietary.integration.crypto;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class LegacyDecryptStringConverterTest {
|
||||
|
||||
private final LegacyDecryptStringConverter converter = new LegacyDecryptStringConverter();
|
||||
|
||||
@BeforeAll
|
||||
static void initKey() throws Exception {
|
||||
KeyGenerator generator = KeyGenerator.getInstance("AES");
|
||||
generator.init(256);
|
||||
SecretKey key = generator.generateKey();
|
||||
CredentialEncryption.initialiseForTesting(key);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writesPlaintext() {
|
||||
String json = "{\"bucket\":\"inbox\",\"mode\":\"consume\"}";
|
||||
|
||||
assertThat(converter.convertToDatabaseColumn(json)).isEqualTo(json);
|
||||
}
|
||||
|
||||
@Test
|
||||
void decryptsLegacyCiphertextOnRead() {
|
||||
String json = "{\"bucket\":\"inbox\"}";
|
||||
String legacyCiphertext = CredentialEncryption.encrypt(json);
|
||||
|
||||
assertThat(legacyCiphertext).isNotEqualTo(json);
|
||||
assertThat(converter.convertToEntityAttribute(legacyCiphertext)).isEqualTo(json);
|
||||
}
|
||||
|
||||
@Test
|
||||
void passesPlaintextThroughOnRead() {
|
||||
String json = "{\"bucket\":\"inbox\",\"mode\":\"consume\"}";
|
||||
|
||||
assertThat(converter.convertToEntityAttribute(json)).isEqualTo(json);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullsPassThrough() {
|
||||
assertThat(converter.convertToDatabaseColumn(null)).isNull();
|
||||
assertThat(converter.convertToEntityAttribute(null)).isNull();
|
||||
}
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
package stirling.software.proprietary.integration.crypto;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class LenientEncryptedStringConverterTest {
|
||||
|
||||
private final LenientEncryptedStringConverter converter = new LenientEncryptedStringConverter();
|
||||
|
||||
@BeforeAll
|
||||
static void initKey() throws Exception {
|
||||
KeyGenerator generator = KeyGenerator.getInstance("AES");
|
||||
generator.init(256);
|
||||
SecretKey key = generator.generateKey();
|
||||
CredentialEncryption.initialiseForTesting(key);
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundTripsThroughCiphertext() {
|
||||
String json = "{\"bucket\":\"inbox\",\"secretAccessKey\":\"shh\"}";
|
||||
|
||||
String stored = converter.convertToDatabaseColumn(json);
|
||||
|
||||
assertThat(stored).isNotEqualTo(json).doesNotContain("shh");
|
||||
assertThat(converter.convertToEntityAttribute(stored)).isEqualTo(json);
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyPlaintextRowsPassThroughOnRead() {
|
||||
String legacy = "{\"bucket\":\"inbox\",\"mode\":\"consume\"}";
|
||||
|
||||
assertThat(converter.convertToEntityAttribute(legacy)).isEqualTo(legacy);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullsPassThrough() {
|
||||
assertThat(converter.convertToDatabaseColumn(null)).isNull();
|
||||
assertThat(converter.convertToEntityAttribute(null)).isNull();
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package stirling.software.proprietary.policy.controller;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
|
||||
class PolicyRunRoutesTest {
|
||||
|
||||
private static boolean matchesUri(String uri) {
|
||||
MockHttpServletRequest req = new MockHttpServletRequest();
|
||||
req.setRequestURI(uri);
|
||||
return PolicyRunRoutes.matches(req);
|
||||
}
|
||||
|
||||
private static boolean matchesPattern(String pattern) {
|
||||
MockHttpServletRequest req = new MockHttpServletRequest();
|
||||
req.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, pattern);
|
||||
return PolicyRunRoutes.matches(req);
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesTheFourExecuteRoutes() {
|
||||
assertThat(matchesUri("/api/v1/policies/run")).isTrue();
|
||||
assertThat(matchesUri("/api/v1/policies/run/stream")).isTrue();
|
||||
assertThat(matchesUri("/api/v1/policies/pol-123/run")).isTrue();
|
||||
assertThat(matchesUri("/api/v1/policies/pol-123/trigger")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void excludesReadListAndCrudRoutes() {
|
||||
assertThat(matchesUri("/api/v1/policies")).isFalse(); // list + create
|
||||
assertThat(matchesUri("/api/v1/policies/runs")).isFalse();
|
||||
assertThat(matchesUri("/api/v1/policies/run/abc-run-id")).isFalse(); // GET /run/{runId}
|
||||
assertThat(matchesUri("/api/v1/policies/overview")).isFalse();
|
||||
assertThat(matchesUri("/api/v1/policies/triggers")).isFalse(); // NB: not "/trigger"
|
||||
assertThat(matchesUri("/api/v1/policies/order")).isFalse();
|
||||
assertThat(matchesUri("/api/v1/policies/pol-123")).isFalse();
|
||||
assertThat(matchesUri("/api/v1/policies/pol-123/processed-history")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isSegmentAnchoredAndContextPathTolerant() {
|
||||
assertThat(matchesUri("/stirling/api/v1/policies/pol-123/run")).isTrue();
|
||||
assertThat(matchesUri("/api/v1/policies-x/pol-123/run"))
|
||||
.isFalse(); // sibling, not the segment
|
||||
assertThat(matchesUri("/api/v1/sources/pol/run")).isFalse();
|
||||
assertThat(matchesUri("/api/v1/misc/compress-pdf")).isFalse();
|
||||
}
|
||||
|
||||
/** Every request mapping on PolicyController, and whether it executes an automation. */
|
||||
private static final Map<String, Boolean> EXPECTED =
|
||||
Map.of(
|
||||
"/api/v1/policies", false, // base: list (GET) + create (POST)
|
||||
"/api/v1/policies/run", true,
|
||||
"/api/v1/policies/run/stream", true,
|
||||
"/api/v1/policies/run/{runId}", false,
|
||||
"/api/v1/policies/runs", false,
|
||||
"/api/v1/policies/order", false,
|
||||
"/api/v1/policies/overview", false,
|
||||
"/api/v1/policies/triggers", false,
|
||||
"/api/v1/policies/{policyId}", false, // GET + DELETE
|
||||
"/api/v1/policies/{policyId}/processed-history", false);
|
||||
|
||||
// Split out because Map.of caps at 10 entries; the execute {id} routes live here.
|
||||
private static final Map<String, Boolean> EXPECTED_ID_EXECUTES =
|
||||
Map.of(
|
||||
"/api/v1/policies/{policyId}/run", true,
|
||||
"/api/v1/policies/{policyId}/trigger", true);
|
||||
|
||||
/**
|
||||
* Fail-safe: this matcher is the sole billing gate, so an unmatched execute route would run
|
||||
* automations for free. Reconstruct every mapping on PolicyController and assert its
|
||||
* classification is declared above - a new/renamed route lands as "unclassified" and fails the
|
||||
* build until someone decides whether it executes an automation.
|
||||
*/
|
||||
@Test
|
||||
void everyControllerMappingIsClassified() {
|
||||
String base = classMapping();
|
||||
Arrays.stream(PolicyController.class.getDeclaredMethods())
|
||||
.filter(m -> AnnotatedElementUtils.hasAnnotation(m, RequestMapping.class))
|
||||
.forEach(
|
||||
m -> {
|
||||
String pattern = base + methodMapping(m);
|
||||
Boolean expected = expectedFor(pattern);
|
||||
assertThat(expected)
|
||||
.as(
|
||||
"unclassified PolicyController route %s - add it to"
|
||||
+ " PolicyRunRoutesTest.EXPECTED",
|
||||
pattern)
|
||||
.isNotNull();
|
||||
assertThat(matchesPattern(pattern))
|
||||
.as("PolicyRunRoutes classification of %s", pattern)
|
||||
.isEqualTo(expected);
|
||||
});
|
||||
}
|
||||
|
||||
private static Boolean expectedFor(String pattern) {
|
||||
if (EXPECTED.containsKey(pattern)) {
|
||||
return EXPECTED.get(pattern);
|
||||
}
|
||||
return EXPECTED_ID_EXECUTES.get(pattern);
|
||||
}
|
||||
|
||||
private static String classMapping() {
|
||||
RequestMapping rm =
|
||||
AnnotatedElementUtils.getMergedAnnotation(
|
||||
PolicyController.class, RequestMapping.class);
|
||||
return rm == null ? "" : firstOrEmpty(rm);
|
||||
}
|
||||
|
||||
private static String methodMapping(java.lang.reflect.Method m) {
|
||||
RequestMapping rm = AnnotatedElementUtils.getMergedAnnotation(m, RequestMapping.class);
|
||||
return rm == null ? "" : firstOrEmpty(rm);
|
||||
}
|
||||
|
||||
private static String firstOrEmpty(RequestMapping rm) {
|
||||
String[] paths = rm.path().length > 0 ? rm.path() : rm.value();
|
||||
return paths.length > 0 ? paths[0] : "";
|
||||
}
|
||||
}
|
||||
+15
-5
@@ -31,6 +31,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.proprietary.policy.controller.PolicyRunRoutes;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
import stirling.software.proprietary.security.model.ApiKeyAuthenticationToken;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
@@ -46,9 +47,11 @@ import stirling.software.saas.util.AuthenticationUtils;
|
||||
*
|
||||
* <p>Scope: routes whose handler method (or bean type) carries either {@link AutoJobPostMapping}
|
||||
* (multipart tool POSTs) or {@link RequiresFeature} (AI controllers, future non-multipart gated
|
||||
* routes). Admin / info / config endpoints are excluded by the path-pattern in {@code
|
||||
* PaygWebMvcConfig} and are additionally skipped here when they carry neither annotation, so non-
|
||||
* billable infra never trips the guard.
|
||||
* routes), plus two proprietary route families recognised by path since they can't carry the
|
||||
* annotation: AI document tools ({@link AiToolRoutes} gated on AI_SUPPORT) and policy execute
|
||||
* endpoints ({@link PolicyRunRoutes} gated on AUTOMATION). Admin / info / config endpoints are
|
||||
* excluded by the path-pattern in {@code PaygWebMvcConfig} and are additionally skipped here when
|
||||
* they carry no annotation and match no such family, so non-billable infra never trips the guard.
|
||||
*
|
||||
* <p>Decision matrix:
|
||||
*
|
||||
@@ -137,13 +140,20 @@ public class EntitlementGuard implements HandlerInterceptor {
|
||||
// @RequiresFeature; recognise them by path so they're gated on AI_SUPPORT — see
|
||||
// AiToolRoutes and PaygChargeInterceptor, which classify the same routes as AI.
|
||||
boolean aiToolRoute = AiToolRoutes.matches(request);
|
||||
if (!hasAutoJobPostMapping && !hasRequiresFeature && !aiToolRoute) {
|
||||
// Policy execute routes (/api/v1/policies/**/run etc.) are proprietary and can't carry
|
||||
// @RequiresFeature; recognise them by path and gate on AUTOMATION (mirrors aiToolRoute).
|
||||
boolean policyRunRoute = PolicyRunRoutes.matches(request);
|
||||
if (!hasAutoJobPostMapping && !hasRequiresFeature && !aiToolRoute && !policyRunRoute) {
|
||||
skippedNoAnnotationCounter.increment();
|
||||
return true;
|
||||
}
|
||||
|
||||
FeatureGate[] required =
|
||||
aiToolRoute ? new FeatureGate[] {FeatureGate.AI_SUPPORT} : resolveRequiredGates(hm);
|
||||
aiToolRoute
|
||||
? new FeatureGate[] {FeatureGate.AI_SUPPORT}
|
||||
: policyRunRoute
|
||||
? new FeatureGate[] {FeatureGate.AUTOMATION}
|
||||
: resolveRequiredGates(hm);
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
boolean anonymous = isAnonymous(auth);
|
||||
|
||||
@@ -74,10 +74,6 @@ supabase.url=https://${app.supabase.project-ref}.supabase.co
|
||||
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://${app.supabase.project-ref}.supabase.co/auth/v1/.well-known/jwks.json
|
||||
spring.security.oauth2.resourceserver.jwt.audiences=${app.supabase.expected-aud}
|
||||
|
||||
# ---------- Policies ----------
|
||||
# Exposes the /api/v1/policies and /api/v1/sources controllers, engine, stores, and triggers.
|
||||
policies.enabled=true
|
||||
|
||||
# ---------- Multi-tenant scoping ----------
|
||||
# Restrict the signing user picker to the caller's team; SaaS must be 'team'
|
||||
# or unrelated tenants leak emails to each other.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- Policy engine schema (gated by policies.enabled): persisted policies and the reusable input
|
||||
-- Policy engine schema: persisted policies and the reusable input
|
||||
-- connections ("sources") they reference by id. The whole policy/source lives as JSON in the
|
||||
-- *_json column (authoritative on read); the scalar columns are denormalized copies for querying,
|
||||
-- notably team_id so a caller's team can be loaded without scanning every team's rows. owner and
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
-- instead of scanning a source's whole bucket history - and so the
|
||||
-- hourly buckets can be pruned without losing it.
|
||||
--
|
||||
-- Gated by policies.enabled like the rest of the subsystem; Hibernate ddl-auto would also create
|
||||
-- these, but the migration keeps the schema explicit for the Flyway-managed deployments.
|
||||
-- Hibernate ddl-auto would also create these, but the migration keeps the schema explicit for the
|
||||
-- Flyway-managed deployments.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS policy_source_doc_counts (
|
||||
source_id VARCHAR(255) NOT NULL,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- Classification labels (gated by policies.enabled): the flat multi-label vocabulary the document
|
||||
-- Classification labels: the flat multi-label vocabulary the document
|
||||
-- classifier runs against. One admin-editable row per team. The whole label set lives as JSON in
|
||||
-- labels_json (authoritative on read). team_id is a natural key and a plain value (not a foreign
|
||||
-- key) to stay decoupled from the security entities, so classification can be enabled or disabled
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
-- the policy's sources, so the table stays near the set of files
|
||||
-- currently present.
|
||||
--
|
||||
-- Gated by policies.enabled like the rest of the subsystem; Hibernate ddl-auto would also create
|
||||
-- this, but the migration keeps the schema explicit for the Flyway-managed deployments.
|
||||
-- Hibernate ddl-auto would also create this, but the migration keeps the schema explicit for the
|
||||
-- Flyway-managed deployments.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS policy_processed_files (
|
||||
policy_id VARCHAR(255) NOT NULL,
|
||||
|
||||
+91
@@ -198,6 +198,97 @@ class EntitlementGuardTest {
|
||||
assertThat(body.get("category").asText()).isEqualTo("AI");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// Policy execute routes (proprietary; recognised by path, gated on AUTOMATION)
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void policyRunRoute_noAnnotation_isInScopeAndGatedOnAutomation() throws Exception {
|
||||
UUID supabaseId = UUID.randomUUID();
|
||||
SecurityContextHolder.getContext().setAuthentication(jwtAuth(supabaseId));
|
||||
when(userRepository.findBySupabaseId(supabaseId))
|
||||
.thenReturn(Optional.of(userWithTeam(7L, 42L)));
|
||||
when(entitlementService.getSnapshot(42L)).thenReturn(degradedSnapshot());
|
||||
|
||||
HandlerMethod hm = handlerFor("plainEndpoint"); // no annotations
|
||||
MockHttpServletRequest req = new MockHttpServletRequest();
|
||||
req.setRequestURI("/api/v1/policies/pol-123/run");
|
||||
MockHttpServletResponse res = new MockHttpServletResponse();
|
||||
|
||||
boolean proceed = guard.preHandle(req, res, hm);
|
||||
|
||||
assertThat(proceed).isFalse();
|
||||
assertThat(res.getStatus()).isEqualTo(402);
|
||||
JsonNode body = json.readTree(res.getContentAsByteArray());
|
||||
assertThat(body.get("error").asText()).isEqualTo("FEATURE_DEGRADED");
|
||||
assertThat(body.get("missingGates").get(0).asText()).isEqualTo("AUTOMATION");
|
||||
verify(entitlementService).getSnapshot(42L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void policyRunRoute_anonymous_returns401WithAutomationCategory() throws Exception {
|
||||
SecurityContextHolder.getContext()
|
||||
.setAuthentication(
|
||||
new AnonymousAuthenticationToken(
|
||||
"key",
|
||||
"anonymousUser",
|
||||
List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS"))));
|
||||
|
||||
HandlerMethod hm = handlerFor("plainEndpoint");
|
||||
MockHttpServletRequest req = new MockHttpServletRequest();
|
||||
req.setRequestURI("/api/v1/policies/pol-123/trigger");
|
||||
MockHttpServletResponse res = new MockHttpServletResponse();
|
||||
|
||||
boolean proceed = guard.preHandle(req, res, hm);
|
||||
|
||||
assertThat(proceed).isFalse();
|
||||
assertThat(res.getStatus()).isEqualTo(401);
|
||||
JsonNode body = json.readTree(res.getContentAsByteArray());
|
||||
assertThat(body.get("error").asText()).isEqualTo("SIGNUP_REQUIRED");
|
||||
assertThat(body.get("category").asText()).isEqualTo("AUTOMATION");
|
||||
}
|
||||
|
||||
@Test
|
||||
void policyRunRoute_authenticatedFull_passesThrough() throws Exception {
|
||||
UUID supabaseId = UUID.randomUUID();
|
||||
SecurityContextHolder.getContext().setAuthentication(jwtAuth(supabaseId));
|
||||
when(userRepository.findBySupabaseId(supabaseId))
|
||||
.thenReturn(Optional.of(userWithTeam(7L, 42L)));
|
||||
when(entitlementService.getSnapshot(42L)).thenReturn(fullSnapshot());
|
||||
|
||||
HandlerMethod hm = handlerFor("plainEndpoint");
|
||||
MockHttpServletRequest req = new MockHttpServletRequest();
|
||||
req.setRequestURI("/api/v1/policies/run");
|
||||
MockHttpServletResponse res = new MockHttpServletResponse();
|
||||
|
||||
boolean proceed = guard.preHandle(req, res, hm);
|
||||
|
||||
assertThat(proceed).isTrue();
|
||||
assertThat(res.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void policyReadRoute_notGated_passesThroughEvenDegraded() throws Exception {
|
||||
// Listing policies must stay ungated so the UI can show them and prompt on use.
|
||||
SecurityContextHolder.getContext()
|
||||
.setAuthentication(
|
||||
new AnonymousAuthenticationToken(
|
||||
"key",
|
||||
"anonymousUser",
|
||||
List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS"))));
|
||||
|
||||
HandlerMethod hm = handlerFor("plainEndpoint");
|
||||
MockHttpServletRequest req = new MockHttpServletRequest();
|
||||
req.setRequestURI("/api/v1/policies");
|
||||
MockHttpServletResponse res = new MockHttpServletResponse();
|
||||
|
||||
boolean proceed = guard.preHandle(req, res, hm);
|
||||
|
||||
assertThat(proceed).isTrue();
|
||||
assertThat(res.getStatus()).isEqualTo(200);
|
||||
Mockito.verifyNoInteractions(entitlementService);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// Anonymous user
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
@@ -8,8 +8,9 @@ dependencies = [
|
||||
"pgvector>=0.3.6",
|
||||
"psycopg[binary,pool]>=3.2",
|
||||
"pydantic>=2.0.0",
|
||||
"pydantic-ai>=1.67.0",
|
||||
"pydantic-ai-slim[voyageai]>=1.67.0",
|
||||
# <2 cap: 1.99.0 patches CVE-2026-46678; 2.0 is an untested major migration
|
||||
"pydantic-ai>=1.99.0,<2.0.0",
|
||||
"pydantic-ai-slim[voyageai]>=1.99.0,<2.0.0",
|
||||
"pydantic-settings>=2.0.0",
|
||||
"python-dotenv>=1.2.1",
|
||||
"sqlite-vec>=0.1.6",
|
||||
|
||||
Generated
+469
-332
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,10 @@ const config: StorybookConfig = {
|
||||
// modules (e.g. the auth supabase client that moved into proprietary).
|
||||
"@proprietary": resolve(__dirname, "../editor/src/proprietary"),
|
||||
"@core": resolve(__dirname, "../editor/src/core"),
|
||||
// Public assets (e.g. the en-US translation TOML loaded ?raw by preview.tsx).
|
||||
// No src alias covers public/, so this lets the config use an alias rather
|
||||
// than a relative path.
|
||||
"@public": resolve(__dirname, "../editor/public"),
|
||||
};
|
||||
// Editor stories import via @app/* (proprietary→core fallback), @core/* and
|
||||
// @proprietary/*. Resolve them exactly the way the editor's own build does —
|
||||
|
||||
@@ -24,11 +24,11 @@ import { initReactI18next } from "react-i18next";
|
||||
import { parse as parseToml } from "smol-toml";
|
||||
// Load the real English copy so stories render human text, not raw keys. Bundled
|
||||
// synchronously via ?raw so it's present on the very first render (no async flash).
|
||||
// eslint-disable-next-line no-restricted-imports -- Storybook-only: read the public i18n asset; no @-alias covers editor/public/.
|
||||
import enTranslationToml from "../editor/public/locales/en-US/translation.toml?raw";
|
||||
import enTranslationToml from "@public/locales/en-US/translation.toml?raw";
|
||||
|
||||
import "@mantine/core/styles.css";
|
||||
import "@core/tokens/tokens.css";
|
||||
import "@core/theme/index.css";
|
||||
import "@core/tokens/base.css";
|
||||
|
||||
// Storybook-only: init react-i18next with the real English resources parsed from
|
||||
@@ -124,6 +124,20 @@ function ThemeBridge({
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the theme attributes colors.css needs — always `data-app-theme="custom"`
|
||||
* with the fixed default accent (data-accent="default"), matching the editor.
|
||||
*/
|
||||
function SchemeSetup({ scheme }: { scheme: "light" | "dark" }) {
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.setAttribute("data-app-theme", "custom");
|
||||
root.setAttribute("data-accent", "default");
|
||||
root.setAttribute("data-mantine-color-scheme", scheme);
|
||||
}, [scheme]);
|
||||
return null;
|
||||
}
|
||||
|
||||
const withProviders: Decorator = (Story, context) => {
|
||||
const tier = (context.globals.tier as Tier) ?? "pro";
|
||||
const linkState =
|
||||
@@ -138,6 +152,7 @@ const withProviders: Decorator = (Story, context) => {
|
||||
return (
|
||||
<MemoryRouter initialEntries={["/"]}>
|
||||
<ThemeProvider>
|
||||
<SchemeSetup scheme={colorScheme} />
|
||||
<ThemeBridge theme={colorScheme}>
|
||||
<SuiProvider colorScheme={colorScheme}>
|
||||
{/* LinkProvider must wrap TierProvider: TierContext derives its tier
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
],
|
||||
"@core/*": ["../editor/src/core/*"],
|
||||
"@proprietary/*": ["../editor/src/proprietary/*"],
|
||||
"@portal/*": ["../editor/src/portal/*"]
|
||||
"@portal/*": ["../editor/src/portal/*"],
|
||||
"@public/*": ["../editor/public/*"]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
|
||||
@@ -14,6 +14,34 @@
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
|
||||
<title>Stirling PDF</title>
|
||||
|
||||
<!-- Apply theme before first paint (no FOUC); mirrors ThemeProvider, which then refines it. -->
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var p = JSON.parse(
|
||||
localStorage.getItem("stirlingpdf_preferences") || "{}",
|
||||
);
|
||||
var mode = p.theme || "system";
|
||||
var scheme =
|
||||
mode === "light"
|
||||
? "light"
|
||||
: mode === "dark"
|
||||
? "dark"
|
||||
: window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
var r = document.documentElement;
|
||||
r.setAttribute("data-theme", scheme);
|
||||
r.setAttribute("data-app-theme", "custom");
|
||||
r.setAttribute("data-accent", "default");
|
||||
r.setAttribute("data-mantine-color-scheme", scheme);
|
||||
} catch (e) {
|
||||
/* first visit / unavailable storage: ThemeProvider handles it */
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "عالِج {{allowance}} ملفات PDF مجانًا"
|
||||
titleWithRate_one = "عالِج {{allowance}} ملف PDF مجانًا، ثم {{rate}}/PDF"
|
||||
titleWithRate_other = "عالِج {{allowance}} ملفات PDF مجانًا، ثم {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "مقفل"
|
||||
openAriaLabel = "فتح مكون {{name}}"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "إضافة إلى المشروع"
|
||||
ariaLabel = "تفاصيل المكون"
|
||||
tabsAriaLabel = "أقسام تفاصيل المكون"
|
||||
upgradeToUnlock = "الترقية لإلغاء القفل"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "تثبيت"
|
||||
usage = "الاستخدام"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} مضمن بدءًا من خطة {{tier}}. قم بالترقية لتضمينه."
|
||||
title = "غير متاح في خطتك"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "معاينة مباشرة"
|
||||
note = "يتم عرض صندوق الرمل التفاعلي هنا"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "مقاس لكل {{unit}}. تتم فوترة الاستخدام بعد الحصة الشهرية المجانية على حسابك ويُفصّل ضمن الاستخدام والفوترة."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "تتم الفوترة على"
|
||||
embeds30d = "التضمينات (30 يومًا)"
|
||||
freeQuota = "الحصة المجانية"
|
||||
freeQuotaValue = "{{amount}} / شهر"
|
||||
maturity = "النضج"
|
||||
none = "لا شيء"
|
||||
perAction = "لكل إجراء"
|
||||
price = "السعر"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "نظرة عامة"
|
||||
pricing = "التسعير"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "اختياري"
|
||||
required = "مطلوب"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "الوصف"
|
||||
name = "Prop"
|
||||
required = "مطلوب"
|
||||
type = "النوع"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "مكونات GA"
|
||||
componentSpendMtd = "إنفاق المكونات (MTD)"
|
||||
embedsThisMonth = "التضمينات هذا الشهر"
|
||||
inBeta = "في Beta"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "موافقة"
|
||||
check = "فحص"
|
||||
@@ -6591,18 +6534,6 @@ signature = "توقيع"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "ودجات SDK قابلة للتضمين تضعها في تطبيقك — عارض، وتدفق توقيع إلكتروني، ولوحة مراجعة AI. كل منها مقاس لكل إجراء. انقر على بطاقة للتثبيت والاستخدام وprops."
|
||||
title = "المكونات"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "تعذر تحميل كتالوج المكونات. حاول مرة أخرى قريبًا."
|
||||
title = "لا توجد مكونات متاحة"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "مكونات GA متاحة على Pay-as-you-go؛ وبعض مكونات Beta مخصصة للمؤسسات فقط. تعرض البطاقات المقفلة تلميحًا للترقية."
|
||||
title = "تحتاج بعض المكونات إلى خطة مدفوعة"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "كل طلب"
|
||||
eyebrow = "البدء"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "{{allowance}} PDF-i pulsuz emal edin"
|
||||
titleWithRate_one = "{{allowance}} PDF-i pulsuz emal edin, sonra {{rate}}/PDF"
|
||||
titleWithRate_other = "{{allowance}} PDF-i pulsuz emal edin, sonra {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Kilidli"
|
||||
openAriaLabel = "{{name}} komponentini aç"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Layihəyə əlavə et"
|
||||
ariaLabel = "Komponent təfərrüatı"
|
||||
tabsAriaLabel = "Komponent təfərrüatı bölmələri"
|
||||
upgradeToUnlock = "Kilidi açmaq üçün yüksəldin"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Quraşdır"
|
||||
usage = "İstifadə"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} {{tier}} planından başlayaraq daxildir. Onu daxil etmək üçün yüksəldin."
|
||||
title = "Planınızda mövcud deyil"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Canlı önizləmə"
|
||||
note = "İnteraktiv sandbox burada render olunur"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "{{unit}} başına ölçülür. Aylıq pulsuz kvotadan artıq istifadə hesabınıza yazılır və Usage & Billing altında maddələnir."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Hesablanma üzrə"
|
||||
embeds30d = "Daxiletmələr (30g)"
|
||||
freeQuota = "Pulsuz kvota"
|
||||
freeQuotaValue = "{{amount}} / ay"
|
||||
maturity = "Yetkinlik"
|
||||
none = "Yoxdur"
|
||||
perAction = "Hər hərəkət üçün"
|
||||
price = "Qiymət"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Kod"
|
||||
overview = "İcmal"
|
||||
pricing = "Qiymətləndirmə"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "istəyə bağlı"
|
||||
required = "tələb olunur"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Təsvir"
|
||||
name = "Prop"
|
||||
required = "Tələb olunur"
|
||||
type = "Tip"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Components GA"
|
||||
componentSpendMtd = "Komponent xərci (MTD)"
|
||||
embedsThisMonth = "Bu ay daxiletmələr"
|
||||
inBeta = "Beta-da"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "təsdiq"
|
||||
check = "yoxlama"
|
||||
@@ -6591,18 +6534,6 @@ signature = "imza"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Öz tətbiqinizə yerləşdirə biləcəyiniz daxil edilə bilən SDK vidcetləri — görüntüləyici, e-imza axını, AI baxış paneli. Hər biri hərəkət üzrə ölçülür. Quraşdırma, istifadə və props üçün karta klikləyin."
|
||||
title = "Komponentlər"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "Komponent kataloqu yüklənə bilmədi. Bir az sonra yenidən cəhd edin."
|
||||
title = "Mövcud komponent yoxdur"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "GA komponentləri Pay-as-you-go planında mövcuddur; bəzi Beta komponentləri yalnız enterprise üçündür. Kilidli kartlar yüksəltmə təklifi göstərir."
|
||||
title = "Bəzi komponentlər üçün ödənişli plan lazımdır"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "hər sorğu"
|
||||
eyebrow = "BAŞLANĞIC"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "Обработете {{allowance}} PDF безплатно"
|
||||
titleWithRate_one = "Обработете {{allowance}} PDF безплатно, после {{rate}}/PDF"
|
||||
titleWithRate_other = "Обработете {{allowance}} PDF безплатно, после {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Заключено"
|
||||
openAriaLabel = "Отвори компонент {{name}}"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Добави към проект"
|
||||
ariaLabel = "Детайли за компонента"
|
||||
tabsAriaLabel = "Секции с детайли за компонента"
|
||||
upgradeToUnlock = "Надстройте, за да отключите"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Инсталиране"
|
||||
usage = "Употреба"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} е включен от план {{tier}}. Надстройте, за да го вградите."
|
||||
title = "Не е налично във вашия план"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Live preview"
|
||||
note = "Интерактивният sandbox се рендира тук"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "Измерва се на {{unit}}. Използването над месечната безплатна квота се таксува към вашия акаунт и се детайлизира в Usage & Billing."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Таксува се по"
|
||||
embeds30d = "Вграждания (30 дни)"
|
||||
freeQuota = "Безплатна квота"
|
||||
freeQuotaValue = "{{amount}} / мес"
|
||||
maturity = "Зрялост"
|
||||
none = "Няма"
|
||||
perAction = "На действие"
|
||||
price = "Цена"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "Общ преглед"
|
||||
pricing = "Ценообразуване"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "по избор"
|
||||
required = "задължително"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Описание"
|
||||
name = "Prop"
|
||||
required = "Задължително"
|
||||
type = "Тип"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Компоненти GA"
|
||||
componentSpendMtd = "Разход за компоненти (MTD)"
|
||||
embedsThisMonth = "Вграждания този месец"
|
||||
inBeta = "В beta"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "одобрение"
|
||||
check = "проверка"
|
||||
@@ -6591,18 +6534,6 @@ signature = "подпис"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Вградими SDK widgets, които поставяте в собственото си приложение — viewer, e-sign поток, AI панел за преглед. Всеки се измерва на действие. Щракнете върху карта за инсталиране, употреба и props."
|
||||
title = "Компоненти"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "Каталогът с компоненти не можа да бъде зареден. Опитайте отново след малко."
|
||||
title = "Няма налични компоненти"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "GA компонентите са налични при Pay-as-you-go; няколко Beta компонента са само за enterprise. Заключените карти показват подкана за надстройване."
|
||||
title = "Някои компоненти изискват платен план"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "всяка заявка"
|
||||
eyebrow = "GETTING STARTED"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "Processa {{allowance}} PDF de franc"
|
||||
titleWithRate_one = "Processa {{allowance}} PDF de franc, després {{rate}}/PDF"
|
||||
titleWithRate_other = "Processa {{allowance}} PDF de franc, després {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Bloquejat"
|
||||
openAriaLabel = "Obre el component {{name}}"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Afegeix al projecte"
|
||||
ariaLabel = "Detall del component"
|
||||
tabsAriaLabel = "Seccions de detall del component"
|
||||
upgradeToUnlock = "Actualitza per desbloquejar"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Instal·la"
|
||||
usage = "Ús"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} s'inclou a partir del pla {{tier}}. Actualitza per incrustar-lo."
|
||||
title = "No disponible al teu pla"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Previsualització en directe"
|
||||
note = "El sandbox interactiu es renderitza aquí"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "Mesurat per {{unit}}. L'ús més enllà de la quota gratuïta mensual es factura al teu compte i es detalla a Ús i facturació."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Facturat per"
|
||||
embeds30d = "Incrustacions (30 d)"
|
||||
freeQuota = "Quota gratuïta"
|
||||
freeQuotaValue = "{{amount}} / mes"
|
||||
maturity = "Maduresa"
|
||||
none = "Cap"
|
||||
perAction = "Per acció"
|
||||
price = "Preu"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "Resum"
|
||||
pricing = "Preus"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "opcional"
|
||||
required = "obligatori"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Descripció"
|
||||
name = "Prop"
|
||||
required = "Obligatori"
|
||||
type = "Tipus"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Components GA"
|
||||
componentSpendMtd = "Despesa en components (MTD)"
|
||||
embedsThisMonth = "Incrustacions aquest mes"
|
||||
inBeta = "En beta"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "aprovació"
|
||||
check = "comprovació"
|
||||
@@ -6591,18 +6534,6 @@ signature = "signatura"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Widgets SDK incrustables que deixes anar a la teva pròpia aplicació: un visor, un flux de signatura electrònica, un panell de revisió d'AI. Cadascun es mesura per acció. Fes clic en una targeta per veure instal·lació, ús i props."
|
||||
title = "Components"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "No s'ha pogut carregar el catàleg de components. Torna-ho a provar d'aquí a poc."
|
||||
title = "No hi ha components disponibles"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "Els components GA estan disponibles amb pagament per ús; alguns components Beta són només per a empreses. Les targetes bloquejades mostren una invitació a actualitzar."
|
||||
title = "Alguns components necessiten un pla de pagament"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "cada sol·licitud"
|
||||
eyebrow = "PRIMERS PASSOS"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "Zpracujte {{allowance}} PDFs zdarma"
|
||||
titleWithRate_one = "Zpracujte {{allowance}} PDFs zdarma, poté {{rate}}/PDF"
|
||||
titleWithRate_other = "Zpracujte {{allowance}} PDFs zdarma, poté {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Zamčeno"
|
||||
openAriaLabel = "Otevřít komponentu {{name}}"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Přidat do projektu"
|
||||
ariaLabel = "Detail komponenty"
|
||||
tabsAriaLabel = "Sekce detailu komponenty"
|
||||
upgradeToUnlock = "Upgradujte pro odemknutí"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Instalace"
|
||||
usage = "Použití"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} je zahrnuto od plánu {{tier}}. Upgradujte, abyste ji mohli vložit."
|
||||
title = "Není dostupné ve vašem plánu"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Živý náhled"
|
||||
note = "Interaktivní sandbox se vykreslí zde"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "Měřeno za {{unit}}. Využití nad měsíční bezplatnou kvótu je účtováno vašemu účtu a rozepsáno v části Využití a fakturace."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Účtováno za"
|
||||
embeds30d = "Vložení (30 dní)"
|
||||
freeQuota = "Bezplatná kvóta"
|
||||
freeQuotaValue = "{{amount}} / měs."
|
||||
maturity = "Vyspělost"
|
||||
none = "Žádné"
|
||||
perAction = "Za akci"
|
||||
price = "Cena"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Kód"
|
||||
overview = "Přehled"
|
||||
pricing = "Ceník"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "volitelné"
|
||||
required = "povinné"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Popis"
|
||||
name = "Prop"
|
||||
required = "Povinné"
|
||||
type = "Typ"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Komponenty GA"
|
||||
componentSpendMtd = "Výdaje za komponenty (MTD)"
|
||||
embedsThisMonth = "Vložení tento měsíc"
|
||||
inBeta = "V beta verzi"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "schválení"
|
||||
check = "kontrola"
|
||||
@@ -6591,18 +6534,6 @@ signature = "podpis"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Vložitelné SDK widgety, které vložíte do své aplikace — prohlížeč, tok e-podpisu, panel AI kontroly. Každý je měřen za akci. Kliknutím na kartu zobrazíte instalaci, použití a props."
|
||||
title = "Komponenty"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "Katalog komponent se nepodařilo načíst. Zkuste to brzy znovu."
|
||||
title = "Žádné dostupné komponenty"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "Komponenty GA jsou dostupné v režimu Pay-as-you-go; několik komponent Beta je pouze pro enterprise. Zamčené karty zobrazují výzvu k upgradu."
|
||||
title = "Některé komponenty vyžadují placený plán"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "každý požadavek"
|
||||
eyebrow = "ZAČÍNÁME"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "Behandl {{allowance}} PDFs gratis"
|
||||
titleWithRate_one = "Behandl {{allowance}} PDFs gratis, derefter {{rate}}/PDF"
|
||||
titleWithRate_other = "Behandl {{allowance}} PDFs gratis, derefter {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Låst"
|
||||
openAriaLabel = "Åbn {{name}}-komponent"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Føj til projekt"
|
||||
ariaLabel = "Komponentdetalje"
|
||||
tabsAriaLabel = "Komponentdetaljesektioner"
|
||||
upgradeToUnlock = "Opgrader for at låse op"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Installér"
|
||||
usage = "Brug"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} er inkluderet fra {{tier}}-planen. Opgrader for at indlejre den."
|
||||
title = "Ikke tilgængelig på din plan"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Live preview"
|
||||
note = "Interaktiv sandbox renderes her"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "Måles pr. {{unit}}. Brug ud over den månedlige gratis kvote faktureres til din konto og specificeres under Brug og fakturering."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Faktureres på"
|
||||
embeds30d = "Indlejringer (30d)"
|
||||
freeQuota = "Gratis kvote"
|
||||
freeQuotaValue = "{{amount}} / md."
|
||||
maturity = "Modenhed"
|
||||
none = "Ingen"
|
||||
perAction = "Pr. handling"
|
||||
price = "Pris"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "Oversigt"
|
||||
pricing = "Priser"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "valgfri"
|
||||
required = "påkrævet"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Beskrivelse"
|
||||
name = "Prop"
|
||||
required = "Påkrævet"
|
||||
type = "Type"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Komponenter GA"
|
||||
componentSpendMtd = "Komponentforbrug (MTD)"
|
||||
embedsThisMonth = "Indlejringer denne måned"
|
||||
inBeta = "I beta"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "godkendelse"
|
||||
check = "kontrol"
|
||||
@@ -6591,18 +6534,6 @@ signature = "signatur"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Indlejrbare SDK-widgets, du kan lægge ind i din egen app — en viewer, et e-sign-flow, et AI-gennemgangspanel. Hver måles pr. handling. Klik på et kort for installation, brug og props."
|
||||
title = "Komponenter"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "Komponentkataloget kunne ikke indlæses. Prøv igen om lidt."
|
||||
title = "Ingen komponenter tilgængelige"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "GA-komponenter er tilgængelige på Pay-as-you-go; nogle få Beta-komponenter er kun til enterprise. Låste kort viser en opgraderingsopfordring."
|
||||
title = "Nogle komponenter kræver en betalt plan"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "hver forespørgsel"
|
||||
eyebrow = "KOM GODT I GANG"
|
||||
|
||||
@@ -6553,63 +6553,6 @@ title_other = "{{allowance}} PDFs kostenlos verarbeiten"
|
||||
titleWithRate_one = "{{allowance}} PDFs kostenlos verarbeiten, danach {{rate}}/PDF"
|
||||
titleWithRate_other = "{{allowance}} PDFs kostenlos verarbeiten, danach {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Gesperrt"
|
||||
openAriaLabel = "Komponente {{name}} öffnen"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Zum Projekt hinzufügen"
|
||||
ariaLabel = "Komponentendetail"
|
||||
tabsAriaLabel = "Komponentendetail-Abschnitte"
|
||||
upgradeToUnlock = "Upgrade zum Entsperren"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Installieren"
|
||||
usage = "Usage"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} ist ab dem {{tier}}-Tarif enthalten. Führen Sie ein Upgrade durch, um es einzubetten."
|
||||
title = "In Ihrem Tarif nicht verfügbar"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Live-Vorschau"
|
||||
note = "Interaktive Sandbox wird hier gerendert"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "Nutzungsbasiert pro {{unit}}. Nutzung über das monatliche Freikontingent hinaus wird Ihrem Konto berechnet und unter Nutzung & Abrechnung einzeln aufgeführt."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Abgerechnet nach"
|
||||
embeds30d = "Einbettungen (30T)"
|
||||
freeQuota = "Freikontingent"
|
||||
freeQuotaValue = "{{amount}} / Mon."
|
||||
maturity = "Reifegrad"
|
||||
none = "Keine"
|
||||
perAction = "Pro Aktion"
|
||||
price = "Preis"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "Übersicht"
|
||||
pricing = "Preise"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "optional"
|
||||
required = "erforderlich"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Beschreibung"
|
||||
name = "Prop"
|
||||
required = "Erforderlich"
|
||||
type = "Typ"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Components GA"
|
||||
componentSpendMtd = "Komponentenausgaben (MTD)"
|
||||
embedsThisMonth = "Einbettungen diesen Monat"
|
||||
inBeta = "In Beta"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "Genehmigung"
|
||||
check = "Prüfung"
|
||||
@@ -6623,18 +6566,6 @@ signature = "Signatur"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Einbettbare SDK-Widgets, die Sie in Ihre eigene App einfügen — ein Viewer, ein E-Sign-Flow, ein KI-Review-Panel. Jedes wird pro Aktion nutzungsbasiert abgerechnet. Klicken Sie auf eine Karte für Installation, Nutzung und Props."
|
||||
title = "Komponenten"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "Der Komponentenkatalog konnte nicht geladen werden. Versuchen Sie es in Kürze erneut."
|
||||
title = "Keine Komponenten verfügbar"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "GA-Komponenten sind mit Pay-as-you-go verfügbar; einige Beta-Komponenten sind nur für Enterprise. Gesperrte Karten zeigen einen Upgrade-Hinweis."
|
||||
title = "Einige Komponenten benötigen einen kostenpflichtigen Tarif"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "jede Anfrage"
|
||||
eyebrow = "ERSTE SCHRITTE"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "Επεξεργαστείτε {{allowance}} PDFs δωρεάν"
|
||||
titleWithRate_one = "Επεξεργαστείτε {{allowance}} PDFs δωρεάν και μετά {{rate}}/PDF"
|
||||
titleWithRate_other = "Επεξεργαστείτε {{allowance}} PDFs δωρεάν και μετά {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Κλειδωμένο"
|
||||
openAriaLabel = "Άνοιγμα στοιχείου {{name}}"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Προσθήκη στο έργο"
|
||||
ariaLabel = "Λεπτομέρειες στοιχείου"
|
||||
tabsAriaLabel = "Ενότητες λεπτομερειών στοιχείου"
|
||||
upgradeToUnlock = "Αναβαθμίστε για ξεκλείδωμα"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Εγκατάσταση"
|
||||
usage = "Χρήση"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "Το {{name}} περιλαμβάνεται από το πλάνο {{tier}}. Αναβαθμίστε για να το ενσωματώσετε."
|
||||
title = "Δεν είναι διαθέσιμο στο πλάνο σας"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Ζωντανή προεπισκόπηση"
|
||||
note = "Το διαδραστικό sandbox αποδίδεται εδώ"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "Μετρημένο ανά {{unit}}. Η χρήση πέρα από τη μηνιαία δωρεάν ποσόστωση χρεώνεται στον λογαριασμό σας και αναλύεται στην ενότητα Χρήση & Χρέωση."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Χρεώνεται σε"
|
||||
embeds30d = "Ενσωματώσεις (30ημ.)"
|
||||
freeQuota = "Δωρεάν ποσόστωση"
|
||||
freeQuotaValue = "{{amount}} / μήνα"
|
||||
maturity = "Ωριμότητα"
|
||||
none = "Καμία"
|
||||
perAction = "Ανά ενέργεια"
|
||||
price = "Τιμή"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "Επισκόπηση"
|
||||
pricing = "Τιμολόγηση"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "προαιρετικό"
|
||||
required = "απαιτείται"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Περιγραφή"
|
||||
name = "Prop"
|
||||
required = "Απαιτείται"
|
||||
type = "Τύπος"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Components GA"
|
||||
componentSpendMtd = "Δαπάνη components (MTD)"
|
||||
embedsThisMonth = "Ενσωματώσεις αυτόν τον μήνα"
|
||||
inBeta = "Σε beta"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "έγκριση"
|
||||
check = "έλεγχος"
|
||||
@@ -6591,18 +6534,6 @@ signature = "υπογραφή"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Ενσωματώσιμα SDK widgets που τοποθετείτε στη δική σας εφαρμογή — ένας viewer, μια ροή e-sign, ένα πάνελ ελέγχου AI. Κάθε ένα μετριέται ανά ενέργεια. Κάντε κλικ σε μια κάρτα για εγκατάσταση, χρήση και props."
|
||||
title = "Components"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "Δεν ήταν δυνατή η φόρτωση του καταλόγου components. Δοκιμάστε ξανά σύντομα."
|
||||
title = "Δεν υπάρχουν διαθέσιμα components"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "Τα GA components είναι διαθέσιμα στο Pay-as-you-go. Μερικά Beta components είναι μόνο για enterprise. Οι κλειδωμένες κάρτες εμφανίζουν προτροπή αναβάθμισης."
|
||||
title = "Ορισμένα components χρειάζονται πληρωμένο πλάνο"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "κάθε αίτημα"
|
||||
eyebrow = "ΞΕΚΙΝΩΝΤΑΣ"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "Process {{allowance}} PDFs free"
|
||||
titleWithRate_one = "Process {{allowance}} PDFs free, then {{rate}}/PDF"
|
||||
titleWithRate_other = "Process {{allowance}} PDFs free, then {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Locked"
|
||||
openAriaLabel = "Open {{name}} component"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Add to project"
|
||||
ariaLabel = "Component detail"
|
||||
tabsAriaLabel = "Component detail sections"
|
||||
upgradeToUnlock = "Upgrade to unlock"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Install"
|
||||
usage = "Usage"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} is included from the {{tier}} plan. Upgrade to embed it."
|
||||
title = "Not available on your plan"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Live preview"
|
||||
note = "Interactive sandbox renders here"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "Metered per {{unit}}. Usage beyond the monthly free quota is billed to your account and itemised under Usage & Billing."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Billed on"
|
||||
embeds30d = "Embeds (30d)"
|
||||
freeQuota = "Free quota"
|
||||
freeQuotaValue = "{{amount}} / mo"
|
||||
maturity = "Maturity"
|
||||
none = "None"
|
||||
perAction = "Per action"
|
||||
price = "Price"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "Overview"
|
||||
pricing = "Pricing"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "optional"
|
||||
required = "required"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Description"
|
||||
name = "Prop"
|
||||
required = "Required"
|
||||
type = "Type"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Components GA"
|
||||
componentSpendMtd = "Component spend (MTD)"
|
||||
embedsThisMonth = "Embeds this month"
|
||||
inBeta = "In beta"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "approval"
|
||||
check = "check"
|
||||
@@ -6591,18 +6534,6 @@ signature = "signature"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Embeddable SDK widgets you drop into your own app — a viewer, an e-sign flow, an AI review panel. Each is metered per action. Click a card for install, usage and props."
|
||||
title = "Components"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "The component catalogue could not be loaded. Try again shortly."
|
||||
title = "No components available"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "GA components are available on Pay-as-you-go; a few Beta components are enterprise-only. Locked cards show an upgrade nudge."
|
||||
title = "Some components need a paid plan"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "every request"
|
||||
eyebrow = "GETTING STARTED"
|
||||
|
||||
@@ -6421,63 +6421,6 @@ title_other = "Process {{allowance}} PDFs free"
|
||||
titleWithRate_one = "Process {{allowance}} PDFs free, then {{rate}}/PDF"
|
||||
titleWithRate_other = "Process {{allowance}} PDFs free, then {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Locked"
|
||||
openAriaLabel = "Open {{name}} component"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Add to project"
|
||||
ariaLabel = "Component detail"
|
||||
tabsAriaLabel = "Component detail sections"
|
||||
upgradeToUnlock = "Upgrade to unlock"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Install"
|
||||
usage = "Usage"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} is included from the {{plan}}. Upgrade to embed it."
|
||||
title = "Not available on your plan"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Live preview"
|
||||
note = "Interactive sandbox renders here"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "Metered per {{unit}}. Usage beyond the monthly free quota is billed to your account and itemised under Usage & Billing."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Billed on"
|
||||
embeds30d = "Embeds (30d)"
|
||||
freeQuota = "Free quota"
|
||||
freeQuotaValue = "{{amount}} / mo"
|
||||
maturity = "Maturity"
|
||||
none = "None"
|
||||
perAction = "Per action"
|
||||
price = "Price"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "Overview"
|
||||
pricing = "Pricing"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "optional"
|
||||
required = "required"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Description"
|
||||
name = "Prop"
|
||||
required = "Required"
|
||||
type = "Type"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Components GA"
|
||||
componentSpendMtd = "Component spend (MTD)"
|
||||
embedsThisMonth = "Embeds this month"
|
||||
inBeta = "In beta"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "approval"
|
||||
check = "check"
|
||||
@@ -6491,18 +6434,6 @@ signature = "signature"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Embeddable SDK widgets you drop into your own app — a viewer, an e-sign flow, an AI review panel. Each is metered per action. Click a card for install, usage and props."
|
||||
title = "Components"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "The component catalogue could not be loaded. Try again shortly."
|
||||
title = "No components available"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "GA components are available on Pay-as-you-go; a few Beta components are enterprise-only. Locked cards show an upgrade nudge."
|
||||
title = "Some components need a paid plan"
|
||||
|
||||
[portal.connections]
|
||||
createTitle = "New S3 connection"
|
||||
delete = "Delete"
|
||||
@@ -8997,9 +8928,18 @@ languageDescription = "Choose the display language"
|
||||
logout = "Log out"
|
||||
setAsDefault = "Set as Default"
|
||||
theme = "Theme"
|
||||
themeAccent = "Accent colour"
|
||||
themeAccentCustom = "Custom colour"
|
||||
themeAccentCustomHint = "Custom (defaults recommended)"
|
||||
themeAccentDark = "Dark mode accent colour"
|
||||
themeAccentDefault = "Default"
|
||||
themeAccentDefaultHint = "Default theme (recommended)"
|
||||
themeAccentDescription = "Buttons, links and highlights follow it, and it subtly tints the app. Light and dark each have their own. System uses whichever is active."
|
||||
themeAccentLight = "Light mode accent colour"
|
||||
themeDark = "Dark"
|
||||
themeDescription = "Choose light, dark, or follow your system"
|
||||
themeDescription = "Choose light, dark, or follow your system so it switches automatically."
|
||||
themeLight = "Light"
|
||||
themeReset = "Restore theme to default"
|
||||
themeSystem = "System"
|
||||
title = "General"
|
||||
user = "User"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "Procesa {{allowance}} PDFs gratis"
|
||||
titleWithRate_one = "Procesa {{allowance}} PDFs gratis, luego {{rate}}/PDF"
|
||||
titleWithRate_other = "Procesa {{allowance}} PDFs gratis, luego {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Bloqueado"
|
||||
openAriaLabel = "Abrir componente {{name}}"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Añadir al proyecto"
|
||||
ariaLabel = "Detalle del componente"
|
||||
tabsAriaLabel = "Secciones de detalle del componente"
|
||||
upgradeToUnlock = "Actualiza para desbloquear"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Instalar"
|
||||
usage = "Uso"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} está incluido desde el plan {{tier}}. Actualiza para incrustarlo."
|
||||
title = "No disponible en tu plan"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Vista previa en vivo"
|
||||
note = "El sandbox interactivo se renderiza aquí"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "Medido por {{unit}}. El uso que supere la cuota gratuita mensual se factura a tu cuenta y se detalla en Uso y facturación."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Facturado por"
|
||||
embeds30d = "Incrustaciones (30 d)"
|
||||
freeQuota = "Cuota gratuita"
|
||||
freeQuotaValue = "{{amount}} / mes"
|
||||
maturity = "Madurez"
|
||||
none = "Ninguna"
|
||||
perAction = "Por acción"
|
||||
price = "Precio"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Código"
|
||||
overview = "Resumen"
|
||||
pricing = "Precios"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "opcional"
|
||||
required = "obligatorio"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Descripción"
|
||||
name = "Prop"
|
||||
required = "Obligatorio"
|
||||
type = "Tipo"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Componentes GA"
|
||||
componentSpendMtd = "Gasto en componentes (MTD)"
|
||||
embedsThisMonth = "Incrustaciones este mes"
|
||||
inBeta = "En beta"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "aprobación"
|
||||
check = "comprobación"
|
||||
@@ -6591,18 +6534,6 @@ signature = "firma"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Widgets SDK incrustables que colocas en tu propia app: un visor, un flujo de firma electrónica, un panel de revisión de IA. Cada uno se mide por acción. Haz clic en una tarjeta para instalación, uso y props."
|
||||
title = "Componentes"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "No se ha podido cargar el catálogo de componentes. Inténtalo de nuevo en breve."
|
||||
title = "No hay componentes disponibles"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "Los componentes GA están disponibles en pago por uso; algunos componentes Beta son solo para empresas. Las tarjetas bloqueadas muestran un aviso para actualizar."
|
||||
title = "Algunos componentes necesitan un plan de pago"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "cada solicitud"
|
||||
eyebrow = "PRIMEROS PASOS"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "Prozesatu {{allowance}} PDF doan"
|
||||
titleWithRate_one = "Prozesatu {{allowance}} PDF doan, gero {{rate}}/PDF"
|
||||
titleWithRate_other = "Prozesatu {{allowance}} PDF doan, gero {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Blokeatuta"
|
||||
openAriaLabel = "Ireki {{name}} osagaia"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Gehitu proiektuari"
|
||||
ariaLabel = "Osagaiaren xehetasuna"
|
||||
tabsAriaLabel = "Osagaiaren xehetasun-atalak"
|
||||
upgradeToUnlock = "Eguneratu desblokeatzeko"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Instalatu"
|
||||
usage = "Erabilera"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} {{tier}} planetik aurrera dago barne. Eguneratu plana txertatzeko."
|
||||
title = "Ez dago erabilgarri zure planean"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Zuzeneko aurrebista"
|
||||
note = "Sandbox interaktiboa hemen errendatuko da"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "{{unit}} bakoitzeko neurtua. Hileko doako kuotatik gorako erabilera zure kontuan fakturatzen da eta Erabilera eta fakturazioa atalean xehatzen da."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Honetan fakturatua"
|
||||
embeds30d = "Txertaketak (30 egun)"
|
||||
freeQuota = "Doako kuota"
|
||||
freeQuotaValue = "{{amount}} / hil"
|
||||
maturity = "Heldutasuna"
|
||||
none = "Bat ere ez"
|
||||
perAction = "Ekintza bakoitzeko"
|
||||
price = "Prezioa"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Kodea"
|
||||
overview = "Ikuspegi orokorra"
|
||||
pricing = "Prezioak"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "aukerakoa"
|
||||
required = "beharrezkoa"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Deskribapena"
|
||||
name = "Prop"
|
||||
required = "Beharrezkoa"
|
||||
type = "Mota"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "GA osagaiak"
|
||||
componentSpendMtd = "Osagaien gastua (MTD)"
|
||||
embedsThisMonth = "Txertaketak hilabete honetan"
|
||||
inBeta = "Beta-n"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "onarpena"
|
||||
check = "egiaztapena"
|
||||
@@ -6591,18 +6534,6 @@ signature = "sinadura"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Zure aplikazioan txertatzeko SDK widgetak — bistaratzailea, sinadura elektronikoaren fluxua, AI berrikuspen-panela. Bakoitza ekintza bakoitzeko neurtzen da. Egin klik txartel batean instalazioa, erabilera eta props ikusteko."
|
||||
title = "Osagaiak"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "Ezin izan da osagaien katalogoa kargatu. Saiatu berriro laster."
|
||||
title = "Ez dago osagairik erabilgarri"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "GA osagaiak Pay-as-you-go planean daude erabilgarri; Beta osagai batzuk Enterprise-rako soilik dira. Blokeatutako txartelek eguneratzeko iradokizuna erakusten dute."
|
||||
title = "Osagai batzuek ordainpeko plana behar dute"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "eskaera bakoitza"
|
||||
eyebrow = "HASTEN"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "{{allowance}} PDF را رایگان پردازش کنید"
|
||||
titleWithRate_one = "{{allowance}} PDF را رایگان پردازش کنید، سپس {{rate}}/PDF"
|
||||
titleWithRate_other = "{{allowance}} PDF را رایگان پردازش کنید، سپس {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "قفلشده"
|
||||
openAriaLabel = "باز کردن مؤلفه {{name}}"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "افزودن به پروژه"
|
||||
ariaLabel = "جزئیات مؤلفه"
|
||||
tabsAriaLabel = "بخشهای جزئیات مؤلفه"
|
||||
upgradeToUnlock = "برای باز کردن قفل ارتقا دهید"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "نصب"
|
||||
usage = "استفاده"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} از طرح {{tier}} شامل میشود. برای جاسازی آن ارتقا دهید."
|
||||
title = "در طرح شما در دسترس نیست"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "پیشنمایش زنده"
|
||||
note = "sandbox تعاملی اینجا رندر میشود"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "بر اساس {{unit}} اندازهگیری میشود. استفاده فراتر از سهمیه رایگان ماهانه به حساب شما صورتحساب میشود و زیر Usage & Billing بهصورت موردی نمایش داده میشود."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "صورتحساب بر اساس"
|
||||
embeds30d = "جاسازیها (30 روز)"
|
||||
freeQuota = "سهمیه رایگان"
|
||||
freeQuotaValue = "{{amount}} / ماه"
|
||||
maturity = "بلوغ"
|
||||
none = "هیچکدام"
|
||||
perAction = "بهازای هر اقدام"
|
||||
price = "قیمت"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "کد"
|
||||
overview = "نمای کلی"
|
||||
pricing = "قیمتگذاری"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "اختیاری"
|
||||
required = "الزامی"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "توضیحات"
|
||||
name = "Prop"
|
||||
required = "الزامی"
|
||||
type = "نوع"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "مؤلفههای GA"
|
||||
componentSpendMtd = "هزینه مؤلفه (MTD)"
|
||||
embedsThisMonth = "جاسازیها در این ماه"
|
||||
inBeta = "در Beta"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "تأیید"
|
||||
check = "بررسی"
|
||||
@@ -6591,18 +6534,6 @@ signature = "امضا"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "ویجتهای SDK قابل جاسازی که در برنامه خودتان قرار میدهید — یک نمایشگر، جریان e-sign، پنل بازبینی AI. هرکدام بهازای هر اقدام اندازهگیری میشود. برای نصب، استفاده و props روی کارت کلیک کنید."
|
||||
title = "مؤلفهها"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "کاتالوگ مؤلفهها بارگیری نشد. کمی بعد دوباره تلاش کنید."
|
||||
title = "هیچ مؤلفهای در دسترس نیست"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "مؤلفههای GA در Pay-as-you-go در دسترس هستند؛ چند مؤلفه Beta فقط برای enterprise هستند. کارتهای قفلشده یک پیشنهاد ارتقا نشان میدهند."
|
||||
title = "برخی مؤلفهها به طرح پولی نیاز دارند"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "هر درخواست"
|
||||
eyebrow = "شروع به کار"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "Traitez {{allowance}} PDF gratuitement"
|
||||
titleWithRate_one = "Traitez {{allowance}} PDF gratuitement, puis {{rate}}/PDF"
|
||||
titleWithRate_other = "Traitez {{allowance}} PDF gratuitement, puis {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Verrouillé"
|
||||
openAriaLabel = "Ouvrir le composant {{name}}"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Ajouter au projet"
|
||||
ariaLabel = "Détail du composant"
|
||||
tabsAriaLabel = "Sections de détail du composant"
|
||||
upgradeToUnlock = "Passer à un niveau supérieur pour déverrouiller"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Installer"
|
||||
usage = "Utilisation"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} est inclus à partir du plan {{tier}}. Passez à un niveau supérieur pour l’intégrer."
|
||||
title = "Non disponible avec votre plan"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Aperçu en direct"
|
||||
note = "Le bac à sable interactif s’affiche ici"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "Mesuré par {{unit}}. L’utilisation au-delà du quota mensuel gratuit est facturée à votre compte et détaillée sous Utilisation et facturation."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Facturé sur"
|
||||
embeds30d = "Intégrations (30 j)"
|
||||
freeQuota = "Quota gratuit"
|
||||
freeQuotaValue = "{{amount}} / mois"
|
||||
maturity = "Maturité"
|
||||
none = "Aucun"
|
||||
perAction = "Par action"
|
||||
price = "Prix"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "Vue d’ensemble"
|
||||
pricing = "Tarification"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "facultatif"
|
||||
required = "obligatoire"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Description"
|
||||
name = "Prop"
|
||||
required = "Obligatoire"
|
||||
type = "Type"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Composants GA"
|
||||
componentSpendMtd = "Dépenses des composants (MTD)"
|
||||
embedsThisMonth = "Intégrations ce mois-ci"
|
||||
inBeta = "En beta"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "approbation"
|
||||
check = "vérification"
|
||||
@@ -6591,18 +6534,6 @@ signature = "signature"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Widgets SDK intégrables que vous ajoutez à votre propre application — une visionneuse, un flux de signature électronique, un panneau d’examen AI. Chacun est mesuré par action. Cliquez sur une carte pour l’installation, l’utilisation et les props."
|
||||
title = "Composants"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "Le catalogue des composants n’a pas pu être chargé. Réessayez bientôt."
|
||||
title = "Aucun composant disponible"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "Les composants GA sont disponibles en paiement à l’usage ; quelques composants Beta sont réservés à Enterprise. Les cartes verrouillées affichent une invitation à passer à un niveau supérieur."
|
||||
title = "Certains composants nécessitent un plan payant"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "chaque requête"
|
||||
eyebrow = "BIEN DÉMARRER"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "Próiseáil {{allowance}} PDFs saor in aisce"
|
||||
titleWithRate_one = "Próiseáil {{allowance}} PDF saor in aisce, ansin {{rate}}/PDF"
|
||||
titleWithRate_other = "Próiseáil {{allowance}} PDFs saor in aisce, ansin {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Faoi ghlas"
|
||||
openAriaLabel = "Oscail comhpháirt {{name}}"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Cuir le tionscadal"
|
||||
ariaLabel = "Sonraí comhpháirte"
|
||||
tabsAriaLabel = "Rannáin shonraí comhpháirte"
|
||||
upgradeToUnlock = "Uasghrádaigh chun díghlasáil"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Suiteáil"
|
||||
usage = "Úsáid"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "Tá {{name}} san áireamh ón bplean {{tier}} ar aghaidh. Uasghrádaigh chun é a leabú."
|
||||
title = "Níl sé ar fáil ar do phlean"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Réamhamharc beo"
|
||||
note = "Rindreálfar bosca gainimh idirghníomhach anseo"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "Méadraithe in aghaidh {{unit}}. Billeáiltear úsáid thar an gcuóta míosúil saor in aisce ar do chuntas agus mírítear í faoi Úsáid & Billeáil."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Billeáilte ar"
|
||||
embeds30d = "Leabuithe (30l)"
|
||||
freeQuota = "Cuóta saor in aisce"
|
||||
freeQuotaValue = "{{amount}} / mí"
|
||||
maturity = "Aibíocht"
|
||||
none = "Dada"
|
||||
perAction = "In aghaidh gnímh"
|
||||
price = "Praghas"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "Forbhreathnú"
|
||||
pricing = "Praghsáil"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "roghnach"
|
||||
required = "riachtanach"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Cur síos"
|
||||
name = "Prop"
|
||||
required = "Riachtanach"
|
||||
type = "Cineál"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Comhpháirteanna GA"
|
||||
componentSpendMtd = "Caiteachas comhpháirte (MTD)"
|
||||
embedsThisMonth = "Leabuithe an mhí seo"
|
||||
inBeta = "I mBeta"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "faomhadh"
|
||||
check = "seiceáil"
|
||||
@@ -6591,18 +6534,6 @@ signature = "síniú"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Giuirléidí SDK inleabaithe a chuireann tú isteach i d’aip féin — breathnóir, sreabhadh e-sign, painéal athbhreithnithe AI. Méadraítear gach ceann in aghaidh gnímh. Cliceáil cárta le haghaidh suiteála, úsáide agus props."
|
||||
title = "Comhpháirteanna"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "Níorbh fhéidir catalóg na gcomhpháirteanna a luchtú. Bain triail eile as gan mhoill."
|
||||
title = "Níl aon chomhpháirteanna ar fáil"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "Tá comhpháirteanna GA ar fáil ar Pay-as-you-go; tá cúpla comhpháirt Beta don fhiontar amháin. Taispeánann cártaí faoi ghlas leid uasghrádaithe."
|
||||
title = "Teastaíonn plean íoctha ó roinnt comhpháirteanna"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "gach iarratas"
|
||||
eyebrow = "AG TOSÚ"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "{{allowance}} PDFs मुफ़्त process करें"
|
||||
titleWithRate_one = "{{allowance}} PDFs मुफ़्त process करें, फिर {{rate}}/PDF"
|
||||
titleWithRate_other = "{{allowance}} PDFs मुफ़्त process करें, फिर {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Locked"
|
||||
openAriaLabel = "{{name}} component खोलें"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "project में जोड़ें"
|
||||
ariaLabel = "Component detail"
|
||||
tabsAriaLabel = "Component detail sections"
|
||||
upgradeToUnlock = "अनलॉक करने के लिए upgrade करें"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Install"
|
||||
usage = "Usage"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} {{tier}} प्लान से शामिल है। इसे embed करने के लिए upgrade करें।"
|
||||
title = "आपके प्लान पर उपलब्ध नहीं"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Live preview"
|
||||
note = "Interactive sandbox यहाँ render होता है"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "प्रति {{unit}} metered। मासिक free quota से अधिक usage का बिल आपके account से लिया जाता है और Usage & Billing के अंतर्गत itemise किया जाता है।"
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Billed on"
|
||||
embeds30d = "Embeds (30d)"
|
||||
freeQuota = "Free quota"
|
||||
freeQuotaValue = "{{amount}} / mo"
|
||||
maturity = "Maturity"
|
||||
none = "कोई नहीं"
|
||||
perAction = "Per action"
|
||||
price = "Price"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "Overview"
|
||||
pricing = "Pricing"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "optional"
|
||||
required = "required"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "विवरण"
|
||||
name = "Prop"
|
||||
required = "Required"
|
||||
type = "Type"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Components GA"
|
||||
componentSpendMtd = "Component spend (MTD)"
|
||||
embedsThisMonth = "इस माह embeds"
|
||||
inBeta = "Beta में"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "approval"
|
||||
check = "check"
|
||||
@@ -6591,18 +6534,6 @@ signature = "signature"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Embeddable SDK widgets जिन्हें आप अपने app में डालते हैं — viewer, e-sign flow, AI review panel। हर action पर metered। install, usage और props के लिए card पर क्लिक करें।"
|
||||
title = "Components"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "component catalogue लोड नहीं किया जा सका। थोड़ी देर में फिर कोशिश करें।"
|
||||
title = "कोई components उपलब्ध नहीं"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "GA components Pay-as-you-go पर उपलब्ध हैं; कुछ Beta components केवल enterprise के लिए हैं। Locked cards upgrade nudge दिखाते हैं।"
|
||||
title = "कुछ components के लिए paid plan चाहिए"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "हर request"
|
||||
eyebrow = "शुरुआत करें"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "Obradite {{allowance}} PDF-ova besplatno"
|
||||
titleWithRate_one = "Obradite {{allowance}} PDF-ova besplatno, zatim {{rate}}/PDF"
|
||||
titleWithRate_other = "Obradite {{allowance}} PDF-ova besplatno, zatim {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Zaključano"
|
||||
openAriaLabel = "Otvori komponentu {{name}}"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Dodaj u projekt"
|
||||
ariaLabel = "Detalji komponente"
|
||||
tabsAriaLabel = "Odjeljci detalja komponente"
|
||||
upgradeToUnlock = "Nadogradite za otključavanje"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Instalacija"
|
||||
usage = "Upotreba"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} je uključeno od plana {{tier}}. Nadogradite da biste ga ugradili."
|
||||
title = "Nije dostupno na vašem planu"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Pregled uživo"
|
||||
note = "Interaktivni sandbox prikazuje se ovdje"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "Mjereno po {{unit}}. Upotreba iznad mjesečne besplatne kvote naplaćuje se vašem računu i prikazuje po stavkama u odjeljku Upotreba i naplata."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Naplaćuje se po"
|
||||
embeds30d = "Ugradnje (30 d)"
|
||||
freeQuota = "Besplatna kvota"
|
||||
freeQuotaValue = "{{amount}} / mj."
|
||||
maturity = "Zrelost"
|
||||
none = "Ništa"
|
||||
perAction = "Po radnji"
|
||||
price = "Cijena"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "Pregled"
|
||||
pricing = "Cijene"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "neobavezno"
|
||||
required = "obavezno"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Opis"
|
||||
name = "Prop"
|
||||
required = "Obavezno"
|
||||
type = "Vrsta"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Komponente GA"
|
||||
componentSpendMtd = "Potrošnja komponenti (MTD)"
|
||||
embedsThisMonth = "Ugradnje ovog mjeseca"
|
||||
inBeta = "U beta verziji"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "odobrenje"
|
||||
check = "provjera"
|
||||
@@ -6591,18 +6534,6 @@ signature = "potpis"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Ugradivi SDK widgeti koje ubacujete u vlastitu aplikaciju — preglednik, tijek e-potpisa, AI panel za pregled. Svaki se mjeri po radnji. Kliknite karticu za instalaciju, upotrebu i props."
|
||||
title = "Komponente"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "Katalog komponenti nije moguće učitati. Pokušajte ponovno uskoro."
|
||||
title = "Nema dostupnih komponenti"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "GA komponente dostupne su na planu Pay-as-you-go; nekoliko Beta komponenti dostupno je samo za Enterprise. Zaključane kartice prikazuju poticaj za nadogradnju."
|
||||
title = "Neke komponente zahtijevaju plaćeni plan"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "svaki zahtjev"
|
||||
eyebrow = "POČETAK RADA"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "Dolgozzon fel {{allowance}} PDF-et ingyen"
|
||||
titleWithRate_one = "Dolgozzon fel {{allowance}} PDF-et ingyen, utána {{rate}}/PDF"
|
||||
titleWithRate_other = "Dolgozzon fel {{allowance}} PDF-et ingyen, utána {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Zárolva"
|
||||
openAriaLabel = "{{name}} komponens megnyitása"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Hozzáadás a projekthez"
|
||||
ariaLabel = "Komponens részletei"
|
||||
tabsAriaLabel = "Komponens részleteinek szakaszai"
|
||||
upgradeToUnlock = "Váltson magasabb csomagra a feloldáshoz"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Telepítés"
|
||||
usage = "Használat"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "A(z) {{name}} a(z) {{tier}} csomagtól érhető el. Váltson magasabb csomagra a beágyazásához."
|
||||
title = "Nem érhető el az Ön csomagjában"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Élő előnézet"
|
||||
note = "Az interaktív sandbox itt jelenik meg"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "Mérés {{unit}} alapján. A havi ingyenes kvótán túli használatot a fiókjára számlázzuk, és a Használat és számlázás alatt tételesen megjelenik."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Számlázás alapja"
|
||||
embeds30d = "Beágyazások (30 nap)"
|
||||
freeQuota = "Ingyenes kvóta"
|
||||
freeQuotaValue = "{{amount}} / hó"
|
||||
maturity = "Érettség"
|
||||
none = "Nincs"
|
||||
perAction = "Műveletenként"
|
||||
price = "Ár"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "Áttekintés"
|
||||
pricing = "Árazás"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "opcionális"
|
||||
required = "kötelező"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Leírás"
|
||||
name = "Prop"
|
||||
required = "Kötelező"
|
||||
type = "Típus"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "GA komponensek"
|
||||
componentSpendMtd = "Komponensköltés (MTD)"
|
||||
embedsThisMonth = "Beágyazások ebben a hónapban"
|
||||
inBeta = "Beta állapotban"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "jóváhagyás"
|
||||
check = "ellenőrzés"
|
||||
@@ -6591,18 +6534,6 @@ signature = "aláírás"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Beágyazható SDK widgetek, amelyeket a saját alkalmazásába illeszthet — megjelenítő, e-aláírási folyamat, AI felülvizsgálati panel. Mindegyik műveletenként mért. Kattintson egy kártyára a telepítéshez, használathoz és props megtekintéséhez."
|
||||
title = "Komponensek"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "A komponenskatalógust nem sikerült betölteni. Próbálja újra rövidesen."
|
||||
title = "Nincsenek elérhető komponensek"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "A GA komponensek Pay-as-you-go csomagban érhetők el; néhány Beta komponens csak vállalati csomagban. A zárolt kártyák frissítési javaslatot mutatnak."
|
||||
title = "Egyes komponensekhez fizetős csomag szükséges"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "minden kérés"
|
||||
eyebrow = "ELSŐ LÉPÉSEK"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "Proses {{allowance}} PDF gratis"
|
||||
titleWithRate_one = "Proses {{allowance}} PDF gratis, lalu {{rate}}/PDF"
|
||||
titleWithRate_other = "Proses {{allowance}} PDF gratis, lalu {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Terkunci"
|
||||
openAriaLabel = "Buka komponen {{name}}"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Tambahkan ke proyek"
|
||||
ariaLabel = "Detail komponen"
|
||||
tabsAriaLabel = "Bagian detail komponen"
|
||||
upgradeToUnlock = "Upgrade untuk membuka akses"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Instal"
|
||||
usage = "Penggunaan"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} disertakan mulai paket {{tier}}. Upgrade untuk menyematkannya."
|
||||
title = "Tidak tersedia pada paket Anda"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Pratinjau langsung"
|
||||
note = "Sandbox interaktif dirender di sini"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "Bermeter per {{unit}}. Penggunaan di luar kuota gratis bulanan ditagihkan ke akun Anda dan dirinci di Penggunaan & Penagihan."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Ditagih pada"
|
||||
embeds30d = "Embed (30h)"
|
||||
freeQuota = "Kuota gratis"
|
||||
freeQuotaValue = "{{amount}} / bln"
|
||||
maturity = "Kematangan"
|
||||
none = "Tidak ada"
|
||||
perAction = "Per tindakan"
|
||||
price = "Harga"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "Ikhtisar"
|
||||
pricing = "Harga"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "opsional"
|
||||
required = "wajib"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Deskripsi"
|
||||
name = "Prop"
|
||||
required = "Wajib"
|
||||
type = "Tipe"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Komponen GA"
|
||||
componentSpendMtd = "Pengeluaran komponen (MTD)"
|
||||
embedsThisMonth = "Embed bulan ini"
|
||||
inBeta = "Dalam beta"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "persetujuan"
|
||||
check = "pemeriksaan"
|
||||
@@ -6591,18 +6534,6 @@ signature = "tanda tangan"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Widget SDK yang dapat disematkan dan Anda letakkan di aplikasi sendiri — viewer, alur e-sign, panel tinjauan AI. Masing-masing bermeter per tindakan. Klik kartu untuk instalasi, penggunaan, dan props."
|
||||
title = "Komponen"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "Katalog komponen tidak dapat dimuat. Coba lagi sebentar lagi."
|
||||
title = "Tidak ada komponen tersedia"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "Komponen GA tersedia pada Bayar sesuai pemakaian; beberapa komponen Beta hanya untuk enterprise. Kartu terkunci menampilkan dorongan upgrade."
|
||||
title = "Beberapa komponen memerlukan paket berbayar"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "setiap permintaan"
|
||||
eyebrow = "MEMULAI"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "Elabora {{allowance}} PDF gratuitamente"
|
||||
titleWithRate_one = "Elabora {{allowance}} PDF gratuitamente, poi {{rate}}/PDF"
|
||||
titleWithRate_other = "Elabora {{allowance}} PDF gratuitamente, poi {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "Bloccato"
|
||||
openAriaLabel = "Apri componente {{name}}"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "Aggiungi al progetto"
|
||||
ariaLabel = "Dettaglio componente"
|
||||
tabsAriaLabel = "Sezioni dettaglio componente"
|
||||
upgradeToUnlock = "Esegui l'upgrade per sbloccare"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "Installazione"
|
||||
usage = "Uso"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} è incluso dal piano {{tier}}. Esegui l'upgrade per incorporarlo."
|
||||
title = "Non disponibile nel tuo piano"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "Anteprima live"
|
||||
note = "La sandbox interattiva viene renderizzata qui"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "Misurato per {{unit}}. L'uso oltre la quota gratuita mensile viene addebitato al tuo account e dettagliato in Uso e fatturazione."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "Fatturato su"
|
||||
embeds30d = "Incorporamenti (30g)"
|
||||
freeQuota = "Quota gratuita"
|
||||
freeQuotaValue = "{{amount}} / mese"
|
||||
maturity = "Maturità"
|
||||
none = "Nessuno"
|
||||
perAction = "Per azione"
|
||||
price = "Prezzo"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "Panoramica"
|
||||
pricing = "Prezzi"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "facoltativo"
|
||||
required = "obbligatorio"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "Descrizione"
|
||||
name = "Prop"
|
||||
required = "Obbligatorio"
|
||||
type = "Tipo"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Componenti GA"
|
||||
componentSpendMtd = "Spesa componenti (MTD)"
|
||||
embedsThisMonth = "Incorporamenti questo mese"
|
||||
inBeta = "In beta"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "approvazione"
|
||||
check = "controllo"
|
||||
@@ -6591,18 +6534,6 @@ signature = "firma"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "Widget SDK incorporabili che inserisci nella tua app — un visualizzatore, un flusso di firma elettronica, un pannello di revisione AI. Ognuno è misurato per azione. Fai clic su una scheda per installazione, uso e props."
|
||||
title = "Componenti"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "Impossibile caricare il catalogo dei componenti. Riprova tra poco."
|
||||
title = "Nessun componente disponibile"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "I componenti GA sono disponibili con Pay-as-you-go; alcuni componenti Beta sono solo enterprise. Le schede bloccate mostrano un invito all'upgrade."
|
||||
title = "Alcuni componenti richiedono un piano a pagamento"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "ogni richiesta"
|
||||
eyebrow = "PER INIZIARE"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "{{allowance}} 件のPDFを無料で処理"
|
||||
titleWithRate_one = "{{allowance}} 件のPDFを無料で処理、その後 {{rate}}/PDF"
|
||||
titleWithRate_other = "{{allowance}} 件のPDFを無料で処理、その後 {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "ロック中"
|
||||
openAriaLabel = "{{name}} コンポーネントを開く"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "プロジェクトに追加"
|
||||
ariaLabel = "コンポーネント詳細"
|
||||
tabsAriaLabel = "コンポーネント詳細セクション"
|
||||
upgradeToUnlock = "アップグレードしてロック解除"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "インストール"
|
||||
usage = "使用方法"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}} は {{tier}} プランから含まれます。埋め込むにはアップグレードしてください。"
|
||||
title = "ご利用のプランでは利用できません"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "ライブプレビュー"
|
||||
note = "インタラクティブなサンドボックスがここにレンダリングされます"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "{{unit}} ごとに従量課金されます。月間無料枠を超えた使用量はアカウントに請求され、Usage & Billing に明細表示されます。"
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "請求単位"
|
||||
embeds30d = "埋め込み(30日)"
|
||||
freeQuota = "無料枠"
|
||||
freeQuotaValue = "{{amount}} / 月"
|
||||
maturity = "成熟度"
|
||||
none = "なし"
|
||||
perAction = "アクションごと"
|
||||
price = "価格"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "概要"
|
||||
pricing = "料金"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "任意"
|
||||
required = "必須"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "説明"
|
||||
name = "Prop"
|
||||
required = "必須"
|
||||
type = "タイプ"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "Components GA"
|
||||
componentSpendMtd = "コンポーネント利用額(MTD)"
|
||||
embedsThisMonth = "今月の埋め込み数"
|
||||
inBeta = "Beta 中"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "承認"
|
||||
check = "チェック"
|
||||
@@ -6591,18 +6534,6 @@ signature = "署名"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "自分のアプリに組み込める SDK ウィジェット — ビューア、電子署名フロー、AI レビューパネル。各項目はアクションごとの従量課金です。カードをクリックすると、インストール、使用方法、props を確認できます。"
|
||||
title = "コンポーネント"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "コンポーネントカタログを読み込めませんでした。しばらくしてからもう一度お試しください。"
|
||||
title = "利用可能なコンポーネントはありません"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "GA コンポーネントは従量課金で利用できます。一部の Beta コンポーネントは Enterprise 専用です。ロックされたカードにはアップグレード案内が表示されます。"
|
||||
title = "一部のコンポーネントには有料プランが必要です"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "すべてのリクエスト"
|
||||
eyebrow = "はじめに"
|
||||
|
||||
@@ -6521,63 +6521,6 @@ title_other = "PDF {{allowance}}개 무료 처리"
|
||||
titleWithRate_one = "PDF {{allowance}}개 무료 처리 후 {{rate}}/PDF"
|
||||
titleWithRate_other = "PDF {{allowance}}개 무료 처리 후 {{rate}}/PDF"
|
||||
|
||||
[portal.catalogue.card]
|
||||
lockedAriaLabel = "잠김"
|
||||
openAriaLabel = "{{name}} 구성 요소 열기"
|
||||
|
||||
[portal.catalogue.detail]
|
||||
addToProject = "프로젝트에 추가"
|
||||
ariaLabel = "구성 요소 상세"
|
||||
tabsAriaLabel = "구성 요소 상세 섹션"
|
||||
upgradeToUnlock = "업그레이드하여 잠금 해제"
|
||||
|
||||
[portal.catalogue.detail.code]
|
||||
install = "설치"
|
||||
usage = "사용법"
|
||||
|
||||
[portal.catalogue.detail.locked]
|
||||
description = "{{name}}은(는) {{tier}} 플랜부터 포함됩니다. 임베드하려면 업그레이드하세요."
|
||||
title = "현재 플랜에서 사용할 수 없음"
|
||||
|
||||
[portal.catalogue.detail.preview]
|
||||
badge = "라이브 미리보기"
|
||||
note = "Interactive sandbox가 여기에 렌더링됩니다"
|
||||
|
||||
[portal.catalogue.detail.pricing]
|
||||
note = "{{unit}}당 종량제입니다. 월별 무료 할당량을 초과한 사용량은 계정에 청구되며 Usage & Billing에 항목별로 표시됩니다."
|
||||
|
||||
[portal.catalogue.detail.stats]
|
||||
billedOn = "청구 기준"
|
||||
embeds30d = "임베드(30일)"
|
||||
freeQuota = "무료 할당량"
|
||||
freeQuotaValue = "{{amount}} / 월"
|
||||
maturity = "성숙도"
|
||||
none = "없음"
|
||||
perAction = "작업당"
|
||||
price = "가격"
|
||||
|
||||
[portal.catalogue.detail.tabs]
|
||||
code = "Code"
|
||||
overview = "개요"
|
||||
pricing = "가격"
|
||||
props = "Props / API"
|
||||
|
||||
[portal.catalogue.props]
|
||||
optional = "선택 사항"
|
||||
required = "필수"
|
||||
|
||||
[portal.catalogue.props.columns]
|
||||
description = "설명"
|
||||
name = "Prop"
|
||||
required = "필수"
|
||||
type = "유형"
|
||||
|
||||
[portal.catalogue.summary]
|
||||
componentsGa = "구성 요소 GA"
|
||||
componentSpendMtd = "구성 요소 지출(MTD)"
|
||||
embedsThisMonth = "이번 달 임베드"
|
||||
inBeta = "Beta 중"
|
||||
|
||||
[portal.components.billingUnit]
|
||||
approval = "승인"
|
||||
check = "확인"
|
||||
@@ -6591,18 +6534,6 @@ signature = "서명"
|
||||
beta = "Beta"
|
||||
ga = "GA"
|
||||
|
||||
[portal.componentsView]
|
||||
subtitle = "자체 앱에 넣을 수 있는 임베드 가능한 SDK 위젯 — viewer, e-sign 흐름, AI 검토 패널. 각 항목은 작업당 종량제입니다. 설치, 사용법, props를 보려면 카드를 클릭하세요."
|
||||
title = "구성 요소"
|
||||
|
||||
[portal.componentsView.empty]
|
||||
description = "구성 요소 카탈로그를 불러올 수 없습니다. 잠시 후 다시 시도하세요."
|
||||
title = "사용 가능한 구성 요소 없음"
|
||||
|
||||
[portal.componentsView.lockedBanner]
|
||||
description = "GA 구성 요소는 Pay-as-you-go에서 사용할 수 있으며, 일부 Beta 구성 요소는 enterprise 전용입니다. 잠긴 카드에는 업그레이드 안내가 표시됩니다."
|
||||
title = "일부 구성 요소에는 유료 플랜이 필요합니다"
|
||||
|
||||
[portal.docs.authentication]
|
||||
codeCaption = "모든 요청"
|
||||
eyebrow = "시작하기"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user