feat(keys): opt-in rotation overlap window and previousKeyExpiresAt parity (0.3.0) - #3
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the Python SDK level with the rotation-overlap contract that shipped in prod and in
@dodomain/node0.4.0 on 2026-08-20. MirrorszRotateAppSecretKeyInput/zRotateAppSecretKeyResponseinpackages/core/src/schemas.tsand the/api/v1/keys/rotateoperation inapps/docs/public/openapi.json— no shapes invented here.The contract change
POST /api/v1/keys/rotatenow accepts an optional JSON body{"overlapHours": 0 | 1 | 24}, and the response gainedpreviousKeyExpiresAt(ISO datetime ornull).{"overlapHours": 1 | 24}appId,publicKey,secretKey,rotatedAtpreviousKeyExpiresAt: string | nullWire behavior for the default is unchanged, deliberately.
keys.rotate()andkeys.rotate(overlap_hours=0)both send no body at all — byte for byte the request every server version has always accepted — matching node 0.4.0. Only a requested window puts a body on the wire, so the common call cannot be broken by a server that has not shipped the field. Two tests pin this; a mutant that always sent a body failed both.Semantics documented (README, docstrings, CHANGELOG)
0is an immediate cutover and remains the kill switch. A zero-overlap rotation also terminates a window still running from an earlier rotation — that is how you revoke a previous key early.previous_key_expires_at. That is the zero-downtime deploy path: rotate, ship the new key, let the old one lapse.What changed
keys.rotate(*, overlap_hours: RotationOverlapHours = 0), sync and async.RotatedSecretKey.previous_key_expires_at: datetime | None, parsed with the repo's tolerant_opt_datetime— a response recorded before the field existed still reads as a zero-overlap rotation.RotationOverlapHours = Literal[0, 1, 24], exported, so callers can type a configured window instead of passing a bareint.validate_overlap_hoursrefuses a value the API does not offer locally, withstatus_code == 0, before the call that would mint a credential.Trueis refused too:True == 1would otherwise buy a one-hour window nobody asked for.__init__.py+ the test that pins it), CHANGELOG entry. Purely additive — upgrading is a drop-in.Tests
14 new tests. No body on the default and on an explicit
0;{"overlapHours": N}for 1 and 24;previous_key_expires_atparsed bothnulland set; the pre-overlap legacy payload still parses; bad windows refused with zero requests sent; async twin sends the same window and reads the same expiry; a newkeys.rotate(overlap_hours=24)case in the sync/async parity matrix.Both halves were proven red before green: forcing a body onto the default failed 2 tests, and dropping the
previousKeyExpiresAtparse failed 3.Gate results (local, CI-equivalent)
ruff check .ruff format --check .ty checkpytest tests --ignore=tests/e2e --cov=dodomain --cov-fail-under=90resources/keys.pyand_validation.pyat 100%)tests/e2eis unchanged and not run here — it never exercises a live rotation (it only asserts a bogus key is refused with a 401), so nothing in this PR touches it.🤖 Generated with Claude Code