Enterprise รุ่น
SaaS — เอกสารอ้างอิงเชิงลึก
โดยสรุป
หัวข้อที่มีชื่อว่า “โดยสรุป”โมดูล Enterprise SaaS จัดหาองค์ประกอบพื้นฐานแบบ multi-tenant สำหรับบริการที่สร้างบน NextPDF
TenantContextเป็น identity value object แบบไม่เปลี่ยนแปลง ที่ resolve จาก authenticated context เท่านั้นApiKeyGeneratorและApiKeyAuthenticatorออกและตรวจสอบ API key ที่มี prefix, checksum และจัดเก็บแบบ hashQuotaCheckerควบคุมคำขอตามโควตาต่อ 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 หรือโควตา ความสามารถนี้ไม่มีสิ่งเทียบเท่าในระดับชั้นที่ต่ำกว่า
composer require nextpdf/enterprise:^3พื้นผิว API สาธารณะ
หัวข้อที่มีชื่อว่า “พื้นผิว API สาธารณะ”สัญลักษณ์ทั้งหมดอยู่ภายใต้ NextPDF\Enterprise\SaaS
| สัญลักษณ์ | พารามิเตอร์ | พฤติกรรมเริ่มต้น | คืนค่า | โยนหรือล้มเหลวด้วย | หมายเหตุ |
|---|---|---|---|---|---|
TenantContext | string $tenantId, string $source, array $scopes = ['read'] | identity value object แบบไม่เปลี่ยนแปลง | value object | ไม่มี | แหล่ง: jwt, mtls, api_key; hasScope() / hasAnyScope() ทดสอบ scope |
TenantContext::singleTenant() | ไม่มี | tenant default ตายตัวพร้อม read, write, admin | TenantContext | ไม่มี | การปรับใช้แบบ single-tenant |
ApiKeyAuthenticator::authenticate() | string $rawKey | การตรวจสอบหกขั้นตอน แล้วจึง resolve context | TenantContext | ApiKeyAuthenticationException (HTTP 401) | source ของ context เป็น api_key; scope คัดลอกมาจากระเบียนคีย์ |
ApiKeyAuthenticator::requireScope() | TenantContext $context, ApiKeyScope $requiredScope | การยืนยัน scope อย่างชัดแจ้ง | void | ApiKeyAuthenticationException::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-checksum | bool | ไม่มี | ป้องกันการพิมพ์ผิดก่อนการค้นหาใน datastore ใด ๆ ไม่ใช่มาตรการความปลอดภัย |
ApiKeyGenerator::hashKey() (static) | string $key | ค่าย่อย hex SHA-256 ของคีย์ดิบ | string | ไม่มี | รูปแบบเดียวของคีย์ที่จัดเก็บ |
ApiKeyGenerator::isLiveKey() / ::isTestKey() | string $key | การตรวจ prefix | bool | ไม่มี | มองเห็นสภาพแวดล้อมได้โดยไม่ต้องค้นหา |
ApiKey | id, tenant, key hash, display prefix, scope mask, ช่วงเวลา created/expires/revoked | ระเบียนคีย์ที่จัดเก็บ; ไม่เคยเก็บ plaintext | value object | ไม่มี | isActive(), isRevoked(), isExpired(), scopeNames() |
ApiKeyScope | backed enum: Read = 1, Write = 2, Admin = 4 | แบบจำลอง scope แบบ bitmask | enum | ไม่มี | maskFromNames(), fromName(), fullAccess(); ชื่อที่ไม่รู้จักจะถูกละเว้นโดยตัวสร้าง mask |
ApiKeyRepositoryInterface | — | สัญญาการจัดเก็บ; จัดเก็บเฉพาะ hash | — | กำหนดโดยการนำไปใช้ | findByHash(), findActiveByTenant(), store(), revoke() |
SidecarJwtMinter::__construct() | string $secret, issuer, audience, int $ttlSeconds = 300 | ปฏิเสธ signing secret ที่ต่ำกว่า 16 ไบต์ตอนสร้าง | instance | InvalidArgumentException | ขั้นต่ำความแข็งแรงของคีย์ 128 บิต; แนะนำสุ่ม 32 ไบต์ขึ้นไป |
SidecarJwtMinter::mint() | TenantContext $tenant | JWT แบบ HS256 พร้อม iss, aud, sub, scope, tenant_id, iat, exp, jti | string | JsonException เมื่อการเข้ารหัส claim ล้มเหลว | อายุเริ่มต้นห้านาที; jti คือ 16 ไบต์สุ่ม เข้ารหัสแบบ hex |
QuotaChecker::check() | TenantContext $tenant, TenantQuota $quota | อ่านการใช้งานปัจจุบัน; เตือนที่ 80%; ปฏิเสธที่ 100%; ปฏิเสธเมื่อไม่ทราบการใช้งาน | array{allowed: bool, warning_percentage: float|null} | QuotaExceededException, QuotaUnavailableException | เรียก alert callback ที่ทั้งสองเกณฑ์ |
TenantQuota | float $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-001 | array | — | HTTP 402, ลองใหม่ไม่ได้; พาค่าปัจจุบัน, ขีดจำกัด และช่วงเวลารีเซ็ตมาด้วย |
QuotaUnavailableException::toErrorEnvelope() | ไม่มี | error envelope SPEC-QUOTA-503 | array | — | HTTP 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 ในรอบปัจจุบัน | float | UsageMeterException เมื่อไม่สามารถระบุการใช้งานได้ | ศูนย์ที่ parse ได้ถือเป็นค่าที่เชื่อถือได้; การใช้งานที่ไม่ทราบจะโยนข้อยกเว้น |
StripeMeteringSyncer::sync() | array<string, int> $watermarks | หนึ่งรอบของ pull, transform, send | array{watermarks, sent, failed} | ไม่มี; ความล้มเหลวในการส่งจะถูกกำหนดเส้นทางไปยัง DLQ callback | ความล้มเหลวในการ pull จะคืนรอบแบบ no-op ที่รักษา cursor ไว้ |
StripeAdapter::sendMeterEvent() | MeterEvent $event | POST ไปยังผู้ให้บริการพร้อม idempotency header | void | StripeSyncException | HTTP 429 และ 5xx ลองใหม่ได้; 4xx อื่นลองใหม่ไม่ได้ |
StripeAdapter::sendBatch() | list<MeterEvent> $events | ส่งแต่ละเหตุการณ์; รวบรวมความล้มเหลว | list<StripeSyncException> | ไม่มี | รายการว่างหมายความว่าทุกเหตุการณ์สำเร็จ |
MeterEvent | meter name, tenant, value, idempotency key, timestamp | meter-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 ใช้defaultcontext ที่กำหนดตายตัวพร้อม 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 (
sent0,failed0) ที่รักษา 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 ที่ตั้งค่าไว้เข้าถึงไม่ได้
พฤติกรรมในโหมด FIPS
หัวข้อที่มีชื่อว่า “พฤติกรรมในโหมด FIPS”- 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-token | RFC 7519 §4.1.4 |
| JWS compact serialization ของ service-token | RFC 7515 §3.1 |
| ขั้นต่ำ secret HS256 16 ไบต์; ห้ามใช้รหัสผ่านที่คนจำได้เป็น MAC key | RFC 8725 §3.5 (threat: §2.2) |
| สัญญา constant-time ของการค้นหาค่าย่อยใน repository | OWASP ASVS 5.0 §11.2.4 |
| ค่าย่อยการจัดเก็บ API key เป็น SHA-256 | FIPS 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 อยู่นอกขอบเขต