{"version":3,"file":"urql-exchange-auth.mjs","sources":["../src/authExchange.ts"],"sourcesContent":["import type { Source } from 'wonka';\nimport {\n  pipe,\n  map,\n  filter,\n  onStart,\n  take,\n  makeSubject,\n  toPromise,\n  merge,\n} from 'wonka';\n\nimport type {\n  Operation,\n  OperationContext,\n  OperationResult,\n  CombinedError,\n  Exchange,\n  DocumentInput,\n  AnyVariables,\n  OperationInstance,\n} from '@urql/core';\nimport { createRequest, makeOperation, makeErrorResult } from '@urql/core';\n\n/** Utilities to use while refreshing authentication tokens. */\nexport interface AuthUtilities {\n  /** Sends a mutation to your GraphQL API, bypassing earlier exchanges and authentication.\n   *\n   * @param query - a GraphQL document containing the mutation operation that will be executed.\n   * @param variables - the variables used to execute the operation.\n   * @param context - {@link OperationContext} options that'll be used in future exchanges.\n   * @returns A `Promise` of an {@link OperationResult} for the GraphQL mutation.\n   *\n   * @remarks\n   * The `mutation()` utility method is useful when your authentication requires you to make a GraphQL mutation\n   * request to update your authentication tokens. In these cases, you likely wish to bypass prior exchanges and\n   * the authentication in the `authExchange` itself.\n   *\n   * This method bypasses the usual mutation flow of the `Client` and instead issues the mutation as directly\n   * as possible. This also means that it doesn’t carry your `Client`'s default {@link OperationContext}\n   * options, so you may have to pass them again, if needed.\n   */\n  mutate<Data = any, Variables extends AnyVariables = AnyVariables>(\n    query: DocumentInput<Data, Variables>,\n    variables: Variables,\n    context?: Partial<OperationContext>\n  ): Promise<OperationResult<Data>>;\n\n  /** Adds additional HTTP headers to an `Operation`.\n   *\n   * @param operation - An {@link Operation} to add headers to.\n   * @param headers - The HTTP headers to add to the `Operation`.\n   * @returns The passed {@link Operation} with the headers added to it.\n   *\n   * @remarks\n   * The `appendHeaders()` utility method is useful to add additional HTTP headers\n   * to an {@link Operation}. It’s a simple convenience function that takes\n   * `operation.context.fetchOptions` into account, since adding headers for\n   * authentication is common.\n   */\n  appendHeaders(\n    operation: Operation,\n    headers: Record<string, string>\n  ): Operation;\n}\n\n/** Configuration for the `authExchange` returned by the initializer function you write. */\nexport interface AuthConfig {\n  /** Called for every operation to add authentication data to your operation.\n   *\n   * @param operation - An {@link Operation} that needs authentication tokens added.\n   * @returns a new {@link Operation} with added authentication tokens.\n   *\n   * @remarks\n   * The {@link authExchange} will call this function you provide and expects that you\n   * add your authentication tokens to your operation here, on the {@link Operation}\n   * that is returned.\n   *\n   * Hint: You likely want to modify your `fetchOptions.headers` here, for instance to\n   * add an `Authorization` header.\n   */\n  addAuthToOperation(operation: Operation): Operation;\n\n  /** Called before an operation is forwaded onwards to make a request.\n   *\n   * @param operation - An {@link Operation} that needs authentication tokens added.\n   * @returns a boolean, if true, authentication must be refreshed.\n   *\n   * @remarks\n   * The {@link authExchange} will call this function before an {@link Operation} is\n   * forwarded onwards to your following exchanges.\n   *\n   * When this function returns `true`, the `authExchange` will call\n   * {@link AuthConfig.refreshAuth} before forwarding more operations\n   * to prompt you to update your authentication tokens.\n   *\n   * Hint: If you define this function, you can use it to check whether your authentication\n   * tokens have expired.\n   */\n  willAuthError?(operation: Operation): boolean;\n\n  /** Called after receiving an operation result to check whether it has failed with an authentication error.\n   *\n   * @param error - A {@link CombinedError} that a result has come back with.\n   * @param operation - The {@link Operation} of that has failed.\n   * @returns a boolean, if true, authentication must be refreshed.\n   *\n   * @remarks\n   * The {@link authExchange} will call this function if it sees an {@link OperationResult}\n   * with a {@link CombinedError} on it, implying that it may have failed due to an authentication\n   * error.\n   *\n   * When this function returns `true`, the `authExchange` will call\n   * {@link AuthConfig.refreshAuth} before forwarding more operations\n   * to prompt you to update your authentication tokens.\n   * Afterwards, this operation will be retried once.\n   *\n   * Hint: You should define a function that detects your API’s authentication\n   * errors, e.g. using `result.extensions`.\n   */\n  didAuthError(error: CombinedError, operation: Operation): boolean;\n\n  /** Called to refresh the authentication state.\n   *\n   * @remarks\n   * The {@link authExchange} will call this function if either {@link AuthConfig.willAuthError}\n   * or {@link AuthConfig.didAuthError} have returned `true` prior, which indicates that the\n   * authentication state you hold has expired or is out-of-date.\n   *\n   * When this function is called, you should refresh your authentication state.\n   * For instance, if you have a refresh token and an access token, you should rotate\n   * these tokens with your API by sending the refresh token.\n   *\n   * Hint: You can use the {@link fetch} API here, or use {@link AuthUtilities.mutate}\n   * if your API requires a GraphQL mutation to refresh your authentication state.\n   */\n  refreshAuth(): Promise<void>;\n}\n\nconst addAuthAttemptToOperation = (\n  operation: Operation,\n  authAttempt: boolean\n) =>\n  makeOperation(operation.kind, operation, {\n    ...operation.context,\n    authAttempt,\n  });\n\n/** Creates an `Exchange` handling control flow for authentication.\n *\n * @param init - An initializer function that returns an {@link AuthConfig} wrapped in a `Promise`.\n * @returns the created authentication {@link Exchange}.\n *\n * @remarks\n * The `authExchange` is used to create an exchange handling authentication and\n * the control flow of refresh authentication.\n *\n * You must pass an initializer function, which receives {@link AuthUtilities} and\n * must return an {@link AuthConfig} wrapped in a `Promise`.\n * When this exchange is used in your `Client`, it will first call your initializer\n * function, which gives you an opportunity to get your authentication state, e.g.\n * from local storage.\n *\n * You may then choose to validate this authentication state and update it, and must\n * then return an {@link AuthConfig}.\n *\n * This configuration defines how you add authentication state to {@link Operation | Operations},\n * when your authentication state expires, when an {@link OperationResult} has errored\n * with an authentication error, and how to refresh your authentication state.\n *\n * @example\n * ```ts\n * authExchange(async (utils) => {\n *   let token = localStorage.getItem('token');\n *   let refreshToken = localStorage.getItem('refreshToken');\n *   return {\n *     addAuthToOperation(operation) {\n *       return utils.appendHeaders(operation, {\n *         Authorization: `Bearer ${token}`,\n *       });\n *     },\n *     didAuthError(error) {\n *       return error.graphQLErrors.some(e => e.extensions?.code === 'FORBIDDEN');\n *     },\n *     async refreshAuth() {\n *       const result = await utils.mutate(REFRESH, { token });\n *       if (result.data?.refreshLogin) {\n *         token = result.data.refreshLogin.token;\n *         refreshToken = result.data.refreshLogin.refreshToken;\n *         localStorage.setItem('token', token);\n *         localStorage.setItem('refreshToken', refreshToken);\n *       }\n *     },\n *   };\n * });\n * ```\n */\nexport function authExchange(\n  init: (utilities: AuthUtilities) => Promise<AuthConfig>\n): Exchange {\n  return ({ client, forward }) => {\n    const bypassQueue = new Set<OperationInstance | undefined>();\n    const retries = makeSubject<Operation>();\n    const errors = makeSubject<OperationResult>();\n\n    let retryQueue = new Map<number, Operation>();\n\n    function flushQueue() {\n      authPromise = undefined;\n      const queue = retryQueue;\n      retryQueue = new Map();\n      queue.forEach(retries.next);\n    }\n\n    function errorQueue(error: Error) {\n      authPromise = undefined;\n      const queue = retryQueue;\n      retryQueue = new Map();\n      queue.forEach(operation => {\n        errors.next(makeErrorResult(operation, error));\n      });\n    }\n\n    let authPromise: Promise<void> | void;\n    let config: AuthConfig | null = null;\n\n    return operations$ => {\n      function initAuth() {\n        authPromise = Promise.resolve()\n          .then(() =>\n            init({\n              mutate<Data = any, Variables extends AnyVariables = AnyVariables>(\n                query: DocumentInput<Data, Variables>,\n                variables: Variables,\n                context?: Partial<OperationContext>\n              ): Promise<OperationResult<Data>> {\n                const baseOperation = client.createRequestOperation(\n                  'mutation',\n                  createRequest(query, variables),\n                  context\n                );\n                return pipe(\n                  result$,\n                  onStart(() => {\n                    const operation = addAuthToOperation(baseOperation);\n                    bypassQueue.add(\n                      operation.context._instance as OperationInstance\n                    );\n                    retries.next(operation);\n                  }),\n                  filter(\n                    result =>\n                      result.operation.key === baseOperation.key &&\n                      baseOperation.context._instance ===\n                        result.operation.context._instance\n                  ),\n                  take(1),\n                  toPromise\n                );\n              },\n              appendHeaders(\n                operation: Operation,\n                headers: Record<string, string>\n              ) {\n                const fetchOptions =\n                  typeof operation.context.fetchOptions === 'function'\n                    ? operation.context.fetchOptions()\n                    : operation.context.fetchOptions || {};\n                return makeOperation(operation.kind, operation, {\n                  ...operation.context,\n                  fetchOptions: {\n                    ...fetchOptions,\n                    headers: {\n                      ...fetchOptions.headers,\n                      ...headers,\n                    },\n                  },\n                });\n              },\n            })\n          )\n          .then((_config: AuthConfig) => {\n            if (_config) config = _config;\n            flushQueue();\n          })\n          .catch((error: Error) => {\n            if (process.env.NODE_ENV !== 'production') {\n              console.warn(\n                'authExchange()’s initialization function has failed, which is unexpected.\\n' +\n                  'If your initialization function is expected to throw/reject, catch this error and handle it explicitly.\\n' +\n                  'Unless this error is handled it’ll be passed onto any `OperationResult` instantly and authExchange() will block further operations and retry.',\n                error\n              );\n            }\n\n            errorQueue(error);\n          });\n      }\n\n      initAuth();\n\n      function refreshAuth(operation: Operation) {\n        // add to retry queue to try again later\n        retryQueue.set(\n          operation.key,\n          addAuthAttemptToOperation(operation, true)\n        );\n\n        // check that another operation isn't already doing refresh\n        if (config && !authPromise) {\n          authPromise = config.refreshAuth().then(flushQueue).catch(errorQueue);\n        }\n      }\n\n      function willAuthError(operation: Operation) {\n        return (\n          !operation.context.authAttempt &&\n          config &&\n          config.willAuthError &&\n          config.willAuthError(operation)\n        );\n      }\n\n      function didAuthError(result: OperationResult) {\n        return (\n          config &&\n          config.didAuthError &&\n          config.didAuthError(result.error!, result.operation)\n        );\n      }\n\n      function addAuthToOperation(operation: Operation) {\n        return config ? config.addAuthToOperation(operation) : operation;\n      }\n\n      const opsWithAuth$ = pipe(\n        merge([retries.source, operations$]),\n        map(operation => {\n          if (operation.kind === 'teardown') {\n            retryQueue.delete(operation.key);\n            return operation;\n          } else if (\n            operation.context._instance &&\n            bypassQueue.has(operation.context._instance)\n          ) {\n            return operation;\n          } else if (operation.context.authAttempt) {\n            return addAuthToOperation(operation);\n          } else if (authPromise || !config) {\n            if (!authPromise) initAuth();\n\n            if (!retryQueue.has(operation.key))\n              retryQueue.set(\n                operation.key,\n                addAuthAttemptToOperation(operation, false)\n              );\n\n            return null;\n          } else if (willAuthError(operation)) {\n            refreshAuth(operation);\n            return null;\n          }\n\n          return addAuthToOperation(\n            addAuthAttemptToOperation(operation, false)\n          );\n        }),\n        filter(Boolean)\n      ) as Source<Operation>;\n\n      const result$ = pipe(opsWithAuth$, forward);\n\n      return merge([\n        errors.source,\n        pipe(\n          result$,\n          filter(result => {\n            if (\n              !bypassQueue.has(result.operation.context._instance) &&\n              result.error &&\n              didAuthError(result) &&\n              !result.operation.context.authAttempt\n            ) {\n              refreshAuth(result.operation);\n              return false;\n            }\n\n            if (bypassQueue.has(result.operation.context._instance)) {\n              bypassQueue.delete(result.operation.context._instance);\n            }\n\n            return true;\n          })\n        ),\n      ]);\n    };\n  };\n}\n"],"names":["addAuthAttemptToOperation","operation","authAttempt","makeOperation","kind","context","authExchange","init","client","forward","bypassQueue","Set","retries","makeSubject","errors","retryQueue","Map","flushQueue","authPromise","undefined","queue","forEach","next","errorQueue","error","makeErrorResult","config","operations$","initAuth","Promise","resolve","then","mutate","query","variables","baseOperation","createRequestOperation","createRequest","toPromise","take","filter","result","key","_instance","onStart","addAuthToOperation","add","result$","appendHeaders","headers","fetchOptions","_config","catch","process","env","NODE_ENV","console","warn","refreshAuth","set","opsWithAuth$","Boolean","map","delete","has","willAuthError","merge","source","didAuthError"],"mappings":"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"}