Repository navigation
Check Transact Enterprise entitlements before loading an entitled feature - #664
Merged
Merged
Conversation
…ture Before DBOS loads a Transact Enterprise feature, it now asks DBOS Conductor whether the organization is entitled to it, as Python (dbos/_enterprise.py), TypeScript (src/enterprise.ts) and Go (dbos/entitlements.go) do. No Java feature needs an entitlement yet; suspend mode, auto-upgrade and Kafka consumers will each add their config switch to Entitlements.enabledFeatures as they land. - DBOSConfig gains transactEnterpriseKey, with withTransactEnterpriseKey, masked in toString. The Conductor key stands in for it when unset. The Spring Boot starter maps dbos.transact-enterprise-key. - Entitlements.require sends the key as a bearer token to GET <conductor>/v2/entitlements, with a 10 s timeout and no redirects, and refuses launch with an IllegalStateException citing the DBOS Conductor Software License when: - an entitlement is missing or not true; - the key is rejected (401); - the org is past its plan limit (403 with X-DBOS-Error: past_limit); - the Conductor has no entitlements route (a bare 404); - any other 4xx comes back. - Only an explicitly configured Conductor is asked without a key. An unreachable or malformed answer fails closed, except from DBOS's hosted Conductor, which warns and starts anyway (as do its 403, 407, 408, 425 and 429 without the past_limit code). DBOS Cloud skips the check. Credentials in the Conductor URL never reach the request or the errors. - DBOSExecutor runs the check at construction, before the Conductor provider is loaded, so a refusal fails launch before anything starts. - The ServiceLoader lookup behind ConductorProviders moves into EnterpriseProviders.load(type, feature), so each entitled feature's provider interface gets the same class-loader handling and the same "requires dev.dbos:transact-enterprise" error.
… its errors
- The 10 s timeout now covers the body as well as the headers. Before,
HttpRequest.timeout ended once the headers arrived, so an answer that
stalled mid-body held launch forever, with neither the hosted Conductor's
warning nor any other Conductor's refusal. The request now goes through
sendAsync with a deadline on the whole exchange, as TypeScript's
AbortSignal.timeout does.
- The key is stripped, and a blank key counts as unset, so a key read from
a secret file that ends in a newline works (TypeScript's fetch strips
header values too). A key that still contains a control character is
refused without being quoted. Before, the HTTP client's
"invalid header value" error escaped with the whole key in its message.
- An unusable Conductor URL is refused naming the reason, never the URL,
and without the URISyntaxException as its cause. That exception's message
quotes the URL, including any user:password in it. Only ws, wss, http and
https are accepted as schemes. Any other exception the HTTP client throws
over the request counts as a failure to reach Conductor, without passing
its message on.
- DBOSConfig treats a blank transactEnterpriseKey as unset instead of
refusing it, so an empty placeholder such as
dbos.transact-enterprise-key=${KEY:} falls back to the Conductor key, as
in Python and TypeScript.
- DBOSConfig.conductorKey now treats a blank value as unset, meaning no
Conductor connection, instead of refusing an empty one. That matches
transactEnterpriseKey, and Python and TypeScript, where an empty
conductor_key / conductorKey is unset. A placeholder such as
dbos.conductor.key=${CONDUCTOR_KEY:} with the variable unset no longer
fails startup. Before, " " passed the empty check and was used as a key.
- toString prints conductorKey as *** (or null), as it already did for
dbPassword and transactEnterpriseKey. It printed the key in clear, and
the Conductor key also serves as the enterprise key when none is set.
DBOSConfig.conductorDomain now treats a blank value as unset (DBOS's hosted Conductor), like conductorKey and transactEnterpriseKey. Before, "" was refused while " " was accepted, and DBOSExecutor quietly treated " " as unset in two places of its own. Those two checks are gone; the executor now passes the configured domain unchanged to the entitlement check and to the Conductor client, so the two always use the same URL.
…check and the client one key - A key that is not printable ASCII is refused before any request, without being quoted. Before, the HTTP client refused a key with a character above U+00FF while building the header. The check counted that as an outage, so against DBOS's hosted Conductor it warned and started anyway, with no verified entitlement. - Any request the HTTP client refuses to send now fails closed, even against the hosted Conductor. The client's message is still never passed on. - DBOSConfig strips surrounding whitespace from conductorKey, conductorDomain and transactEnterpriseKey, not only blank values. Before, the check stripped "key\n" but the Conductor client got it as configured, so the check passed and the client then failed to connect. - The answer is read up to 1 MiB again, through a bounded body subscriber under the same deadline. An oversized answer counts as an outage instead of filling the heap. - The HTTP request has no timeout of its own. The one deadline covers the whole exchange, so the two can no longer race to report a timeout. - DBOSConfig.toString masks any user:password@ in conductorDomain. - Entitlements.require treats a blank Conductor URL as unset, as DBOSConfig does, instead of refusing it as invalid.
…hout a parseable host - An exchange that fails in flight is an outage, whatever exception the JDK raises. A non-numeric or overflowing Content-Length fails it with a NumberFormatException, an IllegalArgumentException. That was reported as "the request could not be sent", which failed closed even against the hosted Conductor. Only an IllegalArgumentException thrown while building or sending the request is a refusal now. - A Conductor URL whose host java.net.URI cannot identify is refused as invalid. With a raw '/' or '@' in the password, the parser left the credentials inside the authority, and the refusal message quoted them. The same goes for a host with an underscore. The Conductor client cannot connect to any of these either: the JDK WebSocket client refuses a URI without a host. The entitlement route is built from the parsed host and port. - DBOSConfig.toString masks everything up to the last '@' in the Conductor domain, so a raw '/' or '@' in the password no longer leaves part of it showing. A path containing '@' is masked too, which is harmless when printing. - Tests: - an answer of exactly 1 MiB is read; - keys with a tab, DEL and characters 0x80-0xFF are refused.
…ipt do Drops the custom bounded body subscriber. Conductor's /v2/entitlements answers with six booleans, or a short problem detail, so the answer is a few hundred bytes. Only a non-Conductor endpoint could send a large body. The JDK has no capped body handler before release 23 (this project builds for 17), and Python and TypeScript read the whole body too. The answer is now read with BodyHandlers.ofString(), still under the one deadline that bounds the whole exchange.
… Error as an outage
- A raw '/' in a password can parse as a port and a path: in
"wss://user:2024/pass@host/...", "user" becomes the host, 2024 the port,
and the rest of the password the path. The check then quoted the password
and would have sent the key to the wrong host. A Conductor URL with '@' in
its path, query or fragment is now refused as invalid. The earlier host
check only caught the case where java.net.URI found no host.
- A port above 65535 and an IPv6 zone ID are refused up front. The HTTP
client fails both in flight, which read as an outage reported as "a
malformed answer".
- A JVM Error raised in flight, such as OutOfMemoryError, now propagates
instead of being wrapped as an outage, which would have failed open
against the hosted Conductor. Other in-flight failures are reported as
"the exchange failed".
- DBOSConfig.conductorDomain's javadoc said "custom hostname". It must be a
ws:// or wss:// URL, both for the check and for the Conductor client.
- Tests:
- a malformed answer from the hosted Conductor, through the real HTTP
client, starts with a warning;
- the numeric-port password cases;
- log assertions now match only their own test's warning, since tests
log to the same logger in parallel.
…nly ws and wss Conductor URLs
- The HTTP client reports an Error in its own threads, such as an
OutOfMemoryError in the selector manager, as the cause of an IOException
("selector manager closed"). The previous fix only caught an Error that
was the direct cause, so this one still read as an outage and failed open
against the hosted Conductor. The whole cause chain is checked now.
- A Conductor URL must be ws:// or wss://. The Conductor client is a
WebSocket client, which rejects http and https, so such a URL passed the
check and then could never connect.
- An IPv6 zone ID is accepted again. java.net.URI separates its credentials
correctly, and the client can resolve the scoped address, so refusing it
only refused a usable URL.
- The tests that read Entitlements' shared log take a JUnit resource lock
instead of filtering on test-specific text. The malformed-answer tests no
longer depend on what the JDK calls a non-numeric Content-Length.
kraftp
approved these changes
Nov 13, 2026
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical issues allow authoritative entitlement refusals to fail open and API keys to leak through diagnostics, while inconsistent endpoint classification can incorrectly block startup or bypass key requirements.
4 open issues
What changed in this PR
Adds Conductor entitlement-checking infrastructure before enterprise provider loading in the Java SDK. No current Java feature enables the check, so launch does not yet make entitlement requests.
Changes:
- Adds entitlement response handling, outage policies, URL validation, and a body-inclusive timeout.
- Introduces enterprise-key configuration and Spring Boot binding, normalizes blank settings, and masks credentials in configuration output.
- Centralizes enterprise provider loading and adds entitlement and configuration tests.
| File | Description |
|---|---|
transact/src/test/java/dev/dbos/transact/internal/EntitlementsTest.java |
Tests entitlement responses, failures, deadlines, and credential handling. |
transact/src/test/java/dev/dbos/transact/config/DBOSConfigKeysTest.java |
Tests key defaults, copying, normalization, and masking. |
transact/src/test/java/dev/dbos/transact/config/ConfigTest.java |
Updates expectations for empty Conductor settings. |
transact/src/main/java/dev/dbos/transact/internal/Entitlements.java |
Implements entitlement checks and response policies. |
transact/src/main/java/dev/dbos/transact/internal/EnterpriseProviders.java |
Centralizes enterprise provider discovery and selection. |
transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java |
Invokes entitlement checks before provider loading. |
transact/src/main/java/dev/dbos/transact/config/DBOSConfig.java |
Adds the enterprise key, setting normalization, and credential masking. |
transact/src/main/java/dev/dbos/transact/conductor/ConductorProviders.java |
Delegates to the shared provider loader. |
transact-spring-boot-starter/src/test/java/dev/dbos/transact/spring/DBOSPropertiesTest.java |
Tests enterprise-key property binding. |
transact-spring-boot-starter/src/test/java/dev/dbos/transact/spring/DBOSAutoConfigurationTest.java |
Tests enterprise-key mapping and empty settings. |
transact-spring-boot-starter/src/main/java/dev/dbos/transact/spring/DBOSProperties.java |
Exposes the enterprise-key property. |
transact-spring-boot-starter/src/main/java/dev/dbos/transact/spring/DBOSAutoConfiguration.java |
Transfers the enterprise-key property into SDK configuration. |
Review effort: Balanced
…sted Conductor by its route - A problem detail from Conductor, and a transport failure's message, are quoted in refusals and in the hosted warning. Any occurrence of the key in them is now masked, so an answer that echoes the key cannot put it in an exception or a log line. - The hosted Conductor is recognized by the entitlement route the check asks, compared case-insensitively, rather than by the exact configured string. So "wss://cloud.dbos.dev/conductor/v1alpha1/" (trailing slash) or an upper-case host is treated like the default: never asked without a key, and failing open on an outage. The URL is validated before the no-key check, so an invalid one is reported as invalid.
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.


Before DBOS loads a Transact Enterprise feature, it now asks DBOS Conductor whether the organization is entitled to it, as Python (
dbos/_enterprise.py), TypeScript (src/enterprise.ts) and Go (dbos/entitlements.go) do. No Java feature needs an entitlement yet. Suspend mode, auto-upgrade and Kafka consumers will each add their config switch toEntitlements.enabledFeaturesas they land, so in this PR the check never runs at launch.The check
Entitlements.requireruns whenDBOSExecutoris constructed, before any enterprise provider is loaded. It sendsGET <conductor>/v2/entitlementswith the key as a bearer token.truetrueX-DBOS-Error: past_limitOutages. DBOS's hosted Conductor fails open on an outage: it logs a warning and starts. Its 403, 407, 408, 425 and 429 count as outages too, unless the 403 carries
past_limit. Any other Conductor fails closed. DBOS Cloud skips the check.Keys. The enterprise key is used if set, otherwise the Conductor key. Only an explicitly configured Conductor is asked without a key, never the hosted one.
Every refusal is an
IllegalStateException. An entitlement refusal cites the DBOS Conductor Software License.Where Java goes beyond the other SDKs
HttpRequest.timeoutstops once headers arrive, so an answer that stalls mid-body would otherwise hang launch.Erroranywhere in a failure's cause chain propagates, so it can never be read as an outage.user:password@in the Conductor URL never reach a message, cause chain or log line. Redirects are never followed.ws://orwss://with a hostjava.net.URIcan parse, and must have no@after the host. Those are the URLs the WebSocket-based Conductor client can use. Anything else is refused as invalid, without quoting the URL.Configuration
DBOSConfig.transactEnterpriseKey, withwithTransactEnterpriseKey. The Spring Boot starter maps it asdbos.transact-enterprise-key.conductorKey,conductorDomainandtransactEnterpriseKeyare now handled alike:This way the check and the Conductor client always see the same values.
toStringmasks both keys, and any credentials inconductorDomain. Before,conductorKeywas printed in clear.Provider loading
The
ServiceLoaderlookup behindConductorProvidersmoved toEnterpriseProviders.load(type, feature), so each entitled feature's provider interface gets the same class-loader handling and the same "requires dev.dbos:transact-enterprise" error.ConductorProviders.load()keeps its signature.Behaviour changes for the release notes
conductorKeyorconductorDomainno longer throws. It means unset. For example,dbos.conductor.key=${KEY:}withKEYunset now starts without Conductor.conductorKeyused to connect to Conductor with that value as its key. It now means no Conductor.conductorKey,conductorDomainandtransactEnterpriseKey.DBOSConfig.toStringmasksconductorKeyand any credentials inconductorDomain.Tests
EntitlementsTestports Python's entitlement tests and adds cases for the points above:Errorbehind an outage.It uses a local
HttpServeror stubs, never the hosted Conductor.DBOSConfigKeysTestcovers the three settings.The Spring starter tests cover the new property and empty placeholders.
None of these tests needs a database.
🤖 Generated with Claude Code