Repository navigation
feat(low-code): rotate RateLimitedMultipleTokenAuthenticator across credentials that log in themselves - #1269
Draft
Daryna Ishchenko (darynaishchenko) wants to merge 8 commits into
Draft
Daryna Ishchenko (darynaishchenko) wants to merge 8 commits into
Daryna Ishchenko (darynaishchenko) wants to merge 8 commits into
Conversation
…redentials that log in themselves Add `credentials: CredentialSet` as an alternative to static `tokens`. Each item of the set gets its own authenticator (a SessionTokenAuthenticator, with the item visible as `config['credential']`), and quota state is kept per credential key, so a refreshed token keeps its credential's counters. Requests are signed with the credential's current header value and responses are attributed back through the header values it issued. Nothing is fetched or validated at construction: an unusable credential list is a config error on first use, so SelectiveAuthenticator branches that are not selected keep building. Identical definitions share one instance through the existing factory cache, keyed on credential keys and item hashes only. Static `tokens` behave and key exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
👋 Greetings, Airbyte Team Member!Here are some helpful tips and reminders for your convenience. 💡 Show Tips and TricksTesting This CDK VersionYou can test this version of the CDK using the following: # Run the CLI from this branch:
uvx 'git+https://github.com/airbytehq/airbyte-python-cdk.git@daryna/rate-limited-credential-set#egg=airbyte-python-cdk[dev]' --help
# Update a connector to use the CDK from this branch ref:
cd airbyte-integrations/connectors/source-example
poe use-cdk-branch daryna/rate-limited-credential-setPR Slash CommandsAirbyte Maintainers can execute the following slash commands on your PR:
|
… cache keys exact - Read the credentials' request header on first use, so an API-key header session no longer logs in while the authenticator is built. - Key the shared instance by everything except `$parameters` and the propagated `name`; a login that interpolates `parameters` is keyed with them, so streams never share an authenticator built for another definition. - Reject at build time: no `expiration_duration`, an API key outside a header, or `header`/`auth_method`/`token_delimiter` set with `credentials`. - Name the credential key in login and quota-status errors. - Docs: literal `items` arrays are not interpolated; the key appears in error messages, not logs. - 23 more tests, 9 of which fail on the previous head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dinary $parameters - Build the instance cache key from the fields the models declare, so the extras `$parameters` propagation copies onto the login requester (`primary_key`, `data_field`, ...) no longer give each stream its own authenticator. A body field named `parameters` now stays in the key. - Evaluate credential keys as strings, so ids like "1_000" and "1000" stay distinct. - Name the credential in every login failure: an API-key header login rejected during header resolution, and a login response with no token. - Tests: stream `$parameters` with `primary_key`/`data_field` in the factory and end-to-end sharing tests; the `parameters`-reference test now fails without the guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…edential cache key - A custom component inside the credential login is keyed with everything it receives (declared fields, extras, `$parameters`), so two logins that differ only in its settings no longer share one instance. Such a login gets an instance per stream, which the schema description now says. - Add a hash of the config to the credentials key fragment, so a login that reads `config['api_url']` is not shared across configs. - Remove the `name`-dropping branch: no declared model under the login has a `name` field, so it never fired. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fer missing config paths - Stream `$parameters` no longer fill fields under `CredentialSet.authenticator` (`STREAM_INDEPENDENT_FIELDS` in the transformer). A stream's `url` filled the login's empty `url`, so each stream got its own instance and quota counters although the login request was the same. The parameters are still passed down as `$parameters`, and parameters declared inside the login still fill its fields; propagation elsewhere is unchanged. - Warn once when the same credentials end up in more than one instance. - An `items` or `key` path whose parent is missing from the config (`config['credentials']['accounts']` without `credentials`) is reported at first use instead of raising `UndefinedError` while streams are built. - Tests for guards that survived mutation: a static-token quota-status failure is raised unchanged, literal `items` differing only in secrets don't share, an empty header is rejected, and failed logins keep their failure type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ic-stream propagation - Record the `$parameters` a component inside `CredentialSet.authenticator` declares itself under `$own_parameters`, so the second propagation of a resolved dynamic stream does not mistake the outer parameters the first pass merged in for its own and fill the login with them. Dynamic and static streams now share one instance, and the login no longer sends a stream's `request_parameters`. - Share the split-warning registry with substream factories, identify credentials by their keys, items, config and quota pools only (so a split by the quota-status request is reported), and skip unusable credential sets such as an unselected `SelectiveAuthenticator` branch. - Name the `items` / `key` expression and the Jinja error in the internal message when it cannot be evaluated. - Document that `CredentialSet`'s own `$parameters` do not fill the login either. - Add transformer-level tests for the boundary (lists, untyped objects, the cycle exclusion, siblings, a second pass). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`RateLimitedMultipleTokenAuthenticator` kept two code paths, one for static `tokens` and one for `credentials`. Each static token is now a credential keyed by the token itself, signing with a fixed header value, so signing, quota-status requests and response attribution share one path. The manifest contract, the factory and the cache key are unchanged. - State stays keyed by the token string, and `_tokens` keeps the configured order and duplicates. - A static token's quota-status failure is still re-raised unchanged, so a token is never named in an error. - Static header values are registered up front, so a response to a request another component signed with the same token is still attributed. - With `auth_method: ""`, the quota-status request is now signed with the bare token instead of a value with a leading space, which `requests` rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 tasks
Airbyte Support Bot (airbyte-support-bot)
temporarily deployed
to
PyPi
November 6, 2026 11:50 — with
GitHub Actions
Inactive
Airbyte Support Bot (airbyte-support-bot)
temporarily deployed
to
DockerHub
November 6, 2026 11:50 — with
GitHub Actions
Inactive
Anatolii Yatsuk (tolik0)
approved these changes
Nov 9, 2026
Airbyte Support Bot (airbyte-support-bot)
temporarily deployed
to
PyPi
November 9, 2026 13:38 — with
GitHub Actions
Inactive
Airbyte Support Bot (airbyte-support-bot)
temporarily deployed
to
DockerHub
November 9, 2026 13:38 — with
GitHub Actions
Inactive
This branch was previously deployed
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.
Lets
RateLimitedMultipleTokenAuthenticatorrotate between credentials that log in and refresh their own tokens, not only between static tokens, with the same quota tracking, rotation and sharing across streams that static tokens get today.Intent
Some APIs give each credential its own rate-limit budget, but the token used on the wire is short-lived: a login exchanges a long-lived secret for a session token that expires and has to be fetched again. Connectors for those APIs want what
RateLimitedMultipleTokenAuthenticatoralready offers static tokens:It can't do that today, for two reasons:
tokensis interpolated once at build time, so there is nowhere to plug in a login, and a token that expires mid-sync can't be refreshed.So a connector in this position has to write a custom authenticator. A custom authenticator doesn't get the factory's instance cache, so each stream ends up with its own copy and its own counters. It also has to re-implement the quota logic.
This PR adds that capability to the existing component, so such connectors can stay declarative.
Changes
credentials: CredentialSetonRateLimitedMultipleTokenAuthenticator, as an alternative totokens. Exactly one of the two must be set.itemsevaluates to a list of objects, one per credential, usually an interpolated config field.authenticator(aSessionTokenAuthenticator) is built once per item, with that item visible in interpolation asconfig['credential']. Existing components interpolate it without any new Jinja variable.keyis a stable, non-secret identifier per credential, evaluated as a string, so ids such as1_000and1000stay distinct. It defaults to the item's position.config_erroron first use.SelectiveAuthenticatorbuilds every branch up front, so a config that selects another branch is unaffected.credentialsis rejected together withheader,auth_methodortoken_delimiter, without anexpiration_durationon its authenticator, or with an API key injected anywhere but a header. Withoutexpiration_durationthe session would be fetched for every request, and responses could no longer be traced back to their credential.$parameters. Stream parameters reach the login only as its$parameters(see below), and the key leaves those out, so ordinary streams share one instance. Every declared field stays in the key, even one whose value happens to equal a parameter.$parameters. Logins that differ only in its settings therefore never share. Such a login gets one instance per stream, because the component receives each stream's parameters.config['api_url']isn't shared across configs. All streams of a source get the same config.parametersis keyed with them, so streams with different parameters get their own instance.tokensdefinition keys exactly as before.$parametersdon't fill the login's fields. Propagation normally fills any field a nested component leaves empty, so a stream'surlwould land in the login's emptyurland give each stream its own instance and quota counters. UnderCredentialSet.authenticatorthe stream parameters are still passed down as$parameters, so the login can read them, but they fill no fields; parameters declared inside the login still do. This is one new entry,STREAM_INDEPENDENT_FIELDS, inmanifest_component_transformer.py; propagation everywhere else is unchanged.$own_parameters. Dynamic streams are propagated a second time once resolved, and without that record the stream parameters merged in by the first pass would look like the login's own and fill it after all.$parametersset on theCredentialSetitself don't fill the login either; declare them on the authenticator.parametersor contains a custom component, or a quota-status request filled from a stream's$parameters. Substream factories share the registry. A credential list that can't be used, such as an unselectedSelectiveAuthenticatorbranch, isn't reported.itemsorkeypath whose parent is missing from the config, such asconfig['credentials']['accounts']with nocredentialsobject, is reported on first use too, instead of failing while streams are built. The internal message names the expression and the Jinja error, since it may be a manifest mistake rather than a config one.auth_method: "", the quota-status request was signed" <token>", whichrequestsrejects as an invalid header. It is now the bare token.tokensbehave exactly as before apart from that fix. All existing tests are untouched and pass, and so dosource-github's 22 authenticator unit tests run against this branch.Example
Files
auth/rate_limited_multiple_token.py:RotatingCredential, and a private_StaticTokenAuthenticatorthat runs a static token as a credentialcredentials/credentials_error/credentials_error_detailconstructor argumentsparsers/model_to_component_factory.py:_resolve_credential_set(evaluates items and keys without building anything), per-credential construction after a cache miss, and the cache-key extension.declarative_component_schema.yamlandmodels/declarative_component_schema.py: theCredentialSetdefinition,tokensmade optional, and acredentialsfield. The model was edited by hand. Checked againstdatamodel-codegen==0.26.3with the flags inbin/generate_component_manifest_files.py: the two changed models have identical pydantic schemas, and only class order and forward-ref style differ. "Exactly one oftokens/credentials" is enforced in the factory rather than as a schemaoneOf, because the generator turns aoneOfinto two model classes.parsers/manifest_component_transformer.py:STREAM_INDEPENDENT_FIELDS, and aparent_field_parametersargument that tracks which parameters may fill fields. It defaults toNone, which means all of them, so other callers are unaffected. Inside the boundary,$own_parametersrecords each component's own parameters for a second propagation.unit_tests/.../auth/test_rate_limited_multiple_token_credentials.py: 80 tests.unit_tests/.../parsers/test_manifest_component_transformer.py: 6 new tests for theSTREAM_INDEPENDENT_FIELDSboundary.Testing
has_alternative_tokenreturning Truetokenstogether withcredentialsis rejected, as are mixed injection headers$parameters; distinct instances for different credentialstokenscache key has exactly the same fields as beforetokensandcredentialsare rejectedf5c2c7a5:X-Api-Keyon first useparametersgets one instance per parameter set, each signed with its own sessionexpiration_duration, with an API key outside a header, or withheader/auth_method/token_delimiterare rejected__call__,tokenandget_auth_header()expiration_duration(freezegun) keeps the credential's counterstokenscache key equals the base commit's, value for value40f9acf7:$parameterscarryprimary_keyanddata_fieldshare one instance, for Bearer and API-key sessions, at factory level and end to endparametersdon't shareparameters-reference test now fails when the guard is removeda920c8c2:ab695b1e:$parameterscarryurlanduse_cacheshare one instance, and the login's fields stay unsetitems(factory level, and end to end behindSelectiveAuthenticator), and a missing level inkey, fail on first use as config errorsitemsdiffering only in secrets don't share56d0c996:$parameterscarryurlandrequest_parametersshare one instance, the login sends no stream request params, and no warning is logged$parameters, after a second propagation like a dynamic stream'sitemsorkeyexpression that can't be evaluated is named in the internal message, with the user message unchanged$own_parametersrecord, now fails a test.3d927ce8; the other 2 pin behaviour that already held:auth_method: ""signs both the data and the quota-status requests with the bare token (fails on3d927ce8withInvalidHeader)headerheadernever makes a lookup error name the tokenSelectiveAuthenticator:$parametersincludeprimary_keyanddata_field, share one authenticator instance, and each credential logs in exactly once for the whole readtest_rate_limited_multiple_token.py(65 tests) andtest_selective_authenticator.pypass unchanged.unit_tests/sources/declarative,unit_tests/sources/streams/httpandunit_tests/legacy: 3366 passed, 4 skipped. The 2 local failures (test_read_concurrent_declarative_sourcewith a partition router,test_per_partition_cursor_within_limit) fail the same way on56d0c996, which passed CI, and the failing case varies between runs.ruffandmypyare clean on the changed files.Open questions for review
credentials/CredentialSet/config['credential']. The config overlay avoids touching every component's interpolation context; a dedicated Jinja variable would read more clearly but has a larger blast radius.authenticator: onlySessionTokenAuthenticatorin this PR. OAuth (one refresh token per credential) could follow if there's demand.🤖 Generated with Claude Code