Skip to content

v5.0.0

Latest

Choose a tag to compare

@shawn-tsanet shawn-tsanet released this 31 Oct 22:59
47cdb4a

connect-library 5.0.0, attachment-receiver 5.0.0, connect-e2ee 5.0.0

Major version. connect-e2ee now reports a sender's keys being unavailable as their own verification outcome, instead of a SENDER_KEY refusal (tsanetgit/Connect_SDK#147). That adds a case to a sealed type, which breaks exhaustive switches at source. TsaNetApiSession becomes AutoCloseable, and the deprecated CaseNoteDto and CollaborationRequestStatusDto constructors are removed (tsanetgit/Connect_SDK#139). PinnedJwksPartnerKeys waits a minute after a failed fetch before fetching that company's keys again (tsanetgit/Connect_SDK#173), and a refused OAuth token call names its error code (tsanetgit/Connect_SDK#45). A token request that fails without being refused is now its own ConnectApiException, and PinnedJwksPartnerKeys is now AutoCloseable. The release also redacts credentials, tokens and partner-written text from the toString() of SDK records, and refuses JWKS URLs that have no host (tsanetgit/Connect_SDK#162). connect-library makes its mapping from the generated OpenAPI models to its records public, as OpenApiRecords (tsanetgit/Connect_SDK#146). attachment-receiver has no changes of its own; it moves to 5.0.0 with the others. Runtime unchanged: Spring Boot 4.1, Jackson 3, JDK 21.

The upgrade guide is the 5.0.0 entry in skills/connect-sdk-deploy/references/consume-artifact.md. Coming from 3.0.0 or earlier, apply the earlier entries first.

New in 5.0.0

  1. Verification.KeysUnavailable (#166). E2eeProfile.verifyCase and verifyNote return it when PartnerKeys throws IOException fetching the sending company's keys, either at first or on the refetch for an unknown kid.

    • Retry, don't refuse: it carries no value and the record wasn't judged. It comes before the replay check, so no jti is used up, and verifying the same record again later is checked from scratch, including as FIRST_RECEIPT.
    • Back off, and bound the retries: after a failed fetch, PinnedJwksPartnerKeys doesn't fetch that company's keys again for a minute. A retry inside the minute returns KeysUnavailable again without contacting the partner, and keys can stay unavailable for up to a minute after the last failed fetch, even once its endpoint has recovered. A set it holds that hasn't expired still answers for a kid in that set, and a kid that isn't there reads as KeysUnavailable during the wait. A timeout or a non-200 answer can clear on its own. But PinnedJwksPartnerKeys also throws for a company with no configured JWKS URL, and for a set that is too large, unparseable or holds a private key, which a retry won't fix until the configuration or the partner's set changes. Cap retries and alert when they run out.
    • Every other SENDER_KEY refusal is unchanged.
  2. Sensitive values are redacted in diagnostics (#165).

    • SDK records print <redacted> in toString() for passwords, client and webhook secrets, and authentication tokens.
    • Case, note and request tokens count as sensitive, because they work as capabilities, not just IDs. They are redacted too, including in webhook raw payloads.
    • Attachment parameters print their names only.
    • CaseNoteDto, CaseResponseDto, CollaborationRequestStatusDto and StoredAttachmentForwardResultDto leave partner-written text and personal details out of toString().
    • Constructors and accessors are unchanged. JSON serialization and explicit accessor calls are not redacted.
  3. OpenApiRecords, the public mapping from generated models to records (#168, #169). A connector that calls the generated client directly can build the records connect-e2ee takes with the same code the facades use, instead of keeping its own copy. A copy that drops a value a manifest covers gets the record refused at FIELDS.

    • collaborationRequest(CollaborationRequestStatusDTO) returns a CollaborationRequestStatusDto.
    • caseNote(CaseNoteDTO, caseToken) returns a CaseNoteDto. The generated note carries no case token, so pass the token of the case the note was read under; verifyNote compares it with the case's.
    • formField(FieldMetadataDTO) returns a FormFieldDto.
    • The parameters are generated types, so a rename in the Connect OpenAPI spec changes these public signatures.
  4. Sessions can be closed (#179). TsaNetApiSession is AutoCloseable. Each session holds a JDK HttpClient, with its selector thread and pooled connections, from the moment it opens, and until now nothing released them. Close a session you drop, for example on a credential change.

    • It doesn't wait: requests already sent run to completion. A later one fails the way an unreachable API does: ConnectApiException of kind CONNECTIVITY, or AttachmentV2Exception client/connectivity on the V2 facades. A send still running can't complete its grant, which stays open until it expires.
    • AccountScopedTsaNetApiSession goes back to having no session until the next login, and a login or authenticate as a different account closes the previous account's session.
    • Each V2 upload runs on a client of its own, stopped when it ends, so uploads no longer share pooled connections.
    • A Spring @Bean that returns a session is now closed when its context shuts down.
  5. PinnedJwksPartnerKeys is now AutoCloseable (#181). Close an instance you drop: a fetch already sent finishes, a set it holds still answers for a kid in that set, and a lookup that would fetch reads as KeysUnavailable.

Breaking changes in 5.0.0

  • A switch over Verification must handle KeysUnavailable. Verification is sealed, and now permits Verified, Refused and KeysUnavailable. An exhaustive switch without the new case stops compiling. A consumer that bumps without recompiling links, but throws MatchException at the first unavailable-keys read. Add the case and retry the record, with a limit:

    switch (e2ee.verifyCase(record, receipt)) {
        case Verified<DecryptedCase> ok -> handle(ok.content());
        case Refused<DecryptedCase> no -> audit(no.step(), no.field(), no.reason());
        case KeysUnavailable<DecryptedCase> later -> retryLater(record);   // a bounded number of times
    }
  • A default arm or an instanceof chain over Verification still compiles, and KeysUnavailable lands in that branch, or throws ClassCastException on a cast. Check those too, and treat KeysUnavailable as retry-with-a-limit, never as refused or verified.

  • TsaNetApiSession gains an abstract close(). Code that implements it must add one.

  • The deprecated record constructors are gone: CaseNoteDto's 14- and 18-argument forms and CollaborationRequestStatusDto's 10-, 11- and 13-argument forms. Call the canonical constructor (20 and 16 arguments), passing null for a value you don't have.

Also check in 5.0.0

  • Refusal steps that move. Step 3's party checks now run before the key lookup: the record naming no receiving party, and this member being neither its iss nor its aud. A record whose parties don't include this member is refused at CLAIMS, where it used to be refused at SENDER_KEY (wrong kid) or SIGNATURE. Anything that audits or alerts by step will see that shift. The reason strings are unchanged.
  • Unavailable keys aren't refusals any more. Code that treated a SENDER_KEY refusal with the reason "the sending company's public keys are unavailable" as retryable, by matching the string, should switch on KeysUnavailable instead. That refusal is no longer produced.
  • PinnedJwksPartnerKeys refuses a host-less URL when it's constructed. It throws IllegalArgumentException("A JWKS URL must have a host"). Before, such a URL failed only when it was fetched, as an unchecked exception. This covers every entry in the map, including companies you never read from.
  • toString() output changes on the records above. Code that parsed toString(), or that relied on a log line carrying a token, a note's text or a case's description, sees <redacted> or no field at all.
  • Messages without tokens. The SDK's own exception and sync messages no longer contain request tokens. WebhookInboundService masks the request token in a failed sync's message. Rows in webhook_inbound_event written by 4.0.0 keep their old messages.
  • A refused OAuth token call names its error code. The IllegalStateException message is OAuth client-credentials request failed: HTTP <status> (<code>) when the token endpoint answers with a known OAuth error code, for example invalid_client. Otherwise it stays ... HTTP <status>. Entra's error_description still isn't included, because it names the app. Code that matched the old message exactly should match its prefix.
  • Tokens and parse errors (#181). A token response whose access token isn't all visible ASCII now fails sign-in with a fixed message, and a malformed token response, an unusable token URL or a webhook callback URL that isn't a URI no longer has its value quoted in an exception. A token request that fails without being refused is now ConnectApiException of kind OTHER, status 0, title Token request: an unusable token URL or API base URL at sign-in, a token or login response the client can't read, a malformed one, a 5xx, 408 or 429 from the token endpoint, or a login response with no access token. Those were IllegalArgumentException, RestClientException or IllegalStateException, so a 4.0.0 catch stops matching, and an expired session that meets one no longer reads as a 401 Session expired. A refusal by the token endpoint stays IllegalStateException, and a 401 Session expired on renewal of an expired token.
  • A note with no priority is refused with A note priority is required (#181).
  • Malformed partner input no longer throws. A manifest or protected value that is the JSON null, or a recipient kid that isn't a string, is now a Refused. A JWKS endpoint serving such a body or a malformed HTTP response is now KeysUnavailable. All of these used to escape verifyCase and verifyNote as unchecked exceptions.

Known limits in 5.0.0

  • A key rotation can still refuse a valid case. PinnedJwksPartnerKeys refetches a company's keys at most once a minute. Within that window, a manifest naming a new kid sees the cached set, and the case is refused at SENDER_KEY.
  • Server text is passed through. A ConnectApiException carries the API's own error text, which can echo a token, for example "Case with token ... not found."
  • TSANet-integration-app still prints tokens where an operator asks for them, because later commands take them as arguments.
  • A slow endpoint can block a company's keys for a minute. A fetch cancelled after running for 2 seconds or more starts the wait for every caller of that instance, for that company.
  • A very short budget isn't throttled. A fetch interrupted before it has run for 2 seconds doesn't start the minute's wait, so a caller with a budget under 2 seconds on an endpoint that hangs still makes a request on each lookup.

demo-ui

  • Cross-site requests are refused (#177). The Basic gate now uses CSRF tokens, and a cross-site form gets a 403.
  • Request errors keep their own status (#178). A malformed request answers 400, 404, 405 or 415 instead of 502, and the SDK's validation messages answer 400 with their reason. Anything unexpected still answers 502, now with a fixed message.
  • Upload filenames (#180). The V1 upload route uses the same filename handling as V2, and a malformed multipart answers 400.
  • A request is refused before its body is read when it isn't signed in (#182).
  • A guest login has a lower upload limit (#182): 1 GB by default, set with TSANET_DEMO_AUTH_GUEST_MAX_UPLOAD_BYTES. A guest's POST, PUT or PATCH must declare a Content-Length, and a guest can't send chunked. The operator keeps 20 GB.