@urql/exchange-auth
Version:
An exchange for managing authentication and token refresh in urql
177 lines (169 loc) • 7.26 kB
JavaScript
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