// src/datasource.ts
import {
  RemoteGraphQLDataSource
} from "@apollo/gateway";
import { HeaderMap } from "@apollo/server";

// src/fingerprint.ts
import { randomBytes } from "crypto";
import { createHash } from "crypto";
var generateFingerprint = () => {
  const length = 32;
  const buffer = randomBytes(Math.ceil(length / 2));
  return buffer.toString("hex").slice(0, length);
};
var hashFingerprint = (fingerprint) => {
  const hash = createHash("sha256");
  hash.update(fingerprint);
  return hash.digest("hex");
};
var validateFingerprint = (fingerprint, hashedFingerprint) => hashFingerprint(fingerprint) === hashedFingerprint;

// src/token.ts
import isEqual from "lodash-es/isEqual";
var FederatedToken = class {
  tokens;
  refreshTokens;
  values;
  _accessTokenModified;
  _refreshTokenModified;
  constructor() {
    this.tokens = {};
    this.refreshTokens = {};
    this.values = {};
    this._accessTokenModified = false;
    this._refreshTokenModified = false;
  }
  setAccessToken(name, token) {
    this.tokens[name] = token;
    this._accessTokenModified = true;
  }
  setRefreshToken(name, token) {
    this.refreshTokens[name] = token;
    this._refreshTokenModified = true;
  }
  isAccessTokenModified() {
    return this._accessTokenModified;
  }
  isRefreshTokenModified() {
    return this._refreshTokenModified;
  }
  loadAccessToken(at, trackModified = false) {
    const token = JSON.parse(Buffer.from(at, "base64").toString("ascii"));
    if (token.refreshTokens) {
      throw new Error("Refresh tokens are not allowed in the Access Token");
    }
    for (const k in token.tokens) {
      if (trackModified && !isEqual(this.tokens[k], token.tokens[k])) {
        this._accessTokenModified = true;
      }
      this.tokens[k] = token.tokens[k];
    }
    this.tokens = token.tokens;
    for (const k in token) {
      if (k !== "tokens") {
        this.values[k] = token[k];
      }
    }
  }
  dumpAccessToken() {
    const data = {
      tokens: this.tokens,
      ...this.values
    };
    if (!data) {
      return;
    }
    return Buffer.from(JSON.stringify(data)).toString("base64");
  }
  loadRefreshToken(value, trackModified = false) {
    const refreshTokens = JSON.parse(
      Buffer.from(value, "base64").toString("ascii")
    );
    for (const k in refreshTokens) {
      if (trackModified && !isEqual(this.refreshTokens[k], refreshTokens[k])) {
        this._refreshTokenModified = true;
      }
      this.refreshTokens[k] = refreshTokens[k];
    }
  }
  dumpRefreshToken() {
    if (!this.refreshTokens) {
      return;
    }
    return Buffer.from(JSON.stringify(this.refreshTokens)).toString("base64");
  }
};

// src/jwt.ts
var TokenExpiredError = class extends Error {
};
var TokenInvalidError = class extends Error {
};
var PublicFederatedToken = class extends FederatedToken {
  async createAccessJWT(signer) {
    const values = Object.values(this.tokens);
    const sorted = values.sort((a, b) => a.exp - b.exp);
    const exp = sorted[0].exp;
    const fingerprint = generateFingerprint();
    const payload = {
      exp,
      jwe: await signer.encryptObject(this.tokens),
      ...this.values,
      _fingerprint: hashFingerprint(fingerprint)
    };
    const token = await signer.signJWT(payload, exp);
    return {
      accessToken: token,
      fingerprint
    };
  }
  async loadAccessJWT(signer, value, fingerprint) {
    const result = await signer.verifyJWT(value);
    if (!result) {
      throw new Error("Invalid JWT");
    }
    const payload = result.payload;
    if (!payload) {
      throw new TokenInvalidError("Invalid JWT");
    }
    const unixTime = Math.floor(Date.now() / 1e3);
    if (!payload.exp || payload.exp < unixTime) {
      throw new TokenExpiredError("JWT expired");
    }
    if (fingerprint && !validateFingerprint(fingerprint, payload._fingerprint)) {
      throw new TokenInvalidError("Invalid fingerprint");
    }
    this.tokens = await signer.decryptObject(payload.jwe);
    const knownKeys = ["jwe", "iat", "exp", "aud", "iss", "_fingerprint"];
    for (const k in payload) {
      if (!knownKeys.includes(k)) {
        this.values[k] = payload[k];
      }
    }
  }
  // createRefreshJWT encrypts the refresh token and return a JWT. The token is
  // valid for 90 days.
  async createRefreshJWT(signer) {
    const payload = this.refreshTokens;
    const exp = Math.floor(Date.now() / 1e3) + 60 * 60 * 24 * 90;
    return signer.encryptJWT(payload, exp);
  }
  async loadRefreshJWT(signer, value) {
    const result = await signer.decryptJWT(value);
    if (!result) {
      throw new Error("Invalid JWT");
    }
    const payload = result.payload;
    const knownKeys = ["jwe", "iat", "exp", "aud", "iss"];
    for (const k in payload) {
      if (!knownKeys.includes(k)) {
        this.refreshTokens[k] = payload[k];
      }
    }
  }
};

// src/datasource.ts
var FederatedGraphQLDataSource = class extends RemoteGraphQLDataSource {
  name;
  constructor({ name, url }) {
    super({ url });
    this.name = name;
  }
  willSendRequest(options) {
    const { request, context } = options;
    const headers = request.http?.headers ?? new HeaderMap();
    const token = context.federatedToken.dumpAccessToken();
    if (token) {
      headers.set("x-access-token", token);
    }
    const refreshToken = context.federatedToken.dumpRefreshToken();
    if (refreshToken) {
      headers.set("x-refresh-token", refreshToken);
    }
  }
  async didReceiveResponse({
    response,
    context
  }) {
    if (!response.http?.headers) {
      return response;
    }
    const headers = response.http?.headers;
    const token = headers.get("x-access-token");
    if (token) {
      if (!context.federatedToken) {
        context.federatedToken = new PublicFederatedToken();
      }
      context.federatedToken.loadAccessToken(token, true);
    }
    const refreshToken = headers.get("x-refresh-token");
    if (refreshToken) {
      if (!context.federatedToken) {
        context.federatedToken = new PublicFederatedToken();
      }
      context.federatedToken.loadRefreshToken(refreshToken, true);
    }
    return response;
  }
};

// src/gateway.ts
import { GraphQLError } from "graphql";
var GatewayAuthPlugin = class {
  signer;
  tokenSource;
  constructor(options) {
    this.signer = options.signer;
    this.tokenSource = options.source;
  }
  async requestDidStart(requestContext) {
    return this;
  }
  async didResolveOperation(requestContext) {
    const { contextValue } = requestContext;
    const request = contextValue.req;
    const accessToken = this.tokenSource.getAccessToken(request);
    const refreshToken = this.tokenSource.getRefreshToken(request);
    const fingerprint = this.tokenSource.getFingerprint(request);
    if (!accessToken && !refreshToken) {
      return;
    }
    if (!contextValue.federatedToken) {
      contextValue.federatedToken = new PublicFederatedToken();
    }
    const token = contextValue.federatedToken;
    if (accessToken && !refreshToken) {
      try {
        await token.loadAccessJWT(this.signer, accessToken, fingerprint);
      } catch (e) {
        if (e instanceof TokenExpiredError) {
          throw new GraphQLError("Your token has expired.", {
            extensions: {
              code: "UNAUTHENTICATED",
              http: {
                statusCode: 401
              }
            }
          });
        } else {
          throw new GraphQLError("Your token is invalid.", {
            extensions: {
              code: "INVALID_TOKEN",
              http: {
                statusCode: 400
              }
            }
          });
        }
      }
    }
    if (refreshToken) {
      await token.loadRefreshJWT(this.signer, refreshToken);
    }
  }
  async willSendResponse(requestContext) {
    const { contextValue } = requestContext;
    const token = contextValue?.federatedToken;
    const response = contextValue.res;
    if (token?.isAccessTokenModified()) {
      const { accessToken, fingerprint } = await token.createAccessJWT(
        this.signer
      );
      if (accessToken && fingerprint) {
        this.tokenSource.setAccessToken(response, accessToken);
        this.tokenSource.setFingerprint(response, fingerprint);
      }
    }
    if (token?.isRefreshTokenModified()) {
      const refreshToken = await token.createRefreshJWT(this.signer);
      this.tokenSource.setRefreshToken(response, refreshToken);
    }
  }
};

// src/plugin.ts
var FederatedAuthPlugin = class {
  async requestDidStart(requestContext) {
    const { contextValue, request } = requestContext;
    if (!contextValue.federatedToken) {
      contextValue.federatedToken = new FederatedToken();
    }
    const at = request.http?.headers.get("X-Access-Token");
    if (at) {
      contextValue.federatedToken.loadAccessToken(at);
    }
    const rt = request.http?.headers.get("X-Refresh-Token");
    if (rt) {
      contextValue.federatedToken.loadRefreshToken(rt);
    }
    return this;
  }
  async willSendResponse(requestContext) {
    const { contextValue, response } = requestContext;
    if (!contextValue.federatedToken) {
      return;
    }
    const t = contextValue.federatedToken;
    if (t.isAccessTokenModified()) {
      const val = t.dumpAccessToken();
      if (val) {
        response.http.headers.set("X-Access-Token", val);
      }
    }
    if (t.isRefreshTokenModified()) {
      const val = t.dumpRefreshToken();
      if (val) {
        response.http.headers.set("X-Refresh-Token", val);
      }
    }
  }
};

// src/sign.ts
import * as jose from "jose";
var ConfigurationError = class extends Error {
};
var TokenSigner = class {
  constructor(config) {
    this.config = config;
    if (!config.audience) {
      throw new ConfigurationError("Missing audience");
    }
    if (!config.issuer) {
      throw new ConfigurationError("Missing issuer");
    }
    this._encryptKeys = config.encryptKeys;
    this._signKeys = config.signKeys;
  }
  _encryptKeys;
  _signKeys;
  // Encrypt the embedded token (JWE) in the JWT
  async encryptObject(value) {
    const { id, key } = await this._encryptKeys.getActiveKey();
    const buf = new TextEncoder().encode(JSON.stringify(value));
    const data = await new jose.CompactEncrypt(buf).setProtectedHeader({ alg: "dir", enc: "A256GCM", kid: id }).encrypt(key);
    return data;
  }
  // Decrypt the embedded token (JWE) in the JWT
  async decryptObject(value) {
    if (typeof value !== "string") {
      throw new Error("Invalid value type");
    }
    const result = await jose.compactDecrypt(
      value,
      this._encryptKeys.getKeyFunction.bind(this._encryptKeys),
      {
        keyManagementAlgorithms: ["dir"],
        contentEncryptionAlgorithms: ["A256GCM"]
      }
    );
    return JSON.parse(result.plaintext.toString());
  }
  async signJWT(payload, exp) {
    const { id, key } = await this._signKeys.getActiveKey();
    return await new jose.SignJWT(payload).setExpirationTime(exp).setIssuedAt().setProtectedHeader({ alg: "HS256", kid: id }).setAudience(this.config.audience).setIssuer(this.config.issuer).sign(key);
  }
  async verifyJWT(value) {
    return await jose.jwtVerify(
      value,
      this._signKeys.getKeyFunction.bind(this._signKeys),
      {
        algorithms: ["HS256"],
        audience: this.config.audience,
        issuer: this.config.issuer
      }
    );
  }
  // For refresh token, encrypt the token (JWE)
  async encryptJWT(payload, exp) {
    const { id, key } = await this._encryptKeys.getActiveKey();
    const data = await new jose.EncryptJWT(payload).setProtectedHeader({ alg: "dir", enc: "A128CBC-HS256", kid: id }).setIssuedAt().setIssuer(this.config.issuer).setAudience(this.config.audience).setExpirationTime(exp).encrypt(key);
    return data;
  }
  async decryptJWT(jwt) {
    return await jose.jwtDecrypt(
      jwt,
      this._encryptKeys.getKeyFunction.bind(this._encryptKeys),
      {
        audience: this.config.audience,
        issuer: this.config.issuer
      }
    );
  }
};
var KeyManager = class {
  keys;
  constructor(keys) {
    if (keys.length === 0) {
      throw new ConfigurationError("Missing keys");
    }
    this.keys = keys;
  }
  // getActiveKey returns a key to sign the JWT. It always returns the first key
  // in the list.
  async getActiveKey() {
    return this.keys[0];
  }
  // getKeyFunction is passed to the jose library to retrieve the key to verify
  // or decrypt the JWE.
  getKeyFunction(header, input) {
    const kid = header?.kid;
    if (!kid) {
      throw new ConfigurationError("Missing kid");
    }
    const key = this.keys.find((k) => k.id === kid)?.key;
    if (!key) {
      throw new ConfigurationError("Missing key");
    }
    return key;
  }
};

// src/tokensource/composite.ts
var CompositeTokenSource = class {
  sources;
  constructor(sources) {
    this.sources = sources;
  }
  getAccessToken(request) {
    for (const source of this.sources) {
      const token = source.getAccessToken(request);
      if (token) {
        return token;
      }
    }
    return "";
  }
  getRefreshToken(request) {
    for (const source of this.sources) {
      const token = source.getRefreshToken(request);
      if (token) {
        return token;
      }
    }
    return "";
  }
  getFingerprint(request) {
    for (const source of this.sources) {
      const token = source.getFingerprint(request);
      if (token) {
        return token;
      }
    }
    return "";
  }
  setAccessToken(response, token) {
    for (const source of this.sources) {
      source.setAccessToken(response, token);
    }
  }
  setRefreshToken(response, token) {
    for (const source of this.sources) {
      source.setRefreshToken(response, token);
    }
  }
  setFingerprint(response, fingerprint) {
    for (const source of this.sources) {
      source.setFingerprint(response, fingerprint);
    }
  }
};

// src/tokensource/cookies.ts
var CookieTokenSource = class {
  constructor(options) {
    this.options = options;
  }
  cookieNames = {
    accessToken: "authToken",
    accessTokenHash: "authTokenHash",
    refreshToken: "authRefreshToken",
    refreshTokenExist: "authRefreshTokenExist"
  };
  _getCookieName(name) {
    if (this.options.secure)
      return `__Host-${name}`;
    return `${name}`;
  }
  _getCookie(request, name) {
    return request.cookies[this._getCookieName(name)];
  }
  _setCookie(response, name, value, options) {
    response.cookie(this._getCookieName(name), value, options);
  }
  getAccessToken(request) {
    return this._getCookie(request, this.cookieNames.accessToken);
  }
  getRefreshToken(request) {
    return this._getCookie(request, this.cookieNames.refreshToken);
  }
  getFingerprint(request) {
    return this._getCookie(request, this.cookieNames.accessTokenHash);
  }
  setAccessToken(response, token) {
    this._setCookie(response, this.cookieNames.accessToken, token, {
      httpOnly: false,
      secure: this.options.secure,
      sameSite: this.options.sameSite
    });
  }
  setRefreshToken(response, token) {
    const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24 * 365);
    this._setCookie(response, this.cookieNames.refreshToken, token, {
      httpOnly: true,
      path: this.options.refreshTokenPath,
      secure: this.options.secure,
      sameSite: this.options.sameSite,
      expires: expiresAt
    });
    this._setCookie(response, this.cookieNames.refreshTokenExist, "1", {
      httpOnly: false,
      secure: this.options.secure,
      sameSite: this.options.sameSite,
      expires: expiresAt
    });
  }
  setFingerprint(response, fingerprint) {
    this._setCookie(response, this.cookieNames.accessTokenHash, fingerprint, {
      httpOnly: true,
      secure: this.options.secure,
      sameSite: this.options.sameSite
    });
  }
};

// src/tokensource/headers.ts
var HeaderTokenSource = class {
  headerNames = {
    accessToken: "x-access-token",
    refreshToken: "x-refresh-token"
  };
  getAccessToken(request) {
    const authHeader = request.headers[this.headerNames.accessToken];
    return authHeader?.replace("Bearer ", "");
  }
  getRefreshToken(request) {
    return request.headers[this.headerNames.refreshToken];
  }
  getFingerprint(request) {
    return "";
  }
  setAccessToken(response, token) {
    response.set(this.headerNames.accessToken, token);
  }
  setRefreshToken(response, token) {
    response.set(this.headerNames.refreshToken, token);
  }
  setFingerprint(response, fingerprint) {
  }
};
export {
  CompositeTokenSource,
  CookieTokenSource,
  FederatedAuthPlugin,
  FederatedGraphQLDataSource,
  FederatedToken,
  GatewayAuthPlugin,
  HeaderTokenSource,
  KeyManager,
  PublicFederatedToken,
  TokenSigner
};
//# sourceMappingURL=index.mjs.map