Lewati ke konten
getnextpdf.com

Enterprise edisi

SaaS — Referensi Mendalam

Modul Enterprise SaaS menyediakan blok penyusun multi-tenant untuk sebuah layanan berbasis NextPDF.

  • TenantContext adalah value object identitas yang immutable, hanya di-resolve dari konteks yang terautentikasi.
  • ApiKeyGenerator dan ApiKeyAuthenticator menerbitkan dan memvalidasi API key yang berprefiks, ber-checksum, dan disimpan sebagai hash.
  • QuotaChecker menggerbangi permintaan terhadap kuota per-tenant: warn pada 80%, reject pada 100%, deny fail-closed saat penggunaan tidak diketahui.
  • SidecarJwtMinter mencetak service token HS256 berumur pendek untuk panggilan antar-komponen.
  • UsageMeter dan StripeMeteringSyncer menarik event penggunaan dan menyinkronkannya ke penyedia billing dengan idempotensi yang deterministik.

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.

Terminal window
composer require nextpdf/enterprise:^3

Semua simbol berada di bawah NextPDF\Enterprise\SaaS.

SymbolParametersDefault behaviorReturnsThrows or fails withNotes
TenantContextstring $tenantId, string $source, array $scopes = ['read']Value object identitas yang immutablevalue objectTidak adaSumber: jwt, mtls, api_key; hasScope() / hasAnyScope() menguji scope
TenantContext::singleTenant()tidak adaTenant default tetap dengan read, write, adminTenantContextTidak adaDeployment single-tenant
ApiKeyAuthenticator::authenticate()string $rawKeyValidasi enam langkah, lalu resolusi konteksTenantContextApiKeyAuthenticationException (HTTP 401)source konteks adalah api_key; scope disalin dari rekaman key
ApiKeyAuthenticator::requireScope()TenantContext $context, ApiKeyScope $requiredScopeAsersi scope eksplisitvoidApiKeyAuthenticationException::insufficientScope() (HTTP 403)Penegakan scope adalah langkah eksplisit yang terpisah
ApiKeyGenerator::generateLive() / ::generateTest()tidak adaKey baru: prefiks, body base62 32-karakter (entropi 192-bit), checksum 4-karakterarray{key, hash, prefix}Tidak adaPrefiks npf_live_ / npf_test_; hash adalah digest penyimpanan
ApiKeyGenerator::validateChecksum()string $keyPemeriksaan bentuk prefiks, panjang, dan CRC32-checksumboolTidak adaPelindung typo sebelum pencarian datastore apa pun; bukan kontrol keamanan
ApiKeyGenerator::hashKey() (static)string $keyDigest hex SHA-256 dari raw keystringTidak adaSatu-satunya representasi key yang disimpan
ApiKeyGenerator::isLiveKey() / ::isTestKey()string $keyInspeksi prefiksboolTidak adaEnvironment terlihat tanpa pencarian
ApiKeyid, tenant, hash key, prefiks tampilan, mask scope, instant created/expires/revokedRekaman key tersimpan; plaintext tidak pernah dipersistensikanvalue objectTidak adaisActive(), isRevoked(), isExpired(), scopeNames()
ApiKeyScopebacked enum: Read = 1, Write = 2, Admin = 4Model scope bitmaskenumTidak adamaskFromNames(), fromName(), fullAccess(); nama tak dikenal diabaikan oleh pembangun mask
ApiKeyRepositoryInterfaceKontrak penyimpanan; persistensi hash-onlyDitentukan-implementasifindByHash(), findActiveByTenant(), store(), revoke()
SidecarJwtMinter::__construct()string $secret, issuer, audience, int $ttlSeconds = 300Menolak signing secret di bawah 16 byte saat konstruksiinstanceInvalidArgumentExceptionBatas bawah kekuatan-key 128-bit; disarankan 32 byte acak atau lebih
SidecarJwtMinter::mint()TenantContext $tenantJWT HS256 dengan iss, aud, sub, scope, tenant_id, iat, exp, jtistringJsonException pada kegagalan encoding klaimUmur default lima menit; jti adalah 16 byte acak, ter-encode hex
QuotaChecker::check()TenantContext $tenant, TenantQuota $quotaMembaca penggunaan saat ini; warn pada 80%; reject pada 100%; deny saat penggunaan tidak diketahuiarray{allowed: bool, warning_percentage: float|null}QuotaExceededException, QuotaUnavailableExceptionCallback alert dipanggil pada kedua ambang
TenantQuotafloat $maxCuPerPeriod, collections, storage byte, concurrent jobsBatas per-periode; konstanta soft-threshold 80%value objectTidak adaDefault fromConfig(): 10.000 CU, 100 collections, 10 GB, 10 jobs
QuotaExceededException::toErrorEnvelope()tidak adaError envelope SPEC-QUOTA-001arrayHTTP 402, tidak retryable; membawa current, limit, dan instant reset
QuotaUnavailableException::toErrorEnvelope()tidak adaError envelope SPEC-QUOTA-503arrayHTTP 503, retryable; alasan usage_undeterminable
UsageMeter::pullUsage()array<string, int> $watermarksMelakukan polling ke setiap host sumber-penggunaan terkonfigurasi dari cursor-nyaarray{events, instance_id}UsageMeterException saat setiap host tak terjangkauGangguan parsial ditoleransi; host tak terjangkau dicatat dan dilewati
UsageMeter::getCurrentUsage()string $tenantIdPenggunaan compute-unit periode saat inifloatUsageMeterException saat penggunaan tak dapat ditentukanNol yang dapat di-parse bersifat otoritatif; penggunaan tak diketahui melempar exception
StripeMeteringSyncer::sync()array<string, int> $watermarksSatu siklus pull, transform, sendarray{watermarks, sent, failed}Tidak ada; kegagalan send dialihkan ke callback DLQKegagalan pull mengembalikan siklus no-op yang mempertahankan cursor
StripeAdapter::sendMeterEvent()MeterEvent $eventPOST ke penyedia dengan header idempotensivoidStripeSyncExceptionHTTP 429 dan 5xx retryable; 4xx lain tidak retryable
StripeAdapter::sendBatch()list<MeterEvent> $eventsMengirim setiap event; mengumpulkan kegagalanlist<StripeSyncException>Tidak adaList kosong berarti setiap event berhasil
MeterEventnama meter, tenant, value, idempotency key, timestampValue object meter-event yang immutablevalue objectTidak adatoStripePayload() 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 {}
}
  • 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 konteks default tetap 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 (sent 0, failed 0) 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, dan jti yang unik. Umur default lima menit. Konstruksi menolak secret di bawah 16 byte, fail-closed.
  • 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 keyExpired bernilai true hanya pada hasil kedaluwarsa. Petakan mereka ke respons klien yang berbeda.
  • QuotaChecker::check() hanya kembali saat diizinkan; allowed yang dikembalikan selalu true. Penolakan dan ketidaktersediaan adalah hasil eksepsional.
  • TenantQuota::usagePercentage() mengembalikan 0.0 untuk 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.
  • 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.

Pernyataan di bawah menjelaskan kapabilitas terhadap klausul yang dikutip. Ini bukan klaim sertifikasi; NextPDF tidak memegang sertifikasi untuk modul ini.

BehaviorReference
Semantik not-after exp service-tokenRFC 7519 §4.1.4
Serialisasi compact JWS service-tokenRFC 7515 §3.1
Batas bawah secret HS256 16-byte; tidak ada password yang mudah-diingat-manusia sebagai key MACRFC 8725 §3.5 (ancaman: §2.2)
Kontrak pencarian-digest constant-time repositoryOWASP ASVS 5.0 §11.2.4
Digest penyimpanan API-key SHA-256FIPS 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.

  • Sediakan implementasi durabel dari ApiKeyRepositoryInterface dan StripeAdapterInterface; 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.

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.