import type { EnterpriseIdentityProviderDescriptor, IntegrationSlug, OAuthClientSummary,} from "@executor-js/sdk/shared";import type { AuthMethod } from "./auth-placements";// ---------------------------------------------------------------------------// Which connect a click runs on an enterprise-managed server.//// Three routes reach one button, and picking the wrong one is not cosmetic://// - ENTERPRISE. The server declares an identity provider AND an `id_jag`// client is registered for it. `oauth.start` names that client with// `enterprise: { idpClient, idpClientOwner }` and NO subject token — the// server resolves the member's held work identity. There is no consent// screen and nothing for the member to grant.// - DCR / CIMD / bring-your-own-app. Today's behavior, and still correct for// every server that declares no identity provider.// - Neither, yet: the server IS declared managed but nobody has registered the// `id_jag` client it needs. The console must not silently register a fresh// `authorization_code` client and walk the member through per-server consent// as if the declaration were absent — that is the failure this module// exists to name. It falls through to the ordinary route (a member has to// be able to work) while telling an administrator exactly what is missing.//// The decision is STRUCTURAL throughout: the declaration comes off the auth// method, and the client is matched on its recorded intent, its RFC 8707// resource, or its host — never on a name, a message, or the grant of whatever// app a picker happened to default to.// ---------------------------------------------------------------------------/** Whether two hosts are the same, case-insensitively, when both are known. */const sameHost = (a: string | undefined, b: string | undefined): boolean => a !== undefined && b !== undefined && a === b;const hostOf = (url: string | null | undefined): string | undefined => { if (url === null || url === undefined || url.length === 0) return undefined; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL() throws on a stored value that is not a URL; treat as "no host" try { return new URL(url).host.toLowerCase(); } catch { return undefined; }};/** How well a registered `id_jag` app matches the server being connected. Lower * is better; `null` means it does not match at all and must never be used — * presenting an assertion to the wrong Resource Authorization Server is not a * degraded connect, it is a different server. */const enterpriseClientTier = ( client: OAuthClientSummary, target: { readonly integration: IntegrationSlug; readonly resource: string | undefined; },): number | null => { // 1. Recorded intent: registered from THIS integration's dialog. if (client.origin.kind === "manual" && client.origin.integration === target.integration) return 0; // 2. The RFC 9728 resource identifier the app was registered against is this // server's endpoint. This is the identity the ID-JAG is audienced to, so an // exact match is as strong a signal as intent. const resource = target.resource; if (resource !== undefined && client.resource != null && client.resource === resource) return 1; // 3. Same host as the server. Weaker (one host can serve several resources), // kept because an administrator registering by hand may not paste the // endpoint character-for-character. const resourceHost = hostOf(resource); if (sameHost(hostOf(client.tokenUrl), resourceHost)) return 2; if (sameHost(hostOf(client.authorizationUrl), resourceHost)) return 2; return null;};/** * The registered enterprise (`id_jag`) app for one server, or null when none is * registered. * * Ties break toward the organization's app: an enterprise registration is the * organization's by definition, and a personal one that happens to match is the * odd case, not the default. */export const selectEnterpriseClient = ( clients: readonly OAuthClientSummary[], target: { readonly integration: IntegrationSlug; readonly resource: string | undefined; },): OAuthClientSummary | null => { const ranked = clients.flatMap((client: OAuthClientSummary) => { if (client.grant !== "id_jag") return []; const tier = enterpriseClientTier(client, target); return tier === null ? [] : [{ client, tier }]; }); const best = ranked.sort( (a, b) => a.tier - b.tier || Number(b.client.owner === "org") - Number(a.client.owner === "org") || String(a.client.slug).localeCompare(String(b.client.slug)), )[0]; return best?.client ?? null;};/** What the connect button on this method should do. */export type EnterpriseConnectPlan = /** The server declares no identity provider: today's routes apply unchanged. */ | { readonly kind: "unmanaged" } /** Declared managed, but no `id_jag` app is registered for this server. The * ordinary route still runs; an administrator has to register one before the * enterprise branch can be taken at all. */ | { readonly kind: "unregistered"; readonly identityProvider: EnterpriseIdentityProviderDescriptor; } /** Declared managed and registered: name this app with the identity provider * and no subject token. */ | { readonly kind: "enterprise"; readonly identityProvider: EnterpriseIdentityProviderDescriptor; readonly client: OAuthClientSummary; };/** * Decide the connect route for one auth method. * * `method.oauth.enterpriseIdentityProvider` is the ONLY thing that makes a * server managed here — deliberately not the presence of an `id_jag` app, which * an administrator may register before (or after) declaring the server, and * which on its own says nothing about whether members should stop consenting. */export const enterpriseConnectPlan = (input: { readonly method: AuthMethod | undefined; readonly integration: IntegrationSlug; readonly clients: readonly OAuthClientSummary[];}): EnterpriseConnectPlan => { const method = input.method; if (method === undefined || method.kind !== "oauth") return { kind: "unmanaged" }; const identityProvider = method.oauth?.enterpriseIdentityProvider; if (identityProvider === undefined) return { kind: "unmanaged" }; const client = selectEnterpriseClient(input.clients, { integration: input.integration, resource: method.oauth?.resource ?? method.oauth?.discoveryUrl, }); return client === null ? { kind: "unregistered", identityProvider } : { kind: "enterprise", identityProvider, client };};
import type {
EnterpriseIdentityProviderDescriptor,
IntegrationSlug,
OAuthClientSummary,
} from "@executor-js/sdk/shared";
import type { AuthMethod } from "./auth-placements";
// ---------------------------------------------------------------------------
// Which connect a click runs on an enterprise-managed server.
//
// Three routes reach one button, and picking the wrong one is not cosmetic:
//
// - ENTERPRISE. The server declares an identity provider AND an `id_jag`
// client is registered for it. `oauth.start` names that client with
// `enterprise: { idpClient, idpClientOwner }` and NO subject token — the
// server resolves the member's held work identity. There is no consent
// screen and nothing for the member to grant.
// - DCR / CIMD / bring-your-own-app. Today's behavior, and still correct for
// every server that declares no identity provider.
// - Neither, yet: the server IS declared managed but nobody has registered the
// `id_jag` client it needs. The console must not silently register a fresh
// `authorization_code` client and walk the member through per-server consent
// as if the declaration were absent — that is the failure this module
// exists to name. It falls through to the ordinary route (a member has to
// be able to work) while telling an administrator exactly what is missing.
//
// The decision is STRUCTURAL throughout: the declaration comes off the auth
// method, and the client is matched on its recorded intent, its RFC 8707
// resource, or its host — never on a name, a message, or the grant of whatever
// app a picker happened to default to.
// ---------------------------------------------------------------------------
/** Whether two hosts are the same, case-insensitively, when both are known. */
const sameHost = (a: string | undefined, b: string | undefined): boolean =>
a !== undefined && b !== undefined && a === b;
const hostOf = (url: string | null | undefined): string | undefined => {
if (url === null || url === undefined || url.length === 0) return undefined;
// oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL() throws on a stored value that is not a URL; treat as "no host"
try {
return new URL(url).host.toLowerCase();
} catch {
return undefined;
}
};
/** How well a registered `id_jag` app matches the server being connected. Lower
* is better; `null` means it does not match at all and must never be used —
* presenting an assertion to the wrong Resource Authorization Server is not a
* degraded connect, it is a different server. */
const enterpriseClientTier = (
client: OAuthClientSummary,
target: {
readonly integration: IntegrationSlug;
readonly resource: string | undefined;
},
): number | null => {
// 1. Recorded intent: registered from THIS integration's dialog.
if (client.origin.kind === "manual" && client.origin.integration === target.integration) return 0;
// 2. The RFC 9728 resource identifier the app was registered against is this
// server's endpoint. This is the identity the ID-JAG is audienced to, so an
// exact match is as strong a signal as intent.
const resource = target.resource;
if (resource !== undefined && client.resource != null && client.resource === resource) return 1;
// 3. Same host as the server. Weaker (one host can serve several resources),
// kept because an administrator registering by hand may not paste the
// endpoint character-for-character.
const resourceHost = hostOf(resource);
if (sameHost(hostOf(client.tokenUrl), resourceHost)) return 2;
if (sameHost(hostOf(client.authorizationUrl), resourceHost)) return 2;
return null;
};
/**
* The registered enterprise (`id_jag`) app for one server, or null when none is
* registered.
*
* Ties break toward the organization's app: an enterprise registration is the
* organization's by definition, and a personal one that happens to match is the
* odd case, not the default.
*/
export const selectEnterpriseClient = (
clients: readonly OAuthClientSummary[],
target: {
readonly integration: IntegrationSlug;
readonly resource: string | undefined;
},
): OAuthClientSummary | null => {
const ranked = clients.flatMap((client: OAuthClientSummary) => {
if (client.grant !== "id_jag") return [];
const tier = enterpriseClientTier(client, target);
return tier === null ? [] : [{ client, tier }];
});
const best = ranked.sort(
(a, b) =>
a.tier - b.tier ||
Number(b.client.owner === "org") - Number(a.client.owner === "org") ||
String(a.client.slug).localeCompare(String(b.client.slug)),
)[0];
return best?.client ?? null;
};
/** What the connect button on this method should do. */
export type EnterpriseConnectPlan =
/** The server declares no identity provider: today's routes apply unchanged. */
| { readonly kind: "unmanaged" }
/** Declared managed, but no `id_jag` app is registered for this server. The
* ordinary route still runs; an administrator has to register one before the
* enterprise branch can be taken at all. */
| {
readonly kind: "unregistered";
readonly identityProvider: EnterpriseIdentityProviderDescriptor;
}
/** Declared managed and registered: name this app with the identity provider
* and no subject token. */
| {
readonly kind: "enterprise";
readonly identityProvider: EnterpriseIdentityProviderDescriptor;
readonly client: OAuthClientSummary;
};
/**
* Decide the connect route for one auth method.
*
* `method.oauth.enterpriseIdentityProvider` is the ONLY thing that makes a
* server managed here — deliberately not the presence of an `id_jag` app, which
* an administrator may register before (or after) declaring the server, and
* which on its own says nothing about whether members should stop consenting.
*/
export const enterpriseConnectPlan = (input: {
readonly method: AuthMethod | undefined;
readonly integration: IntegrationSlug;
readonly clients: readonly OAuthClientSummary[];
}): EnterpriseConnectPlan => {
const method = input.method;
if (method === undefined || method.kind !== "oauth") return { kind: "unmanaged" };
const identityProvider = method.oauth?.enterpriseIdentityProvider;
if (identityProvider === undefined) return { kind: "unmanaged" };
const client = selectEnterpriseClient(input.clients, {
integration: input.integration,
resource: method.oauth?.resource ?? method.oauth?.discoveryUrl,
});
return client === null
? { kind: "unregistered", identityProvider }
: { kind: "enterprise", identityProvider, client };
};