{"version":3,"file":"urql-exchange-auth.min.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","authPromise","bypassQueue","Set","retries","makeSubject","errors","retryQueue","Map","flushQueue","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","refreshAuth","set","opsWithAuth$","Boolean","map","delete","has","willAuthError","merge","source","didAuthError"],"mappings":"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"}