UNPKG

@urql/exchange-auth

Version:

An exchange for managing authentication and token refresh in urql

177 lines (169 loc) 7.26 kB
Object.defineProperty(exports, '__esModule', { value: true }); var wonka = require('wonka'); var core = require('@urql/core'); /** Utilities to use while refreshing authentication tokens. */ /** Configuration for the `authExchange` returned by the initializer function you write. */ var addAuthAttemptToOperation = (operation, authAttempt) => core.makeOperation(operation.kind, operation, { ...operation.context, authAttempt }); /** Creates an `Exchange` handling control flow for authentication. * * @param init - An initializer function that returns an {@link AuthConfig} wrapped in a `Promise`. * @returns the created authentication {@link Exchange}. * * @remarks * The `authExchange` is used to create an exchange handling authentication and * the control flow of refresh authentication. * * You must pass an initializer function, which receives {@link AuthUtilities} and * must return an {@link AuthConfig} wrapped in a `Promise`. * When this exchange is used in your `Client`, it will first call your initializer * function, which gives you an opportunity to get your authentication state, e.g. * from local storage. * * You may then choose to validate this authentication state and update it, and must * then return an {@link AuthConfig}. * * This configuration defines how you add authentication state to {@link Operation | Operations}, * when your authentication state expires, when an {@link OperationResult} has errored * with an authentication error, and how to refresh your authentication state. * * @example * ```ts * authExchange(async (utils) => { * let token = localStorage.getItem('token'); * let refreshToken = localStorage.getItem('refreshToken'); * return { * addAuthToOperation(operation) { * return utils.appendHeaders(operation, { * Authorization: `Bearer ${token}`, * }); * }, * didAuthError(error) { * return error.graphQLErrors.some(e => e.extensions?.code === 'FORBIDDEN'); * }, * async refreshAuth() { * const result = await utils.mutate(REFRESH, { token }); * if (result.data?.refreshLogin) { * token = result.data.refreshLogin.token; * refreshToken = result.data.refreshLogin.refreshToken; * localStorage.setItem('token', token); * localStorage.setItem('refreshToken', refreshToken); * } * }, * }; * }); * ``` */ function authExchange(init) { return ({ client, forward }) => { var bypassQueue = new Set(); var retries = wonka.makeSubject(); var errors = wonka.makeSubject(); var retryQueue = new Map(); function flushQueue() { authPromise = undefined; var queue = retryQueue; retryQueue = new Map(); queue.forEach(retries.next); } function errorQueue(error) { authPromise = undefined; var queue = retryQueue; retryQueue = new Map(); queue.forEach(operation => { errors.next(core.makeErrorResult(operation, error)); }); } var authPromise; var config = null; return operations$ => { function initAuth() { authPromise = Promise.resolve().then(() => init({ mutate(query, variables, context) { var baseOperation = client.createRequestOperation('mutation', core.createRequest(query, variables), context); return wonka.toPromise(wonka.take(1)(wonka.filter(result => result.operation.key === baseOperation.key && baseOperation.context._instance === result.operation.context._instance)(wonka.onStart(() => { var operation = addAuthToOperation(baseOperation); bypassQueue.add(operation.context._instance); retries.next(operation); })(result$)))); }, appendHeaders(operation, headers) { var fetchOptions = typeof operation.context.fetchOptions === 'function' ? operation.context.fetchOptions() : operation.context.fetchOptions || {}; return core.makeOperation(operation.kind, operation, { ...operation.context, fetchOptions: { ...fetchOptions, headers: { ...fetchOptions.headers, ...headers } } }); } })).then(_config => { if (_config) config = _config; flushQueue(); }).catch(error => { if (process.env.NODE_ENV !== 'production') { console.warn('authExchange()’s initialization function has failed, which is unexpected.\n' + 'If your initialization function is expected to throw/reject, catch this error and handle it explicitly.\n' + 'Unless this error is handled it’ll be passed onto any `OperationResult` instantly and authExchange() will block further operations and retry.', error); } errorQueue(error); }); } initAuth(); function refreshAuth(operation) { // add to retry queue to try again later retryQueue.set(operation.key, addAuthAttemptToOperation(operation, true)); // check that another operation isn't already doing refresh if (config && !authPromise) { authPromise = config.refreshAuth().then(flushQueue).catch(errorQueue); } } function willAuthError(operation) { return !operation.context.authAttempt && config && config.willAuthError && config.willAuthError(operation); } function didAuthError(result) { return config && config.didAuthError && config.didAuthError(result.error, result.operation); } function addAuthToOperation(operation) { return config ? config.addAuthToOperation(operation) : operation; } var opsWithAuth$ = wonka.filter(Boolean)(wonka.map(operation => { if (operation.kind === 'teardown') { retryQueue.delete(operation.key); return operation; } else if (operation.context._instance && bypassQueue.has(operation.context._instance)) { return operation; } else if (operation.context.authAttempt) { return addAuthToOperation(operation); } else if (authPromise || !config) { if (!authPromise) initAuth(); if (!retryQueue.has(operation.key)) retryQueue.set(operation.key, addAuthAttemptToOperation(operation, false)); return null; } else if (willAuthError(operation)) { refreshAuth(operation); return null; } return addAuthToOperation(addAuthAttemptToOperation(operation, false)); })(wonka.merge([retries.source, operations$]))); var result$ = forward(opsWithAuth$); return wonka.merge([errors.source, wonka.filter(result => { if (!bypassQueue.has(result.operation.context._instance) && result.error && didAuthError(result) && !result.operation.context.authAttempt) { refreshAuth(result.operation); return false; } if (bypassQueue.has(result.operation.context._instance)) { bypassQueue.delete(result.operation.context._instance); } return true; })(result$)]); }; }; } exports.authExchange = authExchange; //# sourceMappingURL=urql-exchange-auth.js.map