Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/modules/kwp/services/KwpConnectionService.server.ts
T

925 lines
34 KiB
TypeScript

import { getRlsExecutionContext } from "@hivegrid/db/rls-context.server";
import { createAuditEvent } from "@hivegrid/db/audit-events.server";
import { decryptTenantField, encryptTenantField } from "@hivegrid/db/crypto/tenant-envelope.server";
import { CatalogError, foundationErrors } from "@hivegrid/foundation";
import {
discoverKwpMandants,
isValidKwpContextCode,
KWP_APP_ID,
KWP_LEGACY_MANDANT_SELECTION_KIND,
KWP_MANDANT_SELECTION_HEALTH_KEY,
KWP_MANDANT_SELECTION_KIND,
KWP_OAUTH_PENDING_HEALTH_KEY,
KWP_OAUTH_RESERVATION_KIND,
KwpError,
parseKwpOAuthReservationBundle,
parseKwpPendingAttemptLifecycle,
type KwpCredentials,
type KwpMandantBinding,
type KwpOAuthReservationBundle,
type KwpRestorableConnectionSnapshot,
type KwpRestorableConnectionStatus,
} from "@hivegrid/integrations";
import { withTenantRLS, type TenantRlsClient } from "~/utils/db/.server/tenantRls.server";
import { activateOAuthConnection, AppsEngineServiceError } from "~/utils/integrations/appsEngineService.server";
import { buildKwpAuthorizationUrl, exchangeKwpAuthorizationCode, KwpOidcClientError, readKwpOidcRuntimeConfig } from "./kwpOidcClient.server";
import {
createKwpOAuthChallenge,
KWP_OAUTH_CHALLENGE_TTL_MS,
type KwpOAuthChallengePayload,
} from "./kwpOAuthChallenge.server";
const KWP_CONNECTION_LABEL = "KWP";
const KWP_SLOT_LOCK_SALT = 32_001;
const KWP_NOT_CONNECTED_HEALTH_KEY = "integrations.health.notConnected";
const MAX_DISCOVERED_MANDANTS = 100;
const MAX_MANDANT_LABEL_LENGTH = 256;
type KwpUnconfirmedCredentials = Omit<KwpCredentials, "mandantBinding"> & { readonly mandantBinding?: never };
type KwpMandantOption = {
readonly id: number;
readonly code: string;
readonly label: string;
};
type KwpMandantSelectionBundle = {
readonly kind: typeof KWP_MANDANT_SELECTION_KIND;
readonly previous: KwpRestorableConnectionSnapshot | null;
readonly oauth: KwpUnconfirmedCredentials;
readonly discovery: { readonly kind: "available"; readonly options: readonly KwpMandantOption[] } | { readonly kind: "manual" };
readonly issuedAt: string;
};
type PendingKwpMandantSelection = {
readonly connectionId: string;
readonly version: string;
readonly credentialsCiphertext: string;
readonly tokenExpiresAt: Date;
readonly bundle: KwpMandantSelectionBundle;
};
type PendingKwpOAuthReservation = {
readonly connectionId: string;
readonly version: string;
readonly credentialsCiphertext: string;
readonly tokenExpiresAt: Date;
readonly bundle: KwpOAuthReservationBundle;
};
type RestorablePendingKwpAttempt = {
readonly connectionId: string;
readonly version: string;
readonly credentialsCiphertext: string;
readonly tokenExpiresAt: Date;
readonly healthMessageKey: typeof KWP_OAUTH_PENDING_HEALTH_KEY | typeof KWP_MANDANT_SELECTION_HEALTH_KEY;
readonly previous: KwpRestorableConnectionSnapshot | null;
};
export class KwpConnectError extends Error {
readonly code:
| "not_configured"
| "invalid_state"
| "slot_conflict"
| "authorization_failed"
| "exchange_failed"
| "connection_failed"
| "persistence_failed"
| "invalid_selection"
| "selection_expired";
readonly diagnosticCode?: string;
constructor(code: KwpConnectError["code"], options?: { readonly diagnosticCode?: string }) {
super(code);
this.name = "KwpConnectError";
this.code = code;
this.diagnosticCode = options?.diagnosticCode;
}
}
export type BeginKwpOAuthConnectionResult = {
readonly authorizationUrl: string;
readonly setCookie: string;
};
export type CompleteKwpOAuthConnectionResult = {
readonly tenantId: string;
readonly tenantSlug: string;
readonly connectionId: string;
};
export type KwpMandantSelectionView = {
readonly connectionId: string;
readonly customerCode: string;
/** Exact DataConnection.updatedAt CAS fence for the selection submission. */
readonly version: string;
readonly mode: "available" | "manual";
readonly options: readonly KwpMandantOption[];
};
/** Reserve/reuse the tenant's one KWP slot and create a one-time OIDC challenge. */
export async function beginKwpOAuthConnection(input: {
readonly tenantId: string;
readonly tenantSlug: string;
readonly userId: string;
readonly customerCode: string;
}): Promise<BeginKwpOAuthConnectionResult> {
assertBoundIdentity(input.tenantId, input.userId);
let runtimeConfig: ReturnType<typeof readKwpOidcRuntimeConfig>;
try {
runtimeConfig = readKwpOidcRuntimeConfig();
} catch {
throw new KwpConnectError("not_configured");
}
if (runtimeConfig.hiveGridTenantId !== input.tenantId || !isValidKwpContextCode(input.customerCode) || input.customerCode !== runtimeConfig.customerCode) {
throw new KwpConnectError("not_configured");
}
const reservation = await ensureKwpConnectionSlot(input.tenantId, input.userId, input.customerCode);
try {
const challenge = createKwpOAuthChallenge({
tenantId: input.tenantId,
tenantSlug: input.tenantSlug,
userId: input.userId,
connectionId: reservation.connectionId,
connectionVersion: reservation.version,
customerCode: input.customerCode,
});
const authorizationUrl = await buildKwpAuthorizationUrl({
state: challenge.payload.state,
nonce: challenge.payload.nonce,
codeChallenge: challenge.payload.codeChallenge,
customerCode: input.customerCode,
});
return { authorizationUrl, setCookie: challenge.cookie };
} catch {
await restorePendingKwpAttempt({ ...input, connectionId: reservation.connectionId }, reservation).catch(() => undefined);
throw new KwpConnectError("authorization_failed");
}
}
/**
* Exchange the one-time authorization code and discover the user's allowed KWP
* mandates immediately while the five-minute access token is fresh. The OAuth
* material is persisted only as an inert, envelope-encrypted pending bundle;
* no connection becomes active before an explicit mandate binding is confirmed.
*/
export async function completeKwpOAuthConnection(input: {
readonly callbackParameters: { readonly code: string; readonly state: string; readonly iss?: string };
readonly challenge: KwpOAuthChallengePayload;
}): Promise<CompleteKwpOAuthConnectionResult> {
const { challenge } = input;
assertBoundIdentity(challenge.tenantId, challenge.userId);
const reservation = await loadPendingOAuthReservation({
tenantId: challenge.tenantId,
userId: challenge.userId,
connectionId: challenge.connectionId,
expectedVersion: parseConnectionVersion(challenge.connectionVersion),
});
if (isExpired(reservation)) {
await restorePendingKwpAttempt(challenge, reservation);
throw new KwpConnectError("invalid_state");
}
let runtimeConfig: ReturnType<typeof readKwpOidcRuntimeConfig>;
try {
runtimeConfig = readKwpOidcRuntimeConfig();
} catch {
await restorePendingKwpAttempt(challenge, reservation).catch(() => undefined);
throw new KwpConnectError("not_configured");
}
if (
runtimeConfig.hiveGridTenantId !== challenge.tenantId ||
runtimeConfig.customerCode !== challenge.customerCode ||
input.callbackParameters.state !== challenge.state ||
reservation.bundle.customerCode !== challenge.customerCode
) {
await restorePendingKwpAttempt(challenge, reservation).catch(() => undefined);
throw new KwpConnectError("invalid_state");
}
let exchanged: { readonly credentials: KwpCredentials; readonly tokenExpiresAt: Date };
try {
exchanged = await exchangeKwpAuthorizationCode(input.callbackParameters, {
state: challenge.state,
nonce: challenge.nonce,
codeVerifier: challenge.codeVerifier,
customerCode: challenge.customerCode,
});
} catch (error) {
await restorePendingKwpAttempt(challenge, reservation).catch(() => undefined);
throw new KwpConnectError("exchange_failed", {
diagnosticCode: error instanceof KwpOidcClientError ? (error.diagnosticCode ?? `oidc_${error.code.replaceAll("-", "_")}`) : "exchange_unexpected",
});
}
if (!Number.isFinite(exchanged.tokenExpiresAt.getTime())) {
await restorePendingKwpAttempt(challenge, reservation).catch(() => undefined);
throw new KwpConnectError("exchange_failed");
}
const oauth = toUnconfirmedCredentials(exchanged.credentials);
let discovery: KwpMandantSelectionBundle["discovery"];
try {
discovery = await discoverMandantsOrManualFallback(oauth);
} catch (error) {
await restorePendingKwpAttempt(challenge, reservation).catch(() => undefined);
throw error;
}
const bundle: KwpMandantSelectionBundle = {
kind: KWP_MANDANT_SELECTION_KIND,
previous: reservation.bundle.previous,
oauth,
discovery,
issuedAt: new Date().toISOString(),
};
try {
const updated = await withTenantRLS(challenge.tenantId, async (tx) => {
const credentialsCiphertext = await encryptTenantField(challenge.tenantId, JSON.stringify(bundle), { client: tx as never });
return tx.dataConnection.updateMany({
where: {
id: challenge.connectionId,
tenantId: challenge.tenantId,
appId: KWP_APP_ID,
archivedAt: null,
connectionStatus: "pending",
createdByUserId: challenge.userId,
updatedAt: new Date(challenge.connectionVersion),
credentialsCiphertext: reservation.credentialsCiphertext,
tokenExpiresAt: reservation.tokenExpiresAt,
healthMessageKey: KWP_OAUTH_PENDING_HEALTH_KEY,
},
data: {
credentialsCiphertext,
tokenExpiresAt: exchanged.tokenExpiresAt,
connectionStatus: "pending",
healthMessageKey: KWP_MANDANT_SELECTION_HEALTH_KEY,
lastHealthCheck: null,
createdByUserId: challenge.userId,
},
});
});
if (updated.count !== 1) throw new KwpConnectError("invalid_state");
} catch (error) {
if (error instanceof KwpConnectError) throw error;
await restorePendingKwpAttempt(challenge, reservation).catch(() => undefined);
throw new KwpConnectError("persistence_failed");
}
await auditKwpConnection(challenge.tenantId, challenge.userId, "INTEGRATION_OAUTH_MANDANT_SELECTION_PENDING", challenge.connectionId, {
discovery: discovery.kind,
optionCount: discovery.kind === "available" ? discovery.options.length : 0,
});
return {
tenantId: challenge.tenantId,
tenantSlug: challenge.tenantSlug,
connectionId: challenge.connectionId,
};
}
/** Return only the safe selection projection; OAuth material never leaves this service. */
export async function loadKwpMandantSelection(input: {
readonly tenantId: string;
readonly userId: string;
readonly connectionId: string;
}): Promise<KwpMandantSelectionView> {
assertBoundIdentity(input.tenantId, input.userId);
const pending = await loadPendingMandantSelection(input);
if (isExpired(pending)) {
await restorePendingKwpAttempt(input, pending);
throw new KwpConnectError("selection_expired");
}
return {
connectionId: pending.connectionId,
customerCode: pending.bundle.oauth.customerCode,
version: pending.version,
mode: pending.bundle.discovery.kind,
options: pending.bundle.discovery.kind === "available" ? pending.bundle.discovery.options.map((option) => ({ ...option })) : [],
};
}
/** Validate one explicit binding, prove it against KWP, then atomically activate. */
export async function finalizeKwpOAuthConnection(input: {
readonly tenantId: string;
readonly userId: string;
readonly connectionId: string;
readonly version: string;
readonly mandantBinding: KwpMandantBinding;
}): Promise<{ readonly connectionId: string }> {
assertBoundIdentity(input.tenantId, input.userId);
const expectedVersion = parseConnectionVersion(input.version);
const pending = await loadPendingMandantSelection({
tenantId: input.tenantId,
userId: input.userId,
connectionId: input.connectionId,
expectedVersion,
});
if (isExpired(pending)) {
await restorePendingKwpAttempt(input, pending);
throw new KwpConnectError("selection_expired");
}
const mandantBinding = resolveConfirmedBinding(pending.bundle.discovery, input.mandantBinding);
const credentials: KwpCredentials = { ...pending.bundle.oauth, mandantBinding };
try {
await activateOAuthConnection({
tenantId: input.tenantId,
connectionId: input.connectionId,
appId: KWP_APP_ID,
credentials: credentials as unknown as Record<string, unknown>,
tokenExpiresAt: pending.tokenExpiresAt,
actorUserId: input.userId,
expectedUpdatedAt: expectedVersion,
});
} catch (error) {
if (error instanceof AppsEngineServiceError) {
if (error.messageKey === "integrations.kwp.health.tokenInvalid") {
await restorePendingKwpAttempt(input, pending);
throw new KwpConnectError("selection_expired");
}
throw new KwpConnectError("connection_failed", { diagnosticCode: healthDiagnosticCode(error.messageKey) });
}
if (error instanceof CatalogError && error.code === foundationErrors.notFound.code) {
throw new KwpConnectError("invalid_state");
}
throw new KwpConnectError("persistence_failed");
}
await auditKwpConnection(input.tenantId, input.userId, "INTEGRATION_KWP_MANDANT_BIND", input.connectionId, {
binding: mandantBinding.kind,
source: pending.bundle.discovery.kind,
});
return { connectionId: input.connectionId };
}
/** Explicitly abandon one selection and restore the connection from before reconnect. */
export async function cancelKwpOAuthConnectionAttempt(input: {
readonly tenantId: string;
readonly userId: string;
readonly connectionId: string;
readonly version: string;
}): Promise<void> {
assertBoundIdentity(input.tenantId, input.userId);
const pending = await loadRestorablePendingAttempt({
...input,
expectedVersion: parseConnectionVersion(input.version),
});
await restorePendingKwpAttempt(input, pending);
await auditKwpConnection(input.tenantId, input.userId, "INTEGRATION_OAUTH_CANCEL", input.connectionId, {
restored: pending.previous !== null,
});
}
async function discoverMandantsOrManualFallback(credentials: KwpUnconfirmedCredentials): Promise<KwpMandantSelectionBundle["discovery"]> {
try {
const options = sanitizeMandantOptions(await discoverKwpMandants(credentials));
return { kind: "available", options };
} catch (error) {
if (error instanceof KwpConnectError && error.diagnosticCode === "discovery_invalid_response") {
return { kind: "manual" };
}
if (isManualDiscoveryFallback(error)) return { kind: "manual" };
throw new KwpConnectError("connection_failed", { diagnosticCode: discoveryDiagnosticCode(error) });
}
}
function isManualDiscoveryFallback(error: unknown): boolean {
if (!(error instanceof KwpError)) return false;
if (error.httpStatus === 401 || error.code === "auth_token_invalid" || error.code === "invalid_credentials" || error.code === "relay_host_invalid") {
return false;
}
return (
error.httpStatus === 403 ||
error.httpStatus === 404 ||
error.code === "auth_scope_missing" ||
error.code === "relay_not_found" ||
error.code === "mandant_discovery_unavailable" ||
error.code === "invalid_response" ||
error.code === "provider_outage" ||
error.code === "rate_limit_exceeded"
);
}
function discoveryDiagnosticCode(error: unknown): string {
return error instanceof KwpError ? `discovery_${error.code}` : "discovery_unexpected";
}
function sanitizeMandantOptions(value: unknown): readonly KwpMandantOption[] {
if (!Array.isArray(value) || value.length > MAX_DISCOVERED_MANDANTS) {
throw new KwpConnectError("connection_failed", { diagnosticCode: "discovery_invalid_response" });
}
const options: KwpMandantOption[] = [];
const seenCodes = new Set<string>();
for (const candidate of value) {
if (!isRecord(candidate)) throw new KwpConnectError("connection_failed", { diagnosticCode: "discovery_invalid_response" });
const { id, code, label } = candidate;
if (
typeof id !== "number" ||
!Number.isSafeInteger(id) ||
!isValidKwpContextCode(code) ||
(label !== undefined && typeof label !== "string") ||
(typeof label === "string" && (label.length > MAX_MANDANT_LABEL_LENGTH || label.trim() !== label)) ||
seenCodes.has(code)
) {
throw new KwpConnectError("connection_failed", { diagnosticCode: "discovery_invalid_response" });
}
seenCodes.add(code);
options.push({ id, code, label: typeof label === "string" && label ? label : code });
}
return options;
}
function toUnconfirmedCredentials(credentials: KwpCredentials): KwpUnconfirmedCredentials {
return {
accessToken: credentials.accessToken,
refreshToken: credentials.refreshToken,
expiresAt: credentials.expiresAt,
issuer: credentials.issuer,
clientId: credentials.clientId,
clientSecret: credentials.clientSecret,
customerCode: credentials.customerCode,
scopes: [...credentials.scopes],
};
}
async function loadPendingMandantSelection(input: {
readonly tenantId: string;
readonly userId: string;
readonly connectionId: string;
readonly expectedVersion?: Date;
}): Promise<PendingKwpMandantSelection> {
try {
return await withTenantRLS(input.tenantId, async (tx) => {
const row = await findPendingAttemptRow(tx, input);
const plaintext = await decryptTenantField(input.tenantId, row.credentialsCiphertext, { client: tx as never });
const bundle = parseMandantSelectionBundle(plaintext);
if (
row.healthMessageKey !== KWP_MANDANT_SELECTION_HEALTH_KEY ||
row.tokenExpiresAt.toISOString() !== bundle.oauth.expiresAt
) {
throw new KwpConnectError("invalid_state");
}
return {
connectionId: row.id,
version: row.updatedAt.toISOString(),
credentialsCiphertext: row.credentialsCiphertext,
tokenExpiresAt: row.tokenExpiresAt,
bundle,
};
});
} catch (error) {
if (error instanceof KwpConnectError) throw error;
throw new KwpConnectError("persistence_failed");
}
}
async function loadPendingOAuthReservation(input: {
readonly tenantId: string;
readonly userId: string;
readonly connectionId: string;
readonly expectedVersion?: Date;
}): Promise<PendingKwpOAuthReservation> {
try {
return await withTenantRLS(input.tenantId, async (tx) => {
const row = await findPendingAttemptRow(tx, input);
const plaintext = await decryptTenantField(input.tenantId, row.credentialsCiphertext, { client: tx as never });
const bundle = parseKwpOAuthReservationBundle(plaintext);
if (
!bundle ||
row.healthMessageKey !== KWP_OAUTH_PENDING_HEALTH_KEY ||
row.tokenExpiresAt.toISOString() !== bundle.expiresAt
) {
throw new KwpConnectError("invalid_state");
}
return {
connectionId: row.id,
version: row.updatedAt.toISOString(),
credentialsCiphertext: row.credentialsCiphertext,
tokenExpiresAt: row.tokenExpiresAt,
bundle,
};
});
} catch (error) {
if (error instanceof KwpConnectError) throw error;
throw new KwpConnectError("persistence_failed");
}
}
async function loadRestorablePendingAttempt(input: {
readonly tenantId: string;
readonly userId: string;
readonly connectionId: string;
readonly expectedVersion?: Date;
}): Promise<RestorablePendingKwpAttempt> {
try {
return await withTenantRLS(input.tenantId, async (tx) => {
const row = await findPendingAttemptRow(tx, input);
const plaintext = await decryptTenantField(input.tenantId, row.credentialsCiphertext, { client: tx as never });
const lifecycle = parseKwpPendingAttemptLifecycle(plaintext);
const expectedHealthKey = lifecycle?.phase === "oauth" ? KWP_OAUTH_PENDING_HEALTH_KEY : KWP_MANDANT_SELECTION_HEALTH_KEY;
if (!lifecycle || row.healthMessageKey !== expectedHealthKey) throw new KwpConnectError("invalid_state");
return {
connectionId: row.id,
version: row.updatedAt.toISOString(),
credentialsCiphertext: row.credentialsCiphertext,
tokenExpiresAt: row.tokenExpiresAt,
healthMessageKey: expectedHealthKey,
previous: lifecycle.previous,
};
});
} catch (error) {
if (error instanceof KwpConnectError) throw error;
throw new KwpConnectError("persistence_failed");
}
}
async function findPendingAttemptRow(
tx: TenantRlsClient,
input: {
readonly tenantId: string;
readonly userId: string;
readonly connectionId: string;
readonly expectedVersion?: Date;
}
) {
const row = await tx.dataConnection.findFirst({
where: {
id: input.connectionId,
tenantId: input.tenantId,
appId: KWP_APP_ID,
archivedAt: null,
connectionStatus: "pending",
createdByUserId: input.userId,
...(input.expectedVersion ? { updatedAt: input.expectedVersion } : {}),
},
select: {
id: true,
updatedAt: true,
credentialsCiphertext: true,
tokenExpiresAt: true,
healthMessageKey: true,
},
});
if (!row?.credentialsCiphertext || !row.tokenExpiresAt) throw new KwpConnectError("invalid_state");
return {
...row,
credentialsCiphertext: row.credentialsCiphertext,
tokenExpiresAt: row.tokenExpiresAt,
};
}
async function restorePendingKwpAttempt(
input: { readonly tenantId: string; readonly userId: string; readonly connectionId: string },
pending: PendingKwpMandantSelection | PendingKwpOAuthReservation | RestorablePendingKwpAttempt
): Promise<void> {
const previous = "bundle" in pending ? pending.bundle.previous : pending.previous;
const healthMessageKey =
"healthMessageKey" in pending
? pending.healthMessageKey
: pending.bundle.kind === KWP_OAUTH_RESERVATION_KIND
? KWP_OAUTH_PENDING_HEALTH_KEY
: KWP_MANDANT_SELECTION_HEALTH_KEY;
try {
const restored = await withTenantRLS(input.tenantId, (tx) =>
tx.dataConnection.updateMany({
where: {
id: input.connectionId,
tenantId: input.tenantId,
appId: KWP_APP_ID,
archivedAt: null,
connectionStatus: "pending",
createdByUserId: input.userId,
updatedAt: new Date(pending.version),
credentialsCiphertext: pending.credentialsCiphertext,
tokenExpiresAt: pending.tokenExpiresAt,
healthMessageKey,
},
data: {
...restorationData(previous, input.userId),
// Always invalidate the browser's old callback/selection fence.
updatedAt: new Date(Math.max(Date.now(), new Date(pending.version).getTime() + 1)),
},
})
);
if (restored.count !== 1) throw new KwpConnectError("invalid_state");
} catch (error) {
if (error instanceof KwpConnectError) throw error;
throw new KwpConnectError("persistence_failed");
}
}
function parseMandantSelectionBundle(plaintext: string): KwpMandantSelectionBundle {
let value: unknown;
try {
value = JSON.parse(plaintext);
} catch {
throw new KwpConnectError("invalid_state");
}
if (
!isRecord(value) ||
(value.kind !== KWP_MANDANT_SELECTION_KIND && value.kind !== KWP_LEGACY_MANDANT_SELECTION_KIND) ||
!isRecord(value.oauth) ||
!isRecord(value.discovery)
) {
throw new KwpConnectError("invalid_state");
}
const lifecycle = parseKwpPendingAttemptLifecycle(plaintext);
if (!lifecycle || lifecycle.phase !== "mandant") throw new KwpConnectError("invalid_state");
const oauth = value.oauth;
if (
typeof oauth.accessToken !== "string" ||
!oauth.accessToken ||
typeof oauth.refreshToken !== "string" ||
!oauth.refreshToken ||
typeof oauth.expiresAt !== "string" ||
!Number.isFinite(Date.parse(oauth.expiresAt)) ||
typeof oauth.issuer !== "string" ||
!oauth.issuer ||
typeof oauth.clientId !== "string" ||
!oauth.clientId ||
typeof oauth.clientSecret !== "string" ||
!oauth.clientSecret ||
!isValidKwpContextCode(oauth.customerCode) ||
!Array.isArray(oauth.scopes) ||
!oauth.scopes.every((scope) => typeof scope === "string" && scope.length > 0) ||
oauth.mandantBinding !== undefined ||
typeof value.issuedAt !== "string" ||
!Number.isFinite(Date.parse(value.issuedAt))
) {
throw new KwpConnectError("invalid_state");
}
let discovery: KwpMandantSelectionBundle["discovery"];
if (value.discovery.kind === "manual") {
discovery = { kind: "manual" };
} else if (value.discovery.kind === "available") {
try {
discovery = { kind: "available", options: sanitizeMandantOptions(value.discovery.options) };
} catch {
throw new KwpConnectError("invalid_state");
}
} else {
throw new KwpConnectError("invalid_state");
}
return {
kind: KWP_MANDANT_SELECTION_KIND,
previous: lifecycle.previous,
oauth: {
accessToken: oauth.accessToken,
refreshToken: oauth.refreshToken,
expiresAt: oauth.expiresAt,
issuer: oauth.issuer,
clientId: oauth.clientId,
clientSecret: oauth.clientSecret,
customerCode: oauth.customerCode,
scopes: [...oauth.scopes],
},
discovery,
issuedAt: value.issuedAt,
};
}
function resolveConfirmedBinding(discovery: KwpMandantSelectionBundle["discovery"], requested: KwpMandantBinding): KwpMandantBinding {
if (discovery.kind === "available" && discovery.options.length > 0) {
if (requested.kind !== "selected") throw new KwpConnectError("invalid_selection");
const option = discovery.options.find((candidate) => candidate.code === requested.code);
if (!option) throw new KwpConnectError("invalid_selection");
return { kind: "selected", code: option.code, label: option.label };
}
if (requested.kind === "none") return { kind: "none" };
if (requested.kind !== "selected" || !isValidKwpContextCode(requested.code) || requested.code.trim() !== requested.code) {
throw new KwpConnectError("invalid_selection");
}
return { kind: "selected", code: requested.code };
}
function restorationData(previous: KwpRestorableConnectionSnapshot | null, actorUserId: string) {
if (!previous) {
return {
credentialsCiphertext: null,
tokenExpiresAt: null,
connectionStatus: "pending" as const,
healthMessageKey: KWP_NOT_CONNECTED_HEALTH_KEY,
lastHealthCheck: new Date(),
createdByUserId: actorUserId,
};
}
return {
credentialsCiphertext: previous.credentialsCiphertext,
tokenExpiresAt: previous.tokenExpiresAt ? new Date(previous.tokenExpiresAt) : null,
connectionStatus: previous.connectionStatus,
healthMessageKey: previous.healthMessageKey,
lastHealthCheck: previous.lastHealthCheck ? new Date(previous.lastHealthCheck) : null,
createdByUserId: previous.createdByUserId,
};
}
function isExpired(pending: { readonly tokenExpiresAt: Date }): boolean {
return !Number.isFinite(pending.tokenExpiresAt.getTime()) || pending.tokenExpiresAt.getTime() <= Date.now();
}
function parseConnectionVersion(value: string): Date {
const timestamp = Date.parse(value);
if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString() !== value) throw new KwpConnectError("invalid_state");
return new Date(timestamp);
}
function healthDiagnosticCode(messageKey: string): string {
switch (messageKey) {
case "integrations.kwp.health.tokenInvalid":
return "health_token_invalid";
case "integrations.kwp.health.scopeMissing":
return "health_scope_missing";
case "integrations.kwp.health.rateLimited":
return "health_rate_limited";
case "integrations.kwp.health.providerOutage":
return "health_provider_outage";
case "integrations.kwp.health.relayInvalid":
return "health_relay_invalid";
case "integrations.kwp.health.credentialsInvalid":
return "health_credentials_invalid";
case "integrations.kwp.health.mandantUnconfirmed":
return "health_mandant_unconfirmed";
default:
return "health_unknown";
}
}
async function ensureKwpConnectionSlot(
tenantId: string,
actorUserId: string,
customerCode: string
): Promise<PendingKwpOAuthReservation> {
let created = false;
const issuedAt = new Date();
const expiresAt = new Date(issuedAt.getTime() + KWP_OAUTH_CHALLENGE_TTL_MS);
const connection = await withTenantRLS(tenantId, async (tx) => {
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${`kwp-oauth-slot:${tenantId}`}, ${KWP_SLOT_LOCK_SALT}))`;
const candidates = await tx.dataConnection.findMany({
where: { tenantId, appId: KWP_APP_ID, archivedAt: null },
select: {
id: true,
updatedAt: true,
connectionStatus: true,
credentialsCiphertext: true,
tokenExpiresAt: true,
healthMessageKey: true,
lastHealthCheck: true,
createdByUserId: true,
},
orderBy: { createdAt: "asc" },
take: 2,
});
if (candidates.length > 1) throw new KwpConnectError("slot_conflict");
if (candidates[0]) {
const candidate = candidates[0];
if (candidate.healthMessageKey === "integrations.health.refreshInProgress") {
throw new KwpConnectError("slot_conflict");
}
const previous = await resolvePreviousConnectionSnapshot(tenantId, tx, candidate);
const bundle: KwpOAuthReservationBundle = {
kind: KWP_OAUTH_RESERVATION_KIND,
customerCode,
previous,
issuedAt: issuedAt.toISOString(),
expiresAt: expiresAt.toISOString(),
};
const credentialsCiphertext = await encryptTenantField(tenantId, JSON.stringify(bundle), { client: tx as never });
const attemptVersion = new Date(Math.max(Date.now(), candidate.updatedAt.getTime() + 1));
const invalidated = await tx.dataConnection.updateMany({
where: {
id: candidate.id,
tenantId,
appId: KWP_APP_ID,
archivedAt: null,
connectionStatus: candidate.connectionStatus,
updatedAt: candidate.updatedAt,
credentialsCiphertext: candidate.credentialsCiphertext,
tokenExpiresAt: candidate.tokenExpiresAt,
healthMessageKey: candidate.healthMessageKey,
lastHealthCheck: candidate.lastHealthCheck,
},
data: {
credentialsCiphertext,
tokenExpiresAt: expiresAt,
connectionStatus: "pending",
healthMessageKey: KWP_OAUTH_PENDING_HEALTH_KEY,
lastHealthCheck: null,
createdByUserId: actorUserId,
updatedAt: attemptVersion,
},
});
if (invalidated.count !== 1) throw new KwpConnectError("slot_conflict");
return {
connectionId: candidate.id,
version: attemptVersion.toISOString(),
credentialsCiphertext,
tokenExpiresAt: expiresAt,
bundle,
};
}
const bundle: KwpOAuthReservationBundle = {
kind: KWP_OAUTH_RESERVATION_KIND,
customerCode,
previous: null,
issuedAt: issuedAt.toISOString(),
expiresAt: expiresAt.toISOString(),
};
const credentialsCiphertext = await encryptTenantField(tenantId, JSON.stringify(bundle), { client: tx as never });
const connection = await tx.dataConnection.create({
data: {
tenantId,
appId: KWP_APP_ID,
label: KWP_CONNECTION_LABEL,
credentialsCiphertext,
connectionStatus: "pending",
healthMessageKey: KWP_OAUTH_PENDING_HEALTH_KEY,
tokenExpiresAt: expiresAt,
createdByUserId: actorUserId,
},
select: { id: true, updatedAt: true },
});
created = true;
return {
connectionId: connection.id,
version: connection.updatedAt.toISOString(),
credentialsCiphertext,
tokenExpiresAt: expiresAt,
bundle,
};
});
await auditKwpConnection(tenantId, actorUserId, "INTEGRATION_OAUTH_RESERVE", connection.connectionId, { created });
return connection;
}
async function resolvePreviousConnectionSnapshot(
tenantId: string,
tx: TenantRlsClient,
candidate: {
readonly connectionStatus: string;
readonly credentialsCiphertext: string | null;
readonly tokenExpiresAt: Date | null;
readonly healthMessageKey: string | null;
readonly lastHealthCheck: Date | null;
readonly createdByUserId: string | null;
}
): Promise<KwpRestorableConnectionSnapshot | null> {
if (candidate.connectionStatus === "pending") {
if (!candidate.credentialsCiphertext) return null;
const plaintext = await decryptTenantField(tenantId, candidate.credentialsCiphertext, { client: tx as never });
const lifecycle = parseKwpPendingAttemptLifecycle(plaintext);
if (!lifecycle) throw new KwpConnectError("slot_conflict");
return lifecycle.previous;
}
if (!isRestorableConnectionStatus(candidate.connectionStatus)) throw new KwpConnectError("slot_conflict");
return {
credentialsCiphertext: candidate.credentialsCiphertext,
connectionStatus: candidate.connectionStatus,
tokenExpiresAt: candidate.tokenExpiresAt?.toISOString() ?? null,
healthMessageKey: candidate.healthMessageKey,
lastHealthCheck: candidate.lastHealthCheck?.toISOString() ?? null,
createdByUserId: candidate.createdByUserId,
};
}
function isRestorableConnectionStatus(value: string): value is KwpRestorableConnectionStatus {
return value === "active" || value === "degraded" || value === "broken" || value === "revoked";
}
async function auditKwpConnection(tenantId: string, actorId: string, action: string, targetId: string, metadata: Record<string, unknown>): Promise<void> {
await createAuditEvent({
tenantId,
actorId,
actorType: "user",
action,
targetType: "DataConnection",
targetId,
sourceModule: "integrations",
outcome: "success",
metadata: { appId: KWP_APP_ID, ...metadata },
}).catch(() => undefined);
}
function assertBoundIdentity(tenantId: string, userId: string): void {
const context = getRlsExecutionContext();
if (context?.tenantId !== tenantId || context.userId !== userId) {
throw new KwpConnectError("invalid_state");
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}