mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 05:30:01 +00:00
fixed merge conflict
This commit is contained in:
1
.github/renovate.json5
vendored
1
.github/renovate.json5
vendored
@@ -250,6 +250,7 @@
|
||||
"napi-derive",
|
||||
"node-forge",
|
||||
"node-ipc",
|
||||
"nx",
|
||||
"oo7",
|
||||
"oslog",
|
||||
"pin-project",
|
||||
|
||||
4
.github/workflows/deploy-web.yml
vendored
4
.github/workflows/deploy-web.yml
vendored
@@ -242,7 +242,7 @@ jobs:
|
||||
run: |
|
||||
# If run-id was used, get the commit from the download-latest-artifacts-run-id step
|
||||
if [ "${{ inputs.build-web-run-id }}" ]; then
|
||||
echo "commit=${{ steps.download-latest-artifacts-run-id.outputs.artifact_build_commit }}" >> $GITHUB_OUTPUT
|
||||
echo "commit=${{ steps.download-latest-artifacts-run-id.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT
|
||||
|
||||
elif [ "${{ steps.download-latest-artifacts.outcome }}" == "failure" ]; then
|
||||
# If the download-latest-artifacts step failed, query the GH API to get the commit SHA of the artifact that was just built with trigger-build-web.
|
||||
@@ -251,7 +251,7 @@ jobs:
|
||||
|
||||
else
|
||||
# Set the commit to the output of step download-latest-artifacts.
|
||||
echo "commit=${{ steps.download-latest-artifacts.outputs.artifact_build_commit }}" >> $GITHUB_OUTPUT
|
||||
echo "commit=${{ steps.download-latest-artifacts.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
notify-start:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -48,3 +48,6 @@ storybook-static
|
||||
|
||||
# Local app configuration
|
||||
apps/**/config/local.json
|
||||
|
||||
# Nx
|
||||
.nx
|
||||
|
||||
@@ -95,6 +95,7 @@ type NotificationBackgroundExtensionMessageHandlers = {
|
||||
unlockCompleted: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
||||
bgGetFolderData: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<FolderView[]>;
|
||||
bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
||||
bgOpenAtRisksPasswords: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
||||
bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
||||
bgAddLogin: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
||||
bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
import { BehaviorSubject, firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
|
||||
@@ -12,6 +12,7 @@ import { UserNotificationSettingsService } from "@bitwarden/common/autofill/serv
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -19,6 +20,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
||||
import { TaskService, SecurityTask } from "@bitwarden/common/vault/tasks";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
|
||||
@@ -46,6 +48,8 @@ jest.mock("rxjs", () => {
|
||||
});
|
||||
|
||||
describe("NotificationBackground", () => {
|
||||
const messagingService = mock<MessagingService>();
|
||||
const taskService = mock<TaskService>();
|
||||
let notificationBackground: NotificationBackground;
|
||||
const autofillService = mock<AutofillService>();
|
||||
const cipherService = mock<CipherService>();
|
||||
@@ -88,6 +92,8 @@ describe("NotificationBackground", () => {
|
||||
policyService,
|
||||
themeStateService,
|
||||
userNotificationSettingsService,
|
||||
taskService,
|
||||
messagingService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -201,8 +207,8 @@ describe("NotificationBackground", () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(notificationBackground["handleSaveCipherMessage"]).toHaveBeenCalledWith(
|
||||
message.data.commandToRetry.message,
|
||||
message.data.commandToRetry.sender,
|
||||
message.data?.commandToRetry?.message,
|
||||
message.data?.commandToRetry?.sender,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -498,7 +504,7 @@ describe("NotificationBackground", () => {
|
||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||
null,
|
||||
"example.com",
|
||||
message.data.newPassword,
|
||||
message.data?.newPassword,
|
||||
sender.tab,
|
||||
true,
|
||||
);
|
||||
@@ -570,7 +576,7 @@ describe("NotificationBackground", () => {
|
||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||
"cipher-id",
|
||||
"example.com",
|
||||
message.data.newPassword,
|
||||
message.data?.newPassword,
|
||||
sender.tab,
|
||||
);
|
||||
});
|
||||
@@ -618,7 +624,7 @@ describe("NotificationBackground", () => {
|
||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||
"cipher-id",
|
||||
"example.com",
|
||||
message.data.newPassword,
|
||||
message.data?.newPassword,
|
||||
sender.tab,
|
||||
);
|
||||
});
|
||||
@@ -844,6 +850,86 @@ describe("NotificationBackground", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("completes password update notification with a security task notice if any are present for the cipher, and dismisses tasks for the updated cipher", async () => {
|
||||
const mockCipherId = "testId";
|
||||
const mockOrgId = "testOrgId";
|
||||
const mockSecurityTask = {
|
||||
id: "testTaskId",
|
||||
organizationId: mockOrgId,
|
||||
cipherId: mockCipherId,
|
||||
type: 0,
|
||||
status: 0,
|
||||
creationDate: new Date(),
|
||||
revisionDate: new Date(),
|
||||
} as SecurityTask;
|
||||
const mockSecurityTask2 = {
|
||||
...mockSecurityTask,
|
||||
id: "testTaskId2",
|
||||
cipherId: "testId2",
|
||||
} as SecurityTask;
|
||||
taskService.tasksEnabled$.mockImplementation(() => of(true));
|
||||
taskService.pendingTasks$.mockImplementation(() =>
|
||||
of([mockSecurityTask, mockSecurityTask2]),
|
||||
);
|
||||
jest
|
||||
.spyOn(notificationBackground as any, "getNotificationFlag")
|
||||
.mockResolvedValueOnce(true);
|
||||
jest.spyOn(notificationBackground as any, "getOrgData").mockResolvedValueOnce([
|
||||
{
|
||||
id: mockOrgId,
|
||||
name: "Org Name, LLC",
|
||||
productTierType: 3,
|
||||
},
|
||||
]);
|
||||
|
||||
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
|
||||
const sender = mock<chrome.runtime.MessageSender>({ tab });
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgSaveCipher",
|
||||
edit: false,
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddChangePasswordQueueMessage>({
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
newPassword: "newPassword",
|
||||
});
|
||||
notificationBackground["notificationQueue"] = [queueMessage];
|
||||
const cipherView = mock<CipherView>({
|
||||
id: mockCipherId,
|
||||
organizationId: mockOrgId,
|
||||
login: { username: "testUser" },
|
||||
});
|
||||
getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
|
||||
expect(editItemSpy).not.toHaveBeenCalled();
|
||||
expect(createWithServerSpy).not.toHaveBeenCalled();
|
||||
expect(updatePasswordSpy).toHaveBeenCalledWith(
|
||||
cipherView,
|
||||
queueMessage.newPassword,
|
||||
message.edit,
|
||||
sender.tab,
|
||||
mockCipherId,
|
||||
);
|
||||
expect(updateWithServerSpy).toHaveBeenCalled();
|
||||
expect(tabSendMessageDataSpy).toHaveBeenCalledWith(
|
||||
sender.tab,
|
||||
"saveCipherAttemptCompleted",
|
||||
{
|
||||
cipherId: "testId",
|
||||
task: {
|
||||
orgName: "Org Name, LLC",
|
||||
remainingTasksCount: 1,
|
||||
},
|
||||
username: "testUser",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("updates the cipher password if the queue message was locked and an existing cipher has the same username as the message", async () => {
|
||||
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
|
||||
const sender = mock<chrome.runtime.MessageSender>({ tab });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, switchMap } from "rxjs";
|
||||
import { firstValueFrom, switchMap, map } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
@@ -22,16 +22,21 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
|
||||
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { SecurityTaskType } from "@bitwarden/common/vault/tasks/enums";
|
||||
import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task";
|
||||
|
||||
import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
@@ -70,6 +75,8 @@ export default class NotificationBackground {
|
||||
bgChangedPassword: ({ message, sender }) => this.changedPassword(message, sender),
|
||||
bgCloseNotificationBar: ({ message, sender }) =>
|
||||
this.handleCloseNotificationBarMessage(message, sender),
|
||||
bgOpenAtRisksPasswords: ({ message, sender }) =>
|
||||
this.handleOpenAtRisksPasswordsMessage(message, sender),
|
||||
bgGetActiveUserServerConfig: () => this.getActiveUserServerConfig(),
|
||||
bgGetDecryptedCiphers: () => this.getNotificationCipherData(),
|
||||
bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(),
|
||||
@@ -106,6 +113,8 @@ export default class NotificationBackground {
|
||||
private policyService: PolicyService,
|
||||
private themeStateService: ThemeStateService,
|
||||
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
||||
private taskService: TaskService,
|
||||
protected messagingService: MessagingService,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
@@ -154,17 +163,20 @@ export default class NotificationBackground {
|
||||
firstValueFrom(this.domainSettingsService.showFavicons$),
|
||||
firstValueFrom(this.environmentService.environment$),
|
||||
]);
|
||||
|
||||
const iconsServerUrl = env.getIconsUrl();
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||
);
|
||||
|
||||
const decryptedCiphers = await this.cipherService.getAllDecryptedForUrl(
|
||||
currentTab.url,
|
||||
currentTab?.url,
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
return decryptedCiphers.map((view) => {
|
||||
const { id, name, reprompt, favorite, login } = view;
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
@@ -599,13 +611,13 @@ export default class NotificationBackground {
|
||||
try {
|
||||
await this.cipherService.createWithServer(cipher);
|
||||
await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", {
|
||||
username: String(queueMessage?.username),
|
||||
cipherId: String(cipher?.id),
|
||||
username: queueMessage?.username && String(queueMessage.username),
|
||||
cipherId: cipher?.id && String(cipher.id),
|
||||
});
|
||||
await BrowserApi.tabSendMessage(tab, { command: "addedCipher" });
|
||||
} catch (error) {
|
||||
await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", {
|
||||
error: String(error.message),
|
||||
error: error?.message && String(error.message),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -638,15 +650,49 @@ export default class NotificationBackground {
|
||||
return;
|
||||
}
|
||||
const cipher = await this.cipherService.encrypt(cipherView, userId);
|
||||
|
||||
const shouldGetTasks = await this.getNotificationFlag();
|
||||
|
||||
try {
|
||||
const tasks = shouldGetTasks ? await this.getSecurityTasks(userId) : [];
|
||||
const updatedCipherTask = tasks.find((task) => task.cipherId === cipherView?.id);
|
||||
const cipherHasTask = !!updatedCipherTask?.id;
|
||||
|
||||
let taskOrgName: string;
|
||||
if (cipherHasTask && updatedCipherTask?.organizationId) {
|
||||
const userOrgs = await this.getOrgData();
|
||||
taskOrgName = userOrgs.find(({ id }) => id === updatedCipherTask.organizationId)?.name;
|
||||
}
|
||||
|
||||
const taskData = cipherHasTask
|
||||
? {
|
||||
remainingTasksCount: tasks.length - 1,
|
||||
orgName: taskOrgName,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await this.cipherService.updateWithServer(cipher);
|
||||
|
||||
await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", {
|
||||
username: String(cipherView?.login?.username),
|
||||
cipherId: String(cipherView?.id),
|
||||
username: cipherView?.login?.username && String(cipherView.login.username),
|
||||
cipherId: cipherView?.id && String(cipherView.id),
|
||||
task: taskData,
|
||||
});
|
||||
|
||||
// If the cipher had a security task, mark it as complete
|
||||
if (cipherHasTask) {
|
||||
// guard against multiple (redundant) security tasks per cipher
|
||||
await Promise.all(
|
||||
tasks.map((task) => {
|
||||
if (task.cipherId === cipherView?.id) {
|
||||
return this.taskService.markAsComplete(task.id, userId);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", {
|
||||
error: String(error?.message),
|
||||
error: error?.message && String(error.message),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -697,6 +743,32 @@ export default class NotificationBackground {
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getSecurityTasks(userId: UserId) {
|
||||
let tasks: SecurityTask[] = [];
|
||||
|
||||
if (userId) {
|
||||
tasks = await firstValueFrom(
|
||||
this.taskService.tasksEnabled$(userId).pipe(
|
||||
switchMap((tasksEnabled) => {
|
||||
if (!tasksEnabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.taskService
|
||||
.pendingTasks$(userId)
|
||||
.pipe(
|
||||
map((tasks) =>
|
||||
tasks.filter(({ type }) => type === SecurityTaskType.UpdateAtRiskCredential),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current tab's domain to the never save list.
|
||||
*
|
||||
@@ -817,6 +889,41 @@ export default class NotificationBackground {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the background to open the
|
||||
* at-risk passwords extension view. Triggers
|
||||
* notification closure as a side-effect.
|
||||
*
|
||||
* @param message - The extension message
|
||||
* @param sender - The contextual sender of the message
|
||||
*/
|
||||
private async handleOpenAtRisksPasswordsMessage(
|
||||
message: NotificationBackgroundExtensionMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
) {
|
||||
const browserAction = BrowserApi.getBrowserAction();
|
||||
|
||||
try {
|
||||
// Set route of the popup before attempting to open it.
|
||||
// If the vault is locked, this won't have an effect as the auth guards will
|
||||
// redirect the user to the login page.
|
||||
await browserAction.setPopup({ popup: "popup/index.html#/at-risk-passwords" });
|
||||
|
||||
await Promise.all([
|
||||
this.messagingService.send(VaultMessages.OpenAtRiskPasswords),
|
||||
BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar", {
|
||||
fadeOutNotification: !!message.fadeOutNotification,
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
// Reset the popup route to the default route so any subsequent
|
||||
// popup openings will not open to the at-risk-passwords page.
|
||||
await browserAction.setPopup({
|
||||
popup: "popup/index.html#/",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message back to the sender tab which triggers
|
||||
* an CSS adjustment of the notification bar.
|
||||
|
||||
@@ -117,7 +117,6 @@ describe("OverlayBackground", () => {
|
||||
let getFrameDetailsSpy: jest.SpyInstance;
|
||||
let tabsSendMessageSpy: jest.SpyInstance;
|
||||
let tabSendMessageDataSpy: jest.SpyInstance;
|
||||
let sendMessageSpy: jest.SpyInstance;
|
||||
let getTabFromCurrentWindowIdSpy: jest.SpyInstance;
|
||||
let getTabSpy: jest.SpyInstance;
|
||||
let openUnlockPopoutSpy: jest.SpyInstance;
|
||||
@@ -228,7 +227,6 @@ describe("OverlayBackground", () => {
|
||||
tabSendMessageDataSpy = jest
|
||||
.spyOn(BrowserApi, "tabSendMessageData")
|
||||
.mockImplementation(() => Promise.resolve());
|
||||
sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage");
|
||||
getTabFromCurrentWindowIdSpy = jest.spyOn(BrowserApi, "getTabFromCurrentWindowId");
|
||||
getTabSpy = jest.spyOn(BrowserApi, "getTab");
|
||||
openUnlockPopoutSpy = jest.spyOn(overlayBackground as any, "openUnlockPopout");
|
||||
@@ -1553,7 +1551,6 @@ describe("OverlayBackground", () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
|
||||
expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher");
|
||||
expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1579,7 +1576,6 @@ describe("OverlayBackground", () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
|
||||
expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher");
|
||||
expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1618,7 +1614,6 @@ describe("OverlayBackground", () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
|
||||
expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher");
|
||||
expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -2434,7 +2434,6 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
cipherId: cipherView.id,
|
||||
cipherType: addNewCipherType,
|
||||
});
|
||||
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
|
||||
} catch (error) {
|
||||
this.logService.error("Error building cipher and opening add/edit vault item popout", error);
|
||||
}
|
||||
|
||||
@@ -647,9 +647,6 @@ describe("OverlayBackground", () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled();
|
||||
expect(BrowserApi.sendMessage).toHaveBeenCalledWith(
|
||||
"inlineAutofillMenuRefreshAddEditCipher",
|
||||
);
|
||||
expect(overlayBackground["openAddEditVaultItemPopout"]).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -678,7 +678,6 @@ class LegacyOverlayBackground implements OverlayBackgroundInterface {
|
||||
);
|
||||
|
||||
await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id });
|
||||
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,11 +29,14 @@ type NotificationBarIframeInitData = {
|
||||
};
|
||||
|
||||
type NotificationBarWindowMessage = {
|
||||
cipherId?: string;
|
||||
command: string;
|
||||
data?: {
|
||||
cipherId?: string;
|
||||
task?: NotificationTaskInfo;
|
||||
username?: string;
|
||||
};
|
||||
error?: string;
|
||||
initData?: NotificationBarIframeInitData;
|
||||
username?: string;
|
||||
};
|
||||
|
||||
type NotificationBarWindowMessageHandlers = {
|
||||
|
||||
@@ -356,7 +356,8 @@ function openViewVaultItemPopout(e: Event, cipherId: string) {
|
||||
|
||||
function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
|
||||
const { theme, type } = notificationBarIframeInitData;
|
||||
const { error, username, cipherId } = message;
|
||||
const { error, data } = message;
|
||||
const { username, cipherId, task } = data || {};
|
||||
const i18n = getI18n();
|
||||
const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light);
|
||||
|
||||
@@ -371,8 +372,9 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
|
||||
i18n,
|
||||
error,
|
||||
username: username ?? i18n.typeLogin,
|
||||
task,
|
||||
handleOpenVault: (e) => cipherId && openViewVaultItemPopout(e, cipherId),
|
||||
handleOpenTasks: () => {},
|
||||
handleOpenTasks: () => sendPlatformMessage({ command: "bgOpenAtRisksPasswords" }),
|
||||
}),
|
||||
document.body,
|
||||
);
|
||||
|
||||
@@ -23,8 +23,8 @@ describe("OverlayNotificationsContentService", () => {
|
||||
autofillInit = new AutofillInit(
|
||||
domQueryService,
|
||||
domElementVisibilityService,
|
||||
null,
|
||||
null,
|
||||
undefined,
|
||||
undefined,
|
||||
overlayNotificationsContentService,
|
||||
);
|
||||
autofillInit.init();
|
||||
@@ -89,7 +89,7 @@ describe("OverlayNotificationsContentService", () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(
|
||||
overlayNotificationsContentService["notificationBarIframeElement"].style.transform,
|
||||
overlayNotificationsContentService["notificationBarIframeElement"]?.style.transform,
|
||||
).toBe("translateX(100%)");
|
||||
});
|
||||
|
||||
@@ -103,12 +103,12 @@ describe("OverlayNotificationsContentService", () => {
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
overlayNotificationsContentService["notificationBarIframeElement"].dispatchEvent(
|
||||
overlayNotificationsContentService["notificationBarIframeElement"]?.dispatchEvent(
|
||||
new Event("load"),
|
||||
);
|
||||
|
||||
expect(
|
||||
overlayNotificationsContentService["notificationBarIframeElement"].style.transform,
|
||||
overlayNotificationsContentService["notificationBarIframeElement"]?.style.transform,
|
||||
).toBe("translateX(0)");
|
||||
});
|
||||
|
||||
@@ -134,7 +134,7 @@ describe("OverlayNotificationsContentService", () => {
|
||||
globalThis.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: { command: "initNotificationBar" },
|
||||
source: overlayNotificationsContentService["notificationBarIframeElement"].contentWindow,
|
||||
source: overlayNotificationsContentService["notificationBarIframeElement"]?.contentWindow,
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
@@ -168,9 +168,9 @@ describe("OverlayNotificationsContentService", () => {
|
||||
data: { fadeOutNotification: true },
|
||||
});
|
||||
|
||||
expect(overlayNotificationsContentService["notificationBarIframeElement"].style.opacity).toBe(
|
||||
"0",
|
||||
);
|
||||
expect(
|
||||
overlayNotificationsContentService["notificationBarIframeElement"]?.style.opacity,
|
||||
).toBe("0");
|
||||
|
||||
jest.advanceTimersByTime(150);
|
||||
|
||||
@@ -210,7 +210,7 @@ describe("OverlayNotificationsContentService", () => {
|
||||
data: { height: 1000 },
|
||||
});
|
||||
|
||||
expect(overlayNotificationsContentService["notificationBarElement"].style.height).toBe(
|
||||
expect(overlayNotificationsContentService["notificationBarElement"]?.style.height).toBe(
|
||||
"1000px",
|
||||
);
|
||||
});
|
||||
@@ -236,13 +236,13 @@ describe("OverlayNotificationsContentService", () => {
|
||||
|
||||
sendMockExtensionMessage({
|
||||
command: "saveCipherAttemptCompleted",
|
||||
data: { error: "" },
|
||||
data: { error: undefined },
|
||||
});
|
||||
|
||||
expect(
|
||||
overlayNotificationsContentService["notificationBarIframeElement"].contentWindow
|
||||
.postMessage,
|
||||
).toHaveBeenCalledWith({ command: "saveCipherAttemptCompleted", error: "" }, "*");
|
||||
).toHaveBeenCalledWith({ command: "saveCipherAttemptCompleted", error: undefined }, "*");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -135,9 +135,13 @@ export class OverlayNotificationsContentService
|
||||
* @private
|
||||
*/
|
||||
private handleSaveCipherAttemptCompletedMessage(message: NotificationsExtensionMessage) {
|
||||
// destructure error out of data
|
||||
const { error, ...otherData } = message?.data || {};
|
||||
|
||||
this.sendMessageToNotificationBarIframe({
|
||||
command: "saveCipherAttemptCompleted",
|
||||
error: message.data?.error,
|
||||
data: Object.keys(otherData).length ? otherData : undefined,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1195,6 +1195,17 @@ export default class MainBackground {
|
||||
this.authService,
|
||||
() => this.generatePasswordToClipboard(),
|
||||
);
|
||||
|
||||
this.taskService = new DefaultTaskService(
|
||||
this.stateProvider,
|
||||
this.apiService,
|
||||
this.organizationService,
|
||||
this.configService,
|
||||
this.authService,
|
||||
this.notificationsService,
|
||||
messageListener,
|
||||
);
|
||||
|
||||
this.notificationBackground = new NotificationBackground(
|
||||
this.accountService,
|
||||
this.authService,
|
||||
@@ -1209,6 +1220,8 @@ export default class MainBackground {
|
||||
this.policyService,
|
||||
this.themeStateService,
|
||||
this.userNotificationSettingsService,
|
||||
this.taskService,
|
||||
this.messagingService,
|
||||
);
|
||||
|
||||
this.overlayNotificationsBackground = new OverlayNotificationsBackground(
|
||||
@@ -1313,16 +1326,6 @@ export default class MainBackground {
|
||||
this.configService,
|
||||
);
|
||||
|
||||
this.taskService = new DefaultTaskService(
|
||||
this.stateProvider,
|
||||
this.apiService,
|
||||
this.organizationService,
|
||||
this.configService,
|
||||
this.authService,
|
||||
this.notificationsService,
|
||||
messageListener,
|
||||
);
|
||||
|
||||
this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
||||
|
||||
this.ipcContentScriptManagerService = new IpcContentScriptManagerService(this.configService);
|
||||
|
||||
@@ -27,9 +27,6 @@ export class VaultItemsComponent extends BaseVaultItemsComponent {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
searchBarService.searchText$.pipe(distinctUntilChanged()).subscribe((searchText) => {
|
||||
this.searchText = searchText;
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.search(200);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -126,9 +126,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
|
||||
}
|
||||
|
||||
async ngOnChanges() {
|
||||
await super.load();
|
||||
|
||||
if (this.cipher.decryptionFailure) {
|
||||
if (this.cipher?.decryptionFailure) {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: [this.cipherId as CipherId],
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Subject,
|
||||
} from "rxjs";
|
||||
import {
|
||||
catchError,
|
||||
concatMap,
|
||||
debounceTime,
|
||||
filter,
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
take,
|
||||
takeUntil,
|
||||
tap,
|
||||
catchError,
|
||||
} from "rxjs/operators";
|
||||
|
||||
import {
|
||||
@@ -64,6 +64,7 @@ import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-repromp
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
import { DialogRef, DialogService, Icons, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
AddEditFolderDialogComponent,
|
||||
@@ -138,7 +139,6 @@ const SearchTextDebounceInterval = 200;
|
||||
VaultFilterModule,
|
||||
VaultItemsModule,
|
||||
SharedModule,
|
||||
DecryptionFailureDialogComponent,
|
||||
],
|
||||
providers: [
|
||||
RoutedVaultFilterService,
|
||||
@@ -348,9 +348,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
]).pipe(
|
||||
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
|
||||
concatMap(async ([ciphers, filter, searchText]) => {
|
||||
const failedCiphers = await firstValueFrom(
|
||||
this.cipherService.failedToDecryptCiphers$(activeUserId),
|
||||
);
|
||||
const failedCiphers =
|
||||
(await firstValueFrom(this.cipherService.failedToDecryptCiphers$(activeUserId))) ?? [];
|
||||
const filterFunction = createFilterFunction(filter);
|
||||
// Append any failed to decrypt ciphers to the top of the cipher list
|
||||
const allCiphers = [...failedCiphers, ...ciphers];
|
||||
@@ -472,6 +471,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
firstSetup$
|
||||
.pipe(
|
||||
switchMap(() => this.cipherService.failedToDecryptCiphers$(activeUserId)),
|
||||
filterOutNullish(),
|
||||
map((ciphers) => ciphers.filter((c) => !c.isDeleted)),
|
||||
filter((ciphers) => ciphers.length > 0),
|
||||
take(1),
|
||||
@@ -528,6 +528,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.isEmpty = collections?.length === 0 && ciphers?.length === 0;
|
||||
this.performingInitialLoad = false;
|
||||
this.refreshing = false;
|
||||
|
||||
// Explicitly mark for check to ensure the view is updated
|
||||
// Some sources are not always emitted within the Angular zone (e.g. ciphers updated via WS notifications)
|
||||
this.changeDetectorRef.markForCheck();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -420,10 +420,15 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipher = await this.encryptCipher(activeUserId);
|
||||
|
||||
try {
|
||||
this.formPromise = this.saveCipher(cipher);
|
||||
await this.formPromise;
|
||||
this.cipher.id = cipher.id;
|
||||
const savedCipher = await this.formPromise;
|
||||
|
||||
// Reset local cipher from the saved cipher returned from the server
|
||||
this.cipher = await savedCipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(savedCipher, activeUserId),
|
||||
);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { BehaviorSubject, Subject, firstValueFrom, from, switchMap, takeUntil } from "rxjs";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
Subject,
|
||||
combineLatest,
|
||||
filter,
|
||||
from,
|
||||
of,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
@@ -21,17 +30,17 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
|
||||
loaded = false;
|
||||
ciphers: CipherView[] = [];
|
||||
filter: (cipher: CipherView) => boolean = null;
|
||||
deleted = false;
|
||||
organization: Organization;
|
||||
|
||||
protected searchPending = false;
|
||||
|
||||
private userId: UserId;
|
||||
/** Construct filters as an observable so it can be appended to the cipher stream. */
|
||||
private _filter$ = new BehaviorSubject<(cipher: CipherView) => boolean | null>(null);
|
||||
private destroy$ = new Subject<void>();
|
||||
private searchTimeout: any = null;
|
||||
private isSearchable: boolean = false;
|
||||
private _searchText$ = new BehaviorSubject<string>("");
|
||||
|
||||
get searchText() {
|
||||
return this._searchText$.value;
|
||||
}
|
||||
@@ -39,18 +48,28 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
this._searchText$.next(value);
|
||||
}
|
||||
|
||||
get filter() {
|
||||
return this._filter$.value;
|
||||
}
|
||||
|
||||
set filter(value: (cipher: CipherView) => boolean | null) {
|
||||
this._filter$.next(value);
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected searchService: SearchService,
|
||||
protected cipherService: CipherService,
|
||||
protected accountService: AccountService,
|
||||
) {}
|
||||
) {
|
||||
this.subscribeToCiphers();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
this._searchText$
|
||||
combineLatest([getUserId(this.accountService.activeAccount$), this._searchText$])
|
||||
.pipe(
|
||||
switchMap((searchText) => from(this.searchService.isSearchable(this.userId, searchText))),
|
||||
switchMap(([userId, searchText]) =>
|
||||
from(this.searchService.isSearchable(userId, searchText)),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((isSearchable) => {
|
||||
@@ -80,23 +99,6 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
|
||||
async applyFilter(filter: (cipher: CipherView) => boolean = null) {
|
||||
this.filter = filter;
|
||||
await this.search(null);
|
||||
}
|
||||
|
||||
async search(timeout: number = null, indexedCiphers?: CipherView[]) {
|
||||
this.searchPending = false;
|
||||
if (this.searchTimeout != null) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
if (timeout == null) {
|
||||
await this.doSearch(indexedCiphers);
|
||||
return;
|
||||
}
|
||||
this.searchPending = true;
|
||||
this.searchTimeout = setTimeout(async () => {
|
||||
await this.doSearch(indexedCiphers);
|
||||
this.searchPending = false;
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
selectCipher(cipher: CipherView) {
|
||||
@@ -121,25 +123,44 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted;
|
||||
|
||||
protected async doSearch(indexedCiphers?: CipherView[], userId?: UserId) {
|
||||
// Get userId from activeAccount if not provided from parent stream
|
||||
if (!userId) {
|
||||
userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
}
|
||||
/**
|
||||
* Creates stream of dependencies that results in the list of ciphers to display
|
||||
* within the vault list.
|
||||
*
|
||||
* Note: This previously used promises but race conditions with how the ciphers were
|
||||
* stored in electron. Using observables is more reliable as fresh values will always
|
||||
* cascade through the components.
|
||||
*/
|
||||
private subscribeToCiphers() {
|
||||
getUserId(this.accountService.activeAccount$)
|
||||
.pipe(
|
||||
switchMap((userId) =>
|
||||
combineLatest([
|
||||
this.cipherService.cipherViews$(userId).pipe(filter((ciphers) => ciphers != null)),
|
||||
this.cipherService.failedToDecryptCiphers$(userId),
|
||||
this._searchText$,
|
||||
this._filter$,
|
||||
of(userId),
|
||||
]),
|
||||
),
|
||||
switchMap(([indexedCiphers, failedCiphers, searchText, filter, userId]) => {
|
||||
let allCiphers = indexedCiphers ?? [];
|
||||
const _failedCiphers = failedCiphers ?? [];
|
||||
|
||||
indexedCiphers =
|
||||
indexedCiphers ?? (await firstValueFrom(this.cipherService.cipherViews$(userId)));
|
||||
allCiphers = [..._failedCiphers, ...allCiphers];
|
||||
|
||||
const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$(userId));
|
||||
if (failedCiphers != null && failedCiphers.length > 0) {
|
||||
indexedCiphers = [...failedCiphers, ...indexedCiphers];
|
||||
}
|
||||
|
||||
this.ciphers = await this.searchService.searchCiphers(
|
||||
this.userId,
|
||||
this.searchText,
|
||||
[this.filter, this.deletedFilter],
|
||||
indexedCiphers,
|
||||
);
|
||||
return this.searchService.searchCiphers(
|
||||
userId,
|
||||
searchText,
|
||||
[filter, this.deletedFilter],
|
||||
allCiphers,
|
||||
);
|
||||
}),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe((ciphers) => {
|
||||
this.ciphers = ciphers;
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,17 @@ import {
|
||||
OnInit,
|
||||
Output,
|
||||
} from "@angular/core";
|
||||
import { filter, firstValueFrom, map, Observable } from "rxjs";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
of,
|
||||
switchMap,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
@@ -46,11 +56,22 @@ import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
const BroadcasterSubscriptionId = "ViewComponent";
|
||||
const BroadcasterSubscriptionId = "BaseViewComponent";
|
||||
|
||||
@Directive()
|
||||
export class ViewComponent implements OnDestroy, OnInit {
|
||||
@Input() cipherId: string;
|
||||
/** Observable of cipherId$ that will update each time the `Input` updates */
|
||||
private _cipherId$ = new BehaviorSubject<string>(null);
|
||||
|
||||
@Input()
|
||||
set cipherId(value: string) {
|
||||
this._cipherId$.next(value);
|
||||
}
|
||||
|
||||
get cipherId(): string {
|
||||
return this._cipherId$.getValue();
|
||||
}
|
||||
|
||||
@Input() collectionId: string;
|
||||
@Output() onEditCipher = new EventEmitter<CipherView>();
|
||||
@Output() onCloneCipher = new EventEmitter<CipherView>();
|
||||
@@ -126,13 +147,30 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
switch (message.command) {
|
||||
case "syncCompleted":
|
||||
if (message.successfully) {
|
||||
await this.load();
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Set up the subscription to the activeAccount$ and cipherId$ observables
|
||||
combineLatest([this.accountService.activeAccount$.pipe(getUserId), this._cipherId$])
|
||||
.pipe(
|
||||
tap(() => this.cleanUp()),
|
||||
switchMap(([userId, cipherId]) => {
|
||||
const cipher$ = this.cipherService.cipherViews$(userId).pipe(
|
||||
map((ciphers) => ciphers?.find((c) => c.id === cipherId)),
|
||||
filter((cipher) => !!cipher),
|
||||
);
|
||||
return combineLatest([of(userId), cipher$]);
|
||||
}),
|
||||
)
|
||||
.subscribe(([userId, cipher]) => {
|
||||
this.cipher = cipher;
|
||||
|
||||
void this.constructCipherDetails(userId);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -140,70 +178,6 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
this.cleanUp();
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.cleanUp();
|
||||
|
||||
// Grab individual cipher from `cipherViews$` for the most up-to-date information
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.cipher = await firstValueFrom(
|
||||
this.cipherService.cipherViews$(activeUserId).pipe(
|
||||
map((ciphers) => ciphers?.find((c) => c.id === this.cipherId)),
|
||||
filter((cipher) => !!cipher),
|
||||
),
|
||||
);
|
||||
|
||||
this.canAccessPremium = await firstValueFrom(
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
|
||||
);
|
||||
this.showPremiumRequiredTotp =
|
||||
this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp;
|
||||
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [
|
||||
this.collectionId as CollectionId,
|
||||
]);
|
||||
this.canRestoreCipher$ = this.cipherAuthorizationService.canRestoreCipher$(this.cipher);
|
||||
|
||||
if (this.cipher.folderId) {
|
||||
this.folder = await (
|
||||
await firstValueFrom(this.folderService.folderViews$(activeUserId))
|
||||
).find((f) => f.id == this.cipher.folderId);
|
||||
}
|
||||
|
||||
const canGenerateTotp =
|
||||
this.cipher.type === CipherType.Login &&
|
||||
this.cipher.login.totp &&
|
||||
(this.cipher.organizationUseTotp || this.canAccessPremium);
|
||||
|
||||
this.totpInfo$ = canGenerateTotp
|
||||
? this.totpService.getCode$(this.cipher.login.totp).pipe(
|
||||
map((response) => {
|
||||
const epoch = Math.round(new Date().getTime() / 1000.0);
|
||||
const mod = epoch % response.period;
|
||||
|
||||
// Format code
|
||||
const totpCodeFormatted =
|
||||
response.code.length > 4
|
||||
? `${response.code.slice(0, Math.floor(response.code.length / 2))} ${response.code.slice(Math.floor(response.code.length / 2))}`
|
||||
: response.code;
|
||||
|
||||
return {
|
||||
totpCode: response.code,
|
||||
totpCodeFormatted,
|
||||
totpDash: +(Math.round(((78.6 / response.period) * mod + "e+2") as any) + "e-2"),
|
||||
totpSec: response.period - mod,
|
||||
totpLow: response.period - mod <= 7,
|
||||
} as TotpInfo;
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (this.previousCipherId !== this.cipherId) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientViewed, this.cipherId);
|
||||
}
|
||||
this.previousCipherId = this.cipherId;
|
||||
}
|
||||
|
||||
async edit() {
|
||||
this.onEditCipher.emit(this.cipher);
|
||||
}
|
||||
@@ -533,4 +507,61 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
this.showCardCode = false;
|
||||
this.passwordReprompted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a cipher is viewed, construct all details for the view that are not directly
|
||||
* available from the cipher object itself.
|
||||
*/
|
||||
private async constructCipherDetails(userId: UserId) {
|
||||
this.canAccessPremium = await firstValueFrom(
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(userId),
|
||||
);
|
||||
this.showPremiumRequiredTotp =
|
||||
this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp;
|
||||
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [
|
||||
this.collectionId as CollectionId,
|
||||
]);
|
||||
this.canRestoreCipher$ = this.cipherAuthorizationService.canRestoreCipher$(this.cipher);
|
||||
|
||||
if (this.cipher.folderId) {
|
||||
this.folder = await (
|
||||
await firstValueFrom(this.folderService.folderViews$(userId))
|
||||
).find((f) => f.id == this.cipher.folderId);
|
||||
}
|
||||
|
||||
const canGenerateTotp =
|
||||
this.cipher.type === CipherType.Login &&
|
||||
this.cipher.login.totp &&
|
||||
(this.cipher.organizationUseTotp || this.canAccessPremium);
|
||||
|
||||
this.totpInfo$ = canGenerateTotp
|
||||
? this.totpService.getCode$(this.cipher.login.totp).pipe(
|
||||
map((response) => {
|
||||
const epoch = Math.round(new Date().getTime() / 1000.0);
|
||||
const mod = epoch % response.period;
|
||||
|
||||
// Format code
|
||||
const totpCodeFormatted =
|
||||
response.code.length > 4
|
||||
? `${response.code.slice(0, Math.floor(response.code.length / 2))} ${response.code.slice(Math.floor(response.code.length / 2))}`
|
||||
: response.code;
|
||||
|
||||
return {
|
||||
totpCode: response.code,
|
||||
totpCodeFormatted,
|
||||
totpDash: +(Math.round(((78.6 / response.period) * mod + "e+2") as any) + "e-2"),
|
||||
totpSec: response.period - mod,
|
||||
totpLow: response.period - mod <= 7,
|
||||
} as TotpInfo;
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (this.previousCipherId !== this.cipherId) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientViewed, this.cipherId);
|
||||
}
|
||||
this.previousCipherId = this.cipherId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,14 +153,14 @@ export class DefaultNotificationsService implements NotificationsServiceAbstract
|
||||
await this.syncService.syncUpsertCipher(
|
||||
notification.payload as SyncCipherNotification,
|
||||
notification.type === NotificationType.SyncCipherUpdate,
|
||||
payloadUserId,
|
||||
userId,
|
||||
);
|
||||
break;
|
||||
case NotificationType.SyncCipherDelete:
|
||||
case NotificationType.SyncLoginDelete:
|
||||
await this.syncService.syncDeleteCipher(
|
||||
notification.payload as SyncCipherNotification,
|
||||
payloadUserId,
|
||||
userId,
|
||||
);
|
||||
break;
|
||||
case NotificationType.SyncFolderCreate:
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
combineLatest,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
merge,
|
||||
Observable,
|
||||
of,
|
||||
shareReplay,
|
||||
Subject,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
import { combineLatest, filter, firstValueFrom, map, Observable, Subject, switchMap } from "rxjs";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -40,6 +29,7 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt
|
||||
import { StateProvider } from "../../platform/state";
|
||||
import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid";
|
||||
import { OrgKey, UserKey } from "../../types/key";
|
||||
import { perUserCache$ } from "../../vault/utils/observable-utilities";
|
||||
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
|
||||
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
|
||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||
@@ -92,11 +82,12 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
this.sortCiphersByLastUsed,
|
||||
);
|
||||
/**
|
||||
* Observable that forces the `cipherViews$` observable to re-emit with the provided value.
|
||||
* Used to let subscribers of `cipherViews$` know that the decrypted ciphers have been cleared for the active user.
|
||||
* Observable that forces the `cipherViews$` observable for the given user to emit a null value.
|
||||
* Used to let subscribers of `cipherViews$` know that the decrypted ciphers have been cleared for the user and to
|
||||
* clear them from the shareReplay buffer created in perUserCache$().
|
||||
* @private
|
||||
*/
|
||||
private forceCipherViews$: Subject<CipherView[]> = new Subject<CipherView[]>();
|
||||
private clearCipherViewsForUser$: Subject<UserId> = new Subject<UserId>();
|
||||
|
||||
constructor(
|
||||
private keyService: KeyService,
|
||||
@@ -134,13 +125,16 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
* A `null` value indicates that the latest encrypted ciphers have not been decrypted yet and that
|
||||
* decryption is in progress. The latest decrypted ciphers will be emitted once decryption is complete.
|
||||
*/
|
||||
cipherViews$(userId: UserId): Observable<CipherView[] | null> {
|
||||
return combineLatest([this.encryptedCiphersState(userId).state$, this.localData$(userId)]).pipe(
|
||||
filter(([ciphers]) => ciphers != null), // Skip if ciphers haven't been loaded yor synced yet
|
||||
switchMap(() => merge(this.forceCipherViews$, this.getAllDecrypted(userId))),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
cipherViews$ = perUserCache$((userId: UserId): Observable<CipherView[] | null> => {
|
||||
return combineLatest([
|
||||
this.encryptedCiphersState(userId).state$,
|
||||
this.localData$(userId),
|
||||
this.keyService.cipherDecryptionKeys$(userId, true),
|
||||
]).pipe(
|
||||
filter(([ciphers, keys]) => ciphers != null && keys != null), // Skip if ciphers haven't been loaded yor synced yet
|
||||
switchMap(() => this.getAllDecrypted(userId)),
|
||||
);
|
||||
}
|
||||
}, this.clearCipherViewsForUser$);
|
||||
|
||||
addEditCipherInfo$(userId: UserId): Observable<AddEditCipherInfo> {
|
||||
return this.addEditCipherInfoState(userId).state$;
|
||||
@@ -151,13 +145,11 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
*
|
||||
* An empty array indicates that all ciphers were successfully decrypted.
|
||||
*/
|
||||
failedToDecryptCiphers$(userId: UserId): Observable<CipherView[]> {
|
||||
failedToDecryptCiphers$ = perUserCache$((userId: UserId): Observable<CipherView[]> => {
|
||||
return this.failedToDecryptCiphersState(userId).state$.pipe(
|
||||
filter((ciphers) => ciphers != null),
|
||||
switchMap((ciphers) => merge(this.forceCipherViews$, of(ciphers))),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
}
|
||||
}, this.clearCipherViewsForUser$);
|
||||
|
||||
async setDecryptedCipherCache(value: CipherView[], userId: UserId) {
|
||||
// Sometimes we might prematurely decrypt the vault and that will result in no ciphers
|
||||
@@ -192,10 +184,8 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
userId ??= activeUserId;
|
||||
await this.clearDecryptedCiphersState(userId);
|
||||
|
||||
// Force the cipherView$ observable (which always tracks the active user) to re-emit
|
||||
if (userId == activeUserId) {
|
||||
this.forceCipherViews$.next(null);
|
||||
}
|
||||
// Force the cached cipherView$ observable(s) to emit a null value
|
||||
this.clearCipherViewsForUser$.next(userId);
|
||||
}
|
||||
|
||||
async encrypt(
|
||||
@@ -404,10 +394,14 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return await this.getDecryptedCiphers(userId);
|
||||
}
|
||||
|
||||
const [newDecCiphers, failedCiphers] = await this.decryptCiphers(
|
||||
await this.getAll(userId),
|
||||
userId,
|
||||
);
|
||||
const decrypted = await this.decryptCiphers(await this.getAll(userId), userId);
|
||||
|
||||
// We failed to decrypt, return empty array but do not cache
|
||||
if (decrypted == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [newDecCiphers, failedCiphers] = decrypted;
|
||||
|
||||
await this.setDecryptedCipherCache(newDecCiphers, userId);
|
||||
await this.setFailedDecryptedCiphers(failedCiphers, userId);
|
||||
@@ -431,14 +425,14 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
private async decryptCiphers(
|
||||
ciphers: Cipher[],
|
||||
userId: UserId,
|
||||
): Promise<[CipherView[], CipherView[]]> {
|
||||
): Promise<[CipherView[], CipherView[]] | null> {
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.PM19941MigrateCipherDomainToSdk)) {
|
||||
return this.decryptCiphersWithSdk(ciphers, userId);
|
||||
} else {
|
||||
const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true));
|
||||
if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) {
|
||||
// return early if there are no keys to decrypt with
|
||||
return [[], []];
|
||||
return null;
|
||||
}
|
||||
// Group ciphers by orgId or under 'null' for the user's ciphers
|
||||
const grouped = ciphers.reduce(
|
||||
|
||||
@@ -12,8 +12,11 @@ import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { NotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
filterOutNullish,
|
||||
perUserCache$,
|
||||
} from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
|
||||
import { filterOutNullish, perUserCache$ } from "../../utils/observable-utilities";
|
||||
import { TaskService } from "../abstractions/task.service";
|
||||
import { SecurityTaskStatus } from "../enums";
|
||||
import { SecurityTask, SecurityTaskData, SecurityTaskResponse } from "../models";
|
||||
|
||||
@@ -1,20 +1,38 @@
|
||||
import { filter, Observable, OperatorFunction, shareReplay } from "rxjs";
|
||||
import { EMPTY, filter, map, merge, Observable, OperatorFunction, shareReplay } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
/**
|
||||
* Builds an observable once per userId and caches it for future requests.
|
||||
* The built observables are shared among subscribers with a replay buffer size of 1.
|
||||
*
|
||||
* Optionally, a clearBuffer$ observable can be provided to clear the replay buffer for a specific or all userIds.
|
||||
* @param create - A function that creates an observable for a given userId.
|
||||
* @param clearBuffer$ - An observable that, when emitted, clears the buffer for the emitted userId. When null is emitted, all caches are cleared.
|
||||
*/
|
||||
export function perUserCache$<TValue>(
|
||||
create: (userId: UserId) => Observable<TValue>,
|
||||
): (userId: UserId) => Observable<TValue> {
|
||||
const cache = new Map<UserId, Observable<TValue>>();
|
||||
clearBuffer$: Observable<UserId | null>,
|
||||
): (userId: UserId) => Observable<TValue | null>;
|
||||
export function perUserCache$<TValue>(
|
||||
create: (userId: UserId) => Observable<TValue>,
|
||||
): (userId: UserId) => Observable<TValue>;
|
||||
export function perUserCache$<TValue>(
|
||||
create: (userId: UserId) => Observable<TValue>,
|
||||
clearBuffer$: Observable<UserId | null> | undefined = undefined,
|
||||
): (userId: UserId) => Observable<TValue | null> {
|
||||
const cache = new Map<UserId, Observable<TValue | null>>();
|
||||
return (userId: UserId) => {
|
||||
let observable = cache.get(userId);
|
||||
if (!observable) {
|
||||
observable = create(userId).pipe(shareReplay({ bufferSize: 1, refCount: false }));
|
||||
clearBuffer$ ??= EMPTY;
|
||||
observable = merge(
|
||||
create(userId),
|
||||
clearBuffer$.pipe(
|
||||
filter((clearId) => clearId === userId || clearId === null),
|
||||
map(() => null),
|
||||
),
|
||||
).pipe(shareReplay({ bufferSize: 1, refCount: false }));
|
||||
cache.set(userId, observable);
|
||||
}
|
||||
return observable;
|
||||
|
||||
10
nx.json
Normal file
10
nx.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"cacheDirectory": ".nx/cache",
|
||||
"defaultBase": "main",
|
||||
"namedInputs": {
|
||||
"default": ["{projectRoot}/**/*"],
|
||||
"production": ["!{projectRoot}/**/*.spec.ts"]
|
||||
},
|
||||
"parallel": 4,
|
||||
"targetDefaults": {}
|
||||
}
|
||||
528
package-lock.json
generated
528
package-lock.json
generated
@@ -156,6 +156,7 @@
|
||||
"json5": "2.2.3",
|
||||
"lint-staged": "15.4.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"nx": "20.8.0",
|
||||
"postcss": "8.5.1",
|
||||
"postcss-loader": "8.1.1",
|
||||
"prettier": "3.4.2",
|
||||
@@ -5954,6 +5955,34 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.1.tgz",
|
||||
"integrity": "sha512-4JFstCTaToCFrPqrGzgkF8N2NHjtsaY4uRh6brZQ5L9e4wbMieX8oDT8N7qfVFTQecHFEtkj4ve49VIZ3mKVqw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.0.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.1.tgz",
|
||||
"integrity": "sha512-LMshMVP0ZhACNjQNYXiU1iZJ6QCcv0lUdPDPugqGvCGXt5xtRVBPdtA0qU12pEXZzpWAhWlZYptfdAFq10DOVQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz",
|
||||
"integrity": "sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/babel-plugin": {
|
||||
"version": "11.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
||||
@@ -8223,6 +8252,17 @@
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz",
|
||||
"integrity": "sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.1.0",
|
||||
"@emnapi/runtime": "^1.1.0",
|
||||
"@tybys/wasm-util": "^0.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ng-select/ng-select": {
|
||||
"version": "13.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-13.9.1.tgz",
|
||||
@@ -8758,6 +8798,166 @@
|
||||
"node": "^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nx/nx-darwin-arm64": {
|
||||
"version": "20.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-20.8.0.tgz",
|
||||
"integrity": "sha512-A6Te2KlINtcOo/depXJzPyjbk9E0cmgbom/sm/49XdQ8G94aDfyIIY1RIdwmDCK5NVd74KFG3JIByTk5+VnAhA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nx/nx-darwin-x64": {
|
||||
"version": "20.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-20.8.0.tgz",
|
||||
"integrity": "sha512-UpqayUjgalArXaDvOoshqSelTrEp42cGDsZGy0sqpxwBpm3oPQ8wE1d7oBAmRo208rAxOuFP0LZRFUqRrwGvLA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nx/nx-freebsd-x64": {
|
||||
"version": "20.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-20.8.0.tgz",
|
||||
"integrity": "sha512-dUR2fsLyKZYMHByvjy2zvmdMbsdXAiP+6uTlIAuu8eHMZ2FPQCAtt7lPYLwOFUxUXChbek2AJ+uCI0gRAgK/eg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nx/nx-linux-arm-gnueabihf": {
|
||||
"version": "20.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-20.8.0.tgz",
|
||||
"integrity": "sha512-GuZ7t0SzSX5ksLYva7koKZovQ5h/Kr1pFbOsQcBf3VLREBqFPSz6t7CVYpsIsMhiu/I3EKq6FZI3wDOJbee5uw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nx/nx-linux-arm64-gnu": {
|
||||
"version": "20.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-20.8.0.tgz",
|
||||
"integrity": "sha512-CiI955Q+XZmBBZ7cQqQg0MhGEFwZIgSpJnjPfWBt3iOYP8aE6nZpNOkmD7O8XcN/nEwwyeCOF8euXqEStwsk8w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nx/nx-linux-arm64-musl": {
|
||||
"version": "20.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-20.8.0.tgz",
|
||||
"integrity": "sha512-Iy9DpvVisxsfNh4gOinmMQ4cLWdBlgvt1wmry1UwvcXg479p1oJQ1Kp1wksUZoWYqrAG8VPZUmkE0f7gjyHTGg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nx/nx-linux-x64-gnu": {
|
||||
"version": "20.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-20.8.0.tgz",
|
||||
"integrity": "sha512-kZrrXXzVSbqwmdTmQ9xL4Jhi0/FSLrePSxYCL9oOM3Rsj0lmo/aC9kz4NBv1ZzuqT7fumpBOnhqiL1QyhOWOeQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nx/nx-linux-x64-musl": {
|
||||
"version": "20.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-20.8.0.tgz",
|
||||
"integrity": "sha512-0l9jEMN8NhULKYCFiDF7QVpMMNG40duya+OF8dH0OzFj52N0zTsvsgLY72TIhslCB/cC74oAzsmWEIiFslscnA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nx/nx-win32-arm64-msvc": {
|
||||
"version": "20.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-20.8.0.tgz",
|
||||
"integrity": "sha512-5miZJmRSwx1jybBsiB3NGocXL9TxGdT2D+dOqR2fsLklpGz0ItEWm8+i8lhDjgOdAr2nFcuQUfQMY57f9FOHrA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nx/nx-win32-x64-msvc": {
|
||||
"version": "20.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-20.8.0.tgz",
|
||||
"integrity": "sha512-0P5r+bDuSNvoWys+6C1/KqGpYlqwSHpigCcyRzR62iZpT3OooZv+nWO06RlURkxMR8LNvYXTSSLvoLkjxqM8uQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
@@ -10856,6 +11056,15 @@
|
||||
"node": "^16.14.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz",
|
||||
"integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/accepts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz",
|
||||
@@ -13196,6 +13405,59 @@
|
||||
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@yarnpkg/parsers": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.2.tgz",
|
||||
"integrity": "sha512-/HcYgtUSiJiot/XWGLOlGxPYUG65+/31V8oqk17vZLW1xlCoR4PampyePljOxY2n8/3jz9+tIFzICsyGujJZoA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"js-yaml": "^3.10.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@yarnpkg/parsers/node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@yarnpkg/parsers/node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@yarnpkg/parsers/node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@zkochan/js-yaml": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.7.tgz",
|
||||
"integrity": "sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/7zip-bin": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
|
||||
@@ -14298,11 +14560,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
||||
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
@@ -18077,6 +18338,18 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enquirer": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
|
||||
"integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-colors": "^4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
@@ -20259,6 +20532,43 @@
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/front-matter": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz",
|
||||
"integrity": "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"js-yaml": "^3.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/front-matter/node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/front-matter/node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/front-matter/node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
@@ -28045,6 +28355,12 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/node-machine-id": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz",
|
||||
"integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||
@@ -28513,6 +28829,209 @@
|
||||
"integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nx": {
|
||||
"version": "20.8.0",
|
||||
"resolved": "https://registry.npmjs.org/nx/-/nx-20.8.0.tgz",
|
||||
"integrity": "sha512-+BN5B5DFBB5WswD8flDDTnr4/bf1VTySXOv60aUAllHqR+KS6deT0p70TTMZF4/A2n/L2UCWDaDro37MGaYozA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "0.2.4",
|
||||
"@yarnpkg/lockfile": "^1.1.0",
|
||||
"@yarnpkg/parsers": "3.0.2",
|
||||
"@zkochan/js-yaml": "0.0.7",
|
||||
"axios": "^1.8.3",
|
||||
"chalk": "^4.1.0",
|
||||
"cli-cursor": "3.1.0",
|
||||
"cli-spinners": "2.6.1",
|
||||
"cliui": "^8.0.1",
|
||||
"dotenv": "~16.4.5",
|
||||
"dotenv-expand": "~11.0.6",
|
||||
"enquirer": "~2.3.6",
|
||||
"figures": "3.2.0",
|
||||
"flat": "^5.0.2",
|
||||
"front-matter": "^4.0.2",
|
||||
"ignore": "^5.0.4",
|
||||
"jest-diff": "^29.4.1",
|
||||
"jsonc-parser": "3.2.0",
|
||||
"lines-and-columns": "2.0.3",
|
||||
"minimatch": "9.0.3",
|
||||
"node-machine-id": "1.1.12",
|
||||
"npm-run-path": "^4.0.1",
|
||||
"open": "^8.4.0",
|
||||
"ora": "5.3.0",
|
||||
"resolve.exports": "2.0.3",
|
||||
"semver": "^7.5.3",
|
||||
"string-width": "^4.2.3",
|
||||
"tar-stream": "~2.2.0",
|
||||
"tmp": "~0.2.1",
|
||||
"tsconfig-paths": "^4.1.2",
|
||||
"tslib": "^2.3.0",
|
||||
"yaml": "^2.6.0",
|
||||
"yargs": "^17.6.2",
|
||||
"yargs-parser": "21.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"nx": "bin/nx.js",
|
||||
"nx-cloud": "bin/nx-cloud.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@nx/nx-darwin-arm64": "20.8.0",
|
||||
"@nx/nx-darwin-x64": "20.8.0",
|
||||
"@nx/nx-freebsd-x64": "20.8.0",
|
||||
"@nx/nx-linux-arm-gnueabihf": "20.8.0",
|
||||
"@nx/nx-linux-arm64-gnu": "20.8.0",
|
||||
"@nx/nx-linux-arm64-musl": "20.8.0",
|
||||
"@nx/nx-linux-x64-gnu": "20.8.0",
|
||||
"@nx/nx-linux-x64-musl": "20.8.0",
|
||||
"@nx/nx-win32-arm64-msvc": "20.8.0",
|
||||
"@nx/nx-win32-x64-msvc": "20.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc-node/register": "^1.8.0",
|
||||
"@swc/core": "^1.3.85"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc-node/register": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/nx/node_modules/cli-spinners": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz",
|
||||
"integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/nx/node_modules/dotenv": {
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/nx/node_modules/dotenv-expand": {
|
||||
"version": "11.0.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz",
|
||||
"integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/nx/node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/nx/node_modules/jsonc-parser": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
|
||||
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nx/node_modules/lines-and-columns": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz",
|
||||
"integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nx/node_modules/minimatch": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
|
||||
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/nx/node_modules/ora": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ora/-/ora-5.3.0.tgz",
|
||||
"integrity": "sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"chalk": "^4.1.0",
|
||||
"cli-cursor": "^3.1.0",
|
||||
"cli-spinners": "^2.5.0",
|
||||
"is-interactive": "^1.0.0",
|
||||
"log-symbols": "^4.0.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wcwidth": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/nx/node_modules/strip-bom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/nx/node_modules/tmp": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
|
||||
"integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/nx/node_modules/tsconfig-paths": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
|
||||
"integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"json5": "^2.2.2",
|
||||
"minimist": "^1.2.6",
|
||||
"strip-bom": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -31294,7 +31813,6 @@
|
||||
"integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"json5": "2.2.3",
|
||||
"lint-staged": "15.4.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"nx": "20.8.0",
|
||||
"postcss": "8.5.1",
|
||||
"postcss-loader": "8.1.1",
|
||||
"prettier": "3.4.2",
|
||||
|
||||
Reference in New Issue
Block a user