fix(providers): name the real Björn Lundén connect failure (integration not activated, not bad credentials) (#2322)
* fix(providers): name the real Björn Lundén connect failure: integration not activated, not bad credentials Every Björn Lundén connect in prod has failed with "Leverantören avvisade autentiseringen" (10 consents since June; only BL's own sandbox company ever received tokens). Live-verified against a real customer User-Key today: BL answers 403 "<service>:READ is out of allowed scope for service provider Arcim" on every read endpoint. The key is right and binds the company; the company has simply never activated our integration, and it cannot until BL moves the listing out of sandbox. The generic 403 mapping told the user to re-check what they pasted, which can never help. - BjornLundenClient: isBjornLundenScopeError / isBjornLundenUnknownKeyError, matching the verbatim live 403 and 500 bodies. - submitProviderToken: 403-with-scope-body -> ProviderTokenInvalidError kind 'integration-not-activated'; 500/404 -> 'company-key-not-found'; 401 (our own client_credentials token refused) rethrows as a generic submit failure instead of blaming the pasted key. - New 422 structured errors BL_INTEGRATION_NOT_ACTIVATED and BL_COMPANY_KEY_NOT_FOUND with Swedish/English copy that names the fix (activate under Integrationer in Lundify, else SIE) and where the GUID is. - Wizard copy for BL moved to i18n keys and reordered: activate first, then paste the key; the key only works once the integration is activated. - Tests: route mapping for both kinds, probe classification incl. the captured live bodies, registry entries pinned to 422. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQG9jNyxM7mwMUHWBrFUxY * fix(providers): drop the unknown-key body matcher, the live BL 500 body is not stable Verifying through BjornLundenClient against apigateway.blinfo.se, a made-up User-Key answered 500 with a Spring BeanCreationException for databaseConnector, not the null getCurrentUser() message captured earlier. The unknown-key verdict already keys on the status alone in submitProviderToken; keep only the 403 scope matcher, whose body IS stable. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQG9jNyxM7mwMUHWBrFUxY --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
Jakob Wennberg
parent
41a5728ca7
commit
473b1fd2eb
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { BjornLundenApiError, isBjornLundenScopeError } from '@/lib/providers/bjornlunden/client'
|
||||
|
||||
// Verbatim body captured from apigateway.blinfo.se on 2026-09-05 for a real
|
||||
// customer User-Key whose company never activated the integration. The helper
|
||||
// must keep matching this exact shape.
|
||||
const SCOPE_BODY =
|
||||
'{"headers":{},"body":{"status":"FORBIDDEN","timestamp":"2026-09-05 03:48:38","message":"Calls to details:READ is out of allowed scope for service provider Arcim ","debugMessage":"Calls to details:READ is out of allowed scope for service provider Arcim ","causeChain":[{"name":"ChainBreakingAuthException","message":"Calls to details:READ is out of allowed scope for service provider Arcim "}]},"statusCode":"FORBIDDEN","statusCodeValue":403}'
|
||||
|
||||
describe('isBjornLundenScopeError', () => {
|
||||
it('recognises the live 403 "out of allowed scope" body', () => {
|
||||
const err = new BjornLundenApiError('Björn Lunden API error: 403 Forbidden', 403, SCOPE_BODY)
|
||||
expect(isBjornLundenScopeError(err)).toBe(true)
|
||||
})
|
||||
|
||||
it('is false for a 403 without the scope wording, for other statuses, and for foreign errors', () => {
|
||||
expect(isBjornLundenScopeError(new BjornLundenApiError('403', 403, '{"message":"Forbidden"}'))).toBe(false)
|
||||
expect(isBjornLundenScopeError(new BjornLundenApiError('403', 403))).toBe(false)
|
||||
expect(isBjornLundenScopeError(new BjornLundenApiError('500', 500, SCOPE_BODY))).toBe(false)
|
||||
expect(isBjornLundenScopeError(new Error('out of allowed scope'))).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -19,6 +19,31 @@ export class BjornLundenApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True when BL answered 403 because the company behind the User-Key has not
|
||||
* activated our integration: the service provider holds no scopes for that
|
||||
* company. Live-verified 2026-09-05 against a real customer key:
|
||||
*
|
||||
* {"body":{"status":"FORBIDDEN","message":"Calls to details:READ is out of
|
||||
* allowed scope for service provider Arcim "}, "statusCodeValue":403}
|
||||
*
|
||||
* The key itself is right, so this must never be reported as "check what
|
||||
* you pasted": the fix is on the BL side (activate the integration).
|
||||
*
|
||||
* An UNKNOWN key is a different signal: BL fails to bind the company database
|
||||
* and answers 500. That body is not stable (observed both a null
|
||||
* ServiceInfo.getCurrentUser() message and a Spring BeanCreationException for
|
||||
* databaseConnector), so callers key the unknown-key verdict on the status
|
||||
* alone; only the 403 case has a body worth matching.
|
||||
*/
|
||||
export function isBjornLundenScopeError(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof BjornLundenApiError &&
|
||||
error.statusCode === 403 &&
|
||||
/out of allowed scope/i.test(error.body ?? '')
|
||||
)
|
||||
}
|
||||
|
||||
function isRetryableError(error: unknown): boolean {
|
||||
if (isTimeoutError(error)) return true;
|
||||
if (error instanceof BjornLundenApiError) {
|
||||
|
||||
Reference in New Issue
Block a user