diff --git a/src/EntitlementSnapshotContract.php b/src/EntitlementSnapshotContract.php index 6e92d44..e73aa64 100644 --- a/src/EntitlementSnapshotContract.php +++ b/src/EntitlementSnapshotContract.php @@ -27,11 +27,14 @@ public static function canonicalJson(array $snapshot): string public static function canonicalPayload(array $snapshot): array { self::assertValid($snapshot); + $entitlements = $snapshot['entitlements']; + $limitMap = (array) ($entitlements['limits'] ?? []); + $entitlements['limits'] = $limitMap === [] ? (object) [] : $limitMap; return [ 'algorithm' => self::ALGORITHM, 'contract_version' => self::VERSION, - 'entitlements' => self::canonicalize($snapshot['entitlements']), + 'entitlements' => self::canonicalize($entitlements), 'expires_at' => (string) $snapshot['expires_at'], 'generated_at' => (string) $snapshot['generated_at'], 'issuer' => (string) $snapshot['issuer'], diff --git a/tests/EntitlementSnapshotContractTest.php b/tests/EntitlementSnapshotContractTest.php index 933af7a..d007757 100644 --- a/tests/EntitlementSnapshotContractTest.php +++ b/tests/EntitlementSnapshotContractTest.php @@ -44,6 +44,23 @@ public function test_missing_or_changed_authorization_fields_are_rejected(): voi } } + public function test_empty_limit_map_is_stable_across_json_object_decoding(): void + { + $wirePayload = $this->payload(); + $wirePayload['entitlements']['limits'] = (object) []; + $decodedPayload = json_decode( + json_encode($wirePayload, JSON_THROW_ON_ERROR), + true, + 512, + JSON_THROW_ON_ERROR, + ); + + $wireCanonical = EntitlementSnapshotContract::canonicalJson($wirePayload); + + self::assertSame($wireCanonical, EntitlementSnapshotContract::canonicalJson($decodedPayload)); + self::assertStringContainsString('"limits":{}', $wireCanonical); + } + public function test_expiry_must_be_after_generation(): void { $payload = $this->payload();