mirror of
https://github.com/bitwarden/browser
synced 2026-02-06 11:43:51 +00:00
introduce DefaultSendAccessService.authenticate$
This commit is contained in:
@@ -3,7 +3,9 @@ import { Router, UrlTree } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, NEVER } from "rxjs";
|
||||
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { mockAccountServiceWith, FakeStateProvider } from "@bitwarden/common/spec";
|
||||
import { SemanticLogger } from "@bitwarden/common/tools/log";
|
||||
@@ -17,6 +19,7 @@ import { SEND_RESPONSE_KEY, SEND_CONTEXT_KEY } from "./send-access-memory";
|
||||
|
||||
describe("DefaultSendAccessService", () => {
|
||||
let service: DefaultSendAccessService;
|
||||
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||
let stateProvider: FakeStateProvider;
|
||||
let sendApiService: MockProxy<SendApiService>;
|
||||
let router: MockProxy<Router>;
|
||||
@@ -24,6 +27,9 @@ describe("DefaultSendAccessService", () => {
|
||||
let systemServiceProvider: MockProxy<SystemServiceProvider>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const accountService = mockAccountServiceWith("user-id" as UserId);
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
sendApiService = mock<SendApiService>();
|
||||
@@ -36,6 +42,7 @@ describe("DefaultSendAccessService", () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
DefaultSendAccessService,
|
||||
{ provide: CryptoFunctionService, useValue: cryptoFunctionService },
|
||||
{ provide: StateProvider, useValue: stateProvider },
|
||||
{ provide: SendApiService, useValue: sendApiService },
|
||||
{ provide: Router, useValue: router },
|
||||
@@ -127,7 +134,6 @@ describe("DefaultSendAccessService", () => {
|
||||
});
|
||||
|
||||
it("emits timeout error when API response exceeds 10 seconds", fakeAsync(() => {
|
||||
// Mock API to never resolve (simulating a hung request)
|
||||
sendApiService.postSendAccess.mockReturnValue(firstValueFrom(NEVER));
|
||||
|
||||
const result$ = service.redirect$(sendId);
|
||||
@@ -159,7 +165,6 @@ describe("DefaultSendAccessService", () => {
|
||||
|
||||
describe("clear", () => {
|
||||
it("sets both SEND_RESPONSE_KEY and SEND_CONTEXT_KEY to null when called", async () => {
|
||||
// Set initial values
|
||||
await stateProvider.getGlobal(SEND_RESPONSE_KEY).update(() => ({ some: "response" }) as any);
|
||||
await stateProvider.getGlobal(SEND_CONTEXT_KEY).update(() => ({ id: "test", key: "test" }));
|
||||
|
||||
@@ -172,4 +177,179 @@ describe("DefaultSendAccessService", () => {
|
||||
expect(context).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("authenticate$", () => {
|
||||
const sendId = "test-send-id";
|
||||
const password = "test-password";
|
||||
|
||||
it("returns true and stores response when authentication succeeds", async () => {
|
||||
const keyArray = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const key = Utils.fromBufferToB64(keyArray);
|
||||
const hashedArray = new Uint8Array([10, 20, 30, 40]);
|
||||
const expectedPassword = Utils.fromBufferToB64(hashedArray);
|
||||
const mockResponse = { id: sendId, data: "some data" } as any;
|
||||
|
||||
await stateProvider.getGlobal(SEND_CONTEXT_KEY).update(() => ({ id: sendId, key }));
|
||||
cryptoFunctionService.pbkdf2.mockResolvedValue(hashedArray);
|
||||
sendApiService.postSendAccess.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await firstValueFrom(service.authenticate$(sendId, password));
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(sendApiService.postSendAccess).toHaveBeenCalledWith(
|
||||
sendId,
|
||||
expect.objectContaining({ password: expectedPassword }),
|
||||
);
|
||||
|
||||
// Verify response is stored in state
|
||||
const storedResponse = await firstValueFrom(
|
||||
stateProvider.getGlobal(SEND_RESPONSE_KEY).state$,
|
||||
);
|
||||
expect(storedResponse).toBe(mockResponse);
|
||||
});
|
||||
|
||||
it("returns false and logs debug when authentication fails with 401", async () => {
|
||||
const keyArray = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const key = Utils.fromBufferToB64(keyArray);
|
||||
const hashedArray = new Uint8Array([10, 20, 30, 40]);
|
||||
const errorResponse = new ErrorResponse([], 401);
|
||||
await stateProvider.getGlobal(SEND_CONTEXT_KEY).update(() => ({ id: sendId, key }));
|
||||
cryptoFunctionService.pbkdf2.mockResolvedValue(hashedArray);
|
||||
sendApiService.postSendAccess.mockRejectedValue(errorResponse);
|
||||
|
||||
const result = await firstValueFrom(service.authenticate$(sendId, password));
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(logger.debug).toHaveBeenCalledWith("received failed authentication response");
|
||||
|
||||
// Verify state is not updated
|
||||
const storedResponse = await firstValueFrom(
|
||||
stateProvider.getGlobal(SEND_RESPONSE_KEY).state$,
|
||||
);
|
||||
expect(storedResponse).toBeNull();
|
||||
});
|
||||
|
||||
it("panics when sendId mismatch occurs", fakeAsync(async () => {
|
||||
const wrongSendId = "wrong-send-id";
|
||||
const keyArray = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const key = Utils.fromBufferToB64(keyArray);
|
||||
await stateProvider.getGlobal(SEND_CONTEXT_KEY).update(() => ({ id: wrongSendId, key }));
|
||||
logger.panicWhen.mockImplementation((condition, data, message) => {
|
||||
if (condition) {
|
||||
throw new Error(`Panic: ${message}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const result$ = service.authenticate$(sendId, password);
|
||||
let error: any;
|
||||
result$.subscribe({
|
||||
error: (err: unknown) => (error = err),
|
||||
});
|
||||
|
||||
tick();
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Panic: unexpected sendId");
|
||||
expect(logger.panicWhen).toHaveBeenCalledWith(
|
||||
true, // condition: sendId !== id (test-send-id !== wrong-send-id)
|
||||
{ expected: sendId, actual: sendId },
|
||||
"unexpected sendId",
|
||||
);
|
||||
}));
|
||||
|
||||
it("emits timeout error when context is null", fakeAsync(async () => {
|
||||
await stateProvider.getGlobal(SEND_CONTEXT_KEY).update(() => null);
|
||||
|
||||
const result$ = service.authenticate$(sendId, password);
|
||||
let error: any;
|
||||
|
||||
result$.subscribe({
|
||||
error: (err: unknown) => (error = err),
|
||||
});
|
||||
|
||||
// Advance time past 10 seconds
|
||||
tick(10001);
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.name).toBe("TimeoutError");
|
||||
}));
|
||||
|
||||
it("emits timeout error when context is invalid", fakeAsync(async () => {
|
||||
// Set context missing required properties
|
||||
await stateProvider.getGlobal(SEND_CONTEXT_KEY).update(() => ({ missingId: "test" }) as any);
|
||||
|
||||
const result$ = service.authenticate$(sendId, password);
|
||||
let error: any;
|
||||
|
||||
result$.subscribe({
|
||||
error: (err: unknown) => (error = err),
|
||||
});
|
||||
|
||||
// Advance time past 10 seconds
|
||||
tick(10001);
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.name).toBe("TimeoutError");
|
||||
}));
|
||||
|
||||
it("throws error and logs when server error occurs", async () => {
|
||||
const keyArray = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const key = Utils.fromBufferToB64(keyArray);
|
||||
const hashedArray = new Uint8Array([10, 20, 30, 40]);
|
||||
const errorResponse = new ErrorResponse([], 500);
|
||||
await stateProvider.getGlobal(SEND_CONTEXT_KEY).update(() => ({ id: sendId, key }));
|
||||
|
||||
// Mock crypto and API
|
||||
cryptoFunctionService.pbkdf2.mockResolvedValue(hashedArray);
|
||||
sendApiService.postSendAccess.mockRejectedValue(errorResponse);
|
||||
|
||||
await expect(firstValueFrom(service.authenticate$(sendId, password))).rejects.toBe(
|
||||
errorResponse,
|
||||
);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith("an error occurred during authentication");
|
||||
});
|
||||
|
||||
it("throws error and logs when network error occurs", async () => {
|
||||
const keyArray = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const key = Utils.fromBufferToB64(keyArray);
|
||||
const hashedArray = new Uint8Array([10, 20, 30, 40]);
|
||||
const networkError = new Error("Network error");
|
||||
await stateProvider.getGlobal(SEND_CONTEXT_KEY).update(() => ({ id: sendId, key }));
|
||||
cryptoFunctionService.pbkdf2.mockResolvedValue(hashedArray);
|
||||
sendApiService.postSendAccess.mockRejectedValue(networkError);
|
||||
|
||||
await expect(firstValueFrom(service.authenticate$(sendId, password))).rejects.toThrow(
|
||||
"Network error",
|
||||
);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith("an error occurred during authentication");
|
||||
});
|
||||
|
||||
it("emits timeout error when API response exceeds 10 seconds", fakeAsync(async () => {
|
||||
const keyArray = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const key = Utils.fromBufferToB64(keyArray);
|
||||
const hashedArray = new Uint8Array([10, 20, 30, 40]);
|
||||
await stateProvider.getGlobal(SEND_CONTEXT_KEY).update(() => ({ id: sendId, key }));
|
||||
|
||||
// Mock API to never resolve (simulating a hung request)
|
||||
sendApiService.postSendAccess.mockReturnValue(firstValueFrom(NEVER));
|
||||
cryptoFunctionService.pbkdf2.mockResolvedValue(hashedArray);
|
||||
|
||||
const result$ = service.authenticate$(sendId, password);
|
||||
let error: any;
|
||||
|
||||
result$.subscribe({
|
||||
error: (err: unknown) => (error = err),
|
||||
});
|
||||
|
||||
// Advance time past 10 seconds
|
||||
tick(10001);
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.name).toBe("TimeoutError");
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injectable, Inject } from "@angular/core";
|
||||
import { Router, UrlTree } from "@angular/router";
|
||||
import { map, of, from, catchError, timeout } from "rxjs";
|
||||
import { map, of, from, catchError, timeout, concatMap, filter, tap } from "rxjs";
|
||||
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { SemanticLogger } from "@bitwarden/common/tools/log";
|
||||
@@ -10,7 +11,8 @@ import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/s
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components";
|
||||
|
||||
import { SEND_RESPONSE_KEY, SEND_CONTEXT_KEY } from "./send-access-memory";
|
||||
import { keyToSendAccessRequest } from "./rx";
|
||||
import { SEND_RESPONSE_KEY, SEND_CONTEXT_KEY, isSendContext } from "./send-access-memory";
|
||||
import { SendAccessService } from "./send-access-service.abstraction";
|
||||
import { isErrorResponse } from "./util";
|
||||
|
||||
@@ -21,6 +23,7 @@ export class DefaultSendAccessService implements SendAccessService {
|
||||
private readonly logger: SemanticLogger;
|
||||
|
||||
constructor(
|
||||
private readonly crypto: CryptoFunctionService,
|
||||
private readonly state: StateProvider,
|
||||
private readonly api: SendApiService,
|
||||
private readonly router: Router,
|
||||
@@ -60,6 +63,40 @@ export class DefaultSendAccessService implements SendAccessService {
|
||||
return redirect$;
|
||||
}
|
||||
|
||||
authenticate$(sendId: string, password: string) {
|
||||
const authenticate$ = this.state.getGlobal(SEND_CONTEXT_KEY).state$.pipe(
|
||||
filter(isSendContext),
|
||||
tap(({ id }) =>
|
||||
this.logger.panicWhen(
|
||||
sendId !== id,
|
||||
{ expected: sendId, actual: sendId },
|
||||
"unexpected sendId",
|
||||
),
|
||||
),
|
||||
map(({ key }) => key),
|
||||
keyToSendAccessRequest(this.crypto, password),
|
||||
concatMap(async (request) => {
|
||||
const result = await this.api.postSendAccess(sendId, request);
|
||||
|
||||
await this.state.getGlobal(SEND_RESPONSE_KEY).update(() => result);
|
||||
|
||||
return true;
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
if (isErrorResponse(error) && error.statusCode === 401) {
|
||||
this.logger.debug("received failed authentication response");
|
||||
return of(false);
|
||||
}
|
||||
|
||||
this.logger.error("an error occurred during authentication");
|
||||
throw error;
|
||||
}),
|
||||
timeout({ first: TEN_SECONDS }),
|
||||
);
|
||||
|
||||
return authenticate$;
|
||||
}
|
||||
|
||||
private toViewRedirect(sendId: string) {
|
||||
return this.router.createUrlTree(["send", "content", sendId]);
|
||||
}
|
||||
|
||||
126
apps/web/src/app/tools/send/send-access/rx.spec.ts
Normal file
126
apps/web/src/app/tools/send/send-access/rx.spec.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of, firstValueFrom, Subject, toArray } from "rxjs";
|
||||
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request";
|
||||
import { SEND_KDF_ITERATIONS } from "@bitwarden/common/tools/send/send-kdf";
|
||||
|
||||
import { awaitAsync } from "../../../../../../../libs/common/spec";
|
||||
|
||||
import { keyToSendAccessRequest } from "./rx";
|
||||
|
||||
describe("keyToSendAccessRequest", () => {
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("calls pbkdf2 with exact parameters when given a base64 key", async () => {
|
||||
const password = "test-password";
|
||||
const keyArray = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const key = Utils.fromBufferToB64(keyArray);
|
||||
const hashedArray = new Uint8Array([10, 20, 30, 40]);
|
||||
|
||||
cryptoFunctionService.pbkdf2.mockResolvedValue(hashedArray);
|
||||
|
||||
const toRequest = keyToSendAccessRequest(cryptoFunctionService, password);
|
||||
await firstValueFrom(of(key).pipe(toRequest));
|
||||
|
||||
expect(cryptoFunctionService.pbkdf2).toHaveBeenCalledWith(
|
||||
password,
|
||||
keyArray,
|
||||
"sha256",
|
||||
SEND_KDF_ITERATIONS,
|
||||
);
|
||||
expect(cryptoFunctionService.pbkdf2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns SendAccessRequest with base64 encoded password when pbkdf2 succeeds", async () => {
|
||||
const password = "test-password";
|
||||
const keyArray = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const key = Utils.fromBufferToB64(keyArray);
|
||||
const hashedArray = new Uint8Array([10, 20, 30, 40]);
|
||||
const expectedPassword = Utils.fromBufferToB64(hashedArray);
|
||||
|
||||
cryptoFunctionService.pbkdf2.mockResolvedValue(hashedArray);
|
||||
|
||||
const hasher = keyToSendAccessRequest(cryptoFunctionService, password);
|
||||
const result = await firstValueFrom(of(key).pipe(hasher));
|
||||
|
||||
expect(result.password).toBe(expectedPassword);
|
||||
});
|
||||
|
||||
it("propagates error when pbkdf2 fails", async () => {
|
||||
const password = "test-password";
|
||||
const key = Utils.fromBufferToB64(new Uint8Array([1, 2, 3, 4, 5]));
|
||||
const expectedError = new Error("pbkdf2 failed");
|
||||
|
||||
cryptoFunctionService.pbkdf2.mockRejectedValue(expectedError);
|
||||
|
||||
const hasher = keyToSendAccessRequest(cryptoFunctionService, password);
|
||||
await expect(firstValueFrom(of(key).pipe(hasher))).rejects.toThrow("pbkdf2 failed");
|
||||
});
|
||||
|
||||
it("completes when source observable completes", (done) => {
|
||||
const password = "test-password";
|
||||
const source$ = new Subject<string>();
|
||||
const hashedArray = new Uint8Array([10, 20, 30, 40]);
|
||||
|
||||
cryptoFunctionService.pbkdf2.mockResolvedValue(hashedArray);
|
||||
|
||||
const hasher = keyToSendAccessRequest(cryptoFunctionService, password);
|
||||
source$.pipe(hasher).subscribe({
|
||||
complete: () => {
|
||||
done();
|
||||
},
|
||||
});
|
||||
|
||||
// if `hasher` fails to pass through complete, `done()` won't be called
|
||||
// and the test will time out
|
||||
source$.complete();
|
||||
});
|
||||
|
||||
it("handles multiple emissions when source emits multiple keys", async () => {
|
||||
const password = "test-password";
|
||||
const keyArray1 = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const keyArray2 = new Uint8Array([6, 7, 8, 9, 10]);
|
||||
const key1 = Utils.fromBufferToB64(keyArray1);
|
||||
const key2 = Utils.fromBufferToB64(keyArray2);
|
||||
const hashedArray1 = new Uint8Array([10, 20, 30, 40]);
|
||||
const hashedArray2 = new Uint8Array([50, 60, 70, 80]);
|
||||
const expectedPassword1 = Utils.fromBufferToB64(hashedArray1);
|
||||
const expectedPassword2 = Utils.fromBufferToB64(hashedArray2);
|
||||
|
||||
cryptoFunctionService.pbkdf2
|
||||
.mockResolvedValueOnce(hashedArray1)
|
||||
.mockResolvedValueOnce(hashedArray2);
|
||||
|
||||
const hasher = keyToSendAccessRequest(cryptoFunctionService, password);
|
||||
let results: SendAccessRequest[] = [];
|
||||
of(key1, key2)
|
||||
.pipe(hasher, toArray())
|
||||
.subscribe((r) => (results = r));
|
||||
await awaitAsync();
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0].password).toBe(expectedPassword1);
|
||||
expect(results[1].password).toBe(expectedPassword2);
|
||||
expect(cryptoFunctionService.pbkdf2).toHaveBeenCalledTimes(2);
|
||||
expect(cryptoFunctionService.pbkdf2).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
password,
|
||||
keyArray1,
|
||||
"sha256",
|
||||
SEND_KDF_ITERATIONS,
|
||||
);
|
||||
expect(cryptoFunctionService.pbkdf2).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
password,
|
||||
keyArray2,
|
||||
"sha256",
|
||||
SEND_KDF_ITERATIONS,
|
||||
);
|
||||
});
|
||||
});
|
||||
22
apps/web/src/app/tools/send/send-access/rx.ts
Normal file
22
apps/web/src/app/tools/send/send-access/rx.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { map, concatMap, pipe, OperatorFunction } from "rxjs";
|
||||
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request";
|
||||
import { SEND_KDF_ITERATIONS } from "@bitwarden/common/tools/send/send-kdf";
|
||||
|
||||
export function keyToSendAccessRequest(
|
||||
crypto: CryptoFunctionService,
|
||||
password: string,
|
||||
): OperatorFunction<string, SendAccessRequest> {
|
||||
return pipe(
|
||||
map((key) => Utils.fromUrlB64ToArray(key)),
|
||||
// FIXME: support kdf iteration and/or hash updates
|
||||
concatMap((key) => crypto.pbkdf2(password, key, "sha256", SEND_KDF_ITERATIONS)),
|
||||
map((hash) => {
|
||||
const request = new SendAccessRequest();
|
||||
request.password = Utils.fromBufferToB64(hash);
|
||||
return request;
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -48,6 +48,19 @@ export class DefaultSemanticLogger<Context extends object> implements SemanticLo
|
||||
throw new Error(panicMessage);
|
||||
}
|
||||
|
||||
panicWhen<T>(
|
||||
when: boolean,
|
||||
content: Jsonify<T> | string,
|
||||
message?: string,
|
||||
): when is false | never {
|
||||
if (when) {
|
||||
// type conversion safe because Jsonify<string> === string
|
||||
this.panic(content as Jsonify<T | string>, message);
|
||||
}
|
||||
|
||||
return !when;
|
||||
}
|
||||
|
||||
private log<T>(content: Jsonify<T>, level: LogLevelType, message?: string) {
|
||||
const log = {
|
||||
...this.context,
|
||||
|
||||
@@ -92,4 +92,24 @@ export interface SemanticLogger {
|
||||
|
||||
/** combined signature for overloaded methods */
|
||||
panic<T>(content: Jsonify<T> | string, message?: string): never;
|
||||
|
||||
/** Conditionally panics.
|
||||
* @param when - when this condition is true, the logger panics.
|
||||
* @param message - a message to record in the log's `message` field.
|
||||
*/
|
||||
panicWhen(when: boolean, message: string): when is false | never;
|
||||
|
||||
/** Conditionally panics.
|
||||
* @param when - when this condition is true, the logger panics.
|
||||
* @param content - JSON content included in the log's `content` field.
|
||||
* @param message - a message to record in the log's `message` field.
|
||||
*/
|
||||
panicWhen<T>(when: boolean, content: Jsonify<T>, message?: string): when is false | never;
|
||||
|
||||
/** combined signature for overloaded methods */
|
||||
panicWhen<T>(
|
||||
when: boolean,
|
||||
content: Jsonify<T> | string,
|
||||
message?: string,
|
||||
): when is false | never;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user