ข้ามไปยังเนื้อหา
getnextpdf.com

Enterprise รุ่น

SaaS — เอกสารอ้างอิงเชิงลึก

โมดูล Enterprise SaaS จัดหาองค์ประกอบพื้นฐานแบบ multi-tenant สำหรับบริการที่สร้างบน NextPDF

  • TenantContext เป็น identity value object แบบไม่เปลี่ยนแปลง ที่ resolve จาก authenticated context เท่านั้น
  • ApiKeyGenerator และ ApiKeyAuthenticator ออกและตรวจสอบ API key ที่มี prefix, checksum และจัดเก็บแบบ hash
  • QuotaChecker ควบคุมคำขอตามโควตาต่อ tenant คือเตือนที่ 80%, ปฏิเสธที่ 100% และปฏิเสธแบบ fail-closed เมื่อไม่ทราบการใช้งาน
  • SidecarJwtMinter สร้าง service token แบบ HS256 อายุสั้นสำหรับการเรียกระหว่างคอมโพเนนต์
  • UsageMeter และ StripeMeteringSyncer ดึงเหตุการณ์การใช้งานและ sync ไปยังผู้ให้บริการ billing ด้วย idempotency ที่กำหนดได้แน่นอน

ความสามารถนี้มาใน NextPDF Enterprise (nextpdf/enterprise) และเปิดใช้งานด้วย license envelope ระดับ Enterprise การปรับใช้ที่ไม่มีสิทธิ์ดังกล่าวจะไม่โหลดคลาสของความสามารถนี้ เปรียบเทียบรุ่นและขอรับสิทธิ์การใช้งาน

พื้นผิว SaaS เป็นความสามารถพื้นฐานของ Enterprise ไม่มีแฟล็กต่อฟีเจอร์แยกต่างหาก NextPDF Core (Apache-2.0) และ NextPDF Pro ไม่มีแบบจำลอง tenancy, API-key หรือโควตา ความสามารถนี้ไม่มีสิ่งเทียบเท่าในระดับชั้นที่ต่ำกว่า

Terminal window
composer require nextpdf/enterprise:^3

สัญลักษณ์ทั้งหมดอยู่ภายใต้ NextPDF\Enterprise\SaaS

สัญลักษณ์พารามิเตอร์พฤติกรรมเริ่มต้นคืนค่าโยนหรือล้มเหลวด้วยหมายเหตุ
TenantContextstring $tenantId, string $source, array $scopes = ['read']identity value object แบบไม่เปลี่ยนแปลงvalue objectไม่มีแหล่ง: jwt, mtls, api_key; hasScope() / hasAnyScope() ทดสอบ scope
TenantContext::singleTenant()ไม่มีtenant default ตายตัวพร้อม read, write, adminTenantContextไม่มีการปรับใช้แบบ single-tenant
ApiKeyAuthenticator::authenticate()string $rawKeyการตรวจสอบหกขั้นตอน แล้วจึง resolve contextTenantContextApiKeyAuthenticationException (HTTP 401)source ของ context เป็น api_key; scope คัดลอกมาจากระเบียนคีย์
ApiKeyAuthenticator::requireScope()TenantContext $context, ApiKeyScope $requiredScopeการยืนยัน scope อย่างชัดแจ้งvoidApiKeyAuthenticationException::insufficientScope() (HTTP 403)การบังคับใช้ scope เป็นขั้นตอนแยกต่างหากอย่างชัดแจ้ง
ApiKeyGenerator::generateLive() / ::generateTest()ไม่มีคีย์ใหม่: prefix, เนื้อหา base62 ขนาด 32 อักขระ (entropy 192 บิต), checksum 4 อักขระarray{key, hash, prefix}ไม่มีprefix npf_live_ / npf_test_; hash คือค่าย่อยสำหรับจัดเก็บ
ApiKeyGenerator::validateChecksum()string $keyการตรวจรูปร่างของ prefix, ความยาว และ CRC32-checksumboolไม่มีป้องกันการพิมพ์ผิดก่อนการค้นหาใน datastore ใด ๆ ไม่ใช่มาตรการความปลอดภัย
ApiKeyGenerator::hashKey() (static)string $keyค่าย่อย hex SHA-256 ของคีย์ดิบstringไม่มีรูปแบบเดียวของคีย์ที่จัดเก็บ
ApiKeyGenerator::isLiveKey() / ::isTestKey()string $keyการตรวจ prefixboolไม่มีมองเห็นสภาพแวดล้อมได้โดยไม่ต้องค้นหา
ApiKeyid, tenant, key hash, display prefix, scope mask, ช่วงเวลา created/expires/revokedระเบียนคีย์ที่จัดเก็บ; ไม่เคยเก็บ plaintextvalue objectไม่มีisActive(), isRevoked(), isExpired(), scopeNames()
ApiKeyScopebacked enum: Read = 1, Write = 2, Admin = 4แบบจำลอง scope แบบ bitmaskenumไม่มีmaskFromNames(), fromName(), fullAccess(); ชื่อที่ไม่รู้จักจะถูกละเว้นโดยตัวสร้าง mask
ApiKeyRepositoryInterfaceสัญญาการจัดเก็บ; จัดเก็บเฉพาะ hashกำหนดโดยการนำไปใช้findByHash(), findActiveByTenant(), store(), revoke()
SidecarJwtMinter::__construct()string $secret, issuer, audience, int $ttlSeconds = 300ปฏิเสธ signing secret ที่ต่ำกว่า 16 ไบต์ตอนสร้างinstanceInvalidArgumentExceptionขั้นต่ำความแข็งแรงของคีย์ 128 บิต; แนะนำสุ่ม 32 ไบต์ขึ้นไป
SidecarJwtMinter::mint()TenantContext $tenantJWT แบบ HS256 พร้อม iss, aud, sub, scope, tenant_id, iat, exp, jtistringJsonException เมื่อการเข้ารหัส claim ล้มเหลวอายุเริ่มต้นห้านาที; jti คือ 16 ไบต์สุ่ม เข้ารหัสแบบ hex
QuotaChecker::check()TenantContext $tenant, TenantQuota $quotaอ่านการใช้งานปัจจุบัน; เตือนที่ 80%; ปฏิเสธที่ 100%; ปฏิเสธเมื่อไม่ทราบการใช้งานarray{allowed: bool, warning_percentage: float|null}QuotaExceededException, QuotaUnavailableExceptionเรียก alert callback ที่ทั้งสองเกณฑ์
TenantQuotafloat $maxCuPerPeriod, collections, storage bytes, concurrent jobsขีดจำกัดต่อรอบ; ค่าคงที่ soft-threshold 80%value objectไม่มีค่าเริ่มต้น fromConfig(): 10,000 CU, 100 collections, 10 GB, 10 jobs
QuotaExceededException::toErrorEnvelope()ไม่มีerror envelope SPEC-QUOTA-001arrayHTTP 402, ลองใหม่ไม่ได้; พาค่าปัจจุบัน, ขีดจำกัด และช่วงเวลารีเซ็ตมาด้วย
QuotaUnavailableException::toErrorEnvelope()ไม่มีerror envelope SPEC-QUOTA-503arrayHTTP 503, ลองใหม่ได้; เหตุผล usage_undeterminable
UsageMeter::pullUsage()array<string, int> $watermarksสำรวจทุก usage-source host ที่ตั้งค่าไว้จาก cursor ของมันarray{events, instance_id}UsageMeterException เมื่อทุก host เข้าถึงไม่ได้ยอมรับการล่มบางส่วน; host ที่เข้าถึงไม่ได้จะถูกบันทึกและข้าม
UsageMeter::getCurrentUsage()string $tenantIdการใช้งาน compute-unit ในรอบปัจจุบันfloatUsageMeterException เมื่อไม่สามารถระบุการใช้งานได้ศูนย์ที่ parse ได้ถือเป็นค่าที่เชื่อถือได้; การใช้งานที่ไม่ทราบจะโยนข้อยกเว้น
StripeMeteringSyncer::sync()array<string, int> $watermarksหนึ่งรอบของ pull, transform, sendarray{watermarks, sent, failed}ไม่มี; ความล้มเหลวในการส่งจะถูกกำหนดเส้นทางไปยัง DLQ callbackความล้มเหลวในการ pull จะคืนรอบแบบ no-op ที่รักษา cursor ไว้
StripeAdapter::sendMeterEvent()MeterEvent $eventPOST ไปยังผู้ให้บริการพร้อม idempotency headervoidStripeSyncExceptionHTTP 429 และ 5xx ลองใหม่ได้; 4xx อื่นลองใหม่ไม่ได้
StripeAdapter::sendBatch()list<MeterEvent> $eventsส่งแต่ละเหตุการณ์; รวบรวมความล้มเหลวlist<StripeSyncException>ไม่มีรายการว่างหมายความว่าทุกเหตุการณ์สำเร็จ
MeterEventmeter name, tenant, value, idempotency key, timestampmeter-event value object แบบไม่เปลี่ยนแปลงvalue objectไม่มีtoStripePayload() ทำ serialize payload ของผู้ให้บริการ
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 {}
}
  • Tenant identity. tenant context เป็นแบบไม่เปลี่ยนแปลง: tenant identifier, แหล่งที่ resolve, scope identity ถูก resolve จาก authenticated context เท่านั้น (jwt, mtls, api_key) ไม่เคยมาจาก header หรือ query parameter ที่ client จัดหา การปรับใช้แบบ single-tenant ใช้ default context ที่กำหนดตายตัวพร้อม scope เต็ม
  • Authentication order. การตรวจสอบสิทธิ์ด้วย API key ดำเนินตามลำดับตายตัว: checksum, SHA-256 hash, การค้นหาใน repository, การตรวจการเพิกถอน, การตรวจการหมดอายุ, การ resolve context คีย์ที่ไม่รู้จัก ถูกเพิกถอน และหมดอายุเป็นผลลัพธ์สามแบบที่แตกต่างกัน ทั้งหมดเป็น HTTP 401; scope ไม่เพียงพอเป็น HTTP 403
  • Key secrecy. คีย์ดิบไม่เคยถูกจัดเก็บหรือบันทึก; เก็บและค้นหาเฉพาะค่าย่อย SHA-256 เท่านั้น ตัวตรวจสอบสิทธิ์ไม่ได้เปรียบเทียบ secret แบบ byte-wise ด้วยตัวเอง; การค้นหาค่าย่อยแบบ constant-time เป็นสัญญาของการนำ repository ไปใช้
  • Quota thresholds. ที่ soft limit 80% คำขอดำเนินต่อไป คืนค่าเปอร์เซ็นต์คำเตือน และ alert callback ถูกเรียก ที่ hard limit 100% คำขอถูกปฏิเสธด้วย SPEC-QUOTA-001 (HTTP 402) ที่พาช่วงเวลารีเซ็ตมาด้วย คือวันแรกของเดือนถัดไป เที่ยงคืน UTC
  • Quota fail-closed. การใช้งานที่ไม่สามารถระบุได้จะปฏิเสธคำขอด้วย SPEC-QUOTA-503 (HTTP 503, ลองใหม่ได้) การใช้งานที่ไม่ทราบจะไม่ถูกมองว่าเป็นศูนย์ การใช้งานที่เป็นศูนย์จริงและ parse ได้ถือเป็นค่าที่เชื่อถือได้และอนุญาต
  • Alert deduplication. ตัวตรวจสอบไม่ deduplicate การแจ้งเตือน; การ deduplicate ต่อรอบเป็นความรับผิดชอบของ callback
  • Metering sync. รอบนี้ถูกกำหนดเวลา ไม่เคยอยู่บนเส้นทางคำขอ กลับมาทำต่อจาก watermark ต่อแหล่ง และเลื่อน cursor แต่ละตัวไปยัง event identity สูงสุดที่ส่งสำเร็จ idempotency key เป็นแบบกำหนดได้แน่นอน คือ tenant, รอบ, event identity ดังนั้นเหตุการณ์ที่ส่งซ้ำจึงยุบรวมด้วยการ deduplicate ฝั่งผู้ให้บริการ
  • Pull failure. การ pull ที่ล้มเหลวจะคืนรอบแบบ no-op (sent 0, failed 0) ที่รักษา watermark ไว้; รอบถัดไปจะลองหน้าต่างเดิมซ้ำแทนที่จะข้ามไป
  • Service tokens. token เป็นแบบ HS256 ด้วย shared secret และพา iss, aud, sub, scope, tenant_id, iat, exp และ jti ที่ไม่ซ้ำมาด้วย อายุเริ่มต้นคือห้านาที การสร้างจะปฏิเสธ secret ที่ต่ำกว่า 16 ไบต์ แบบ fail-closed
  • คีย์ที่ผิดรูปจะไม่ผ่าน checksum และถูกปฏิเสธก่อนการเข้าถึง datastore ใด ๆ คีย์ที่รูปแบบถูกต้องแต่ไม่รู้จักจะถูกปฏิเสธหลังการค้นหา ทั้งสองปรากฏเป็นผลลัพธ์ invalid-key
  • คีย์ที่ไม่รู้จัก ถูกเพิกถอน และหมดอายุใช้ exception factory ที่แตกต่างกัน; แฟล็ก keyExpired เป็นจริงเฉพาะในผลลัพธ์ expired เท่านั้น ให้แมปไปยังการตอบสนองต่อ client ที่แตกต่างกัน
  • QuotaChecker::check() คืนค่าเฉพาะเมื่ออนุญาตเท่านั้น; ค่า allowed ที่คืนมาเป็น true เสมอ การปฏิเสธและการไม่พร้อมใช้งานเป็นผลลัพธ์แบบข้อยกเว้น
  • TenantQuota::usagePercentage() คืนค่า 0.0 สำหรับโควตาที่ไม่เป็นบวก; fromConfig() แทนค่าเริ่มต้นสำหรับค่าที่ขาดหายและ clamp ขีดจำกัดจำนวนเต็มให้อย่างน้อย 1
  • watermark เป็นแบบต่อแหล่ง; watermark ที่ขาดหายจะเริ่มจากต้นสตรีมของแหล่งนั้น (cursor 0) การปรับใช้แบบหลายแหล่งจะรักษา watermark อิสระ
  • การแปลงจะข้ามเหตุการณ์ที่ไม่ใช่อาร์เรย์ เหตุการณ์ที่มี operation หรือ tenant ขาดหายหรือว่างเปล่า ค่าที่ไม่เป็นบวก หรือ operation ที่ไม่ได้แมป โดยไม่ทำให้รอบล้มเหลว เหตุการณ์ที่ขาด positive integer identity ที่ใช้ได้จะถูกปฏิเสธพร้อมคำเตือน: fallback key แบบสุ่มจะทำลายการ deduplicate ฝั่งผู้ให้บริการและอาจเรียกเก็บเงิน tenant ซ้ำสองครั้ง
  • ความล้มเหลวในการส่งสิบครั้งติดต่อกันจะยกระดับเป็นบันทึก log ระดับ critical; ตัวนับจะรีเซ็ตเมื่อส่งสำเร็จครั้งใด ๆ เหตุการณ์ที่ล้มเหลวทุกครั้งยังคงไปถึง dead-letter callback
  • เนื้อหา JSON ที่ผิดรูปจาก usage-source host จะให้รายการเหตุการณ์ว่างเปล่า ไม่ใช่ความล้มเหลวของรอบ pullUsage() จะโยนข้อยกเว้นเฉพาะเมื่อทุก host ที่ตั้งค่าไว้เข้าถึงไม่ได้
  • primitive สำหรับ digest และ MAC เป็น SHA-256 และ HMAC-SHA256 ผ่าน crypto provider ของ host PHP build ที่ถูกจำกัดด้วย FIPS จะ fail closed กับอัลกอริทึมที่ไม่ผ่านการอนุมัติแทนที่จะลดระดับลง; เลเยอร์ SaaS ไม่ได้เพิ่มนโยบายทางการเข้ารหัสของตัวเอง
  • เนื้อหาคีย์และตัวระบุ token มาจาก CSPRNG (random_int(), random_bytes())
  • CRC32 checksum ไม่ใช่มาตรการทางการเข้ารหัสและไม่ได้รับผลกระทบจากโหมด FIPS

ข้อความด้านล่างอธิบายความสามารถเทียบกับข้อกำหนดที่อ้างอิง ไม่ใช่คำกล่าวรับรอง; NextPDF ไม่มีการรับรองสำหรับโมดูลนี้

พฤติกรรมเอกสารอ้างอิง
ความหมาย not-after ของ exp ใน service-tokenRFC 7519 §4.1.4
JWS compact serialization ของ service-tokenRFC 7515 §3.1
ขั้นต่ำ secret HS256 16 ไบต์; ห้ามใช้รหัสผ่านที่คนจำได้เป็น MAC keyRFC 8725 §3.5 (threat: §2.2)
สัญญา constant-time ของการค้นหาค่าย่อยใน repositoryOWASP ASVS 5.0 §11.2.4
ค่าย่อยการจัดเก็บ API key เป็น SHA-256FIPS 180-4 (code-declared)

การอ้างอิง RFC 8725 และ OWASP ASVS 5.0 ผ่านการตรวจสอบด้วย RAG; ตัวระบุอ้างอิงฉบับเต็มถูกบันทึกไว้ใน frontmatter ของหน้านี้ การอ้างอิง FIPS 180-4, FIPS 198-1 และ BSI TR-02102-1 ประกาศไว้ในโค้ดของ product source (hash('sha256', …) และขั้นต่ำของคีย์ที่ minter ระบุไว้); ไม่ได้ถูกดึงมาจากคลังข้อมูล RAG สำหรับหน้านี้ ข้อกำหนด constant-time ของ ASVS §11.2.4 ผูกกับการนำ repository ไปใช้ที่ผู้ดำเนินการจัดหา ไม่ใช่กับคลาสตัวตรวจสอบสิทธิ์เอง

  • จัดหาการนำ ApiKeyRepositoryInterface และ StripeAdapterInterface ไปใช้แบบถาวร; แพ็กเกจมาพร้อมสัญญาและ PSR-18 provider client ไม่ใช่การจัดเก็บข้อมูล
  • การพึ่งพาเป็นนามธรรมแบบ PSR เท่านั้น: PSR-3 logger, PSR-18 HTTP client, PSR-17 request และ stream factory ไม่ต้องใช้ provider SDK
  • รัน metering sync เป็น scheduled job จัดเก็บ watermark ที่คืนมาแบบถาวรหลังแต่ละรอบ
  • แสดงเปอร์เซ็นต์คำเตือนโควตาให้ client เห็น เช่นเป็น warning header และ deduplicate การแจ้งเตือนโควตาต่อรอบใน callback
  • จัดหา secret ของ token-minter จากการตั้งค่าเป็นค่าสุ่มที่มี entropy สูง; แนะนำสุ่ม 32 ไบต์ขึ้นไป อย่าอนุมานจากรหัสผ่าน
  • prefix ของคีย์ทำให้มองเห็นสภาพแวดล้อมได้โดยไม่ต้องค้นหา; คีย์ sandbox และ production ไม่เคยชนกันเพราะ prefix มีส่วนร่วมในค่าย่อยที่จัดเก็บ
  • รายละเอียดกลไกภายในอยู่ในเอกสารภายในของ source repository และอยู่นอกขอบเขตของคู่มือนี้

หน้านี้บันทึกเฉพาะพฤติกรรมที่สังเกตได้จากภายนอกและพื้นผิว API สาธารณะที่รองรับเท่านั้น เส้นทาง namespace ภายใน คลาสตัวช่วย ตารางกลไก ชื่อไฟล์ runbook และคำนำหน้า ticket อยู่นอกขอบเขต