跳转到内容
getnextpdf.com

Enterprise 版本

SaaS — 深度参考

Enterprise SaaS 模块为基于 NextPDF 的服务提供多租户构建块。

  • TenantContext 是一个不可变的身份值对象,仅从经过认证的上下文解析而来。
  • ApiKeyGeneratorApiKeyAuthenticator 签发并验证带前缀、带校验和、以哈希存储的 API key。
  • QuotaChecker 按每租户配额对请求进行门控:80% 时告警,100% 时拒绝,用量未知时以失败关闭方式拒绝。
  • SidecarJwtMinter 为组件间调用铸造短期的 HS256 服务 token。
  • UsageMeterStripeMeteringSyncer 拉取用量事件,并以确定性幂等的方式将其同步至计费提供方。

此能力随 NextPDF Enterprisenextpdf/enterprise)一同交付,并通过 Enterprise 层级的授权信封(license envelope)激活。缺少该授权的部署不会加载此能力的类。比较版本并获取授权

SaaS 范围是一项 Enterprise 基础能力;不存在单独的按功能划分的标志。NextPDF Core(Apache-2.0)与 NextPDF Pro 没有任何租户、API-key 或配额模型;此能力在更低层级没有对应物。

Terminal window
composer require nextpdf/enterprise:^3

所有符号位于 NextPDF\Enterprise\SaaS 下。

符号参数默认行为返回抛出或失败于备注
TenantContextstring $tenantId, string $source, array $scopes = ['read']不可变的身份值对象值对象来源:jwtmtlsapi_keyhasScope() / hasAnyScope() 检测 scope
TenantContext::singleTenant()固定的 default 租户,具有 readwriteadminTenantContext单租户部署
ApiKeyAuthenticator::authenticate()string $rawKey六步验证,随后解析上下文TenantContextApiKeyAuthenticationException(HTTP 401)上下文 sourceapi_key;scope 从 key 记录复制而来
ApiKeyAuthenticator::requireScope()TenantContext $context, ApiKeyScope $requiredScope显式的 scope 断言voidApiKeyAuthenticationException::insufficientScope()(HTTP 403)scope 强制是一个独立的显式步骤
ApiKeyGenerator::generateLive() / ::generateTest()新 key:前缀、32 字符 base62 主体(192 位熵)、4 字符校验和array{key, hash, prefix}前缀 npf_live_ / npf_test_hash 为存储摘要
ApiKeyGenerator::validateChecksum()string $key前缀、长度与 CRC32 校验和的形态检查bool在任何数据存储查询之前防笔误;不是安全控制
ApiKeyGenerator::hashKey() (static)string $key原始 key 的 SHA-256 十六进制摘要stringkey 唯一被存储的表示
ApiKeyGenerator::isLiveKey() / ::isTestKey()string $key前缀检查bool无需查询即可看出所处环境
ApiKeyid, tenant, key hash, display prefix, scope mask, created/expires/revoked instants已存储的 key 记录;明文绝不持久化值对象isActive(), isRevoked(), isExpired(), scopeNames()
ApiKeyScopebacked enum: Read = 1, Write = 2, Admin = 4位掩码 scope 模型枚举maskFromNames(), fromName(), fullAccess();掩码构建器会忽略未知名称
ApiKeyRepositoryInterface存储契约;仅以哈希持久化由实现定义findByHash(), findActiveByTenant(), store(), revoke()
SidecarJwtMinter::__construct()string $secret, issuer, audience, int $ttlSeconds = 300构造时拒绝小于 16 字节的签名密钥实例InvalidArgumentException128 位密钥强度下限;建议 32 字节或更多的随机字节
SidecarJwtMinter::mint()TenantContext $tenant携带 iss, aud, sub, scope, tenant_id, iat, exp, jti 的 HS256 JWTstringclaim 编码失败时抛出 JsonException默认五分钟生命周期;jti 为 16 个随机字节的十六进制编码
QuotaChecker::check()TenantContext $tenant, TenantQuota $quota读取当前用量;80% 时告警;100% 时拒绝;用量未知时拒绝array{allowed: bool, warning_percentage: float|null}QuotaExceededException, QuotaUnavailableException两个阈值处都会调用告警回调
TenantQuotafloat $maxCuPerPeriod, collections, storage bytes, concurrent jobs按周期的限额;80% 软阈值常量值对象fromConfig() 默认值:10,000 CU、100 collections、10 GB、10 jobs
QuotaExceededException::toErrorEnvelope()SPEC-QUOTA-001 错误信封arrayHTTP 402,不可重试;携带当前值、限额与重置时刻
QuotaUnavailableException::toErrorEnvelope()SPEC-QUOTA-503 错误信封arrayHTTP 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携带幂等 header 向提供方 POSTvoidStripeSyncExceptionHTTP 429 与 5xx 可重试;其他 4xx 不可重试
StripeAdapter::sendBatch()list<MeterEvent> $events发送每个事件;收集失败项list<StripeSyncException>空列表意味着每个事件都成功
MeterEventmeter name, tenant, value, idempotency key, timestamp不可变的计量事件值对象值对象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 {}
}
  • 租户身份。 租户上下文是不可变的:租户标识符、解析来源、scope。身份仅从经过认证的上下文解析(jwtmtlsapi_key)——绝不从客户端提供的 header 或查询参数解析。单租户部署使用具有完整 scope 的固定 default 上下文。
  • 认证顺序。 API-key 认证按固定顺序进行:校验和、SHA-256 哈希、仓库查询、吊销检查、过期检查、上下文解析。未知、已吊销与已过期的 key 是三种独立结果,均为 HTTP 401;scope 不足则为 HTTP 403。
  • key 保密性。 原始 key 绝不被存储或记录到日志;只有其 SHA-256 摘要被持久化并用于查询。认证器本身不执行任何逐字节的密钥比较;时序恒定的摘要查询是仓库实现的契约。
  • 配额阈值。 在 80% 软限制处,请求继续进行,返回告警百分比,并触发告警回调。在 100% 硬限制处,请求以携带重置时刻的 SPEC-QUOTA-001(HTTP 402)被拒绝——重置时刻为下个月第一天的 UTC 午夜。
  • 配额失败关闭。 无法确定的用量会以 SPEC-QUOTA-503(HTTP 503,可重试)拒绝请求。用量未知绝不被当作零处理。真正可解析的零用量是权威的,并予以放行。
  • 告警去重。 检查器不对告警去重;按周期去重是回调的职责。
  • 计量同步。 该周期是计划执行的,绝不在请求路径上。它从每来源水位处恢复,并将每个游标推进到成功发送的最高事件标识。幂等键是确定性的——租户、周期、事件标识——因此重发的事件会在提供方的去重中被折叠。
  • 拉取失败。 失败的拉取返回一个无操作周期(sent 为 0,failed 为 0)并保留水位;下一个周期会重试同一窗口而非跳过它。
  • 服务 token。 token 使用带共享密钥的 HS256,并携带 issaudsubscopetenant_idiatexp 以及一个唯一的 jti。默认生命周期为五分钟。构造会以失败关闭方式拒绝小于 16 字节的密钥。
  • 格式错误的 key 未通过校验和,会在任何数据存储访问之前被拒绝。格式正确但未知的 key 会在查询之后被拒绝。两者都表现为无效 key 结果。
  • 未知、已吊销与已过期的 key 使用不同的异常工厂;keyExpired 标志仅在已过期结果中为 true。请将它们映射到不同的客户端响应。
  • QuotaChecker::check() 仅在放行时返回;返回的 allowed 始终为 true。拒绝与不可用属于异常结果。
  • 对于非正配额,TenantQuota::usagePercentage() 返回 0.0fromConfig() 会用默认值替换缺失的值,并将整数限额下限钳制为至少 1。
  • 水位是按来源划分的;缺失的水位从该来源数据流的起点(游标 0)开始。多来源部署维护各自独立的水位。
  • 转换会跳过非数组事件、operation 或 tenant 缺失或为空的事件、值非正的事件,或 operation 未映射的事件——而不令整个周期失败。缺少可用的正整数标识的事件会被拒绝并附带一条告警:随机的回退键会破坏提供方侧的去重,并可能对该租户重复计费。
  • 连续十次发送失败会升级为一条 critical 日志条目;任何一次成功发送都会重置计数器。每个失败的事件仍会到达死信回调。
  • 来自用量来源主机的格式错误的 JSON 主体会产生一个空的事件列表,而非导致周期失败。仅当每个已配置的主机都不可达时,pullUsage() 才抛出。
  • 摘要与 MAC 原语是通过宿主 PHP 密码学提供方使用的 SHA-256 与 HMAC-SHA256。受 FIPS 约束的构建在遇到非批准算法时会以失败关闭方式处理而非降级;SaaS 层不附加自身的任何密码学策略。
  • key 主体与 token 标识符来自 CSPRNG(random_int()random_bytes())。
  • CRC32 校验和不是密码学控制,不受 FIPS 模式影响。

以下陈述描述的是相对于所引用条款的能力。它们不是认证声明;NextPDF 未持有此模块的任何认证。

行为引用
服务 token 的 exp not-after 语义RFC 7519 §4.1.4
服务 token 的 JWS compact serializationRFC 7515 §3.1
16 字节 HS256 密钥下限;不以人类可记忆的口令作为 MAC 密钥RFC 8725 §3.5 (threat: §2.2)
仓库摘要查询的时序恒定契约OWASP ASVS 5.0 §11.2.4
API-key 存储摘要 SHA-256FIPS 180-4 (code-declared)

RFC 8725 与 OWASP ASVS 5.0 的引用经过 RAG 验证;完整的 reference identifier 记录在本页的 frontmatter 中。FIPS 180-4、FIPS 198-1 与 BSI TR-02102-1 的引用是在产品源码中以代码声明的(hash('sha256', …) 及铸造器所记录的密钥下限);本页并未从 RAG 语料库检索它们。ASVS §11.2.4 的时序恒定要求约束的是运营方提供的仓库实现,而非认证器类本身。

  • 请提供 ApiKeyRepositoryInterfaceStripeAdapterInterface 的持久化实现;该包交付的是契约与一个 PSR-18 提供方客户端,而非持久化。
  • 依赖仅为 PSR 抽象:PSR-3 logger、PSR-18 HTTP client、PSR-17 request 与 stream 工厂。无需任何提供方 SDK。
  • 将计量同步作为计划任务运行。在每个周期之后持久化返回的水位。
  • 将配额告警百分比呈现给客户端,例如作为一个 warning header,并在回调中按周期对配额告警去重。
  • 从配置中以高熵随机值提供 token 铸造器的密钥;建议使用 32 字节或更多的随机字节。绝不从口令派生它。
  • key 前缀使得无需查询即可看出所处环境;sandbox 与 production 的 key 绝不会冲突,因为前缀参与了所存储的摘要。
  • 内部机制细节保留在源码仓库的内部文档中,不在本手册范围之内。

本页仅记录外部可观察的行为与受支持的公开 API 界面。内部命名空间路径、辅助类、机制表、runbook 文件名与工单前缀均不在范围之内。