계약에 선언만 되어 있고 구현이 없던 네 오퍼레이션과, 의도된 스텁으로 남아 있던 catalog 두 종류가 공개 화면 다섯 곳을 조용히 비워 두고 있었다. 프로젝트 게시 — 프로젝트는 `RecordKind` 에 없어 문서 게시 파이프라인을 타지 못하는데, 공개 조회들(프로젝트 목록·상세·프로필의 "현재 프로젝트"·홈 focus)은 전부 `public_resource_projection` 의 PROJECT 행을 가시성 관문으로 쓴다. 그 행을 세우는 경로가 없었으므로 프로젝트는 만들 수는 있어도 공개될 수는 없었다. 릴리스가 자체 경로를 갖는 것과 같은 이유로, 문서 파이프라인에 끼워 넣지 않고 원본 상태·투영·canonical route 세 곳을 한 트랜잭션에서 함께 세운다. 홈 focus — `home_focus_config` 는 마이그레이션이 빈 행 하나만 넣어 두었고 그 값을 읽고 쓸 use case 가 없었다. 지목한 대상이 실제로 있는지는 여기서 확인한다. 테이블에 FK 가 없어(설정이 대상보다 오래 살아남는 것을 허용하는 설계다) 없는 id 도 저장되고, 그러면 공개 화면은 조용히 빈 focus 를 그린다 — 저장은 성공했는데 아무것도 안 나오는, 이유를 알 수 없는 실패가 된다. 대상이 공개인지는 확인하지 않는다: 미리 지목해 두고 게시와 동시에 뜨게 하는 것이 정상적인 순서다. catalog RELATION/EVIDENCE — 「슬라이스 2·5에서 채운다」는 주석과 함께 `List.of()` 로 남아 있었다. 그래서 어떤 기록도 연결 대상 목록을 채울 수 없었다. RELATION 은 작성 중에 고르는 것이라 작업본까지 포함하고(두 문서를 같이 쓰면서 서로 잇는 것이 정상적인 순서다), EVIDENCE 는 읽는 사람이 따라갈 수 있어야 하므로 공개된 것만 포함한다. 계약은 이 네 오퍼레이션을 ProblemDetails 모양으로 두고 있었다. 백엔드가 모든 JSON 응답을 envelope 으로 감싸므로 구현하는 순간 어긋난다 — 나머지와 같은 모양으로 옮겼다. `/home-focus` 는 케밥 세그먼트라 D19(AIP-122)를 위반해 `/home/focus` 로 나눴다. 함께 고친 것들 (모두 이 작업 전부터 빨간 상태였다): - `error-codes.yaml` 의 DOCUMENT_NOT_FOUND 가 표면마다 하나씩 두 행이었다. 이 레지스트리의 식별자는 code 하나뿐이라 로딩 자체가 깨졌고, 그 여파로 거버넌스 테스트 네 개와 outbox 계약 테스트가 함께 넘어졌다. 같은 코드는 같은 말을 해야 한다. - 벤더된 계약 세 개의 MANIFEST.sha256 이 실제 파일과 어긋나 있었다. - ActuatorSecurityHttpTest 는 "DB·Redis 없는 슬라이스"라고 적어 두고 Redis 자동설정을 막지 않아, localhost:6379 연결 실패가 /actuator/health 를 503 으로 만들었다. 보안 태세와 무관한 이유로 빨개지던 테스트다. - 테스트 JVM 힙이 Gradle 기본 512m 이라 Spring context 캐시 + ArchUnit + Testcontainers 조합에서 OOM 이 났다. 증상이 테스트 실패가 아니라 "Executor 를 완료할 수 없음"이어서 원인을 가리켰다. - APP_SESSION_TIMEOUT 이 env-keys 레지스트리에 없었다. 새 SQL 은 실제 PostgreSQL 위에서 돌린다 — 컴파일도 단위 테스트도 컬럼 이름을 검증하지 못한다는 것이 이 파일이 존재하는 이유다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu
763 lines
43 KiB
YAML
763 lines
43 KiB
YAML
# =============================================================================
|
|
# Mirrors src/.env into Spring's Environment. Comments document allowed values;
|
|
# input validation lives in the *Settings records under each module.
|
|
# =============================================================================
|
|
|
|
spring:
|
|
application:
|
|
# free-form string (also exposed as the JSON log "app" field)
|
|
name: ${APP_NAME}
|
|
mvc:
|
|
problemdetails:
|
|
# Architectural decision D1/D5: RFC 7807 ProblemDetail is rejected in favour
|
|
# of the custom {success,data,error,meta} envelope. Pin OFF explicitly so a
|
|
# future Spring Boot default flip cannot silently re-enable it.
|
|
enabled: false
|
|
web:
|
|
error:
|
|
# always | never | on_param
|
|
include-stacktrace: ${APP_SERVER_ERROR_INCLUDE_STACKTRACE}
|
|
# always | never | on_param
|
|
include-message: ${APP_SERVER_ERROR_INCLUDE_MESSAGE}
|
|
profiles:
|
|
# common values: local | dev | stage | prod (free-form)
|
|
active: ${SPRING_PROFILES_ACTIVE:local}
|
|
datasource:
|
|
# jdbc URL: jdbc:postgresql://host:5432/db
|
|
url: ${APP_DATASOURCE_URL}
|
|
username: ${APP_DATASOURCE_USERNAME}
|
|
password: ${APP_DATASOURCE_PASSWORD}
|
|
driver-class-name: ${APP_DATASOURCE_DRIVER}
|
|
hikari:
|
|
# D1 (feature-database-connection-pool-contract): small-pool axiom + PostgreSQL formula
|
|
# starting point (maximumPoolSize = cores * 2 + effective_spindle_count, adjust via load
|
|
# test). Fixed-size pool recommended (minimumIdle = maximumPoolSize). Must satisfy
|
|
# application-port D12 REQUIRES_NEW lower bound:
|
|
# maxPoolSize >= concurrent_threads * (1 + max_inNew_depth) + 1
|
|
# Value owner: feature-env-driven-runtime-configuration (APP_DATASOURCE_POOL_MAX_SIZE).
|
|
# integer >= 1
|
|
maximum-pool-size: ${APP_DATASOURCE_POOL_MAX_SIZE}
|
|
# D1: fixed-size pool recommended (minimumIdle = maximumPoolSize per HikariCP #HIKARI-CFG-C8).
|
|
# Current registry value min-idle=2 is a MIN_IDLE_POLICY_DRIFT vs the fixed-size
|
|
# recommendation; value alignment is delegated to feature-env-driven-runtime-configuration.
|
|
# integer >= 0
|
|
minimum-idle: ${APP_DATASOURCE_POOL_MIN_IDLE}
|
|
# D2 (HIKARI-CFG-C1): fail-fast pin — reject pool-starved threads quickly rather than
|
|
# holding them for 30 s (HikariCP default). Must be >= 250 ms (enforced at startup by
|
|
# HikariPoolConstraintValidator). Typical synchronous HTTP path value: a few seconds.
|
|
# env-keys.yaml default "5s", plain milliseconds and ISO-8601 values are parsed by
|
|
# HikariPoolConstraintValidator with Spring Boot DurationStyle; invalid values fail startup.
|
|
connection-timeout: ${APP_DATASOURCE_CONNECTION_TIMEOUT}
|
|
# milliseconds
|
|
idle-timeout: ${APP_DATASOURCE_POOL_IDLE_TIMEOUT}
|
|
# D3 (HIKARI-CFG-C2): must be several seconds shorter than the DB/infrastructure idle
|
|
# timeout (DB wait_timeout, PgBouncer idle_transaction_timeout, firewall NAT timeout).
|
|
# Current default (30 min) is a placeholder until the actual DB wait_timeout is confirmed
|
|
# (see §Claims "DB wait_timeout 미확인" — needs-confirmation). Recommended: DB_idle_limit
|
|
# minus at least 60 s as a conservative margin. Value owner: feature-env-driven.
|
|
# milliseconds
|
|
max-lifetime: ${APP_DATASOURCE_POOL_MAX_LIFETIME}
|
|
# D4 (HIKARI-CFG-C4): greenfield — ping idle connections to prevent NAT/firewall/DB
|
|
# idle-kill from silently dropping them. Constraint: keepalive-time < max-lifetime
|
|
# (enforced by HikariPoolConstraintValidator). Provisional literal 120 000 ms (2 min);
|
|
# adjust once actual DB/firewall idle timeout is confirmed (§Claims).
|
|
# UNSUPPORTED_IMPL_DECISION: literal value is a provisional policy default.
|
|
# New env key APP_DATASOURCE_KEEPALIVE_TIME registration delegated to
|
|
# feature-env-driven-runtime-configuration.
|
|
keepalive-time: 120000
|
|
# D5 (HIKARI-CFG-C5): greenfield — enable connection leak early warning. Value must be
|
|
# >= 2000 ms to activate (enforced by HikariPoolConstraintValidator; 0 = disabled).
|
|
# Provisional literal 30 000 ms (30 s) — chosen well above the estimated longest normal
|
|
# transaction (~5 s) to avoid false positives on legitimate slow operations.
|
|
# UNSUPPORTED_IMPL_DECISION: literal value is a provisional policy default.
|
|
# New env key APP_DATASOURCE_LEAK_DETECTION_THRESHOLD delegated to
|
|
# feature-env-driven-runtime-configuration.
|
|
leak-detection-threshold: 30000
|
|
# D7 (HIKARI-CFG-C6): greenfield — must be < connection-timeout (HIKARI-CFG-C6),
|
|
# enforced by HikariPoolConstraintValidator. Resolves VALIDATION_TIMEOUT_CONFLICT:
|
|
# HikariCP default 5000 ms equals connection-timeout 5 s / 5000 ms → constraint
|
|
# violation. Provisional literal 3000 ms satisfies the constraint for connection-timeout
|
|
# values >= 3001 ms.
|
|
# UNSUPPORTED_IMPL_DECISION: literal value is a provisional policy default.
|
|
# New env key APP_DATASOURCE_VALIDATION_TIMEOUT delegated to
|
|
# feature-env-driven-runtime-configuration.
|
|
validation-timeout: 3000
|
|
# D6 (HIKARI-CFG-C7): greenfield — keep positive (default = 1 ms) to fail fast when DB
|
|
# is unavailable at startup. Aligns with runtime-health startup validation and project-note
|
|
# §9 "잘못된 env 값 startup fail-fast" policy. Negative value disables fail-fast (allowed
|
|
# only in orchestration environments where DB may start after the app — coordinate with
|
|
# runtime-health-lifecycle branch).
|
|
# UNSUPPORTED_IMPL_DECISION: literal value is a provisional policy default.
|
|
# New env key APP_DATASOURCE_INIT_FAIL_TIMEOUT delegated to
|
|
# feature-env-driven-runtime-configuration.
|
|
initialization-fail-timeout: 1
|
|
# ---------------------------------------------------------------------------
|
|
# D8 (feature-database-connection-pool-contract) — SLOW QUERY DETECTION
|
|
# POLICY ONLY — no library dependency added here. Implementation is DEFERRED
|
|
# pending local verification (TODO #3: confirm ParameterTransformer masking
|
|
# applies to slow-query listener output).
|
|
#
|
|
# Baseline (app layer, param-safe):
|
|
# datasource-proxy SlowQueryListener + ParameterTransformer ([REDACTED] masking).
|
|
# Requires the datasource-proxy / spring-boot-data-source-decorator dependency
|
|
# and a @Bean ParameterTransformer; deferred until masking behavior on slow-query
|
|
# output is locally verified.
|
|
#
|
|
# Production augment (DBA-owned):
|
|
# DB-side log_min_duration_statement — parameters included in extended-protocol
|
|
# output (PostgreSQL official security warning). DBA controls and redacts.
|
|
#
|
|
# Dev only (PROD-FORBIDDEN):
|
|
# Hibernate SQL_SLOW (LOG_QUERIES_SLOWER_THAN_MS) logs materialized SQL with
|
|
# substituted parameters — violates the "SQL/param 로그 금지" hard rule from
|
|
# feature-persistence-failure-baseline. NEVER enable in prod.
|
|
#
|
|
# Rejected:
|
|
# P6Spy — no built-in masking API; effective SQL exposes parameters by default
|
|
# with no safe override. Rejected per D8 (#C3/#C4).
|
|
# ---------------------------------------------------------------------------
|
|
# ---------------------------------------------------------------------------
|
|
# Flyway forward-only migration (feature-migration-startup-contract D1/D2/D4).
|
|
# Flyway runs during context refresh (Spring Boot autoconfig + the
|
|
# migrationStartupRunner FlywayMigrationStrategy), BEFORE the app reports
|
|
# readiness — so migration is inherently readiness-gated (D5): a failed or
|
|
# in-progress migration can never serve traffic.
|
|
#
|
|
# The three options below are PINNED, not env-driven: they are forbidden under
|
|
# prod (D2/D4) and a static pin means a future Flyway/Spring Boot default flip
|
|
# cannot silently re-enable them (same reasoning as spring.mvc.problemdetails.
|
|
# enabled above). FlywayProdSafetyValidator fails the boot (exit 71) if any
|
|
# per-environment override re-enables them under the prod profile.
|
|
# ---------------------------------------------------------------------------
|
|
flyway:
|
|
# false: never auto-baseline an existing schema — keep the missing-migration
|
|
# safety net (FLYWAY-C6). Enabling under prod is forbidden (D4).
|
|
baseline-on-migrate: false
|
|
# false: reject out-of-order migrations — preserve cross-developer ordering
|
|
# consistency (FLYWAY-C5). Enabling under prod is forbidden (D4).
|
|
out-of-order: false
|
|
# true: keep Flyway `clean` (drops the whole schema) disarmed. Disabling this
|
|
# under prod is forbidden — clean is destructive.
|
|
clean-disabled: true
|
|
jpa:
|
|
hibernate:
|
|
# none | validate | update | create | create-drop
|
|
# prod accepts only none|validate; JpaSchemaSafetyValidator rejects schema-writing modes.
|
|
ddl-auto: ${APP_DATASOURCE_DDL_AUTO}
|
|
# true | false
|
|
show-sql: ${APP_DATASOURCE_SHOW_SQL}
|
|
# true | false (don't enable in prod)
|
|
open-in-view: ${APP_DATASOURCE_OPEN_IN_VIEW}
|
|
properties:
|
|
hibernate:
|
|
# true | false (paired with show-sql)
|
|
format_sql: ${APP_DATASOURCE_FORMAT_SQL}
|
|
security:
|
|
oauth2:
|
|
resourceserver:
|
|
jwt:
|
|
# OIDC issuer (e.g., Keycloak realm URL). Spring Boot resolves JWKS from
|
|
# /.well-known/openid-configuration at this URI.
|
|
issuer-uri: ${APP_SECURITY_JWT_ISSUER}
|
|
# validated against the JWT `aud` claim; blank disables the check
|
|
audiences: ${APP_SECURITY_JWT_AUDIENCE}
|
|
main:
|
|
# off | console | log
|
|
banner-mode: ${SPRING_BANNER_MODE}
|
|
# true | false
|
|
lazy-initialization: ${SPRING_MAIN_LAZY_INITIALIZATION}
|
|
# true | false
|
|
log-startup-info: ${SPRING_MAIN_LOG_STARTUP_INFO}
|
|
threads:
|
|
virtual:
|
|
# true | false (Java 21 virtual threads for Tomcat request handlers)
|
|
enabled: ${SPRING_THREADS_VIRTUAL_ENABLED}
|
|
servlet:
|
|
session:
|
|
# Studio 작성자는 한 기록을 여러 번 저장하며 오래 머문다. Spring 기본 30분 유휴 만료는 그
|
|
# 리듬보다 짧아 작성 도중 로그인 화면으로 돌아가는 일이 잦았다 — 저장하지 않은 편집이
|
|
# 있으면 그 시점에 잃는다.
|
|
#
|
|
# 값은 배포가 정한다. 늘릴수록 훔친 session cookie가 유효한 창도 같이 늘어나므로,
|
|
# Keycloak realm 의 SSO idle 과 따로 놀지 않게 함께 맞춘다.
|
|
timeout: ${APP_SESSION_TIMEOUT:8h}
|
|
multipart:
|
|
# feature-api-contract-baseline D8: bound request body size so an oversized
|
|
# upload classifies as 413 PAYLOAD_TOO_LARGE inside the envelope (via
|
|
# GlobalExceptionHandler#handleMaxUploadSizeExceededException), never a raw 500.
|
|
# Multipart-specific upload limits (UPLOAD_SIZE_EXCEEDED) are refined by
|
|
# feature-file-resource-handling-contract.
|
|
# Same placeholders as app.fileserver-platform.upload.*, deliberately. Two independent
|
|
# ceilings meant the servlet container rejected at 10MB whatever the Fileserver policy said,
|
|
# so a 100MB upload failed before any Fileserver code — including its error mapping — ran,
|
|
# and the configured limit described a policy nobody could reach.
|
|
max-file-size: ${APP_FILESERVER_PLATFORM_UPLOAD_MAX_FILE_SIZE:100MB}
|
|
max-request-size: ${APP_FILESERVER_PLATFORM_UPLOAD_MAX_REQUEST_SIZE:110MB}
|
|
# Jackson deserialization policy (feature-boundary-validation-mapping-contract B1).
|
|
# Every request DTO crosses this boundary; the switches make malformed payloads
|
|
# fail at the edge rather than silently coercing or dropping fields.
|
|
jackson:
|
|
deserialization:
|
|
# true | false (Jackson 2.13+ default; reject unknown JSON keys)
|
|
fail-on-unknown-properties: ${SPRING_JACKSON_DESER_FAIL_ON_UNKNOWN_PROPERTIES}
|
|
# true | false (block JSON null → primitive 0/false coercion)
|
|
fail-on-null-for-primitives: ${SPRING_JACKSON_DESER_FAIL_ON_NULL_FOR_PRIMITIVES}
|
|
# true | false (surface JSON containing fields the target @JsonIgnore'd)
|
|
fail-on-ignored-properties: ${SPRING_JACKSON_DESER_FAIL_ON_IGNORED_PROPERTIES}
|
|
# Serialization output policy (feature-schema-serialization-contract D2/D3).
|
|
# These mirror current defaults but are pinned so a future Spring Boot default
|
|
# flip cannot silently break the datetime / decimal wire contract — the same
|
|
# reasoning as spring.mvc.problemdetails.enabled above.
|
|
datatype:
|
|
enum:
|
|
# true | false (false = Jackson default; unknown enum -> throw, not null)
|
|
read-unknown-enum-values-as-null: ${SPRING_JACKSON_DESER_READ_UNKNOWN_ENUM_VALUES_AS_NULL}
|
|
datetime:
|
|
# true | false (false: java.time -> ISO-8601 string via JavaTimeModule, D2)
|
|
write-dates-as-timestamps: ${SPRING_JACKSON_SER_WRITE_DATES_AS_TIMESTAMPS}
|
|
lifecycle:
|
|
# duration: 30s | 1m | 500ms
|
|
timeout-per-shutdown-phase: ${APP_SERVER_SHUTDOWN_TIMEOUT}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Actuator / management — MERGED ownership at integration:
|
|
# - feature-management-actuator-security-contract owns server.port, the exposure
|
|
# allowlist/exclude, health show-details, shutdown-disabled, and info.
|
|
# - feature-runtime-health-lifecycle-contract owns the health probe SHAPE
|
|
# (probes.enabled + the liveness/readiness/startup groups + dependency taxonomy).
|
|
# ---------------------------------------------------------------------------
|
|
management:
|
|
# feature-management-actuator-security-contract D1: separate management port so actuator
|
|
# endpoints are not exposed on the same socket as the app API. Default: 9001.
|
|
server:
|
|
port: ${MANAGEMENT_SERVER_PORT:9001}
|
|
endpoints:
|
|
web:
|
|
# D2: production allowlist — only safe, scrape-friendly endpoints are exposed.
|
|
exposure:
|
|
include: health,prometheus,info,loggers
|
|
# D2/D4/D5: explicitly excluded dangerous endpoints (env leaks secrets,
|
|
# heapdump/threaddump = memory forensics, shutdown = remote kill, configprops = secret leak).
|
|
exclude: env,configprops,heapdump,threaddump,shutdown
|
|
endpoint:
|
|
health:
|
|
# D8: never expose health details to unauthenticated callers.
|
|
show-details: when-authorized
|
|
# Membership is validated. With validation off, a group naming a contributor that does not
|
|
# exist is silently dropped and readiness reports UP while proving nothing about the
|
|
# dependency it claims to gate on — a false green that survives exactly as long as nobody
|
|
# checks. On, the cost is that every name below must resolve, in every deployment.
|
|
#
|
|
# That cost is paid by construction rather than by convention: the groups below name only
|
|
# unconditional contributors, and the one conditional member — `redisRequired` — is appended
|
|
# by RedisReadinessGroupPostProcessor from the same predicate that creates the bean. A
|
|
# contributor can therefore be named only where it exists, and a misspelled one still fails
|
|
# startup.
|
|
validate-group-membership: true
|
|
# feature-runtime-health-lifecycle-contract: expose the Kubernetes-ready
|
|
# liveness/readiness/startup probe paths.
|
|
probes:
|
|
enabled: true
|
|
group:
|
|
# Liveness: JVM can continue (OOM → liveness DOWN → pod restart).
|
|
# Only livenessState is included; dependency health MUST NOT be here —
|
|
# a DOWN DB must never trigger a pod restart when the JVM is healthy.
|
|
liveness:
|
|
include: livenessState
|
|
# Readiness: ready to serve traffic AND all REQUIRED dependencies up.
|
|
# Every name here must resolve to a contributor that exists (validate-group-membership
|
|
# above). A correctness-role Redis contributor belongs in this list and is added when the
|
|
# Redis runtime composition creates one; naming it before then produced a readiness probe
|
|
# that reported UP without ever checking Redis.
|
|
# `redisRequired` is NOT listed here, and must not be: group membership does not tolerate a
|
|
# conditional contributor being absent. Boot validates the group against the contributors
|
|
# that exist, so naming it statically made every Redis-off and cache-only deployment fail at
|
|
# startup with "Included health contributor 'redisRequired' in group 'readiness' does not
|
|
# exist". RedisReadinessGroupPostProcessor appends it to this list — from the same predicate
|
|
# that creates the bean — when Redis is on and a correctness role selected it.
|
|
# `redisOptional` is never appended: a cache outage is reported as degraded detail and never
|
|
# turns a healthy JVM or an otherwise-ready pod unavailable.
|
|
readiness:
|
|
include: readinessState,db
|
|
# Startup: startup/migration validation complete.
|
|
# readinessState acts as the startup completion gate — it flips UP only
|
|
# after the context is fully initialized (Flyway migration included).
|
|
startup:
|
|
include: readinessState
|
|
shutdown:
|
|
# D4: shutdown endpoint disabled globally — even if somehow exposed, it cannot be invoked.
|
|
access: none
|
|
info:
|
|
build:
|
|
# Build-info only (no env leak).
|
|
enabled: true
|
|
|
|
server:
|
|
# integer 1-65535
|
|
port: ${APP_SERVER_PORT}
|
|
# graceful | immediate
|
|
shutdown: ${APP_SERVER_SHUTDOWN}
|
|
# none | native | framework (trust X-Forwarded-* behind LB/proxy)
|
|
forward-headers-strategy: ${APP_SERVER_FORWARD_HEADERS_STRATEGY}
|
|
tomcat:
|
|
threads:
|
|
# integer >= 1
|
|
max: ${APP_SERVER_TOMCAT_MAX_THREADS}
|
|
# integer >= 0
|
|
min-spare: ${APP_SERVER_TOMCAT_MIN_SPARE_THREADS}
|
|
# integer >= 0 (OS-level backlog queue depth)
|
|
accept-count: ${APP_SERVER_TOMCAT_ACCEPT_COUNT}
|
|
# integer >= 1 (hard cap on simultaneously open connections)
|
|
max-connections: ${APP_SERVER_TOMCAT_MAX_CONNECTIONS}
|
|
# duration: 20s | 1m
|
|
connection-timeout: ${APP_SERVER_TOMCAT_CONNECTION_TIMEOUT}
|
|
compression:
|
|
# true | false
|
|
enabled: ${APP_SERVER_COMPRESSION_ENABLED}
|
|
# bytes or sized: 1024 | 1KB | 2KB
|
|
min-response-size: ${APP_SERVER_COMPRESSION_MIN_RESPONSE_SIZE}
|
|
logging:
|
|
level:
|
|
# TRACE | DEBUG | INFO | WARN | ERROR | OFF
|
|
root: ${APP_LOG_LEVEL_ROOT}
|
|
dev.caskeleton: ${APP_LOG_LEVEL_APP}
|
|
org.springframework: ${APP_LOG_LEVEL_SPRING}
|
|
org.springframework.web: ${APP_LOG_LEVEL_WEB}
|
|
# DEBUG here prints SQL once JPA/jdbc is wired in
|
|
org.hibernate.SQL: ${APP_LOG_LEVEL_SQL}
|
|
|
|
# Module-scoped knobs. Each block is bound into a *Settings @ConfigurationProperties
|
|
# record in the corresponding module, which is where allowed-value validation lives.
|
|
ca-skeleton:
|
|
# Canonical HTTP capability activation. Bindings are the sole activation SSOT: provider
|
|
# definitions alone are inert, and the current NOT_IMPLEMENTED readiness card rejects ACTIVE
|
|
# before any client/executor/pool resource can be created.
|
|
capabilities:
|
|
# Every capability below renders its keys under one namespace — app.redis.namespace, which is
|
|
# {environment}:{service}:{domain}. The per-capability namespace-application /
|
|
# namespace-environment pairs that used to live here are gone: four capabilities each joining
|
|
# two free-form tokens in their own order produced four different key prefixes, and the ACL
|
|
# pattern meant to fence the deployment in matched none of them.
|
|
cache:
|
|
# Canonical semantic cache activation. Disabled by default; "redis" composes
|
|
# RedisCacheRegionAdapter for the region named below and resolves HMAC material by reference.
|
|
bindings:
|
|
default: ${APP_CACHE_CANONICAL_DEFAULT_PROVIDER:disabled}
|
|
semantic-region: ${APP_CACHE_REDIS_SEMANTIC_REGION:default}
|
|
key-version: 1
|
|
key-hmac-secret-reference: secret://environment/APP_CACHE_REDIS_KEY_HMAC_SECRET
|
|
command-timeout: ${APP_CACHE_REDIS_COMMAND_TIMEOUT:200ms}
|
|
positive-soft-ttl: ${APP_CACHE_REDIS_POSITIVE_SOFT_TTL:30s}
|
|
positive-hard-ttl: ${APP_CACHE_REDIS_POSITIVE_HARD_TTL:5m}
|
|
negative-ttl: ${APP_CACHE_REDIS_NEGATIVE_TTL:10s}
|
|
# Floor on the hard TTL. Below it, entries expire faster than the round trip that wrote them:
|
|
# every read misses and every miss writes.
|
|
minimum-hard-ttl: ${APP_CACHE_REDIS_MINIMUM_HARD_TTL:1s}
|
|
idempotency:
|
|
# disabled | jdbc | redis. JDBC is the existing V1 provider; Redis is the owner-safe V2
|
|
# provider. They are mutually exclusive and no V1-to-V2 facade is inferred.
|
|
provider: ${APP_IDEMPOTENCY_PROVIDER:jdbc}
|
|
key-hmac-secret-reference: secret://environment/APP_IDEMPOTENCY_REDIS_KEY_HMAC_SECRET
|
|
key-version: 1
|
|
command-timeout: ${APP_IDEMPOTENCY_REDIS_COMMAND_TIMEOUT:200ms}
|
|
processing-lease: ${APP_IDEMPOTENCY_PROCESSING_LEASE:30s}
|
|
replay-ttl: ${APP_IDEMPOTENCY_TTL:24h}
|
|
failure-retention: ${APP_IDEMPOTENCY_FAILURE_RETENTION:24h}
|
|
response-codec-id: json-v2
|
|
# Numeric: the owner-safe claim writes this into the record and compares it there.
|
|
policy-revision: 2
|
|
lease:
|
|
# disabled | redis. This is EFFICIENCY_ONLY and never supplies fencing.
|
|
provider: ${APP_LEASE_PROVIDER:disabled}
|
|
key-hmac-secret-reference: secret://environment/APP_LEASE_REDIS_KEY_HMAC_SECRET
|
|
key-version: 1
|
|
command-timeout: ${APP_LEASE_REDIS_COMMAND_TIMEOUT:200ms}
|
|
contention-retry-after: ${APP_LEASE_REDIS_CONTENTION_RETRY_AFTER:50ms}
|
|
# How much shorter than the server's TTL this holder considers its lease valid. The two
|
|
# clocks are not the same clock, and a holder that measured the full TTL locally would still
|
|
# believe it held a lease the server had already handed to somebody else.
|
|
drift-budget: ${APP_LEASE_REDIS_DRIFT_BUDGET:10ms}
|
|
rate-limit:
|
|
# disabled | redis. This is the sole outbound provider activation selector.
|
|
provider: ${APP_RATE_LIMIT_PROVIDER:disabled}
|
|
failure-policy: ${APP_RATE_LIMIT_FAILURE_POLICY:fail-closed}
|
|
default-policy-id: ${APP_RATE_LIMIT_DEFAULT_POLICY_ID:api-default}
|
|
failure-retry-after: ${APP_RATE_LIMIT_FAILURE_RETRY_AFTER:100ms}
|
|
key-version: ${APP_RATE_LIMIT_KEY_VERSION:1}
|
|
command-timeout: ${APP_RATE_LIMIT_REDIS_COMMAND_TIMEOUT:200ms}
|
|
policies:
|
|
api-default:
|
|
revision: ${APP_RATE_LIMIT_POLICY_REVISION:v1}
|
|
algorithm: ${APP_RATE_LIMIT_ALGORITHM:sliding-counter}
|
|
limit: ${APP_RATE_LIMIT_LIMIT:100}
|
|
window: ${APP_RATE_LIMIT_WINDOW:1s}
|
|
capacity: ${APP_RATE_LIMIT_CAPACITY:100}
|
|
refill-tokens: ${APP_RATE_LIMIT_REFILL_TOKENS:100}
|
|
refill-period: ${APP_RATE_LIMIT_REFILL_PERIOD:1s}
|
|
maximum-cost: ${APP_RATE_LIMIT_MAXIMUM_COST:10}
|
|
cleanup-grace: ${APP_RATE_LIMIT_CLEANUP_GRACE:5s}
|
|
maximum-clock-regression: ${APP_RATE_LIMIT_MAXIMUM_CLOCK_REGRESSION:250ms}
|
|
security:
|
|
bootstrap:
|
|
# required, non-blank — startup fails if blank (see BootstrapSettings)
|
|
app-name: ${APP_NAME}
|
|
runtime:
|
|
# feature-env-driven-runtime-configuration D8: enforced at startup by
|
|
# StartupSafetyValidator. The two prod-unsafe toggles fail startup if true under
|
|
# the prod profile; multi-instance fails startup if its coordination beans are absent.
|
|
# true | false (internal error detail in responses — forbidden under prod)
|
|
error-detail-exposure-enabled: ${APP_ERROR_DETAIL_EXPOSURE_ENABLED:false}
|
|
# true | false (request/response body capture in logs — forbidden under prod)
|
|
log-body-capture-enabled: ${APP_LOG_BODY_CAPTURE_ENABLED:false}
|
|
# true | false (requires the 5 instance-coordination beans when true)
|
|
multi-instance-enabled: ${APP_MULTI_INSTANCE_ENABLED:false}
|
|
# true | false (whether to run Flyway migrations automatically at startup)
|
|
migration-on-startup: ${APP_MIGRATION_ON_STARTUP:true}
|
|
# feature-distributed-lock-contract D5 — distributed lock acquisition contract (try-lock +
|
|
# finite wait + lease TTL). Bound to LockSettings (adapter-persistence). Plain values, NOT
|
|
# APP_* env keys — new env-key registration is feature-env-driven-runtime-configuration's
|
|
# domain (out of scope for this branch); code defaults in LockSettings mirror these.
|
|
lock:
|
|
wait-time: 3s
|
|
lease-ttl: 30s
|
|
# Which RDBMS vendor composition the persistence adapter registers: postgresql | h2. Bound to
|
|
# PersistenceVendorSettings (adapter-persistence), which rejects any other value at startup.
|
|
#
|
|
# A property rather than a profile check, because the vendor belongs to the datastore and not to
|
|
# the environment that happens to use it. The profiles then state their choice:
|
|
# application-local.yml selects h2, application-dev.yml and application-prod.yml select
|
|
# postgresql, and prod additionally refuses h2 through PersistenceVendorProdSafetyValidator.
|
|
#
|
|
# The value here is the default for a deployment that activates no profile at all — PostgreSQL,
|
|
# the vendor every deployment ran before this selector existed.
|
|
persistence:
|
|
vendor: postgresql
|
|
# JPA named-policy deadline envelope. JpaTransactionSettings validates the hierarchy;
|
|
# SpringPolicyTransactionPort intersects these limits with the caller's absolute CallBudget
|
|
# and the actual Hikari connection timeout before acquiring a transaction.
|
|
jpa:
|
|
transaction:
|
|
transaction-timeout: 30s
|
|
begin-budget: 250ms
|
|
minimum-action-window: 1s
|
|
completion-margin: 500ms
|
|
statement-timeout: 10s
|
|
lock-timeout: 2s
|
|
idle-guard-timeout: 15s
|
|
transaction-margin: 250ms
|
|
lock-margin: 100ms
|
|
retry-base-delay: 10ms
|
|
retry-maximum-delay: 50ms
|
|
retry-maximum-attempts: 2
|
|
presentation:
|
|
# feature-api-contract-baseline D2: API version prefix. Default is the URI
|
|
# prefix "/v1" (major-version path, AIP-185); override via env, or set "" for
|
|
# no prefix. The supplemental "X-Api-Version" header never overrides the path.
|
|
api-base-path: ${PRESENTATION_API_BASE_PATH:/v1}
|
|
techlog:
|
|
studio:
|
|
# Studio 문서 목록 커서 서명 키. 값이 없거나 16바이트 미만이면 StudioSettings가 경고하고 개발용
|
|
# 값으로 대체한다 — 커서에 권한이 실리지 않아 부팅을 막을 사유는 아니지만, 인스턴스마다 값이
|
|
# 다르면 한 인스턴스가 발급한 커서를 다른 인스턴스가 거부한다.
|
|
cursor-signing-key: ${APP_STUDIO_CURSOR_SIGNING_KEY:}
|
|
# 검증 결과가 유효한 기간(studio_validation.valid_until).
|
|
validation-ttl: ${APP_STUDIO_VALIDATION_TTL:1h}
|
|
# 미리보기가 유효한 기간(studio_preview.expires_at).
|
|
preview-ttl: ${APP_STUDIO_PREVIEW_TTL:24h}
|
|
idempotency:
|
|
# feature-rate-limit-idempotency-contract D6/§E. ttl is env-driven (<=72h,
|
|
# validated in IdempotencyProperties); reaper-interval is literal operational tuning.
|
|
ttl: ${APP_IDEMPOTENCY_TTL}
|
|
reaper-interval: 10m
|
|
async:
|
|
# feature-background-job-async-contract D7 — @Async ThreadPoolTaskExecutor pool sizing.
|
|
# Registry SSOT: docs/registries/env-keys.yaml (APP_ASYNC_EXECUTOR_* rows 1337-1377).
|
|
# Bound into AsyncExecutorSettings @ConfigurationProperties(prefix = "ca-skeleton.async.executor").
|
|
# Bounded queue is mandatory (D7) — an unbounded queue makes max-size unreachable.
|
|
executor:
|
|
# int >= 1 (positive_int). Always-alive worker count.
|
|
core-size: ${APP_ASYNC_EXECUTOR_CORE_SIZE:10}
|
|
# int >= core-size (positive_int_ge_core). Hard ceiling on workers.
|
|
max-size: ${APP_ASYNC_EXECUTOR_MAX_SIZE:50}
|
|
# int in 1..<Integer.MAX_VALUE (positive_int_bounded). Bounded backlog depth.
|
|
queue-capacity: ${APP_ASYNC_EXECUTOR_QUEUE_CAPACITY:200}
|
|
outbox:
|
|
# feature-domain-event-outbox-contract I11 — all six values are literal defaults;
|
|
# NO env placeholders (spec: 신규 env key 없음). Bound into OutboxProperties.
|
|
# true | false — enable/disable the relay scheduler (OutboxRelayScheduler)
|
|
relay-enabled: true
|
|
# ISO-8601 duration — how often the relay polls for pending rows
|
|
poll-interval: PT5S
|
|
# integer >= 1 — maximum rows claimed per relay cycle
|
|
batch-size: 20
|
|
# ISO-8601 duration — IN_FLIGHT orphan visibility window (I6: next_attempt_at reuse)
|
|
in-flight-timeout: PT5M
|
|
# ISO-8601 duration — read also by adapter-persistence OutboxReaper @Scheduled
|
|
reaper-interval: PT10M
|
|
# ISO-8601 duration — PUBLISHED row retention before reaper deletes them (I3)
|
|
# read also by adapter-persistence OutboxReaper via ${ca-skeleton.outbox.published-retention:P7D}
|
|
published-retention: P7D
|
|
security:
|
|
# jwt | redis-session; the bootstrap composition validator rejects mixed infrastructure.
|
|
auth-mode: ${APP_SECURITY_AUTH_MODE:jwt}
|
|
# Required only in jwt mode.
|
|
issuer-uri: ${APP_SECURITY_JWT_ISSUER}
|
|
# blank to skip audience check
|
|
audience: ${APP_SECURITY_JWT_AUDIENCE}
|
|
# comma-separated list (Spring binds to List<String>)
|
|
public-paths: ${SECURITY_PUBLIC_PATHS}
|
|
session:
|
|
cookie-name: ${APP_SESSION_COOKIE_NAME:CA_SESSION}
|
|
secure: ${APP_SESSION_COOKIE_SECURE:true}
|
|
http-only: ${APP_SESSION_COOKIE_HTTP_ONLY:true}
|
|
same-site: ${APP_SESSION_COOKIE_SAME_SITE:Lax}
|
|
path: ${APP_SESSION_COOKIE_PATH:/}
|
|
csrf-cookie-name: ${APP_SESSION_CSRF_COOKIE_NAME:XSRF-TOKEN}
|
|
csrf-header-name: ${APP_SESSION_CSRF_HEADER_NAME:X-XSRF-TOKEN}
|
|
authz:
|
|
# feature-authentication-authorization-contract D2/D3/D8: app-side role→permission
|
|
# mapping (the default source; IdP-issued permission claims are an out-of-scope
|
|
# alternative). Keys are RAW IdP role names (no ROLE_ prefix — that prefix only
|
|
# exists on Spring authorities, not on the principal's raw role set), looked up
|
|
# case-insensitively. Values are explicitly enumerated `resource:action` permissions
|
|
# (no wildcards — least-privilege, OWASP-AUTHZ-C4). Product-specific roles belong here.
|
|
role-permissions: {}
|
|
cors:
|
|
# true | false
|
|
enabled: ${APP_SECURITY_CORS_ENABLED}
|
|
# comma-separated
|
|
allowed-origins: ${APP_SECURITY_CORS_ORIGINS}
|
|
# comma-separated; empty -> defaults
|
|
allowed-methods: ${APP_SECURITY_CORS_ALLOWED_METHODS}
|
|
# comma-separated; "*" allows any
|
|
allowed-headers: ${APP_SECURITY_CORS_ALLOWED_HEADERS}
|
|
# true | false
|
|
allow-credentials: ${APP_SECURITY_CORS_ALLOW_CREDENTIALS}
|
|
# seconds
|
|
max-age-seconds: ${APP_SECURITY_CORS_MAX_AGE}
|
|
logging:
|
|
file:
|
|
# true | false (wraps console + adds rolling JSON file appender)
|
|
enabled: ${APP_LOG_FILE_ENABLED}
|
|
# relative (to src/) or absolute
|
|
path: ${APP_LOG_FILE_PATH}
|
|
# size with unit: KB | MB | GB
|
|
max-size: ${APP_LOG_FILE_MAX_SIZE}
|
|
# integer >= 1
|
|
max-history: ${APP_LOG_FILE_MAX_HISTORY}
|
|
# size with unit or 0
|
|
total-size-cap: ${APP_LOG_FILE_TOTAL_SIZE_CAP}
|
|
async:
|
|
# true | false (AsyncAppender wrapper for non-blocking I/O)
|
|
enabled: ${APP_LOG_ASYNC_ENABLED}
|
|
# integer >= 1
|
|
queue-size: ${APP_LOG_ASYNC_QUEUE_SIZE}
|
|
# integer >= 0 (0 = never drop)
|
|
discarding-threshold: ${APP_LOG_ASYNC_DISCARDING_THRESHOLD}
|
|
json:
|
|
# IANA timezone (UTC | Asia/Seoul | ...) or "default"
|
|
timezone: ${APP_LOG_JSON_TIMEZONE}
|
|
# ISO 8601 pattern
|
|
timestamp-pattern: ${APP_LOG_JSON_TIMESTAMP_PATTERN}
|
|
# true | false (file/method/line — expensive)
|
|
include-caller-data: ${APP_LOG_JSON_INCLUDE_CALLER_DATA}
|
|
# integer; 0 = full name, positive = abbreviated
|
|
logger-name-length: ${APP_LOG_JSON_LOGGER_NAME_LENGTH}
|
|
# float in [0.0, 1.0] — keep-probability for <=INFO logs (prod 0.1 = 10% sampling;
|
|
# WARN/ERROR always 100%). Consumed by SamplingTurboFilter via logback <springProperty>.
|
|
sampling-rate: ${APP_LOG_SAMPLING_RATE}
|
|
privacy:
|
|
# secret-tier HMAC salt for user_principal pseudonymization (DRIFT-6; algorithm SSOT
|
|
# feature-data-retention-privacy-contract). Blank -> PrivacySettings warns + uses a dev
|
|
# sentinel; prod MUST supply a real secret-manager value.
|
|
pseudonymization-salt: ${APP_PRIVACY_PSEUDONYMIZATION_SALT:}
|
|
# feature-distributed-tracing-contract D1/D4/D6 — tracing seam settings.
|
|
# The OTel/Micrometer tracer runtime is active in this repo (Plan A — seam activated):
|
|
# micrometer-tracing-bridge-otel + opentelemetry-exporter-otlp are wired in
|
|
# app-bootstrap/build.gradle. The exporter remains off while OTEL_EXPORTER_OTLP_ENDPOINT
|
|
# is blank (url_or_empty default). A fork can supply the endpoint via env or override
|
|
# SpanErrorRecorder with its own bean.
|
|
# D4 disabled-fallback: even when enabled=false, RequestLoggingFilter always
|
|
# generates a W3C traceparent so meta.traceId and log trace_id are never null.
|
|
# D6 per-profile defaults: prod=0.01 / staging=0.10 / dev·local=1.0.
|
|
# APP_TRACING_SAMPLE_RATE overrides the per-profile default when set.
|
|
tracing:
|
|
# true | false (boolean_strict). Tracing seam on/off.
|
|
enabled: ${APP_TRACING_ENABLED:true}
|
|
# float in [0.0, 1.0]. Per-profile override (D6 float_between_0_and_1 validation at startup).
|
|
# blank = per-profile default via TracingSampleRateResolver (D-1 ISSUE-1 fix).
|
|
sample-rate: ${APP_TRACING_SAMPLE_RATE:}
|
|
exporter:
|
|
# url_or_empty: blank = exporter off (D1 SEAM default); non-blank must be a valid URL.
|
|
otlp-endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:}
|
|
|
|
# feature-integration-adapter-templates — optional integration adapter toggles.
|
|
# The `app.<domain>.<adapter>.enabled` flags feed Spring @ConditionalOnProperty (Layer 1):
|
|
# disabled (default): messaging binds a fail-fast Disabled* sentinel bean; cache/notification
|
|
# contribute nothing and fail fast in their router (CacheStoreRouter / RoutingNotifier) on
|
|
# unbound access. Enabled registers the real adapter
|
|
# (which needs its project-supplied integration client bean). Domain namespace, NOT a
|
|
# generic `app.adapter.*` prefix (branch-note §Audit A1). Env keys are the registry SSOT.
|
|
app:
|
|
# The one global Redis switch. False (the default) means no Redis settings are bound, no Redis
|
|
# credential is required, no client, connection, thread or health contributor is created, and no
|
|
# Redis-specific resource is read. There is deliberately no second master switch: a role such as
|
|
# cache or session selects *which* Redis capabilities compose, never *whether* Redis exists.
|
|
redis:
|
|
enabled: ${APP_REDIS_ENABLED:false}
|
|
# Fileserver R2 exact destination/provider composition. Disabled by default: while false,
|
|
# these blank attestation placeholders do not create directories, probe a filesystem, or
|
|
# contribute FilePublicationPort. Enabling fails closed unless every local-persistent
|
|
# attestation value matches the pre-provisioned root. No implicit local fallback exists.
|
|
fileserver:
|
|
enabled: ${APP_FILESERVER_ENABLED:false}
|
|
destinations:
|
|
local-export:
|
|
provider-ref: local-primary
|
|
required-publication: unique-atomic-create
|
|
required-durability: file-and-directory-sync
|
|
maximum-rows: 1000000
|
|
maximum-encoded-bytes: 1073741824
|
|
providers:
|
|
local-primary:
|
|
# local-persistent is the only implemented/qualified R2 provider.
|
|
# shared-mounted/NFS and SFTP settings must not be added before their providers exist.
|
|
type: local-persistent
|
|
root-directory: ${APP_FILESERVER_LOCAL_ROOT:}
|
|
auto-create: false
|
|
strict-path-security: true
|
|
expected-file-store-name: ${APP_FILESERVER_LOCAL_EXPECTED_FILE_STORE_NAME:}
|
|
expected-file-store-type: ${APP_FILESERVER_LOCAL_EXPECTED_FILE_STORE_TYPE:}
|
|
mount-sentinel-name: .ca-fileserver-volume
|
|
mount-sentinel-sha256: ${APP_FILESERVER_LOCAL_MOUNT_SENTINEL_SHA256:}
|
|
expected-owner: ${APP_FILESERVER_LOCAL_EXPECTED_OWNER:}
|
|
maximum-root-mode: "0750"
|
|
# HTTP Fileserver platform — a different capability from app.fileserver above, which publishes
|
|
# tabular exports. This one serves upload/download/lifecycle routes over HTTP and owns its own
|
|
# namespace so the two cannot be switched on by accident together.
|
|
#
|
|
# While enabled=false none of the detail below is bound: FileserverPlatformAutoConfiguration
|
|
# binds the block itself, and it is not processed until the master switch is true. Unknown keys
|
|
# under this prefix are refused rather than ignored.
|
|
# Outbound HTTP Client platform. Only the master switch lives here.
|
|
#
|
|
# The per-client surface is an indexed list, and templating one element would materialise a
|
|
# nameless client in every deployment — which the settings' own aggregate validation refuses, so
|
|
# the template could not be left in place. Clients are therefore declared straight from the
|
|
# environment as APP_HTTPCLIENT_CLIENTS_0_*, registered in
|
|
# docs/httpclient/env-fields.yaml, and an APP_HTTPCLIENT_ variable absent from that
|
|
# registry fails startup rather than being ignored.
|
|
#
|
|
# While enabled=false none of it is bound: HttpClientPlatformAutoConfiguration binds the block
|
|
# itself and is not processed until the master switch is true.
|
|
httpclient:
|
|
enabled: ${APP_HTTPCLIENT_ENABLED:false}
|
|
fileserver-platform:
|
|
enabled: ${APP_FILESERVER_PLATFORM_ENABLED:false}
|
|
# Writer-lease owner. Must be unique per instance in a multi-instance deployment; the startup
|
|
# gate treats the default as single-instance.
|
|
instance-id: ${APP_FILESERVER_PLATFORM_INSTANCE_ID:local-node}
|
|
default-namespace: ${APP_FILESERVER_PLATFORM_DEFAULT_NAMESPACE:default}
|
|
storage:
|
|
# Absolute, on its own volume, and never under a web or configuration root. A relative path
|
|
# resolves against the process working directory, which differs between a container and a
|
|
# test, so it is refused.
|
|
root: ${APP_FILESERVER_PLATFORM_STORAGE_ROOT:/var/lib/backend/files}
|
|
publish-mode: ${APP_FILESERVER_PLATFORM_STORAGE_PUBLISH_MODE:atomic-move-preferred}
|
|
buffer-size: ${APP_FILESERVER_PLATFORM_STORAGE_BUFFER_SIZE:128KB}
|
|
forbidden-root-ancestors: ${APP_FILESERVER_PLATFORM_STORAGE_FORBIDDEN_ROOT_ANCESTORS:/app,/etc,/usr/share/nginx/html}
|
|
upload:
|
|
# Shared with spring.servlet.multipart below through the same placeholder. Two independent
|
|
# limits would let the servlet container reject an upload the Fileserver policy allows,
|
|
# before any Fileserver code — including its error mapping — ever runs.
|
|
max-file-size: ${APP_FILESERVER_PLATFORM_UPLOAD_MAX_FILE_SIZE:100MB}
|
|
max-request-size: ${APP_FILESERVER_PLATFORM_UPLOAD_MAX_REQUEST_SIZE:110MB}
|
|
initial-reservation: ${APP_FILESERVER_PLATFORM_UPLOAD_INITIAL_RESERVATION:8MB}
|
|
max-parts: ${APP_FILESERVER_PLATFORM_UPLOAD_MAX_PARTS:16}
|
|
ttl: ${APP_FILESERVER_PLATFORM_UPLOAD_TTL:1h}
|
|
reservation-ttl: ${APP_FILESERVER_PLATFORM_UPLOAD_RESERVATION_TTL:24h}
|
|
lease-duration: ${APP_FILESERVER_PLATFORM_UPLOAD_LEASE_DURATION:30s}
|
|
require-content-length: ${APP_FILESERVER_PLATFORM_UPLOAD_REQUIRE_CONTENT_LENGTH:false}
|
|
download:
|
|
cache-control: ${APP_FILESERVER_PLATFORM_DOWNLOAD_CACHE_CONTROL:private, no-store}
|
|
inline-allowed: ${APP_FILESERVER_PLATFORM_DOWNLOAD_INLINE_ALLOWED:false}
|
|
max-ranges: ${APP_FILESERVER_PLATFORM_DOWNLOAD_MAX_RANGES:1}
|
|
# Applies to the single-range profile too, so the ceiling is not inert by default.
|
|
max-range-bytes: ${APP_FILESERVER_PLATFORM_DOWNLOAD_MAX_RANGE_BYTES:100MB}
|
|
zero-copy-enabled: ${APP_FILESERVER_PLATFORM_DOWNLOAD_ZERO_COPY_ENABLED:true}
|
|
zero-copy-minimum-bytes: ${APP_FILESERVER_PLATFORM_DOWNLOAD_ZERO_COPY_MINIMUM_BYTES:16MB}
|
|
transfer:
|
|
core-size: ${APP_FILESERVER_PLATFORM_TRANSFER_CORE_SIZE:8}
|
|
max-size: ${APP_FILESERVER_PLATFORM_TRANSFER_MAX_SIZE:32}
|
|
queue-capacity: ${APP_FILESERVER_PLATFORM_TRANSFER_QUEUE_CAPACITY:64}
|
|
await-seconds: ${APP_FILESERVER_PLATFORM_TRANSFER_AWAIT_SECONDS:300}
|
|
security:
|
|
# required | role-based | unenforced. `required` has no built-in policy and fails startup
|
|
# unless the deployment supplies a FileAccessPolicy bean; `unenforced` is refused under a
|
|
# production profile.
|
|
access-policy: ${APP_FILESERVER_PLATFORM_SECURITY_ACCESS_POLICY:required}
|
|
read-roles: ${APP_FILESERVER_PLATFORM_SECURITY_READ_ROLES:ROLE_FILE_READ}
|
|
write-roles: ${APP_FILESERVER_PLATFORM_SECURITY_WRITE_ROLES:ROLE_FILE_WRITE}
|
|
admin-roles: ${APP_FILESERVER_PLATFORM_SECURITY_ADMIN_ROLES:ROLE_FILE_ADMIN}
|
|
verification:
|
|
timeout: ${APP_FILESERVER_PLATFORM_VERIFICATION_TIMEOUT:5s}
|
|
require-media-type-verdict: ${APP_FILESERVER_PLATFORM_VERIFICATION_REQUIRE_MEDIA_TYPE_VERDICT:false}
|
|
inline-safe-profile: ${APP_FILESERVER_PLATFORM_VERIFICATION_INLINE_SAFE_PROFILE:false}
|
|
quota:
|
|
instance-upload-permits: ${APP_FILESERVER_PLATFORM_QUOTA_INSTANCE_UPLOAD_PERMITS:16}
|
|
scope-upload-permits: ${APP_FILESERVER_PLATFORM_QUOTA_SCOPE_UPLOAD_PERMITS:4}
|
|
direct-download-permits: ${APP_FILESERVER_PLATFORM_QUOTA_DIRECT_DOWNLOAD_PERMITS:64}
|
|
soft-high-water: ${APP_FILESERVER_PLATFORM_QUOTA_SOFT_HIGH_WATER:0.70}
|
|
hard-high-water: ${APP_FILESERVER_PLATFORM_QUOTA_HARD_HIGH_WATER:0.85}
|
|
admin:
|
|
enabled: ${APP_FILESERVER_PLATFORM_ADMIN_ENABLED:false}
|
|
orphan-minimum-age: ${APP_FILESERVER_PLATFORM_ADMIN_ORPHAN_MINIMUM_AGE:1h}
|
|
cleanup:
|
|
enabled: ${APP_FILESERVER_PLATFORM_CLEANUP_ENABLED:false}
|
|
interval: ${APP_FILESERVER_PLATFORM_CLEANUP_INTERVAL:60s}
|
|
max-items: ${APP_FILESERVER_PLATFORM_CLEANUP_MAX_ITEMS:100}
|
|
max-bytes: ${APP_FILESERVER_PLATFORM_CLEANUP_MAX_BYTES:1GB}
|
|
retry-backoff: ${APP_FILESERVER_PLATFORM_CLEANUP_RETRY_BACKOFF:5m}
|
|
tus:
|
|
enabled: ${APP_FILESERVER_PLATFORM_TUS_ENABLED:false}
|
|
httpbis-draft12:
|
|
# Unratified protocol; the contract can change without notice.
|
|
enabled: ${APP_FILESERVER_PLATFORM_HTTPBIS_DRAFT12_ENABLED:false}
|
|
nginx:
|
|
enabled: ${APP_FILESERVER_PLATFORM_NGINX_ENABLED:false}
|
|
internal-prefix: ${APP_FILESERVER_PLATFORM_NGINX_INTERNAL_PREFIX:/__files/}
|
|
object-suffix: ${APP_FILESERVER_PLATFORM_NGINX_OBJECT_SUFFIX:.bin}
|
|
minimum-size: ${APP_FILESERVER_PLATFORM_NGINX_MINIMUM_SIZE:16MB}
|
|
observability:
|
|
metrics-enabled: ${APP_FILESERVER_PLATFORM_OBSERVABILITY_METRICS_ENABLED:true}
|
|
# Secret. Keyed HMAC over file identifiers; an unkeyed digest of an enumerable id is
|
|
# reversible, so startup fails while metrics are on and this is blank.
|
|
fingerprint-key: ${APP_FILESERVER_PLATFORM_OBSERVABILITY_FINGERPRINT_KEY:}
|
|
rate-limit:
|
|
# Inbound HTTP enforcement is a separate axis from outbound provider activation.
|
|
# enabled=true with no exact EdgeRateLimitPort fails fast; it never installs a local fallback.
|
|
enabled: ${APP_RATE_LIMIT_ENABLED:false}
|
|
default-policy-id: ${APP_RATE_LIMIT_DEFAULT_POLICY_ID:api-default}
|
|
hash-key-version: ${APP_RATE_LIMIT_HASH_KEY_VERSION:1}
|
|
caller-deadline-budget: 2s
|
|
# remote-addr-only | forwarded-headers-trusted (trusted ingress only)
|
|
client-ip-mode: ${APP_RATE_LIMIT_CLIENT_IP_MODE:remote-addr-only}
|
|
# Logical-cache-name → backendId routing (CacheStoreRouter). No keys by default —
|
|
# forks add e.g. `bindings: { worklog: redis }` or env APP_CACHE_BINDINGS_WORKLOG=redis.
|
|
# A binding to a backend that is not enabled fails startup (Layer 3 moved to router).
|
|
messaging:
|
|
# Active message broker id (e.g. kafka). Blank = messaging disabled (fail-fast on use).
|
|
# Selects the single MessageBroker; adding a broker is new files only (MessagingConfig).
|
|
broker: ${APP_MESSAGING_BROKER}
|
|
kafka:
|
|
# CSV of host:port; required (non-empty) only when broker=kafka. Bound +
|
|
# validated by KafkaAdapterSettings (format) + KafkaAdapterConfig (required-when-active).
|
|
brokers: ${APP_MESSAGING_KAFKA_BROKERS:}
|
|
notification:
|
|
# Active provider id per kind; blank = that kind disabled (fail-fast on use).
|
|
# Add a provider = new files only (NotificationConfig); select it here.
|
|
slack:
|
|
provider: ${APP_NOTIFICATION_SLACK_PROVIDER}
|
|
email:
|
|
provider: ${APP_NOTIFICATION_EMAIL_PROVIDER}
|