Enterprise edisi
SaaS — Referensi Mendalam
Sekilas
Bagian berjudul “Sekilas”Modul Enterprise SaaS menyediakan blok penyusun multi-tenant untuk sebuah layanan berbasis NextPDF.
TenantContextadalah value object identitas yang immutable, hanya di-resolve dari konteks yang terautentikasi.ApiKeyGeneratordanApiKeyAuthenticatormenerbitkan dan memvalidasi API key yang berprefiks, ber-checksum, dan disimpan sebagai hash.QuotaCheckermenggerbangi permintaan terhadap kuota per-tenant: warn pada 80%, reject pada 100%, deny fail-closed saat penggunaan tidak diketahui.SidecarJwtMintermencetak service token HS256 berumur pendek untuk panggilan antar-komponen.UsageMeterdanStripeMeteringSyncermenarik event penggunaan dan menyinkronkannya ke penyedia billing dengan idempotensi yang deterministik.
Ketersediaan & lisensi
Bagian berjudul “Ketersediaan & lisensi”Kapabilitas ini dikirim dalam NextPDF Enterprise (nextpdf/enterprise) dan aktif dengan envelope lisensi tier Enterprise. Deployment tanpa entitlement tersebut tidak memuat kelas-kelas kapabilitas ini. Bandingkan edisi dan dapatkan lisensi.
Permukaan SaaS adalah kapabilitas dasar Enterprise; tidak ada flag per-fitur terpisah. NextPDF Core (Apache-2.0) dan NextPDF Pro tidak memiliki model tenancy, API-key, atau kuota; kapabilitas ini tidak memiliki padanan di tier yang lebih rendah.
composer require nextpdf/enterprise:^3Permukaan API publik
Bagian berjudul “Permukaan API publik”Semua simbol berada di bawah NextPDF\Enterprise\SaaS.
| Symbol | Parameters | Default behavior | Returns | Throws or fails with | Notes |
|---|---|---|---|---|---|
TenantContext | string $tenantId, string $source, array $scopes = ['read'] | Value object identitas yang immutable | value object | Tidak ada | Sumber: jwt, mtls, api_key; hasScope() / hasAnyScope() menguji scope |
TenantContext::singleTenant() | tidak ada | Tenant default tetap dengan read, write, admin | TenantContext | Tidak ada | Deployment single-tenant |
ApiKeyAuthenticator::authenticate() | string $rawKey | Validasi enam langkah, lalu resolusi konteks | TenantContext | ApiKeyAuthenticationException (HTTP 401) | source konteks adalah api_key; scope disalin dari rekaman key |
ApiKeyAuthenticator::requireScope() | TenantContext $context, ApiKeyScope $requiredScope | Asersi scope eksplisit | void | ApiKeyAuthenticationException::insufficientScope() (HTTP 403) | Penegakan scope adalah langkah eksplisit yang terpisah |
ApiKeyGenerator::generateLive() / ::generateTest() | tidak ada | Key baru: prefiks, body base62 32-karakter (entropi 192-bit), checksum 4-karakter | array{key, hash, prefix} | Tidak ada | Prefiks npf_live_ / npf_test_; hash adalah digest penyimpanan |
ApiKeyGenerator::validateChecksum() | string $key | Pemeriksaan bentuk prefiks, panjang, dan CRC32-checksum | bool | Tidak ada | Pelindung typo sebelum pencarian datastore apa pun; bukan kontrol keamanan |
ApiKeyGenerator::hashKey() (static) | string $key | Digest hex SHA-256 dari raw key | string | Tidak ada | Satu-satunya representasi key yang disimpan |
ApiKeyGenerator::isLiveKey() / ::isTestKey() | string $key | Inspeksi prefiks | bool | Tidak ada | Environment terlihat tanpa pencarian |
ApiKey | id, tenant, hash key, prefiks tampilan, mask scope, instant created/expires/revoked | Rekaman key tersimpan; plaintext tidak pernah dipersistensikan | value object | Tidak ada | isActive(), isRevoked(), isExpired(), scopeNames() |
ApiKeyScope | backed enum: Read = 1, Write = 2, Admin = 4 | Model scope bitmask | enum | Tidak ada | maskFromNames(), fromName(), fullAccess(); nama tak dikenal diabaikan oleh pembangun mask |
ApiKeyRepositoryInterface | — | Kontrak penyimpanan; persistensi hash-only | — | Ditentukan-implementasi | findByHash(), findActiveByTenant(), store(), revoke() |
SidecarJwtMinter::__construct() | string $secret, issuer, audience, int $ttlSeconds = 300 | Menolak signing secret di bawah 16 byte saat konstruksi | instance | InvalidArgumentException | Batas bawah kekuatan-key 128-bit; disarankan 32 byte acak atau lebih |
SidecarJwtMinter::mint() | TenantContext $tenant | JWT HS256 dengan iss, aud, sub, scope, tenant_id, iat, exp, jti | string | JsonException pada kegagalan encoding klaim | Umur default lima menit; jti adalah 16 byte acak, ter-encode hex |
QuotaChecker::check() | TenantContext $tenant, TenantQuota $quota | Membaca penggunaan saat ini; warn pada 80%; reject pada 100%; deny saat penggunaan tidak diketahui | array{allowed: bool, warning_percentage: float|null} | QuotaExceededException, QuotaUnavailableException | Callback alert dipanggil pada kedua ambang |
TenantQuota | float $maxCuPerPeriod, collections, storage byte, concurrent jobs | Batas per-periode; konstanta soft-threshold 80% | value object | Tidak ada | Default fromConfig(): 10.000 CU, 100 collections, 10 GB, 10 jobs |
QuotaExceededException::toErrorEnvelope() | tidak ada | Error envelope SPEC-QUOTA-001 | array | — | HTTP 402, tidak retryable; membawa current, limit, dan instant reset |
QuotaUnavailableException::toErrorEnvelope() | tidak ada | Error envelope SPEC-QUOTA-503 | array | — | HTTP 503, retryable; alasan usage_undeterminable |
UsageMeter::pullUsage() | array<string, int> $watermarks | Melakukan polling ke setiap host sumber-penggunaan terkonfigurasi dari cursor-nya | array{events, instance_id} | UsageMeterException saat setiap host tak terjangkau | Gangguan parsial ditoleransi; host tak terjangkau dicatat dan dilewati |
UsageMeter::getCurrentUsage() | string $tenantId | Penggunaan compute-unit periode saat ini | float | UsageMeterException saat penggunaan tak dapat ditentukan | Nol yang dapat di-parse bersifat otoritatif; penggunaan tak diketahui melempar exception |
StripeMeteringSyncer::sync() | array<string, int> $watermarks | Satu siklus pull, transform, send | array{watermarks, sent, failed} | Tidak ada; kegagalan send dialihkan ke callback DLQ | Kegagalan pull mengembalikan siklus no-op yang mempertahankan cursor |
StripeAdapter::sendMeterEvent() | MeterEvent $event | POST ke penyedia dengan header idempotensi | void | StripeSyncException | HTTP 429 dan 5xx retryable; 4xx lain tidak retryable |
StripeAdapter::sendBatch() | list<MeterEvent> $events | Mengirim setiap event; mengumpulkan kegagalan | list<StripeSyncException> | Tidak ada | List kosong berarti setiap event berhasil |
MeterEvent | nama meter, tenant, value, idempotency key, timestamp | Value object meter-event yang immutable | value object | Tidak ada | toStripePayload() menserialisasi payload penyedia |
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 {}}Kontrak perilaku
Bagian berjudul “Kontrak perilaku”- Identitas tenant. Konteks tenant bersifat immutable: identifier tenant, sumber resolusi, scope. Identitas hanya di-resolve dari konteks yang terautentikasi (
jwt,mtls,api_key) — tidak pernah dari header atau parameter query yang dipasok klien. Deployment single-tenant menggunakan konteksdefaulttetap dengan scope penuh. - Urutan autentikasi. Autentikasi API-key berjalan dalam urutan tetap: checksum, hash SHA-256, pencarian repository, pemeriksaan revocation, pemeriksaan kedaluwarsa, resolusi konteks. Key yang tak dikenal, di-revoke, dan kedaluwarsa adalah tiga hasil berbeda, semuanya HTTP 401; scope yang tidak mencukupi adalah HTTP 403.
- Kerahasiaan key. Raw key tidak pernah disimpan atau dicatat; hanya digest SHA-256-nya yang dipersistensikan dan dicari. Authenticator tidak melakukan perbandingan secret byte-per-byte sendiri; pencarian digest constant-time adalah kontrak implementasi repository.
- Ambang kuota. Pada batas soft 80% permintaan tetap berlanjut, persentase peringatan dikembalikan, dan callback alert dijalankan. Pada batas hard 100% permintaan ditolak dengan
SPEC-QUOTA-001(HTTP 402) yang membawa instant reset — hari pertama bulan berikutnya, tengah malam UTC. - Kuota fail-closed. Penggunaan yang tak dapat ditentukan menolak permintaan dengan
SPEC-QUOTA-503(HTTP 503, retryable). Penggunaan tak diketahui tidak pernah diperlakukan sebagai nol. Penggunaan nol yang asli dan dapat di-parse bersifat otoritatif dan diizinkan. - Deduplikasi alert. Checker tidak melakukan deduplikasi alert; deduplikasi per-periode adalah tanggung jawab callback.
- Sinkronisasi metering. Siklus terjadwal, tidak pernah pada jalur request. Ia melanjutkan dari watermark per-sumber dan memajukan setiap cursor ke identitas event tertinggi yang berhasil dikirim. Idempotency key bersifat deterministik — tenant, periode, identitas event — sehingga event yang dikirim ulang runtuh pada deduplikasi sisi-penyedia.
- Kegagalan pull. Pull yang gagal mengembalikan siklus no-op (
sent0,failed0) yang mempertahankan watermark; siklus berikutnya mencoba ulang window yang sama alih-alih melewatkannya. - Service token. Token bersifat HS256 dengan shared secret dan membawa
iss,aud,sub,scope,tenant_id,iat,exp, danjtiyang unik. Umur default lima menit. Konstruksi menolak secret di bawah 16 byte, fail-closed.
Kasus tepi & mode kegagalan
Bagian berjudul “Kasus tepi & mode kegagalan”- Key yang malformed gagal checksum dan ditolak sebelum akses datastore apa pun. Key yang well-formed tetapi tak dikenal ditolak setelah pencarian. Keduanya muncul sebagai hasil key-tidak-valid.
- Key yang tak dikenal, di-revoke, dan kedaluwarsa menggunakan factory exception yang berbeda; flag
keyExpiredbernilai true hanya pada hasil kedaluwarsa. Petakan mereka ke respons klien yang berbeda. QuotaChecker::check()hanya kembali saat diizinkan;allowedyang dikembalikan selalutrue. Penolakan dan ketidaktersediaan adalah hasil eksepsional.TenantQuota::usagePercentage()mengembalikan0.0untuk kuota non-positif;fromConfig()menggantikan default untuk nilai yang absen dan menjepit batas integer hingga minimal 1.- Watermark bersifat per-sumber; watermark yang hilang dimulai dari awal stream sumber tersebut (cursor
0). Deployment multi-sumber memelihara watermark yang independen. - Transform melewati event non-array, event dengan operasi atau tenant yang hilang atau kosong, value non-positif, atau operasi yang tak terpetakan — tanpa menggagalkan siklus. Event yang kekurangan identitas integer positif yang dapat dipakai ditolak dengan peringatan: key fallback acak akan mematahkan deduplikasi sisi-penyedia dan dapat menagih tenant dua kali.
- Sepuluh kegagalan send berturut-turut naik menjadi entri log critical; counter direset pada setiap send yang berhasil. Setiap event yang gagal tetap mencapai callback dead-letter.
- Body JSON yang malformed dari host sumber-penggunaan menghasilkan list event kosong, bukan kegagalan siklus.
pullUsage()hanya melempar exception saat setiap host terkonfigurasi tak terjangkau.
Perilaku mode-FIPS
Bagian berjudul “Perilaku mode-FIPS”- Primitif digest dan MAC adalah SHA-256 dan HMAC-SHA256 melalui penyedia crypto PHP host. Build yang terbatas-FIPS gagal-tertutup pada algoritma yang tak-disetujui alih-alih menurunkan (downgrade); lapisan SaaS tidak menambahkan kebijakan kriptografis miliknya sendiri.
- Body key dan identifier token berasal dari CSPRNG (
random_int(),random_bytes()). - Checksum CRC32 bukan kontrol kriptografis dan tidak terpengaruh oleh mode FIPS.
Konformansi
Bagian berjudul “Konformansi”Pernyataan di bawah menjelaskan kapabilitas terhadap klausul yang dikutip. Ini bukan klaim sertifikasi; NextPDF tidak memegang sertifikasi untuk modul ini.
| Behavior | Reference |
|---|---|
Semantik not-after exp service-token | RFC 7519 §4.1.4 |
| Serialisasi compact JWS service-token | RFC 7515 §3.1 |
| Batas bawah secret HS256 16-byte; tidak ada password yang mudah-diingat-manusia sebagai key MAC | RFC 8725 §3.5 (ancaman: §2.2) |
| Kontrak pencarian-digest constant-time repository | OWASP ASVS 5.0 §11.2.4 |
| Digest penyimpanan API-key SHA-256 | FIPS 180-4 (code-declared) |
Kutipan RFC 8725 dan OWASP ASVS 5.0 telah diverifikasi-RAG; identifier referensi lengkap tercatat dalam frontmatter halaman ini. Referensi FIPS 180-4, FIPS 198-1, dan BSI TR-02102-1 adalah code-declared dalam source produk (hash('sha256', …) dan batas bawah key yang terdokumentasi pada minter); referensi tersebut tidak diambil dari korpus RAG untuk halaman ini. Persyaratan constant-time ASVS §11.2.4 mengikat implementasi repository yang dipasok operator, bukan kelas authenticator itu sendiri.
Catatan pengembangan
Bagian berjudul “Catatan pengembangan”- Sediakan implementasi durabel dari
ApiKeyRepositoryInterfacedanStripeAdapterInterface; paket ini mengirim kontrak dan sebuah client penyedia PSR-18, bukan persistensi. - Dependensi hanyalah abstraksi PSR: logger PSR-3, HTTP client PSR-18, factory request dan stream PSR-17. Tidak ada SDK penyedia yang diperlukan.
- Jalankan sinkronisasi metering sebagai job terjadwal. Persistensikan watermark yang dikembalikan secara durabel setelah setiap siklus.
- Ungkapkan persentase peringatan kuota ke klien, misalnya sebagai warning header, dan deduplikasi alert kuota per periode di dalam callback.
- Pasok secret token-minter dari konfigurasi sebagai nilai acak berentropi tinggi; 32 byte acak atau lebih disarankan. Jangan pernah menurunkannya dari sebuah password.
- Prefiks key membuat environment terlihat tanpa pencarian; key sandbox dan produksi tidak pernah bertabrakan karena prefiks turut serta dalam digest tersimpan.
- Detail mekanisme internal tetap berada di dokumentasi internal repository source dan berada di luar ruang lingkup manual ini.
Batas publikasi
Bagian berjudul “Batas publikasi”Halaman ini mendokumentasikan hanya perilaku yang dapat diamati secara eksternal dan permukaan API publik yang didukung. Path namespace internal, kelas helper, tabel mekanisme, nama file runbook, dan prefiks tiket berada di luar ruang lingkup.