{ "schema": 1, "files": { "spec/cache-key-format.md": { "prefix": "KEY", "next": 13, "requirements": { "KEY-1": { "section": "Serializer Codes", "binds": "sdk", "sdks": [ "cachekit-py" ], "vectors": [ "cache-keys.json:serializer_default", "cache-keys.json:serializer_auto", "cache-keys.json:serializer_pythonic_alias", "cache-keys.json:serializer_orjson", "cache-keys.json:serializer_arrow", "cache-keys.json:serializer_local", "cache-keys.json:serializer_custom_identity", "cache-keys.json:serializer_derived_spec_example", "cache-keys.json:serializer_derived_leading_zero", "cache-keys.json:serializer_custom_identity_orjson", "cache-keys.json:serializer_custom_identity_non_ascii" ], "gap": "The vectors give the serializer to key generation as an input; whether an SDK's test keys a call through a cache configured with it, which a cache that hands the generator a fixed default fails, is SDK test code no vector can check." }, "KEY-2": { "section": "Serializer Codes", "binds": "sdk", "sdks": [ "cachekit-py" ], "vectors": [ "cache-keys.json:serializer_default", "cache-keys.json:serializer_auto", "cache-keys.json:serializer_orjson", "cache-keys.json:serializer_arrow", "cache-keys.json:serializer_local", "cache-keys.json:serializer_custom_identity", "cache-keys.json:serializer_derived_spec_example", "cache-keys.json:serializer_derived_leading_zero", "cache-keys.json:serializer_custom_identity_orjson", "cache-keys.json:serializer_custom_identity_non_ascii", "python-frame.json:std_alias_write", "python-frame.json:pythonic_alias_write", "cache-keys.json:object_arrow_serializer", "cache-keys.json:object_class_named_auto", "cache-keys.json:object_standard_serializer", "cache-keys.json:object_orjson_serializer", "cache-keys.json:object_auto_serializer" ] }, "KEY-3": { "section": "Serializer Codes", "binds": "sdk", "sdks": [ "cachekit-py" ], "vectors": [ "python-frame.json:default_saas_write_msgpack_bytestorage", "python-frame.json:default_saas_write_msgpack_bytestorage_bin", "python-frame.json:arrow_dataframe_write", "python-frame.json:forged_plaintext_orjson", "python-frame.json:ciphertext_key_not_in_keyring", "python-frame.json:ciphertext_other_tenant", "python-frame.json:auto_serializer_write", "python-frame.json:standard_serializer_instance_write", "python-frame.json:std_alias_write", "python-frame.json:pythonic_alias_write", "python-frame.json:integrity_checking_off_write", "python-frame.json:arrow_compression_off_write" ], "tests": [ "tools/python-frame-reference.py:verify" ], "gap": "Encrypted writes are pinned only for the default serializer, so a writer that records the name on encrypted entries only for that serializer passes." }, "KEY-4": { "section": "Serializer Codes", "binds": "sdk", "sdks": [ "cachekit-py" ], "vectors": [ "python-frame.json:default_saas_write_msgpack_bytestorage", "python-frame.json:default_saas_write_msgpack_bytestorage_bin", "python-frame.json:arrow_dataframe_write", "python-frame.json:auto_serializer_write", "python-frame.json:standard_serializer_instance_write", "python-frame.json:std_alias_write", "python-frame.json:pythonic_alias_write", "python-frame.json:arrow_compression_off_write", "python-frame.json:std_recorded_as_given_frame", "python-frame.json:ciphertext_serializer_name_auto" ], "gap": "A test exercises the check by planting a frame under another configuration's reader: auto_serializer_write, standard_serializer_instance_write and std_recorded_as_given_frame carry the default write's payload, and ciphertext_serializer_name_auto an encrypted write that authenticates under the reader's key. A default-configured reader that skips the check, on plaintext or on encrypted entries, returns the value, and so does one that resolves the recorded name through its alias table before it compares, on plaintext entries; a reader configured with a StandardSerializer() instance must read standard_serializer_instance_write, which one that compares its key identity (:StandardSerializer) instead of the recorded name misses. No encrypted frame records an alias, so a reader whose encrypted path alone resolves names through its alias table passes. A reader that decodes the payload before it compares names reaches the same outcome on every frame, so no vector shows the order, and none records a name that differs from the reader's only in case." }, "KEY-5": { "section": "Serializer Codes", "binds": "sdk", "sdks": [ "cachekit-py" ], "vectors": [ "python-frame.json:forged_plaintext_orjson", "python-frame.json:default_saas_write_msgpack_bytestorage_bin", "python-frame.json:std_recorded_as_given_frame", "python-frame.json:auto_serializer_write", "python-frame.json:standard_serializer_instance_write", "python-frame.json:serializer_name_missing", "python-frame.json:serializer_name_empty", "python-frame.json:ciphertext_serializer_name_auto", "python-frame.json:ciphertext_serializer_name_missing" ], "tests": [ "tools/python-frame-reference.py:_require_serializer_name" ], "gap": "A reader configured with auto or a StandardSerializer() instance must miss on the default write, whose payload it decodes, which is the reverse pair a test plants. The vectors show which frames a reader must reject, plaintext and encrypted; that the rejection reaches the caller as a miss, not as an error, is what a test asserts when it reads them through its SDK's cache rather than its frame parser. No frame records a name that differs from the reader's only in case, and no encrypted frame records an empty name." }, "KEY-6": { "section": "Serializer Codes", "binds": "sdk", "sdks": [ "cachekit-py" ], "vectors": [ "cache-keys.json:serializer_default", "cache-keys.json:serializer_auto", "cache-keys.json:serializer_pythonic_alias", "cache-keys.json:serializer_orjson", "cache-keys.json:serializer_arrow", "cache-keys.json:serializer_local", "cache-keys.json:serializer_custom_identity", "cache-keys.json:serializer_derived_spec_example", "cache-keys.json:serializer_derived_leading_zero", "cache-keys.json:serializer_custom_identity_orjson", "cache-keys.json:serializer_custom_identity_non_ascii", "cache-keys.json:object_arrow_serializer", "cache-keys.json:object_class_named_auto", "cache-keys.json:object_standard_serializer", "cache-keys.json:object_orjson_serializer", "cache-keys.json:object_auto_serializer" ] }, "KEY-7": { "section": "Serializer Codes", "binds": "sdk", "sdks": [ "cachekit-py" ], "vectors": [ "cache-keys.json:single_integer", "cache-keys.json:serializer_pythonic_alias", "python-frame.json:std_alias_write", "python-frame.json:pythonic_alias_write" ] }, "KEY-8": { "section": "Serializer Codes", "binds": "sdk", "sdks": [ "cachekit-py" ], "vectors": [ "cache-keys.json:single_integer", "cache-keys.json:serializer_pythonic_alias", "python-frame.json:std_alias_write", "python-frame.json:pythonic_alias_write" ] }, "KEY-9": { "section": "Serializer Codes", "binds": "sdk", "sdks": [ "cachekit-py" ], "vectors": [ "cache-keys.json:serializer_custom_identity", "cache-keys.json:object_arrow_serializer", "cache-keys.json:object_class_named_auto", "cache-keys.json:object_standard_serializer", "cache-keys.json:object_orjson_serializer", "cache-keys.json:object_auto_serializer" ] }, "KEY-10": { "section": "Serializer Codes", "binds": "sdk", "sdks": [ "cachekit-py" ], "vectors": [ "cache-keys.json:serializer_type_empty", "cache-keys.json:serializer_type_null", "cache-keys.json:serializer_type_integer", "cache-keys.json:serializer_type_truthy_integer" ], "gap": "The rows give the identity to key generation directly, so a cache that turns an empty or missing serializer into its default before key generation sees it passes them; whether a cache configured that way refuses it is SDK test code." }, "KEY-11": { "section": "Serializer Codes", "binds": "caller", "gap": "Which namespace an application keys each configuration under is its own choice, which no vector or SDK test can observe." }, "KEY-12": { "section": "Serializer Codes", "binds": "caller", "gap": "As KEY-11, for distinct identities that share one recorded name: the namespace is the application's choice, which no vector or SDK test can observe." } }, "retired": {} }, "spec/encryption.md": { "prefix": "ENC", "next": 11, "requirements": { "ENC-1": { "section": "AAD v0x03 Format", "binds": "sdk", "vectors": [ "encryption.json:aad_compressed_false_sealed_true", "encryption.json:aad_compressed_true_sealed_false", "encryption.json:aad_format_msgpack_sealed_arrow", "encryption.json:aad_without_original_type_sealed_with", "encryption.json:aad_with_original_type_sealed_without", "encryption.json:aad_key_with_prefix_sealed_without", "encryption.json:aad_compressed_false_sealed_true_five_components", "encryption.json:aad_compressed_true_sealed_false_five_components", "encryption.json:aad_without_original_type_sealed_with_msgpack", "encryption.json:aad_format_arrow_sealed_msgpack" ], "gap": "After a retry, aad_without_original_type_sealed_with, aad_compressed_false_sealed_true_five_components, aad_without_original_type_sealed_with_msgpack and aad_format_arrow_sealed_msgpack decrypt to a container the reader that presents them reads, so the retry returns a value. aad_compressed_false_sealed_true, aad_compressed_true_sealed_false, aad_format_msgpack_sealed_arrow and aad_compressed_true_sealed_false_five_components decrypt to bytes that reader cannot read, so a retry still ends with no value and only a test that asserts the authentication failure itself, not just an empty read, tells it apart. aad_with_original_type_sealed_without and aad_key_with_prefix_sealed_without decrypt to one MessagePack document with bytes after it, which only an interop reader must refuse (ENC-3's gap), so a test asserts the authentication failure there too. A reader that builds its AAD inputs from configuration presents only the rows its configuration produces: cachekit-ts the four-component rows, the compressed flips one per compression setting, and the prefixed key through a backend key prefix; cachekit-rs and interop mode only the four-component rows with compressed False, cachekit-rs reaching the prefixed key through a namespace and interop mode, which refuses a key prefix, not at all; cachekit-py, which takes the inputs from its frame header, presents every row. A reader that retries only with AADs no row was sealed under passes, such as one that retries with one original_type value swapped for another, original_type msgpack added to a four-component compressed-False AAD, original_type dropped from a compressed-True AAD, the format changed to or from orjson, a key prefix or namespace added, another tenant, or an empty or pre-0x03 AAD." }, "ENC-2": { "section": "AAD v0x03 Format", "binds": "sdk", "vectors": [ "python-frame.json:ciphertext_plain_msgpack_not_envelope", "python-frame.json:ciphertext_plain_msgpack_claims_uncompressed", "encryption.json:container_envelope_to_plain_reader", "encryption.json:container_plain_to_envelope_reader", "encryption.json:container_bare_arrow_to_arrow_reader", "encryption.json:container_plain_json_to_orjson_reader" ], "gap": "The envelopes the rows hold are in the bin encoding, so a plain-MessagePack reader that unwraps only the legacy array-of-integers envelope passes. The plain-MessagePack rows are sealed under four-component AADs, so they do not reach cachekit-py's reader with integrity checking off, and no row gives an orjson reader with integrity checking off a checksummed payload. No row gives an Arrow or orjson reader plain MessagePack, or an envelope reader JSON, so a reader that falls back to the other family passes." }, "ENC-3": { "section": "AAD v0x03 Format", "binds": "sdk", "vectors": [ "python-frame.json:ciphertext_plain_msgpack_not_envelope", "python-frame.json:ciphertext_plain_msgpack_claims_uncompressed", "encryption.json:container_trailing_byte_to_interop_reader", "encryption.json:container_incomplete_tail_to_interop_reader", "encryption.json:container_plain_to_envelope_reader", "encryption.json:container_bare_arrow_to_arrow_reader", "encryption.json:container_plain_json_to_orjson_reader" ], "gap": "Only interop readers are held to bytes after the document, since spec/interop-mode.md obliges them to consume exactly one: whether a reader of an auto-mode container, plain MessagePack (cachekit-ts with compression off, cachekit-rs) or a ByteStorage envelope, rejects bytes after its document or envelope is not stated, so no row asks it of them (cachekit-core ignores bytes after an envelope, and cachekit-ts refuses them). No row gives an Arrow or orjson reader plain MessagePack, or an envelope reader JSON, so a reader that falls back to the other family on a parse failure passes." }, "ENC-4": { "section": "`format` tokens", "binds": "sdk", "gap": "A rule on how an SDK ships a serializer, which no vector or test can observe; what it protects on the wire is ENC-5." }, "ENC-5": { "section": "`format` tokens", "binds": "sdk", "vectors": [ "interop-mode.json:interop_key_aad", "interop-mode.json:interop_encryption_roundtrip", "python-frame.json:default_saas_write_msgpack_bytestorage", "python-frame.json:default_saas_write_msgpack_bytestorage_bin", "python-frame.json:arrow_dataframe_write", "python-frame.json:forged_plaintext_orjson", "python-frame.json:ciphertext_key_not_in_keyring", "python-frame.json:ciphertext_other_tenant", "python-frame.json:auto_serializer_write", "python-frame.json:standard_serializer_instance_write", "python-frame.json:integrity_checking_off_write", "python-frame.json:arrow_compression_off_write" ], "gap": "Only cachekit-py's default, AutoSerializer, Arrow and OrjsonSerializer writes on plaintext frames, its default writes on encrypted frames, and the interop AAD are pinned against the token a writer emits: no frame holds an AutoSerializer numpy, Series or DataFrame write, no vector holds an auto-mode cachekit-ts or cachekit-rs write, and encryption.json takes the token as an input, so such a writer that emits an off-registry token passes; no SDK can seal with a fixed nonce, so only a test that decrypts what its write path stores under the aad_hex of the encryption.json vector with its shape catches it." }, "ENC-6": { "section": "`compressed` tokens", "binds": "sdk", "vectors": [ "encryption.json:basic_bytes", "encryption.json:special_cache_key", "encryption.json:compressed_basic", "encryption.json:arrow_uncompressed", "encryption.json:arrow_compressed", "encryption.json:orjson_uncompressed", "encryption.json:original_type_numpy", "encryption.json:standard_serializer_default", "encryption.json:standard_serializer_integrity_off", "encryption.json:encrypted_with_k1", "encryption.json:encrypted_with_k2", "encryption.json:default_tenant_interop", "encryption.json:master_key_every_hex_digit", "encryption.json:master_key_first_byte_80", "interop-mode.json:interop_key_aad", "interop-mode.json:interop_encryption_roundtrip" ], "tests": [ "tools/encryption-verify.py:verify_sealed_vector", "tools/interop-crosscheck.mjs:aadV3" ], "gap": "The vectors pin the bytes a renderer emits for a JSON boolean, not the AAD an SDK's write path emits: an SDK whose writer spells the token itself, in stored metadata or on a second code path, and whose reader rebuilds the AAD from that spelling round-trips with itself and passes them through whichever renderer its test drives; no SDK can seal with a fixed nonce, so only a test that decrypts what its write path stores under the aad_hex of the vector with the same shape catches it." }, "ENC-7": { "section": "Key Rotation (Keyring)", "binds": "sdk", "vectors": [ "encryption.json:keyring_three_decrypt_only_keys", "encryption.json:keyring_four_decrypt_only_keys" ], "gap": "The rows give a configuration, not where it enters: whether a test loads each one through every way its SDK takes decrypt-only keys (CACHEKIT_PREVIOUS_MASTER_KEYS, the programmatic keyring, each constructor that reads them) is SDK test code." }, "ENC-8": { "section": "Key Rotation (Keyring)", "binds": "caller", "gap": "The spec makes this operator-enforced: nothing an SDK holds shows that a key was once current, and ENC-9 is the part an SDK can detect." }, "ENC-9": { "section": "Key Rotation (Keyring)", "binds": "sdk", "vectors": [ "encryption.json:keyring_current_key_decrypt_only", "encryption.json:keyring_current_key_decrypt_only_uppercase" ], "gap": "As ENC-7, and no row repeats the current key with white space or a 0x prefix, which rule 1 leaves open, so a string comparison that folds case but keeps those passes." }, "ENC-10": { "section": "Encryption Flow", "binds": "sdk", "vectors": [ "interop-mode.json:interop_key_aad", "interop-mode.json:interop_encryption_roundtrip", "python-frame.json:default_saas_write_msgpack_bytestorage", "python-frame.json:default_saas_write_msgpack_bytestorage_bin", "python-frame.json:arrow_dataframe_write", "encryption.json:standard_serializer_default", "encryption.json:standard_serializer_integrity_off", "python-frame.json:integrity_checking_off_write", "python-frame.json:arrow_compression_off_write", "python-frame.json:auto_serializer_write", "python-frame.json:forged_plaintext_orjson" ], "gap": "cachekit-py's StandardSerializer writes with integrity checking on and off, and its Arrow writes with compression on and off, are pinned against the compressed flag they record, so a writer that records one flag whatever its container did fails one of each pair; the interop write is pinned against its container. The Arrow frames check frame structure, not IPC bytes, so an Arrow writer whose IPC body compression disagrees with the flag it records passes. No frame holds an AutoSerializer numpy, Series or DataFrame write, whose flag reaches an AAD only through an encryption wrapper built around AutoSerializer by hand, since cachekit-py's cache refuses AutoSerializer under encryption. No vector holds a cachekit-ts write, with compression on or off, or a cachekit-rs write: no SDK can seal with a fixed nonce, so only a test that decrypts what its write path stores under the aad_hex of the vector with its shape (compressed_basic, basic_bytes) catches a flag that disagrees with its container." } }, "retired": {} }, "spec/file-backend-format.md": { "prefix": "FILE", "next": 7, "requirements": { "FILE-1": { "section": "Entry layout", "binds": "sdk", "vectors": [ "file-backend.json:permanent_ascii", "file-backend.json:future_expiry", "file-backend.json:expired_entry", "file-backend.json:empty_payload", "file-backend.json:binary_payload" ], "gap": "Every payload is 256 B or less, so a writer that sets the reserved byte or a flag only above some size passes them; a size threshold has no edge to pin, and a vector large enough to pass a plausible one would put its payload twice in hex (payload_hex, file_hex). binary_payload holds every byte value, so a writer that flags binary or ciphertext-like payloads fails it." }, "FILE-2": { "section": "Version and flag negotiation", "binds": "sdk", "vectors": [ "file-backend.json:unknown_flag_preserved", "file-backend.json:reserved_nonzero_preserved", "file-backend.json:unknown_flag_high_bit", "file-backend.json:reserved_one_preserved", "file-backend.json:reserved_ff_preserved", "file-backend.json:unknown_flag_expired", "file-backend.json:reserved_nonzero_expired" ] }, "FILE-3": { "section": "Version and flag negotiation", "binds": "sdk", "vectors": [ "file-backend.json:unknown_flag_preserved", "file-backend.json:reserved_nonzero_preserved", "file-backend.json:unknown_flag_high_bit", "file-backend.json:reserved_one_preserved", "file-backend.json:reserved_ff_preserved", "file-backend.json:unknown_flag_expired", "file-backend.json:reserved_nonzero_expired" ], "gap": "The expired vectors differ from a miss_expired read only in the file surviving, so only a test that checks the file afterwards catches a reader that checks expiry first and deletes. No vector refreshes the TTL of an entry with an unknown flag or reserved value. A refresh rewrites bytes 6 to 13, but the rule's object is the payload, which a refresh leaves alone, so whether FILE-3 forbids it is open; every SDK's refresh skips such an entry today." }, "FILE-4": { "section": "Version and flag negotiation", "binds": "sdk", "vectors": [ "file-backend.json:permanent_ascii", "file-backend.json:future_expiry", "file-backend.json:expired_entry", "file-backend.json:empty_payload", "file-backend.json:binary_payload" ], "gap": "In version 1 every flag is unknown, so this is FILE-1's check and shares its gap." }, "FILE-5": { "section": "Write and TTL behavior", "binds": "sdk", "gap": "A torn expiry shows only while a refresh races a read; no byte vector can show it." }, "FILE-6": { "section": "Write and TTL behavior", "binds": "sdk", "gap": "How many reads load the header is the reader's I/O, which no byte vector can show." } }, "retired": {} }, "spec/intent-presets.md": { "prefix": "PRE", "next": 56, "requirements": { "PRE-1": { "section": "Default TTL", "binds": "sdk", "gap": "No fixture holds a preset: the spec makes its Canonical Preset Table the fixture and leaves each SDK's tests to assert the four TTLs, so nothing here catches a preset whose default TTL is missing, infinite or another value." }, "PRE-2": { "section": "Default TTL", "binds": "sdk", "gap": "No fixture holds a preset or a TTL argument, so nothing here catches an SDK whose preset default wins over an explicit TTL at any level it offers (call, decorator or builder)." }, "PRE-3": { "section": "Default TTL", "binds": "sdk", "gap": "Whether an SDK reads an environment variable or a global switch is invisible to any vector, so nothing here catches a process-wide default-TTL override." }, "PRE-4": { "section": "Default TTL", "binds": "sdk", "gap": "No fixture holds a preset's resolved TTL or an SDK's no-expiry spelling, which the spec leaves SDK-local, so nothing here catches an SDK that reaches no-expiry without the caller asking for it, by an omitted or zero TTL." }, "PRE-5": { "section": "Default TTL", "binds": "sdk", "gap": "No fixture holds a preset's resolved TTL, so nothing here catches a preset whose default never expires." }, "PRE-6": { "section": "Default TTL", "binds": "sdk", "gap": "A rule on an SDK's changelog, which no vector or test can observe." }, "PRE-7": { "section": "L1 Posture", "binds": "sdk", "gap": "No fixture holds a preset's posture, and an L1 hit leaves no trace in the bytes a vector pins, so nothing here catches a preset that ships with L1 off, as cachekit-rs's minimal did through 0.7.0." }, "PRE-8": { "section": "L1 Posture", "binds": "sdk", "gap": "No fixture holds a preset's posture, so nothing here catches a minimal that serves stale entries, or evicts on another process's invalidation, by default." }, "PRE-9": { "section": "L1 Posture", "binds": "sdk", "gap": "No fixture holds a preset's posture, so nothing here catches a secure preset that ships with L1 off." }, "PRE-10": { "section": "L1 Posture", "binds": "sdk", "gap": "What an SDK's L1 holds is in-process state no vector reaches, so nothing here catches a secure preset whose L1 keeps the decrypted value." }, "PRE-11": { "section": "L1 Posture", "binds": "sdk", "gap": "Every encrypted vector is a stored entry that a reader decrypts, so none shows what a writer puts in each layer: nothing here catches a secure preset that stores plaintext in L1, in L2 or in any other cache layer." }, "PRE-12": { "section": "Integrity Posture", "binds": "sdk", "sdks": [ "cachekit-rs" ], "gap": "A rule on an SDK's documentation, which no vector or test can check." }, "PRE-13": { "section": "Reliability Floor", "binds": "sdk", "gap": "A reliability stack shows only in how a client behaves under backend faults, which no vector reaches, so nothing here catches a production, secure or io preset that ships with it off." }, "PRE-14": { "section": "Reliability Floor", "binds": "sdk", "gap": "A rule on each preset's documentation, which no vector or test can check." }, "PRE-15": { "section": "Encrypted Preset Name", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "gap": "Which names an SDK exports is API surface no vector can observe, so nothing here catches an encrypted alias." }, "PRE-16": { "section": "Master Key Input", "binds": "sdk", "vectors": [ "interop-mode.json:interop_encryption_roundtrip", "encryption.json:basic_bytes", "encryption.json:special_cache_key", "encryption.json:compressed_basic", "encryption.json:arrow_uncompressed", "encryption.json:arrow_compressed", "encryption.json:orjson_uncompressed", "encryption.json:original_type_numpy", "encryption.json:standard_serializer_default", "encryption.json:standard_serializer_integrity_off", "encryption.json:encrypted_with_k1", "encryption.json:encrypted_with_k2", "encryption.json:default_tenant_interop", "encryption.json:master_key_every_hex_digit", "encryption.json:master_key_first_byte_80" ], "tests": [ "tools/encryption-verify.py:verify_master_key_input", "tools/encryption-verify.py:verify_wrong_entry_points" ], "gap": "Whether an SDK's test hands the hex string to its secure preset (which a preset that takes only raw bytes fails), rather than decoding it first for a lower-level API, is SDK test code no vector can check. Only default_tenant_interop and the accept rows master_key_every_hex_digit and master_key_first_byte_80 reach a preset that takes no tenant; the others are sealed under named tenants. No vector's verdict turns on an uppercase letter, white space or a 0x prefix, which rule 1 does not settle, so none shows what a preset does with a valid key that holds one." }, "PRE-17": { "section": "Master Key Input", "binds": "sdk", "vectors": [ "interop-mode.json:interop_encryption_roundtrip", "encryption.json:basic_bytes", "encryption.json:special_cache_key", "encryption.json:compressed_basic", "encryption.json:arrow_uncompressed", "encryption.json:arrow_compressed", "encryption.json:orjson_uncompressed", "encryption.json:original_type_numpy", "encryption.json:standard_serializer_default", "encryption.json:standard_serializer_integrity_off", "encryption.json:encrypted_with_k1", "encryption.json:encrypted_with_k2", "encryption.json:default_tenant_interop", "encryption.json:master_key_every_hex_digit", "encryption.json:master_key_first_byte_80", "encryption.json:master_key_odd_65_digits", "encryption.json:master_key_odd_63_digits", "encryption.json:master_key_non_hex_digit", "encryption.json:master_key_trailing_non_hex", "encryption.json:master_key_plus_sign" ], "tests": [ "tools/encryption-verify.py:verify_master_key_input", "tools/encryption-verify.py:verify_wrong_entry_points" ], "gap": "Every key the vectors accept is exactly 32 bytes, so a decoder that accepts a longer key and cuts it to 32 bytes passes. No vector's verdict turns on an uppercase letter, white space or a 0x prefix, which rule 1 does not settle, so none shows what a decoder does with a valid key that holds one. Whether a test decodes each string through the preset's own path is SDK test code." }, "PRE-18": { "section": "Master Key Input", "binds": "sdk", "vectors": [ "interop-mode.json:interop_encryption_roundtrip", "encryption.json:basic_bytes", "encryption.json:special_cache_key", "encryption.json:compressed_basic", "encryption.json:arrow_uncompressed", "encryption.json:arrow_compressed", "encryption.json:orjson_uncompressed", "encryption.json:original_type_numpy", "encryption.json:standard_serializer_default", "encryption.json:standard_serializer_integrity_off", "encryption.json:encrypted_with_k1", "encryption.json:encrypted_with_k2", "encryption.json:default_tenant_interop", "encryption.json:master_key_every_hex_digit", "encryption.json:master_key_first_byte_80", "encryption.json:master_key_odd_65_digits", "encryption.json:master_key_odd_63_digits", "encryption.json:master_key_non_hex_digit", "encryption.json:master_key_trailing_non_hex", "encryption.json:master_key_plus_sign" ], "tests": [ "tools/encryption-verify.py:verify_master_key_input", "tools/encryption-verify.py:verify_wrong_entry_points" ], "gap": "As PRE-17: SDKs that disagree on a key longer than 32 bytes, or on a valid key that holds an uppercase letter, white space or a 0x prefix, each pass, and whether each test feeds the string through its SDK's own preset path is SDK test code." }, "PRE-19": { "section": "Master Key Input", "binds": "sdk", "vectors": [ "encryption.json:default_tenant_interop" ], "gap": "The vector is an entry any secure preset can read with no key or tenant argument, so it catches a preset that ignores CACHEKIT_MASTER_KEY only when an SDK's test supplies the key through that variable and passes no key argument, which is SDK test code no vector can check." }, "PRE-20": { "section": "Master Key Input", "binds": "sdk", "gap": "No vector stands for a missing key, so nothing here catches a secure preset that constructs with no key and fails only on first use." }, "PRE-21": { "section": "Master Key Input", "binds": "sdk", "gap": "No vector stands for a missing key, so nothing here catches a secure preset that, given no key, builds a cache that stores plaintext." }, "PRE-22": { "section": "Master Key Input", "binds": "sdk", "vectors": [ "interop-mode.json:interop_encryption_roundtrip", "encryption.json:basic_bytes", "encryption.json:special_cache_key", "encryption.json:compressed_basic", "encryption.json:arrow_uncompressed", "encryption.json:arrow_compressed", "encryption.json:orjson_uncompressed", "encryption.json:original_type_numpy", "encryption.json:standard_serializer_default", "encryption.json:standard_serializer_integrity_off", "encryption.json:encrypted_with_k1", "encryption.json:encrypted_with_k2", "encryption.json:default_tenant_interop", "encryption.json:master_key_every_hex_digit", "encryption.json:master_key_first_byte_80" ], "tests": [ "tools/encryption-verify.py:verify_master_key_input", "tools/encryption-verify.py:verify_wrong_entry_points" ], "gap": "As PRE-16, for a preset that refuses a 32-byte key." }, "PRE-23": { "section": "Master Key Input", "binds": "sdk", "vectors": [ "encryption.json:master_key_31_bytes", "encryption.json:master_key_24_bytes", "encryption.json:master_key_16_bytes", "encryption.json:master_key_31_bytes_and_crlf", "encryption.json:master_key_31_bytes_with_spaces", "encryption.json:master_key_0x_and_31_bytes" ], "tests": [ "tools/encryption-verify.py:verify_master_key_input", "tools/encryption-verify.py:verify_wrong_entry_points" ], "gap": "The rows reach whichever hex entry point an SDK's test feeds them to; whether it feeds them to every place a key enters (the preset's argument, CACHEKIT_MASTER_KEY, a companion constructor, a previous key), rather than to one shared decoder, is SDK test code no vector can check. The short keys are 16, 24 and 31 bytes, so a length rule that refuses those and accepts another length under 32 passes." }, "PRE-24": { "section": "Master Key Input", "binds": "sdk", "gap": "A rule on an SDK's documentation, which no vector or test can check." }, "PRE-25": { "section": "Master Key Input", "binds": "sdk", "vectors": [ "interop-mode.json:interop_encryption_roundtrip", "encryption.json:basic_bytes", "encryption.json:special_cache_key", "encryption.json:compressed_basic", "encryption.json:arrow_uncompressed", "encryption.json:arrow_compressed", "encryption.json:orjson_uncompressed", "encryption.json:original_type_numpy", "encryption.json:standard_serializer_default", "encryption.json:standard_serializer_integrity_off", "encryption.json:encrypted_with_k1", "encryption.json:encrypted_with_k2", "encryption.json:default_tenant_interop", "encryption.json:master_key_every_hex_digit", "encryption.json:master_key_first_byte_80" ], "gap": "As PRE-16, when the test hands the hex string to the preset itself: a preset that takes the key only as bytes, while a lower-level builder takes hex, passes a test that drives the builder." }, "PRE-26": { "section": "Master Key Input", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-rs" ], "vectors": [ "interop-mode.json:interop_encryption_roundtrip", "encryption.json:master_key_every_hex_digit", "encryption.json:raw_key_31_bytes", "encryption.json:raw_key_24_bytes", "encryption.json:raw_key_16_bytes", "encryption.json:raw_key_33_bytes", "encryption.json:raw_key_ascii_hex_string" ], "tests": [ "tools/encryption-verify.py:verify_master_key_input", "tools/encryption-verify.py:verify_wrong_entry_points" ], "gap": "Each vector reaches a raw-bytes entry point only when an SDK's test drives it through one: the 32-byte keys of interop_encryption_roundtrip and master_key_every_hex_digit to be accepted (the second has bytes above 7f, which an entry point that passes the bytes through a text string corrupts), and the raw reject rows of 16, 24, 31 and 33 bytes and the ASCII bytes of a hex string to be refused, so one that refuses those and accepts another length but 32 passes. Whether a test drives them through every raw-bytes entry point the SDK offers, for current and previous keys alike, is SDK test code no vector can check." }, "PRE-27": { "section": "Master Key Input", "binds": "sdk", "vectors": [ "encryption.json:default_tenant_interop" ], "tests": [ "tools/encryption-verify.py:verify_default_tenant" ], "gap": "The vector states its tenant, \"default\", beside the bytes; whether an SDK's test builds each constructor that makes an encrypting client (the secure preset, a companion or an environment constructor) with no tenant and reads it, which a constructor that resolves \"\", a namespace or a deployment id fails, is SDK test code no vector can check, and a lower-level manager built with no tenant does not show what a preset passes it." }, "PRE-28": { "section": "Master Key Input", "binds": "sdk", "vectors": [ "interop-mode.json:interop_encryption_roundtrip", "encryption.json:basic_bytes", "encryption.json:special_cache_key", "encryption.json:compressed_basic", "encryption.json:arrow_uncompressed", "encryption.json:arrow_compressed", "encryption.json:orjson_uncompressed", "encryption.json:original_type_numpy", "encryption.json:standard_serializer_default", "encryption.json:standard_serializer_integrity_off", "encryption.json:encrypted_with_k1", "encryption.json:encrypted_with_k2", "encryption.json:default_tenant_interop", "encryption.json:master_key_every_hex_digit", "encryption.json:master_key_first_byte_80" ], "tests": [ "tools/encryption-verify.py:verify_sealed_vector", "tools/encryption-verify.py:verify_default_tenant", "tools/encryption-verify.py:verify_master_key_input" ], "gap": "The vectors fail a reader whose key and AAD use different tenants, the default or a named one, only when its test drives the SDK's own key derivation and AAD builder from one resolved tenant; a test that compares the AAD with a vector's aad_hex, or hands the tenant to each step itself, passes a reader that derives under \"default\" and builds the AAD under \"\". The writer side is PRE-29's gap." }, "PRE-29": { "section": "Master Key Input", "binds": "sdk", "vectors": [ "encryption.json:default_tenant_interop" ], "tests": [ "tools/encryption-verify.py:verify_default_tenant" ], "gap": "The vector pins one reference ciphertext and its derived-key fingerprint, so it shows that an SDK reads under the default tenant, not that what it writes decrypts in another SDK; no SDK can seal with a fixed nonce, so only a write in one SDK read by another catches a writer whose key or AAD differs." }, "PRE-30": { "section": "Encryption Activation", "binds": "sdk", "gap": "Whether a client encrypts is decided at construction: a planted plaintext entry catches activation by CACHEKIT_MASTER_KEY's presence (PRE-34's case), but nothing here catches activation by any other implicit signal, which a test must know of to provoke." }, "PRE-31": { "section": "Encryption Activation", "binds": "sdk", "vectors": [ "interop-mode.json:interop_encryption_roundtrip", "encryption.json:default_tenant_interop", "interop-mode.json:issue_example_object" ], "gap": "The ciphertext vectors are stored interop entries a test can plant and read through a client built with an explicit encryption option, which a read path that ignores the option fails. Only a test that plants issue_example_object under that client and expects a miss or an error catches a read path that also serves plaintext. Which read paths a test drives is SDK test code, and no vector shows what a write path stores, since no SDK can seal with a fixed nonce." }, "PRE-32": { "section": "Encryption Activation", "binds": "sdk", "vectors": [ "interop-mode.json:interop_encryption_roundtrip", "encryption.json:default_tenant_interop", "interop-mode.json:issue_example_object" ], "gap": "As PRE-31, for an option that is accepted and then ignored on some path, or a read path that also serves plaintext; a build without the encryption capability is reached only by a test run in that build." }, "PRE-33": { "section": "Encryption Activation", "binds": "sdk", "gap": "No vector holds an encryption option an SDK cannot honour, so nothing here catches an SDK that accepts one instead of rejecting it." }, "PRE-34": { "section": "Encryption Activation", "binds": "sdk", "vectors": [ "interop-mode.json:issue_example_object" ], "gap": "The vector's bytes are a plaintext interop entry a test can plant and read through minimal, production or io built with CACHEKIT_MASTER_KEY set, which a client that decrypts its reads because the variable is present fails. That is the read path only: a client that encrypts its writes because of the variable, and reads by trying to decrypt and falling back to plaintext, passes. The vector also gives the bytes a non-encrypting write must store, so only a test that writes through the client with the variable set and inspects the stored bytes catches it; a test that drives the encoder alone does not, and setting the variable at all is SDK test code no vector can check." }, "PRE-35": { "section": "Encryption Activation", "binds": "sdk", "sdks": [ "cachekit-rs" ], "vectors": [ "interop-mode.json:issue_example_object" ], "gap": "As PRE-34, for a configure-from-environment constructor with CACHEKIT_MASTER_KEY set." }, "PRE-36": { "section": "Encryption Activation", "binds": "sdk", "vectors": [ "interop-mode.json:issue_example_object" ], "gap": "As PRE-34, for a client built with an explicit opt-out while CACHEKIT_MASTER_KEY is set." }, "PRE-37": { "section": "Encryption Activation", "binds": "sdk", "sdks": [ "cachekit-py" ], "gap": "No vector holds an encrypted CK frame, so nothing here catches a non-encrypting cache that stops decrypting a still-encrypting peer's entries when CACHEKIT_MASTER_KEY is set." }, "PRE-38": { "section": "Encryption Activation", "binds": "sdk", "sdks": [ "cachekit-py" ], "gap": "A rule on the cachekit 0.20.0 release; no vector holds an encrypted CK frame, so nothing here catches a release that stops decrypting existing ciphertext." }, "PRE-39": { "section": "Encryption Activation", "binds": "sdk", "sdks": [ "cachekit-py" ], "gap": "A rule on the cachekit 0.20.0 release's construction, which no vector reaches." }, "PRE-40": { "section": "Encryption Activation", "binds": "sdk", "sdks": [ "cachekit-py" ], "gap": "A logging rule on the cachekit 0.20.0 release, which no vector can observe: nothing here catches a warning issued only as a DeprecationWarning, which Python silences outside __main__, or one that names neither explicit spelling." }, "PRE-41": { "section": "Encryption Activation", "binds": "sdk", "sdks": [ "cachekit-py" ], "gap": "A rule on the cachekit 0.21.0 release's construction, which no vector reaches; PRE-34 states the steady-state rule for minimal, production and io." }, "PRE-42": { "section": "Encryption Activation", "binds": "sdk", "sdks": [ "cachekit-py" ], "gap": "No vector holds an encrypted CK frame, so nothing here catches an encryption=False cache that stops decrypting existing ciphertext while CACHEKIT_MASTER_KEY is set." }, "PRE-43": { "section": "Encryption Activation", "binds": "sdk", "sdks": [ "cachekit-py" ], "gap": "A rule on the cachekit 0.21.0 release's construction, which no vector reaches: nothing here catches a cache that constructs, encrypting or not, when the key is present and no intent is stated, or an error that does not name both explicit spellings." }, "PRE-44": { "section": "Encryption Activation", "binds": "sdk", "sdks": [ "cachekit-py" ], "gap": "A rule on the cachekit 0.21.0 release's construction, which no vector reaches: nothing here catches a 0.21.0 cache that silently builds as plaintext when the key is present and no intent is stated." }, "PRE-45": { "section": "`io` Credentials", "binds": "sdk", "gap": "No vector holds an API key or reaches the environment, so nothing here catches an io that takes its API key from only one of the argument and CACHEKIT_API_KEY, or lets the variable win over an argument." }, "PRE-46": { "section": "`io` Credentials", "binds": "sdk", "gap": "No vector stands for a missing API key, so nothing here catches an io that constructs with no API key and fails only on its first request." }, "PRE-47": { "section": "`io` Credentials", "binds": "sdk", "vectors": [ "interop-mode.json:issue_example_object" ], "gap": "As PRE-34, for io built with CACHEKIT_MASTER_KEY set." }, "PRE-48": { "section": "Explicit Configuration", "binds": "sdk", "gap": "No fixture holds a preset's defaults or an argument, so nothing here catches a preset whose default wins over an explicit argument." }, "PRE-49": { "section": "Explicit Configuration", "binds": "sdk", "gap": "No vector holds an argument a preset does not support, so nothing here catches a preset that drops one, as cachekit-py's io did with backend= through 0.19.0." }, "PRE-50": { "section": "Explicit Configuration", "binds": "sdk", "gap": "As PRE-20 for secure and PRE-46 for io." }, "PRE-51": { "section": "Explicit Configuration", "binds": "sdk", "sdks": [ "cachekit-rs" ], "vectors": [ "encryption.json:default_tenant_interop", "encryption.json:master_key_every_hex_digit" ], "gap": "The vectors catch a secure companion that does not read CACHEKIT_MASTER_KEY only when a test supplies the key through that variable. They are default-tenant entries under two different master keys and cache keys, so a companion that ignores CACHEKIT_PREVIOUS_MASTER_KEYS fails a test that sets one key as current and the other as previous only if the test asserts a hit, since a fail-open read recomputes instead; and apart from CACHEKIT_MASTER_KEY, which PRE-53's planted entry reaches, nothing observes an io companion that reads a variable besides CACHEKIT_API_KEY." }, "PRE-52": { "section": "Explicit Configuration", "binds": "sdk", "sdks": [ "cachekit-rs" ], "gap": "The rule asks for a construction error, which no vector observes." }, "PRE-53": { "section": "Explicit Configuration", "binds": "sdk", "sdks": [ "cachekit-rs" ], "vectors": [ "interop-mode.json:issue_example_object" ], "gap": "As PRE-34, for the io companion with CACHEKIT_MASTER_KEY set; a variable that would turn a secure companion's encryption off is reached only by a test that sets it." }, "PRE-54": { "section": "SDK-Local Presets", "binds": "sdk", "sdks": [ "cachekit-py" ], "gap": "Which names an SDK exports, and what each configures, is API surface no vector can observe, so nothing here catches an SDK-local preset that reuses a canonical name." }, "PRE-55": { "section": "SDK-Local Presets", "binds": "sdk", "sdks": [ "cachekit-py" ], "gap": "A rule on an SDK's documentation, which no vector or test can check." } }, "retired": {} }, "spec/interop-mode.md": { "prefix": "IOP", "next": 37, "requirements": { "IOP-1": { "section": "The Problem", "binds": "caller", "gap": "Whether a value is meant for another SDK is the application's choice; the SDK's part, writing interop values as plain MessagePack, is IOP-17." }, "IOP-2": { "section": "Segment grammar", "binds": "sdk", "vectors": [ "interop-mode.json:reject_uppercase_namespace", "interop-mode.json:reject_colon_in_operation", "interop-mode.json:reject_trailing_newline", "interop-mode.json:segment_max_length", "interop-mode.json:reject_namespace_too_long", "interop-mode.json:reject_operation_too_long", "interop-mode.json:reject_leading_dot_namespace", "interop-mode.json:reject_leading_hyphen_operation", "interop-mode.json:reject_leading_underscore_operation", "interop-mode.json:reject_empty_namespace", "interop-mode.json:reject_empty_operation", "interop-mode.json:reject_non_ascii_digit_namespace", "interop-mode.json:reject_non_ascii_letter_operation", "interop-mode.json:reject_slash_namespace", "interop-mode.json:reject_space_operation", "interop-mode.json:reject_uppercase_operation", "interop-mode.json:reject_colon_in_namespace", "interop-mode.json:reject_trailing_newline_operation", "interop-mode.json:reject_fullwidth_operation" ], "gap": "The character-set vectors each name one character outside the set (an uppercase letter, ':', a newline, a non-ASCII digit, an accented and a full-width letter, '/', a space), so a validator that bans those characters one by one, instead of admitting only the set, passes them all; no finite list of characters closes that." }, "IOP-3": { "section": "Segment grammar", "binds": "sdk", "vectors": [ "interop-mode.json:reject_uppercase_namespace", "interop-mode.json:reject_colon_in_operation", "interop-mode.json:reject_trailing_newline", "interop-mode.json:reject_reserved_namespace_ns", "interop-mode.json:reject_reserved_namespace_nsapi", "interop-mode.json:reject_double_dot_namespace", "interop-mode.json:reject_double_dot_operation", "interop-mode.json:reject_namespace_too_long", "interop-mode.json:reject_operation_too_long", "interop-mode.json:reject_leading_dot_namespace", "interop-mode.json:reject_leading_hyphen_operation", "interop-mode.json:reject_leading_underscore_operation", "interop-mode.json:reject_empty_namespace", "interop-mode.json:reject_empty_operation", "interop-mode.json:reject_non_ascii_digit_namespace", "interop-mode.json:reject_non_ascii_letter_operation", "interop-mode.json:reject_slash_namespace", "interop-mode.json:reject_space_operation", "interop-mode.json:reject_uppercase_operation", "interop-mode.json:reject_colon_in_namespace", "interop-mode.json:reject_trailing_newline_operation", "interop-mode.json:reject_fullwidth_operation" ], "gap": "The vectors drive key derivation, so they share IOP-2's remaining hole (a ban list of the tested characters); rejection at decoration or registration time rather than at the first call, and on every backend, are SDK API properties no vector observes." }, "IOP-4": { "section": "Segment grammar", "binds": "sdk", "vectors": [ "interop-mode.json:reject_reserved_namespace_ns", "interop-mode.json:reject_reserved_namespace_nsapi", "interop-mode.json:reservation_scope" ], "gap": "The paragraph also requires the rejection at decoration or registration time and on every backend, the SDK API property recorded on IOP-3, which the vectors cannot observe: they drive key derivation only." }, "IOP-5": { "section": "Segment grammar", "binds": "sdk", "vectors": [ "interop-mode.json:reject_double_dot_namespace", "interop-mode.json:reject_double_dot_operation", "interop-mode.json:lone_dots_stay_valid" ], "gap": "The paragraph also requires the rejection at decoration or registration time and on every backend, the SDK API property recorded on IOP-3, which the vectors cannot observe: they drive key derivation only." }, "IOP-6": { "section": "The Canonical Argument Array", "binds": "caller", "gap": "No vector can carry the signature of the function an application passes to the TypeScript wrapper." }, "IOP-7": { "section": "The Canonical Argument Array", "binds": "caller", "gap": "Every vector carries the full argument list, so none can show a call that passes fewer arguments." }, "IOP-8": { "section": "The Interop Data Model", "binds": "sdk", "vectors": [ "interop-mode.json:reject_nan", "interop-mode.json:reject_infinity", "interop-mode.json:reject_negative_infinity", "interop-mode.json:reject_int_overflow", "interop-mode.json:reject_int_underflow", "interop-mode.json:reject_naive_datetime", "interop-mode.json:reject_naive_datetime_nested", "interop-mode.json:reject_nan_nested", "interop-mode.json:reject_int_overflow_in_list", "interop-mode.json:reject_int_underflow_in_map", "interop-mode.json:set_in_map_normalized", "interop-mode.json:map_key_proto" ], "tests": [ "tools/interop-reference.py:_out_of_model_selftest", "tools/interop-crosscheck.mjs:outOfModelSelftest" ], "gap": "Portable JSON cannot express a value whose type is outside the data model (an arbitrary object, a non-string map key), so only the reference tools' self-tests reject one; no vector reaches an SDK with it." }, "IOP-9": { "section": "The Interop Data Model", "binds": "sdk", "vectors": [ "interop-mode.json:reject_int_overflow", "interop-mode.json:reject_int_underflow", "interop-mode.json:uint64_max", "interop-mode.json:int64_min", "interop-mode.json:reject_int_overflow_in_list", "interop-mode.json:reject_int_underflow_in_list", "interop-mode.json:reject_int_underflow_in_map", "interop-mode.json:reject_int_overflow_in_map" ] }, "IOP-10": { "section": "The Interop Data Model", "binds": "sdk", "tests": [ "tools/interop-reference.py:_surrogate_selftest", "tools/interop-crosscheck.mjs:loneSurrogateSelftest" ], "gap": "The self-tests reject a lone high or low surrogate as an argument, in a map key, in a nested value and in a set, but only in the reference tools: portable JSON cannot carry a lone surrogate, so no vector reaches an SDK." }, "IOP-11": { "section": "The Interop Data Model", "binds": "sdk", "tests": [ "tools/interop-reference.py:_surrogate_selftest", "tools/interop-crosscheck.mjs:loneSurrogateSelftest" ], "gap": "The self-tests reject a lone high or low surrogate as an argument, in a map key, in a nested value and in a set, but only in the reference tools: portable JSON cannot carry a lone surrogate, so no vector reaches an SDK." }, "IOP-12": { "section": "The Interop Data Model", "binds": "sdk", "sdks": [ "cachekit-ts" ], "tests": [ "tools/interop-crosscheck.mjs:loneSurrogateSelftest" ], "gap": "The self-test exercises the reference cross-check, not cachekit-ts; portable JSON cannot carry a lone surrogate, so only the SDK's own suite can test it." }, "IOP-13": { "section": "The Interop Data Model", "binds": "sdk", "vectors": [ "interop-mode.json:map_key_proto" ], "tests": [ "tools/interop-reference.py:_non_string_key_selftest", "tools/interop-crosscheck.mjs:nonStringKeySelftest" ], "gap": "Portable JSON object keys are always strings, so no vector carries a non-string key and only the reference tools' self-tests reject one; map_key_proto pins only that a string key such as __proto__ survives normalization. A JavaScript object can also carry Symbol keys: the cross-check rejects an own enumerable Symbol key, ignores a non-enumerable one as it ignores a non-enumerable string key, and rejects a Map as outside the data model (IOP-8)." }, "IOP-14": { "section": "The Interop Data Model", "binds": "sdk", "sdks": [ "cachekit-ts" ], "vectors": [ "interop-mode.json:map_key_sort_supplementary", "interop-mode.json:map_key_sort_ascii", "interop-mode.json:nested_map_recursive_sort", "interop-mode.json:map_key_sort_case_across_letters", "interop-mode.json:map_key_sort_punctuation", "interop-mode.json:map_key_sort_shorter_key_later", "interop-mode.json:map_key_sort_supplementary_shorter" ] }, "IOP-15": { "section": "Number canonicalization", "binds": "sdk", "sdks": [ "cachekit-ts" ], "vectors": [ "interop-mode.json:uint64_max", "interop-mode.json:int64_min", "interop-mode.json:int_beyond_float64_precision", "interop-mode.json:small_int_as_bigint" ] }, "IOP-16": { "section": "Number canonicalization", "binds": "sdk", "sdks": [ "cachekit-ts" ], "tests": [ "tools/interop-crosscheck.mjs:unsafeNumberSelftest" ], "gap": "The self-test exercises the reference cross-check, not cachekit-ts: Python and Rust read a bare JSON integer beyond 2^53 exactly, so no shared vector can carry the unsafe Number only JavaScript would see." }, "IOP-17": { "section": "Interop Value Format", "binds": "sdk", "vectors": [ "interop-mode.json:issue_example_object", "interop-mode.json:float_value_stays_float64", "interop-mode.json:mixed_array", "interop-mode.json:datetime_sentinel_value", "interop-mode.json:proto_key_value", "interop-mode.json:bom_strings_value" ], "gap": "The vectors pin the bytes an SDK's value encoder emits for each value, not the bytes its write path stores: an SDK whose write path serializes through a general MessagePack library instead, taking map order and number widths from the caller's type, passes them through whichever encoder its test drives; only a test that writes each value through the SDK's write path and compares the stored bytes catches it." }, "IOP-18": { "section": "Interop Value Format", "binds": "sdk", "vectors": [ "interop-mode.json:issue_example_object", "interop-mode.json:float_value_stays_float64", "interop-mode.json:mixed_array", "interop-mode.json:datetime_sentinel_value", "interop-mode.json:proto_key_value", "interop-mode.json:bom_strings_value", "decode-bounds.json:nested_fixarray_depth_32", "decode-bounds.json:nested_fixmap_depth_32", "decode-bounds.json:array16_256_backed_nils", "interop-mode.json:reader_padded_int_widths", "interop-mode.json:reader_padded_headers", "interop-mode.json:reader_unsorted_map_keys", "interop-mode.json:reader_float32", "interop-mode.json:reader_non_string_map_key", "interop-mode.json:reader_ext_type" ], "gap": "The reader vectors hold one non-string map key, an integer that fits a fixint, and stock decoders reject other well-formed keys: @msgpack/msgpack 3.1.3 throws on a nil, bool, bin, array or map key and on an integer key written at 64-bit width, and msgpack-python with strict_map_key=False on an array or map key, so a reader built on either passes every vector without accepting every well-formed document." }, "IOP-19": { "section": "Interop Value Format", "binds": "sdk", "vectors": [ "interop-mode.json:reader_trailing_byte", "python-frame.json:ck_frame_fed_to_interop_reader", "encryption.json:container_trailing_byte_to_interop_reader", "encryption.json:container_incomplete_tail_to_interop_reader" ], "gap": "Both post-decryption rows end in exactly one byte after the document, so an end-of-input check that tolerates one stray byte after a decrypt passes them; only ck_frame_fed_to_interop_reader, a plaintext read, has more." }, "IOP-20": { "section": "Interop Value Format", "binds": "sdk", "sdks": [ "cachekit-rs" ], "vectors": [ "interop-mode.json:reader_trailing_byte", "python-frame.json:ck_frame_fed_to_interop_reader", "encryption.json:container_trailing_byte_to_interop_reader", "encryption.json:container_incomplete_tail_to_interop_reader" ], "gap": "Both post-decryption rows end in exactly one byte after the document, so an end-of-input check that tolerates one stray byte after a decrypt passes them; only ck_frame_fed_to_interop_reader, a plaintext read, has more." }, "IOP-21": { "section": "SDK Implementation Requirements", "binds": "sdk", "vectors": [ "interop-mode.json", "interop-mode.json:small_int_as_bigint" ], "gap": "The list's demands on segment validation, normalization, value encoding and hashing share the holes still recorded on IOP-2, IOP-8, IOP-10, IOP-13, IOP-16 and IOP-17; no vector reaches the 32-bit length tier (untested by design); binding named and defaulted arguments to their positions is an SDK API property no vector observes; leaving auto mode unchanged is pinned only where an SDK vendors cache-keys.json or wire-format.json." }, "IOP-22": { "section": "SDK Implementation Requirements", "binds": "sdk", "vectors": [ "interop-mode.json:reject_nan", "interop-mode.json:reject_infinity", "interop-mode.json:reject_negative_infinity", "interop-mode.json:reject_int_overflow", "interop-mode.json:reject_int_underflow", "interop-mode.json:reject_naive_datetime", "interop-mode.json:reject_uppercase_namespace", "interop-mode.json:reject_colon_in_operation", "interop-mode.json:reject_trailing_newline", "interop-mode.json:reject_reserved_namespace_ns", "interop-mode.json:reject_reserved_namespace_nsapi", "interop-mode.json:reject_double_dot_namespace", "interop-mode.json:reject_double_dot_operation", "interop-mode.json:reject_namespace_too_long", "interop-mode.json:reject_operation_too_long", "interop-mode.json:reject_leading_dot_namespace", "interop-mode.json:reject_leading_hyphen_operation", "interop-mode.json:reject_leading_underscore_operation", "interop-mode.json:reject_empty_namespace", "interop-mode.json:reject_empty_operation", "interop-mode.json:reject_non_ascii_digit_namespace", "interop-mode.json:reject_non_ascii_letter_operation", "interop-mode.json:reject_fullwidth_operation", "interop-mode.json:reject_slash_namespace", "interop-mode.json:reject_space_operation", "interop-mode.json:reject_uppercase_operation", "interop-mode.json:reject_colon_in_namespace", "interop-mode.json:reject_trailing_newline_operation", "interop-mode.json:reject_naive_datetime_nested", "interop-mode.json:reject_nan_nested", "interop-mode.json:reject_int_overflow_in_list", "interop-mode.json:reject_int_underflow_in_map", "interop-mode.json:reject_int_overflow_in_map", "interop-mode.json:reject_int_underflow_in_list" ] }, "IOP-23": { "section": "SDK Implementation Requirements", "binds": "caller", "gap": "A UUID passed as a plain string reaches the SDK as an ordinary string, so neither the SDK nor a vector can see how it was spelled." }, "IOP-24": { "section": "Decode bounds", "binds": "sdk", "vectors": [ "decode-bounds.json" ], "gap": "The rule against pre-allocating beyond the input cannot fail by verdict, because every overclaim vector is truncated and any decoder rejects it at end of input; the fail-closed rule (a rejection becomes a cache miss, never a crash or an abort) is a property of the SDK read path; and which entry points the vectors reach is SDK test code (IOP-31)." }, "IOP-25": { "section": "Decode bounds", "binds": "sdk", "vectors": [ "decode-bounds.json:nested_fixarray_depth_32", "decode-bounds.json:nested_fixmap_depth_32" ], "gap": "The accept vectors reach only the entry points an SDK's test drives them through, while the bound binds every untrusted decode, invalidation events included, and IOP-31 asks for every entry point only with reject vectors. At the commit sdks.json pins, cachekit-ts runs the two depth-32 accept vectors through its invalidation-event decoder and asserts they are rejected there, because that decoder caps depth at 3." }, "IOP-26": { "section": "Decode bounds", "binds": "sdk", "vectors": [ "decode-bounds.json:nested_fixarray_depth_1025_complete", "decode-bounds.json:nested_fixmap_depth_1025_complete" ], "gap": "These vectors reach only the decode entry points an SDK's test drives them through, while the rule binds every untrusted decode, invalidation events included; which entry points a test covers is SDK test code (IOP-31), recorded per SDK in sdk-feature-matrix.md's \"Test vectors in CI\" row." }, "IOP-27": { "section": "Decode bounds", "binds": "sdk", "vectors": [ "decode-bounds.json:nested_array16_depth_2048", "decode-bounds.json:nested_array32_input_len_depth_1100", "decode-bounds.json:nested_map16_depth_2048", "decode-bounds.json:nested_array16_each_header_fits_sum_overclaims", "decode-bounds.json:array16_overclaim_shallow", "decode-bounds.json:array32_max_claim_alone", "decode-bounds.json:map32_max_claim_alone", "decode-bounds.json:array32_sum_wraps_u32", "decode-bounds.json:array32_sum_wraps_u32_small_first", "decode-bounds.json:map32_half_claim_wraps_u32_mul", "decode-bounds.json:fixmap_short_by_one", "decode-bounds.json:bin32_overclaim", "decode-bounds.json:str32_overclaim", "decode-bounds.json:ext32_overclaim", "decode-bounds.json:fixarray_short_by_one", "decode-bounds.json:nested_fixarray_depth_32", "decode-bounds.json:nested_fixmap_depth_32", "decode-bounds.json:array16_256_backed_nils" ], "gap": "A verdict cannot fail this: every overclaim vector is truncated, so any decoder rejects it at end of input whatever its guard does; only an SDK test that asserts the guard's own error (IOP-31) exercises it. The accept vectors do pin a budget set too tight." }, "IOP-28": { "section": "Decode bounds", "binds": "sdk", "vectors": [ "decode-bounds.json:nested_array16_depth_2048", "decode-bounds.json:nested_array32_input_len_depth_1100", "decode-bounds.json:nested_map16_depth_2048", "decode-bounds.json:nested_array16_each_header_fits_sum_overclaims", "decode-bounds.json:array16_overclaim_shallow", "decode-bounds.json:array32_max_claim_alone", "decode-bounds.json:map32_max_claim_alone", "decode-bounds.json:array32_sum_wraps_u32", "decode-bounds.json:array32_sum_wraps_u32_small_first", "decode-bounds.json:map32_half_claim_wraps_u32_mul", "decode-bounds.json:fixmap_short_by_one", "decode-bounds.json:bin32_overclaim", "decode-bounds.json:str32_overclaim", "decode-bounds.json:ext32_overclaim", "decode-bounds.json:fixarray_short_by_one" ], "gap": "A verdict cannot fail this: every overclaim vector is truncated, so any decoder rejects it at end of input whatever its guard does; only an SDK test that asserts the guard's own error (IOP-31) exercises it." }, "IOP-29": { "section": "Decode bounds", "binds": "sdk", "vectors": [ "decode-bounds.json:fixarray_short_by_one", "decode-bounds.json:fixmap_short_by_one", "decode-bounds.json:incomplete_within_slot_budget", "decode-bounds.json:incomplete_map_within_slot_budget" ], "gap": "Any decoder rejects every mapped vector at end of input, so a verdict cannot show which check rejected it: only an SDK test that asserts its guard's own error (IOP-31) fails a guard that skips the completeness check, which the two incomplete_*within_slot_budget vectors are built for (each fits the slot budget, every header fits, and the map one passes a check that owes a pair as one item)." }, "IOP-30": { "section": "Decode bounds", "binds": "sdk", "vectors": [ "decode-bounds.json:array32_sum_wraps_u32", "decode-bounds.json:array32_sum_wraps_u32_small_first", "decode-bounds.json:map32_half_claim_wraps_u32_mul" ], "gap": "A verdict cannot fail this: every overclaim vector is truncated, so any decoder rejects it at end of input whatever its guard does; only an SDK test that asserts the guard's own error (IOP-31) exercises it." }, "IOP-31": { "section": "Decode bounds", "binds": "sdk", "vectors": [ "decode-bounds.json:nested_array16_depth_2048", "decode-bounds.json:nested_array32_input_len_depth_1100", "decode-bounds.json:nested_map16_depth_2048", "decode-bounds.json:nested_array16_each_header_fits_sum_overclaims", "decode-bounds.json:nested_fixarray_depth_1025_complete", "decode-bounds.json:nested_fixmap_depth_1025_complete", "decode-bounds.json:array16_overclaim_shallow", "decode-bounds.json:array32_max_claim_alone", "decode-bounds.json:map32_max_claim_alone", "decode-bounds.json:array32_sum_wraps_u32", "decode-bounds.json:array32_sum_wraps_u32_small_first", "decode-bounds.json:map32_half_claim_wraps_u32_mul", "decode-bounds.json:fixmap_short_by_one", "decode-bounds.json:bin32_overclaim", "decode-bounds.json:str32_overclaim", "decode-bounds.json:ext32_overclaim", "decode-bounds.json:fixarray_short_by_one", "decode-bounds.json:incomplete_within_slot_budget", "decode-bounds.json:incomplete_map_within_slot_budget" ], "gap": "Which entry points an SDK's test drives each vector through, and which error it asserts, is SDK test code no vector can check; sdk-feature-matrix.md's \"Test vectors in CI\" row records it per SDK." }, "IOP-32": { "section": "Decode bounds", "binds": "sdk", "vectors": [ "decode-bounds.json:nested_array16_depth_2048", "decode-bounds.json:nested_array32_input_len_depth_1100", "decode-bounds.json:nested_map16_depth_2048", "decode-bounds.json:nested_array16_each_header_fits_sum_overclaims", "decode-bounds.json:nested_fixarray_depth_1025_complete", "decode-bounds.json:nested_fixmap_depth_1025_complete", "decode-bounds.json:array16_overclaim_shallow", "decode-bounds.json:array32_max_claim_alone", "decode-bounds.json:map32_max_claim_alone", "decode-bounds.json:array32_sum_wraps_u32", "decode-bounds.json:array32_sum_wraps_u32_small_first", "decode-bounds.json:map32_half_claim_wraps_u32_mul", "decode-bounds.json:fixmap_short_by_one", "decode-bounds.json:bin32_overclaim", "decode-bounds.json:str32_overclaim", "decode-bounds.json:ext32_overclaim", "decode-bounds.json:fixarray_short_by_one", "decode-bounds.json:incomplete_within_slot_budget", "decode-bounds.json:incomplete_map_within_slot_budget" ], "gap": "Which entry points an SDK's test drives each vector through, and which error it asserts, is SDK test code no vector can check; sdk-feature-matrix.md's \"Test vectors in CI\" row records it per SDK." }, "IOP-33": { "section": "Decode bounds", "binds": "sdk", "vectors": [ "decode-bounds.json:nested_array16_depth_2048", "decode-bounds.json:nested_array32_input_len_depth_1100", "decode-bounds.json:nested_map16_depth_2048", "decode-bounds.json:nested_array16_each_header_fits_sum_overclaims", "decode-bounds.json:nested_fixarray_depth_1025_complete", "decode-bounds.json:nested_fixmap_depth_1025_complete", "decode-bounds.json:array16_overclaim_shallow", "decode-bounds.json:array32_max_claim_alone", "decode-bounds.json:map32_max_claim_alone", "decode-bounds.json:array32_sum_wraps_u32", "decode-bounds.json:array32_sum_wraps_u32_small_first", "decode-bounds.json:map32_half_claim_wraps_u32_mul", "decode-bounds.json:fixmap_short_by_one", "decode-bounds.json:bin32_overclaim", "decode-bounds.json:str32_overclaim", "decode-bounds.json:ext32_overclaim", "decode-bounds.json:fixarray_short_by_one", "decode-bounds.json:incomplete_within_slot_budget", "decode-bounds.json:incomplete_map_within_slot_budget" ], "gap": "These vectors reach only the decode entry points an SDK's test drives them through, while the rule binds every untrusted decode, invalidation events included; which entry points a test covers is SDK test code (IOP-31), recorded per SDK in sdk-feature-matrix.md's \"Test vectors in CI\" row." }, "IOP-34": { "section": "Decode bounds", "binds": "sdk", "vectors": [ "decode-bounds.json:nested_fixarray_depth_32", "decode-bounds.json:nested_fixmap_depth_32", "decode-bounds.json:array16_256_backed_nils" ], "gap": "The accept vectors reach only the entry points an SDK's test drives them through, while the rule binds every untrusted decode, invalidation events included, and IOP-31 asks for every entry point only with reject vectors. At the commit sdks.json pins, cachekit-ts runs the accept vectors through its invalidation-event decoder and asserts they are rejected there: the two depth-32 vectors by its depth cap of 3, and array16_256_backed_nils because an event must be a map." }, "IOP-35": { "section": "Test Vectors", "binds": "sdk", "vectors": [ "interop-mode.json:reject_nan", "interop-mode.json:reject_infinity", "interop-mode.json:reject_negative_infinity", "interop-mode.json:reject_int_overflow", "interop-mode.json:reject_int_underflow", "interop-mode.json:reject_naive_datetime", "interop-mode.json:reject_uppercase_namespace", "interop-mode.json:reject_colon_in_operation", "interop-mode.json:reject_trailing_newline", "interop-mode.json:reject_reserved_namespace_ns", "interop-mode.json:reject_reserved_namespace_nsapi", "interop-mode.json:reject_double_dot_namespace", "interop-mode.json:reject_double_dot_operation", "interop-mode.json:reject_namespace_too_long", "interop-mode.json:reject_operation_too_long", "interop-mode.json:reject_leading_dot_namespace", "interop-mode.json:reject_leading_hyphen_operation", "interop-mode.json:reject_leading_underscore_operation", "interop-mode.json:reject_empty_namespace", "interop-mode.json:reject_empty_operation", "interop-mode.json:reject_non_ascii_digit_namespace", "interop-mode.json:reject_non_ascii_letter_operation", "interop-mode.json:reject_fullwidth_operation", "interop-mode.json:reject_slash_namespace", "interop-mode.json:reject_space_operation", "interop-mode.json:reject_uppercase_operation", "interop-mode.json:reject_colon_in_namespace", "interop-mode.json:reject_trailing_newline_operation", "interop-mode.json:reject_naive_datetime_nested", "interop-mode.json:reject_nan_nested", "interop-mode.json:reject_int_overflow_in_list", "interop-mode.json:reject_int_underflow_in_map", "interop-mode.json:reject_int_overflow_in_map", "interop-mode.json:reject_int_underflow_in_list" ] }, "IOP-36": { "section": "Test Vectors", "binds": "sdk", "vectors": [ "interop-mode.json" ], "gap": "Whether an SDK's suite runs every vector, not just vendors the file, is SDK test code no vector can check; sdk-feature-matrix.md's \"Test vectors in CI\" row records it per SDK." } }, "retired": {} }, "spec/saas-api.md": { "prefix": "API", "next": 108, "requirements": { "API-1": { "section": "Authentication", "binds": "sdk", "gap": "No vector covers request headers, so nothing here catches a client that, with a ck_sdk_ key, sends X-CacheKit-L1-Status on GET and PUT but not on HEAD, DELETE, /ttl, /lock or health requests; the server's 400 shows the omission only on a live call." }, "API-2": { "section": "Authentication", "binds": "server", "gap": "No public vector exercises the server's responses, so nothing here catches a server that omits either header on one path, such as an error raised before the cache handler, the OPTIONS preflight or the health endpoint." }, "API-3": { "section": "Authentication", "binds": "server", "gap": "A tier's internal cache key is invisible to any public vector; only a test that sends two tenants' requests for one path through the tier catches a tier keyed by URL alone." }, "API-4": { "section": "Encoding rules", "binds": "sdk", "vectors": [ "path-encoding.json:default:../../admin", "path-encoding.json:x/../../health", "path-encoding.json:k?x=1#f", "path-encoding.json:a\\..\\health", "path-encoding.json:.\t.", "path-encoding.json:.\r\n.", "path-encoding.json:.. ", "path-encoding.json:health\u000b", "path-encoding.json:%2e%2e", "path-encoding.json:‥" ], "tests": [ "tools/path-encoding-verify.py:verify" ], "gap": "The rows pin the encoder, not the request path (API-15)." }, "API-5": { "section": "Encoding rules", "binds": "sdk", "vectors": [ "path-encoding.json:ns:test:func:__main__.get_user:args:3870b2ea5735ae639ded9450ef117768db676f037bec636503796c5b81095153:1s", "path-encoding.json:default:../../admin", "path-encoding.json:x/../../health", "path-encoding.json:k?x=1#f", "path-encoding.json:a b", "path-encoding.json:100%", "path-encoding.json:a:..", "path-encoding.json:ns:key", "path-encoding.json:f(x)!*'", "path-encoding.json:a\\..\\health", "path-encoding.json:.\t.", "path-encoding.json:.\r\n.", "path-encoding.json:.. ", "path-encoding.json:health\u000b", "path-encoding.json:a\u0000b\u001fc\u007fd", "path-encoding.json:%2e%2e", "path-encoding.json:User%3AKey", "path-encoding.json:café", "path-encoding.json:😀", "path-encoding.json:‥", "path-encoding.json:User:Key", "path-encoding.json:a+b&c;d,e@f$g[h]i", "path-encoding.json:a\"bd^e`f{g|h}i" ], "tests": [ "tools/path-encoding-verify.py:verify" ], "gap": "The rows pin the encoder, not the request path (API-15), and a test that reads only the parsed path cannot see a raw byte the URL parser encodes itself (API-7)." }, "API-6": { "section": "Encoding rules", "binds": "sdk", "vectors": [ "path-encoding.json:ns:test:func:__main__.get_user:args:3870b2ea5735ae639ded9450ef117768db676f037bec636503796c5b81095153:1s", "path-encoding.json:default:../../admin", "path-encoding.json:x/../../health", "path-encoding.json:a:..", "path-encoding.json:..a", "path-encoding.json:a-b~c", "path-encoding.json:User:Key" ], "tests": [ "tools/path-encoding-verify.py:verify" ], "gap": "The rows pin the encoder, not the request path (API-15)." }, "API-7": { "section": "Encoding rules", "binds": "sdk", "vectors": [ "path-encoding.json:ns:test:func:__main__.get_user:args:3870b2ea5735ae639ded9450ef117768db676f037bec636503796c5b81095153:1s", "path-encoding.json:default:../../admin", "path-encoding.json:x/../../health", "path-encoding.json:k?x=1#f", "path-encoding.json:a b", "path-encoding.json:100%", "path-encoding.json:a:..", "path-encoding.json:ns:key", "path-encoding.json:f(x)!*'", "path-encoding.json:a\\..\\health", "path-encoding.json:.\t.", "path-encoding.json:.\r\n.", "path-encoding.json:.. ", "path-encoding.json:health\u000b", "path-encoding.json:a\u0000b\u001fc\u007fd", "path-encoding.json:%2e%2e", "path-encoding.json:User%3AKey", "path-encoding.json:café", "path-encoding.json:😀", "path-encoding.json:‥", "path-encoding.json:a+b&c;d,e@f$g[h]i", "path-encoding.json:a\"bd^e`f{g|h}i" ], "tests": [ "tools/path-encoding-verify.py:verify" ], "gap": "The rows pin the encoder, not the request path (API-15), and a test that reads only the parsed path cannot tell a raw byte the URL parser encodes itself (a space or a C0 control other than tab, LF and CR inside the key, DEL, a byte of 0x80 or above, `\"<>^{}` or the backtick) from the encoder's `%HH`, though the parser then sends the same bytes." }, "API-8": { "section": "Encoding rules", "binds": "sdk", "vectors": [ "path-encoding.json:", "path-encoding.json:.", "path-encoding.json:..", "path-encoding.json:health", "path-encoding.json:ttl", "path-encoding.json:lock" ], "gap": "The rows reach whichever function an SDK's test calls: a client that guards the key where it builds one endpoint's URL but builds its /ttl or /lock URLs elsewhere passes them, and so does one that raises only after a request is built, unless the test asserts the raise comes first (API-14)." }, "API-9": { "section": "Encoding rules", "binds": "sdk", "vectors": [ "path-encoding.json:", "path-encoding.json:.", "path-encoding.json:..", "path-encoding.json:health", "path-encoding.json:ttl", "path-encoding.json:lock" ], "gap": "As API-8: the rows reach only the entry points an SDK's test drives, and only the test discipline of API-14 shows the error comes before a URL is built." }, "API-10": { "section": "Encoding rules", "binds": "sdk", "vectors": [ "path-encoding.json:", "path-encoding.json:.", "path-encoding.json:..", "path-encoding.json:health", "path-encoding.json:ttl", "path-encoding.json:lock" ], "gap": "An SDK must not leave the check to the server, which cannot make it: a dot key never reaches the server's validator, a route-token key addresses another route or key, and an empty key addresses no entry. So the rows fail a client that relies on the server, but, as on API-8, only on the entry points its test drives." }, "API-11": { "section": "Encoding rules", "binds": "sdk", "vectors": [ "path-encoding.json:default:../../admin", "path-encoding.json:x/../../health", "path-encoding.json:a:..", "path-encoding.json:..a" ], "tests": [ "tools/path-encoding-verify.py:verify" ], "gap": "The rows pin the encoder, not the request path (API-15): a client whose request builder refuses any key that contains '..' as a traversal guard passes them when its test calls only the encoder." }, "API-12": { "section": "Encoding rules", "binds": "server", "gap": "No public vector exercises the server's routes: the route-token rows pin the reserved set clients hard-code, which a server that adds a route under /v1/cache/ leaves unchanged." }, "API-13": { "section": "Encoding rules", "binds": "server", "gap": "As API-12: the empty-key row pins only that clients never send /v1/cache/ itself, not what the server serves there." }, "API-14": { "section": "Encoding rules", "binds": "sdk", "gap": "Whether an SDK's test asserts that each row raises before any URL or request is built, on every entry point, is SDK test code no vector can check: a suite that asserts something weaker passes every reject row." }, "API-15": { "section": "Encoding rules", "binds": "sdk", "gap": "Whether an SDK's test reads the parsed path of the request it actually builds, rather than the encoder's return value or a template string, is SDK test code no vector can check; until it does, the encoding rules' rows pin the encoder alone." }, "API-16": { "section": "Encoding rules", "binds": "sdk", "vectors": [ "path-encoding.json:default:../../admin", "path-encoding.json:x/../../health" ], "tests": [ "tools/path-encoding-verify.py:verify" ], "gap": "The lead-in's one item without an id of its own, a '/' in a key reaching the server as %2F, is pinned on the encoder, not the request path (API-15); API-17 owns the other item." }, "API-17": { "section": "Encoding rules", "binds": "sdk", "vectors": [ "path-encoding.json:100%", "path-encoding.json:ns:key", "path-encoding.json:%2e%2e", "path-encoding.json:User%3AKey" ], "tests": [ "tools/path-encoding-verify.py:verify" ], "gap": "The rows pin one call of the encoder: a client that hands the encoded segment to a URL builder that escapes '%' again, turning ns%3Akey into ns%253Akey, passes them unless its test asserts on the parsed request path (API-15)." }, "API-18": { "section": "GET /v1/cache/{key}", "binds": "sdk", "vectors": [ "freshness-headers.json:freshness_absent", "freshness-headers.json:freshness_fresh", "freshness-headers.json:freshness_stale", "freshness-headers.json:freshness_capitalised", "freshness-headers.json:freshness_uppercase_stale", "freshness-headers.json:freshness_empty", "freshness-headers.json:freshness_nbsp_suffix", "freshness-headers.json:freshness_list_fresh_first", "freshness-headers.json:freshness_list_fresh_last" ], "tests": [ "tools/freshness-headers-verify.py:verify" ], "gap": "The rows reach the parser an SDK's test hands each value, not its read path: whether it applies the result to the GET 200 it received where it backfills a local cache (API-58), revalidates or treats a stale hit as a miss, and what it does with two field lines of the header, which the rows hold only combined into one value." }, "API-19": { "section": "Remaining Freshness", "binds": "server", "gap": "No public vector exercises a re-serving tier, so nothing here catches a tier that passes a pre-signal store's missing X-CacheKit-Fresh-For through, or omits it for a copy it populated without a hint." }, "API-22": { "section": "Remaining Freshness", "binds": "server", "gap": "As API-19, for a tier that reads only X-CacheKit-TTL and so records a write that carried legacy X-TTL as unbounded." }, "API-23": { "section": "Remaining Freshness", "binds": "server", "gap": "As API-19, for a tier that omits the header, rather than sending 0, for a copy whose remainder it does not know." }, "API-24": { "section": "Remaining Freshness", "binds": "server", "gap": "As API-19, for a tier that strips a received X-CacheKit-Fresh-For, or drops it when it re-serves the copy." }, "API-25": { "section": "Remaining Freshness", "binds": "server", "gap": "As API-19, for a tier that keeps no record at populate of whether its copy was unbounded or merely unhinted." }, "API-26": { "section": "Remaining Freshness", "binds": "server", "gap": "As API-19, for a tier that cannot tell an unbounded copy from an unhinted one and omits the header for both." }, "API-28": { "section": "Remaining Freshness", "binds": "sdk", "gap": "No vector reaches an SDK's local cache, so nothing here catches one that creates a local entry, or extends its deadline, on a HEAD 200." }, "API-29": { "section": "Remaining Freshness", "binds": "sdk", "gap": "No vector reaches an SDK's local cache, so nothing here catches one that backfills with its configured local TTL whatever X-CacheKit-Fresh-For says." }, "API-30": { "section": "Remaining Freshness", "binds": "sdk", "vectors": [ "freshness-headers.json:fresh_for_zero" ], "tests": [ "tools/freshness-headers-verify.py:verify" ], "gap": "The row reaches the parser an SDK's test hands the value: a parser that reads 0 as no value fails it, but no vector reaches the local cache, so nothing here catches an SDK that parses 0 and backfills anyway (API-29)." }, "API-31": { "section": "Remaining Freshness", "binds": "sdk", "gap": "As API-29, for an SDK that serves a local copy past its bound through client-side stale-while-revalidate or a local stale-grace policy." }, "API-32": { "section": "Remaining Freshness", "binds": "server", "gap": "No public vector exercises the server's headers, so nothing here catches a server or tier that emits a value of more than seven digits or over 2,592,000; API-33 is the SDK side." }, "API-33": { "section": "Remaining Freshness", "binds": "sdk", "vectors": [ "freshness-headers.json:fresh_for_empty", "freshness-headers.json:fresh_for_negative", "freshness-headers.json:fresh_for_plus_sign", "freshness-headers.json:fresh_for_decimal", "freshness-headers.json:fresh_for_exponent", "freshness-headers.json:fresh_for_hex", "freshness-headers.json:fresh_for_digit_separator", "freshness-headers.json:fresh_for_inner_space", "freshness-headers.json:fresh_for_nbsp_prefix", "freshness-headers.json:fresh_for_eight_digits", "freshness-headers.json:fresh_for_over_cap", "freshness-headers.json:fresh_for_wraps_32_bit", "freshness-headers.json:fresh_for_over_64_bits", "freshness-headers.json:fresh_for_4301_digits", "freshness-headers.json:fresh_for_superscript_two", "freshness-headers.json:fresh_for_arabic_indic_one", "freshness-headers.json:fresh_for_list" ], "tests": [ "tools/freshness-headers-verify.py:verify" ], "gap": "The rows reach the parser an SDK's test hands each value, not its read path: whether the GET 200's header reaches it as its HTTP stack delivers the bytes and the result bounds the backfill (API-29), and what it does with two field lines of the header, which the rows hold only combined into one value." }, "API-34": { "section": "Remaining Freshness", "binds": "sdk", "vectors": [ "freshness-headers.json:fresh_for_eight_digits", "freshness-headers.json:fresh_for_wraps_32_bit", "freshness-headers.json:fresh_for_over_64_bits", "freshness-headers.json:fresh_for_4301_digits" ], "tests": [ "tools/freshness-headers-verify.py:verify" ], "gap": "A parser that converts first, with arithmetic that neither wraps nor fails on a long value, and checks the length after returns the same values, so the order shows only through the conversions the rows break (32- and 64-bit integers, CPython's 4,300-digit limit); and the rows reach the parser, not the read path (API-33)." }, "API-36": { "section": "Remaining Freshness", "binds": "sdk", "gap": "As API-103, for an SDK that keeps a copy after a timeout or a transport error and starts its clock only at a later read, or never." }, "API-37": { "section": "Remaining Freshness", "binds": "sdk", "gap": "As API-103, for an SDK that serves such a copy past its bound through a local stale-grace policy." }, "API-38": { "section": "Remaining Freshness", "binds": "caller", "gap": "The TTL an application gives a security-sensitive cache, and whether it versions its keys, are its own choices, which no vector or SDK test can observe." }, "API-39": { "section": "Remaining Freshness", "binds": "caller", "gap": "As API-38: whether a key whose TTL is a revocation boundary is written with an explicit TTL is the application's configuration." }, "API-42": { "section": "PUT /v1/cache/{key}", "binds": "sdk", "gap": "No vector maps a duration to a header, so nothing here catches a client that sends a sub-second TTL unrounded, or truncates it to 0 and then rejects it or sends 0." }, "API-43": { "section": "PUT /v1/cache/{key}", "binds": "server", "gap": "No public vector exercises the server, so nothing here catches one that ignores X-TTL, or prefers it to X-CacheKit-TTL when a write carries both." }, "API-44": { "section": "PUT /v1/cache/{key}", "binds": "sdk", "gap": "No vector covers request headers, so nothing here catches a client that sends X-TTL instead of, or beside, X-CacheKit-TTL." }, "API-45": { "section": "PUT /v1/cache/{key}", "binds": "sdk", "gap": "No vector presents an error response, so nothing here catches a client whose retry policy counts 413 as transient." }, "API-46": { "section": "PUT /v1/cache/{key}", "binds": "sdk", "gap": "As API-45, for a client that reports a value too large only when its own size check says so, and surfaces any other 413 as a generic error." }, "API-47": { "section": "HEAD /v1/cache/{key}", "binds": "server", "gap": "No public vector exercises the server, so nothing here catches a HEAD handler that answers 404 for an entry in its stale window, which GET still serves, or returns a body." }, "API-48": { "section": "HEAD /v1/cache/{key}", "binds": "sdk", "gap": "No vector presents a HEAD response, so nothing here catches a client that reports a key absent when its HEAD response is labelled stale." }, "API-49": { "section": "HEAD /v1/cache/{key}", "binds": "sdk", "gap": "As API-48, for a client that applies GET's defaulting to a HEAD label, for example firing revalidation on an unrecognized one (API-60)." }, "API-50": { "section": "Entry lifecycle", "binds": "server", "gap": "No public vector exercises the store's clock, so nothing here catches a store that stamps from the unfloored time, putting fresh_until between seconds." }, "API-51": { "section": "Entry lifecycle", "binds": "server", "gap": "As API-50, for PATCH /v1/cache/{key}/ttl." }, "API-52": { "section": "Entry lifecycle", "binds": "server", "gap": "No public vector exercises the store, so nothing here catches one that serves a stale entry past evict_at, for example until a background sweep removes it." }, "API-53": { "section": "Entry lifecycle", "binds": "sdk", "gap": "No vector drives an SDK's read path, so nothing here catches an SDK that computes an entry's freshness from its own clock and the TTL it wrote, instead of the server's label." }, "API-54": { "section": "Validation", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "gap": "As API-42, for X-CacheKit-Stale-TTL." }, "API-55": { "section": "Write semantics", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "gap": "No vector covers request headers, so nothing here catches a revalidation write that omits X-CacheKit-TTL, which stores a no-expiry entry (or draws a 400 if it still sends X-CacheKit-Stale-TTL), or omits X-CacheKit-Stale-TTL, which leaves no stale window." }, "API-56": { "section": "Write semantics", "binds": "server", "gap": "No public vector exercises the server, so nothing here catches a PATCH /ttl handler that renews an entry past fresh_until instead of answering 409." }, "API-57": { "section": "Reading a stale entry", "binds": "sdk", "vectors": [ "freshness-headers.json:freshness_stale" ], "tests": [ "tools/freshness-headers-verify.py:verify" ], "gap": "The row reaches the parser an SDK's test hands the value, not its read path: a parser that raises on `stale` fails it, but nothing here catches a read path that classifies the GET 200 as stale and then raises instead of serving it or taking its local miss policy." }, "API-58": { "section": "Reading a stale entry", "binds": "sdk", "gap": "No vector reaches an SDK's local cache, so nothing here catches one that backfills a stale response, as fresh or as locally stale." }, "API-59": { "section": "Reading a stale entry", "binds": "sdk", "gap": "As API-58, for an SDK whose local copy outlives the store's evict_at because it ignores X-CacheKit-Fresh-For." }, "API-60": { "section": "Reading a stale entry", "binds": "sdk", "gap": "No vector drives a background recompute, so nothing here catches a client that starts one from a HEAD labelled stale. Unlike API-61 to API-63, the rule binds every SDK: a local refresh-ahead is a background recompute too." }, "API-61": { "section": "Revalidation flow (SDK)", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "gap": "As API-60, for a client that holds a stale read until its recompute or its lock attempt finishes." }, "API-62": { "section": "Revalidation flow (SDK)", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "gap": "No vector presents a lock response, so nothing here catches a client that waits, or retries the lock, when another client holds it." }, "API-63": { "section": "Semantics notes", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "gap": "No vector covers request headers, so nothing here catches a client that counts a background revalidation as a miss in X-CacheKit-Misses." }, "API-64": { "section": "Semantics notes", "binds": "caller", "gap": "Whether a key's readers rely on hard expiry is a fact of the deployment, which no vector or SDK test can observe." }, "API-65": { "section": "POST /v1/cache/{key}/lock", "binds": "sdk", "gap": "No vector presents a lock response, so nothing here catches a client that takes a 200 as its lease unless lock_id is null, so that an absent or empty lock_id counts as held." }, "API-66": { "section": "POST /v1/cache/{key}/lock", "binds": "sdk", "gap": "As API-65, for a client that decides on the status alone, for example taking every non-200 for contention." }, "API-67": { "section": "DELETE /v1/cache/{key}/lock", "binds": "sdk", "gap": "No vector covers request headers, so nothing here catches a client that sends the lock id as ?lock_id=." }, "API-68": { "section": "DELETE /v1/cache/{key}/lock", "binds": "server", "gap": "No public vector exercises the server, so nothing here catches a release handler that ignores the legacy ?lock_id=, or prefers it to the header." }, "API-69": { "section": "DELETE /v1/cache/{key}/lock", "binds": "sdk", "gap": "As API-67, for a client that sends both the header and the query parameter." }, "API-70": { "section": "GET /v1/cache/{key}/ttl", "binds": "server", "gap": "No public vector exercises the server, so nothing here catches a /ttl handler that answers 404 for a no-expiry entry." }, "API-71": { "section": "GET /v1/cache/{key}/ttl", "binds": "sdk", "gap": "No vector presents a /ttl response, so nothing here catches a client that coerces a null ttl to 0 or to a negative sentinel such as -1, or raises on it." }, "API-72": { "section": "GET /v1/cache/{key}/ttl", "binds": "caller", "gap": "Whether every /ttl reader of a key accepts null is a fact of the deployment, which no vector or SDK test can observe." }, "API-73": { "section": "PATCH /v1/cache/{key}/ttl", "binds": "server", "gap": "As API-56." }, "API-74": { "section": "SSRF Protection", "binds": "sdk", "gap": "No vector holds an API URL, so nothing here checks HTTPS-only, the listed private ranges or the hostname allowlist." }, "API-75": { "section": "Counting outcomes", "binds": "sdk", "gap": "No vector covers request headers, so nothing here catches a client that counts a plain read twice, or counts a read that failed at the backend; only the SDK's own counter tests do." }, "API-76": { "section": "Counting outcomes", "binds": "sdk", "gap": "No vector covers request headers, so nothing here catches a client that counts a wrapped call that runs the function as anything but one miss, a joined in-process caller as anything but one L1 hit, or a miss on each single-flight re-poll or lock re-check; only the SDK's own counter tests do." }, "API-77": { "section": "Optional Metrics Headers", "binds": "sdk", "gap": "No vector covers request headers, so nothing here catches a client that sends the optional metrics headers, or a report built for them, through a Redis, Memcached or File backend or to any host but its configured CachekitIO API." }, "API-78": { "section": "L1 hit rate", "binds": "sdk", "gap": "No vector covers request headers, so nothing here catches a client that computes X-CacheKit-L1-Hit-Rate as L1-Hits / (L1-Hits + L2-Hits), sends it with other than three decimal places, or sends other than 0.000 for all-zero counters." }, "API-79": { "section": "L1 hit latency", "binds": "sdk", "gap": "No vector reaches an SDK's timing, so nothing here catches a client that adds L2 hits, misses or failed calls to the latency report; only the SDK's own tests do." }, "API-80": { "section": "L1 hit latency", "binds": "sdk", "gap": "As API-79, for a client that adds a joined in-process call, an existence check or a read made inside a wrapped call." }, "API-81": { "section": "L1 hit latency", "binds": "sdk", "gap": "As API-79, for a client that decides to time a call after it knows the outcome, samples by a counter, lets the key, arguments, position, outcome or duration sway the draw, or draws with a probability other than the 1/N it sends." }, "API-82": { "section": "L1 hit latency", "binds": "sdk", "gap": "No vector covers request headers, so nothing here catches a client that sends latency headers timed with a clock that does not advance during synchronous execution or resolves coarser than 100 ns, such as on Cloudflare Workers." }, "API-83": { "section": "L1 hit latency", "binds": "sdk", "gap": "No vector reaches a forked process, so nothing here catches a child that inherits and re-sends its parent's accumulated report." }, "API-84": { "section": "L1 hit latency", "binds": "sdk", "gap": "No vector covers request headers, so nothing here catches a client that attaches a latency report to a health request." }, "API-85": { "section": "L1 hit latency", "binds": "sdk", "gap": "No vector covers concurrent requests, so nothing here catches a client whose concurrent attempts carry the same timed hit, whose buckets and sum describe different hits, or that sends cumulative rather than per-report values." }, "API-86": { "section": "L1 hit latency", "binds": "sdk", "gap": "No vector covers request headers, so nothing here catches a client that adds a consumed report back to its accumulator, or whose own or its HTTP library's retries resend it after an attempt that may have been delivered." }, "API-87": { "section": "L1 hit latency", "binds": "sdk", "gap": "No vector covers request headers, so nothing here catches a client that sends a latency value outside the integer grammar, other than sixteen buckets, all-zero buckets, a sum of 0, a one-in value outside 1 to 1,000,000, a duration in the wrong bucket, or a duration above 60 s." }, "API-88": { "section": "Server handling", "binds": "server", "gap": "No public vector exercises the server, so nothing here catches one that drops the session headers because a latency header is malformed, or the latency headers because the session headers are missing, malformed or rejected." }, "API-90": { "section": "Server handling", "binds": "server", "gap": "As API-88, for a server that answers a request with an error because an optional metrics header is missing or malformed." }, "API-91": { "section": "Server handling", "binds": "server", "gap": "No public vector exercises the server, so nothing here catches one that drops latency sets deterministically, such as once a write budget is spent, or keeps them without recording the probability it kept each one." }, "API-92": { "section": "Server handling", "binds": "server", "gap": "How a server's cross-tenant or public latency aggregate weights tenants is server code no public vector or SDK test can observe." }, "API-93": { "section": "Server handling", "binds": "server", "gap": "Which inputs a server's billing and rate-limit verdicts read, including whether billing reads X-CacheKit-L1-Status, is server code no public vector or SDK test can observe." }, "API-94": { "section": "Telemetry switch", "binds": "sdk", "gap": "No vector reaches an SDK's configuration, so nothing here catches a client that lacks the telemetry setting, defaults it to true or false, or ignores CACHEKIT_TELEMETRY." }, "API-95": { "section": "Telemetry switch", "binds": "sdk", "gap": "As API-94, for a client that lets a configuration loader fill the setting from the environment, lets on win over off, reads an unrecognised value as on, or re-reads the controls after construction." }, "API-96": { "section": "Telemetry switch", "binds": "sdk", "gap": "No vector covers request headers, so nothing here catches a client that, with telemetry off, still sends a session or latency header." }, "API-89": { "section": "Server handling", "binds": "server", "gap": "No public vector exercises the server, so nothing here catches one that records a latency set with a missing or malformed header, a sum of 0, a One-In outside 1 to 1,000,000, or a sum its buckets cannot produce." }, "API-97": { "section": "Plan maximum TTL", "binds": "server", "gap": "No public vector exercises the server, so nothing here catches one that rejects a TTL over the tenant's plan maximum, stores it unclamped, or keeps a stale window that carries the entry past the maximum." }, "API-98": { "section": "Plan maximum TTL", "binds": "server", "gap": "As API-97, for a PATCH /ttl handler that stores the requested ttl past the plan maximum, or shortens the fresh window to make room for the stored stale window." }, "API-99": { "section": "PUT /v1/cache/{key}", "binds": "server", "gap": "No public vector exercises the server, so nothing here catches one that stores a write without TTL headers under any default other than the tenant's plan maximum TTL." }, "API-100": { "section": "Remaining Freshness", "binds": "server", "gap": "No public vector exercises the server's responses, so nothing here catches a clamping store that answers a bounded PUT without X-CacheKit-Fresh-For, or reports the stated TTL instead of the remainder it stamped." }, "API-101": { "section": "Remaining Freshness", "binds": "server", "gap": "As API-100, for a deployment that clamps writes before its store emits X-CacheKit-Fresh-For on PUT, or while a server-side hop that relays the PUT 2xx drops the header or a tier bounds its copy by the stated TTL." }, "API-102": { "section": "Remaining Freshness", "binds": "server", "gap": "As API-19, for a tier or other server-side hop that drops the X-CacheKit-Fresh-For of the store's PUT 2xx, or relays a larger value." }, "API-103": { "section": "Remaining Freshness", "binds": "sdk", "gap": "No vector reaches an SDK's write path or its local cache, so nothing here catches one that bounds its copy of an accepted write by the local TTL alone, keeps the stated TTL when the PUT 2xx reported a shorter bound, or lengthens the copy by X-CacheKit-Stale-TTL." }, "API-104": { "section": "PATCH /v1/cache/{key}/ttl", "binds": "sdk", "gap": "No vector reaches an SDK's local cache, so nothing here catches one that re-anchors its local copy to the requested ttl when a PATCH /ttl succeeds, though the store may have clamped it." }, "API-105": { "section": "Remaining Freshness", "binds": "server", "gap": "As API-19, for a tier that stamps each hit with its copy's original bound instead of the decayed remainder, or bounds its copy of a forwarded write by the stated TTL when the store's PUT 2xx reported a shorter bound." }, "API-106": { "section": "Remaining Freshness", "binds": "server", "gap": "As API-19, for a tier that emits its own copy TTL when that is larger than the decayed remainder, or a remainder above 2,592,000." }, "API-107": { "section": "Remaining Freshness", "binds": "server", "gap": "As API-19, for a tier that stamps a positive value on a stale, exhausted or unknown copy, or rewrites its freshness label; or stamps a positive value on a copy of a forwarded write whose PUT 2xx carried 0 or a malformed value." } }, "retired": { "API-20": "Replaced by API-105: a tier's copy of a forwarded write takes its bound from the X-CacheKit-Fresh-For on the store's PUT 2xx when that carries one, and a malformed value is 0.", "API-21": "Replaced by API-106: the decayed remainder it bounds now comes from API-105's bound source, capped at 2,592,000.", "API-27": "Replaced by API-107: an accepted write qualifies for a positive value by its bound, which the store's PUT 2xx can set, not by its stated TTL.", "API-35": "Replaced by API-103: the writer's copy is also bounded by the X-CacheKit-Fresh-For on the PUT 2xx.", "API-40": "Merged with API-41 into API-99: the plan maximum TTL is the only default.", "API-41": "Merged with API-40 into API-99: the plan maximum TTL is the only default." } }, "spec/wire-format.md": { "prefix": "WIRE", "next": 25, "requirements": { "WIRE-1": { "section": "Byte Layout (canonical encoding)", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "vectors": [ "wire-format.json:empty_bin", "wire-format.json:simple_string_bin", "wire-format.json:binary_data_bin", "wire-format.json:large_compressible_bin", "wire-format.json:json_like_bin", "wire-format.json:single_byte_bin", "wire-format.json:width_boundary_bin16_bin", "wire-format.json:width_boundary_bin8_max_bin", "wire-format.json:width_boundary_bin16_min_bin", "wire-format.json:envelope_bin16_max", "wire-format.json:envelope_bin32_min" ], "tests": [ "tools/wire-format-reference.py:_verify_vector" ], "gap": "The bin8 edge is pinned on both sides, 255 and 256 B, by twins a write path reproduces. The bin16 edge is pinned only by envelope_bin16_max and envelope_bin32_min, whose block no compressor emits for their input, so only a test that hands their fields to an envelope encoder checks a writer there; cachekit-py and cachekit-ts expose none, since both write through cachekit-core's ByteStorage, which compresses its input. A pair a write path reproduces at that edge needs some 260 KB of incompressible hex, which the spec declines." }, "WIRE-2": { "section": "Byte Layout (canonical encoding)", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "vectors": [ "wire-format.json:empty", "wire-format.json:simple_string", "wire-format.json:binary_data", "wire-format.json:large_compressible", "wire-format.json:json_like", "wire-format.json:single_byte", "wire-format.json:width_boundary_bin16", "wire-format.json:width_boundary_bin8_max", "wire-format.json:width_boundary_bin16_min", "wire-format.json:envelope_legacy_array32_min" ], "tests": [ "tools/wire-format-reference.py:_verify_vector" ] }, "WIRE-3": { "section": "Encoding compatibility (dual-read)", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "vectors": [ "wire-format.json:empty", "wire-format.json:simple_string", "wire-format.json:binary_data", "wire-format.json:large_compressible", "wire-format.json:json_like", "wire-format.json:single_byte", "wire-format.json:width_boundary_bin16", "wire-format.json:empty_bin", "wire-format.json:simple_string_bin", "wire-format.json:binary_data_bin", "wire-format.json:large_compressible_bin", "wire-format.json:json_like_bin", "wire-format.json:single_byte_bin", "wire-format.json:width_boundary_bin16_bin", "wire-format.json:width_boundary_bin8_max", "wire-format.json:width_boundary_bin16_min", "wire-format.json:width_boundary_bin8_max_bin", "wire-format.json:width_boundary_bin16_min_bin", "wire-format.json:envelope_bin16_max", "wire-format.json:envelope_bin32_min", "wire-format.json:envelope_legacy_array32_min" ], "tests": [ "tools/wire-format-reference.py:_verify_vector" ] }, "WIRE-4": { "section": "Compressed-byte reproducibility (per-vector scoping)", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "vectors": [ "wire-format.json:empty", "wire-format.json:simple_string", "wire-format.json:binary_data", "wire-format.json:large_compressible", "wire-format.json:json_like", "wire-format.json:single_byte", "wire-format.json:width_boundary_bin16", "wire-format.json:empty_bin", "wire-format.json:simple_string_bin", "wire-format.json:binary_data_bin", "wire-format.json:large_compressible_bin", "wire-format.json:json_like_bin", "wire-format.json:single_byte_bin", "wire-format.json:width_boundary_bin16_bin", "wire-format.json:envelope_ratio_product_wraps_32_bits", "wire-format.json:width_boundary_bin8_max", "wire-format.json:width_boundary_bin16_min", "wire-format.json:width_boundary_bin8_max_bin", "wire-format.json:width_boundary_bin16_min_bin", "wire-format.json:envelope_bin16_max", "wire-format.json:envelope_bin32_min", "wire-format.json:envelope_legacy_array32_min" ], "tests": [ "tools/wire-format-reference.py:_verify_vector", "tools/wire-format-reference.py:_verify_constructed" ] }, "WIRE-5": { "section": "Compressed-byte reproducibility (per-vector scoping)", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "vectors": [ "wire-format.json:reject_original_size_over_cap", "wire-format.json:reject_original_size_wraps_u32", "wire-format.json:reject_zero_length_compressed_data", "wire-format.json:reject_ratio_bomb", "wire-format.json:reject_decompressed_length_mismatch", "decode-bounds.json", "wire-format.json:reject_envelope_arity_5", "wire-format.json:reject_envelope_arity_3", "wire-format.json:reject_checksum_nine_elements", "wire-format.json:reject_checksum_seven_elements", "wire-format.json:reject_legacy_element_above_255", "wire-format.json:reject_envelope_slots_overclaim", "wire-format.json:reject_original_size_sign_bit", "wire-format.json:reject_ratio_float32_rounds" ], "tests": [ "tools/test_wire_format_reference.py:check_reader_rejects", "tools/test_wire_format_reference.py:_check_allocation_probe" ], "gap": "The accept vectors evidence none of the bounds. A verdict on the reject vectors misses a reader without the zero-length check whose LZ4 decoder refuses an empty block, as liblz4 does, one without step 9 that zero-pads to original_size and fails the checksum instead, and one without the pre-scan, which rejects reject_envelope_slots_overclaim in its typed decode (as truncated, or as a wrong type at element 1); only a test that asserts the named error catches them (WIRE-16). The typed-decode vectors cover a fifth element, a missing fourth, a checksum of 9 and of 7 integers, and a legacy element above 255; none holds a legacy element written as a negative integer or a float, or an original_size written as a float, which a JavaScript reader that range-checks only above 255 accepts. Whether a checksum written as a bin8 of 8 bytes breaks step 2's typed decode is open: the spec gives the checksum as an array of 8 integers, and cachekit-core accepts the bin8 form, since rmp-serde reads [u8; 8] from either. The order is WIRE-11's; steps 4 and 5 carry WIRE-7, WIRE-12, WIRE-13 and WIRE-14." }, "WIRE-6": { "section": "Compressed-byte reproducibility (per-vector scoping)", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "tests": [ "tools/wire-format-reference.py:_verify_vector" ], "gap": "Both bound SDKs write through cachekit-core's lz4_flex, the canonical writer the rule exempts, so it has no subject in either; this repository's verifier is its one live judge, declaring the large_compressible pair's expected divergence (LZ4_ENCODE_DIVERGENT) instead of failing it." }, "WIRE-7": { "section": "Security Limits", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "vectors": [ "wire-format.json:reject_original_size_over_cap", "wire-format.json:reject_original_size_wraps_u32", "wire-format.json:reject_ratio_bomb", "wire-format.json:reject_original_size_sign_bit", "wire-format.json:reject_ratio_float32_rounds" ], "tests": [ "tools/test_wire_format_reference.py:check_reader_rejects" ], "gap": "A verdict shows none of the three: a reader without the size cap rejects all three cap vectors by the ratio bound, and one without the ratio bound still rejects both ratio vectors, whose blocks are not valid LZ4, so only a test that asserts the named error does (WIRE-16). No vector sits on the size cap, so a reader that also rejects an original_size of exactly 512 MiB passes them all: an accept vector there costs every reader a 512 MiB decode, and a reject vector there with an undecodable block still lets a conforming reader size its output from original_size, 512 MiB, which on wasm32 is a 512 MiB memory.grow. No vector reaches the compressed-size cap: compressed_data over 512 MiB needs an envelope over 512 MiB, which step 1 rejects first." }, "WIRE-8": { "section": "Security Limits", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "vectors": [ "decode-bounds.json:nested_array16_depth_2048", "decode-bounds.json:nested_array32_input_len_depth_1100", "decode-bounds.json:nested_map16_depth_2048", "decode-bounds.json:nested_array16_each_header_fits_sum_overclaims", "decode-bounds.json:array16_overclaim_shallow", "decode-bounds.json:array32_max_claim_alone", "decode-bounds.json:map32_max_claim_alone", "decode-bounds.json:array32_sum_wraps_u32", "decode-bounds.json:array32_sum_wraps_u32_small_first", "decode-bounds.json:map32_half_claim_wraps_u32_mul", "decode-bounds.json:fixmap_short_by_one", "decode-bounds.json:bin32_overclaim", "decode-bounds.json:str32_overclaim", "decode-bounds.json:ext32_overclaim", "decode-bounds.json:fixarray_short_by_one", "wire-format.json:reject_envelope_slots_overclaim", "wire-format.json:payload_array32_max_claim_alone", "wire-format.json:payload_nested_array16_each_header_fits_sum_overclaims" ], "gap": "A verdict cannot fail this: every overclaim vector is truncated, so any decoder rejects it whatever its guard does, at end of input or, for a typed envelope decoder, on a wrong type; only an SDK test that asserts the guard's own error (IOP-31) exercises it. reject_envelope_slots_overclaim overclaims in the envelope's bytes and the payload_reject_vectors in the payload after unwrapping, so such a test can assert each decode's guard. No released cachekit-core pre-scans the envelope (sdk-feature-matrix.md)." }, "WIRE-9": { "section": "Security Limits", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "vectors": [ "decode-bounds.json:nested_array16_depth_2048", "decode-bounds.json:nested_array32_input_len_depth_1100", "decode-bounds.json:nested_map16_depth_2048", "decode-bounds.json:nested_array16_each_header_fits_sum_overclaims", "decode-bounds.json:array16_overclaim_shallow", "decode-bounds.json:array32_max_claim_alone", "decode-bounds.json:map32_max_claim_alone", "decode-bounds.json:array32_sum_wraps_u32", "decode-bounds.json:array32_sum_wraps_u32_small_first", "decode-bounds.json:map32_half_claim_wraps_u32_mul", "decode-bounds.json:fixmap_short_by_one", "decode-bounds.json:bin32_overclaim", "decode-bounds.json:str32_overclaim", "decode-bounds.json:ext32_overclaim", "decode-bounds.json:fixarray_short_by_one", "decode-bounds.json:nested_fixarray_depth_32", "decode-bounds.json:nested_fixmap_depth_32", "decode-bounds.json:array16_256_backed_nils", "wire-format.json:reject_envelope_slots_overclaim", "wire-format.json:payload_array32_max_claim_alone", "wire-format.json:payload_nested_array16_each_header_fits_sum_overclaims" ], "gap": "A verdict cannot fail this: every overclaim vector is truncated, so any decoder rejects it whatever its guard does, at end of input or, for a typed envelope decoder, on a wrong type; only an SDK test that asserts the guard's own error (IOP-31) exercises it. The accept vectors do pin a budget set too tight. reject_envelope_slots_overclaim, whose every header fits what follows it, and payload_nested_array16_each_header_fits_sum_overclaims carry the whole-document sum into the envelope's decode and the payload's, so such a test can assert each one; no released cachekit-core pre-scans the envelope (sdk-feature-matrix.md)." }, "WIRE-10": { "section": "Decompression Bomb Detection", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "vectors": [ "wire-format.json:reject_original_size_over_cap", "wire-format.json:reject_original_size_wraps_u32", "wire-format.json:reject_original_size_sign_bit" ], "tests": [ "tools/test_wire_format_reference.py:check_reader_rejects" ], "gap": "Only a test that asserts the size-cap error shows the order: a reader that runs the ratio product first rejects all three vectors too, with the ratio error. No vector reaches the compressed-size cap (WIRE-7)." }, "WIRE-11": { "section": "Decompression Bomb Detection", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "vectors": [ "wire-format.json:reject_original_size_over_cap", "wire-format.json:reject_ratio_bomb" ], "tests": [ "tools/test_wire_format_reference.py:check_reader_rejects", "tools/test_wire_format_reference.py:_check_allocation_probe" ], "gap": "A verdict cannot show the order, and an error shows it only for a reader that decompresses first with a strict decoder and lets its error propagate. The allocation bound the spec requires on these two vectors (WIRE-17) misses a reader that allocates less than original_size: one that decompresses first with a growing decoder, holds a strict decoder's error until after the checks, or sizes its output from compressed_data, a constant, or original_size clamped below itself." }, "WIRE-12": { "section": "Decompression Bomb Detection", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "vectors": [ "wire-format.json:reject_original_size_wraps_u32", "wire-format.json:reject_original_size_sign_bit" ], "tests": [ "tools/test_wire_format_reference.py:check_reader_rejects" ], "gap": "A reader that joins the 32-bit halves rejects reject_original_size_wraps_u32 too, on length, and one that reinterprets the uint64 as an i64 rejects reject_original_size_sign_bit later, or crashes, so only a test that asserts a rejection before decompression catches either (WIRE-16)." }, "WIRE-13": { "section": "Decompression Bomb Detection", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "vectors": [ "wire-format.json:envelope_ratio_product_wraps_32_bits" ], "tests": [ "tools/test_wire_format_reference.py:check_32_bit_ratio_readers" ], "gap": "A pointer-width product is exact on a 64-bit host, so the vector fails it only on a 32-bit target (WIRE-15); an explicit 32-bit multiply fails it on any host. Since it is also the file's only multi-megabyte decode, a failure on it shows a 32-bit product only if the rejection reason is checked." }, "WIRE-14": { "section": "Decompression Bomb Detection", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "vectors": [ "wire-format.json:reject_ratio_bomb", "wire-format.json:reject_ratio_float32_rounds" ], "tests": [ "tools/test_wire_format_reference.py:check_reader_rejects" ], "gap": "A binary64 ratio, and a division that rounds up, give the product's verdict at every legal size, so no vector can fail them. Both ratio vectors' blocks are not valid LZ4, so every other forbidden form fails a vector only by its error: a truncating division never raises the ratio error on either, and a float32 ratio, of float32 operands or rounded once, never on reject_ratio_float32_rounds. Only a test that asserts the ratio error catches them (WIRE-16)." }, "WIRE-15": { "section": "Decompression Bomb Detection", "binds": "sdk", "sdks": [ "cachekit-ts" ], "vectors": [ "wire-format.json:envelope_ratio_product_wraps_32_bits" ], "gap": "Running the vector on each 32-bit build, as consumers get it, is a property of the SDK's CI that no vector shows; cachekit-ts is the SDK that publishes a 32-bit (wasm32) build." }, "WIRE-16": { "section": "Reject vectors", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "vectors": [ "wire-format.json:reject_original_size_over_cap", "wire-format.json:reject_original_size_wraps_u32", "wire-format.json:reject_zero_length_compressed_data", "wire-format.json:reject_ratio_bomb", "wire-format.json:reject_decompressed_length_mismatch", "wire-format.json:reject_checksum_mismatch", "wire-format.json:reject_envelope_arity_5", "wire-format.json:reject_envelope_arity_3", "wire-format.json:reject_checksum_nine_elements", "wire-format.json:reject_checksum_seven_elements", "wire-format.json:reject_legacy_element_above_255", "wire-format.json:reject_envelope_slots_overclaim", "wire-format.json:reject_original_size_sign_bit", "wire-format.json:reject_ratio_float32_rounds" ], "gap": "Which path an SDK's test drives each vector through, and what it asserts, is SDK test code no vector can check; sdk-feature-matrix.md's \"Wire format (ByteStorage)\" row and its footnote 16 record it per SDK." }, "WIRE-17": { "section": "Reject vectors", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "vectors": [ "wire-format.json:reject_original_size_over_cap", "wire-format.json:reject_ratio_bomb" ], "tests": [ "tools/test_wire_format_reference.py:_check_allocation_probe" ], "gap": "Whether an SDK's test bounds what the read allocates is SDK test code no vector can check." }, "WIRE-18": { "section": "Reject vectors", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "tests": [ "tools/test_wire_format_reference.py:_check_allocation_probe" ], "gap": "The probe is SDK test code no vector can check; the reference tool's probe and its positive control measure the reference reader only." }, "WIRE-19": { "section": "Reject vectors", "binds": "sdk", "sdks": [ "cachekit-py", "cachekit-ts" ], "tests": [ "tools/test_wire_format_reference.py:check_reader_rejects" ], "gap": "No vector covers step 1, which needs an envelope over 512 MiB, or step 3, which cannot fire once step 1 has passed. The reference tool reaches step 1 by lowering its cap; for cachekit-py and cachekit-ts, whose step 1 runs in cachekit-core behind compile-time caps, only an envelope over 512 MiB reaches it." }, "WIRE-20": { "section": "Datetime Extension Format", "binds": "sdk", "gap": "The vectors pin the three canonical maps, with a whole-second UTC datetime, so a reviver that drops a non-UTC offset or sub-second digits passes; whether a JavaScript Date's millisecond precision meets the rule is not specified. A map's shape beyond them (an extra key, a marker other than true) is not specified either, so nothing pins it. cachekit-ts and cachekit-rs do not revive them in auto mode (sdk-feature-matrix.md).", "vectors": [ "wire-format.json:temporal_sentinel_datetime", "wire-format.json:temporal_sentinel_date", "wire-format.json:temporal_sentinel_time" ] }, "WIRE-21": { "section": "SDK Storage Containers (auto mode)", "binds": "sdk", "vectors": [ "python-frame.json:ck_frame_fed_to_interop_reader", "python-frame.json:bare_envelope_fed_to_frame_reader", "python-frame.json:plain_msgpack_fed_to_frame_reader" ], "gap": "cachekit-py's refusal of a bare envelope and of plain MessagePack is pinned, and so is a plain-MessagePack reader's refusal of a CK frame, by its trailing bytes. No vector names cachekit-ts's envelope reader as the one refusing a CK frame or plain MessagePack, though the vectors carry both, and none can tell cachekit-ts's uncompressed container from cachekit-rs's: both are plain MessagePack. cachekit-rs decodes bare_envelope_fed_to_frame_reader, a WIRE-21 violation (sdk-feature-matrix.md note 15)." }, "WIRE-22": { "section": "Python: CK v3 frame", "binds": "sdk", "sdks": [ "cachekit-py" ], "vectors": [ "python-frame.json:truncated_frame", "python-frame.json:unsupported_frame_version", "python-frame.json:header_overrun", "python-frame.json:truncated_frame_one_short", "python-frame.json:unsupported_frame_version_2", "python-frame.json:unsupported_frame_version_4", "python-frame.json:header_overrun_by_one" ], "tests": [ "tools/python-frame-reference.py:verify" ], "gap": "unsupported_frame_version_2 and unsupported_frame_version_4 fail a reader that rejects only versions above or below 3, since their header and payload are intact. The other boundary vectors do not isolate their checks: a reader that accepts a 6-byte frame rejects truncated_frame_one_short at the header-length check, and one that slices an overrunning header to the bytes present gets header_overrun_by_one's real header and an empty payload, which its envelope decode rejects. Only a test that asserts the frame parser's own error, the check rejected_by names, catches either." }, "WIRE-23": { "section": "Python: CK v3 frame", "binds": "sdk", "sdks": [ "cachekit-py" ], "vectors": [ "python-frame.json:default_saas_write_msgpack_bytestorage", "python-frame.json:default_saas_write_msgpack_bytestorage_bin", "python-frame.json:forged_plaintext_encrypted_false", "python-frame.json:forged_plaintext_orjson" ], "gap": "The forged frames claim plaintext two ways, an explicit \"encrypted\": false and another serializer's plaintext write, so a reader that trusts either claim returns the value; forged_plaintext_orjson also trips a default-configured reader's serializer-name check first, so on its own it does not show the downgrade check. No vector alters an m value the AAD binds (format, compressed, original_type) on a real ciphertext, which authentication then rejects." }, "WIRE-24": { "section": "Python: CK v3 frame", "binds": "sdk", "sdks": [ "cachekit-py" ], "vectors": [ "python-frame.json:default_saas_write_msgpack_bytestorage", "python-frame.json:default_saas_write_msgpack_bytestorage_bin", "python-frame.json:forged_plaintext_encrypted_false", "python-frame.json:forged_plaintext_orjson", "python-frame.json:ciphertext_key_not_in_keyring", "python-frame.json:ciphertext_other_tenant" ], "gap": "ciphertext_key_not_in_keyring fails every reader that does not return plaintext, so it pins the keyring path's outcome, not a check a wrong reader skips. ciphertext_other_tenant fails closed for a reader that resolves its own tenant (encrypted_reader's tenant_source); one that takes the tenant from the frame header decrypts it. Whether a single-tenant reader may take the header's tenant is open: spec/encryption.md's Decryption Flow takes tenant_id as an input without saying where it comes from." } }, "retired": {} } } }