Skip to content

Client: accept server epoch-second expires_at, normalize to ISO#227

Merged
platypii merged 1 commit into
masterfrom
oidc-expires-at-epoch-seconds
Jul 1, 2026
Merged

Client: accept server epoch-second expires_at, normalize to ISO#227
platypii merged 1 commit into
masterfrom
oidc-expires-at-epoch-seconds

Conversation

@platypii

@platypii platypii commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Align the OIDC client with the OAuth wire form the server actually sends. The server emits expires_at as a Unix epoch-second (the JWT exp), matching its bootstrap/refresh identity endpoints. The client now normalizes that to an ISO string on the way in, so session storage and the refresh-freshness check (isFresh) stay ISO-based.

  • expiryTimestamp accepts a finite positive JSON number (epoch-second) and converts to ISO; still accepts an ISO string for older mocks/servers.
  • An epoch-as-a-string stays rejected, so an unparseable expiry can't make every forwarded message refresh forever.
  • Smoke stub and LLP 0059 updated to reflect the epoch-second wire form.

Testing

  • npm test (1775 passing)

🤖 Generated with Claude Code

The server sends the OIDC `expires_at` as a Unix epoch-second (the JWT
`exp`), matching its bootstrap/refresh identity endpoints. The client now
normalizes that wire form to an ISO string on the way in, keeping storage
and the refresh-freshness check ISO-based. An ISO string is still accepted
for older mocks/servers, and an epoch-as-a-string stays rejected so an
unparseable expiry can't loop the refresh forever.
if (!Number.isFinite(v) || v <= 0) {
throw new Error(`identity response field '${field}' is not a valid timestamp`)
}
return new Date(v * 1000).toISOString()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low severity, non-blocking. The number branch guards Number.isFinite(v) && v > 0 but not the magnitude, so a large-but-finite positive value (a server mistakenly sending exp in ms or µs) slips through. Two out-of-contract outcomes:

  • v > ~8.64e12 (e.g. microsecond exp): new Date(v * 1000).toISOString() throws an uncontrolled RangeError: Invalid time value, defeating this function's stated purpose of failing loudly with a descriptive error — it escapes as an unclassified error on the refresh hot path.
  • v ≈ 1.7e12 (millisecond exp): produces a valid year-56461 date, so isFresh treats the token as fresh forever and it never refreshes — silently.

Both require an out-of-spec server, so this is not blocking. If you want to harden it, validate the constructed date and/or bound the epoch to a sane range, e.g.:

if (typeof v === 'number') {
  const ms = v * 1000
  const d = new Date(ms)
  if (!Number.isFinite(v) || v <= 0 || Number.isNaN(d.getTime())) {
    throw new Error(`identity response field '${field}' is not a valid timestamp`)
  }
  return d.toISOString()
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meh

@platypii platypii merged commit bf46d7e into master Jul 1, 2026
6 checks passed
@platypii platypii deleted the oidc-expires-at-epoch-seconds branch July 1, 2026 21:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant