Files
accounted/lib/providers/bjornlunden/activation.ts
T
a769bc9d03 feat(migration): Björn Lundén activation through Lundify's redirect flow (#2374)
* feat(migration): Björn Lundén activation through Lundify's redirect flow

BL issued our integration activation key on 2026-09-07. With
BJORN_LUNDEN_ACTIVATION_KEY set, the connect step offers "Aktivera i
Lundify": the customer logs in at Lundify, picks the company and accepts
the scopes, and Lundify returns the company's User-Key to our callback as
publicKey with our one-time state echoed as extra. The manual User-Key
field stays as a folded fallback for companies that activated inside
Lundify already.

The callback folds publicKey/extra into the OAuth-shaped locals, so the
atomic state consumption, initiator binding and white-label handoff run
unchanged; only the final step differs: submitProviderToken (the same
client-credentials probe as the manual field) instead of an OAuth code
exchange, owned by the consent's company read from the server-written row.
consumeOAuthState/consumeHandoff now return that company id.

Closes #2323.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGDm5S2XPm6np1sKWB4U6L

* fix(migration): reset the previous connect attempt before a new provider request

Review follow-up: a failed /connect used to leave the earlier consent id
and one-time activation URL in place, so the step kept offering a link
that completed the previous consent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGDm5S2XPm6np1sKWB4U6L

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:38:07 +02:00

54 lines
2.3 KiB
TypeScript

/**
* Björn Lundén activation via Lundify's redirect flow.
*
* BL documents three ways a customer can activate an integration (Company
* Activation & Key Retrieval, developer.bjornlunden.se/2025/03/31/activation-guide/).
* The third one removes the GUID copy-paste from our connect step: we send the
* user to Lundify with our integration activation key, they log in, pick the
* company and accept the scopes, and Lundify sends them back to our callback
* with the company's User-Key as `publicKey` and our opaque state as `extra`.
*
* https://lundify.com/activate-integration/{integrationActivationKey}/{encodedRedirectUrl}?extra={state}
* -> {redirectUrl}?publicKey={userKey}&extra={state}
*
* The activation key is issued by BL once per service provider (ours arrived
* 2026-09-07). It is not a secret in the credential sense: it is embedded in a
* URL the customer's browser visits. It still lives in an env var so
* self-hosted installs without a BL listing simply keep the manual User-Key
* field.
*/
export const LUNDIFY_ACTIVATION_BASE_URL = 'https://lundify.com/activate-integration';
/**
* The activation key BL issued for this service provider, or null when the
* redirect flow is not configured (the connect step then only offers the
* manual User-Key field).
*/
export function getBjornLundenActivationKey(): string | null {
const key = process.env.BJORN_LUNDEN_ACTIVATION_KEY?.trim();
return key ? key : null;
}
/**
* Build the Lundify activation URL for one connect attempt.
*
* `redirectUrl` is our callback (the same one the OAuth providers use) and is
* percent-encoded as a single path segment, which is what BL's
* `{encodedRedirectUrl}` placeholder asks for: an unencoded URL would split on
* its own slashes. `state` is the opaque one-time code minted for the consent;
* Lundify echoes it back untouched as `extra`, so the callback can resolve the
* consent from a server-written row instead of trusting anything in the query.
*/
export function buildLundifyActivationUrl(
activationKey: string,
redirectUrl: string,
state: string,
): string {
const url = new URL(
`${LUNDIFY_ACTIVATION_BASE_URL}/${encodeURIComponent(activationKey)}/${encodeURIComponent(redirectUrl)}`,
);
url.searchParams.set('extra', state);
return url.toString();
}