Enterprise 에디션
SaaS — 심층 참조
한눈에 보기
섹션 제목: “한눈에 보기”Enterprise SaaS 모듈은 NextPDF 기반 서비스를 위한 멀티테넌트 구성 요소를 제공합니다.
TenantContext는 인증된 컨텍스트에서만 해석되는 불변 신원 값 객체입니다.ApiKeyGenerator와ApiKeyAuthenticator는 접두사가 붙고 체크섬이 있으며 해시로 저장되는 API 키를 발급하고 검증합니다.QuotaChecker는 테넌트별 할당량에 대해 요청을 게이트합니다. 80%에서 경고하고, 100%에서 거부하며, 사용량을 알 수 없을 때는 실패-차단으로 거부합니다.SidecarJwtMinter는 컴포넌트 간 호출을 위한 단기 HS256 서비스 토큰을 발행합니다.UsageMeter와StripeMeteringSyncer는 사용량 이벤트를 풀(pull)하여 결정적 멱등성으로 청구 공급자에 동기화합니다.
가용성 및 라이선싱
섹션 제목: “가용성 및 라이선싱”이 역량은 NextPDF Enterprise(nextpdf/enterprise)에 포함되어 제공되며 Enterprise 등급 라이선스 엔벨로프로 활성화됩니다. 해당 권한이 없는 배포는 이 역량의 클래스를 로드하지 않습니다. 에디션 비교 및 라이선스 받기.
SaaS 표면은 기본 Enterprise 역량이며, 별도의 기능별 플래그는 없습니다. NextPDF Core(Apache-2.0)와 NextPDF Pro에는 테넌시, API 키, 할당량 모델이 없습니다. 이 역량에는 하위 등급에 해당하는 것이 없습니다.
composer require nextpdf/enterprise:^3공개 API 표면
섹션 제목: “공개 API 표면”모든 심볼은 NextPDF\Enterprise\SaaS 아래에 있습니다.
| 심볼 | 파라미터 | 기본 동작 | 반환 | 예외 또는 실패 조건 | 비고 |
|---|---|---|---|---|---|
TenantContext | string $tenantId, string $source, array $scopes = ['read'] | 불변 신원 값 객체 | 값 객체 | 없음 | 출처: jwt, mtls, api_key; hasScope() / hasAnyScope()로 스코프 검사 |
TenantContext::singleTenant() | 없음 | read, write, admin을 가진 고정 default 테넌트 | TenantContext | 없음 | 단일 테넌트 배포 |
ApiKeyAuthenticator::authenticate() | string $rawKey | 6단계 검증 후 컨텍스트 해석 | TenantContext | ApiKeyAuthenticationException (HTTP 401) | 컨텍스트 source는 api_key; 스코프는 키 레코드에서 복사됨 |
ApiKeyAuthenticator::requireScope() | TenantContext $context, ApiKeyScope $requiredScope | 명시적 스코프 단언 | void | ApiKeyAuthenticationException::insufficientScope() (HTTP 403) | 스코프 적용은 별개의 명시적 단계 |
ApiKeyGenerator::generateLive() / ::generateTest() | 없음 | 새 키: 접두사, 32자 base62 본문(192비트 엔트로피), 4자 체크섬 | array{key, hash, prefix} | 없음 | 접두사 npf_live_ / npf_test_; hash는 저장 다이제스트 |
ApiKeyGenerator::validateChecksum() | string $key | 접두사, 길이, CRC32 체크섬 형태 검사 | bool | 없음 | 데이터스토어 조회 전 오타 방지 장치; 보안 통제가 아님 |
ApiKeyGenerator::hashKey() (정적) | string $key | 원시 키의 SHA-256 16진 다이제스트 | string | 없음 | 키의 유일한 저장 표현 |
ApiKeyGenerator::isLiveKey() / ::isTestKey() | string $key | 접두사 검사 | bool | 없음 | 조회 없이 환경 확인 가능 |
ApiKey | id, 테넌트, 키 해시, 표시 접두사, 스코프 마스크, 생성/만료/폐기 시점 | 저장된 키 레코드; 평문은 절대 저장되지 않음 | 값 객체 | 없음 | isActive(), isRevoked(), isExpired(), scopeNames() |
ApiKeyScope | 백드 enum: Read = 1, Write = 2, Admin = 4 | 비트마스크 스코프 모델 | enum | 없음 | maskFromNames(), fromName(), fullAccess(); 알 수 없는 이름은 마스크 빌더가 무시함 |
ApiKeyRepositoryInterface | — | 저장 계약; 해시 전용 영속화 | — | 구현 정의됨 | findByHash(), findActiveByTenant(), store(), revoke() |
SidecarJwtMinter::__construct() | string $secret, issuer, audience, int $ttlSeconds = 300 | 생성 시 16바이트 미만의 서명 시크릿을 거부함 | 인스턴스 | InvalidArgumentException | 128비트 키 강도 하한; 32바이트 이상의 무작위 바이트 권장 |
SidecarJwtMinter::mint() | TenantContext $tenant | iss, aud, sub, scope, tenant_id, iat, exp, jti를 가진 HS256 JWT | string | 클레임 인코딩 실패 시 JsonException | 기본 수명 5분; jti는 16 무작위 바이트를 16진 인코딩한 값 |
QuotaChecker::check() | TenantContext $tenant, TenantQuota $quota | 현재 사용량을 읽음; 80%에서 경고; 100%에서 거부; 사용량을 알 수 없으면 거부 | array{allowed: bool, warning_percentage: float|null} | QuotaExceededException, QuotaUnavailableException | 두 임계값 모두에서 알림 콜백 호출 |
TenantQuota | float $maxCuPerPeriod, 컬렉션, 저장 바이트, 동시 작업 | 기간별 한도; 80% 소프트 임계값 상수 | 값 객체 | 없음 | fromConfig() 기본값: 10,000 CU, 100 컬렉션, 10 GB, 10 작업 |
QuotaExceededException::toErrorEnvelope() | 없음 | SPEC-QUOTA-001 오류 엔벨로프 | array | — | HTTP 402, 재시도 불가; 현재값, 한도, 리셋 시점을 담음 |
QuotaUnavailableException::toErrorEnvelope() | 없음 | SPEC-QUOTA-503 오류 엔벨로프 | array | — | HTTP 503, 재시도 가능; 사유 usage_undeterminable |
UsageMeter::pullUsage() | array<string, int> $watermarks | 구성된 모든 사용량 출처 호스트를 커서에서부터 폴링 | array{events, instance_id} | 모든 호스트가 도달 불가일 때 UsageMeterException | 부분 장애 허용; 도달 불가 호스트는 로그되고 건너뜀 |
UsageMeter::getCurrentUsage() | string $tenantId | 현재 기간 컴퓨트 유닛 사용량 | float | 사용량을 확정할 수 없을 때 UsageMeterException | 파싱 가능한 0은 권위적; 알 수 없는 사용량은 예외를 던짐 |
StripeMeteringSyncer::sync() | array<string, int> $watermarks | 한 번의 풀, 변환, 전송 주기 | array{watermarks, sent, failed} | 없음; 전송 실패는 DLQ 콜백으로 라우팅됨 | 풀 실패는 커서를 보존하는 무동작 주기를 반환함 |
StripeAdapter::sendMeterEvent() | MeterEvent $event | 멱등성 헤더와 함께 공급자에 POST | void | StripeSyncException | HTTP 429 및 5xx 재시도 가능; 그 외 4xx 재시도 불가 |
StripeAdapter::sendBatch() | list<MeterEvent> $events | 각 이벤트를 전송; 실패를 수집 | list<StripeSyncException> | 없음 | 빈 목록은 모든 이벤트가 성공했음을 의미 |
MeterEvent | 미터 이름, 테넌트, 값, 멱등성 키, 타임스탬프 | 불변 미터 이벤트 값 객체 | 값 객체 | 없음 | toStripePayload()가 공급자 페이로드를 직렬화함 |
final readonly class ApiKeyAuthenticator{ public function __construct( private ApiKeyRepositoryInterface $repository, private ApiKeyGenerator $generator, private LoggerInterface $logger, ) {}
public function authenticate(string $rawKey): TenantContext {}
public function requireScope(TenantContext $context, ApiKeyScope $requiredScope): void {}}final class QuotaChecker{ public function __construct( private readonly UsageMeterInterface $usageMeter, private readonly LoggerInterface $logger, private readonly Closure $quotaAlertCallback, ) {}
/** @return array{allowed: bool, warning_percentage: float|null} */ public function check(TenantContext $tenant, TenantQuota $quota): array {}}interface UsageMeterInterface{ /** @return array<string, mixed> */ public function pullUsage(array $watermarks): array;
public function getCurrentUsage(string $tenantId): float;}final class StripeMeteringSyncer{ public function __construct( private readonly UsageMeterInterface $usageMeter, private readonly StripeAdapterInterface $stripeAdapter, private readonly LoggerInterface $logger, private readonly Closure $dlqCallback, ) {}
/** @return array{watermarks: array<string, int>, sent: int, failed: int} */ public function sync(array $watermarks): array {}}final readonly class SidecarJwtMinter{ public function __construct( private string $secret, private string $issuer = 'nextpdf-enterprise', private string $audience = 'nextpdf-spectrum', private int $ttlSeconds = self::DEFAULT_TTL_SECONDS, ) {}
public function mint(TenantContext $tenant): string {}}동작 계약
섹션 제목: “동작 계약”- 테넌트 신원. 테넌트 컨텍스트는 불변입니다. 즉 테넌트 식별자, 해석 출처, 스코프입니다. 신원은 인증된 컨텍스트에서만(
jwt,mtls,api_key) 해석되며, 클라이언트가 제공한 헤더나 쿼리 파라미터로부터는 결코 해석되지 않습니다. 단일 테넌트 배포는 전체 스코프를 가진 고정default컨텍스트를 사용합니다. - 인증 순서. API 키 인증은 고정된 순서로 진행됩니다. 즉 체크섬, SHA-256 해시, 저장소 조회, 폐기 검사, 만료 검사, 컨텍스트 해석입니다. 알 수 없는 키, 폐기된 키, 만료된 키는 세 가지 별개의 결과이며 모두 HTTP 401입니다. 스코프 부족은 HTTP 403입니다.
- 키 비밀성. 원시 키는 절대 저장되거나 로그에 기록되지 않습니다. 오직 SHA-256 다이제스트만 저장되고 조회됩니다. 인증기 자체는 어떤 바이트 단위 시크릿 비교도 수행하지 않습니다. 상수 시간 다이제스트 조회는 저장소 구현의 계약입니다.
- 할당량 임계값. 80% 소프트 한도에서는 요청이 진행되고, 경고 백분율이 반환되며, 알림 콜백이 발동합니다. 100% 하드 한도에서는 요청이 리셋 시점을 담은
SPEC-QUOTA-001(HTTP 402)로 거부됩니다 — 다음 달 첫째 날, 자정 UTC입니다. - 할당량 실패-차단. 확정할 수 없는 사용량은 요청을
SPEC-QUOTA-503(HTTP 503, 재시도 가능)으로 거부합니다. 알 수 없는 사용량은 결코 0으로 취급되지 않습니다. 진정하고 파싱 가능한 0 사용량은 권위적이며 허용됩니다. - 알림 중복 제거. 검사기는 알림을 중복 제거하지 않습니다. 기간별 중복 제거는 콜백의 책임입니다.
- 미터링 동기화. 주기는 예약되며 결코 요청 경로에 있지 않습니다. 출처별 워터마크에서 재개하며 각 커서를 성공적으로 전송된 가장 높은 이벤트 신원으로 전진시킵니다. 멱등성 키는 결정적이므로 — 테넌트, 기간, 이벤트 신원 — 재전송된 이벤트는 공급자의 중복 제거에서 병합됩니다.
- 풀 실패. 실패한 풀은 워터마크를 보존하는 무동작 주기(
sent0,failed0)를 반환합니다. 다음 주기는 동일한 윈도를 건너뛰지 않고 재시도합니다. - 서비스 토큰. 토큰은 공유 시크릿을 사용한 HS256이며
iss,aud,sub,scope,tenant_id,iat,exp, 그리고 고유한jti를 담습니다. 기본 수명은 5분입니다. 생성은 16바이트 미만의 시크릿을 실패-차단으로 거부합니다.
엣지 케이스 및 실패 모드
섹션 제목: “엣지 케이스 및 실패 모드”- 잘못된 형식의 키는 체크섬에 실패하여 어떤 데이터스토어 접근보다 먼저 거부됩니다. 형식은 올바르지만 알 수 없는 키는 조회 후 거부됩니다. 둘 다 유효하지 않은 키 결과로 나타납니다.
- 알 수 없는 키, 폐기된 키, 만료된 키는 별개의 예외 팩토리를 사용합니다.
keyExpired플래그는 만료 결과에서만 참입니다. 이를 별개의 클라이언트 응답으로 매핑하십시오. QuotaChecker::check()는 허용 시에만 반환합니다. 반환되는allowed는 항상true입니다. 거부와 이용 불가는 예외적 결과입니다.TenantQuota::usagePercentage()는 양수가 아닌 할당량에 대해0.0을 반환합니다.fromConfig()는 없는 값에 기본값을 대체하고 정수 한도를 최소 1로 클램프합니다.- 워터마크는 출처별입니다. 없는 워터마크는 해당 출처 스트림의 처음(커서
0)부터 시작합니다. 다중 출처 배포는 독립적인 워터마크를 유지합니다. - 변환은 배열이 아닌 이벤트, 작업 또는 테넌트가 없거나 빈 이벤트, 양수가 아닌 값, 매핑되지 않은 작업을 — 주기를 실패시키지 않고 — 건너뜁니다. 사용 가능한 양의 정수 신원이 없는 이벤트는 경고와 함께 거부됩니다. 무작위 대체 키는 공급자 측 중복 제거를 무력화하고 테넌트에게 이중 청구할 수 있기 때문입니다.
- 연속 10회 전송 실패는 심각(critical) 로그 항목으로 에스컬레이션합니다. 카운터는 성공적인 전송이 있으면 리셋됩니다. 실패한 모든 이벤트는 여전히 데드 레터 콜백에 도달합니다.
- 사용량 출처 호스트로부터의 잘못된 형식의 JSON 본문은 주기 실패가 아니라 빈 이벤트 목록을 낳습니다.
pullUsage()는 구성된 모든 호스트가 도달 불가일 때만 예외를 던집니다.
FIPS 모드 동작
섹션 제목: “FIPS 모드 동작”- 다이제스트 및 MAC 프리미티브는 호스트 PHP 암호화 공급자를 통한 SHA-256 및 HMAC-SHA256입니다. FIPS 제약 빌드는 비승인 알고리즘에서 다운그레이드하지 않고 실패-차단됩니다. SaaS 계층은 자체 암호화 정책을 추가하지 않습니다.
- 키 본문과 토큰 식별자는 CSPRNG(
random_int(),random_bytes())에서 나옵니다. - CRC32 체크섬은 암호화 통제가 아니며 FIPS 모드의 영향을 받지 않습니다.
준수(Conformance)
섹션 제목: “준수(Conformance)”아래 진술은 인용된 조항에 대한 역량을 설명합니다. 이는 인증 주장이 아니며, NextPDF는 이 모듈에 대해 어떤 인증도 보유하지 않습니다.
| 동작 | 참조 |
|---|---|
서비스 토큰 exp not-after 의미론 | RFC 7519 §4.1.4 |
| 서비스 토큰 JWS 압축 직렬화 | RFC 7515 §3.1 |
| 16바이트 HS256 시크릿 하한; 사람이 기억할 수 있는 비밀번호를 MAC 키로 사용 금지 | RFC 8725 §3.5 (위협: §2.2) |
| 저장소 다이제스트 조회 상수 시간 계약 | OWASP ASVS 5.0 §11.2.4 |
| API 키 저장 다이제스트 SHA-256 | FIPS 180-4 (코드 선언) |
RFC 8725 및 OWASP ASVS 5.0 인용은 RAG 검증되었으며, 전체 참조 식별자는 이 페이지의 프론트매터에 기록되어 있습니다. FIPS 180-4, FIPS 198-1, BSI TR-02102-1 참조는 제품 소스에서 코드로 선언되어 있으며(hash('sha256', …) 및 발행기의 문서화된 키 하한), 이 페이지에서는 RAG 코퍼스에서 검색되지 않았습니다. ASVS §11.2.4의 상수 시간 요구 사항은 인증기 클래스 자체가 아니라 운영자가 제공하는 저장소 구현에 구속됩니다.
개발 노트
섹션 제목: “개발 노트”ApiKeyRepositoryInterface와StripeAdapterInterface의 지속적 구현을 제공하십시오. 패키지는 계약과 PSR-18 공급자 클라이언트를 제공하며, 영속화는 제공하지 않습니다.- 의존성은 PSR 추상화만입니다. 즉 PSR-3 로거, PSR-18 HTTP 클라이언트, PSR-17 요청 및 스트림 팩토리입니다. 공급자 SDK는 필요하지 않습니다.
- 미터링 동기화를 예약 작업으로 실행하십시오. 각 주기 후 반환된 워터마크를 지속적으로 영속화하십시오.
- 할당량 경고 백분율을 클라이언트에 노출하고(예: 경고 헤더로), 콜백에서 할당량 알림을 기간별로 중복 제거하십시오.
- 토큰 발행기 시크릿을 구성에서 고엔트로피 무작위 값으로 공급하십시오. 32바이트 이상의 무작위 바이트를 권장합니다. 결코 비밀번호에서 도출하지 마십시오.
- 키 접두사는 조회 없이 환경을 확인할 수 있게 합니다. 접두사가 저장 다이제스트에 참여하므로 sandbox 키와 production 키는 결코 충돌하지 않습니다.
- 내부 메커니즘 세부 정보는 소스 저장소의 내부 문서에 남아 있으며 이 매뉴얼의 범위 밖입니다.
게시 경계
섹션 제목: “게시 경계”이 페이지는 외부에서 관찰 가능한 동작과 지원되는 공개 API 표면만 문서화합니다. 내부 네임스페이스 경로, 헬퍼 클래스, 메커니즘 표, 런북 파일명, 티켓 접두사는 범위 밖입니다.