mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-08 04:36:13 +08:00
feat(core): improve login flow (#15219)
#### PR Dependency Tree * **PR #15219** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added secure, automatic auth session token refresh and request replay for expired-token responses across Android, iOS, and Electron. * Updated sign-in flows to manage sessions without returning tokens to the app layer. * Added “Devices” management UI with sign out per device and sign out all. * Enabled support for both Hashcash and Turnstile captcha providers. * **Bug Fixes** * Improved refresh de-duplication, inflight cancellation/clear behavior, and recovery from corrupted/invalid sessions. * **Tests** * Expanded auth-session, refresh/revoke, and replay coverage (Electron unit tests, Android instrumentation tests, iOS auth date parser tests). * **Chores** * Removed CAPTCHA site key from build-time configuration and adjusted CI test execution. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
AuthTokenBroker,
|
||||
type AuthTokenPair,
|
||||
type AuthTokenResponse,
|
||||
classifyAuthError,
|
||||
} from '@affine/auth';
|
||||
import { app, net, safeStorage } from 'electron';
|
||||
|
||||
import { logger } from '../logger';
|
||||
|
||||
const FILEPATH = path.join(app.getPath('userData'), 'auth-sessions.json');
|
||||
const TEMP_FILEPATH = `${FILEPATH}.tmp`;
|
||||
const INSTALLATION_FILEPATH = path.join(
|
||||
app.getPath('userData'),
|
||||
'installation-id'
|
||||
);
|
||||
const brokers = new Map<string, AuthTokenBroker>();
|
||||
const memoryStore = new Map<string, AuthTokenPair>();
|
||||
let fileMutation = Promise.resolve();
|
||||
let installationId: Promise<string> | undefined;
|
||||
const AUTH_REQUEST_TIMEOUT = 10_000;
|
||||
|
||||
function secureStorageAvailable() {
|
||||
return (
|
||||
safeStorage.isEncryptionAvailable() &&
|
||||
(process.platform !== 'linux' ||
|
||||
safeStorage.getSelectedStorageBackend() !== 'basic_text')
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeEndpoint(endpoint: string) {
|
||||
return new URL(endpoint).origin;
|
||||
}
|
||||
|
||||
async function readStore(): Promise<Record<string, string>> {
|
||||
try {
|
||||
const value = JSON.parse(await fs.readFile(FILEPATH, 'utf8'));
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value
|
||||
: {};
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
logger.error('failed to read auth session store', error);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function mutateFile(mutator: (store: Record<string, string>) => void) {
|
||||
const operation = fileMutation.then(async () => {
|
||||
const store = await readStore();
|
||||
mutator(store);
|
||||
await fs.writeFile(TEMP_FILEPATH, JSON.stringify(store), { mode: 0o600 });
|
||||
await fs.rename(TEMP_FILEPATH, FILEPATH);
|
||||
});
|
||||
fileMutation = operation.catch(() => {});
|
||||
return await operation;
|
||||
}
|
||||
|
||||
function encrypt(pair: AuthTokenPair) {
|
||||
return safeStorage.encryptString(JSON.stringify(pair)).toString('base64');
|
||||
}
|
||||
|
||||
function decrypt(value: string): AuthTokenPair | null {
|
||||
try {
|
||||
return JSON.parse(safeStorage.decryptString(Buffer.from(value, 'base64')));
|
||||
} catch (error) {
|
||||
logger.error('failed to decrypt auth session', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function storage(endpoint: string) {
|
||||
return {
|
||||
async load() {
|
||||
if (!secureStorageAvailable()) {
|
||||
return memoryStore.get(endpoint) ?? null;
|
||||
}
|
||||
const encrypted = (await readStore())[endpoint];
|
||||
if (!encrypted) return null;
|
||||
const pair = decrypt(encrypted);
|
||||
if (!pair) await mutateFile(store => delete store[endpoint]);
|
||||
return pair;
|
||||
},
|
||||
async save(pair: AuthTokenPair) {
|
||||
if (!secureStorageAvailable()) {
|
||||
await mutateFile(store => delete store[endpoint]);
|
||||
memoryStore.set(endpoint, pair);
|
||||
return;
|
||||
}
|
||||
await mutateFile(store => {
|
||||
store[endpoint] = encrypt(pair);
|
||||
});
|
||||
memoryStore.delete(endpoint);
|
||||
},
|
||||
async clear() {
|
||||
memoryStore.delete(endpoint);
|
||||
await mutateFile(store => delete store[endpoint]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function refresh(endpoint: string, refreshToken: string) {
|
||||
const response = await net.fetch(
|
||||
new URL('/api/auth/session/refresh', endpoint).toString(),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-affine-client-kind': 'native',
|
||||
'x-affine-version': BUILD_CONFIG.appVersion,
|
||||
},
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
signal: AbortSignal.timeout(AUTH_REQUEST_TIMEOUT),
|
||||
}
|
||||
);
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw classifyAuthError({
|
||||
code:
|
||||
typeof body === 'object' && body && 'code' in body
|
||||
? String(body.code)
|
||||
: 'AUTH_SESSION_TEMPORARILY_UNAVAILABLE',
|
||||
});
|
||||
}
|
||||
return body as AuthTokenResponse;
|
||||
}
|
||||
|
||||
export function getAuthSessionBroker(endpoint: string) {
|
||||
const normalized = normalizeEndpoint(endpoint);
|
||||
let broker = brokers.get(normalized);
|
||||
if (!broker) {
|
||||
broker = new AuthTokenBroker(storage(normalized), {
|
||||
refresh: (token: string) => refresh(normalized, token),
|
||||
});
|
||||
brokers.set(normalized, broker);
|
||||
}
|
||||
return broker;
|
||||
}
|
||||
|
||||
export function isManagedAuthEndpoint(url: string) {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
|
||||
if (parsed.protocol === 'wss:') parsed.protocol = 'https:';
|
||||
return brokers.has(parsed.origin);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setAuthSession(
|
||||
endpoint: string,
|
||||
response: AuthTokenResponse
|
||||
) {
|
||||
await getAuthSessionBroker(endpoint).set(response);
|
||||
return { persistent: secureStorageAvailable() };
|
||||
}
|
||||
|
||||
export function getInstallationId() {
|
||||
if (installationId) return installationId;
|
||||
const pending = fs
|
||||
.readFile(INSTALLATION_FILEPATH, 'utf8')
|
||||
.then(value => {
|
||||
if (
|
||||
!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
throw new Error('Invalid installation id');
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.catch(async () => {
|
||||
const value = randomUUID();
|
||||
await fs.writeFile(INSTALLATION_FILEPATH, value, { mode: 0o600 });
|
||||
return value;
|
||||
});
|
||||
installationId = pending;
|
||||
void pending.catch(() => {
|
||||
if (installationId === pending) installationId = undefined;
|
||||
});
|
||||
return pending;
|
||||
}
|
||||
|
||||
export async function revokeAuthSession(endpoint: string) {
|
||||
const normalized = normalizeEndpoint(endpoint);
|
||||
await getAuthSessionBroker(normalized).revoke(
|
||||
'sign-out',
|
||||
async (refreshToken: string) => {
|
||||
const response = await net.fetch(
|
||||
new URL('/api/auth/session/revoke', normalized).toString(),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-affine-client-kind': 'native',
|
||||
'x-affine-version': BUILD_CONFIG.appVersion,
|
||||
},
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
signal: AbortSignal.timeout(AUTH_REQUEST_TIMEOUT),
|
||||
}
|
||||
);
|
||||
if (!response.ok) throw new Error('Failed to revoke auth session');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function clearAuthSession(endpoint: string, reason: string) {
|
||||
await getAuthSessionBroker(endpoint).clear(reason);
|
||||
}
|
||||
|
||||
export async function getValidAccessToken(
|
||||
endpoint: string,
|
||||
minValidity = 60_000
|
||||
) {
|
||||
try {
|
||||
return await getAuthSessionBroker(endpoint).getValidAccessToken(
|
||||
minValidity
|
||||
);
|
||||
} catch (error) {
|
||||
const classified = classifyAuthError(error);
|
||||
if (classified.code === 'AUTH_SESSION_EMPTY' || !classified.transient) {
|
||||
return null;
|
||||
}
|
||||
throw classified;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAccessTokenForUrl(url: string, minValidity = 60_000) {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
|
||||
if (parsed.protocol === 'wss:') parsed.protocol = 'https:';
|
||||
return await getValidAccessToken(parsed.origin, minValidity);
|
||||
} catch (error) {
|
||||
if (error instanceof TypeError) return null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshAccessTokenForUrl(url: string) {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
|
||||
if (parsed.protocol === 'wss:') parsed.protocol = 'https:';
|
||||
return (
|
||||
await getAuthSessionBroker(parsed.origin).refresh('access-token-expired')
|
||||
).accessToken;
|
||||
}
|
||||
|
||||
async function authorizedRequest(
|
||||
request: Request,
|
||||
targetUrl: string,
|
||||
accessToken?: string
|
||||
) {
|
||||
const cloned = request.clone();
|
||||
const headers = new Headers(cloned.headers);
|
||||
headers.delete('Authorization');
|
||||
const token = accessToken ?? (await getAccessTokenForUrl(targetUrl));
|
||||
if (token) headers.set('Authorization', `Bearer ${token}`);
|
||||
return new Request(targetUrl, {
|
||||
body:
|
||||
cloned.method === 'GET' || cloned.method === 'HEAD'
|
||||
? undefined
|
||||
: cloned.body,
|
||||
headers,
|
||||
method: cloned.method,
|
||||
redirect: cloned.redirect,
|
||||
signal: cloned.signal,
|
||||
duplex: 'half',
|
||||
});
|
||||
}
|
||||
|
||||
export async function executeAuthSessionRequest(
|
||||
request: Request,
|
||||
targetUrl: string,
|
||||
execute: (request: Request) => Promise<Response>
|
||||
) {
|
||||
const retry = request.clone();
|
||||
const response = await execute(await authorizedRequest(request, targetUrl));
|
||||
if (response.status !== 401) return response;
|
||||
const body = (await response
|
||||
.clone()
|
||||
.json()
|
||||
.catch(() => null)) as { code?: string } | null;
|
||||
if (body?.code !== 'ACCESS_TOKEN_EXPIRED') return response;
|
||||
const token = await refreshAccessTokenForUrl(targetUrl);
|
||||
return await execute(await authorizedRequest(retry, targetUrl, token));
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
import os from 'node:os';
|
||||
|
||||
import type { AuthTokenResponse } from '@affine/auth';
|
||||
import { net, session } from 'electron';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
import {
|
||||
deleteNativeAuthToken,
|
||||
getNativeAuthToken,
|
||||
setNativeAuthToken,
|
||||
} from './native-token';
|
||||
clearAuthSession,
|
||||
getInstallationId,
|
||||
getValidAccessToken,
|
||||
revokeAuthSession,
|
||||
setAuthSession,
|
||||
} from './auth-session';
|
||||
|
||||
export interface SignInResponse {
|
||||
id?: string;
|
||||
@@ -29,10 +34,6 @@ export interface PasswordSignInResponse extends SignInResponse {
|
||||
sessionOnly?: boolean;
|
||||
}
|
||||
|
||||
interface ExchangeResponse {
|
||||
token?: string;
|
||||
}
|
||||
|
||||
const authCookieNames = [
|
||||
'affine_session',
|
||||
'affine_user_id',
|
||||
@@ -88,15 +89,16 @@ async function exchangeSession(endpoint: string, response: SignInResponse) {
|
||||
|
||||
const exchangeResponse = await fetchAuth(
|
||||
endpoint,
|
||||
'/api/auth/native/exchange',
|
||||
{ code: response.exchangeCode }
|
||||
'/api/auth/session/exchange',
|
||||
{
|
||||
code: response.exchangeCode,
|
||||
installationId: await getInstallationId(),
|
||||
platform: 'electron',
|
||||
deviceName: os.hostname(),
|
||||
}
|
||||
);
|
||||
const body = await readJson<ExchangeResponse>(exchangeResponse);
|
||||
if (!body.token) {
|
||||
throw new Error('Missing native auth token.');
|
||||
}
|
||||
|
||||
const persistent = setNativeAuthToken(endpoint, body.token);
|
||||
const body = await readJson<AuthTokenResponse>(exchangeResponse);
|
||||
const { persistent } = await setAuthSession(endpoint, body);
|
||||
await clearAuthCookies(endpoint);
|
||||
return { persistent };
|
||||
}
|
||||
@@ -155,6 +157,13 @@ export const authHandlers = {
|
||||
...(credential.verifyToken
|
||||
? { 'x-captcha-token': credential.verifyToken }
|
||||
: {}),
|
||||
...(credential.verifyToken
|
||||
? {
|
||||
'x-captcha-provider': credential.challenge
|
||||
? 'hashcash'
|
||||
: 'turnstile',
|
||||
}
|
||||
: {}),
|
||||
...(credential.challenge
|
||||
? { 'x-captcha-challenge': credential.challenge }
|
||||
: {}),
|
||||
@@ -181,22 +190,18 @@ export const authHandlers = {
|
||||
},
|
||||
|
||||
signOut: async (_e, endpoint: string) => {
|
||||
const token = getNativeAuthToken(endpoint);
|
||||
if (token) {
|
||||
await net.fetch(authUrl(endpoint, '/api/auth/sign-out'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'x-affine-version': BUILD_CONFIG.appVersion,
|
||||
},
|
||||
});
|
||||
try {
|
||||
await revokeAuthSession(endpoint);
|
||||
} finally {
|
||||
await clearAuthCookies(endpoint);
|
||||
}
|
||||
|
||||
deleteNativeAuthToken(endpoint);
|
||||
await clearAuthCookies(endpoint);
|
||||
},
|
||||
|
||||
readEndpointToken: async (_e, endpoint: string) => {
|
||||
return { token: getNativeAuthToken(endpoint) };
|
||||
clearSession: async (_e, endpoint: string) => {
|
||||
await clearAuthSession(endpoint, 'local-clear');
|
||||
},
|
||||
|
||||
getValidAccessToken: async (_e, endpoint: string) => {
|
||||
return { token: await getValidAccessToken(endpoint, 120_000) };
|
||||
},
|
||||
} satisfies NamespaceHandlers;
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { app, safeStorage } from 'electron';
|
||||
|
||||
import { logger } from '../logger';
|
||||
|
||||
const FILEPATH = path.join(app.getPath('userData'), 'native-auth-tokens.json');
|
||||
|
||||
type TokenRecord = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
// safeStorage may not be available in some environments (e.g. Linux without a keyring), so we fall back to an in-memory store in that case
|
||||
const memoryTokenStore: Record<string, string> = {};
|
||||
|
||||
function normalizeEndpoint(endpoint: string) {
|
||||
return new URL(endpoint).origin;
|
||||
}
|
||||
|
||||
function readStore(): Record<string, string> {
|
||||
if (!fs.existsSync(FILEPATH)) return {};
|
||||
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(FILEPATH, 'utf-8'));
|
||||
} catch (error) {
|
||||
logger.error('failed to read native auth token store', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeStore(store: Record<string, string>) {
|
||||
fs.writeFileSync(FILEPATH, JSON.stringify(store, null, 2));
|
||||
}
|
||||
|
||||
function encryptToken(record: TokenRecord) {
|
||||
if (!safeStorage.isEncryptionAvailable()) {
|
||||
throw new Error('Secure native auth token storage is not available.');
|
||||
}
|
||||
return safeStorage.encryptString(JSON.stringify(record)).toString('base64');
|
||||
}
|
||||
|
||||
function decryptToken(value: string): TokenRecord | null {
|
||||
if (!safeStorage.isEncryptionAvailable()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(safeStorage.decryptString(Buffer.from(value, 'base64')));
|
||||
} catch (error) {
|
||||
logger.error('failed to decrypt native auth token', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function setNativeAuthToken(endpoint: string, token: string) {
|
||||
const normalizedEndpoint = normalizeEndpoint(endpoint);
|
||||
if (!safeStorage.isEncryptionAvailable()) {
|
||||
memoryTokenStore[normalizedEndpoint] = token;
|
||||
return false;
|
||||
}
|
||||
|
||||
const store = readStore();
|
||||
store[normalizedEndpoint] = encryptToken({ token });
|
||||
writeStore(store);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function deleteNativeAuthToken(endpoint: string) {
|
||||
const normalizedEndpoint = normalizeEndpoint(endpoint);
|
||||
delete memoryTokenStore[normalizedEndpoint];
|
||||
|
||||
const store = readStore();
|
||||
delete store[normalizedEndpoint];
|
||||
writeStore(store);
|
||||
}
|
||||
|
||||
export function getNativeAuthToken(endpoint: string) {
|
||||
const normalizedEndpoint = normalizeEndpoint(endpoint);
|
||||
const memoryToken = memoryTokenStore[normalizedEndpoint];
|
||||
if (memoryToken) return memoryToken;
|
||||
|
||||
const encrypted = readStore()[normalizedEndpoint];
|
||||
if (!encrypted) return null;
|
||||
return decryptToken(encrypted)?.token ?? null;
|
||||
}
|
||||
|
||||
export function getAuthTokenForUrl(url: string) {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol === 'ws:') {
|
||||
parsed.protocol = 'http:';
|
||||
} else if (parsed.protocol === 'wss:') {
|
||||
parsed.protocol = 'https:';
|
||||
}
|
||||
return getNativeAuthToken(parsed.origin);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
resolvePathInBase,
|
||||
resourcesPath,
|
||||
} from '../shared/utils';
|
||||
import { getAuthTokenForUrl } from './auth/native-token';
|
||||
import {
|
||||
executeAuthSessionRequest,
|
||||
getAccessTokenForUrl,
|
||||
isManagedAuthEndpoint,
|
||||
} from './auth/auth-session';
|
||||
import { buildType, isDev } from './config';
|
||||
import { logger } from './logger';
|
||||
|
||||
@@ -64,26 +68,6 @@ function buildTargetUrl(base: string, urlObject: URL) {
|
||||
return new URL(`${urlObject.pathname}${urlObject.search}`, base).toString();
|
||||
}
|
||||
|
||||
async function buildAuthorizedRequest(request: Request, targetUrl: string) {
|
||||
const clonedRequest = request.clone();
|
||||
const headers = new Headers(clonedRequest.headers);
|
||||
const token = getAuthTokenForUrl(targetUrl);
|
||||
if (token) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
return new Request(targetUrl, {
|
||||
body:
|
||||
clonedRequest.method === 'GET' || clonedRequest.method === 'HEAD'
|
||||
? undefined
|
||||
: clonedRequest.body,
|
||||
headers,
|
||||
method: clonedRequest.method,
|
||||
redirect: clonedRequest.redirect,
|
||||
signal: clonedRequest.signal,
|
||||
});
|
||||
}
|
||||
|
||||
async function proxyRequest(
|
||||
request: Request,
|
||||
urlObject: URL,
|
||||
@@ -92,13 +76,13 @@ async function proxyRequest(
|
||||
) {
|
||||
const { bypassCustomProtocolHandlers = true } = options;
|
||||
const targetUrl = buildTargetUrl(base, urlObject);
|
||||
const authorizedRequest = await buildAuthorizedRequest(request, targetUrl);
|
||||
const proxiedRequest = bypassCustomProtocolHandlers
|
||||
? Object.assign(authorizedRequest, {
|
||||
bypassCustomProtocolHandlers: true,
|
||||
})
|
||||
: authorizedRequest;
|
||||
return net.fetch(proxiedRequest);
|
||||
return await executeAuthSessionRequest(request, targetUrl, request =>
|
||||
net.fetch(
|
||||
bypassCustomProtocolHandlers
|
||||
? Object.assign(request, { bypassCustomProtocolHandlers: true })
|
||||
: request
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function handleFileRequest(request: Request) {
|
||||
@@ -268,17 +252,20 @@ export function registerProtocol() {
|
||||
|
||||
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
|
||||
const url = new URL(details.url);
|
||||
|
||||
(async () => {
|
||||
if (
|
||||
url.protocol === 'http:' ||
|
||||
const managedAuthRequest =
|
||||
(url.protocol === 'http:' ||
|
||||
url.protocol === 'https:' ||
|
||||
url.protocol === 'ws:' ||
|
||||
url.protocol === 'wss:'
|
||||
) {
|
||||
const token = getAuthTokenForUrl(details.url);
|
||||
url.protocol === 'wss:') &&
|
||||
isManagedAuthEndpoint(details.url);
|
||||
let cancel = false;
|
||||
|
||||
(async () => {
|
||||
if (managedAuthRequest) {
|
||||
delete details.requestHeaders.authorization;
|
||||
delete details.requestHeaders.Authorization;
|
||||
const token = await getAccessTokenForUrl(details.url, 120_000);
|
||||
if (token) {
|
||||
delete details.requestHeaders.authorization;
|
||||
details.requestHeaders.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
@@ -292,11 +279,12 @@ export function registerProtocol() {
|
||||
}
|
||||
})()
|
||||
.catch(err => {
|
||||
cancel = managedAuthRequest;
|
||||
logger.error('error handling before send headers', err);
|
||||
})
|
||||
.finally(() => {
|
||||
callback({
|
||||
cancel: false,
|
||||
cancel,
|
||||
requestHeaders: details.requestHeaders,
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user