Enterprise 版本
SaaS — 深入參考
Enterprise SaaS 模組為以 NextPDF 為基礎的服務提供多租戶的建構元件。
TenantContext是一個不可變的身分值物件,僅從已驗證情境解析。ApiKeyGenerator與ApiKeyAuthenticator負責簽發並驗證帶前綴、帶 checksum、以雜湊儲存的 API key。QuotaChecker依逐租戶配額對請求設下閘門:80% 時警告、100% 時拒絕、用量未知時 fail-closed 拒絕。SidecarJwtMinter為元件間呼叫簽鑄短效的 HS256 服務 token。UsageMeter與StripeMeteringSyncer拉取用量事件,並以確定性的冪等性將其同步至計費供應商。
可用性與授權
標題為「可用性與授權」的區段此能力隨附於 NextPDF Enterprise(nextpdf/enterprise),並在具備 Enterprise 層級授權信封時啟用。缺少該授權的部署不會載入此能力的類別。比較版本並取得授權。
SaaS 介面是一項基礎 Enterprise 能力;不存在獨立的逐功能旗標。NextPDF Core(Apache-2.0)與 NextPDF Pro 沒有租戶、API-key 或配額模型;此能力沒有較低層級的對應。
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() | 無 | 固定的 default 租戶,帶有 read、write、admin | TenantContext | 無 | 單租戶部署 |
ApiKeyAuthenticator::authenticate() | string $rawKey | 六步驗證,接著解析情境 | TenantContext | ApiKeyAuthenticationException(HTTP 401) | 情境的 source 為 api_key;範圍由金鑰紀錄複製而來 |
ApiKeyAuthenticator::requireScope() | TenantContext $context、ApiKeyScope $requiredScope | 明確的範圍斷言 | void | ApiKeyAuthenticationException::insufficientScope()(HTTP 403) | 範圍強制是一個獨立而明確的步驟 |
ApiKeyGenerator::generateLive() / ::generateTest() | 無 | 新金鑰:前綴、32 字元的 base62 主體(192-bit 熵)、4 字元 checksum | array{key, hash, prefix} | 無 | 前綴 npf_live_ / npf_test_;hash 是儲存用摘要 |
ApiKeyGenerator::validateChecksum() | string $key | 前綴、長度與 CRC32-checksum 形態檢查 | bool | 無 | 任何資料存放區查詢之前的打字錯誤防護;不是安全控制 |
ApiKeyGenerator::hashKey()(靜態) | string $key | 原始金鑰的 SHA-256 十六進位摘要 | string | 無 | 金鑰唯一被儲存的表示形式 |
ApiKeyGenerator::isLiveKey() / ::isTestKey() | string $key | 前綴檢視 | bool | 無 | 不必查詢即可看出環境 |
ApiKey | id、租戶、金鑰雜湊、顯示前綴、範圍遮罩、建立/到期/撤銷時點 | 已儲存的金鑰紀錄;明文永不留存 | 值物件 | 無 | isActive()、isRevoked()、isExpired()、scopeNames() |
ApiKeyScope | 帶值列舉:Read = 1、Write = 2、Admin = 4 | 位元遮罩範圍模型 | 列舉 | 無 | maskFromNames()、fromName()、fullAccess();遮罩建構器會忽略未知的名稱 |
ApiKeyRepositoryInterface | — | 儲存合約;僅以雜湊留存 | — | 由實作定義 | findByHash()、findActiveByTenant()、store()、revoke() |
SidecarJwtMinter::__construct() | string $secret、issuer、audience、int $ttlSeconds = 300 | 在建構時拒絕小於 16 bytes 的簽章密鑰 | 實例 | InvalidArgumentException | 128-bit 金鑰強度下限;建議 32 bytes 以上的隨機位元組 |
SidecarJwtMinter::mint() | TenantContext $tenant | 帶有 iss、aud、sub、scope、tenant_id、iat、exp、jti 的 HS256 JWT | string | claim 編碼失敗時拋出 JsonException | 預設五分鐘生命週期;jti 為 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 | 可解析的零具權威性;未知的用量會拋出例外 |
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-key 驗證以固定順序進行:checksum、SHA-256 雜湊、儲存庫查詢、撤銷檢查、到期檢查、情境解析。未知、已撤銷與已過期的金鑰是三種各自獨立的結果,全部為 HTTP 401;範圍不足則為 HTTP 403。
- 金鑰保密。 原始金鑰絕不儲存或記錄;只有其 SHA-256 摘要會被留存並查詢。驗證器本身不執行任何逐位元組的密鑰比較;時序安全的摘要查詢是儲存庫實作的合約。
- 配額門檻。 在 80% 軟上限時,請求會繼續進行、回傳警告百分比,並觸發警示回呼。在 100% 硬上限時,請求會以
SPEC-QUOTA-001(HTTP 402)被拒絕,並攜帶重設時點——下個月第一天的午夜 UTC。 - 配額 fail-closed。 無法判定的用量會以
SPEC-QUOTA-503(HTTP 503,可重試)拒絕請求。未知的用量絕不被視為零。真正、可解析的零用量具權威性並允許通過。 - 警示去重。 檢查器不對警示去重;逐期去重是回呼的責任。
- 計量同步。 此週期採排程,絕不在請求路徑上。它從逐來源的 watermark 續行,並將每個游標推進到最後一個成功送出的事件身分。冪等鍵是確定性的——租戶、期間、事件身分——因此重送的事件會在供應商端的去重中收斂。
- 拉取失敗。 失敗的拉取會回傳一個保留 watermark 的無操作週期(
sent為 0、failed為 0);下一個週期會重試同一個視窗,而不是略過它。 - 服務 token。 token 採用帶共用密鑰的 HS256,並攜帶
iss、aud、sub、scope、tenant_id、iat、exp,以及一個唯一的jti。預設生命週期為五分鐘。建構會 fail-closed 地拒絕小於 16 bytes 的密鑰。
邊界案例與失敗模式
標題為「邊界案例與失敗模式」的區段- 格式錯誤的金鑰會未通過 checksum,並在任何資料存放區存取之前被拒絕。格式正確但未知的金鑰會在查詢後被拒絕。兩者都以無效金鑰的結果呈現。
- 未知、已撤銷與已過期的金鑰使用各自獨立的例外工廠;
keyExpired旗標只在已過期的結果為 true。請將它們對應到各自獨立的客戶端回應。 QuotaChecker::check()只在允許通過時回傳;回傳的allowed永遠為true。拒絕與不可用都是例外的結果。TenantQuota::usagePercentage()在配額為非正值時回傳0.0;fromConfig()會以預設值取代缺失的值,並將整數上限夾制到至少 1。- watermark 是逐來源的;缺失的 watermark 會從該來源串流的開頭開始(游標
0)。多來源部署會維護各自獨立的 watermark。 - 轉換會略過非陣列事件、操作或租戶缺失或為空的事件、非正值,或未對應的操作——而不會使週期失敗。缺少可用正整數身分的事件會伴隨一則警告被拒絕:隨機的後備鍵會破壞供應商端的去重,並可能對租戶重複計費。
- 連續十次送出失敗會升級為一則 critical 日誌項目;計數器會在任何一次成功送出時重設。每個失敗的事件仍會抵達死信回呼。
- 來自用量來源主機的格式錯誤 JSON 主體會產生一個空的事件清單,而非週期失敗。
pullUsage()只在每個已設定的主機都無法連線時才拋出例外。
FIPS 模式行為
標題為「FIPS 模式行為」的區段- 摘要與 MAC 原語透過主機 PHP 密碼學供應者採用 SHA-256 與 HMAC-SHA256。FIPS 受限的建置在遇到非核准演算法時會 fail closed,而非降級;SaaS 層不另加任何自有的密碼學原則。
- 金鑰主體與 token 識別碼來自 CSPRNG(
random_int()、random_bytes())。 - CRC32 checksum 不是密碼學控制,且不受 FIPS 模式影響。
一致性
標題為「一致性」的區段以下陳述描述本能力對照所引用條款的表現。它們不是認證聲明;NextPDF 未針對此模組持有任何認證。
| 行為 | 參考 |
|---|---|
服務 token 的 exp not-after 語意 | RFC 7519 §4.1.4 |
| 服務 token 的 JWS compact serialization | RFC 7515 §3.1 |
| 16-byte HS256 密鑰下限;不以人類可記憶的密碼作為 MAC 金鑰 | RFC 8725 §3.5(威脅:§2.2) |
| 儲存庫摘要查詢的時序安全合約 | OWASP ASVS 5.0 §11.2.4 |
| API-key 儲存摘要 SHA-256 | FIPS 180-4(程式碼宣告) |
RFC 8725 與 OWASP ASVS 5.0 的引用經 RAG 驗證;完整的 reference identifier 記錄於本頁的 frontmatter。FIPS 180-4、FIPS 198-1 與 BSI TR-02102-1 的參考是在產品原始碼中由程式碼宣告的(hash('sha256', …) 與 minter 所記載的金鑰下限);本頁並未從 RAG 語料庫取得它們。ASVS §11.2.4 的時序安全要求約束的是操作者所提供的儲存庫實作,而非驗證器類別本身。
開發注意事項
標題為「開發注意事項」的區段- 請提供
ApiKeyRepositoryInterface與StripeAdapterInterface的持久化實作;此套件隨附的是合約與一個 PSR-18 供應商客戶端,而非持久化。 - 相依項僅為 PSR 抽象:PSR-3 logger、PSR-18 HTTP client、PSR-17 request 與 stream factory。不需要任何供應商 SDK。
- 請將計量同步作為排程工作執行。每個週期後請持久化地留存回傳的 watermark。
- 請將配額警告百分比呈現給客戶端,例如作為警告標頭,並在回呼中逐期對配額警示去重。
- 請從設定提供 token minter 的密鑰,作為高熵的隨機值;建議使用 32 bytes 以上的隨機位元組。絕不要從密碼衍生它。
- 金鑰前綴讓環境不必查詢就能看出;sandbox 與 production 金鑰絕不會相撞,因為前綴會參與到儲存的摘要中。
- 內部機制細節保留在原始碼儲存庫的內部文件中,不在本手冊的範圍內。
發佈邊界
標題為「發佈邊界」的區段本頁僅記載外部可觀察的行為與受支援的公開 API 介面。內部命名空間路徑、輔助類別、機制表格、runbook 檔名與工單前綴都不在範圍內。