Skip to content

feat(node): add auto-refresh - #12

Merged
edlng merged 6 commits into
release-2from
node/auto-refresh
Sep 21, 2026
Merged

edlng merged 6 commits into
release-2from
node/auto-refresh

Conversation

@edlng

@edlng edlng commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds ElastiCacheIAMAuthTokenManager for applications that need to reuse an IAM auth token across connections and refresh it before expiry. The existing ElastiCacheIAMAuthTokenProvider remains stateless and continues to mint a new token for every getToken() call.

Before After
Callers minted a token for each connection. Callers can reuse a cached token and refresh it in the background.
Refresh failures surfaced on the next token request. A valid cached token continues to be served while refresh retries run.

What's in it

Area Change
node/src/token-manager.ts Adds ElastiCacheIAMAuthTokenManager, configurable refresh timing, single-flight initial mints, background refresh, retry backoff, token-change callbacks, and explicit cleanup through close().
node/src/errors.ts, node/src/index.ts Exports TokenRefreshError and the token-manager API/types.
node/src/token-generator.ts Exposes the token TTL internally and factors region validation into ensureRegionResolved() so manager creation can fail early when region resolution fails.
node/test/ Covers caching, refresh timing, rotated credentials, retries, expiry, shutdown, callback failures, public exports, and normalized mixed-case user IDs from getCredentials().
node/README.md, node/examples/connect-iovalkey.mjs, node/CHANGELOG.md Documents manager behavior and updates the iovalkey example to obtain credentials through the manager.

Design decisions

The manager mints lazily through either getToken() or getCredentials(). Concurrent callers share one initial mint.

Refresh starts at refreshAfterSeconds and makes up to eight attempts with jittered exponential backoff. If the cached token remains valid, callers keep receiving it while retries continue. Once it expires, failed refreshes throw TokenRefreshError, which intentionally extends Error rather than ToolkitInputError.

getCredentials() returns the normalized user ID with the cached token for clients that accept a no-argument credential-provider hook.

Tests

  • node --test dist-test/test/token-manager.test.js — passed, 17/17 tests.
  • npm test — build completed; 80/81 tests passed. The packed-package installation test was killed with SIGTERM while npm install attempted to access the npm registry.

Not in this PR

The manager refreshes credentials for newly created clients; it does not re-authenticate an existing iovalkey connection. Applications still need to create a replacement client after a connection is lost.

Signed-off-by: Edward Liang <liawedwa@amazon.com>
Signed-off-by: Edward Liang <liawedwa@amazon.com>
Signed-off-by: Edward Liang <liawedwa@amazon.com>
Comment thread node/src/token-manager.ts
Signed-off-by: Edward Liang <liawedwa@amazon.com>
@edlng
edlng marked this pull request as ready for review September 16, 2026 21:34
@edlng
edlng requested a review from mwaisaida-13 September 17, 2026 15:39
* the background, so every new client is created with a valid token without
* signing a new one per connection. Automatic reconnects are disabled because
* they would reuse the token the connection was opened with; long-running
* applications should create a replacement client, calling getCredentials()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: since iovalkey only takes a static password, this example still has to disable reconnects, so the manager's main benefit isn't visible. Could we add a second example with node-redis v5, whose credentialsProvider: { type: "async-credentials-provider", credentials: () => auth.getCredentials().then(([username, password]) => ({ username, password })) } is called on every connect and reconnect? Keep this one as the static-password variant.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

added

Comment thread node/src/token-manager.ts
}

const cached = this.cached;
if (cached && this.now() < cached.expiresAt) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A cached token is served right up to expiresAt, so a caller can receive one with a few hundred ms left and fail AUTH once we add network latency into account. how about we add a serve margin, e.g. treat the token as expired ~20 s early (expiresAt = issuedAt + (TTL − margin) * 1000) so callers block on the refresh instead?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

added

Comment thread node/src/token-manager.ts
// but it has expired, throw a TokenRefreshError with the last error as its
// cause.
if (cached && this.now() < cached.expiresAt) {
this.scheduleRefresh(MAX_DELAY_MS);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

non-blocking: the 5 sec pause after a failed cycle only holds while nothing calls getToken(), this line starts a new cycle on any request past refreshAt, so under a reconnect storm we retry back-to-back against a credential source that's already struggling. Callers are unaffected (they keep getting the cached token)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

added

Comment thread node/src/token-manager.ts
cancelSleep?.();
}

private refresh(): Promise<string> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could this be public (with the cache invalidated first)? After a WRONGPASS the client currently has no way to drop the cached token, so the manager adds up to 300 sec of serving a rejected token on top of whatever the credential chain does. It won't fix every auth failure, but it stops the cache from making recovery slower than the stateless provider. GLIDE exposes refresh_token() for this.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

added refreshToken()

Signed-off-by: Edward Liang <liawedwa@amazon.com>
@edlng
edlng merged commit c5462a6 into release-2 Sep 21, 2026
11 checks passed
@edlng
edlng deleted the node/auto-refresh branch September 21, 2026 16:26
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.

3 participants