feat(credentials): record, show, and explain saved token expiry

Merged
WA
3d ago
+983-7

Summary

  • When a GitHub or GitLab saved token is verified, Sim now records its expiry. The value goes on the existing credential.access_token_expires_at column. An expiry that has already passed contradicts the vendor accepting the token, so it is dropped. An imminent one is kept.
    • GitLab sends expires_at from /personal_access_tokens/self, and the token stops working at midnight UTC on that date (GitLab docs).
    • GitHub sends the GitHub-Authentication-Token-Expiration header (changelog). GitHub does not document the header's value format. The reader accepts the two forms seen in the API responses that GitHub's own octokit.rb records: a numeric offset (2023-04-19 16:34:22 -0700) and a UTC marker (2022-09-20 23:45:13 UTC). It leaves any other form unread. In that case the token is still accepted, just with no expiry recorded.
  • Reconnecting replaces the stored expiry. A replacement token with no expiry clears it.
  • The workspace and organization credential presentations report an optional expiresAt.
  • The integrations list shows "expires in N days" or "expired". The credential detail page shows the expiry date and time in UTC.
  • When a provider sends a 401 for a saved token whose recorded expiry has passed, the run says the token expired on <date> UTC and asks for a new one. The provider's status and body are kept, and the refusal stays attributed to the provider in logs. A token is never refused on the recorded expiry alone.
  • The GitLab personal-token path reads expires_at with the same strict parser.
  • Not included:
    • Slack token rotation. Slack refresh tokens are single-use and the old one is revoked after an unquantified grace period (Slack docs). Rotation needs a new provider id with durable, compare-and-swap rotating-chain storage, plus a live proof. The design is written up separately. The existing xoxe. refusal stays.
    • Atlassian expiry. API tokens get a 1–365 day expiry when they are created, but Atlassian documents no way for a token to read its own expiry (Atlassian docs).
    • Infisical expiry. The TTL is returned only by the login that issues the token, and a pasted token has no documented self-lookup (Infisical token auth).
    • Expiry notifications. No credential notification system exists yet.

Deploy notes

  • No schema change or migration.
  • During the rolling deploy, an old worker that handles a reconnect of a GitHub/GitLab saved token updates only the secret. If a new worker had already recorded an expiry on that row, the old token's expiry stays until the next reconnect on new code, and the list and run error may show that stale date. Hitting this needs a create and a reconnect of the same credential inside the rollout window. Nothing is refused because of it.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Other: ___________

Testing

  • Unit tests (written red first) cover:
    • the GitHub and GitLab expiry parsers, including imminent expiries, both GitHub header forms, and other zone names being left unread
    • the expiry caption
    • the expired-token explanation in executeTool, for external requests and internal operations
  • A real-Postgres integration test, saved-token-expiry.integration.ts, covers create, list, reconnect replace/clear, org-owned credentials, and the expired-token message.
  • Revert proofs turned the attribution and org-presentation tests red.
  • What to review closely: the compatibility story. No schema change, no change to stored secret shapes, and the contract field is optional. Credentials saved before this have NULL expiry and behave exactly as before.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)