// src/oauth-helpers.ts
import { Data, Effect, Predicate } from "effect";
import * as oauth from "oauth4webapi";
var OAuth2Error = class extends Data.TaggedError("OAuth2Error") {
};
var OAUTH2_REFRESH_SKEW_MS = 6e4;
var OAUTH2_DEFAULT_TIMEOUT_MS = 2e4;
var isLoopbackHttpUrl = (value) => {
  if (!URL.canParse(value)) return false;
  const url = new URL(value);
  if (url.protocol !== "http:") return false;
  const hostname = url.hostname.toLowerCase();
  return hostname === "localhost" || hostname === "0.0.0.0" || hostname === "::1" || hostname === "[::1]" || hostname.startsWith("127.");
};
var isSupportedOAuthEndpointUrl = (value, policy = {}) => {
  if (!URL.canParse(value)) return false;
  const url = new URL(value);
  return url.protocol === "https:" || isLoopbackHttpUrl(value) || url.protocol === "http:" && policy.allowHttp === true;
};
var assertSupportedOAuthEndpointUrl = (value, label = "OAuth endpoint URL", policy = {}) => {
  if (isSupportedOAuthEndpointUrl(value, policy)) return value;
  throw new TypeError(`${label} must use https: or loopback http:`);
};
var createPkceCodeVerifier = () => oauth.generateRandomCodeVerifier();
var createPkceCodeChallenge = (verifier) => oauth.calculatePKCECodeChallenge(verifier);
var createOAuthState = () => oauth.generateRandomState();
var buildAuthorizationUrl = (input) => {
  const url = new URL(
    assertSupportedOAuthEndpointUrl(
      input.authorizationUrl,
      "Authorization URL",
      input.endpointUrlPolicy
    )
  );
  const separator = input.scopeSeparator ?? " ";
  url.searchParams.set("client_id", input.clientId);
  url.searchParams.set("redirect_uri", input.redirectUrl);
  url.searchParams.set("response_type", "code");
  if (input.scopes.length > 0) {
    url.searchParams.set("scope", input.scopes.join(separator));
  }
  url.searchParams.set("state", input.state);
  url.searchParams.set("code_challenge_method", "S256");
  url.searchParams.set("code_challenge", input.codeChallenge);
  if (input.resource) {
    url.searchParams.set("resource", input.resource);
  }
  if (input.extraParams) {
    for (const [k, v] of Object.entries(input.extraParams)) {
      url.searchParams.set(k, v);
    }
  }
  return url.toString();
};
var providerAuthorizeExtras = (authorizationUrl) => {
  try {
    const host = new URL(authorizationUrl).host.toLowerCase();
    if (host === "accounts.google.com") {
      return { access_type: "offline", prompt: "consent" };
    }
  } catch {
  }
  return {};
};
var isOAuth2Error = Predicate.isTagged("OAuth2Error");
var responseFromOAuthErrorCause = (cause) => {
  if (cause instanceof Response) return cause;
  if (typeof cause !== "object" || cause === null) return void 0;
  const envelope = cause;
  if (envelope.response instanceof Response) return envelope.response;
  if (envelope.cause instanceof Response) return envelope.cause;
  return void 0;
};
var redactTokenEndpointBody = (body) => body.replaceAll(
  /("(?:access_token|refresh_token|id_token|client_secret)"\s*:\s*")[^"]*(")/gi,
  "$1[redacted]$2"
).replaceAll(
  /((?:access_token|refresh_token|id_token|client_secret|code)=)[^&\s]*/gi,
  "$1[redacted]"
);
var tokenEndpointHttpSummary = async (response) => {
  const status = `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`;
  const contentType = response.headers.get("content-type");
  const url = response.url ? ` from ${response.url}` : "";
  const parts = [`${status}${url}`];
  if (contentType) parts.push(`content-type ${contentType}`);
  const preview = await bodyPreviewFromResponse(response);
  if (preview) parts.push(`body: ${preview}`);
  return parts.join("; ");
};
var bodyPreviewFromResponse = async (response) => {
  const text = await Promise.resolve().then(() => response.clone().text()).then(
    (value) => value.trim(),
    () => ""
  );
  if (!text) return void 0;
  const redacted = redactTokenEndpointBody(text.replaceAll(/\s+/g, " "));
  return redacted.length > 500 ? `${redacted.slice(0, 500)}...` : redacted;
};
var toOAuth2Error = (cause) => {
  if (isOAuth2Error(cause)) return cause;
  if (typeof cause === "object" && cause !== null) {
    const c = cause;
    const code = typeof c.error === "string" ? c.error : void 0;
    const description = typeof c.error_description === "string" ? c.error_description : typeof c.message === "string" ? c.message : void 0;
    return new OAuth2Error({
      message: `OAuth token exchange failed: ${description ?? code ?? "unknown error"}`,
      error: code,
      cause
    });
  }
  return new OAuth2Error({
    message: "OAuth token exchange failed",
    cause
  });
};
var toOAuth2ErrorWithHttpSummary = (cause) => {
  if (isOAuth2Error(cause)) return Effect.succeed(cause);
  const base = toOAuth2Error(cause);
  const response = responseFromOAuthErrorCause(cause);
  if (!response) return Effect.succeed(base);
  return Effect.promise(() => tokenEndpointHttpSummary(response)).pipe(
    Effect.map(
      (summary) => new OAuth2Error({
        message: `${base.message} (${summary})`,
        error: base.error,
        cause
      })
    )
  );
};
var failOAuth2WithHttpSummary = (cause) => toOAuth2ErrorWithHttpSummary(cause).pipe(Effect.flatMap((error) => Effect.fail(error)));
var DEFAULT_CLIENT_AUTH_METHOD = "body";
var asFromTokenUrl = (tokenUrl, endpointUrlPolicy = {}) => {
  assertSupportedOAuthEndpointUrl(tokenUrl, "Token URL", endpointUrlPolicy);
  const url = new URL(tokenUrl);
  return {
    issuer: `${url.protocol}//${url.host}`,
    token_endpoint: tokenUrl
  };
};
var asFromTokenUrlAndIssuer = (tokenUrl, issuerUrl, options = {}) => {
  const as = asFromTokenUrl(tokenUrl, options.endpointUrlPolicy);
  const withIssuer = issuerUrl ? { ...as, issuer: issuerUrl } : as;
  return options.idTokenSigningAlgValuesSupported ? {
    ...withIssuer,
    id_token_signing_alg_values_supported: [...options.idTokenSigningAlgValuesSupported]
  } : withIssuer;
};
var oauth4webapiRequestOptions = (targetUrl, timeoutMs, endpointUrlPolicy = {}) => {
  const options = {
    signal: AbortSignal.timeout(timeoutMs ?? OAUTH2_DEFAULT_TIMEOUT_MS)
  };
  if (isLoopbackHttpUrl(targetUrl) || URL.canParse(targetUrl) && new URL(targetUrl).protocol === "http:" && endpointUrlPolicy.allowHttp === true) {
    options[oauth.allowInsecureRequests] = true;
  }
  return options;
};
var pickClientAuth = (clientSecret, method) => {
  if (!clientSecret) return oauth.None();
  return method === "basic" ? oauth.ClientSecretBasic(clientSecret) : oauth.ClientSecretPost(clientSecret);
};
var tokenResponseFrom = (r) => ({
  access_token: r.access_token,
  token_type: r.token_type,
  refresh_token: r.refresh_token,
  expires_in: typeof r.expires_in === "number" ? r.expires_in : void 0,
  scope: r.scope
});
var stripIdToken = async (response) => {
  const body = await response.clone().json().then(
    (value) => value,
    () => null
  );
  if (!body || typeof body !== "object" || !("id_token" in body)) {
    return response;
  }
  const { id_token: _ignored, ...rest } = body;
  return new Response(JSON.stringify(rest), {
    status: response.status,
    statusText: response.statusText,
    headers: response.headers
  });
};
var processTokenEndpointResponse = async (as, client, response) => tokenResponseFrom(
  await oauth.processGenericTokenEndpointResponse(as, client, await stripIdToken(response))
);
var exchangeAuthorizationCode = (input) => Effect.tryPromise({
  try: async () => {
    const as = asFromTokenUrlAndIssuer(input.tokenUrl, input.issuerUrl, {
      idTokenSigningAlgValuesSupported: input.idTokenSigningAlgValuesSupported,
      endpointUrlPolicy: input.endpointUrlPolicy
    });
    const client = { client_id: input.clientId };
    const clientAuth = pickClientAuth(
      input.clientSecret,
      input.clientAuth ?? DEFAULT_CLIENT_AUTH_METHOD
    );
    const params = new URLSearchParams({
      code: input.code,
      redirect_uri: input.redirectUrl,
      code_verifier: input.codeVerifier
    });
    if (input.resource) {
      params.set("resource", input.resource);
    }
    const response = await oauth.genericTokenEndpointRequest(
      as,
      client,
      clientAuth,
      "authorization_code",
      params,
      oauth4webapiRequestOptions(input.tokenUrl, input.timeoutMs, input.endpointUrlPolicy)
    );
    return await processTokenEndpointResponse(as, client, response);
  },
  catch: (cause) => cause
}).pipe(Effect.catch(failOAuth2WithHttpSummary));
var exchangeClientCredentials = (input) => Effect.tryPromise({
  try: async () => {
    const as = asFromTokenUrl(input.tokenUrl, input.endpointUrlPolicy);
    const client = { client_id: input.clientId };
    const clientAuth = pickClientAuth(
      input.clientSecret,
      input.clientAuth ?? DEFAULT_CLIENT_AUTH_METHOD
    );
    const params = new URLSearchParams();
    if (input.scopes && input.scopes.length > 0) {
      params.set("scope", input.scopes.join(input.scopeSeparator ?? " "));
    }
    if (input.resource) {
      params.set("resource", input.resource);
    }
    const response = await oauth.clientCredentialsGrantRequest(
      as,
      client,
      clientAuth,
      params,
      oauth4webapiRequestOptions(input.tokenUrl, input.timeoutMs, input.endpointUrlPolicy)
    );
    const result = await oauth.processClientCredentialsResponse(as, client, response);
    return tokenResponseFrom(result);
  },
  catch: (cause) => cause
}).pipe(Effect.catch(failOAuth2WithHttpSummary));
var refreshAccessToken = (input) => Effect.tryPromise({
  try: async () => {
    const as = asFromTokenUrlAndIssuer(input.tokenUrl, input.issuerUrl, {
      idTokenSigningAlgValuesSupported: input.idTokenSigningAlgValuesSupported,
      endpointUrlPolicy: input.endpointUrlPolicy
    });
    const client = { client_id: input.clientId };
    const clientAuth = pickClientAuth(
      input.clientSecret,
      input.clientAuth ?? DEFAULT_CLIENT_AUTH_METHOD
    );
    const extraParams = new URLSearchParams();
    if (input.scopes && input.scopes.length > 0) {
      extraParams.set("scope", input.scopes.join(input.scopeSeparator ?? " "));
    }
    if (input.resource) {
      extraParams.set("resource", input.resource);
    }
    const additionalParameters = Array.from(extraParams.keys()).length > 0 ? extraParams : void 0;
    const response = await oauth.refreshTokenGrantRequest(
      as,
      client,
      clientAuth,
      input.refreshToken,
      {
        ...oauth4webapiRequestOptions(input.tokenUrl, input.timeoutMs, input.endpointUrlPolicy),
        additionalParameters
      }
    );
    const result = await oauth.processRefreshTokenResponse(
      as,
      client,
      await stripIdToken(response)
    );
    return tokenResponseFrom(result);
  },
  catch: (cause) => cause
}).pipe(Effect.catch(failOAuth2WithHttpSummary));
var shouldRefreshToken = (input) => {
  if (input.expiresAt === null) return false;
  const now = input.now ?? Date.now();
  const skew = input.skewMs ?? OAUTH2_REFRESH_SKEW_MS;
  return input.expiresAt <= now + skew;
};

export {
  OAUTH2_DEFAULT_TIMEOUT_MS,
  assertSupportedOAuthEndpointUrl,
  createPkceCodeVerifier,
  createPkceCodeChallenge,
  createOAuthState,
  buildAuthorizationUrl,
  providerAuthorizeExtras,
  exchangeAuthorizationCode,
  exchangeClientCredentials,
  refreshAccessToken,
  shouldRefreshToken
};
//# sourceMappingURL=chunk-EIHWBY6T.js.map