147 lines · 6.5 KB · TypeScript
RawDownload
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 };};