1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-08-27 23:31:41 +02:00

Merge branch 'main' into autofill/pm-6426-create-alarms-manager-and-update-usage-of-long-lived-timeouts-rework

This commit is contained in:
Cesar Gonzalez 2024-04-04 14:39:39 -05:00 committed by GitHub
commit bd886cab49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
122 changed files with 11063 additions and 8634 deletions

View File

@ -40,7 +40,10 @@ jobs:
base_uri: https://ast.checkmarx.net/
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
additional_params: --report-format sarif --output-path . ${{ env.INCREMENTAL }}
additional_params: |
--report-format sarif \
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9

View File

@ -604,9 +604,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
* @param sender - The sender of the port message
*/
private getNewVaultItemDetails({ sender }: chrome.runtime.Port) {
// 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
BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" });
void BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" });
}
/**
@ -643,8 +641,8 @@ class OverlayBackground implements OverlayBackgroundInterface {
collectionIds: cipherView.collectionIds,
});
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id });
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
}
/**

View File

@ -16,10 +16,6 @@ import {
logServiceFactory,
LogServiceInitOptions,
} from "../../../platform/background/service-factories/log-service.factory";
import {
stateServiceFactory,
StateServiceInitOptions,
} from "../../../platform/background/service-factories/state-service.factory";
import {
cipherServiceFactory,
CipherServiceInitOptions,
@ -44,7 +40,6 @@ type AutoFillServiceOptions = FactoryOptions;
export type AutoFillServiceInitOptions = AutoFillServiceOptions &
CipherServiceInitOptions &
StateServiceInitOptions &
AutofillSettingsServiceInitOptions &
TotpServiceInitOptions &
EventCollectionServiceInitOptions &
@ -63,7 +58,6 @@ export function autofillServiceFactory(
async () =>
new AutofillService(
await cipherServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),
await autofillSettingsServiceFactory(cache, opts),
await totpServiceFactory(cache, opts),
await eventCollectionServiceFactory(cache, opts),

View File

@ -99,9 +99,7 @@ class AutofillInit implements AutofillInitInterface {
return pageDetails;
}
// 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
chrome.runtime.sendMessage({
void chrome.runtime.sendMessage({
command: "collectPageDetailsResponse",
tab: message.tab,
details: pageDetails,

View File

@ -2,9 +2,7 @@
exports[`AutofillOverlayList initAutofillOverlayList the list of ciphers for an authenticated user creates the view for a list of ciphers 1`] = `
<div
aria-modal="true"
class="overlay-list-container theme_light"
role="dialog"
>
<ul
class="overlay-actions-list"
@ -436,9 +434,7 @@ exports[`AutofillOverlayList initAutofillOverlayList the list of ciphers for an
exports[`AutofillOverlayList initAutofillOverlayList the locked overlay for an unauthenticated user creates the views for the locked overlay 1`] = `
<div
aria-modal="true"
class="overlay-list-container theme_light"
role="dialog"
>
<div
class="locked-overlay overlay-list-message"
@ -490,9 +486,7 @@ exports[`AutofillOverlayList initAutofillOverlayList the locked overlay for an u
exports[`AutofillOverlayList initAutofillOverlayList the overlay with an empty list of ciphers creates the views for the no results overlay 1`] = `
<div
aria-modal="true"
class="overlay-list-container theme_light"
role="dialog"
>
<div
class="no-items overlay-list-message"

View File

@ -312,6 +312,24 @@ describe("AutofillOverlayList", () => {
});
describe("directing user focus into the overlay list", () => {
it("sets ARIA attributes that define the list as a `dialog` to screen reader users", () => {
postWindowMessage(
createInitAutofillOverlayListMessageMock({
authStatus: AuthenticationStatus.Locked,
cipherList: [],
}),
);
const overlayContainerSetAttributeSpy = jest.spyOn(
autofillOverlayList["overlayListContainer"],
"setAttribute",
);
postWindowMessage({ command: "focusOverlayList" });
expect(overlayContainerSetAttributeSpy).toHaveBeenCalledWith("role", "dialog");
expect(overlayContainerSetAttributeSpy).toHaveBeenCalledWith("aria-modal", "true");
});
it("focuses the unlock button element if the user is not authenticated", () => {
postWindowMessage(
createInitAutofillOverlayListMessageMock({

View File

@ -59,8 +59,6 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
this.overlayListContainer = globalThis.document.createElement("div");
this.overlayListContainer.classList.add("overlay-list-container", themeClass);
this.overlayListContainer.setAttribute("role", "dialog");
this.overlayListContainer.setAttribute("aria-modal", "true");
this.resizeObserver.observe(this.overlayListContainer);
this.shadowDom.append(linkElement, this.overlayListContainer);
@ -487,6 +485,9 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
* the first cipher button.
*/
private focusOverlayList() {
this.overlayListContainer.setAttribute("role", "dialog");
this.overlayListContainer.setAttribute("aria-modal", "true");
const unlockButtonElement = this.overlayListContainer.querySelector(
"#unlock-button",
) as HTMLElement;

View File

@ -32,7 +32,6 @@ import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { BrowserApi } from "../../platform/browser/browser-api";
import { BrowserStateService } from "../../platform/services/browser-state.service";
import { AutofillPort } from "../enums/autofill-port.enums";
import AutofillField from "../models/autofill-field";
import AutofillPageDetails from "../models/autofill-page-details";
@ -63,7 +62,6 @@ const mockEquivalentDomains = [
describe("AutofillService", () => {
let autofillService: AutofillService;
const cipherService = mock<CipherService>();
const stateService = mock<BrowserStateService>();
const autofillSettingsService = mock<AutofillSettingsService>();
const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
@ -78,7 +76,6 @@ describe("AutofillService", () => {
beforeEach(() => {
autofillService = new AutofillService(
cipherService,
stateService,
autofillSettingsService,
totpService,
eventCollectionService,

View File

@ -20,7 +20,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
import { BrowserApi } from "../../platform/browser/browser-api";
import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service";
import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window";
import { AutofillPort } from "../enums/autofill-port.enums";
import AutofillField from "../models/autofill-field";
@ -49,7 +48,6 @@ export default class AutofillService implements AutofillServiceInterface {
constructor(
private cipherService: CipherService,
private stateService: BrowserStateService,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private totpService: TotpService,
private eventCollectionService: EventCollectionService,

View File

@ -807,6 +807,36 @@ describe("CollectAutofillContentService", () => {
});
describe("buildAutofillFieldItem", () => {
it("returns a `null` value if the field is a child of a `button[type='submit']`", async () => {
const usernameField = {
labelText: "Username",
id: "username-id",
type: "text",
};
document.body.innerHTML = `
<form>
<div>
<div>
<label for="${usernameField.id}">${usernameField.labelText}</label>
<button type="submit">
<input id="${usernameField.id}" type="${usernameField.type}" />
</button>
</div>
</div>
</form>
`;
const usernameInput = document.getElementById(
usernameField.id,
) as ElementWithOpId<FillableFormFieldElement>;
const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"](
usernameInput,
0,
);
expect(autofillFieldItem).toBeNull();
});
it("returns an existing autofill field item if it exists", async () => {
const index = 0;
const usernameField = {
@ -847,27 +877,6 @@ describe("CollectAutofillContentService", () => {
/>
</form>
`;
document.body.innerHTML = `
<form>
<label for="${usernameField.id}">${usernameField.labelText}</label>
<input
id="${usernameField.id}"
class="${usernameField.classes}"
name="${usernameField.name}"
type="${usernameField.type}"
maxlength="${usernameField.maxLength}"
tabindex="${usernameField.tabIndex}"
title="${usernameField.title}"
autocomplete="${usernameField.autocomplete}"
data-label="${usernameField.dataLabel}"
aria-label="${usernameField.ariaLabel}"
placeholder="${usernameField.placeholder}"
rel="${usernameField.rel}"
value="${usernameField.value}"
data-stripe="${usernameField.dataStripe}"
/>
</form>
`;
const existingFieldData: AutofillField = {
elementNumber: index,
htmlClass: usernameField.classes,

View File

@ -92,9 +92,9 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
const { formElements, formFieldElements } = this.queryAutofillFormAndFieldElements();
const autofillFormsData: Record<string, AutofillForm> =
this.buildAutofillFormsData(formElements);
const autofillFieldsData: AutofillField[] = await this.buildAutofillFieldsData(
formFieldElements as FormFieldElement[],
);
const autofillFieldsData: AutofillField[] = (
await this.buildAutofillFieldsData(formFieldElements as FormFieldElement[])
).filter((field) => !!field);
this.sortAutofillFieldElementsMap();
if (!autofillFieldsData.length) {
@ -333,15 +333,18 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
* Builds an AutofillField object from the given form element. Will only return
* shared field values if the element is a span element. Will not return any label
* values if the element is a hidden input element.
* @param {ElementWithOpId<FormFieldElement>} element
* @param {number} index
* @returns {Promise<AutofillField>}
* @private
*
* @param element - The form field element to build the AutofillField object from
* @param index - The index of the form field element
*/
private buildAutofillFieldItem = async (
element: ElementWithOpId<FormFieldElement>,
index: number,
): Promise<AutofillField> => {
): Promise<AutofillField | null> => {
if (element.closest("button[type='submit']")) {
return null;
}
element.opid = `__${index}`;
const existingAutofillField = this.autofillFieldElements.get(element);

View File

@ -98,7 +98,7 @@ describe("InsertAutofillContentService", () => {
});
afterEach(() => {
jest.resetAllMocks();
jest.restoreAllMocks();
windowLocationSpy.mockRestore();
confirmSpy.mockRestore();
document.body.innerHTML = "";

View File

@ -776,7 +776,6 @@ export default class MainBackground {
this.autofillService = new AutofillService(
this.cipherService,
this.stateService,
this.autofillSettingsService,
this.totpService,
this.eventCollectionService,

View File

@ -172,7 +172,7 @@ describe("Browser Utils Service", () => {
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
it("sends a copy to clipboard message to the desktop application if a user is using the safari browser", async () => {
@ -264,7 +264,7 @@ describe("Browser Utils Service", () => {
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
it("sends a ready from clipboard message to the desktop application if a user is using the safari browser", async () => {

View File

@ -19,6 +19,7 @@ import {
AuthRequestServiceAbstraction,
LoginStrategyServiceAbstraction,
} from "@bitwarden/auth/common";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
@ -47,6 +48,7 @@ import {
UserNotificationSettingsService,
UserNotificationSettingsServiceAbstraction,
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
@ -86,7 +88,8 @@ import { DialogService } from "@bitwarden/components";
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
import { UnauthGuardService } from "../../auth/popup/services";
import { AutofillService } from "../../autofill/services/abstractions/autofill.service";
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
import AutofillService from "../../autofill/services/autofill.service";
import MainBackground from "../../background/main.background";
import { Account } from "../../models/account";
import { BrowserApi } from "../../platform/browser/browser-api";
@ -314,10 +317,22 @@ const safeProviders: SafeProvider[] = [
useClass: BrowserLocalStorageService,
deps: [],
}),
safeProvider({
provide: AutofillServiceAbstraction,
useExisting: AutofillService,
}),
safeProvider({
provide: AutofillService,
useFactory: getBgService<AutofillService>("autofillService"),
deps: [],
deps: [
CipherService,
AutofillSettingsServiceAbstraction,
TotpService,
EventCollectionServiceAbstraction,
LogService,
DomainSettingsService,
UserVerificationService,
BillingAccountProfileStateService,
],
}),
safeProvider({
provide: VaultExportServiceAbstraction,

View File

@ -30,9 +30,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "1.1.2"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
@ -62,15 +62,15 @@ dependencies = [
[[package]]
name = "autocfg"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
[[package]]
name = "backtrace"
version = "0.3.69"
version = "0.3.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
dependencies = [
"addr2line",
"cc",
@ -95,9 +95,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.1"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
[[package]]
name = "block"
@ -123,12 +123,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bytecount"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205"
[[package]]
name = "cbc"
version = "0.1.2"
@ -140,18 +134,15 @@ dependencies = [
[[package]]
name = "cc"
version = "1.0.83"
version = "1.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
[[package]]
name = "cfg-expr"
version = "0.15.5"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3"
checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d"
dependencies = [
"smallvec",
"target-lexicon",
@ -163,6 +154,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cipher"
version = "0.4.4"
@ -219,9 +216,9 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "cpufeatures"
version = "0.2.11"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0"
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
dependencies = [
"libc",
]
@ -238,19 +235,19 @@ dependencies = [
[[package]]
name = "ctor"
version = "0.2.5"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e366bff8cd32dd8754b0991fb66b279dc48f598c3a18914852a6673deef583"
checksum = "ad291aa74992b9b7a7e88c38acbbf6ad7e107f1d90ee8775b7bc1fc3394f485c"
dependencies = [
"quote",
"syn 2.0.38",
"syn",
]
[[package]]
name = "cxx"
version = "1.0.110"
version = "1.0.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7129e341034ecb940c9072817cd9007974ea696844fc4dd582dc1653a7fbe2e8"
checksum = "ff4dc7287237dd438b926a81a1a5605dad33d286870e5eee2db17bf2bcd9e92a"
dependencies = [
"cc",
"cxxbridge-flags",
@ -260,9 +257,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.110"
version = "1.0.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2a24f3f5f8eed71936f21e570436f024f5c2e25628f7496aa7ccd03b90109d5"
checksum = "f47c6c8ad7c1a10d3ef0fe3ff6733f4db0d78f08ef0b13121543163ef327058b"
dependencies = [
"cc",
"codespan-reporting",
@ -270,35 +267,35 @@ dependencies = [
"proc-macro2",
"quote",
"scratch",
"syn 2.0.38",
"syn",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.110"
version = "1.0.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06fdd177fc61050d63f67f5bd6351fac6ab5526694ea8e359cd9cd3b75857f44"
checksum = "701a1ac7a697e249cdd8dc026d7a7dafbfd0dbcd8bd24ec55889f2bc13dd6287"
[[package]]
name = "cxxbridge-macro"
version = "1.0.110"
version = "1.0.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "587663dd5fb3d10932c8aecfe7c844db1bcf0aee93eeab08fac13dc1212c2e7f"
checksum = "b404f596046b0bb2d903a9c786b875a126261b52b7c3a64bbb66382c41c771df"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn",
]
[[package]]
name = "derive-new"
version = "0.5.9"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535"
checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@ -378,9 +375,9 @@ checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b"
[[package]]
name = "fastrand"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
[[package]]
name = "fixedbitset"
@ -396,24 +393,24 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "futures-channel"
version = "0.3.29"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.29"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-executor"
version = "0.3.29"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc"
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
dependencies = [
"futures-core",
"futures-task",
@ -422,32 +419,32 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.29"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-macro"
version = "0.3.29"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn",
]
[[package]]
name = "futures-task"
version = "0.3.29"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-util"
version = "0.3.29"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-core",
"futures-macro",
@ -479,9 +476,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.10"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if",
"libc",
@ -490,9 +487,9 @@ dependencies = [
[[package]]
name = "gimli"
version = "0.28.0"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "gio"
@ -527,11 +524,11 @@ dependencies = [
[[package]]
name = "glib"
version = "0.19.2"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab9e86540b5d8402e905ad4ce7d6aa544092131ab564f3102175af176b90a053"
checksum = "01e191cc1af1f35b9699213107068cd3fe05d9816275ac118dc785a0dd8faebf"
dependencies = [
"bitflags 2.4.1",
"bitflags 2.5.0",
"futures-channel",
"futures-core",
"futures-executor",
@ -549,15 +546,15 @@ dependencies = [
[[package]]
name = "glib-macros"
version = "0.19.2"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f5897ca27a83e4cdc7b4666850bade0a2e73e17689aabafcc9acddad9d823b8"
checksum = "9972bb91643d589c889654693a4f1d07697fdcb5d104b5c44fb68649ba1bf68d"
dependencies = [
"heck",
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.38",
"syn",
]
[[package]]
@ -583,27 +580,36 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.14.2"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
name = "heck"
version = "0.4.1"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.3"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "home"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys",
]
[[package]]
name = "indexmap"
version = "2.0.2"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown",
@ -640,17 +646,11 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.152"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libloading"
@ -699,9 +699,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
version = "0.4.12"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "lock_api"
@ -715,9 +715,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.20"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "malloc_buf"
@ -730,18 +730,9 @@ dependencies = [
[[package]]
name = "memchr"
version = "2.7.1"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "memoffset"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
dependencies = [
"autocfg",
]
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "minimal-lexical"
@ -751,9 +742,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.7.1"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
dependencies = [
"adler",
]
@ -764,7 +755,7 @@ version = "2.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a63d0570e4c3e0daf7a8d380563610e159f538e20448d6c911337246f40e84"
dependencies = [
"bitflags 2.4.1",
"bitflags 2.5.0",
"ctor",
"napi-derive",
"napi-sys",
@ -789,14 +780,14 @@ dependencies = [
"napi-derive-backend",
"proc-macro2",
"quote",
"syn 2.0.38",
"syn",
]
[[package]]
name = "napi-derive-backend"
version = "1.0.62"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f785a8b8d7b83e925f5aa6d2ae3c159d17fe137ac368dc185bef410e7acdaeb4"
checksum = "ce5126b64f6ad9e28e30e6d15213dd378626b38f556454afebc42f7f02a90902"
dependencies = [
"convert_case",
"once_cell",
@ -804,7 +795,7 @@ dependencies = [
"quote",
"regex",
"semver",
"syn 2.0.38",
"syn",
]
[[package]]
@ -818,15 +809,14 @@ dependencies = [
[[package]]
name = "nix"
version = "0.26.4"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.5.0",
"cfg-if",
"cfg_aliases",
"libc",
"memoffset",
"pin-utils",
]
[[package]]
@ -880,9 +870,9 @@ dependencies = [
[[package]]
name = "object"
version = "0.32.1"
version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
dependencies = [
"memchr",
]
@ -938,9 +928,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.13"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pin-utils"
@ -950,9 +940,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.27"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "ppv-lite86"
@ -971,27 +961,27 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.69"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quick-xml"
version = "0.30.0"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.33"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
@ -1037,9 +1027,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.2"
version = "1.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
dependencies = [
"aho-corasick",
"memchr",
@ -1049,9 +1039,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.3"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
dependencies = [
"aho-corasick",
"memchr",
@ -1060,9 +1050,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
[[package]]
name = "retry"
@ -1081,11 +1071,11 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustix"
version = "0.38.28"
version = "0.38.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
dependencies = [
"bitflags 2.4.1",
"bitflags 2.5.0",
"errno",
"libc",
"linux-raw-sys",
@ -1135,35 +1125,35 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.20"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
[[package]]
name = "serde"
version = "1.0.190"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.190"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn",
]
[[package]]
name = "serde_spanned"
version = "0.6.4"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80"
checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
dependencies = [
"serde",
]
@ -1190,26 +1180,15 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.13.1"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "syn"
version = "1.0.109"
version = "2.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687"
dependencies = [
"proc-macro2",
"quote",
@ -1218,9 +1197,9 @@ dependencies = [
[[package]]
name = "system-deps"
version = "6.1.2"
version = "6.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94af52f9402f94aac4948a2518b43359be8d9ce6cd9efc1c4de3b2f7b7e897d6"
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
dependencies = [
"cfg-expr",
"heck",
@ -1231,28 +1210,27 @@ dependencies = [
[[package]]
name = "target-lexicon"
version = "0.12.12"
version = "0.12.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a"
checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
[[package]]
name = "tempfile"
version = "3.9.0"
version = "3.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall",
"rustix",
"windows-sys",
]
[[package]]
name = "termcolor"
version = "1.3.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
@ -1274,14 +1252,14 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn",
]
[[package]]
name = "tokio"
version = "1.36.0"
version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
dependencies = [
"backtrace",
"num_cpus",
@ -1290,14 +1268,14 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.6"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc"
checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.20.7",
"toml_edit 0.22.9",
]
[[package]]
@ -1309,19 +1287,6 @@ dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.20.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "toml_edit"
version = "0.21.1"
@ -1330,18 +1295,31 @@ checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
"winnow 0.5.40",
]
[[package]]
name = "toml_edit"
version = "0.22.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow 0.6.5",
]
[[package]]
name = "tree_magic_mini"
version = "3.0.3"
version = "3.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91adfd0607cacf6e4babdb870e9bec4037c1c4b151cfd279ccefc5e0c7feaa6d"
checksum = "77ee137597cdb361b55a4746983e4ac1b35ab6024396a419944ad473bb915265"
dependencies = [
"bytecount",
"fnv",
"lazy_static",
"home",
"memchr",
"nom",
"once_cell",
"petgraph",
@ -1361,9 +1339,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-segmentation"
version = "1.10.1"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-width"
@ -1373,9 +1351,9 @@ checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "version-compare"
version = "0.1.1"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
[[package]]
name = "version_check"
@ -1391,13 +1369,13 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wayland-backend"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19152ddd73f45f024ed4534d9ca2594e0ef252c1847695255dae47f34df9fbe4"
checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40"
dependencies = [
"cc",
"downcast-rs",
"nix",
"rustix",
"scoped-tls",
"smallvec",
"wayland-sys",
@ -1405,23 +1383,23 @@ dependencies = [
[[package]]
name = "wayland-client"
version = "0.31.1"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ca7d52347346f5473bf2f56705f360e8440873052e575e55890c4fa57843ed3"
checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f"
dependencies = [
"bitflags 2.4.1",
"nix",
"bitflags 2.5.0",
"rustix",
"wayland-backend",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols"
version = "0.31.0"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e253d7107ba913923dc253967f35e8561a3c65f914543e46843c88ddd729e21c"
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
dependencies = [
"bitflags 2.4.1",
"bitflags 2.5.0",
"wayland-backend",
"wayland-client",
"wayland-scanner",
@ -1433,7 +1411,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6"
dependencies = [
"bitflags 2.4.1",
"bitflags 2.5.0",
"wayland-backend",
"wayland-client",
"wayland-protocols",
@ -1442,9 +1420,9 @@ dependencies = [
[[package]]
name = "wayland-scanner"
version = "0.31.0"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb8e28403665c9f9513202b7e1ed71ec56fde5c107816843fb14057910b2c09c"
checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283"
dependencies = [
"proc-macro2",
"quick-xml",
@ -1653,18 +1631,27 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
[[package]]
name = "winnow"
version = "0.5.17"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8"
dependencies = [
"memchr",
]
[[package]]
name = "wl-clipboard-rs"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57af79e973eadf08627115c73847392e6b766856ab8e3844a59245354b23d2fa"
checksum = "12b41773911497b18ca8553c3daaf8ec9fe9819caf93d451d3055f69de028adb"
dependencies = [
"derive-new",
"libc",

View File

@ -19,18 +19,16 @@
},
"devDependencies": {
"@tsconfig/node16": "1.0.4",
"@types/node": "18.19.19",
"@types/node": "18.19.29",
"@types/node-ipc": "9.2.0",
"typescript": "4.7.4"
}
},
"../../../libs/common": {
"name": "@bitwarden/common",
"version": "0.0.0",
"license": "GPL-3.0"
},
"../../../libs/node": {
"name": "@bitwarden/node",
"version": "0.0.0",
"license": "GPL-3.0",
"dependencies": {
@ -57,9 +55,9 @@
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"engines": {
"node": ">=6.0.0"
}
@ -79,9 +77,9 @@
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA=="
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.10.tgz",
"integrity": "sha512-PiaIWIoPvO6qm6t114ropMCagj6YAF24j9OkCA2mJDXFnlionEwhsBCJ8yek4aib575BI3OkART/90WsgHgLWw=="
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
@ -99,9 +97,9 @@
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="
},
"node_modules/@types/node": {
"version": "18.19.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.19.tgz",
"integrity": "sha512-qqV6hSy9zACEhQUy5CEGeuXAZN0fNjqLWRIvOXOwdFYhFoKBiY08VKR5kgchr90+TitLVhpUEb54hk4bYaArUw==",
"version": "18.19.29",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.29.tgz",
"integrity": "sha512-5pAX7ggTmWZdhUrhRWLPf+5oM7F80bcKVCBbr0zwEkTNzTJL2CWQjznpFgHYy6GrzkYi2Yjy7DHKoynFxqPV8g==",
"dependencies": {
"undici-types": "~5.26.4"
}
@ -116,9 +114,9 @@
}
},
"node_modules/acorn": {
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"bin": {
"acorn": "bin/acorn"
},
@ -127,9 +125,9 @@
}
},
"node_modules/acorn-walk": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
"integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
"engines": {
"node": ">=0.4.0"
}
@ -217,9 +215,9 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"engines": {
"node": ">=6"
}

View File

@ -24,7 +24,7 @@
},
"devDependencies": {
"@tsconfig/node16": "1.0.4",
"@types/node": "18.19.19",
"@types/node": "18.19.29",
"@types/node-ipc": "9.2.0",
"typescript": "4.7.4"
},

View File

@ -9,19 +9,607 @@
"version": "2024.3.2",
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/desktop-native": "file:../desktop_native"
"@bitwarden/desktop-native": "file:../desktop_native",
"argon2": "0.31.0"
}
},
"../desktop_native": {
"version": "0.1.0",
"license": "GPL-3.0",
"devDependencies": {
"@napi-rs/cli": "2.14.8"
"@napi-rs/cli": "2.16.2"
}
},
"node_modules/@bitwarden/desktop-native": {
"resolved": "../desktop_native",
"link": true
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@phc/format": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
"integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==",
"engines": {
"node": ">=10"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
},
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/argon2": {
"version": "0.31.0",
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.31.0.tgz",
"integrity": "sha512-r56NWwlE3tjD/FIqL1T+V4Ka+Mb5yMF35w1YWHpwpEjeONXBUbxmjhWkWqY63mse8lpcZ+ZZIGpKL+s+qXhyfg==",
"hasInstallScript": true,
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
"@phc/format": "^1.0.0",
"node-addon-api": "^7.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"engines": {
"node": ">=10"
}
},
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"engines": {
"node": ">=8"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"dependencies": {
"minipass": "^3.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/fs-minipass/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/make-dir/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/node-addon-api": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz",
"integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==",
"engines": {
"node": "^16 || ^18 || >= 20"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/semver": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
},
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^5.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
}
}

View File

@ -3,7 +3,7 @@
<a routerLink="." class="tw-m-5 tw-mt-7 tw-block" [appA11yTitle]="'adminConsole' | i18n">
<bit-icon [icon]="logo"></bit-icon>
</a>
<org-switcher [filter]="orgFilter"></org-switcher>
<org-switcher [filter]="orgFilter" [hideNewButton]="hideNewOrgButton$ | async"></org-switcher>
<bit-nav-item
icon="bwi-collection"
@ -105,6 +105,8 @@
*ngIf="organization.canManageScim"
></bit-nav-item>
</bit-nav-group>
<app-toggle-width></app-toggle-width>
</nav>
<ng-container *ngIf="organization$ | async as organization">

View File

@ -15,6 +15,8 @@ import {
getOrganizationById,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@ -23,6 +25,7 @@ import { BannerModule, IconModule, LayoutComponent, NavigationModule } from "@bi
import { PaymentMethodWarningsModule } from "../../../billing/shared";
import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher.component";
import { ToggleWidthComponent } from "../../../layouts/toggle-width.component";
import { AdminConsoleLogo } from "../../icons/admin-console-logo";
@Component({
@ -39,6 +42,7 @@ import { AdminConsoleLogo } from "../../icons/admin-console-logo";
OrgSwitcherComponent,
BannerModule,
PaymentMethodWarningsModule,
ToggleWidthComponent,
],
})
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
@ -48,6 +52,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
organization$: Observable<Organization>;
showPaymentAndHistory$: Observable<boolean>;
hideNewOrgButton$: Observable<boolean>;
private _destroy = new Subject<void>();
@ -61,6 +66,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
private organizationService: OrganizationService,
private platformUtilsService: PlatformUtilsService,
private configService: ConfigService,
private policyService: PolicyService,
) {}
async ngOnInit() {
@ -85,6 +91,8 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
org?.canEditPaymentMethods,
),
);
this.hideNewOrgButton$ = this.policyService.policyAppliesToActiveUser$(PolicyType.SingleOrg);
}
ngOnDestroy() {

View File

@ -124,10 +124,7 @@ export class PremiumComponent implements OnInit {
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
this.platformUtilsService.showToast("success", null, this.i18nService.t("premiumUpdated"));
this.messagingService.send("purchasedPremium");
// 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.router.navigate(["/settings/subscription/user-subscription"]);
await this.router.navigate(["/settings/subscription/user-subscription"]);
}
get additionalStorageTotal(): number {

View File

@ -46,7 +46,6 @@ export class OrgSwitcherComponent {
/**
* Visibility of the New Organization button
* (Temporary; will be removed when ability to create organizations is added to SM.)
*/
@Input()
hideNewButton = false;

View File

@ -0,0 +1,33 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { NavigationModule } from "@bitwarden/components";
@Component({
selector: "app-toggle-width",
template: `<bit-nav-item
text="Toggle Width"
icon="bwi-bug"
*ngIf="isDev"
class="tw-absolute tw-bottom-0 tw-w-full"
(click)="toggleWidth()"
></bit-nav-item>`,
standalone: true,
imports: [CommonModule, NavigationModule],
})
export class ToggleWidthComponent {
protected isDev: boolean;
constructor(platformUtilsService: PlatformUtilsService) {
this.isDev = platformUtilsService.isDev();
}
protected toggleWidth() {
if (document.body.style.minWidth === "unset") {
document.body.style.minWidth = "";
} else {
document.body.style.minWidth = "unset";
}
}
}

View File

@ -19,7 +19,7 @@
<bit-nav-item
[text]="'subscription' | i18n"
route="settings/subscription"
*ngIf="!hideSubscription"
*ngIf="showSubscription$ | async"
></bit-nav-item>
<bit-nav-item [text]="'domainRules' | i18n" route="settings/domain-rules"></bit-nav-item>
<bit-nav-item
@ -29,9 +29,11 @@
<bit-nav-item
[text]="'sponsoredFamilies' | i18n"
route="settings/sponsored-families"
*ngIf="hasFamilySponsorshipAvailable"
*ngIf="hasFamilySponsorshipAvailable$ | async"
></bit-nav-item>
</bit-nav-group>
<app-toggle-width></app-toggle-width>
</nav>
<app-payment-method-warnings
*ngIf="showPaymentMethodWarningBanners$ | async"

View File

@ -1,14 +1,13 @@
import { CommonModule } from "@angular/common";
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import { RouterModule } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { Observable, combineLatest, concatMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@ -17,8 +16,7 @@ import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/compon
import { PaymentMethodWarningsModule } from "../billing/shared";
import { PasswordManagerLogo } from "./password-manager-logo";
const BroadcasterSubscriptionId = "UserLayoutComponent";
import { ToggleWidthComponent } from "./toggle-width.component";
@Component({
selector: "app-user-layout",
@ -32,12 +30,13 @@ const BroadcasterSubscriptionId = "UserLayoutComponent";
IconModule,
NavigationModule,
PaymentMethodWarningsModule,
ToggleWidthComponent,
],
})
export class UserLayoutComponent implements OnInit, OnDestroy {
export class UserLayoutComponent implements OnInit {
protected readonly logo = PasswordManagerLogo;
hasFamilySponsorshipAvailable: boolean;
hideSubscription: boolean;
protected hasFamilySponsorshipAvailable$: Observable<boolean>;
protected showSubscription$: Observable<boolean>;
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
FeatureFlag.ShowPaymentMethodWarningBanners,
@ -45,8 +44,6 @@ export class UserLayoutComponent implements OnInit, OnDestroy {
);
constructor(
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService,
private apiService: ApiService,
@ -58,43 +55,28 @@ export class UserLayoutComponent implements OnInit, OnDestroy {
async ngOnInit() {
document.body.classList.remove("layout_frontend");
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
// 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.ngZone.run(async () => {
switch (message.command) {
case "purchasedPremium":
await this.load();
break;
default:
}
});
});
await this.syncService.fullSync(false);
await this.load();
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
this.hasFamilySponsorshipAvailable$ = this.organizationService.canManageSponsorships$;
async load() {
const hasPremiumPersonally = await firstValueFrom(
// We want to hide the subscription menu for organizations that provide premium.
// Except if the user has premium personally or has a billing history.
this.showSubscription$ = combineLatest([
this.billingAccountProfileStateService.hasPremiumPersonally$,
);
const hasPremiumFromOrg = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$,
);
const selfHosted = this.platformUtilsService.isSelfHost();
]).pipe(
concatMap(async ([hasPremiumPersonally, hasPremiumFromOrg]) => {
const isCloud = !this.platformUtilsService.isSelfHost();
this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships();
let billing = null;
if (!selfHosted) {
// TODO: We should remove the need to call this!
billing = await this.apiService.getUserBillingHistory();
}
this.hideSubscription =
!hasPremiumPersonally && hasPremiumFromOrg && (selfHosted || billing?.hasNoHistory);
let billing = null;
if (isCloud) {
// TODO: We should remove the need to call this!
billing = await this.apiService.getUserBillingHistory();
}
const cloudAndBillingHistory = isCloud && !billing?.hasNoHistory;
return hasPremiumPersonally || !hasPremiumFromOrg || cloudAndBillingHistory;
}),
);
}
}

View File

@ -1,68 +0,0 @@
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
const BroadcasterSubscriptionId = "SettingsComponent";
@Component({
selector: "app-settings",
templateUrl: "settings.component.html",
})
export class SettingsComponent implements OnInit, OnDestroy {
premium: boolean;
selfHosted: boolean;
hasFamilySponsorshipAvailable: boolean;
hideSubscription: boolean;
constructor(
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService,
private apiService: ApiService,
private billingAccountProfileStateServiceAbstraction: BillingAccountProfileStateService,
) {}
async ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
// 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.ngZone.run(async () => {
switch (message.command) {
case "purchasedPremium":
await this.load();
break;
default:
}
});
});
this.selfHosted = await this.platformUtilsService.isSelfHost();
await this.load();
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async load() {
this.premium = await firstValueFrom(
this.billingAccountProfileStateServiceAbstraction.hasPremiumPersonally$,
);
this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships();
const hasPremiumFromOrg = await firstValueFrom(
this.billingAccountProfileStateServiceAbstraction.hasPremiumFromAnyOrganization$,
);
let billing = null;
if (!this.selfHosted) {
billing = await this.apiService.getUserBillingHistory();
}
this.hideSubscription =
!this.premium && hasPremiumFromOrg && (this.selfHosted || billing?.hasNoHistory);
}
}

View File

@ -45,6 +45,7 @@ export class VaultItemsComponent {
@Input() showBulkAddToCollections = false;
@Input() showPermissionsColumn = false;
@Input() viewingOrgVault: boolean;
@Input({ required: true }) flexibleCollectionsV1Enabled = false;
private _ciphers?: CipherView[] = [];
@Input() get ciphers(): CipherView[] {
@ -101,7 +102,7 @@ export class VaultItemsComponent {
}
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
return collection.canEdit(organization);
return collection.canEdit(organization, this.flexibleCollectionsV1Enabled);
}
protected canDeleteCollection(collection: CollectionView): boolean {

View File

@ -31,10 +31,11 @@ export class CollectionAdminView extends CollectionView {
this.assigned = response.assigned;
}
override canEdit(org: Organization): boolean {
override canEdit(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
return org?.flexibleCollections
? org?.canEditAnyCollection || this.manage
: org?.canEditAnyCollection || (org?.canEditAssignedCollections && this.assigned);
? org?.canEditAnyCollection(flexibleCollectionsV1Enabled) || this.manage
: org?.canEditAnyCollection(flexibleCollectionsV1Enabled) ||
(org?.canEditAssignedCollections && this.assigned);
}
override canDelete(org: Organization): boolean {

View File

@ -1,8 +1,11 @@
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -49,6 +52,11 @@ export class BulkDeleteDialogComponent {
organizations: Organization[];
collections: CollectionView[];
private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
false,
);
constructor(
@Inject(DIALOG_DATA) params: BulkDeleteDialogParams,
private dialogRef: DialogRef<BulkDeleteDialogResult>,
@ -57,6 +65,7 @@ export class BulkDeleteDialogComponent {
private i18nService: I18nService,
private apiService: ApiService,
private collectionService: CollectionService,
private configService: ConfigService,
) {
this.cipherIds = params.cipherIds ?? [];
this.permanent = params.permanent;
@ -72,7 +81,12 @@ export class BulkDeleteDialogComponent {
protected submit = async () => {
const deletePromises: Promise<void>[] = [];
if (this.cipherIds.length) {
if (!this.organization || !this.organization.canEditAnyCollection) {
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
if (
!this.organization ||
!this.organization.canEditAllCiphers(flexibleCollectionsV1Enabled)
) {
deletePromises.push(this.deleteCiphers());
} else {
deletePromises.push(this.deleteCiphersAdmin());
@ -104,7 +118,8 @@ export class BulkDeleteDialogComponent {
};
private async deleteCiphers(): Promise<any> {
const asAdmin = this.organization?.canEditAnyCollection;
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
const asAdmin = this.organization?.canEditAllCiphers(flexibleCollectionsV1Enabled);
if (this.permanent) {
await this.cipherService.deleteManyWithServer(this.cipherIds, asAdmin);
} else {

View File

@ -1,6 +1,16 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core";
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnInit,
Output,
} from "@angular/core";
import { firstValueFrom } from "rxjs";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
@ -17,7 +27,7 @@ import {
templateUrl: "./vault-header.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class VaultHeaderComponent {
export class VaultHeaderComponent implements OnInit {
protected Unassigned = Unassigned;
protected All = All;
protected CollectionDialogTabType = CollectionDialogTabType;
@ -55,7 +65,18 @@ export class VaultHeaderComponent {
/** Emits an event when the delete collection button is clicked in the header */
@Output() onDeleteCollection = new EventEmitter<void>();
constructor(private i18nService: I18nService) {}
private flexibleCollectionsV1Enabled = false;
constructor(
private i18nService: I18nService,
private configService: ConfigService,
) {}
async ngOnInit() {
this.flexibleCollectionsV1Enabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
);
}
/**
* The id of the organization that is currently being filtered on.
@ -137,7 +158,7 @@ export class VaultHeaderComponent {
const organization = this.organizations.find(
(o) => o.id === this.collection?.node.organizationId,
);
return this.collection.node.canEdit(organization);
return this.collection.node.canEdit(organization, this.flexibleCollectionsV1Enabled);
}
async editCollection(tab: CollectionDialogTabType): Promise<void> {

View File

@ -50,6 +50,7 @@
[cloneableOrganizationCiphers]="false"
[showAdminActions]="false"
(onEvent)="onVaultItemsEvent($event)"
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled$ | async"
>
</app-vault-items>
<div

View File

@ -39,6 +39,7 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -144,6 +145,10 @@ export class VaultComponent implements OnInit, OnDestroy {
protected selectedCollection: TreeNode<CollectionView> | undefined;
protected canCreateCollections = false;
protected currentSearchText$: Observable<string>;
protected flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
false,
);
private searchText$ = new Subject<string>();
private refresh$ = new BehaviorSubject<void>(null);

View File

@ -1,8 +1,11 @@
import { Component } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -21,10 +24,12 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from "../individual-
selector: "app-org-vault-attachments",
templateUrl: "../individual-vault/attachments.component.html",
})
export class AttachmentsComponent extends BaseAttachmentsComponent {
export class AttachmentsComponent extends BaseAttachmentsComponent implements OnInit {
viewOnly = false;
organization: Organization;
private flexibleCollectionsV1Enabled = false;
constructor(
cipherService: CipherService,
i18nService: I18nService,
@ -36,6 +41,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
fileDownloadService: FileDownloadService,
dialogService: DialogService,
billingAccountProfileStateService: BillingAccountProfileStateService,
private configService: ConfigService,
) {
super(
cipherService,
@ -51,14 +57,24 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
);
}
async ngOnInit() {
await super.ngOnInit();
this.flexibleCollectionsV1Enabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1, false),
);
}
protected async reupload(attachment: AttachmentView) {
if (this.organization.canEditAnyCollection && this.showFixOldAttachments(attachment)) {
if (
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
this.showFixOldAttachments(attachment)
) {
await super.reuploadCipherAttachment(attachment, true);
}
}
protected async loadCipher() {
if (!this.organization.canEditAnyCollection) {
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
return await super.loadCipher();
}
const response = await this.apiService.getCipherAdmin(this.cipherId);
@ -69,18 +85,21 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
return this.cipherService.saveAttachmentWithServer(
this.cipherDomain,
file,
this.organization.canEditAnyCollection,
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled),
);
}
protected deleteCipherAttachment(attachmentId: string) {
if (!this.organization.canEditAnyCollection) {
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
return super.deleteCipherAttachment(attachmentId);
}
return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId);
}
protected showFixOldAttachments(attachment: AttachmentView) {
return attachment.key == null && this.organization.canEditAnyCollection;
return (
attachment.key == null &&
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)
);
}
}

View File

@ -1,10 +1,12 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProductType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
@ -22,7 +24,7 @@ import {
selector: "app-org-vault-header",
templateUrl: "./vault-header.component.html",
})
export class VaultHeaderComponent {
export class VaultHeaderComponent implements OnInit {
protected All = All;
protected Unassigned = Unassigned;
@ -56,14 +58,23 @@ export class VaultHeaderComponent {
protected CollectionDialogTabType = CollectionDialogTabType;
protected organizations$ = this.organizationService.organizations$;
private flexibleCollectionsV1Enabled = false;
constructor(
private organizationService: OrganizationService,
private i18nService: I18nService,
private dialogService: DialogService,
private collectionAdminService: CollectionAdminService,
private router: Router,
private configService: ConfigService,
) {}
async ngOnInit() {
this.flexibleCollectionsV1Enabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
);
}
get title() {
const headerType = this.organization?.flexibleCollections
? this.i18nService.t("collections").toLowerCase()
@ -153,7 +164,7 @@ export class VaultHeaderComponent {
}
// Otherwise, check if we can edit the specified collection
return this.collection.node.canEdit(this.organization);
return this.collection.node.canEdit(this.organization, this.flexibleCollectionsV1Enabled);
}
addCipher() {

View File

@ -54,6 +54,7 @@
[showBulkEditCollectionAccess]="organization?.flexibleCollections"
[showBulkAddToCollections]="organization?.flexibleCollections"
[viewingOrgVault]="true"
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled"
>
</app-vault-items>
<ng-container *ngIf="!flexibleCollectionsV1Enabled">
@ -98,7 +99,10 @@
</bit-no-items>
<collection-access-restricted
*ngIf="showCollectionAccessRestricted"
[canEdit]="selectedCollection != null && selectedCollection.node.canEdit(organization)"
[canEdit]="
selectedCollection != null &&
selectedCollection.node.canEdit(organization, flexibleCollectionsV1Enabled)
"
(editInfoClicked)="editCollection(selectedCollection.node, CollectionDialogTabType.Info)"
>
</collection-access-restricted>

View File

@ -213,7 +213,7 @@ export class VaultComponent implements OnInit, OnDestroy {
switchMap(async ([organization]) => {
this.organization = organization;
if (!organization.canUseAdminCollections) {
if (!organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) {
await this.syncService.fullSync(false);
}
@ -322,7 +322,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
} else {
// Pre-flexible collections logic, to be removed after flexible collections is fully released
if (organization.canEditAnyCollection) {
if (organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
} else {
ciphers = (await this.cipherService.getAllDecrypted()).filter(
@ -407,7 +407,8 @@ export class VaultComponent implements OnInit, OnDestroy {
]).pipe(
map(([filter, collection, organization]) => {
return (
(filter.collectionId === Unassigned && !organization.canUseAdminCollections) ||
(filter.collectionId === Unassigned &&
!organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) ||
(!organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
collection != undefined &&
!collection.node.assigned)
@ -453,11 +454,12 @@ export class VaultComponent implements OnInit, OnDestroy {
map(([filter, collection, organization]) => {
return (
// Filtering by unassigned, show message if not admin
(filter.collectionId === Unassigned && !organization.canUseAdminCollections) ||
(filter.collectionId === Unassigned &&
!organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) ||
// Filtering by a collection, so show message if user is not assigned
(collection != undefined &&
!collection.node.assigned &&
!organization.canUseAdminCollections)
!organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled))
);
}),
shareReplay({ refCount: true, bufferSize: 1 }),
@ -480,7 +482,7 @@ export class VaultComponent implements OnInit, OnDestroy {
(await firstValueFrom(allCipherMap$))[cipherId] != undefined;
} else {
canEditCipher =
organization.canUseAdminCollections ||
organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled) ||
(await this.cipherService.get(cipherId)) != null;
}
@ -856,7 +858,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
try {
const asAdmin = this.organization?.canEditAnyCollection;
const asAdmin = this.organization?.canEditAnyCollection(this.flexibleCollectionsV1Enabled);
await this.cipherService.restoreWithServer(c.id, asAdmin);
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem"));
this.refresh();
@ -1143,7 +1145,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
protected deleteCipherWithServer(id: string, permanent: boolean) {
const asAdmin = this.organization?.canEditAnyCollection;
const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
return permanent
? this.cipherService.deleteWithServer(id, asAdmin)
: this.cipherService.softDeleteWithServer(id, asAdmin);

View File

@ -27,6 +27,8 @@
route="settings"
*ngIf="showSettingsTab"
></bit-nav-item>
<app-toggle-width></app-toggle-width>
</nav>
<app-payment-method-warnings
*ngIf="showPaymentMethodWarningBanners$ | async"

View File

@ -10,6 +10,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components";
import { ProviderPortalLogo } from "@bitwarden/web-vault/app/admin-console/icons/provider-portal-logo";
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
import { ToggleWidthComponent } from "@bitwarden/web-vault/app/layouts/toggle-width.component";
@Component({
selector: "providers-layout",
@ -23,6 +24,7 @@ import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/sh
IconModule,
NavigationModule,
PaymentMethodWarningsModule,
ToggleWidthComponent,
],
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil

File diff suppressed because it is too large Load Diff

View File

@ -2,13 +2,20 @@ import { NgModule } from "@angular/core";
import { LayoutComponent as BitLayoutComponent, NavigationModule } from "@bitwarden/components";
import { OrgSwitcherComponent } from "@bitwarden/web-vault/app/layouts/org-switcher/org-switcher.component";
import { ToggleWidthComponent } from "@bitwarden/web-vault/app/layouts/toggle-width.component";
import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module";
import { LayoutComponent } from "./layout.component";
import { NavigationComponent } from "./navigation.component";
@NgModule({
imports: [SharedModule, NavigationModule, BitLayoutComponent, OrgSwitcherComponent],
imports: [
SharedModule,
NavigationModule,
BitLayoutComponent,
OrgSwitcherComponent,
ToggleWidthComponent,
],
declarations: [LayoutComponent, NavigationComponent],
})
export class LayoutModule {}

View File

@ -41,4 +41,6 @@
[relativeTo]="route.parent"
></bit-nav-item>
</bit-nav-group>
<app-toggle-width></app-toggle-width>
</nav>

View File

@ -33,7 +33,7 @@ export class GeneratorComponent implements OnInit {
subaddressOptions: any[];
catchallOptions: any[];
forwardOptions: EmailForwarderOptions[];
usernameOptions: UsernameGeneratorOptions = {};
usernameOptions: UsernameGeneratorOptions = { website: null };
passwordOptions: PasswordGeneratorOptions = {};
username = "-";
password = "-";
@ -199,12 +199,12 @@ export class GeneratorComponent implements OnInit {
}
async sliderInput() {
this.normalizePasswordOptions();
await this.normalizePasswordOptions();
this.password = await this.passwordGenerationService.generatePassword(this.passwordOptions);
}
async savePasswordOptions(regenerate = true) {
this.normalizePasswordOptions();
await this.normalizePasswordOptions();
await this.passwordGenerationService.saveOptions(this.passwordOptions);
if (regenerate && this.regenerateWithoutButtonPress()) {
@ -271,7 +271,7 @@ export class GeneratorComponent implements OnInit {
return this.type !== "username" || this.usernameOptions.type !== "forwarded";
}
private normalizePasswordOptions() {
private async normalizePasswordOptions() {
// Application level normalize options dependent on class variables
this.passwordOptions.ambiguous = !this.avoidAmbiguous;
@ -290,9 +290,8 @@ export class GeneratorComponent implements OnInit {
}
}
this.passwordGenerationService.normalizeOptions(
await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(
this.passwordOptions,
this.enforcedPasswordPolicyOptions,
);
this._passwordOptionsMinLengthForReader.next(this.passwordOptions.minLength);

View File

@ -662,7 +662,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
// if a cipher is unassigned we want to check if they are an admin or have permission to edit any collection
if (!cipher.collectionIds) {
orgAdmin = this.organization?.canEditAnyCollection;
orgAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
}
return this.cipher.id == null
@ -671,14 +671,14 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
protected deleteCipher() {
const asAdmin = this.organization?.canEditAnyCollection;
const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
return this.cipher.isDeleted
? this.cipherService.deleteWithServer(this.cipher.id, asAdmin)
: this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin);
}
protected restoreCipher() {
const asAdmin = this.organization?.canEditAnyCollection;
const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
return this.cipherService.restoreWithServer(this.cipher.id, asAdmin);
}

View File

@ -116,7 +116,7 @@ export abstract class OrganizationService {
* https://bitwarden.atlassian.net/browse/AC-2252.
*/
getFromState: (id: string) => Promise<Organization>;
canManageSponsorships: () => Promise<boolean>;
canManageSponsorships$: Observable<boolean>;
hasOrganizations: () => Promise<boolean>;
get$: (id: string) => Observable<Organization | undefined>;
get: (id: string) => Promise<Organization>;

View File

@ -188,18 +188,29 @@ export class Organization {
return this.isManager || this.permissions.createNewCollections;
}
get canEditAnyCollection() {
return this.isAdmin || this.permissions.editAnyCollection;
canEditAnyCollection(flexibleCollectionsV1Enabled: boolean) {
if (!this.flexibleCollections || !flexibleCollectionsV1Enabled) {
// Pre-Flexible Collections v1 logic
return this.isAdmin || this.permissions.editAnyCollection;
}
// Post Flexible Collections V1, the allowAdminAccessToAllCollectionItems flag can restrict admins
// Providers and custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag
return (
this.isProviderUser ||
(this.type === OrganizationUserType.Custom && this.permissions.editAnyCollection) ||
(this.allowAdminAccessToAllCollectionItems && this.isAdmin)
);
}
get canUseAdminCollections() {
return this.canEditAnyCollection;
canUseAdminCollections(flexibleCollectionsV1Enabled: boolean) {
return this.canEditAnyCollection(flexibleCollectionsV1Enabled);
}
canEditAllCiphers(flexibleCollectionsV1Enabled: boolean) {
// Before Flexible Collections, anyone with editAnyCollection permission could edit all ciphers
if (!flexibleCollectionsV1Enabled) {
return this.canEditAnyCollection;
// Before Flexible Collections, any admin or anyone with editAnyCollection permission could edit all ciphers
if (!this.flexibleCollections || !flexibleCollectionsV1Enabled) {
return this.isAdmin || this.permissions.editAnyCollection;
}
// Post Flexible Collections V1, the allowAdminAccessToAllCollectionItems flag can restrict admins
// Providers and custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag
@ -214,8 +225,13 @@ export class Organization {
return this.isAdmin || this.permissions.deleteAnyCollection;
}
/**
* Whether the user can view all collection information, such as collection name and access.
* This does not indicate that the user can view items inside any collection - for that, see {@link canEditAllCiphers}
*/
get canViewAllCollections() {
return this.canEditAnyCollection || this.canDeleteAnyCollection;
// Admins can always see all collections even if collection management settings prevent them from editing them or seeing items
return this.isAdmin || this.permissions.editAnyCollection || this.canDeleteAnyCollection;
}
/**

View File

@ -121,7 +121,7 @@ describe("OrganizationService", () => {
const mockData: OrganizationData[] = buildMockOrganizations(1);
mockData[0].familySponsorshipAvailable = true;
fakeActiveUserState.nextState(arrayToRecord(mockData));
const result = await organizationService.canManageSponsorships();
const result = await firstValueFrom(organizationService.canManageSponsorships$);
expect(result).toBe(true);
});
@ -129,7 +129,7 @@ describe("OrganizationService", () => {
const mockData: OrganizationData[] = buildMockOrganizations(1);
mockData[0].familySponsorshipFriendlyName = "Something";
fakeActiveUserState.nextState(arrayToRecord(mockData));
const result = await organizationService.canManageSponsorships();
const result = await firstValueFrom(organizationService.canManageSponsorships$);
expect(result).toBe(true);
});
@ -137,7 +137,7 @@ describe("OrganizationService", () => {
const mockData: OrganizationData[] = buildMockOrganizations(1);
mockData[0].familySponsorshipFriendlyName = null;
fakeActiveUserState.nextState(arrayToRecord(mockData));
const result = await organizationService.canManageSponsorships();
const result = await firstValueFrom(organizationService.canManageSponsorships$);
expect(result).toBe(false);
});
});

View File

@ -77,14 +77,10 @@ export class OrganizationService implements InternalOrganizationServiceAbstracti
return await firstValueFrom(this.getOrganizationsFromState$(userId as UserId));
}
async canManageSponsorships(): Promise<boolean> {
return await firstValueFrom(
this.organizations$.pipe(
mapToExcludeOrganizationsWithoutFamilySponsorshipSupport(),
mapToBooleanHasAnyOrganizations(),
),
);
}
canManageSponsorships$ = this.organizations$.pipe(
mapToExcludeOrganizationsWithoutFamilySponsorshipSupport(),
mapToBooleanHasAnyOrganizations(),
);
async hasOrganizations(): Promise<boolean> {
return await firstValueFrom(this.organizations$.pipe(mapToBooleanHasAnyOrganizations()));

View File

@ -9,40 +9,46 @@ import {
GlobalState,
KeyDefinition,
StateProvider,
UserKeyDefinition,
} from "../../platform/state";
import { ClearClipboardDelay, AutofillOverlayVisibility } from "../constants";
import { ClearClipboardDelaySetting, InlineMenuVisibilitySetting } from "../types";
const AUTOFILL_ON_PAGE_LOAD = new KeyDefinition(AUTOFILL_SETTINGS_DISK, "autofillOnPageLoad", {
const AUTOFILL_ON_PAGE_LOAD = new UserKeyDefinition(AUTOFILL_SETTINGS_DISK, "autofillOnPageLoad", {
deserializer: (value: boolean) => value ?? false,
clearOn: [],
});
const AUTOFILL_ON_PAGE_LOAD_DEFAULT = new KeyDefinition(
const AUTOFILL_ON_PAGE_LOAD_DEFAULT = new UserKeyDefinition(
AUTOFILL_SETTINGS_DISK,
"autofillOnPageLoadDefault",
{
deserializer: (value: boolean) => value ?? false,
clearOn: [],
},
);
const AUTOFILL_ON_PAGE_LOAD_CALLOUT_DISMISSED = new KeyDefinition(
const AUTOFILL_ON_PAGE_LOAD_CALLOUT_DISMISSED = new UserKeyDefinition(
AUTOFILL_SETTINGS_DISK,
"autofillOnPageLoadCalloutIsDismissed",
{
deserializer: (value: boolean) => value ?? false,
clearOn: [],
},
);
const AUTOFILL_ON_PAGE_LOAD_POLICY_TOAST_HAS_DISPLAYED = new KeyDefinition(
const AUTOFILL_ON_PAGE_LOAD_POLICY_TOAST_HAS_DISPLAYED = new UserKeyDefinition(
AUTOFILL_SETTINGS_DISK,
"autofillOnPageLoadPolicyToastHasDisplayed",
{
deserializer: (value: boolean) => value ?? false,
clearOn: [],
},
);
const AUTO_COPY_TOTP = new KeyDefinition(AUTOFILL_SETTINGS_DISK, "autoCopyTotp", {
const AUTO_COPY_TOTP = new UserKeyDefinition(AUTOFILL_SETTINGS_DISK, "autoCopyTotp", {
deserializer: (value: boolean) => value ?? true,
clearOn: [],
});
const INLINE_MENU_VISIBILITY = new KeyDefinition(
@ -57,11 +63,12 @@ const ENABLE_CONTEXT_MENU = new KeyDefinition(AUTOFILL_SETTINGS_DISK, "enableCon
deserializer: (value: boolean) => value ?? true,
});
const CLEAR_CLIPBOARD_DELAY = new KeyDefinition(
const CLEAR_CLIPBOARD_DELAY = new UserKeyDefinition(
AUTOFILL_SETTINGS_DISK_LOCAL,
"clearClipboardDelay",
{
deserializer: (value: ClearClipboardDelaySetting) => value ?? ClearClipboardDelay.Never,
clearOn: [],
},
);

View File

@ -3,12 +3,13 @@ import { map, Observable } from "rxjs";
import {
BADGE_SETTINGS_DISK,
ActiveUserState,
KeyDefinition,
StateProvider,
UserKeyDefinition,
} from "../../platform/state";
const ENABLE_BADGE_COUNTER = new KeyDefinition(BADGE_SETTINGS_DISK, "enableBadgeCounter", {
const ENABLE_BADGE_COUNTER = new UserKeyDefinition(BADGE_SETTINGS_DISK, "enableBadgeCounter", {
deserializer: (value: boolean) => value ?? true,
clearOn: [],
});
export abstract class BadgeSettingsServiceAbstraction {

View File

@ -29,11 +29,12 @@ const EQUIVALENT_DOMAINS = new UserKeyDefinition(DOMAIN_SETTINGS_DISK, "equivale
clearOn: ["logout"],
});
const DEFAULT_URI_MATCH_STRATEGY = new KeyDefinition(
const DEFAULT_URI_MATCH_STRATEGY = new UserKeyDefinition(
DOMAIN_SETTINGS_DISK,
"defaultUriMatchStrategy",
{
deserializer: (value: UriMatchStrategySetting) => value ?? UriMatchStrategy.Domain,
clearOn: [],
},
);

View File

@ -1,7 +1,7 @@
import { BILLING_DISK, KeyDefinition } from "../../platform/state";
import { BILLING_DISK, UserKeyDefinition } from "../../platform/state";
import { PaymentMethodWarning } from "../models/domain/payment-method-warning";
export const PAYMENT_METHOD_WARNINGS_KEY = KeyDefinition.record<PaymentMethodWarning>(
export const PAYMENT_METHOD_WARNINGS_KEY = UserKeyDefinition.record<PaymentMethodWarning>(
BILLING_DISK,
"paymentMethodWarnings",
{
@ -9,5 +9,6 @@ export const PAYMENT_METHOD_WARNINGS_KEY = KeyDefinition.record<PaymentMethodWar
...warnings,
savedAt: new Date(warnings.savedAt),
}),
clearOn: ["logout"],
},
);

View File

@ -3,19 +3,20 @@ import { map, Observable, of, switchMap } from "rxjs";
import {
ActiveUserState,
BILLING_DISK,
KeyDefinition,
StateProvider,
UserKeyDefinition,
} from "../../../platform/state";
import {
BillingAccountProfile,
BillingAccountProfileStateService,
} from "../../abstractions/account/billing-account-profile-state.service";
export const BILLING_ACCOUNT_PROFILE_KEY_DEFINITION = new KeyDefinition<BillingAccountProfile>(
export const BILLING_ACCOUNT_PROFILE_KEY_DEFINITION = new UserKeyDefinition<BillingAccountProfile>(
BILLING_DISK,
"accountProfile",
{
deserializer: (billingAccountProfile) => billingAccountProfile,
clearOn: ["logout"],
},
);

View File

@ -14,7 +14,7 @@ describe("AppIdService", () => {
});
afterEach(() => {
jest.resetAllMocks();
jest.restoreAllMocks();
});
describe("getAppId", () => {

View File

@ -0,0 +1,42 @@
import { Observable } from "rxjs";
import { UserId } from "../../../types/guid";
import { GeneratorNavigation } from "../navigation/generator-navigation";
import { GeneratorNavigationPolicy } from "../navigation/generator-navigation-policy";
import { PolicyEvaluator } from "./policy-evaluator.abstraction";
/** Loads and stores generator navigational data
*/
export abstract class GeneratorNavigationService {
/** An observable monitoring the options saved to disk.
* The observable updates when the options are saved.
* @param userId: Identifies the user making the request
*/
options$: (userId: UserId) => Observable<GeneratorNavigation>;
/** Gets the default options. */
defaults$: (userId: UserId) => Observable<GeneratorNavigation>;
/** An observable monitoring the options used to enforce policy.
* The observable updates when the policy changes.
* @param userId: Identifies the user making the request
*/
evaluator$: (
userId: UserId,
) => Observable<PolicyEvaluator<GeneratorNavigationPolicy, GeneratorNavigation>>;
/** Enforces the policy on the given options
* @param userId: Identifies the user making the request
* @param options the options to enforce the policy on
* @returns a new instance of the options with the policy enforced
*/
enforcePolicy: (userId: UserId, options: GeneratorNavigation) => Promise<GeneratorNavigation>;
/** Saves the navigation options to disk.
* @param userId: Identifies the user making the request
* @param options the options to save
* @returns a promise that resolves when the options are saved
*/
saveOptions: (userId: UserId, options: GeneratorNavigation) => Promise<void>;
}

View File

@ -17,6 +17,9 @@ export abstract class GeneratorStrategy<Options, Policy> {
*/
durableState: (userId: UserId) => SingleUserState<Options>;
/** Gets the default options. */
defaults$: (userId: UserId) => Observable<Options>;
/** Identifies the policy enforced by the generator. */
policy: PolicyType;

View File

@ -21,6 +21,9 @@ export abstract class GeneratorService<Options, Policy> {
*/
evaluator$: (userId: UserId) => Observable<PolicyEvaluator<Policy, Options>>;
/** Gets the default options. */
defaults$: (userId: UserId) => Observable<Options>;
/** Enforces the policy on the given options
* @param userId: Identifies the user making the request
* @param options the options to enforce the policy on

View File

@ -1,3 +1,4 @@
export { GeneratorNavigationService } from "./generator-navigation.service.abstraction";
export { GeneratorService } from "./generator.service.abstraction";
export { GeneratorStrategy } from "./generator-strategy.abstraction";
export { PolicyEvaluator } from "./policy-evaluator.abstraction";

View File

@ -1,8 +1,8 @@
import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options";
import { GeneratedPasswordHistory } from "../password/generated-password-history";
import { PasswordGeneratorOptions } from "../password/password-generator-options";
import { GeneratedPasswordHistory } from "./generated-password-history";
import { PasswordGeneratorOptions } from "./password-generator-options";
/** @deprecated Use {@link GeneratorService} with a password or passphrase {@link GeneratorStrategy} instead. */
export abstract class PasswordGenerationServiceAbstraction {
generatePassword: (options: PasswordGeneratorOptions) => Promise<string>;
generatePassphrase: (options: PasswordGeneratorOptions) => Promise<string>;
@ -10,13 +10,8 @@ export abstract class PasswordGenerationServiceAbstraction {
enforcePasswordGeneratorPoliciesOnOptions: (
options: PasswordGeneratorOptions,
) => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>;
getPasswordGeneratorPolicyOptions: () => Promise<PasswordGeneratorPolicyOptions>;
saveOptions: (options: PasswordGeneratorOptions) => Promise<void>;
getHistory: () => Promise<GeneratedPasswordHistory[]>;
addHistory: (password: string) => Promise<void>;
clear: (userId?: string) => Promise<void>;
normalizeOptions: (
options: PasswordGeneratorOptions,
enforcedPolicyOptions: PasswordGeneratorPolicyOptions,
) => void;
}

View File

@ -1,5 +1,6 @@
import { UsernameGeneratorOptions } from "./username-generation-options";
import { UsernameGeneratorOptions } from "../username/username-generation-options";
/** @deprecated Use {@link GeneratorService} with a username {@link GeneratorStrategy} instead. */
export abstract class UsernameGenerationServiceAbstraction {
generateUsername: (options: UsernameGeneratorOptions) => Promise<string>;
generateWord: (options: UsernameGeneratorOptions) => Promise<string>;

View File

@ -37,6 +37,7 @@ function mockGeneratorStrategy(config?: {
userState?: SingleUserState<any>;
policy?: PolicyType;
evaluator?: any;
defaults?: any;
}) {
const durableState =
config?.userState ?? new FakeSingleUserState<PasswordGenerationOptions>(SomeUser);
@ -45,6 +46,7 @@ function mockGeneratorStrategy(config?: {
// whether they're used properly are guaranteed to test
// the value from `config`.
durableState: jest.fn(() => durableState),
defaults$: jest.fn(() => new BehaviorSubject(config?.defaults)),
policy: config?.policy ?? PolicyType.DisableSend,
toEvaluator: jest.fn(() =>
pipe(map(() => config?.evaluator ?? mock<PolicyEvaluator<any, any>>())),
@ -72,6 +74,20 @@ describe("Password generator service", () => {
});
});
describe("defaults$", () => {
it("should retrieve default state from the service", async () => {
const policy = mockPolicyService();
const defaults = {};
const strategy = mockGeneratorStrategy({ defaults });
const service = new DefaultGeneratorService(strategy, policy);
const result = await firstValueFrom(service.defaults$(SomeUser));
expect(strategy.defaults$).toHaveBeenCalledWith(SomeUser);
expect(result).toBe(defaults);
});
});
describe("saveOptions()", () => {
it("should trigger an options$ update", async () => {
const policy = mockPolicyService();

View File

@ -21,17 +21,22 @@ export class DefaultGeneratorService<Options, Policy> implements GeneratorServic
private _evaluators$ = new Map<UserId, Observable<PolicyEvaluator<Policy, Options>>>();
/** {@link GeneratorService.options$()} */
/** {@link GeneratorService.options$} */
options$(userId: UserId) {
return this.strategy.durableState(userId).state$;
}
/** {@link GeneratorService.defaults$} */
defaults$(userId: UserId) {
return this.strategy.defaults$(userId);
}
/** {@link GeneratorService.saveOptions} */
async saveOptions(userId: UserId, options: Options): Promise<void> {
await this.strategy.durableState(userId).update(() => options);
}
/** {@link GeneratorService.evaluator$()} */
/** {@link GeneratorService.evaluator$} */
evaluator$(userId: UserId) {
let evaluator$ = this._evaluators$.get(userId);
@ -59,7 +64,7 @@ export class DefaultGeneratorService<Options, Policy> implements GeneratorServic
return evaluator$;
}
/** {@link GeneratorService.enforcePolicy()} */
/** {@link GeneratorService.enforcePolicy} */
async enforcePolicy(userId: UserId, options: Options): Promise<Options> {
const policy = await firstValueFrom(this.evaluator$(userId));
const evaluated = policy.applyPolicy(options);

View File

@ -1,3 +1,5 @@
export type GeneratorOptions = {
type?: "password" | "username";
};
// this export provided solely for backwards compatibility
export {
/** @deprecated use `GeneratorNavigation` from './navigation' instead. */
GeneratorNavigation as GeneratorOptions,
} from "./navigation/generator-navigation";

View File

@ -0,0 +1,2 @@
/** The kind of credential being generated. */
export type GeneratorType = "password" | "passphrase" | "username";

View File

@ -10,9 +10,18 @@ import {
FASTMAIL_FORWARDER,
DUCK_DUCK_GO_FORWARDER,
ADDY_IO_FORWARDER,
GENERATOR_SETTINGS,
} from "./key-definitions";
describe("Key definitions", () => {
describe("GENERATOR_SETTINGS", () => {
it("should pass through deserialization", () => {
const value = {};
const result = GENERATOR_SETTINGS.deserializer(value);
expect(result).toBe(value);
});
});
describe("PASSWORD_SETTINGS", () => {
it("should pass through deserialization", () => {
const value = {};
@ -31,7 +40,7 @@ describe("Key definitions", () => {
describe("EFF_USERNAME_SETTINGS", () => {
it("should pass through deserialization", () => {
const value = {};
const value = { website: null as string };
const result = EFF_USERNAME_SETTINGS.deserializer(value);
expect(result).toBe(value);
});
@ -39,7 +48,7 @@ describe("Key definitions", () => {
describe("CATCHALL_SETTINGS", () => {
it("should pass through deserialization", () => {
const value = {};
const value = { website: null as string };
const result = CATCHALL_SETTINGS.deserializer(value);
expect(result).toBe(value);
});
@ -47,7 +56,7 @@ describe("Key definitions", () => {
describe("SUBADDRESS_SETTINGS", () => {
it("should pass through deserialization", () => {
const value = {};
const value = { website: null as string };
const result = SUBADDRESS_SETTINGS.deserializer(value);
expect(result).toBe(value);
});

View File

@ -1,6 +1,7 @@
import { GENERATOR_DISK, KeyDefinition } from "../../platform/state";
import { GENERATOR_DISK, GENERATOR_MEMORY, KeyDefinition } from "../../platform/state";
import { GeneratedCredential } from "./history/generated-credential";
import { GeneratorNavigation } from "./navigation/generator-navigation";
import { PassphraseGenerationOptions } from "./passphrase/passphrase-generation-options";
import { PasswordGenerationOptions } from "./password/password-generation-options";
import { SecretClassifier } from "./state/secret-classifier";
@ -15,6 +16,15 @@ import {
} from "./username/options/forwarder-options";
import { SubaddressGenerationOptions } from "./username/subaddress-generator-options";
/** plaintext password generation options */
export const GENERATOR_SETTINGS = new KeyDefinition<GeneratorNavigation>(
GENERATOR_MEMORY,
"generatorSettings",
{
deserializer: (value) => value,
},
);
/** plaintext password generation options */
export const PASSWORD_SETTINGS = new KeyDefinition<PasswordGenerationOptions>(
GENERATOR_DISK,
@ -42,7 +52,7 @@ export const EFF_USERNAME_SETTINGS = new KeyDefinition<EffUsernameGenerationOpti
},
);
/** catchall email generation options */
/** plaintext configuration for a domain catch-all address. */
export const CATCHALL_SETTINGS = new KeyDefinition<CatchallGenerationOptions>(
GENERATOR_DISK,
"catchallGeneratorSettings",
@ -51,7 +61,7 @@ export const CATCHALL_SETTINGS = new KeyDefinition<CatchallGenerationOptions>(
},
);
/** email subaddress generation options */
/** plaintext configuration for an email subaddress. */
export const SUBADDRESS_SETTINGS = new KeyDefinition<SubaddressGenerationOptions>(
GENERATOR_DISK,
"subaddressGeneratorSettings",
@ -60,6 +70,7 @@ export const SUBADDRESS_SETTINGS = new KeyDefinition<SubaddressGenerationOptions
},
);
/** backing store configuration for {@link Forwarders.AddyIo} */
export const ADDY_IO_FORWARDER = new KeyDefinition<SelfHostedApiOptions & EmailDomainOptions>(
GENERATOR_DISK,
"addyIoForwarder",
@ -68,6 +79,7 @@ export const ADDY_IO_FORWARDER = new KeyDefinition<SelfHostedApiOptions & EmailD
},
);
/** backing store configuration for {@link Forwarders.DuckDuckGo} */
export const DUCK_DUCK_GO_FORWARDER = new KeyDefinition<ApiOptions>(
GENERATOR_DISK,
"duckDuckGoForwarder",
@ -76,6 +88,7 @@ export const DUCK_DUCK_GO_FORWARDER = new KeyDefinition<ApiOptions>(
},
);
/** backing store configuration for {@link Forwarders.FastMail} */
export const FASTMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailPrefixOptions>(
GENERATOR_DISK,
"fastmailForwarder",
@ -84,6 +97,7 @@ export const FASTMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailPrefixOpti
},
);
/** backing store configuration for {@link Forwarders.FireFoxRelay} */
export const FIREFOX_RELAY_FORWARDER = new KeyDefinition<ApiOptions>(
GENERATOR_DISK,
"firefoxRelayForwarder",
@ -92,6 +106,7 @@ export const FIREFOX_RELAY_FORWARDER = new KeyDefinition<ApiOptions>(
},
);
/** backing store configuration for {@link Forwarders.ForwardEmail} */
export const FORWARD_EMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailDomainOptions>(
GENERATOR_DISK,
"forwardEmailForwarder",
@ -100,6 +115,7 @@ export const FORWARD_EMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailDomai
},
);
/** backing store configuration for {@link forwarders.SimpleLogin} */
export const SIMPLE_LOGIN_FORWARDER = new KeyDefinition<SelfHostedApiOptions>(
GENERATOR_DISK,
"simpleLoginForwarder",

View File

@ -0,0 +1,470 @@
/**
* include structuredClone in test environment.
* @jest-environment ../../../shared/test.environment.ts
*/
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { mockAccountServiceWith } from "../../../spec";
import { UserId } from "../../types/guid";
import { GeneratorNavigationService, GeneratorService } from "./abstractions";
import { LegacyPasswordGenerationService } from "./legacy-password-generation.service";
import { DefaultGeneratorNavigation, GeneratorNavigation } from "./navigation/generator-navigation";
import { GeneratorNavigationEvaluator } from "./navigation/generator-navigation-evaluator";
import { GeneratorNavigationPolicy } from "./navigation/generator-navigation-policy";
import {
DefaultPassphraseGenerationOptions,
PassphraseGenerationOptions,
PassphraseGeneratorOptionsEvaluator,
PassphraseGeneratorPolicy,
} from "./passphrase";
import { DisabledPassphraseGeneratorPolicy } from "./passphrase/passphrase-generator-policy";
import {
DefaultPasswordGenerationOptions,
PasswordGenerationOptions,
PasswordGeneratorOptions,
PasswordGeneratorOptionsEvaluator,
PasswordGeneratorPolicy,
} from "./password";
import { DisabledPasswordGeneratorPolicy } from "./password/password-generator-policy";
const SomeUser = "some user" as UserId;
function createPassphraseGenerator(
options: PassphraseGenerationOptions = {},
policy: PassphraseGeneratorPolicy = DisabledPassphraseGeneratorPolicy,
) {
let savedOptions = options;
const generator = mock<GeneratorService<PassphraseGenerationOptions, PassphraseGeneratorPolicy>>({
evaluator$(id: UserId) {
const evaluator = new PassphraseGeneratorOptionsEvaluator(policy);
return of(evaluator);
},
options$(id: UserId) {
return of(savedOptions);
},
defaults$(id: UserId) {
return of(DefaultPassphraseGenerationOptions);
},
saveOptions(userId, options) {
savedOptions = options;
return Promise.resolve();
},
});
return generator;
}
function createPasswordGenerator(
options: PasswordGenerationOptions = {},
policy: PasswordGeneratorPolicy = DisabledPasswordGeneratorPolicy,
) {
let savedOptions = options;
const generator = mock<GeneratorService<PasswordGenerationOptions, PasswordGeneratorPolicy>>({
evaluator$(id: UserId) {
const evaluator = new PasswordGeneratorOptionsEvaluator(policy);
return of(evaluator);
},
options$(id: UserId) {
return of(savedOptions);
},
defaults$(id: UserId) {
return of(DefaultPasswordGenerationOptions);
},
saveOptions(userId, options) {
savedOptions = options;
return Promise.resolve();
},
});
return generator;
}
function createNavigationGenerator(
options: GeneratorNavigation = {},
policy: GeneratorNavigationPolicy = {},
) {
let savedOptions = options;
const generator = mock<GeneratorNavigationService>({
evaluator$(id: UserId) {
const evaluator = new GeneratorNavigationEvaluator(policy);
return of(evaluator);
},
options$(id: UserId) {
return of(savedOptions);
},
defaults$(id: UserId) {
return of(DefaultGeneratorNavigation);
},
saveOptions(userId, options) {
savedOptions = options;
return Promise.resolve();
},
});
return generator;
}
describe("LegacyPasswordGenerationService", () => {
// NOTE: in all tests, `null` constructor arguments are not used by the test.
// They're set to `null` to avoid setting up unnecessary mocks.
describe("generatePassword", () => {
it("invokes the inner password generator to generate passwords", async () => {
const innerPassword = createPasswordGenerator();
const generator = new LegacyPasswordGenerationService(null, null, innerPassword, null);
const options = { type: "password" } as PasswordGeneratorOptions;
await generator.generatePassword(options);
expect(innerPassword.generate).toHaveBeenCalledWith(options);
});
it("invokes the inner passphrase generator to generate passphrases", async () => {
const innerPassphrase = createPassphraseGenerator();
const generator = new LegacyPasswordGenerationService(null, null, null, innerPassphrase);
const options = { type: "passphrase" } as PasswordGeneratorOptions;
await generator.generatePassword(options);
expect(innerPassphrase.generate).toHaveBeenCalledWith(options);
});
});
describe("generatePassphrase", () => {
it("invokes the inner passphrase generator", async () => {
const innerPassphrase = createPassphraseGenerator();
const generator = new LegacyPasswordGenerationService(null, null, null, innerPassphrase);
const options = {} as PasswordGeneratorOptions;
await generator.generatePassphrase(options);
expect(innerPassphrase.generate).toHaveBeenCalledWith(options);
});
});
describe("getOptions", () => {
it("combines options from its inner services", async () => {
const innerPassword = createPasswordGenerator({
length: 29,
minLength: 20,
ambiguous: false,
uppercase: true,
minUppercase: 1,
lowercase: false,
minLowercase: 2,
number: true,
minNumber: 3,
special: false,
minSpecial: 4,
});
const innerPassphrase = createPassphraseGenerator({
numWords: 10,
wordSeparator: "-",
capitalize: true,
includeNumber: false,
});
const navigation = createNavigationGenerator({
type: "passphrase",
username: "word",
forwarder: "simplelogin",
});
const accountService = mockAccountServiceWith(SomeUser);
const generator = new LegacyPasswordGenerationService(
accountService,
navigation,
innerPassword,
innerPassphrase,
);
const [result] = await generator.getOptions();
expect(result).toEqual({
type: "passphrase",
username: "word",
forwarder: "simplelogin",
length: 29,
minLength: 20,
ambiguous: false,
uppercase: true,
minUppercase: 1,
lowercase: false,
minLowercase: 2,
number: true,
minNumber: 3,
special: false,
minSpecial: 4,
numWords: 10,
wordSeparator: "-",
capitalize: true,
includeNumber: false,
});
});
it("sets default options when an inner service lacks a value", async () => {
const innerPassword = createPasswordGenerator(null);
const innerPassphrase = createPassphraseGenerator(null);
const navigation = createNavigationGenerator(null);
const accountService = mockAccountServiceWith(SomeUser);
const generator = new LegacyPasswordGenerationService(
accountService,
navigation,
innerPassword,
innerPassphrase,
);
const [result] = await generator.getOptions();
expect(result).toEqual({
...DefaultGeneratorNavigation,
...DefaultPassphraseGenerationOptions,
...DefaultPasswordGenerationOptions,
});
});
it("combines policies from its inner services", async () => {
const innerPassword = createPasswordGenerator(
{},
{
minLength: 20,
numberCount: 10,
specialCount: 11,
useUppercase: true,
useLowercase: false,
useNumbers: true,
useSpecial: false,
},
);
const innerPassphrase = createPassphraseGenerator(
{},
{
minNumberWords: 5,
capitalize: true,
includeNumber: false,
},
);
const accountService = mockAccountServiceWith(SomeUser);
const navigation = createNavigationGenerator(
{},
{
defaultType: "password",
},
);
const generator = new LegacyPasswordGenerationService(
accountService,
navigation,
innerPassword,
innerPassphrase,
);
const [, policy] = await generator.getOptions();
expect(policy).toEqual({
defaultType: "password",
minLength: 20,
numberCount: 10,
specialCount: 11,
useUppercase: true,
useLowercase: false,
useNumbers: true,
useSpecial: false,
minNumberWords: 5,
capitalize: true,
includeNumber: false,
});
});
});
describe("enforcePasswordGeneratorPoliciesOnOptions", () => {
it("returns its options parameter with password policy applied", async () => {
const innerPassword = createPasswordGenerator(
{},
{
minLength: 15,
numberCount: 5,
specialCount: 5,
useUppercase: true,
useLowercase: true,
useNumbers: true,
useSpecial: true,
},
);
const innerPassphrase = createPassphraseGenerator();
const accountService = mockAccountServiceWith(SomeUser);
const navigation = createNavigationGenerator();
const options = {
type: "password" as const,
};
const generator = new LegacyPasswordGenerationService(
accountService,
navigation,
innerPassword,
innerPassphrase,
);
const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options);
expect(result).toBe(options);
expect(result).toMatchObject({
length: 15,
minLength: 15,
minLowercase: 1,
minNumber: 5,
minUppercase: 1,
minSpecial: 5,
uppercase: true,
lowercase: true,
number: true,
special: true,
});
});
it("returns its options parameter with passphrase policy applied", async () => {
const innerPassword = createPasswordGenerator();
const innerPassphrase = createPassphraseGenerator(
{},
{
minNumberWords: 5,
capitalize: true,
includeNumber: true,
},
);
const accountService = mockAccountServiceWith(SomeUser);
const navigation = createNavigationGenerator();
const options = {
type: "passphrase" as const,
};
const generator = new LegacyPasswordGenerationService(
accountService,
navigation,
innerPassword,
innerPassphrase,
);
const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options);
expect(result).toBe(options);
expect(result).toMatchObject({
numWords: 5,
capitalize: true,
includeNumber: true,
});
});
it("returns the applied policy", async () => {
const innerPassword = createPasswordGenerator(
{},
{
minLength: 20,
numberCount: 10,
specialCount: 11,
useUppercase: true,
useLowercase: false,
useNumbers: true,
useSpecial: false,
},
);
const innerPassphrase = createPassphraseGenerator(
{},
{
minNumberWords: 5,
capitalize: true,
includeNumber: false,
},
);
const accountService = mockAccountServiceWith(SomeUser);
const navigation = createNavigationGenerator(
{},
{
defaultType: "password",
},
);
const generator = new LegacyPasswordGenerationService(
accountService,
navigation,
innerPassword,
innerPassphrase,
);
const [, policy] = await generator.enforcePasswordGeneratorPoliciesOnOptions({});
expect(policy).toEqual({
defaultType: "password",
minLength: 20,
numberCount: 10,
specialCount: 11,
useUppercase: true,
useLowercase: false,
useNumbers: true,
useSpecial: false,
minNumberWords: 5,
capitalize: true,
includeNumber: false,
});
});
});
describe("saveOptions", () => {
it("loads saved password options", async () => {
const innerPassword = createPasswordGenerator();
const innerPassphrase = createPassphraseGenerator();
const navigation = createNavigationGenerator();
const accountService = mockAccountServiceWith(SomeUser);
const generator = new LegacyPasswordGenerationService(
accountService,
navigation,
innerPassword,
innerPassphrase,
);
const options = {
type: "password" as const,
username: "word" as const,
forwarder: "simplelogin" as const,
length: 29,
minLength: 20,
ambiguous: false,
uppercase: true,
minUppercase: 1,
lowercase: false,
minLowercase: 2,
number: true,
minNumber: 3,
special: false,
minSpecial: 4,
};
await generator.saveOptions(options);
const [result] = await generator.getOptions();
expect(result).toMatchObject(options);
});
it("loads saved passphrase options", async () => {
const innerPassword = createPasswordGenerator();
const innerPassphrase = createPassphraseGenerator();
const navigation = createNavigationGenerator();
const accountService = mockAccountServiceWith(SomeUser);
const generator = new LegacyPasswordGenerationService(
accountService,
navigation,
innerPassword,
innerPassphrase,
);
const options = {
type: "passphrase" as const,
username: "word" as const,
forwarder: "simplelogin" as const,
numWords: 10,
wordSeparator: "-",
capitalize: true,
includeNumber: false,
};
await generator.saveOptions(options);
const [result] = await generator.getOptions();
expect(result).toMatchObject(options);
});
});
});

View File

@ -0,0 +1,184 @@
import { concatMap, zip, map, firstValueFrom } from "rxjs";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { PasswordGeneratorPolicyOptions } from "../../admin-console/models/domain/password-generator-policy-options";
import { AccountService } from "../../auth/abstractions/account.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { StateProvider } from "../../platform/state";
import { GeneratorService, GeneratorNavigationService } from "./abstractions";
import { PasswordGenerationServiceAbstraction } from "./abstractions/password-generation.service.abstraction";
import { DefaultGeneratorService } from "./default-generator.service";
import { DefaultGeneratorNavigationService } from "./navigation/default-generator-navigation.service";
import {
PassphraseGenerationOptions,
PassphraseGeneratorPolicy,
PassphraseGeneratorStrategy,
} from "./passphrase";
import {
PasswordGenerationOptions,
PasswordGenerationService,
PasswordGeneratorOptions,
PasswordGeneratorPolicy,
PasswordGeneratorStrategy,
} from "./password";
export function legacyPasswordGenerationServiceFactory(
cryptoService: CryptoService,
policyService: PolicyService,
accountService: AccountService,
stateProvider: StateProvider,
): PasswordGenerationServiceAbstraction {
// FIXME: Once the password generation service is replaced with this service
// in the clients, factor out the deprecated service in its entirety.
const deprecatedService = new PasswordGenerationService(cryptoService, null, null);
const passwords = new DefaultGeneratorService(
new PasswordGeneratorStrategy(deprecatedService, stateProvider),
policyService,
);
const passphrases = new DefaultGeneratorService(
new PassphraseGeneratorStrategy(deprecatedService, stateProvider),
policyService,
);
const navigation = new DefaultGeneratorNavigationService(stateProvider, policyService);
return new LegacyPasswordGenerationService(accountService, navigation, passwords, passphrases);
}
/** Adapts the generator 2.0 design to 1.0 angular services. */
export class LegacyPasswordGenerationService implements PasswordGenerationServiceAbstraction {
constructor(
private readonly accountService: AccountService,
private readonly navigation: GeneratorNavigationService,
private readonly passwords: GeneratorService<
PasswordGenerationOptions,
PasswordGeneratorPolicy
>,
private readonly passphrases: GeneratorService<
PassphraseGenerationOptions,
PassphraseGeneratorPolicy
>,
) {}
generatePassword(options: PasswordGeneratorOptions) {
if (options.type === "password") {
return this.passwords.generate(options);
} else {
return this.passphrases.generate(options);
}
}
generatePassphrase(options: PasswordGeneratorOptions) {
return this.passphrases.generate(options);
}
async getOptions() {
const options$ = this.accountService.activeAccount$.pipe(
concatMap((activeUser) =>
zip(
this.passwords.options$(activeUser.id),
this.passwords.defaults$(activeUser.id),
this.passwords.evaluator$(activeUser.id),
this.passphrases.options$(activeUser.id),
this.passphrases.defaults$(activeUser.id),
this.passphrases.evaluator$(activeUser.id),
this.navigation.options$(activeUser.id),
this.navigation.defaults$(activeUser.id),
this.navigation.evaluator$(activeUser.id),
),
),
map(
([
passwordOptions,
passwordDefaults,
passwordEvaluator,
passphraseOptions,
passphraseDefaults,
passphraseEvaluator,
generatorOptions,
generatorDefaults,
generatorEvaluator,
]) => {
const options: PasswordGeneratorOptions = Object.assign(
{},
passwordOptions ?? passwordDefaults,
passphraseOptions ?? passphraseDefaults,
generatorOptions ?? generatorDefaults,
);
const policy = Object.assign(
new PasswordGeneratorPolicyOptions(),
passwordEvaluator.policy,
passphraseEvaluator.policy,
generatorEvaluator.policy,
);
return [options, policy] as [PasswordGenerationOptions, PasswordGeneratorPolicyOptions];
},
),
);
const options = await firstValueFrom(options$);
return options;
}
async enforcePasswordGeneratorPoliciesOnOptions(options: PasswordGeneratorOptions) {
const options$ = this.accountService.activeAccount$.pipe(
concatMap((activeUser) =>
zip(
this.passwords.evaluator$(activeUser.id),
this.passphrases.evaluator$(activeUser.id),
this.navigation.evaluator$(activeUser.id),
),
),
map(([passwordEvaluator, passphraseEvaluator, navigationEvaluator]) => {
const policy = Object.assign(
new PasswordGeneratorPolicyOptions(),
passwordEvaluator.policy,
passphraseEvaluator.policy,
navigationEvaluator.policy,
);
const navigationApplied = navigationEvaluator.applyPolicy(options);
const navigationSanitized = {
...options,
...navigationEvaluator.sanitize(navigationApplied),
};
if (options.type === "password") {
const applied = passwordEvaluator.applyPolicy(navigationSanitized);
const sanitized = passwordEvaluator.sanitize(applied);
return [sanitized, policy];
} else {
const applied = passphraseEvaluator.applyPolicy(navigationSanitized);
const sanitized = passphraseEvaluator.sanitize(applied);
return [sanitized, policy];
}
}),
);
const [sanitized, policy] = await firstValueFrom(options$);
return [
// callers assume this function updates the options parameter
Object.assign(options, sanitized),
policy,
] as [PasswordGenerationOptions, PasswordGeneratorPolicyOptions];
}
async saveOptions(options: PasswordGeneratorOptions) {
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
await this.navigation.saveOptions(activeAccount.id, options);
if (options.type === "password") {
await this.passwords.saveOptions(activeAccount.id, options);
} else {
await this.passphrases.saveOptions(activeAccount.id, options);
}
}
getHistory: () => Promise<any[]>;
addHistory: (password: string) => Promise<void>;
clear: (userId?: string) => Promise<void>;
}

View File

@ -0,0 +1,748 @@
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { mockAccountServiceWith } from "../../../spec";
import { UserId } from "../../types/guid";
import { GeneratorNavigationService, GeneratorService } from "./abstractions";
import { DefaultPolicyEvaluator } from "./default-policy-evaluator";
import { LegacyUsernameGenerationService } from "./legacy-username-generation.service";
import { DefaultGeneratorNavigation, GeneratorNavigation } from "./navigation/generator-navigation";
import { GeneratorNavigationEvaluator } from "./navigation/generator-navigation-evaluator";
import { GeneratorNavigationPolicy } from "./navigation/generator-navigation-policy";
import { NoPolicy } from "./no-policy";
import { UsernameGeneratorOptions } from "./username";
import {
CatchallGenerationOptions,
DefaultCatchallOptions,
} from "./username/catchall-generator-options";
import {
DefaultEffUsernameOptions,
EffUsernameGenerationOptions,
} from "./username/eff-username-generator-options";
import { DefaultAddyIoOptions } from "./username/forwarders/addy-io";
import { DefaultDuckDuckGoOptions } from "./username/forwarders/duck-duck-go";
import { DefaultFastmailOptions } from "./username/forwarders/fastmail";
import { DefaultFirefoxRelayOptions } from "./username/forwarders/firefox-relay";
import { DefaultForwardEmailOptions } from "./username/forwarders/forward-email";
import { DefaultSimpleLoginOptions } from "./username/forwarders/simple-login";
import { Forwarders } from "./username/options/constants";
import {
ApiOptions,
EmailDomainOptions,
EmailPrefixOptions,
SelfHostedApiOptions,
} from "./username/options/forwarder-options";
import {
DefaultSubaddressOptions,
SubaddressGenerationOptions,
} from "./username/subaddress-generator-options";
const SomeUser = "userId" as UserId;
function createGenerator<Options>(options: Options, defaults: Options) {
let savedOptions = options;
const generator = mock<GeneratorService<Options, NoPolicy>>({
evaluator$(id: UserId) {
const evaluator = new DefaultPolicyEvaluator<Options>();
return of(evaluator);
},
options$(id: UserId) {
return of(savedOptions);
},
defaults$(id: UserId) {
return of(defaults);
},
saveOptions: jest.fn((userId, options) => {
savedOptions = options;
return Promise.resolve();
}),
});
return generator;
}
function createNavigationGenerator(
options: GeneratorNavigation = {},
policy: GeneratorNavigationPolicy = {},
) {
let savedOptions = options;
const generator = mock<GeneratorNavigationService>({
evaluator$(id: UserId) {
const evaluator = new GeneratorNavigationEvaluator(policy);
return of(evaluator);
},
options$(id: UserId) {
return of(savedOptions);
},
defaults$(id: UserId) {
return of(DefaultGeneratorNavigation);
},
saveOptions: jest.fn((userId, options) => {
savedOptions = options;
return Promise.resolve();
}),
});
return generator;
}
describe("LegacyUsernameGenerationService", () => {
// NOTE: in all tests, `null` constructor arguments are not used by the test.
// They're set to `null` to avoid setting up unnecessary mocks.
describe("generateUserName", () => {
it("should generate a catchall username", async () => {
const options = { type: "catchall" } as UsernameGeneratorOptions;
const catchall = createGenerator<CatchallGenerationOptions>(null, null);
catchall.generate.mockReturnValue(Promise.resolve("catchall@example.com"));
const generator = new LegacyUsernameGenerationService(
null,
null,
catchall,
null,
null,
null,
null,
null,
null,
null,
null,
);
const result = await generator.generateUsername(options);
expect(catchall.generate).toHaveBeenCalledWith(options);
expect(result).toBe("catchall@example.com");
});
it("should generate an EFF word username", async () => {
const options = { type: "word" } as UsernameGeneratorOptions;
const effWord = createGenerator<EffUsernameGenerationOptions>(null, null);
effWord.generate.mockReturnValue(Promise.resolve("eff word"));
const generator = new LegacyUsernameGenerationService(
null,
null,
null,
effWord,
null,
null,
null,
null,
null,
null,
null,
);
const result = await generator.generateUsername(options);
expect(effWord.generate).toHaveBeenCalledWith(options);
expect(result).toBe("eff word");
});
it("should generate a subaddress username", async () => {
const options = { type: "subaddress" } as UsernameGeneratorOptions;
const subaddress = createGenerator<SubaddressGenerationOptions>(null, null);
subaddress.generate.mockReturnValue(Promise.resolve("subaddress@example.com"));
const generator = new LegacyUsernameGenerationService(
null,
null,
null,
null,
subaddress,
null,
null,
null,
null,
null,
null,
);
const result = await generator.generateUsername(options);
expect(subaddress.generate).toHaveBeenCalledWith(options);
expect(result).toBe("subaddress@example.com");
});
it("should generate a forwarder username", async () => {
// set up an arbitrary forwarder for the username test; all forwarders tested in their own tests
const options = {
type: "forwarded",
forwardedService: Forwarders.AddyIo.id,
} as UsernameGeneratorOptions;
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(null, null);
addyIo.generate.mockReturnValue(Promise.resolve("addyio@example.com"));
const generator = new LegacyUsernameGenerationService(
null,
null,
null,
null,
null,
addyIo,
null,
null,
null,
null,
null,
);
const result = await generator.generateUsername(options);
expect(addyIo.generate).toHaveBeenCalledWith({});
expect(result).toBe("addyio@example.com");
});
});
describe("generateCatchall", () => {
it("should generate a catchall username", async () => {
const options = { type: "catchall" } as UsernameGeneratorOptions;
const catchall = createGenerator<CatchallGenerationOptions>(null, null);
catchall.generate.mockReturnValue(Promise.resolve("catchall@example.com"));
const generator = new LegacyUsernameGenerationService(
null,
null,
catchall,
null,
null,
null,
null,
null,
null,
null,
null,
);
const result = await generator.generateCatchall(options);
expect(catchall.generate).toHaveBeenCalledWith(options);
expect(result).toBe("catchall@example.com");
});
});
describe("generateSubaddress", () => {
it("should generate a subaddress username", async () => {
const options = { type: "subaddress" } as UsernameGeneratorOptions;
const subaddress = createGenerator<SubaddressGenerationOptions>(null, null);
subaddress.generate.mockReturnValue(Promise.resolve("subaddress@example.com"));
const generator = new LegacyUsernameGenerationService(
null,
null,
null,
null,
subaddress,
null,
null,
null,
null,
null,
null,
);
const result = await generator.generateSubaddress(options);
expect(subaddress.generate).toHaveBeenCalledWith(options);
expect(result).toBe("subaddress@example.com");
});
});
describe("generateForwarded", () => {
it("should generate a AddyIo username", async () => {
const options = {
forwardedService: Forwarders.AddyIo.id,
forwardedAnonAddyApiToken: "token",
forwardedAnonAddyBaseUrl: "https://example.com",
forwardedAnonAddyDomain: "example.com",
website: "example.com",
} as UsernameGeneratorOptions;
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(null, null);
addyIo.generate.mockReturnValue(Promise.resolve("addyio@example.com"));
const generator = new LegacyUsernameGenerationService(
null,
null,
null,
null,
null,
addyIo,
null,
null,
null,
null,
null,
);
const result = await generator.generateForwarded(options);
expect(addyIo.generate).toHaveBeenCalledWith({
token: "token",
baseUrl: "https://example.com",
domain: "example.com",
website: "example.com",
});
expect(result).toBe("addyio@example.com");
});
it("should generate a DuckDuckGo username", async () => {
const options = {
forwardedService: Forwarders.DuckDuckGo.id,
forwardedDuckDuckGoToken: "token",
website: "example.com",
} as UsernameGeneratorOptions;
const duckDuckGo = createGenerator<ApiOptions>(null, null);
duckDuckGo.generate.mockReturnValue(Promise.resolve("ddg@example.com"));
const generator = new LegacyUsernameGenerationService(
null,
null,
null,
null,
null,
null,
duckDuckGo,
null,
null,
null,
null,
);
const result = await generator.generateForwarded(options);
expect(duckDuckGo.generate).toHaveBeenCalledWith({
token: "token",
website: "example.com",
});
expect(result).toBe("ddg@example.com");
});
it("should generate a Fastmail username", async () => {
const options = {
forwardedService: Forwarders.Fastmail.id,
forwardedFastmailApiToken: "token",
website: "example.com",
} as UsernameGeneratorOptions;
const fastmail = createGenerator<ApiOptions & EmailPrefixOptions>(null, null);
fastmail.generate.mockReturnValue(Promise.resolve("fastmail@example.com"));
const generator = new LegacyUsernameGenerationService(
null,
null,
null,
null,
null,
null,
null,
fastmail,
null,
null,
null,
);
const result = await generator.generateForwarded(options);
expect(fastmail.generate).toHaveBeenCalledWith({
token: "token",
website: "example.com",
});
expect(result).toBe("fastmail@example.com");
});
it("should generate a FirefoxRelay username", async () => {
const options = {
forwardedService: Forwarders.FirefoxRelay.id,
forwardedFirefoxApiToken: "token",
website: "example.com",
} as UsernameGeneratorOptions;
const firefoxRelay = createGenerator<ApiOptions>(null, null);
firefoxRelay.generate.mockReturnValue(Promise.resolve("firefoxrelay@example.com"));
const generator = new LegacyUsernameGenerationService(
null,
null,
null,
null,
null,
null,
null,
null,
firefoxRelay,
null,
null,
);
const result = await generator.generateForwarded(options);
expect(firefoxRelay.generate).toHaveBeenCalledWith({
token: "token",
website: "example.com",
});
expect(result).toBe("firefoxrelay@example.com");
});
it("should generate a ForwardEmail username", async () => {
const options = {
forwardedService: Forwarders.ForwardEmail.id,
forwardedForwardEmailApiToken: "token",
forwardedForwardEmailDomain: "example.com",
website: "example.com",
} as UsernameGeneratorOptions;
const forwardEmail = createGenerator<ApiOptions & EmailDomainOptions>(null, null);
forwardEmail.generate.mockReturnValue(Promise.resolve("forwardemail@example.com"));
const generator = new LegacyUsernameGenerationService(
null,
null,
null,
null,
null,
null,
null,
null,
null,
forwardEmail,
null,
);
const result = await generator.generateForwarded(options);
expect(forwardEmail.generate).toHaveBeenCalledWith({
token: "token",
domain: "example.com",
website: "example.com",
});
expect(result).toBe("forwardemail@example.com");
});
it("should generate a SimpleLogin username", async () => {
const options = {
forwardedService: Forwarders.SimpleLogin.id,
forwardedSimpleLoginApiKey: "token",
forwardedSimpleLoginBaseUrl: "https://example.com",
website: "example.com",
} as UsernameGeneratorOptions;
const simpleLogin = createGenerator<SelfHostedApiOptions>(null, null);
simpleLogin.generate.mockReturnValue(Promise.resolve("simplelogin@example.com"));
const generator = new LegacyUsernameGenerationService(
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
simpleLogin,
);
const result = await generator.generateForwarded(options);
expect(simpleLogin.generate).toHaveBeenCalledWith({
token: "token",
baseUrl: "https://example.com",
website: "example.com",
});
expect(result).toBe("simplelogin@example.com");
});
});
describe("getOptions", () => {
it("combines options from its inner generators", async () => {
const account = mockAccountServiceWith(SomeUser);
const navigation = createNavigationGenerator({
type: "username",
username: "catchall",
forwarder: Forwarders.AddyIo.id,
});
const catchall = createGenerator<CatchallGenerationOptions>(
{
catchallDomain: "example.com",
catchallType: "random",
website: null,
},
null,
);
const effUsername = createGenerator<EffUsernameGenerationOptions>(
{
wordCapitalize: true,
wordIncludeNumber: false,
website: null,
},
null,
);
const subaddress = createGenerator<SubaddressGenerationOptions>(
{
subaddressType: "random",
subaddressEmail: "foo@example.com",
website: null,
},
null,
);
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(
{
token: "addyIoToken",
domain: "addyio.example.com",
baseUrl: "https://addyio.api.example.com",
website: null,
},
null,
);
const duckDuckGo = createGenerator<ApiOptions>(
{
token: "ddgToken",
website: null,
},
null,
);
const fastmail = createGenerator<ApiOptions & EmailPrefixOptions>(
{
token: "fastmailToken",
domain: "fastmail.example.com",
prefix: "foo",
website: null,
},
null,
);
const firefoxRelay = createGenerator<ApiOptions>(
{
token: "firefoxToken",
website: null,
},
null,
);
const forwardEmail = createGenerator<ApiOptions & EmailDomainOptions>(
{
token: "forwardEmailToken",
domain: "example.com",
website: null,
},
null,
);
const simpleLogin = createGenerator<SelfHostedApiOptions>(
{
token: "simpleLoginToken",
baseUrl: "https://simplelogin.api.example.com",
website: null,
},
null,
);
const generator = new LegacyUsernameGenerationService(
account,
navigation,
catchall,
effUsername,
subaddress,
addyIo,
duckDuckGo,
fastmail,
firefoxRelay,
forwardEmail,
simpleLogin,
);
const result = await generator.getOptions();
expect(result).toEqual({
type: "catchall",
wordCapitalize: true,
wordIncludeNumber: false,
subaddressType: "random",
subaddressEmail: "foo@example.com",
catchallType: "random",
catchallDomain: "example.com",
forwardedService: Forwarders.AddyIo.id,
forwardedAnonAddyApiToken: "addyIoToken",
forwardedAnonAddyDomain: "addyio.example.com",
forwardedAnonAddyBaseUrl: "https://addyio.api.example.com",
forwardedDuckDuckGoToken: "ddgToken",
forwardedFirefoxApiToken: "firefoxToken",
forwardedFastmailApiToken: "fastmailToken",
forwardedForwardEmailApiToken: "forwardEmailToken",
forwardedForwardEmailDomain: "example.com",
forwardedSimpleLoginApiKey: "simpleLoginToken",
forwardedSimpleLoginBaseUrl: "https://simplelogin.api.example.com",
});
});
it("sets default options when an inner service lacks a value", async () => {
const account = mockAccountServiceWith(SomeUser);
const navigation = createNavigationGenerator(null);
const catchall = createGenerator<CatchallGenerationOptions>(null, DefaultCatchallOptions);
const effUsername = createGenerator<EffUsernameGenerationOptions>(
null,
DefaultEffUsernameOptions,
);
const subaddress = createGenerator<SubaddressGenerationOptions>(
null,
DefaultSubaddressOptions,
);
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(
null,
DefaultAddyIoOptions,
);
const duckDuckGo = createGenerator<ApiOptions>(null, DefaultDuckDuckGoOptions);
const fastmail = createGenerator<ApiOptions & EmailPrefixOptions>(
null,
DefaultFastmailOptions,
);
const firefoxRelay = createGenerator<ApiOptions>(null, DefaultFirefoxRelayOptions);
const forwardEmail = createGenerator<ApiOptions & EmailDomainOptions>(
null,
DefaultForwardEmailOptions,
);
const simpleLogin = createGenerator<SelfHostedApiOptions>(null, DefaultSimpleLoginOptions);
const generator = new LegacyUsernameGenerationService(
account,
navigation,
catchall,
effUsername,
subaddress,
addyIo,
duckDuckGo,
fastmail,
firefoxRelay,
forwardEmail,
simpleLogin,
);
const result = await generator.getOptions();
expect(result).toEqual({
type: DefaultGeneratorNavigation.username,
catchallType: DefaultCatchallOptions.catchallType,
catchallDomain: DefaultCatchallOptions.catchallDomain,
wordCapitalize: DefaultEffUsernameOptions.wordCapitalize,
wordIncludeNumber: DefaultEffUsernameOptions.wordIncludeNumber,
subaddressType: DefaultSubaddressOptions.subaddressType,
subaddressEmail: DefaultSubaddressOptions.subaddressEmail,
forwardedService: DefaultGeneratorNavigation.forwarder,
forwardedAnonAddyApiToken: DefaultAddyIoOptions.token,
forwardedAnonAddyDomain: DefaultAddyIoOptions.domain,
forwardedAnonAddyBaseUrl: DefaultAddyIoOptions.baseUrl,
forwardedDuckDuckGoToken: DefaultDuckDuckGoOptions.token,
forwardedFastmailApiToken: DefaultFastmailOptions.token,
forwardedFirefoxApiToken: DefaultFirefoxRelayOptions.token,
forwardedForwardEmailApiToken: DefaultForwardEmailOptions.token,
forwardedForwardEmailDomain: DefaultForwardEmailOptions.domain,
forwardedSimpleLoginApiKey: DefaultSimpleLoginOptions.token,
forwardedSimpleLoginBaseUrl: DefaultSimpleLoginOptions.baseUrl,
});
});
});
describe("saveOptions", () => {
it("saves option sets to its inner generators", async () => {
const account = mockAccountServiceWith(SomeUser);
const navigation = createNavigationGenerator({ type: "password" });
const catchall = createGenerator<CatchallGenerationOptions>(null, null);
const effUsername = createGenerator<EffUsernameGenerationOptions>(null, null);
const subaddress = createGenerator<SubaddressGenerationOptions>(null, null);
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(null, null);
const duckDuckGo = createGenerator<ApiOptions>(null, null);
const fastmail = createGenerator<ApiOptions & EmailPrefixOptions>(null, null);
const firefoxRelay = createGenerator<ApiOptions>(null, null);
const forwardEmail = createGenerator<ApiOptions & EmailDomainOptions>(null, null);
const simpleLogin = createGenerator<SelfHostedApiOptions>(null, null);
const generator = new LegacyUsernameGenerationService(
account,
navigation,
catchall,
effUsername,
subaddress,
addyIo,
duckDuckGo,
fastmail,
firefoxRelay,
forwardEmail,
simpleLogin,
);
await generator.saveOptions({
type: "catchall",
wordCapitalize: true,
wordIncludeNumber: false,
subaddressType: "random",
subaddressEmail: "foo@example.com",
catchallType: "random",
catchallDomain: "example.com",
forwardedService: Forwarders.AddyIo.id,
forwardedAnonAddyApiToken: "addyIoToken",
forwardedAnonAddyDomain: "addyio.example.com",
forwardedAnonAddyBaseUrl: "https://addyio.api.example.com",
forwardedDuckDuckGoToken: "ddgToken",
forwardedFirefoxApiToken: "firefoxToken",
forwardedFastmailApiToken: "fastmailToken",
forwardedForwardEmailApiToken: "forwardEmailToken",
forwardedForwardEmailDomain: "example.com",
forwardedSimpleLoginApiKey: "simpleLoginToken",
forwardedSimpleLoginBaseUrl: "https://simplelogin.api.example.com",
website: null,
});
expect(navigation.saveOptions).toHaveBeenCalledWith(SomeUser, {
type: "password",
username: "catchall",
forwarder: Forwarders.AddyIo.id,
});
expect(catchall.saveOptions).toHaveBeenCalledWith(SomeUser, {
catchallDomain: "example.com",
catchallType: "random",
website: null,
});
expect(effUsername.saveOptions).toHaveBeenCalledWith(SomeUser, {
wordCapitalize: true,
wordIncludeNumber: false,
website: null,
});
expect(subaddress.saveOptions).toHaveBeenCalledWith(SomeUser, {
subaddressType: "random",
subaddressEmail: "foo@example.com",
website: null,
});
expect(addyIo.saveOptions).toHaveBeenCalledWith(SomeUser, {
token: "addyIoToken",
domain: "addyio.example.com",
baseUrl: "https://addyio.api.example.com",
website: null,
});
expect(duckDuckGo.saveOptions).toHaveBeenCalledWith(SomeUser, {
token: "ddgToken",
website: null,
});
expect(fastmail.saveOptions).toHaveBeenCalledWith(SomeUser, {
token: "fastmailToken",
website: null,
});
expect(firefoxRelay.saveOptions).toHaveBeenCalledWith(SomeUser, {
token: "firefoxToken",
website: null,
});
expect(forwardEmail.saveOptions).toHaveBeenCalledWith(SomeUser, {
token: "forwardEmailToken",
domain: "example.com",
website: null,
});
expect(simpleLogin.saveOptions).toHaveBeenCalledWith(SomeUser, {
token: "simpleLoginToken",
baseUrl: "https://simplelogin.api.example.com",
website: null,
});
});
});
});

View File

@ -0,0 +1,383 @@
import { zip, firstValueFrom, map, concatMap } from "rxjs";
import { ApiService } from "../../abstractions/api.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "../../auth/abstractions/account.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateProvider } from "../../platform/state";
import { GeneratorService, GeneratorNavigationService } from "./abstractions";
import { UsernameGenerationServiceAbstraction } from "./abstractions/username-generation.service.abstraction";
import { DefaultGeneratorService } from "./default-generator.service";
import { DefaultGeneratorNavigationService } from "./navigation/default-generator-navigation.service";
import { GeneratorNavigation } from "./navigation/generator-navigation";
import { NoPolicy } from "./no-policy";
import {
CatchallGeneratorStrategy,
SubaddressGeneratorStrategy,
EffUsernameGeneratorStrategy,
} from "./username";
import { CatchallGenerationOptions } from "./username/catchall-generator-options";
import { EffUsernameGenerationOptions } from "./username/eff-username-generator-options";
import { AddyIoForwarder } from "./username/forwarders/addy-io";
import { DuckDuckGoForwarder } from "./username/forwarders/duck-duck-go";
import { FastmailForwarder } from "./username/forwarders/fastmail";
import { FirefoxRelayForwarder } from "./username/forwarders/firefox-relay";
import { ForwardEmailForwarder } from "./username/forwarders/forward-email";
import { SimpleLoginForwarder } from "./username/forwarders/simple-login";
import { Forwarders } from "./username/options/constants";
import {
ApiOptions,
EmailDomainOptions,
EmailPrefixOptions,
RequestOptions,
SelfHostedApiOptions,
} from "./username/options/forwarder-options";
import { SubaddressGenerationOptions } from "./username/subaddress-generator-options";
import { UsernameGeneratorOptions } from "./username/username-generation-options";
import { UsernameGenerationService } from "./username/username-generation.service";
type MappedOptions = {
generator: GeneratorNavigation;
algorithms: {
catchall: CatchallGenerationOptions;
effUsername: EffUsernameGenerationOptions;
subaddress: SubaddressGenerationOptions;
};
forwarders: {
addyIo: SelfHostedApiOptions & EmailDomainOptions & RequestOptions;
duckDuckGo: ApiOptions & RequestOptions;
fastmail: ApiOptions & EmailPrefixOptions & RequestOptions;
firefoxRelay: ApiOptions & RequestOptions;
forwardEmail: ApiOptions & EmailDomainOptions & RequestOptions;
simpleLogin: SelfHostedApiOptions & RequestOptions;
};
};
export function legacyPasswordGenerationServiceFactory(
apiService: ApiService,
i18nService: I18nService,
cryptoService: CryptoService,
encryptService: EncryptService,
policyService: PolicyService,
accountService: AccountService,
stateProvider: StateProvider,
): UsernameGenerationServiceAbstraction {
// FIXME: Once the username generation service is replaced with this service
// in the clients, factor out the deprecated service in its entirety.
const deprecatedService = new UsernameGenerationService(cryptoService, null, null);
const effUsername = new DefaultGeneratorService(
new EffUsernameGeneratorStrategy(deprecatedService, stateProvider),
policyService,
);
const subaddress = new DefaultGeneratorService(
new SubaddressGeneratorStrategy(deprecatedService, stateProvider),
policyService,
);
const catchall = new DefaultGeneratorService(
new CatchallGeneratorStrategy(deprecatedService, stateProvider),
policyService,
);
const addyIo = new DefaultGeneratorService(
new AddyIoForwarder(apiService, i18nService, encryptService, cryptoService, stateProvider),
policyService,
);
const duckDuckGo = new DefaultGeneratorService(
new DuckDuckGoForwarder(apiService, i18nService, encryptService, cryptoService, stateProvider),
policyService,
);
const fastmail = new DefaultGeneratorService(
new FastmailForwarder(apiService, i18nService, encryptService, cryptoService, stateProvider),
policyService,
);
const firefoxRelay = new DefaultGeneratorService(
new FirefoxRelayForwarder(
apiService,
i18nService,
encryptService,
cryptoService,
stateProvider,
),
policyService,
);
const forwardEmail = new DefaultGeneratorService(
new ForwardEmailForwarder(
apiService,
i18nService,
encryptService,
cryptoService,
stateProvider,
),
policyService,
);
const simpleLogin = new DefaultGeneratorService(
new SimpleLoginForwarder(apiService, i18nService, encryptService, cryptoService, stateProvider),
policyService,
);
const navigation = new DefaultGeneratorNavigationService(stateProvider, policyService);
return new LegacyUsernameGenerationService(
accountService,
navigation,
catchall,
effUsername,
subaddress,
addyIo,
duckDuckGo,
fastmail,
firefoxRelay,
forwardEmail,
simpleLogin,
);
}
/** Adapts the generator 2.0 design to 1.0 angular services. */
export class LegacyUsernameGenerationService implements UsernameGenerationServiceAbstraction {
constructor(
private readonly accountService: AccountService,
private readonly navigation: GeneratorNavigationService,
private readonly catchall: GeneratorService<CatchallGenerationOptions, NoPolicy>,
private readonly effUsername: GeneratorService<EffUsernameGenerationOptions, NoPolicy>,
private readonly subaddress: GeneratorService<SubaddressGenerationOptions, NoPolicy>,
private readonly addyIo: GeneratorService<SelfHostedApiOptions & EmailDomainOptions, NoPolicy>,
private readonly duckDuckGo: GeneratorService<ApiOptions, NoPolicy>,
private readonly fastmail: GeneratorService<ApiOptions & EmailPrefixOptions, NoPolicy>,
private readonly firefoxRelay: GeneratorService<ApiOptions, NoPolicy>,
private readonly forwardEmail: GeneratorService<ApiOptions & EmailDomainOptions, NoPolicy>,
private readonly simpleLogin: GeneratorService<SelfHostedApiOptions, NoPolicy>,
) {}
generateUsername(options: UsernameGeneratorOptions) {
if (options.type === "catchall") {
return this.generateCatchall(options);
} else if (options.type === "subaddress") {
return this.generateSubaddress(options);
} else if (options.type === "forwarded") {
return this.generateForwarded(options);
} else {
return this.generateWord(options);
}
}
generateWord(options: UsernameGeneratorOptions) {
return this.effUsername.generate(options);
}
generateSubaddress(options: UsernameGeneratorOptions) {
return this.subaddress.generate(options);
}
generateCatchall(options: UsernameGeneratorOptions) {
return this.catchall.generate(options);
}
generateForwarded(options: UsernameGeneratorOptions) {
if (!options.forwardedService) {
return null;
}
const stored = this.toStoredOptions(options);
switch (options.forwardedService) {
case Forwarders.AddyIo.id:
return this.addyIo.generate(stored.forwarders.addyIo);
case Forwarders.DuckDuckGo.id:
return this.duckDuckGo.generate(stored.forwarders.duckDuckGo);
case Forwarders.Fastmail.id:
return this.fastmail.generate(stored.forwarders.fastmail);
case Forwarders.FirefoxRelay.id:
return this.firefoxRelay.generate(stored.forwarders.firefoxRelay);
case Forwarders.ForwardEmail.id:
return this.forwardEmail.generate(stored.forwarders.forwardEmail);
case Forwarders.SimpleLogin.id:
return this.simpleLogin.generate(stored.forwarders.simpleLogin);
}
}
getOptions() {
const options$ = this.accountService.activeAccount$.pipe(
concatMap((account) =>
zip(
this.navigation.options$(account.id),
this.navigation.defaults$(account.id),
this.catchall.options$(account.id),
this.catchall.defaults$(account.id),
this.effUsername.options$(account.id),
this.effUsername.defaults$(account.id),
this.subaddress.options$(account.id),
this.subaddress.defaults$(account.id),
this.addyIo.options$(account.id),
this.addyIo.defaults$(account.id),
this.duckDuckGo.options$(account.id),
this.duckDuckGo.defaults$(account.id),
this.fastmail.options$(account.id),
this.fastmail.defaults$(account.id),
this.firefoxRelay.options$(account.id),
this.firefoxRelay.defaults$(account.id),
this.forwardEmail.options$(account.id),
this.forwardEmail.defaults$(account.id),
this.simpleLogin.options$(account.id),
this.simpleLogin.defaults$(account.id),
),
),
map(
([
generatorOptions,
generatorDefaults,
catchallOptions,
catchallDefaults,
effUsernameOptions,
effUsernameDefaults,
subaddressOptions,
subaddressDefaults,
addyIoOptions,
addyIoDefaults,
duckDuckGoOptions,
duckDuckGoDefaults,
fastmailOptions,
fastmailDefaults,
firefoxRelayOptions,
firefoxRelayDefaults,
forwardEmailOptions,
forwardEmailDefaults,
simpleLoginOptions,
simpleLoginDefaults,
]) =>
this.toUsernameOptions({
generator: generatorOptions ?? generatorDefaults,
algorithms: {
catchall: catchallOptions ?? catchallDefaults,
effUsername: effUsernameOptions ?? effUsernameDefaults,
subaddress: subaddressOptions ?? subaddressDefaults,
},
forwarders: {
addyIo: addyIoOptions ?? addyIoDefaults,
duckDuckGo: duckDuckGoOptions ?? duckDuckGoDefaults,
fastmail: fastmailOptions ?? fastmailDefaults,
firefoxRelay: firefoxRelayOptions ?? firefoxRelayDefaults,
forwardEmail: forwardEmailOptions ?? forwardEmailDefaults,
simpleLogin: simpleLoginOptions ?? simpleLoginDefaults,
},
}),
),
);
return firstValueFrom(options$);
}
async saveOptions(options: UsernameGeneratorOptions) {
const stored = this.toStoredOptions(options);
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a.id)));
// generator settings needs to preserve whether password or passphrase is selected,
// so `navigationOptions` is mutated.
let navigationOptions = await firstValueFrom(this.navigation.options$(userId));
navigationOptions = Object.assign(navigationOptions, stored.generator);
await this.navigation.saveOptions(userId, navigationOptions);
// overwrite all other settings with latest values
await Promise.all([
this.catchall.saveOptions(userId, stored.algorithms.catchall),
this.effUsername.saveOptions(userId, stored.algorithms.effUsername),
this.subaddress.saveOptions(userId, stored.algorithms.subaddress),
this.addyIo.saveOptions(userId, stored.forwarders.addyIo),
this.duckDuckGo.saveOptions(userId, stored.forwarders.duckDuckGo),
this.fastmail.saveOptions(userId, stored.forwarders.fastmail),
this.firefoxRelay.saveOptions(userId, stored.forwarders.firefoxRelay),
this.forwardEmail.saveOptions(userId, stored.forwarders.forwardEmail),
this.simpleLogin.saveOptions(userId, stored.forwarders.simpleLogin),
]);
}
private toStoredOptions(options: UsernameGeneratorOptions) {
const forwarders = {
addyIo: {
baseUrl: options.forwardedAnonAddyBaseUrl,
token: options.forwardedAnonAddyApiToken,
domain: options.forwardedAnonAddyDomain,
website: options.website,
},
duckDuckGo: {
token: options.forwardedDuckDuckGoToken,
website: options.website,
},
fastmail: {
token: options.forwardedFastmailApiToken,
website: options.website,
},
firefoxRelay: {
token: options.forwardedFirefoxApiToken,
website: options.website,
},
forwardEmail: {
token: options.forwardedForwardEmailApiToken,
domain: options.forwardedForwardEmailDomain,
website: options.website,
},
simpleLogin: {
token: options.forwardedSimpleLoginApiKey,
baseUrl: options.forwardedSimpleLoginBaseUrl,
website: options.website,
},
};
const generator = {
username: options.type,
forwarder: options.forwardedService,
};
const algorithms = {
effUsername: {
wordCapitalize: options.wordCapitalize,
wordIncludeNumber: options.wordIncludeNumber,
website: options.website,
},
subaddress: {
subaddressType: options.subaddressType,
subaddressEmail: options.subaddressEmail,
website: options.website,
},
catchall: {
catchallType: options.catchallType,
catchallDomain: options.catchallDomain,
website: options.website,
},
};
return { generator, algorithms, forwarders } as MappedOptions;
}
private toUsernameOptions(options: MappedOptions) {
return {
type: options.generator.username,
wordCapitalize: options.algorithms.effUsername.wordCapitalize,
wordIncludeNumber: options.algorithms.effUsername.wordIncludeNumber,
subaddressType: options.algorithms.subaddress.subaddressType,
subaddressEmail: options.algorithms.subaddress.subaddressEmail,
catchallType: options.algorithms.catchall.catchallType,
catchallDomain: options.algorithms.catchall.catchallDomain,
forwardedService: options.generator.forwarder,
forwardedAnonAddyApiToken: options.forwarders.addyIo.token,
forwardedAnonAddyDomain: options.forwarders.addyIo.domain,
forwardedAnonAddyBaseUrl: options.forwarders.addyIo.baseUrl,
forwardedDuckDuckGoToken: options.forwarders.duckDuckGo.token,
forwardedFirefoxApiToken: options.forwarders.firefoxRelay.token,
forwardedFastmailApiToken: options.forwarders.fastmail.token,
forwardedForwardEmailApiToken: options.forwarders.forwardEmail.token,
forwardedForwardEmailDomain: options.forwarders.forwardEmail.domain,
forwardedSimpleLoginApiKey: options.forwarders.simpleLogin.token,
forwardedSimpleLoginBaseUrl: options.forwarders.simpleLogin.baseUrl,
} as UsernameGeneratorOptions;
}
}

View File

@ -0,0 +1,100 @@
/**
* include structuredClone in test environment.
* @jest-environment ../../../../shared/test.environment.ts
*/
import { mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "../../../admin-console/enums";
// FIXME: use index.ts imports once policy abstractions and models
// implement ADR-0002
import { Policy } from "../../../admin-console/models/domain/policy";
import { UserId } from "../../../types/guid";
import { GENERATOR_SETTINGS } from "../key-definitions";
import {
GeneratorNavigationEvaluator,
DefaultGeneratorNavigationService,
DefaultGeneratorNavigation,
} from "./";
const SomeUser = "some user" as UserId;
describe("DefaultGeneratorNavigationService", () => {
describe("options$", () => {
it("emits options", async () => {
const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser));
const settings = { type: "password" as const };
await stateProvider.setUserState(GENERATOR_SETTINGS, settings, SomeUser);
const navigation = new DefaultGeneratorNavigationService(stateProvider, null);
const result = await firstValueFrom(navigation.options$(SomeUser));
expect(result).toEqual(settings);
});
});
describe("defaults$", () => {
it("emits default options", async () => {
const navigation = new DefaultGeneratorNavigationService(null, null);
const result = await firstValueFrom(navigation.defaults$(SomeUser));
expect(result).toEqual(DefaultGeneratorNavigation);
});
});
describe("evaluator$", () => {
it("emits a GeneratorNavigationEvaluator", async () => {
const policyService = mock<PolicyService>({
getAll$() {
return of([]);
},
});
const navigation = new DefaultGeneratorNavigationService(null, policyService);
const result = await firstValueFrom(navigation.evaluator$(SomeUser));
expect(result).toBeInstanceOf(GeneratorNavigationEvaluator);
});
});
describe("enforcePolicy", () => {
it("applies policy", async () => {
const policyService = mock<PolicyService>({
getAll$(_type: PolicyType, _user: UserId) {
return of([
new Policy({
id: "" as any,
organizationId: "" as any,
enabled: true,
type: PolicyType.PasswordGenerator,
data: { defaultType: "password" },
}),
]);
},
});
const navigation = new DefaultGeneratorNavigationService(null, policyService);
const options = {};
const result = await navigation.enforcePolicy(SomeUser, options);
expect(result).toMatchObject({ type: "password" });
});
});
describe("saveOptions", () => {
it("updates options$", async () => {
const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser));
const navigation = new DefaultGeneratorNavigationService(stateProvider, null);
const settings = { type: "password" as const };
await navigation.saveOptions(SomeUser, settings);
const result = await firstValueFrom(navigation.options$(SomeUser));
expect(result).toEqual(settings);
});
});
});

View File

@ -0,0 +1,71 @@
import { BehaviorSubject, Observable, firstValueFrom, map } from "rxjs";
import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "../../../admin-console/enums";
import { StateProvider } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { GeneratorNavigationService } from "../abstractions/generator-navigation.service.abstraction";
import { GENERATOR_SETTINGS } from "../key-definitions";
import { reduceCollection } from "../reduce-collection.operator";
import { DefaultGeneratorNavigation, GeneratorNavigation } from "./generator-navigation";
import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator";
import { DisabledGeneratorNavigationPolicy, preferPassword } from "./generator-navigation-policy";
export class DefaultGeneratorNavigationService implements GeneratorNavigationService {
/** instantiates the password generator strategy.
* @param stateProvider provides durable state
* @param policy provides the policy to enforce
*/
constructor(
private readonly stateProvider: StateProvider,
private readonly policy: PolicyService,
) {}
/** An observable monitoring the options saved to disk.
* The observable updates when the options are saved.
* @param userId: Identifies the user making the request
*/
options$(userId: UserId): Observable<GeneratorNavigation> {
return this.stateProvider.getUserState$(GENERATOR_SETTINGS, userId);
}
/** Gets the default options. */
defaults$(userId: UserId): Observable<GeneratorNavigation> {
return new BehaviorSubject({ ...DefaultGeneratorNavigation });
}
/** An observable monitoring the options used to enforce policy.
* The observable updates when the policy changes.
* @param userId: Identifies the user making the request
*/
evaluator$(userId: UserId) {
const evaluator$ = this.policy.getAll$(PolicyType.PasswordGenerator, userId).pipe(
reduceCollection(preferPassword, DisabledGeneratorNavigationPolicy),
map((policy) => new GeneratorNavigationEvaluator(policy)),
);
return evaluator$;
}
/** Enforces the policy on the given options
* @param userId: Identifies the user making the request
* @param options the options to enforce the policy on
* @returns a new instance of the options with the policy enforced
*/
async enforcePolicy(userId: UserId, options: GeneratorNavigation) {
const evaluator = await firstValueFrom(this.evaluator$(userId));
const applied = evaluator.applyPolicy(options);
const sanitized = evaluator.sanitize(applied);
return sanitized;
}
/** Saves the navigation options to disk.
* @param userId: Identifies the user making the request
* @param options the options to save
* @returns a promise that resolves when the options are saved
*/
async saveOptions(userId: UserId, options: GeneratorNavigation): Promise<void> {
await this.stateProvider.setUserState(GENERATOR_SETTINGS, options, userId);
}
}

View File

@ -0,0 +1,64 @@
import { DefaultGeneratorNavigation } from "./generator-navigation";
import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator";
describe("GeneratorNavigationEvaluator", () => {
describe("policyInEffect", () => {
it.each([["passphrase"], ["password"]] as const)(
"returns true if the policy has a defaultType (= %p)",
(defaultType) => {
const evaluator = new GeneratorNavigationEvaluator({ defaultType });
expect(evaluator.policyInEffect).toEqual(true);
},
);
it.each([[undefined], [null], ["" as any]])(
"returns false if the policy has a falsy defaultType (= %p)",
(defaultType) => {
const evaluator = new GeneratorNavigationEvaluator({ defaultType });
expect(evaluator.policyInEffect).toEqual(false);
},
);
});
describe("applyPolicy", () => {
it("returns the input options", () => {
const evaluator = new GeneratorNavigationEvaluator(null);
const options = { type: "password" as const };
const result = evaluator.applyPolicy(options);
expect(result).toEqual(options);
});
});
describe("sanitize", () => {
it.each([["passphrase"], ["password"]] as const)(
"defaults options to the policy's default type (= %p) when a policy is in effect",
(defaultType) => {
const evaluator = new GeneratorNavigationEvaluator({ defaultType });
const result = evaluator.sanitize({});
expect(result).toEqual({ type: defaultType });
},
);
it("defaults options to the default generator navigation type when a policy is not in effect", () => {
const evaluator = new GeneratorNavigationEvaluator(null);
const result = evaluator.sanitize({});
expect(result.type).toEqual(DefaultGeneratorNavigation.type);
});
it("retains the options type when it is set", () => {
const evaluator = new GeneratorNavigationEvaluator({ defaultType: "passphrase" });
const result = evaluator.sanitize({ type: "password" });
expect(result).toEqual({ type: "password" });
});
});
});

View File

@ -0,0 +1,43 @@
import { PolicyEvaluator } from "../abstractions";
import { DefaultGeneratorNavigation, GeneratorNavigation } from "./generator-navigation";
import { GeneratorNavigationPolicy } from "./generator-navigation-policy";
/** Enforces policy for generator navigation options.
*/
export class GeneratorNavigationEvaluator
implements PolicyEvaluator<GeneratorNavigationPolicy, GeneratorNavigation>
{
/** Instantiates the evaluator.
* @param policy The policy applied by the evaluator. When this conflicts with
* the defaults, the policy takes precedence.
*/
constructor(readonly policy: GeneratorNavigationPolicy) {}
/** {@link PolicyEvaluator.policyInEffect} */
get policyInEffect(): boolean {
return this.policy?.defaultType ? true : false;
}
/** Apply policy to the input options.
* @param options The options to build from. These options are not altered.
* @returns A new password generation request with policy applied.
*/
applyPolicy(options: GeneratorNavigation): GeneratorNavigation {
return options;
}
/** Ensures internal options consistency.
* @param options The options to cascade. These options are not altered.
* @returns A passphrase generation request with cascade applied.
*/
sanitize(options: GeneratorNavigation): GeneratorNavigation {
const defaultType = this.policyInEffect
? this.policy.defaultType
: DefaultGeneratorNavigation.type;
return {
...options,
type: options.type ?? defaultType,
};
}
}

View File

@ -0,0 +1,63 @@
import { PolicyType } from "../../../admin-console/enums";
// FIXME: use index.ts imports once policy abstractions and models
// implement ADR-0002
import { Policy } from "../../../admin-console/models/domain/policy";
import { PolicyId } from "../../../types/guid";
import { DisabledGeneratorNavigationPolicy, preferPassword } from "./generator-navigation-policy";
function createPolicy(
data: any,
type: PolicyType = PolicyType.PasswordGenerator,
enabled: boolean = true,
) {
return new Policy({
id: "id" as PolicyId,
organizationId: "organizationId",
data,
enabled,
type,
});
}
describe("leastPrivilege", () => {
it("should return the accumulator when the policy type does not apply", () => {
const policy = createPolicy({}, PolicyType.RequireSso);
const result = preferPassword(DisabledGeneratorNavigationPolicy, policy);
expect(result).toEqual(DisabledGeneratorNavigationPolicy);
});
it("should return the accumulator when the policy is not enabled", () => {
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
const result = preferPassword(DisabledGeneratorNavigationPolicy, policy);
expect(result).toEqual(DisabledGeneratorNavigationPolicy);
});
it("should take the %p from the policy", () => {
const policy = createPolicy({ defaultType: "passphrase" });
const result = preferPassword({ ...DisabledGeneratorNavigationPolicy }, policy);
expect(result).toEqual({ defaultType: "passphrase" });
});
it("should override passphrase with password", () => {
const policy = createPolicy({ defaultType: "password" });
const result = preferPassword({ defaultType: "passphrase" }, policy);
expect(result).toEqual({ defaultType: "password" });
});
it("should not override password", () => {
const policy = createPolicy({ defaultType: "passphrase" });
const result = preferPassword({ defaultType: "password" }, policy);
expect(result).toEqual({ defaultType: "password" });
});
});

View File

@ -0,0 +1,39 @@
import { PolicyType } from "../../../admin-console/enums";
// FIXME: use index.ts imports once policy abstractions and models
// implement ADR-0002
import { Policy } from "../../../admin-console/models/domain/policy";
import { GeneratorType } from "../generator-type";
/** Policy settings affecting password generator navigation */
export type GeneratorNavigationPolicy = {
/** The type of generator that should be shown by default when opening
* the password generator.
*/
defaultType?: GeneratorType;
};
/** Reduces a policy into an accumulator by preferring the password generator
* type to other generator types.
* @param acc the accumulator
* @param policy the policy to reduce
* @returns the resulting `GeneratorNavigationPolicy`
*/
export function preferPassword(
acc: GeneratorNavigationPolicy,
policy: Policy,
): GeneratorNavigationPolicy {
const isEnabled = policy.type === PolicyType.PasswordGenerator && policy.enabled;
if (!isEnabled) {
return acc;
}
const isOverridable = acc.defaultType !== "password" && policy.data.defaultType;
const result = isOverridable ? { ...acc, defaultType: policy.data.defaultType } : acc;
return result;
}
/** The default options for password generation policy. */
export const DisabledGeneratorNavigationPolicy: GeneratorNavigationPolicy = Object.freeze({
defaultType: undefined,
});

View File

@ -0,0 +1,26 @@
import { GeneratorType } from "../generator-type";
import { ForwarderId } from "../username/options";
import { UsernameGeneratorType } from "../username/options/generator-options";
/** Stores credential generator UI state. */
export type GeneratorNavigation = {
/** The kind of credential being generated.
* @remarks The legacy generator only supports "password" and "passphrase".
* The componentized generator supports all values.
*/
type?: GeneratorType;
/** When `type === "username"`, this stores the username algorithm. */
username?: UsernameGeneratorType;
/** When `username === "forwarded"`, this stores the forwarder implementation. */
forwarder?: ForwarderId | "";
};
/** The default options for password generation. */
export const DefaultGeneratorNavigation: Partial<GeneratorNavigation> = Object.freeze({
type: "password",
username: "word",
forwarder: "",
});

View File

@ -0,0 +1,3 @@
export { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator";
export { DefaultGeneratorNavigationService } from "./default-generator-navigation.service";
export { GeneratorNavigation, DefaultGeneratorNavigation } from "./generator-navigation";

View File

@ -2,4 +2,7 @@
export { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
export { PassphraseGeneratorPolicy } from "./passphrase-generator-policy";
export { PassphraseGeneratorStrategy } from "./passphrase-generator-strategy";
export { DefaultPassphraseGenerationOptions } from "./passphrase-generation-options";
export {
DefaultPassphraseGenerationOptions,
PassphraseGenerationOptions,
} from "./passphrase-generation-options";

View File

@ -2,7 +2,6 @@
* include structuredClone in test environment.
* @jest-environment ../../../../shared/test.environment.ts
*/
import { mock } from "jest-mock-extended";
import { of, firstValueFrom } from "rxjs";
@ -12,12 +11,16 @@ import { PolicyType } from "../../../admin-console/enums";
import { Policy } from "../../../admin-console/models/domain/policy";
import { StateProvider } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction";
import { PASSPHRASE_SETTINGS } from "../key-definitions";
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
import { DisabledPassphraseGeneratorPolicy } from "./passphrase-generator-policy";
import { PassphraseGeneratorOptionsEvaluator, PassphraseGeneratorStrategy } from ".";
import {
DefaultPassphraseGenerationOptions,
PassphraseGeneratorOptionsEvaluator,
PassphraseGeneratorStrategy,
} from ".";
const SomeUser = "some user" as UserId;
@ -71,6 +74,16 @@ describe("Password generation strategy", () => {
});
});
describe("defaults$", () => {
it("should return the default subaddress options", async () => {
const strategy = new PassphraseGeneratorStrategy(null, null);
const result = await firstValueFrom(strategy.defaults$(SomeUser));
expect(result).toEqual(DefaultPassphraseGenerationOptions);
});
});
describe("cache_ms", () => {
it("should be a positive non-zero number", () => {
const legacy = mock<PasswordGenerationServiceAbstraction>();

View File

@ -1,14 +1,17 @@
import { map, pipe } from "rxjs";
import { BehaviorSubject, map, pipe } from "rxjs";
import { GeneratorStrategy } from "..";
import { PolicyType } from "../../../admin-console/enums";
import { StateProvider } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction";
import { PASSPHRASE_SETTINGS } from "../key-definitions";
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
import { reduceCollection } from "../reduce-collection.operator";
import { PassphraseGenerationOptions } from "./passphrase-generation-options";
import {
PassphraseGenerationOptions,
DefaultPassphraseGenerationOptions,
} from "./passphrase-generation-options";
import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
import {
DisabledPassphraseGeneratorPolicy,
@ -36,6 +39,11 @@ export class PassphraseGeneratorStrategy
return this.stateProvider.getUser(id, PASSPHRASE_SETTINGS);
}
/** Gets the default options. */
defaults$(_: UserId) {
return new BehaviorSubject({ ...DefaultPassphraseGenerationOptions }).asObservable();
}
/** {@link GeneratorStrategy.policy} */
get policy() {
return PolicyType.PasswordGenerator;

View File

@ -6,6 +6,6 @@ export { PasswordGeneratorStrategy } from "./password-generator-strategy";
// legacy interfaces
export { PasswordGeneratorOptions } from "./password-generator-options";
export { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
export { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction";
export { PasswordGenerationService } from "./password-generation.service";
export { GeneratedPasswordHistory } from "./generated-password-history";

View File

@ -5,10 +5,10 @@ import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { EFFLongWordList } from "../../../platform/misc/wordlist";
import { EncString } from "../../../platform/models/domain/enc-string";
import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction";
import { PassphraseGeneratorOptionsEvaluator } from "../passphrase/passphrase-generator-options-evaluator";
import { GeneratedPasswordHistory } from "./generated-password-history";
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
import { PasswordGeneratorOptions } from "./password-generator-options";
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
@ -341,24 +341,6 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId });
}
normalizeOptions(
options: PasswordGeneratorOptions,
enforcedPolicyOptions: PasswordGeneratorPolicyOptions,
) {
const evaluator =
options.type == "password"
? new PasswordGeneratorOptionsEvaluator(enforcedPolicyOptions)
: new PassphraseGeneratorOptionsEvaluator(enforcedPolicyOptions);
const evaluatedOptions = evaluator.applyPolicy(options);
const santizedOptions = evaluator.sanitize(evaluatedOptions);
// callers assume this function updates the options parameter
Object.assign(options, santizedOptions);
return options;
}
private capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}

View File

@ -1,3 +1,4 @@
import { GeneratorNavigation } from "../navigation/generator-navigation";
import { PassphraseGenerationOptions } from "../passphrase/passphrase-generation-options";
import { PasswordGenerationOptions } from "./password-generation-options";
@ -6,12 +7,5 @@ import { PasswordGenerationOptions } from "./password-generation-options";
* This type includes all properties suitable for reactive data binding.
*/
export type PasswordGeneratorOptions = PasswordGenerationOptions &
PassphraseGenerationOptions & {
/** The algorithm to use for credential generation.
* Properties on @see PasswordGenerationOptions should be processed
* only when `type === "password"`.
* Properties on @see PassphraseGenerationOptions should be processed
* only when `type === "passphrase"`.
*/
type?: "password" | "passphrase";
};
PassphraseGenerationOptions &
GeneratorNavigation;

View File

@ -17,6 +17,7 @@ import { PASSWORD_SETTINGS } from "../key-definitions";
import { DisabledPasswordGeneratorPolicy } from "./password-generator-policy";
import {
DefaultPasswordGenerationOptions,
PasswordGenerationServiceAbstraction,
PasswordGeneratorOptionsEvaluator,
PasswordGeneratorStrategy,
@ -82,6 +83,16 @@ describe("Password generation strategy", () => {
});
});
describe("defaults$", () => {
it("should return the default subaddress options", async () => {
const strategy = new PasswordGeneratorStrategy(null, null);
const result = await firstValueFrom(strategy.defaults$(SomeUser));
expect(result).toEqual(DefaultPasswordGenerationOptions);
});
});
describe("cache_ms", () => {
it("should be a positive non-zero number", () => {
const legacy = mock<PasswordGenerationServiceAbstraction>();

View File

@ -1,14 +1,17 @@
import { map, pipe } from "rxjs";
import { BehaviorSubject, map, pipe } from "rxjs";
import { GeneratorStrategy } from "..";
import { PolicyType } from "../../../admin-console/enums";
import { StateProvider } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction";
import { PASSWORD_SETTINGS } from "../key-definitions";
import { reduceCollection } from "../reduce-collection.operator";
import { PasswordGenerationOptions } from "./password-generation-options";
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
import {
DefaultPasswordGenerationOptions,
PasswordGenerationOptions,
} from "./password-generation-options";
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
import {
DisabledPasswordGeneratorPolicy,
@ -35,6 +38,11 @@ export class PasswordGeneratorStrategy
return this.stateProvider.getUser(id, PASSWORD_SETTINGS);
}
/** Gets the default options. */
defaults$(_: UserId) {
return new BehaviorSubject({ ...DefaultPasswordGenerationOptions }).asObservable();
}
/** {@link GeneratorStrategy.policy} */
get policy() {
return PolicyType.PasswordGenerator;

View File

@ -1,10 +1,21 @@
import { RequestOptions } from "./options/forwarder-options";
import { UsernameGenerationMode } from "./options/generator-options";
/** Settings supported when generating an email subaddress */
export type CatchallGenerationOptions = {
type?: "random" | "website-name";
domain?: string;
};
/** selects the generation algorithm for the catchall email address. */
catchallType?: UsernameGenerationMode;
/** The default options for email subaddress generation. */
export const DefaultCatchallOptions: Partial<CatchallGenerationOptions> = Object.freeze({
type: "random",
/** The domain part of the generated email address.
* @example If the domain is `domain.io` and the generated username
* is `jd`, then the generated email address will be `jd@mydomain.io`
*/
catchallDomain?: string;
} & RequestOptions;
/** The default options for catchall address generation. */
export const DefaultCatchallOptions: CatchallGenerationOptions = Object.freeze({
catchallType: "random",
catchallDomain: "",
website: null,
});

View File

@ -10,6 +10,8 @@ import { UserId } from "../../../types/guid";
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
import { CATCHALL_SETTINGS } from "../key-definitions";
import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options";
import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
const SomeUser = "some user" as UserId;
@ -47,6 +49,16 @@ describe("Email subaddress list generation strategy", () => {
});
});
describe("defaults$", () => {
it("should return the default subaddress options", async () => {
const strategy = new CatchallGeneratorStrategy(null, null);
const result = await firstValueFrom(strategy.defaults$(SomeUser));
expect(result).toEqual(DefaultCatchallOptions);
});
});
describe("cache_ms", () => {
it("should be a positive non-zero number", () => {
const legacy = mock<UsernameGenerationServiceAbstraction>();
@ -70,16 +82,14 @@ describe("Email subaddress list generation strategy", () => {
const legacy = mock<UsernameGenerationServiceAbstraction>();
const strategy = new CatchallGeneratorStrategy(legacy, null);
const options = {
type: "website-name" as const,
domain: "example.com",
};
catchallType: "website-name",
catchallDomain: "example.com",
website: "foo.com",
} as CatchallGenerationOptions;
await strategy.generate(options);
expect(legacy.generateCatchall).toHaveBeenCalledWith({
catchallType: "website-name" as const,
catchallDomain: "example.com",
});
expect(legacy.generateCatchall).toHaveBeenCalledWith(options);
});
});
});

View File

@ -1,15 +1,15 @@
import { map, pipe } from "rxjs";
import { BehaviorSubject, map, pipe } from "rxjs";
import { PolicyType } from "../../../admin-console/enums";
import { StateProvider } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { GeneratorStrategy } from "../abstractions";
import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction";
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
import { CATCHALL_SETTINGS } from "../key-definitions";
import { NoPolicy } from "../no-policy";
import { CatchallGenerationOptions } from "./catchall-generator-options";
import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction";
import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options";
const ONE_MINUTE = 60 * 1000;
@ -30,6 +30,11 @@ export class CatchallGeneratorStrategy
return this.stateProvider.getUser(id, CATCHALL_SETTINGS);
}
/** {@link GeneratorStrategy.defaults$} */
defaults$(userId: UserId) {
return new BehaviorSubject({ ...DefaultCatchallOptions }).asObservable();
}
/** {@link GeneratorStrategy.policy} */
get policy() {
// Uses password generator since there aren't policies
@ -49,9 +54,6 @@ export class CatchallGeneratorStrategy
/** {@link GeneratorStrategy.generate} */
generate(options: CatchallGenerationOptions) {
return this.usernameService.generateCatchall({
catchallDomain: options.domain,
catchallType: options.type,
});
return this.usernameService.generateCatchall(options);
}
}

View File

@ -1,11 +1,17 @@
/** Settings supported when generating an ASCII username */
import { RequestOptions } from "./options/forwarder-options";
/** Settings supported when generating a username using the EFF word list */
export type EffUsernameGenerationOptions = {
/** when true, the word is capitalized */
wordCapitalize?: boolean;
/** when true, a random number is appended to the username */
wordIncludeNumber?: boolean;
};
} & RequestOptions;
/** The default options for EFF long word generation. */
export const DefaultEffUsernameOptions: Partial<EffUsernameGenerationOptions> = Object.freeze({
export const DefaultEffUsernameOptions: EffUsernameGenerationOptions = Object.freeze({
wordCapitalize: false,
wordIncludeNumber: false,
website: null,
});

View File

@ -10,6 +10,8 @@ import { UserId } from "../../../types/guid";
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
import { DefaultEffUsernameOptions } from "./eff-username-generator-options";
import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
const SomeUser = "some user" as UserId;
@ -47,6 +49,16 @@ describe("EFF long word list generation strategy", () => {
});
});
describe("defaults$", () => {
it("should return the default subaddress options", async () => {
const strategy = new EffUsernameGeneratorStrategy(null, null);
const result = await firstValueFrom(strategy.defaults$(SomeUser));
expect(result).toEqual(DefaultEffUsernameOptions);
});
});
describe("cache_ms", () => {
it("should be a positive non-zero number", () => {
const legacy = mock<UsernameGenerationServiceAbstraction>();
@ -72,6 +84,7 @@ describe("EFF long word list generation strategy", () => {
const options = {
wordCapitalize: false,
wordIncludeNumber: false,
website: null as string,
};
await strategy.generate(options);

View File

@ -1,15 +1,18 @@
import { map, pipe } from "rxjs";
import { BehaviorSubject, map, pipe } from "rxjs";
import { PolicyType } from "../../../admin-console/enums";
import { StateProvider } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { GeneratorStrategy } from "../abstractions";
import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction";
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
import { NoPolicy } from "../no-policy";
import { EffUsernameGenerationOptions } from "./eff-username-generator-options";
import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction";
import {
DefaultEffUsernameOptions,
EffUsernameGenerationOptions,
} from "./eff-username-generator-options";
const ONE_MINUTE = 60 * 1000;
@ -30,6 +33,11 @@ export class EffUsernameGeneratorStrategy
return this.stateProvider.getUser(id, EFF_USERNAME_SETTINGS);
}
/** {@link GeneratorStrategy.defaults$} */
defaults$(userId: UserId) {
return new BehaviorSubject({ ...DefaultEffUsernameOptions }).asObservable();
}
/** {@link GeneratorStrategy.policy} */
get policy() {
// Uses password generator since there aren't policies

View File

@ -15,6 +15,7 @@ import { DUCK_DUCK_GO_FORWARDER } from "../key-definitions";
import { SecretState } from "../state/secret-state";
import { ForwarderGeneratorStrategy } from "./forwarder-generator-strategy";
import { DefaultDuckDuckGoOptions } from "./forwarders/duck-duck-go";
import { ApiOptions } from "./options/forwarder-options";
class TestForwarder extends ForwarderGeneratorStrategy<ApiOptions> {
@ -30,6 +31,10 @@ class TestForwarder extends ForwarderGeneratorStrategy<ApiOptions> {
// arbitrary.
return DUCK_DUCK_GO_FORWARDER;
}
defaults$ = (userId: UserId) => {
return of(DefaultDuckDuckGoOptions);
};
}
const SomeUser = "some user" as UserId;

View File

@ -1,4 +1,4 @@
import { map, pipe } from "rxjs";
import { Observable, map, pipe } from "rxjs";
import { PolicyType } from "../../../admin-console/enums";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
@ -79,6 +79,9 @@ export abstract class ForwarderGeneratorStrategy<
return new UserKeyEncryptor(this.encryptService, this.keyService, packer);
}
/** Gets the default options. */
abstract defaults$: (userId: UserId) => Observable<Options>;
/** Determine where forwarder configuration is stored */
protected abstract readonly key: KeyDefinition<Options>;

View File

@ -2,12 +2,17 @@
* include Request in test environment.
* @jest-environment ../../../../shared/test.environment.ts
*/
import { firstValueFrom } from "rxjs";
import { UserId } from "../../../../types/guid";
import { ADDY_IO_FORWARDER } from "../../key-definitions";
import { Forwarders } from "../options/constants";
import { AddyIoForwarder } from "./addy-io";
import { AddyIoForwarder, DefaultAddyIoOptions } from "./addy-io";
import { mockApiService, mockI18nService } from "./mocks.jest";
const SomeUser = "some user" as UserId;
describe("Addy.io Forwarder", () => {
it("key returns the Addy IO forwarder key", () => {
const forwarder = new AddyIoForwarder(null, null, null, null, null);
@ -15,6 +20,16 @@ describe("Addy.io Forwarder", () => {
expect(forwarder.key).toBe(ADDY_IO_FORWARDER);
});
describe("defaults$", () => {
it("should return the default subaddress options", async () => {
const strategy = new AddyIoForwarder(null, null, null, null, null);
const result = await firstValueFrom(strategy.defaults$(SomeUser));
expect(result).toEqual(DefaultAddyIoOptions);
});
});
describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => {
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => {
const apiService = mockApiService(200, {});

View File

@ -1,13 +1,23 @@
import { BehaviorSubject } from "rxjs";
import { ApiService } from "../../../../abstractions/api.service";
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
import { I18nService } from "../../../../platform/abstractions/i18n.service";
import { StateProvider } from "../../../../platform/state";
import { UserId } from "../../../../types/guid";
import { ADDY_IO_FORWARDER } from "../../key-definitions";
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
import { Forwarders } from "../options/constants";
import { EmailDomainOptions, SelfHostedApiOptions } from "../options/forwarder-options";
export const DefaultAddyIoOptions: SelfHostedApiOptions & EmailDomainOptions = Object.freeze({
website: null,
baseUrl: "https://app.addy.io",
token: "",
domain: "",
});
/** Generates a forwarding address for addy.io (formerly anon addy) */
export class AddyIoForwarder extends ForwarderGeneratorStrategy<
SelfHostedApiOptions & EmailDomainOptions
@ -34,6 +44,11 @@ export class AddyIoForwarder extends ForwarderGeneratorStrategy<
return ADDY_IO_FORWARDER;
}
/** {@link ForwarderGeneratorStrategy.defaults$} */
defaults$ = (userId: UserId) => {
return new BehaviorSubject({ ...DefaultAddyIoOptions });
};
/** {@link ForwarderGeneratorStrategy.generate} */
generate = async (options: SelfHostedApiOptions & EmailDomainOptions) => {
if (!options.token || options.token === "") {
@ -91,3 +106,10 @@ export class AddyIoForwarder extends ForwarderGeneratorStrategy<
}
};
}
export const DefaultOptions = Object.freeze({
website: null,
baseUrl: "https://app.addy.io",
domain: "",
token: "",
});

View File

@ -2,12 +2,17 @@
* include Request in test environment.
* @jest-environment ../../../../shared/test.environment.ts
*/
import { firstValueFrom } from "rxjs";
import { UserId } from "../../../../types/guid";
import { DUCK_DUCK_GO_FORWARDER } from "../../key-definitions";
import { Forwarders } from "../options/constants";
import { DuckDuckGoForwarder } from "./duck-duck-go";
import { DuckDuckGoForwarder, DefaultDuckDuckGoOptions } from "./duck-duck-go";
import { mockApiService, mockI18nService } from "./mocks.jest";
const SomeUser = "some user" as UserId;
describe("DuckDuckGo Forwarder", () => {
it("key returns the Duck Duck Go forwarder key", () => {
const forwarder = new DuckDuckGoForwarder(null, null, null, null, null);
@ -15,6 +20,16 @@ describe("DuckDuckGo Forwarder", () => {
expect(forwarder.key).toBe(DUCK_DUCK_GO_FORWARDER);
});
describe("defaults$", () => {
it("should return the default subaddress options", async () => {
const strategy = new DuckDuckGoForwarder(null, null, null, null, null);
const result = await firstValueFrom(strategy.defaults$(SomeUser));
expect(result).toEqual(DefaultDuckDuckGoOptions);
});
});
describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => {
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => {
const apiService = mockApiService(200, {});

View File

@ -1,13 +1,21 @@
import { BehaviorSubject } from "rxjs";
import { ApiService } from "../../../../abstractions/api.service";
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
import { I18nService } from "../../../../platform/abstractions/i18n.service";
import { StateProvider } from "../../../../platform/state";
import { UserId } from "../../../../types/guid";
import { DUCK_DUCK_GO_FORWARDER } from "../../key-definitions";
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
import { Forwarders } from "../options/constants";
import { ApiOptions } from "../options/forwarder-options";
export const DefaultDuckDuckGoOptions: ApiOptions = Object.freeze({
website: null,
token: "",
});
/** Generates a forwarding address for DuckDuckGo */
export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy<ApiOptions> {
/** Instantiates the forwarder
@ -32,6 +40,11 @@ export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy<ApiOptions>
return DUCK_DUCK_GO_FORWARDER;
}
/** {@link ForwarderGeneratorStrategy.defaults$} */
defaults$ = (userId: UserId) => {
return new BehaviorSubject({ ...DefaultDuckDuckGoOptions });
};
/** {@link ForwarderGeneratorStrategy.generate} */
generate = async (options: ApiOptions): Promise<string> => {
if (!options.token || options.token === "") {
@ -68,3 +81,8 @@ export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy<ApiOptions>
}
};
}
export const DefaultOptions = Object.freeze({
website: null,
token: "",
});

View File

@ -2,13 +2,18 @@
* include Request in test environment.
* @jest-environment ../../../../shared/test.environment.ts
*/
import { firstValueFrom } from "rxjs";
import { ApiService } from "../../../../abstractions/api.service";
import { UserId } from "../../../../types/guid";
import { FASTMAIL_FORWARDER } from "../../key-definitions";
import { Forwarders } from "../options/constants";
import { FastmailForwarder } from "./fastmail";
import { FastmailForwarder, DefaultFastmailOptions } from "./fastmail";
import { mockI18nService } from "./mocks.jest";
const SomeUser = "some user" as UserId;
type MockResponse = { status: number; body: any };
// fastmail calls nativeFetch first to resolve the accountId,
@ -52,6 +57,16 @@ describe("Fastmail Forwarder", () => {
expect(forwarder.key).toBe(FASTMAIL_FORWARDER);
});
describe("defaults$", () => {
it("should return the default subaddress options", async () => {
const strategy = new FastmailForwarder(null, null, null, null, null);
const result = await firstValueFrom(strategy.defaults$(SomeUser));
expect(result).toEqual(DefaultFastmailOptions);
});
});
describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => {
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => {
const apiService = mockApiService(AccountIdSuccess, EmptyResponse);

View File

@ -1,13 +1,23 @@
import { BehaviorSubject } from "rxjs";
import { ApiService } from "../../../../abstractions/api.service";
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
import { I18nService } from "../../../../platform/abstractions/i18n.service";
import { StateProvider } from "../../../../platform/state";
import { UserId } from "../../../../types/guid";
import { FASTMAIL_FORWARDER } from "../../key-definitions";
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
import { Forwarders } from "../options/constants";
import { EmailPrefixOptions, ApiOptions } from "../options/forwarder-options";
export const DefaultFastmailOptions: ApiOptions & EmailPrefixOptions = Object.freeze({
website: null,
domain: "",
prefix: "",
token: "",
});
/** Generates a forwarding address for Fastmail */
export class FastmailForwarder extends ForwarderGeneratorStrategy<ApiOptions & EmailPrefixOptions> {
/** Instantiates the forwarder
@ -32,6 +42,11 @@ export class FastmailForwarder extends ForwarderGeneratorStrategy<ApiOptions & E
return FASTMAIL_FORWARDER;
}
/** {@link ForwarderGeneratorStrategy.defaults$} */
defaults$ = (userId: UserId) => {
return new BehaviorSubject({ ...DefaultFastmailOptions });
};
/** {@link ForwarderGeneratorStrategy.generate} */
generate = async (options: ApiOptions & EmailPrefixOptions) => {
if (!options.token || options.token === "") {
@ -141,3 +156,10 @@ export class FastmailForwarder extends ForwarderGeneratorStrategy<ApiOptions & E
return null;
}
}
export const DefaultOptions = Object.freeze({
website: null,
domain: "",
prefix: "",
token: "",
});

Some files were not shown because too many files have changed in this diff Show More