Showing preview only (374K chars total). Download the full file or copy to clipboard to get everything.
Repository: buttercup/buttercup-browser-extension
Branch: master
Commit: 4a5e2c359fb4
Files: 165
Total size: 334.7 KB
Directory structure:
gitextract_eo89ckcu/
├── .editorconfig
├── .github/
│ └── workflows/
│ └── test.yml
├── .gitignore
├── .prettierrc
├── .vscode/
│ └── settings.json
├── CHANGELOG.md
├── LICENSE
├── PRIVACY_POLICY.md
├── README.md
├── package.json
├── resources/
│ ├── full.pug
│ ├── manifest.v2.json
│ ├── manifest.v3.json
│ └── popup.pug
├── scripts/
│ └── version.js
├── source/
│ ├── background/
│ │ ├── index.ts
│ │ ├── library/
│ │ │ └── domain.ts
│ │ ├── services/
│ │ │ ├── autoLogin.ts
│ │ │ ├── config.ts
│ │ │ ├── crypto.ts
│ │ │ ├── cryptoKeys.ts
│ │ │ ├── desktop/
│ │ │ │ ├── actions.ts
│ │ │ │ ├── header.ts
│ │ │ │ └── request.ts
│ │ │ ├── disabledDomains.ts
│ │ │ ├── entry.ts
│ │ │ ├── init.ts
│ │ │ ├── log.ts
│ │ │ ├── loginMemory.ts
│ │ │ ├── messaging.ts
│ │ │ ├── notifications.ts
│ │ │ ├── recents.ts
│ │ │ ├── storage/
│ │ │ │ └── BrowserStorageInterface.ts
│ │ │ ├── storage.ts
│ │ │ └── tabs.ts
│ │ └── types.ts
│ ├── full/
│ │ ├── applications/
│ │ │ └── full.tsx
│ │ ├── components/
│ │ │ ├── App.tsx
│ │ │ ├── Layout.tsx
│ │ │ └── pages/
│ │ │ ├── AttributionsPage.tsx
│ │ │ ├── DisabledDomainsPage.tsx
│ │ │ ├── NotificationsPage.tsx
│ │ │ ├── connect/
│ │ │ │ ├── CodeInput.tsx
│ │ │ │ ├── ConnectPage.tsx
│ │ │ │ └── index.tsx
│ │ │ └── saveCredentials/
│ │ │ ├── CredentialsSaver.tsx
│ │ │ ├── CredentialsSelector.tsx
│ │ │ ├── NewEntrySavePrompt.tsx
│ │ │ └── index.tsx
│ │ ├── hooks/
│ │ │ ├── credentials.ts
│ │ │ ├── disabledDomains.ts
│ │ │ ├── document.ts
│ │ │ └── vaultContents.ts
│ │ ├── index.pug
│ │ ├── services/
│ │ │ ├── credentials.ts
│ │ │ ├── disabledDomains.ts
│ │ │ ├── init.ts
│ │ │ ├── log.ts
│ │ │ ├── notifications.ts
│ │ │ └── vaults.ts
│ │ ├── styles/
│ │ │ └── full.sass
│ │ └── types.ts
│ ├── popup/
│ │ ├── applications/
│ │ │ └── popup.tsx
│ │ ├── components/
│ │ │ ├── App.tsx
│ │ │ ├── contexts/
│ │ │ │ └── LaunchContext.tsx
│ │ │ ├── entries/
│ │ │ │ ├── EntryInfoDialog.tsx
│ │ │ │ ├── EntryItem.tsx
│ │ │ │ └── EntryItemList.tsx
│ │ │ ├── navigation/
│ │ │ │ └── Navigator.tsx
│ │ │ ├── otps/
│ │ │ │ ├── OTPItem.tsx
│ │ │ │ └── OTPItemList.tsx
│ │ │ ├── pages/
│ │ │ │ ├── AboutPage.tsx
│ │ │ │ ├── EntriesPage.tsx
│ │ │ │ ├── OTPsPage.tsx
│ │ │ │ ├── SaveDialogPage.tsx
│ │ │ │ ├── SettingsPage.tsx
│ │ │ │ └── VaultsPage.tsx
│ │ │ └── vaults/
│ │ │ ├── VaultItem.tsx
│ │ │ ├── VaultItemList.tsx
│ │ │ └── VaultStateIndicator.tsx
│ │ ├── hooks/
│ │ │ ├── credentials.ts
│ │ │ ├── desktop.ts
│ │ │ ├── document.ts
│ │ │ ├── otp.ts
│ │ │ └── tab.ts
│ │ ├── index.pug
│ │ ├── queries/
│ │ │ ├── desktop.ts
│ │ │ ├── disabledDomains.ts
│ │ │ └── loginMemory.ts
│ │ ├── services/
│ │ │ ├── clipboard.ts
│ │ │ ├── entry.ts
│ │ │ ├── init.ts
│ │ │ ├── log.ts
│ │ │ ├── recents.ts
│ │ │ ├── reset.ts
│ │ │ └── tab.ts
│ │ ├── state/
│ │ │ └── app.ts
│ │ ├── styles/
│ │ │ └── popup.sass
│ │ └── types.ts
│ ├── shared/
│ │ ├── components/
│ │ │ ├── ConfirmDialog.tsx
│ │ │ ├── ErrorBoundary.tsx
│ │ │ ├── ErrorMessage.tsx
│ │ │ ├── RouteError.tsx
│ │ │ ├── ThemeProvider.tsx
│ │ │ └── loading/
│ │ │ └── BusyLoader.tsx
│ │ ├── extension.ts
│ │ ├── hooks/
│ │ │ ├── async.ts
│ │ │ ├── config.ts
│ │ │ ├── global.ts
│ │ │ ├── theme.ts
│ │ │ └── timer.ts
│ │ ├── i18n/
│ │ │ ├── trans.ts
│ │ │ └── translations/
│ │ │ ├── en.json
│ │ │ └── nl.json
│ │ ├── library/
│ │ │ ├── buffer.ts
│ │ │ ├── clone.ts
│ │ │ ├── domain.ts
│ │ │ ├── error.ts
│ │ │ ├── extension.ts
│ │ │ ├── i18n.ts
│ │ │ ├── log.ts
│ │ │ ├── otp.ts
│ │ │ ├── url.ts
│ │ │ ├── vaultTypes.ts
│ │ │ └── version.ts
│ │ ├── notifications/
│ │ │ ├── index.ts
│ │ │ └── pages/
│ │ │ └── WelcomeV3.tsx
│ │ ├── queries/
│ │ │ └── config.ts
│ │ ├── services/
│ │ │ ├── messaging.ts
│ │ │ └── notifications.ts
│ │ ├── styles/
│ │ │ ├── base.sass
│ │ │ └── fonts.sass
│ │ ├── symbols.ts
│ │ ├── themes.ts
│ │ └── types.ts
│ ├── tab/
│ │ ├── index.ts
│ │ ├── library/
│ │ │ ├── disable.ts
│ │ │ ├── dismount.ts
│ │ │ ├── frames.ts
│ │ │ ├── page.ts
│ │ │ ├── position.ts
│ │ │ ├── resize.ts
│ │ │ ├── styles.ts
│ │ │ └── zIndex.ts
│ │ ├── services/
│ │ │ ├── LoginTracker.ts
│ │ │ ├── autoLogin.ts
│ │ │ ├── config.ts
│ │ │ ├── form.ts
│ │ │ ├── formDetection.ts
│ │ │ ├── init.ts
│ │ │ ├── log.ts
│ │ │ ├── logins/
│ │ │ │ ├── disabled.ts
│ │ │ │ ├── saving.ts
│ │ │ │ └── watcher.ts
│ │ │ └── messaging.ts
│ │ ├── state/
│ │ │ ├── form.ts
│ │ │ └── frame.ts
│ │ ├── types.ts
│ │ └── ui/
│ │ ├── launch.ts
│ │ ├── popup.ts
│ │ └── saveDialog.ts
│ └── typings/
│ ├── assets.d.ts
│ └── globals.d.ts
├── tsconfig.json
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
[{.prettierrc,package.json,package-lock.json,.babelrc}]
indent_size = 2
================================================
FILE: .github/workflows/test.yml
================================================
name: Tests
on: push
jobs:
lint:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v2
- name: Node.js specs ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run test:format
release:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v2
- name: Node.js specs ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run release
================================================
FILE: .gitignore
================================================
*.log
.DS_Store
.history
node_modules
/dist
/release
Archive.zip
dist.zip
/secrets.json
================================================
FILE: .prettierrc
================================================
{
"printWidth": 120,
"tabWidth": 4,
"trailingComma": "none"
}
================================================
FILE: .vscode/settings.json
================================================
{
"vsicons.presets.angular": false,
"eslint.enable": false,
"files.associations": {
"**/*.js": "javascriptreact"
},
"typescript.preferences.importModuleSpecifierEnding": "js"
}
================================================
FILE: CHANGELOG.md
================================================
# Buttercup browser extension changelog
## v3.2.0
_2024-04-09_
* Input button customisation (global)
* Dutch language (not yet selectable)
* **Bugfix**:
* Some fields not filled correctly (partial fix)
* Login save prompt shown for existing credentials
## v3.1.0
_2024-03-27_
* ([#467](https://github.com/buttercup/buttercup-browser-extension/issues/467)) Entry info popup
* **Bugfix**:
* ([#469](https://github.com/buttercup/buttercup-browser-extension/issues/469)) Forms recognised as login forms when they should not be
* ([#466](https://github.com/buttercup/buttercup-browser-extension/issues/466)) Bad OTP URIs crash application
* Entry page search does nothing when first opening the extension
## v3.0.0
_2024-03-26_
* **Major rewrite**
* Requires desktop application for vault access
* Nested iframe traversal
* OTP support
## v2.26.0
_2023-11-08_
* **Important version 3 update notice**
* Updates for core libraries and datasources
## v2.25.3
_2023-01-31_
* **Bugfix**:
* Google Drive would fail when tokens expire (new response format)
## v2.25.2
_2022-09-03_
* **Bugfix**:
* Fixed Dropbox connectivity issues
* Fixed Google Drive re-authentication loop, short auth time
## v2.25.1
_2022-08-16_
* **Bugfix**:
* Format B saving new properties would fail
* Format B conversion would omit history
## v2.25.0
_2022-06-02_
* Buttercup upgrade: v6
* Improved Dropbox/Google Drive integrations
* Improved vault stability and performance
* Improved support for Vault Format B
* Removed My Buttercup integration
* Vault editor page redesign
* Updated vault UI
## v2.24.3
_2021-05-24_
* **Bugfix**:
* Vault pages not taking up 100% of the height of the window
## v2.24.2
_2021-05-22_
* **Bugfix**:
* ([#381](https://github.com/buttercup/buttercup-browser-extension/issues/381)) Search results not showing most recent result (URL detection)
* WebDAV connection crashes tab during failed connection attempt (when adding a vault)
* **Critical auto-update issue**: Core crash when receiving updated vaults in the background
## v2.24.1
_2021-01-06_
* Remove `activeTab` permission requirement
* **Bugfix**:
* ([#393](https://github.com/buttercup/buttercup-browser-extension/issues/393)) Copy to clipboard not working for in-page dialog
## v2.24.0
_2020-12-09_
* Site icons for results within in-page search dialog
* Performance improvements regarding search
* Vault unlock/save performance improvements
## v2.23.1
_2020-11-27_
* **Bugfix**:
* ([#368](https://github.com/buttercup/buttercup-browser-extension/issues/368)) Popup / Dialog menus very slow to open (performance bugfix for search)
## v2.23.0
_2020-09-05_
* Dynamic icons defaults to **enabled**
* Removed dynamic icons setting popup page
* **Bugfix**:
* ([#366](https://github.com/buttercup/buttercup-browser-extension/issues/366)) Google Drive bad refresh-token method call
## v2.22.0
_2020-09-04_
* Core group/entry lookup performance upgrades
* **Bugfix**:
* ([#370](https://github.com/buttercup/buttercup-browser-extension/issues/370)) Critical CPU/memory use after some time
* Entries not able to be moved from group to group
## v2.21.0
_2020-08-30_
* **Buttercup Core v5**
* Improved performance
* Improved stability
* Future support for **Vault Format B**
* Dynamic icons for entries (optional)
* Reduced extension size (< 50% of the size of 2.20.2)
* Removed `Buffer` dependencies
* **Bugfix**:
* Search wouldn't work (no results)
## v2.20.2
_2020-08-19_
* **Bugfix**:
* ([buttercup-core#287](https://github.com/buttercup/buttercup-core/issues/287)) Vaults grow to enormous size
## v2.20.1
_2020-08-03_
* **Attachments** (My Buttercup)
* Add, remove, preview and download attachments when using My Buttercup vaults
* Core memory/stability improvements when merging vault changes from remote sources
## v2.19.0
_2020-07-25_
* New Buttercup form button behaviour (to improve login form stability)
* Clipboard-writing permission for certain browsers
* Improved auto-update stability
## v2.18.0
_2020-07-07_
* Search results won't show items in trash
* **Bugfix**:
* ([#337](https://github.com/buttercup/buttercup-browser-extension/pull/337)) No login-save-prompt when entry selected for login form (includes auto login)
## v2.17.0
_2020-07-05_
* New search functionality
* Result scoring per domain
* Vault type icons on unlock-all-vaults page
## v2.16.2
_2020-07-02_
* **Bugfix**:
* Unable to enter form details when selecting entry result in dialog
## v2.16.1
_2020-07-01_
* **Bugfix**:
* Unable to select vault in save credentials form
* No search results in popup dialog
* Broken vault lock state in menu
## v2.16.0
_2020-06-30_
* Core version 4
* My Buttercup datasource support
* ([#340](https://github.com/buttercup/buttercup-browser-extension/pull/340)) Allow localhost in disabled domains
## v2.15.1
_2020-04-02_
* **Bugfix**:
* WebDAV would fail to connect on some services, such as ownCloud
## v2.15.0
_2020-03-18_
* Disable save prompt for domains
* Memory for all login form inputs
* **Bugfix**:
* Buttons would disappear from some forms (Dropbox)
## v2.14.0
_2020-02-03_
* Upgrade webdav for reduced application size
* **Bugfix**:
* ([#325](https://github.com/buttercup/buttercup-browser-extension/issues/325)) New vaults fail to create
* ([#286](https://github.com/buttercup/buttercup-browser-extension/issues/286)) Unlock-vaults page not opening in FF after clicking button in on-page dialog
## v2.13.1
_2020-01-24_
* **Bugfix**:
* ([#324](https://github.com/buttercup/buttercup-browser-extension/issues/324)) Very slow vault contents navigation
* ([#323](https://github.com/buttercup/buttercup-browser-extension/issues/323)) OTP (HOTP) failures crashing entire vault management UI
* ([#322](https://github.com/buttercup/buttercup-browser-extension/issues/322)) Auto-update of search results by URL not working
## v2.13.0
_2020-01-22_
* Group context menu: creation, renaming, moving and deletion
* New group in root button
* Entry field history (basic)
* **Bugfix**:
* Credit card entry type would crash application
* State sync for vault management interface inconsistent
## v2.12.0
_2020-01-18_
* ([#320](https://github.com/buttercup/buttercup-browser-extension/issues/320)) Open permissions option when adding Google Drive vaults
* **Bugfix**:
* ([#270](https://github.com/buttercup/buttercup-browser-extension/issues/270)) _(2nd attempt)_: Support multiple Google accounts
* ([#319](https://github.com/buttercup/buttercup-browser-extension/issues/319)) Google Drive authentication not working in Microsoft Edge (unofficial patch - pre-release)
## v2.11.1
_2020-01-08_
* **Bugfix**:
* ([#316](https://github.com/buttercup/buttercup-browser-extension/issues/316)) `createSession` is not defined (local file host vaults)
## v2.11.0
_2020-01-05_
* Core integration with App-Env for web-based crypto improvement
* ([#270](https://github.com/buttercup/buttercup-browser-extension/issues/270)) Prompt for account-selection on Google authentication (Google Drive) to support multiple accounts
* ([#245](https://github.com/buttercup/buttercup-browser-extension/issues/245)) Google Drive permissions reduced - Only files touched by Buttercup are accessible to the application
* **Bugfix**:
* ([#314](https://github.com/buttercup/buttercup-browser-extension/issues/314)) Unable to open Google Drive vault
## v2.10.1
_2019-12-26_
* **Bugfix**:
* ([#312](https://github.com/buttercup/buttercup-browser-extension/issues/312)) No login prompt visible on Firefox
## v2.10.0
_2019-12-24_
* Ability to change vault password
* **Bugfix**:
* ([#307](https://github.com/buttercup/buttercup-browser-extension/issues/307)) Cannot save new note-type entry (or any other custom types)
## v2.9.0
_2019-11-12_
* My Buttercup preparation
* ownCloud/Nextcloud removed in favour of WebDAV (existing connections should still function)
* ([#95](https://github.com/buttercup/buttercup-browser-extension/issues/95)) Context menus to choose credentials for form-filling and login
## v2.8.2
_2019-09-01_
* **Bugfix**:
* ([#269](https://github.com/buttercup/buttercup-browser-extension/issues/269)) Password field in popup menu not copy-able and always visible
## v2.8.1
_2019-09-01_
* **Bugfix**:
* ([#253](https://github.com/buttercup/buttercup-browser-extension/issues/253)) Vault saving via editing UI (second attempt)
## v2.8.0
_2019-07-23_
* **Bugfix**:
* ~~([#253](https://github.com/buttercup/buttercup-browser-extension/issues/253)) Vault saving via editing UI~~
* ([#259](https://github.com/buttercup/buttercup-browser-extension/pull/259)) General improvements to the add-vault page
* Unlock button on in-page dialog when vaults are locked
* Unlock button for single-vault now navigates to edit page
* TOTP / HOTP support via vault UI (display only, no form-fill)
* Entry value type support via vault UI
* Updated Dropbox/Google Drive clients for compatibiltiy
## v2.7.0
_2019-04-28_
* Vault editing interface
## v2.6.0
_2019-04-14_
* ([#246](https://github.com/buttercup/buttercup-browser-extension/issues/246)) Google Drive refresh token support
## v2.5.1
_2019-03-13_
* **Bugfix**:
* ([#244](https://github.com/buttercup/buttercup-browser-extension/issues/244)) Google Drive fetching fails on large directories
## v2.5.0
_2019-03-09_
* **Google Drive** support
* Unlock-vaults button in popup
## v2.4.1
_2019-02-05_
* **Bugfix**:
* Regression in auto-unlock functionality
## v2.4.0
_2019-02-04_
* ([#235](https://github.com/buttercup/buttercup-browser-extension/issues/235)) Use local (static) icons and don't request them from remote sources
* ([#171](https://github.com/buttercup/buttercup-browser-extension/issues/171)) Auto-lock vaults after a configurable time
* Auto-login button in popup menu
## v2.3.1
_2019-01-19_
* **Bugfixes**:
* ([#214](https://github.com/buttercup/buttercup-browser-extension/issues/214)) Popup menu layout broken for long items
* ([#216](https://github.com/buttercup/buttercup-browser-extension/issues/216)) Autofocus extension popover search input
## v2.3.0
_2018-01-12_
* **Bugfixes**:
* ([#217](https://github.com/buttercup/buttercup-browser-extension/issues/217)) Popup menu layout broken
* ([#218](https://github.com/buttercup/buttercup-browser-extension/issues/218)) Some website forms not recognised
* Improved entry details UI in popup menus
* Improved entry results in popup menus
* What's New section on auto-unlock page
* Improved login form detection
## v2.2.0
_2019-01-05_
* ([#212](https://github.com/buttercup/buttercup-browser-extension/issues/212)) Poor search results performance
* ([#202](https://github.com/buttercup/buttercup-browser-extension/issues/202)) Auto-unlock setting not working
## v2.1.3
_2018-12-16_
* ([#190](https://github.com/buttercup/buttercup-browser-extension/issues/190)) Dropbox connection never completes loading procedure (UI spinner)
## v2.1.2
_2018-12-08_
* ([#203](https://github.com/buttercup/buttercup-browser-extension/issues/203)) Failure saving Dropbox changes
## v2.1.1
_2018-11-27_
* **Bugfix**: ownCloud / Nextcloud / WebDAV vaults could not be added (Chrome)
## v2.1.0
_2018-11-24_
* ([#91](https://github.com/buttercup/buttercup-browser-extension/issues/91)) Connect through desktop application (local filesystem access)
* New WebDAV client
* New Dropbox client
## v2.0.0
_2018-10-29_
* **Major UI overhaul**
* ([#180](https://github.com/buttercup/buttercup-browser-extension/issues/180)) Option to disable "save-new" dialog
* ([#160](https://github.com/buttercup/buttercup-browser-extension/issues/160)) Settings page
* ([#173](https://github.com/buttercup/buttercup-browser-extension/issues/173)) Source type is empty - display glitches (final cleanup)
* Dark/Light mode themes
* Setting for showing the auto-unlock page (default on)
* Vault/Account sync via Chrome/Firefox's account logins (sync storage)
* Show/Copy entry properties in dialog/popup menus
## v1.12.1
_2018-10-18_
* ([#173](https://github.com/buttercup/buttercup-browser-extension/issues/173)) Source type is empty - display glitches
* ([#182](https://github.com/buttercup/buttercup-browser-extension/issues/182)) Add bundling process for Chrome/Firefox
## v1.12.0
_2018-10-07_
* ([#110](https://github.com/buttercup/buttercup-browser-extension/issues/110)) Reload archives after some time
* ([#174](https://github.com/buttercup/buttercup-browser-extension/issues/174)) Unable to access via WebDAV (Seafile)
## v1.11.1
_2018-08-27_
* ([#164](https://github.com/buttercup/buttercup-browser-extension/issues/164)) `t is null` error when unlocking archives
## v1.11.0
_2018-08-24_
* ([#136](https://github.com/buttercup/buttercup-browser-extension/issues/136)) Use last generated password form context menu
* ([#153](https://github.com/buttercup/buttercup-browser-extension/issues/153)) **Bugfix**: Button layout issues
* Upgraded login form targetting
## v1.10.0
_2018-07-11_
* New popup menu design
* Search and open items from the popup
## v1.9.1
_2018-07-08_
* ([#152](https://github.com/buttercup/buttercup-browser-extension/issues/152)) **Bugfix**: Failure while adding WebDAV archives
## v1.9.0
_2018-06-29_
* ([#131](https://github.com/buttercup/buttercup-browser-extension/issues/131)) Upgrade core to v2
* New archive format (supporting future encryption standards)
* ([#130](https://github.com/buttercup/buttercup-browser-extension/issues/130)) Auto unlock prompt shown when browser is opened
* ([#147](https://github.com/buttercup/buttercup-browser-extension/issues/147)) Remove settings page link
## v1.8.0
_2018-06-22_
* Upgrade core to 1.7.1
* Future proofing for archive format
## v1.7.0
_2018-04-28_
* ([#124](https://github.com/buttercup/buttercup-browser-extension/issues/124)) Simplify save-new login screen by removing password confirmation
* ([#141](https://github.com/buttercup/buttercup-browser-extension/issues/141)) Buttercup launch button layout issues
* Dependency updates
## v1.6.2
_2018-04-24_
* ([#139](https://github.com/buttercup/buttercup-browser-extension/issues/139)) No "Generate password" option shown when right-clicking inputs (Firefox/Chrome)
## v1.6.1
_2018-04-01_
* ([#137](https://github.com/buttercup/buttercup-browser-extension/issues/137)) Unable to close save-new dialog
## v1.6.0
_2018-04-01_
* "password" type input support for Firefox and the password generator
* ([#134](https://github.com/buttercup/buttercup-browser-extension/issues/134)) Password generator "Use this" button fails on Firefox
* ([#133](https://github.com/buttercup/buttercup-browser-extension/issues/133)) Password generator bad padding issue
## v1.5.0
_2018-03-31_
* Password generator
* Right-click context menu
## v1.4.0
_2018-03-23_
* ([#126](https://github.com/buttercup/buttercup-browser-extension/issues/126)) Add an attribute to allow inputs and forms to be ignored by Buttercup
* Support new login forms
* Update login form detection priorities
## v1.3.1
_2018-02-20_
* ([#121](https://github.com/buttercup/buttercup-browser-extension/issues/121)) Unable to click "Save" for new logins
## v1.3.0
_2018-02-05_
* Use `chrome.storage` for better data persistence
## v1.2.1
_2018-02-04_
* Update code splitting configuration (Firefox submission fixes)
## v1.1.1
_2018-02-04_
* Improve URL filtering for new credentials saving
## v1.1.0
_2018-02-04_
* ([#112](https://github.com/buttercup/buttercup-browser-extension/issues/106)) Save newly-entered credentials
## v1.0.7
_2018-01-22_
* Add data collection event listeners for form attachments
## v1.0.6
_2018-01-21_
* Fix component communication in Firefox
* Add storage permission
## v1.0.5
_2018-01-21_
* ([#106](https://github.com/buttercup/buttercup-browser-extension/issues/106)) Fix Nextcloud archive searching
## v1.0.4
_2018-01-20_
* First 1.* release for Firefox
* Fix character encoding
* Fix button icon not showing on some sites
* Fix scrollbars in popup
## v1.0.3
_2018-01-19_
* Fix GitHub login
* Fix logins that have multiple detected inputs
## v1.0.2
_2018-01-18_
* ([#97](https://github.com/buttercup/buttercup-browser-extension/issues/97)) Fixed Twitter login bug
## **v1.0.1**
_2018-01-18_
* Full re-work of the entire extension
* **Nextcloud** official support
* ([#92](https://github.com/buttercup/buttercup-browser-extension/issues/92)) Fixed security vulnerability
## v0.14.2
_2017-07-15_
* Bugfix: [ownCloud subfolder installations: Subfolder ignored](https://github.com/buttercup/buttercup-browser-extension/issues/80)
## v0.14.1
_2017-06-24_
* Bugfix: [Unable to connect to certain WebDAV services](https://github.com/buttercup/buttercup-desktop/issues/303)
## v0.14.0
_2017-06-07_
* Bugfix: [Malformed URL error](https://github.com/buttercup/buttercup-browser-extension/issues/71) - old WebDAV client caused issues with special characters in passwords
* Add back old Buttercup-core-web classes for compatibility
## v0.13.1
_2017-05-27_
* Bugfix: Archive creation in root level
## v0.13.0
_2017-04-23_
* Added right-click context menu drilldown for choosing which entry to fill form with
* Updated hotkey for auto-login (Command+Shift+L for Mac, Ctrl+Shift+L for Windows/Linux)
* Fixed deprecation warnings during build and updated some packages
## v0.12.1
_2017-04-19_
* Spring clean:
* Reduced permissions requirements in manifest
* Improved last submitted form security
## v0.12.0
_2017-04-15_
* Bugfix: Popup formatting issues
* Added hotkey for auto-login (Command+B (Mac) / Ctrl+B (Windows/Linux))
## v0.11.0
_2017-04-14_
* Bugfix: Clicking "No" on save prompt would not cancel further popups
* Bugfix: Form submission error when selecting credentials
* Prefill title when saving new credentials
## v0.10.0
_2017-03-31_
* Added right-click context menu on form inputs
* Fixed overflow on remote filesystem explorer when adding archives
* Finalised styling on unlock-archive form
## v0.9.0
_2017-03-29_
* Improved UI for popup password list (archive + groups pathing)
* Bugfix: Fixed wrong-password during unlock sequence breaking state
## v0.8.0
_2017-03-22_
* Fuzzy searching for entries on login-forms
* Login-form popup available on password fields
## v0.7.0
_2017-03-15_
* **Support for Firefox**
* Auto-submit when selecting credentials on a form
* Styles normalisation
## v0.6.0
_2017-03-14_
* Upgrade core
* Use serialisation for archive properties
## v0.5.0
_2017-03-09_
* Upgrade core
* Drastically increase PBKDF2 rounds
* Improve URI matching
## v0.4.0
_2017-02-11_
* First alpha release
## v0.3.0
* Pre-release development build
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2016 Perry Mitchell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: PRIVACY_POLICY.md
================================================
# Privacy Policy
This privacy policy concerns the Browser Extension for Buttercup, its use and the data it makes use of.
## About Buttercup
Buttercup is a software suite designed to provide a secure application with which to store highly-sensitive information (such as account credentials) in encrypted vault files. Great care is taken to ensure that all secure data remains protected, with as little information as possible being handled by the application and the Buttercup platform therein.
### Terms
The following terms will appear throughout this document and their meaning is important to grasp for a proper understanding of this policy.
| Term | Description |
|-------------------|-------------------------------------------------------|
| Archive | See "vault". |
| Call-to-action | A button or link that indicates to the user that it performs some kind of action. A button with "Login" on it is a good example. |
| Encryption | The process of transforming vault contents into a secure, un-readable format for storage that can only be _read_ by providing a master password. |
| Master password | The highly secure secret password used to lock and unlock vaults, known only to the user. |
| Vault | An encrypted password vault, stored as a file either locally or remotely (on some kind of service). |
### Types of data
Vaults, in their locked state, contain **encrypted data** which represent secret information that Buttercup uses to function (passwords etc.). When unlocked, the data is **unencrypted** and resides in memory on the user's device.
Buttercup applications may store **unencrypted configuration information** on the device in a standard directory or location. Configuration data does not include any sensitive information. Configuration refers to settings that allow the application to function in a desired manner for the user.
Buttercup for Browsers does not collect any **analytics data**, but the hosting platforms (eg. Mozilla/Google) may collect anonymous analytics with regards to the extension itself (installations vs uninstallations, etc.).
Due to the fact that Buttercup interacts with webpages, the **Document Object Model (DOM)** may be modified by its use.
## Data storage and transfer with regards to 3rd Parties
### Remote vault storage
Buttercup vault files, especially with regards to this browser extension, are stored remotely on hosting services. These services (eg. Dropbox, Nextcloud etc.) utilise their own privacy policies and standards, and are responsible for the files stored within their platform. Vaults transferred from the extension to these services are encrypted **before** they leave the application. No unencrypted data is stored on these remote services, besides the filename itself.
It should be noted that connection logs may be kept on various services. It is the responsibility of the user to take care as to what services they choose to use and connect to, if any.
### DOM (Document Object Model)
The browser extention modifies the DOM of open webpages so that it can track several different items while the page is live:
* Detected login forms
* Detected inputs that can be used for login (username/email, password etc.)
* Submit buttons
* Potential submit buttons (tracking text that resembles login call-to-actions)
The extension may add attributes to existing elements, as well as adding entirely new elements (eg. buttons to open the Buttercup login menu). These modifications may indicate to the site, as well as to scripts running on the site, the fact that the current user has the Buttercup browser extension installed.
### Analytics
Buttercup does not track any analytics directly. Platforms that Buttercup uses, such as Mozilla (for Firefox addons) or Google (for Chrome extensions), may track analytics that relate to the use of the extension in an anonymous manner. Buttercup makes use of this data for the purpose of analysing product health and potential market expansions.
## Internal data storage and use
### In-memory vaults
When the vaults are decrypted using the user's master password, the contents of the vault are loaded into memory within the browser. The contents of the vaults are stored within the extension and are not accessible outside of the extension's context. Raw credentials may be transferred to webpages not owned by Buttercup at the request of the user when trying to log in to a website.
## Your responsibility as a user
By using Buttercup's applications or services, you acknowledge that Buttercup stores highly-sensitive information for you using a **master password**. This password must not be shared with anyone, and should sufficiently strong to ensure that vault integrity is not broken if a bad actor gains access to it. Both the password and the encrypted vault file are the responsibility of the user, and both should be treated as highly sensitive.
## Our responsibility to you, the user
We provide Buttercup as free-to-use software - we take every reasonable precaution to ensure that the vaults are encrypted in a manner that is secure by industry standards. We must ensure that only encrypted data leaves the user's device when storing vaults. We must ensure that only the bare minimum information is shared with 3rd party services to allow Buttercup to integrate with them.
================================================
FILE: README.md
================================================
<h1 align="center">
<br/>
<img src="https://cdn.rawgit.com/buttercup-pw/buttercup-assets/4bbfd317/badge/browsers.svg" alt="Buttercup for Browsers">
<br/>
<br/>
<br/>
</h1>
# Buttercup Browser Extension
Buttercup credentials manager extension for the browser.
<p align="center">
<img src="https://raw.githubusercontent.com/buttercup/buttercup-browser-extension/master/chrome-extension.jpg" />
</p>
[](https://buttercup.pw)  [](https://chrome.google.com/webstore/detail/buttercup/heflipieckodmcppbnembejjmabajjjj?hl=en-GB) [](https://chrome.google.com/webstore/detail/buttercup/heflipieckodmcppbnembejjmabajjjj?hl=en-GB) [](https://addons.mozilla.org/en-US/firefox/addon/buttercup-pw/) [](https://addons.mozilla.org/en-US/firefox/addon/buttercup-pw/) [](https://keybase.io/team/bcup)
---
⚠️ **Project Closure** ⚠️
The Buttercup project has come to an end, and these repositories are in transition to becoming public archives. No public-facing resources will be removed, wherever possible. Please do not create issues or PRs - they will unfortunately be ignored. Discussion can be found [here](https://github.com/buttercup/buttercup-desktop/discussions/1395), and explanation [here](https://gist.github.com/perry-mitchell/43ebfcec4d874b77a704be1d4f2262e6).
---
## About
This browser extension allows users to interface with password archives authored by the [Buttercup password manager](https://github.com/buttercup-pw/buttercup) (it _requires_ v2.26 and later of the desktop application to be installed to function).
The extension makes secured requests to the desktop application for information within its unlocked vaults, and makes those credentials available within the browser. It is also able to save new logins from the browser extension as they're recognised. Besides a username and password, the extension can also enter OTP codes when required.
<img src="https://raw.githubusercontent.com/buttercup/buttercup-browser-extension/master/chrome-extension-2.jpg" />
### Forms & Logins
Buttercup for Browsers auto-detects some login forms and login inputs, allowing the user to auto-fill them at their discretion. This extension uses [Locust](https://github.com/buttercup/locust) under the hood to **detect forms and inputs** (any issues with detecting forms and inputs should be opened there).
### Supported browsers
[Chrome](https://chrome.google.com/webstore/detail/buttercup/heflipieckodmcppbnembejjmabajjjj?hl=en-GB), [Firefox](https://addons.mozilla.org/en-US/firefox/addon/buttercup-pw/), [Edge](https://www.microsoft.com/en-us/edge) (version 79+) and [Brave](https://chrome.google.com/webstore/detail/buttercup/heflipieckodmcppbnembejjmabajjjj) are supported.
_Some browsers, such as **Brave** for example, will be able to install Buttercup via the Google Chrome web store._
Other browsers will be supported in order of request/popularity. Issues created for unsupported browsers, or for browsers not on the roadmap, may be closed without warning.
**Opera** is not supported due to their incredibly slow and unreliable release process. We will not be adding support for Opera.
### Integrated platforms
The extension allows for connections to several services, such as Dropbox and Google Drive. The extension supports whatever platforms the desktop application does, including local vaults.
#### Supported platforms
The browsers listed above, running on Windows, Mac or Linux on a desktop platform. This extension is not supported on any mobile or tablet devices.
### Usage
The browser extension can be controlled from the **popup menu**, which is launched by pressing the Buttercup button in the browser menu. This menu displays a list of archives as well as settings and other items.
When viewing pages that contain login forms, Buttercup can assist logging in when you interact with the login buttons (displayed beside detected login inputs).
Buttercup can also remember new logins, which are detected as they occur.
You can **block** Buttercup from detecting forms and inputs by applying the attribute `data-bcupignore=true`:
```html
<input type="email" data-bcupignore="true" />
```
### Development
Development of features and bugfixes is supported in the following environment:
* NodeJS version 20 (latest minor version)
* Linux / Mac
* Tested in at least Chrome / Firefox
To set up your development environment:
* Clone this repo
* Ensure API keys are available (Google Drive)
* Execute `npm install` inside the project directory
* Run `npm run dev:chrome` or similar
* Load the unpacked `dist` folder in your browser addons
#### Chrome
Run the following to develop the extension:
1. Execute `npm run dev:chrome` to build and watch the project (to build production code, execute `npm run build`)
2. Go to [chrome://extensions](chrome://extensions) and enable _"Developer mode"_
3. Select the new button _"Load unpacked"_, then select the `./dist` directory built on step 1
#### Firefox
Run the following to develop the extension:
* Execute `npm run dev:firefox` to build and watch the project (to build production code, execute `npm run release`)
#### Releasing
To build release-ready zip archives, run the command `npm run release` after having set up the development environment. The archives will be written to `release/(browser)` where `(browser)` is the browser type. Archives named `extension.zip` contain the built extension sourcecode and `source.zip` contains the raw source.
### Adding to Chrome
You can load an **unpacked extension** in Chrome by navigating to [chrome://extensions/](chrome://extensions/). Simply locate the project's directory and use **dist/** as the extension directory.
### Adding to Firefox
You can load an **unpacked extension** in Firefox by navigating to [about:debugging](about:debugging). Click "Load Temporary Add-on" and locate the project's directory, using **dist/** as the extension directory.
================================================
FILE: package.json
================================================
{
"name": "buttercup-browser-extension",
"version": "3.2.0",
"description": "Buttercup browser extension",
"exports": "./dist/background/index.js",
"type": "module",
"types": "./dist/background/index.d.ts",
"scripts": {
"build": "run-s set-version build:production",
"build:chrome": "BROWSER=chrome npm run build",
"build:edge": "BROWSER=edge npm run build",
"build:firefox": "BROWSER=firefox npm run build",
"build:production": "webpack --mode production --progress --config webpack.config.js",
"clean": "rimraf dist/* release/*",
"dev": "npm run clean && npm run set-version && webpack --mode development -w --progress --config webpack.config.js",
"dev:chrome": "BROWSER=chrome npm run dev",
"dev:edge": "BROWSER=edge npm run dev",
"dev:firefox": "concurrently -k \"BROWSER=firefox npm run dev\" \"cd dist && web-ext run\" --restart-tries 20 --restart-after 5000 --devtools --keep-profile-changes",
"format": "prettier --write '{{source,test}/**/*.{ts,js},webpack.config.js}'",
"release": "run-s clean release:chrome release:firefox release:edge",
"release:chrome": "npm run build:chrome && mkdirp release/chrome && zip -r release/chrome/extension.zip ./dist",
"release:edge": "npm run build:edge && mkdirp release/edge && zip -r release/edge/extension.zip ./dist",
"release:firefox": "npm run build:firefox && mkdirp release/firefox && run-s release:firefox:extension release:firefox:source",
"release:firefox:extension": "cd dist && web-ext build --overwrite-dest && cd .. && mv ./dist/web-ext-artifacts/*.zip ./release/firefox/",
"release:firefox:source": "zip -r release/firefox/source.zip . --exclude=/.git* --exclude=/node_modules* --exclude=/.history* --exclude=/dist* --exclude=/release* --exclude=*.DS_Store*",
"set-version": "node scripts/version.js",
"test": "run-s test:format",
"test:format": "prettier --check '{{source,test}/**/*.{ts,js},webpack.config.js}'"
},
"lint-staged": {
"{{source,test}/**/*.{ts,js},webpack.config.js}": [
"prettier --write"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/buttercup/buttercup-browser-extension.git"
},
"keywords": [
"buttercup",
"password",
"vault",
"login",
"secure"
],
"author": "Perry Mitchell <perry@perrymitchell.net>",
"license": "MIT",
"bugs": {
"url": "https://github.com/buttercup/buttercup-browser-extension/issues"
},
"homepage": "https://github.com/buttercup/buttercup-browser-extension#readme",
"devDependencies": {
"@babel/core": "^7.23.3",
"@babel/preset-env": "^7.23.3",
"@babel/preset-react": "^7.23.3",
"@blueprintjs/core": "^4.20.2",
"@blueprintjs/icons": "^4.16.0",
"@blueprintjs/popover2": "^1.14.11",
"@blueprintjs/select": "^4.9.24",
"@buttercup/channel-queue": "^1.4.0",
"@buttercup/locust": "^2.3.1",
"@buttercup/ui": "^6.2.2",
"@types/chrome": "^0.0.251",
"@types/ms": "^0.7.34",
"@types/react": "^17.0.38",
"@types/react-dom": "^17.0.11",
"babel-loader": "^9.1.3",
"buttercup": "^7.6.0",
"classnames": "^2.2.6",
"concurrently": "^8.2.2",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1",
"eventemitter3": "^5.0.1",
"expiry-map": "^2.0.0",
"file-loader": "^6.0.0",
"gle": "^1.0.3",
"husky": "^4.2.5",
"i18next": "^23.7.6",
"iocane": "^5.1.1",
"layerr": "^2.0.0",
"lint-staged": "^15.1.0",
"mkdirp": "^3.0.1",
"ms": "^2.1.3",
"mucus": "^1.0.0",
"npm-run-all": "^4.1.5",
"obstate": "^0.1.4",
"on-navigate": "^0.1.1",
"otpauth": "^9.1.5",
"parse-domain": "^8.0.2",
"prettier": "^3.1.0",
"pug-plugin": "^5.0.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-obstate": "^0.1.3",
"react-router-dom": "^6.21.3",
"redom": "^3.29.1",
"resolve-typescript-plugin": "^2.0.1",
"rimraf": "^5.0.5",
"sass": "^1.69.5",
"sass-loader": "^13.3.2",
"style-loader": "^3.3.3",
"styled-components": "^5.3.11",
"ts-loader": "^9.5.0",
"typescript": "^5.2.2",
"ulidx": "^2.2.1",
"url-join": "^5.0.0",
"url-loader": "^4.1.1",
"web-ext": "^7.8.0",
"webpack": "^5.90.3",
"webpack-cli": "^5.1.4",
"webpack-merge": "^5.10.0"
}
}
================================================
FILE: resources/full.pug
================================================
doctype html
html
head
title Buttercup
meta(charset="utf-8")
link(rel="icon", type="image/png", href=require("./buttercup-256.png").default)
link(rel="stylesheet" href="full.css")
link(rel="stylesheet" href="vendors.css")
script(defer, src="full.js")
script(defer, src="vendors.js")
body
div#root
================================================
FILE: resources/manifest.v2.json
================================================
{
"manifest_version": 2,
"name": "Buttercup",
"description": "Browser extension for Buttercup, the secure and easy-to-use password manager.",
"version": "0.0.0",
"icons": {
"256": "manifest-res/buttercup-256.png",
"128": "manifest-res/buttercup-128.png",
"48": "manifest-res/buttercup-48.png",
"16": "manifest-res/buttercup-16.png"
},
"background": {
"scripts": [
"background.js"
]
},
"browser_action": {
"default_icon": "manifest-res/buttercup-256.png",
"default_popup": "popup.html#/"
},
"content_scripts" : [
{
"matches": ["http://*/*", "https://*/*"],
"run_at": "document_end",
"all_frames": true,
"js": ["tab.js"]
}
],
"permissions": [
"clipboardWrite",
"http://*/*",
"https://*/*",
"storage",
"tabs",
"unlimitedStorage"
],
"web_accessible_resources": [
"*.png",
"*.jpg"
],
"applications": {
"gecko": {
"id": "{10e7d273-2e63-47c9-82af-76c45dc1b624}"
}
}
}
================================================
FILE: resources/manifest.v3.json
================================================
{
"manifest_version": 3,
"name": "Buttercup",
"description": "Browser extension for Buttercup, the secure and easy-to-use password manager.",
"version": "0.0.0",
"icons": {
"256": "manifest-res/buttercup-256.png",
"128": "manifest-res/buttercup-128.png",
"48": "manifest-res/buttercup-48.png",
"16": "manifest-res/buttercup-16.png"
},
"background": {
"service_worker": "background.js"
},
"action": {
"default_title": "Buttercup",
"default_icon": "manifest-res/buttercup-256.png",
"default_popup": "/popup.html#/"
},
"content_scripts" : [
{
"matches": ["http://*/*", "https://*/*"],
"run_at": "document_end",
"all_frames": true,
"js": ["tab.js"]
}
],
"permissions": [
"clipboardWrite",
"storage",
"tabs",
"unlimitedStorage"
],
"web_accessible_resources": [
{
"resources": ["*.png", "*.jpg"],
"matches": ["http://*/*", "https://*/*"]
},
{
"resources": ["popup.html"],
"matches": ["http://*/*", "https://*/*"]
}
]
}
================================================
FILE: resources/popup.pug
================================================
doctype html
html
head
title Menu ⋅ Buttercup
meta(charset="utf-8")
link(rel="icon", type="image/png", href=require("./buttercup-256.png").default)
link(rel="stylesheet" href="popup.css")
link(rel="stylesheet" href="vendors.css")
script(defer, src="popup.js")
script(defer, src="vendors.js")
body
div#root
================================================
FILE: scripts/version.js
================================================
import { readFileSync, writeFileSync } from "fs";
import { resolve } from "path";
import { fileURLToPath } from "url";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const packageInfo = JSON.parse(readFileSync(
resolve(__dirname, "../package.json"),
"utf8"
));
const buildDate = new Date();
const built = `${buildDate.getUTCFullYear()}-${String(buildDate.getUTCMonth() + 1).padStart(2, "0")}-${String(buildDate.getUTCDate()).padStart(2, "0")}`;
const output = `// Do not edit this file - it is generated automatically at build time
export const BUILD_DATE = "${built}";
export const VERSION = "${packageInfo.version}";
`;
writeFileSync(
resolve(__dirname, "../source/shared/library/version.ts"),
output
);
================================================
FILE: source/background/index.ts
================================================
import { initialise } from "./services/init.js";
import { log } from "./services/log.js";
initialise().catch((err) => {
console.error(err);
log("initialisation failed");
});
================================================
FILE: source/background/library/domain.ts
================================================
import { UsedCredentials } from "../types.js";
export function extractDomainFromCredentials(credentials: UsedCredentials): string | null {
const match = /^https?:\/\/([^\/]+)/.exec(credentials.url);
return match ? match[1] : null;
}
================================================
FILE: source/background/services/autoLogin.ts
================================================
import { SearchResult } from "buttercup";
import ExpiryMap from "expiry-map";
interface RegisteredItem {
entry: SearchResult;
tabID: number;
}
const REGISTER_MAX_AGE = 30 * 1000; // 30 seconds
let __register: ExpiryMap<string, RegisteredItem> | null = null;
export function getAutoLoginForTab(tabID: number): SearchResult | null {
const register = getRegister();
const key = `tab-${tabID}`;
if (register.has(key)) {
const item = (register.get(key) as RegisteredItem).entry;
register.delete(key);
return item;
}
return null;
}
function getRegister(): ExpiryMap<string, RegisteredItem> {
if (!__register) {
__register = new ExpiryMap(REGISTER_MAX_AGE);
}
return __register;
}
export function registerAutoLogin(entry: SearchResult, tabID: number): void {
const register = getRegister();
register.set(`tab-${tabID}`, { entry, tabID });
}
================================================
FILE: source/background/services/config.ts
================================================
import { getSyncValue, setSyncValue } from "./storage.js";
import { Configuration, InputButtonType, SyncStorageItem } from "../types.js";
import { naiveClone } from "../../shared/library/clone.js";
const DEFAULTS: Configuration = {
entryIcons: true,
inputButtonDefault: InputButtonType.LargeButton,
saveNewLogins: true,
theme: "light",
useSystemTheme: true
};
let __lastConfig: Configuration | null = null;
export function getConfig(): Configuration {
if (!__lastConfig) {
throw new Error("No configuration available");
}
return __lastConfig;
}
export async function initialise() {
__lastConfig = await updateConfigWithDefaults();
}
export async function updateConfigValue<T extends keyof Configuration>(key: T, value: Configuration[T]): Promise<void> {
const configRaw = await getSyncValue(SyncStorageItem.Configuration);
const config = configRaw ? JSON.parse(configRaw) : naiveClone(DEFAULTS);
config[key] = value;
__lastConfig = config;
await setSyncValue(SyncStorageItem.Configuration, JSON.stringify(config));
}
async function updateConfigWithDefaults(): Promise<Configuration> {
let configRaw = await getSyncValue(SyncStorageItem.Configuration);
const config = configRaw ? JSON.parse(configRaw) : { ...DEFAULTS };
for (const key in DEFAULTS) {
if (typeof config[key] === "undefined") {
config[key] = DEFAULTS[key];
}
}
await setSyncValue(SyncStorageItem.Configuration, JSON.stringify(config));
return config;
}
================================================
FILE: source/background/services/crypto.ts
================================================
import { EncryptionAlgorithm, createAdapter } from "iocane";
import { deriveSecretKey, importECDHKey } from "./cryptoKeys.js";
export async function decryptPayload(
payload: string,
sourcePublicKey: string,
targetPrivateKey: string
): Promise<string> {
const privateKey = await importECDHKey(targetPrivateKey);
const publicKey = await importECDHKey(sourcePublicKey);
const secret = await deriveSecretKey(privateKey, publicKey);
return createAdapter().decrypt(payload, secret) as Promise<string>;
}
export async function encryptPayload(
payload: string,
sourcePrivateKey: string,
targetPublicKey: string
): Promise<string> {
const privateKey = await importECDHKey(sourcePrivateKey);
const publicKey = await importECDHKey(targetPublicKey);
const secret = await deriveSecretKey(privateKey, publicKey);
return createAdapter()
.setAlgorithm(EncryptionAlgorithm.GCM)
.setDerivationRounds(100000)
.encrypt(payload, secret) as Promise<string>;
}
================================================
FILE: source/background/services/cryptoKeys.ts
================================================
import { ulid } from "ulidx";
import { log } from "./log.js";
import { getLocalValue, setLocalValue } from "./storage.js";
import { arrayBufferToHex } from "../../shared/library/buffer.js";
import { API_KEY_ALGO, API_KEY_CURVE } from "../../shared/symbols.js";
import { LocalStorageItem } from "../types.js";
import { Layerr } from "layerr";
async function createKeys(): Promise<{
privateKey: string;
publicKey: string;
}> {
const { privateKey, publicKey } = await window.crypto.subtle.generateKey(
{
name: API_KEY_ALGO,
namedCurve: API_KEY_CURVE
},
true,
["deriveKey"]
);
log("generating public and private key pair for browser auth");
const privateKeyStr = await exportECDHKey(privateKey);
const publicKeyStr = await exportECDHKey(publicKey);
log("generated new browser auth keys");
return {
privateKey: privateKeyStr,
publicKey: publicKeyStr
};
}
export async function deriveSecretKey(privateKey: CryptoKey, publicKey: CryptoKey): Promise<string> {
let cryptoKey: CryptoKey;
try {
cryptoKey = await window.crypto.subtle.deriveKey(
{
name: API_KEY_ALGO,
public: publicKey
},
privateKey,
{
name: "AES-GCM",
length: 256
},
true,
["encrypt", "decrypt"]
);
} catch (err) {
throw new Layerr(err, "Failed deriving secret key");
}
const exported = await window.crypto.subtle.exportKey("raw", cryptoKey);
return arrayBufferToHex(exported);
}
async function exportECDHKey(key: CryptoKey): Promise<string> {
try {
const exported = await window.crypto.subtle.exportKey("jwk", key);
return JSON.stringify(exported);
} catch (err) {
throw new Layerr(err, "Failed exporting ECDH key");
}
}
export async function generateKeys(): Promise<void> {
let [apiPrivate, apiPublic, clientID] = await Promise.all([
getLocalValue(LocalStorageItem.APIPrivateKey),
getLocalValue(LocalStorageItem.APIPublicKey),
getLocalValue(LocalStorageItem.APIClientID)
]);
if (apiPrivate && apiPublic && clientID) return;
// Regenerate
log("api keys missing: will generate");
const { privateKey, publicKey } = await createKeys();
clientID = ulid();
await setLocalValue(LocalStorageItem.APIPrivateKey, privateKey);
await setLocalValue(LocalStorageItem.APIPublicKey, publicKey);
await setLocalValue(LocalStorageItem.APIClientID, clientID);
}
export async function importECDHKey(key: string): Promise<CryptoKey> {
let jwk: JsonWebKey;
try {
jwk = JSON.parse(key) as JsonWebKey;
} catch (err) {
throw new Layerr(err, "Failed importing ECDH key");
}
const usages: Array<KeyUsage> = jwk.key_ops && jwk.key_ops.includes("deriveKey") ? ["deriveKey"] : [];
return window.crypto.subtle.importKey(
"jwk",
jwk,
{
name: API_KEY_ALGO,
namedCurve: API_KEY_CURVE
},
true,
usages
);
}
================================================
FILE: source/background/services/desktop/actions.ts
================================================
import { Layerr } from "layerr";
import { EntryID, EntryType, GroupID, SearchResult, VaultFacade, VaultSourceID } from "buttercup";
import { getLocalValue } from "../storage.js";
import { sendDesktopRequest } from "./request.js";
import { generateAuthHeader } from "./header.js";
import { LocalStorageItem, OTP, VaultSourceDescription, VaultsTree } from "../../types.js";
export async function authenticateBrowserAccess(code: string): Promise<string> {
const localPublicKey = await getLocalValue(LocalStorageItem.APIPublicKey);
const clientID = await getLocalValue(LocalStorageItem.APIClientID);
if (!localPublicKey) {
throw new Error("No local public key available");
}
if (!clientID) {
throw new Error("No API client ID set");
}
const { publicKey } = (await sendDesktopRequest({
method: "POST",
route: "/v1/auth/response",
payload: {
code,
id: clientID,
publicKey: localPublicKey
}
})) as { publicKey: string };
if (!publicKey) {
throw new Layerr("No server public key received from browser authentication");
}
return publicKey;
}
export async function getEntrySearchResults(
entries: Array<{ entryID: EntryID; sourceID: VaultSourceID }>
): Promise<Array<SearchResult>> {
const authHeader = await generateAuthHeader();
const { results } = (await sendDesktopRequest({
method: "POST",
route: "/v1/entries/specific",
auth: authHeader,
payload: {
entries
}
})) as {
results: Array<SearchResult>;
};
return results;
}
export async function getOTPs(): Promise<Array<OTP>> {
const authHeader = await generateAuthHeader();
const { otps } = (await sendDesktopRequest({
method: "GET",
route: "/v1/otps",
auth: authHeader
})) as {
otps: Array<OTP>;
};
return otps;
}
export async function getVaultSources(): Promise<Array<VaultSourceDescription>> {
const authHeader = await generateAuthHeader();
const { sources } = (await sendDesktopRequest({
method: "GET",
route: "/v1/vaults",
auth: authHeader
})) as {
sources: Array<VaultSourceDescription>;
};
return sources;
}
export async function getVaultsTree(): Promise<VaultsTree> {
const authHeader = await generateAuthHeader();
const { names, tree } = (await sendDesktopRequest({
method: "GET",
route: "/v1/vaults-tree",
auth: authHeader
})) as {
names?: Record<VaultSourceID, string>;
tree: Record<VaultSourceID, VaultFacade>;
};
return Object.keys(tree).reduce((output, sourceID) => {
return {
...output,
[sourceID]: {
...tree[sourceID],
name: names?.[sourceID] ?? "Untitled vault"
}
};
}, {});
}
export async function hasConnection(): Promise<boolean> {
const serverPublicKey = await getLocalValue(LocalStorageItem.APIServerPublicKey);
return !!serverPublicKey;
}
export async function initiateConnection(): Promise<void> {
await sendDesktopRequest({
method: "POST",
route: "/v1/auth/request",
payload: {
client: "browser",
purpose: "vaults-access",
rev: 1
}
});
}
export async function promptSourceLock(sourceID: VaultSourceID): Promise<boolean> {
const authHeader = await generateAuthHeader();
const status = await sendDesktopRequest({
method: "POST",
route: `/v1/vaults/${sourceID}/lock`,
auth: authHeader,
output: "status"
});
return status === 200;
}
export async function promptSourceUnlock(sourceID: VaultSourceID): Promise<void> {
const authHeader = await generateAuthHeader();
await sendDesktopRequest({
method: "POST",
route: `/v1/vaults/${sourceID}/unlock`,
auth: authHeader
});
}
export async function saveExistingEntry(
sourceID: VaultSourceID,
groupID: GroupID,
entryID: EntryID,
properties: Record<string, string>
): Promise<void> {
const authHeader = await generateAuthHeader();
await sendDesktopRequest({
method: "PATCH",
route: `/v1/vaults/${sourceID}/group/${groupID}/entry/${entryID}`,
auth: authHeader,
payload: {
properties
}
});
}
export async function saveNewEntry(
sourceID: VaultSourceID,
groupID: GroupID,
entryType: EntryType,
properties: Record<string, string>
): Promise<EntryID> {
const authHeader = await generateAuthHeader();
const { entryID } = (await sendDesktopRequest({
method: "POST",
route: `/v1/vaults/${sourceID}/group/${groupID}/entry`,
auth: authHeader,
payload: {
properties,
type: entryType
}
})) as {
entryID: EntryID;
};
return entryID;
}
export async function searchEntriesByURL(url: string): Promise<Array<SearchResult>> {
const authHeader = await generateAuthHeader();
const { results } = (await sendDesktopRequest({
method: "GET",
route: "/v1/entries",
payload: {
type: "url",
url
},
auth: authHeader
})) as {
results: Array<SearchResult>;
};
return results;
}
export async function searchEntriesByTerm(term: string): Promise<Array<SearchResult>> {
const authHeader = await generateAuthHeader();
const { results } = (await sendDesktopRequest({
method: "GET",
route: "/v1/entries",
payload: {
type: "term",
term
},
auth: authHeader
})) as {
results: Array<SearchResult>;
};
return results;
}
export async function testAuth(): Promise<void> {
const authHeader = await generateAuthHeader();
try {
await sendDesktopRequest({
method: "POST",
route: "/v1/auth/test",
payload: {
client: "browser",
purpose: "vaults-access",
rev: 1
},
auth: authHeader
});
} catch (err) {
console.error(err);
throw new Layerr(err, "Desktop connection failed");
}
}
================================================
FILE: source/background/services/desktop/header.ts
================================================
import { Layerr } from "layerr";
import { LocalStorageItem } from "../../types.js";
import { getLocalValue } from "../storage.js";
export async function generateAuthHeader(): Promise<string> {
const clientID = await getLocalValue(LocalStorageItem.APIClientID);
if (!clientID) {
throw new Layerr(
{
info: {
i18n: "error.code.desktop-connection-not-authorised"
}
},
"No API client ID set"
);
}
return `Client ${clientID}`;
}
================================================
FILE: source/background/services/desktop/request.ts
================================================
import { Layerr } from "layerr";
import joinURL from "url-join";
import { DESKTOP_API_PORT } from "../../../shared/symbols.js";
import { decryptPayload, encryptPayload } from "../crypto.js";
import { getLocalValue } from "../storage.js";
import { LocalStorageItem } from "../../types.js";
type OutputType = "body" | "status" | undefined;
interface DesktopRequestConfig<O extends OutputType> {
auth?: string | null;
method: string;
output?: O;
payload?: Record<string, any> | null;
route: string;
}
const DESKTOP_URL_BASE = `http://localhost:${DESKTOP_API_PORT}`;
export async function sendDesktopRequest<O extends undefined>(
config: DesktopRequestConfig<O>
): Promise<string | Record<string, any>>;
export async function sendDesktopRequest<O extends "status">(config: DesktopRequestConfig<O>): Promise<number>;
export async function sendDesktopRequest<O extends "body">(
config: DesktopRequestConfig<O>
): Promise<string | Record<string, any>>;
export async function sendDesktopRequest<O extends OutputType>(
config: DesktopRequestConfig<O>
): Promise<string | Record<string, any> | number> {
const { auth = null, method, output = "body", payload = null, route } = config;
// Prepare un-encrypted configuration first
let url = joinURL(DESKTOP_URL_BASE, route);
const requestConfig: RequestInit = {
method,
headers: {}
};
if (payload !== null) {
if (/^get$/i.test(method)) {
const newURL = new URL(url);
for (const prop in payload) {
if (payload.hasOwnProperty(prop)) {
newURL.searchParams.set(prop, payload[prop]);
}
}
url = newURL.toString();
} else {
requestConfig.body = JSON.stringify(payload);
Object.assign(requestConfig.headers as HeadersInit, {
"Content-Type": "application/json"
});
}
}
if (auth !== null) {
requestConfig.headers = requestConfig.headers || {};
// Request requires encryption, perform setup now
requestConfig.headers["Authorization"] = auth;
if (typeof requestConfig.body === "string") {
requestConfig.headers["X-Content-Type"] = requestConfig.headers["Content-Type"];
requestConfig.headers["Content-Type"] = "text/plain";
// Encrypt
const privateKey = await getLocalValue(LocalStorageItem.APIPrivateKey);
const publicKey = await getLocalValue(LocalStorageItem.APIServerPublicKey);
if (!privateKey) {
throw new Error("Authenticated request failed: No private key available");
}
if (!publicKey) {
throw new Error("Authenticated request failed: No public key available");
}
requestConfig.body = await encryptPayload(requestConfig.body, privateKey, publicKey);
}
}
// Make request
const resp = await fetch(url, requestConfig);
if (!resp.ok) {
throw new Layerr(
{
info: {
code: "desktop-request-failed",
status: resp.status,
statusText: resp.statusText
}
},
`Desktop request failed: ${resp.status} ${resp.statusText}`
);
}
if (output === "status") {
return resp.status;
}
// Handle encrypted response
if (resp.headers.get("X-Bcup-API")) {
const components = resp.headers.get("X-Bcup-API")?.split(",") ?? [];
if (components.includes("enc")) {
const content = await resp.text();
const contentType = resp.headers.get("X-Content-Type") || resp.headers.get("Content-Type") || "text/plain";
// Decrypt
const privateKey = await getLocalValue(LocalStorageItem.APIPrivateKey);
const publicKey = await getLocalValue(LocalStorageItem.APIServerPublicKey);
if (!privateKey) {
throw new Error("Decrypting response failed: No private key available");
}
if (!publicKey) {
throw new Error("Decrypting response failed: No public key available");
}
const rawDecrypted = await decryptPayload(content, publicKey, privateKey);
return /application\/json/.test(contentType) ? JSON.parse(rawDecrypted) : rawDecrypted;
}
}
// Standard, unencrypted response
if (/application\/json/.test(resp.headers.get("Content-Type") ?? "")) {
return resp.json();
}
return resp.text();
}
================================================
FILE: source/background/services/disabledDomains.ts
================================================
import { getSyncValue, setSyncValue } from "./storage.js";
import { SyncStorageItem } from "../types.js";
export async function disableLoginsOnDomain(domain: string): Promise<void> {
const currentDomains = new Set(await getDisabledDomains());
currentDomains.add(domain);
await setSyncValue(SyncStorageItem.DisabledDomains, JSON.stringify([...currentDomains]));
}
export async function getDisabledDomains(): Promise<Array<string>> {
const currentDomainsRaw = await getSyncValue(SyncStorageItem.DisabledDomains);
return currentDomainsRaw ? JSON.parse(currentDomainsRaw) : [];
}
export async function removeDisabledFlagForDomain(domain: string): Promise<void> {
const currentDomains = new Set(await getDisabledDomains());
currentDomains.delete(domain);
await setSyncValue(SyncStorageItem.DisabledDomains, JSON.stringify([...currentDomains]));
}
================================================
FILE: source/background/services/entry.ts
================================================
import { SearchResult } from "buttercup";
import { createNewTab } from "../../shared/library/extension.js";
import { formatURL } from "../../shared/library/url.js";
export async function openEntryPageInNewTab(_: SearchResult, url: string): Promise<number> {
const tab = await createNewTab(formatURL(url));
if (typeof tab?.id !== "number") {
throw new Error("No tab ID for created tab");
}
return tab.id;
}
================================================
FILE: source/background/services/init.ts
================================================
import { EventEmitter } from "eventemitter3";
import { log } from "./log.js";
import { initialise as initialiseMessaging } from "./messaging.js";
import { initialise as initialiseStorage } from "./storage.js";
import { initialise as initialiseConfig } from "./config.js";
import { generateKeys } from "./cryptoKeys.js";
import { initialise as initialiseI18n } from "../../shared/i18n/trans.js";
import { getLanguage } from "../../shared/library/i18n.js";
import { showPendingNotifications } from "./notifications.js";
enum Initialisation {
Complete = "complete",
Idle = "idle",
Running = "running"
}
const __initEE = new EventEmitter();
let __initialisation: Initialisation = Initialisation.Idle;
export async function initialise(): Promise<void> {
if (__initialisation !== Initialisation.Idle) return;
__initialisation = Initialisation.Running;
log("initialising");
initialiseMessaging();
await initialiseStorage();
await initialiseConfig();
await initialiseI18n(getLanguage());
await generateKeys();
log("initialisation complete");
__initialisation = Initialisation.Complete;
__initEE.emit("initialised");
await showPendingNotifications();
}
export async function resetInitialisation(): Promise<void> {
log("resetting initialisation");
__initialisation = Initialisation.Idle;
await initialise();
}
export async function waitForInitialisation(): Promise<void> {
return new Promise<void>((resolve) => {
if (__initialisation === Initialisation.Complete) return resolve();
__initEE.once("initialised", resolve);
});
}
================================================
FILE: source/background/services/log.ts
================================================
import { createLog } from "../../shared/library/log.js";
const LOG_NAME = "buttercup:browser:background";
let __logger: ReturnType<typeof createLog>;
export function log(...args: Array<any>): void {
if (!__logger) {
__logger = createLog(LOG_NAME, true);
}
return __logger(...args);
}
================================================
FILE: source/background/services/loginMemory.ts
================================================
import ExpiryMap from "expiry-map";
import { searchEntriesByTerm } from "./desktop/actions.js";
import { domainsReferToSameParent, extractDomain } from "../../shared/library/domain.js";
import { UsedCredentials } from "../types.js";
interface LoginMemoryItem {
credentials: UsedCredentials;
tabID: number;
}
const LOGIN_MAX_AGE = 15 * 60 * 1000; // 15 min
let __loginMemory: ExpiryMap<string, LoginMemoryItem> | null = null;
export function clearCredentials(id: string): void {
const memory = getLoginMemory();
if (memory.has(id)) {
memory.delete(id);
}
if (memory.has("last")) {
const last = memory.get("last");
if (last?.credentials.id === id) {
memory.delete("last");
}
}
}
export async function credentialsAlreadyStored(credentials: UsedCredentials): Promise<boolean> {
const results = await searchEntriesByTerm(credentials.username);
const usedDomain = extractDomain(credentials.url);
return results.some((result) => {
// Check username
if (credentials.username !== result.properties.username) return false;
// Skip if search result has no URLs
if (result.urls.length <= 0) return false;
// Check if any of the domains match this one
const resultDomains = result.urls.map((url) => extractDomain(url));
if (!resultDomains.some((resDomain) => domainsReferToSameParent(resDomain, usedDomain))) {
// No matches
return false;
}
// Check if props match
return (
result.properties.username === credentials.username && result.properties.password === credentials.password
);
});
}
export function getAllCredentials(): Array<UsedCredentials> {
const memory = getLoginMemory();
const credentials: Array<UsedCredentials> = [];
for (const [key, item] of memory.entries()) {
if (key === "last") continue;
credentials.push(item.credentials);
}
return credentials;
}
export function getCredentialsForID(id: string): UsedCredentials | null {
const memory = getLoginMemory();
return memory.has(id) ? (memory.get(id) as LoginMemoryItem).credentials : null;
}
export function getLastCredentials(tabID: number): UsedCredentials | null {
const memory = getLoginMemory();
const last = memory.has("last") ? memory.get("last") : null;
if (!last) return null;
return last.tabID === tabID ? last.credentials : null;
}
function getLoginMemory(): ExpiryMap<string, LoginMemoryItem> {
if (!__loginMemory) {
__loginMemory = new ExpiryMap(LOGIN_MAX_AGE);
}
return __loginMemory;
}
export function stopPromptForID(id: string): void {
const memory = getLoginMemory();
if (memory.has(id)) {
const existing = memory.get(id) as LoginMemoryItem;
memory.set(id, {
...existing,
credentials: {
...existing.credentials,
promptSave: false
}
});
}
const last = memory.has("last") ? memory.get("last") : null;
if (last?.credentials.id === id) {
memory.set("last", {
...last,
credentials: {
...last.credentials,
promptSave: false
}
});
}
}
export function updateUsedCredentials(credentials: UsedCredentials, tabID: number): void {
const memory = getLoginMemory();
const payload: LoginMemoryItem = {
credentials,
tabID
};
memory.set(credentials.id, payload);
memory.set("last", payload);
}
================================================
FILE: source/background/services/messaging.ts
================================================
import { Layerr } from "layerr";
import { EntryType, EntryURLType, VaultSourceID, VaultSourceStatus, getEntryURLs } from "buttercup";
import { getExtensionAPI } from "../../shared/extension.js";
import {
authenticateBrowserAccess,
getEntrySearchResults,
getOTPs,
getVaultSources,
getVaultsTree,
hasConnection,
initiateConnection,
promptSourceLock,
promptSourceUnlock,
saveExistingEntry,
saveNewEntry,
searchEntriesByTerm,
searchEntriesByURL,
testAuth
} from "./desktop/actions.js";
import { clearLocalStorage, removeLocalValue, setLocalValue } from "./storage.js";
import { errorToString } from "../../shared/library/error.js";
import {
clearCredentials,
credentialsAlreadyStored,
getAllCredentials,
getCredentialsForID,
getLastCredentials,
stopPromptForID,
updateUsedCredentials
} from "./loginMemory.js";
import { getConfig, updateConfigValue } from "./config.js";
import { disableLoginsOnDomain, getDisabledDomains, removeDisabledFlagForDomain } from "./disabledDomains.js";
import { log } from "./log.js";
import { resetInitialisation } from "./init.js";
import { getRecents, trackRecentUsage } from "./recents.js";
import { openEntryPageInNewTab } from "./entry.js";
import { getAutoLoginForTab, registerAutoLogin } from "./autoLogin.js";
import { extractDomainFromCredentials } from "../library/domain.js";
import {
BackgroundMessage,
BackgroundMessageType,
BackgroundResponse,
LocalStorageItem,
TabEventType
} from "../types.js";
import { markNotificationRead } from "./notifications.js";
import { createNewTab, getExtensionURL } from "../../shared/library/extension.js";
import { sendTabsMessage } from "./tabs.js";
async function handleMessage(
msg: BackgroundMessage,
sender: chrome.runtime.MessageSender,
sendResponse: (resp: BackgroundResponse) => void
) {
switch (msg.type) {
case BackgroundMessageType.AuthenticateDesktopConnection: {
const { code } = msg;
if (!code) {
throw new Error("No auth code provided");
}
log("complete desktop authentication");
const publicKey = await authenticateBrowserAccess(code);
await setLocalValue(LocalStorageItem.APIServerPublicKey, publicKey);
sendResponse({});
break;
}
case BackgroundMessageType.CheckDesktopConnection: {
const available = await hasConnection();
if (available) {
await testAuth();
}
sendResponse({
available
});
break;
}
case BackgroundMessageType.ClearDesktopAuthentication: {
log("clear desktop authentication");
await removeLocalValue(LocalStorageItem.APIClientID);
await removeLocalValue(LocalStorageItem.APIServerPublicKey);
sendResponse({});
break;
}
case BackgroundMessageType.ClearSavedCredentials: {
const { credentialsID } = msg;
if (!credentialsID) {
throw new Error("No credentials ID provided");
}
log(`clear saved credentials: ${credentialsID}`);
clearCredentials(credentialsID);
sendResponse({});
break;
}
case BackgroundMessageType.ClearSavedCredentialsPrompt: {
const { credentialsID } = msg;
if (!credentialsID) {
throw new Error("No credentials ID provided");
}
log(`clear saved credentials prompt: ${credentialsID}`);
stopPromptForID(credentialsID);
await sendTabsMessage({
type: TabEventType.CloseSaveDialog
});
sendResponse({});
break;
}
case BackgroundMessageType.DeleteDisabledDomains: {
const { domains } = msg;
if (!domains) {
throw new Error("No domains list provided");
}
log(`remove disabled domains: ${domains.join(", ")}`);
for (const domain of domains) {
await removeDisabledFlagForDomain(domain);
}
sendResponse({});
break;
}
case BackgroundMessageType.DisableSavePromptForCredentials: {
const { credentialsID } = msg;
if (!credentialsID) {
throw new Error("No credentials ID provided");
}
log(`disable save prompt for credentials: ${credentialsID}`);
try {
const credentials = getCredentialsForID(credentialsID);
const domain = credentials ? extractDomainFromCredentials(credentials) : null;
if (domain) {
log(`disable save prompt for domain: ${domain}`);
await disableLoginsOnDomain(domain);
}
} catch (err) {
throw new Layerr(err, "Failed disabling save prompt for domain");
}
await sendTabsMessage({
type: TabEventType.CloseSaveDialog
});
sendResponse({});
break;
}
case BackgroundMessageType.GetAutoLoginForTab: {
const tabID = sender.tab?.id;
if (!tabID) {
sendResponse({ autoLogin: null });
break;
}
const entry = getAutoLoginForTab(tabID);
sendResponse({ autoLogin: entry });
break;
}
case BackgroundMessageType.GetConfiguration: {
const config = getConfig();
sendResponse({
config
});
break;
}
case BackgroundMessageType.GetDesktopVaultSources: {
const sources = await getVaultSources();
sendResponse({
vaultSources: sources
});
break;
}
case BackgroundMessageType.GetDesktopVaultsTree: {
const tree = await getVaultsTree();
sendResponse({
vaultsTree: tree
});
break;
}
case BackgroundMessageType.GetDisabledDomains: {
const domains = await getDisabledDomains();
sendResponse({
domains
});
break;
}
case BackgroundMessageType.GetLastSavedCredentials: {
const { excludeSaved = false } = msg;
const tabID = sender.tab?.id;
if (!tabID) {
sendResponse({ credentials: [null] });
break;
}
let credentials = getLastCredentials(tabID);
if (credentials && excludeSaved && (await credentialsAlreadyStored(credentials))) {
credentials = null;
}
sendResponse({
credentials: [credentials]
});
break;
}
case BackgroundMessageType.GetOTPs: {
const otps = await getOTPs();
sendResponse({
otps
});
break;
}
case BackgroundMessageType.GetRecentEntries: {
const { count = 10 } = msg;
const sources = await getVaultSources();
const unlockedIDs: Array<VaultSourceID> = sources.reduce((output, source) => {
if (source.state === VaultSourceStatus.Unlocked) {
return [...output, source.id];
}
return output;
}, []);
const recentItems = await getRecents(unlockedIDs);
recentItems.splice(count, Infinity);
const searchResults = await getEntrySearchResults(
recentItems.map((item) => ({
entryID: item.entryID,
sourceID: item.sourceID
}))
);
sendResponse({
searchResults
});
break;
}
case BackgroundMessageType.GetSavedCredentials: {
const credentials = getAllCredentials();
sendResponse({
credentials
});
break;
}
case BackgroundMessageType.GetSavedCredentialsForID: {
const { credentialsID, excludeSaved = false } = msg;
if (!credentialsID) {
throw new Error("No credentials ID provided");
}
let credentials = getCredentialsForID(credentialsID);
if (credentials && excludeSaved && (await credentialsAlreadyStored(credentials))) {
credentials = null;
}
sendResponse({
credentials: [credentials]
});
break;
}
case BackgroundMessageType.InitiateDesktopConnection: {
log("start desktop authentication");
await initiateConnection();
await createNewTab(getExtensionURL("full.html#/connect"));
sendResponse({});
break;
}
case BackgroundMessageType.MarkNotificationRead: {
const { notification } = msg;
if (!notification) {
throw new Error("No notification provided");
}
log(`mark notification read: ${notification}`);
await markNotificationRead(notification);
sendResponse({});
break;
}
case BackgroundMessageType.OpenEntryPage: {
const { autoLogin, entry } = msg;
if (!entry) {
throw new Error("No entry provided");
}
const [url = null] = getEntryURLs(entry.properties, EntryURLType.Login);
if (!url) {
sendResponse({ opened: false });
return;
}
log(`open entry page by url: ${entry.id} (${url})`);
const tabID = await openEntryPageInNewTab(entry, url);
if (autoLogin) {
registerAutoLogin(entry, tabID);
}
sendResponse({ opened: true });
break;
}
case BackgroundMessageType.OpenSaveCredentialsPage: {
await createNewTab(getExtensionURL("full.html#/save-credentials"));
await sendTabsMessage({
type: TabEventType.CloseSaveDialog
});
sendResponse({});
break;
}
case BackgroundMessageType.PromptLockSource: {
const { sourceID } = msg;
if (!sourceID) {
throw new Error("No source ID provided");
}
log(`request lock source: ${sourceID}`);
const locked = await promptSourceLock(sourceID);
sendResponse({
locked
});
break;
}
case BackgroundMessageType.PromptUnlockSource: {
const { sourceID } = msg;
if (!sourceID) {
throw new Error("No source ID provided");
}
log(`request unlock source: ${sourceID}`);
await promptSourceUnlock(sourceID);
sendResponse({});
break;
}
case BackgroundMessageType.ResetSettings: {
log(`reset settings`);
await clearLocalStorage();
await resetInitialisation();
sendResponse({});
break;
}
case BackgroundMessageType.SaveCredentialsToVault: {
const { sourceID, groupID, entryID = null, entryProperties, entryType = EntryType.Website } = msg;
if (!sourceID) {
throw new Error("No source ID provided");
}
if (!groupID) {
throw new Error("No group ID provided");
}
if (!entryProperties) {
throw new Error("No entry properties provided");
}
if (entryID) {
log(`save credentials to existing entry: ${entryID} (source=${sourceID})`);
await saveExistingEntry(sourceID, groupID, entryID, entryProperties);
sendResponse({
entryID: null
});
} else {
log(`save credentials to new entry (source=${sourceID})`);
const entryID = await saveNewEntry(sourceID, groupID, entryType, entryProperties);
sendResponse({
entryID
});
}
break;
}
case BackgroundMessageType.SaveUsedCredentials: {
const { credentials } = msg;
if (!credentials) {
throw new Error("No source ID provided");
}
if (!sender.tab?.id) {
throw new Error("No tab ID available for background message");
}
updateUsedCredentials(credentials, sender.tab.id);
sendResponse({});
break;
}
case BackgroundMessageType.SearchEntriesByTerm: {
const { searchTerm } = msg;
if (!searchTerm) {
throw new Error("No search term provided");
}
const searchResults = await searchEntriesByTerm(searchTerm);
sendResponse({
searchResults
});
break;
}
case BackgroundMessageType.SearchEntriesByURL: {
const { url } = msg;
if (!url) {
throw new Error("No URL provided");
}
const searchResults = await searchEntriesByURL(url);
sendResponse({
searchResults
});
break;
}
case BackgroundMessageType.SetConfigurationValue: {
const { configKey, configValue } = msg;
if (!configKey || typeof configValue === "undefined") {
throw new Error("Invalid configuration proivided provided");
}
await updateConfigValue(configKey, configValue);
sendResponse({});
break;
}
case BackgroundMessageType.TrackRecentEntry: {
const { entry } = msg;
if (!entry) {
throw new Error("No entry provided");
}
if (!entry.sourceID) {
throw new Error(`No source ID in entry result: ${entry.id}`);
}
await trackRecentUsage(entry.sourceID, entry.id);
sendResponse({});
break;
}
default:
throw new Layerr(`Unrecognised message type: ${(msg as any).type}`);
}
}
export function initialise() {
getExtensionAPI().runtime.onMessage.addListener((request, sender, sendResponse) => {
handleMessage(request, sender, sendResponse).catch((err) => {
console.error(err);
sendResponse({
error: errorToString(new Layerr(err, "Background task failed"))
});
});
return true;
});
}
================================================
FILE: source/background/services/notifications.ts
================================================
import { getSyncValue, setSyncValue } from "./storage.js";
import { SyncStorageItem } from "../types.js";
import { NOTIFICATION_NAMES } from "../../shared/notifications/index.js";
import { createNewTab, getExtensionURL } from "../../shared/library/extension.js";
async function getPendingNotifications(): Promise<Array<string>> {
const existingNotificationsRaw = await getSyncValue(SyncStorageItem.Notifications);
const existingNotifications = existingNotificationsRaw ? existingNotificationsRaw.split(",") : [];
return NOTIFICATION_NAMES.filter((name) => !existingNotifications.includes(name));
}
export async function markNotificationRead(name: string): Promise<void> {
const existingNotificationsRaw = await getSyncValue(SyncStorageItem.Notifications);
const notifications = existingNotificationsRaw ? existingNotificationsRaw.split(",") : [];
if (!notifications.includes(name)) {
notifications.push(name);
}
await setSyncValue(SyncStorageItem.Notifications, notifications.join(","));
}
export async function showPendingNotifications(): Promise<void> {
const notifications = await getPendingNotifications();
if (notifications.length <= 0) return;
await createNewTab(getExtensionURL(`full.html#/notifications?notifications=${notifications.join(",")}`));
}
================================================
FILE: source/background/services/recents.ts
================================================
import { EntryID, VaultSourceID } from "buttercup";
import { ChannelQueue, TaskPriority } from "@buttercup/channel-queue";
import ms from "ms";
import { getSyncValue, setSyncValue } from "./storage.js";
import { SyncStorageItem } from "../types.js";
export interface RecentItem {
entryID: EntryID;
sourceID: VaultSourceID;
uses: Array<number>;
}
const MAX_USE_AGE = ms("30d");
let __queue: ChannelQueue | null = null;
function getQueue(): ChannelQueue {
if (!__queue) {
__queue = new ChannelQueue();
}
return __queue;
}
export async function getRecents(sourceIDs: Array<VaultSourceID>): Promise<Array<RecentItem>> {
const currentRecentsRaw = await getSyncValue(SyncStorageItem.RecentItems);
if (!currentRecentsRaw) return [];
const currentRecents = JSON.parse(currentRecentsRaw) as Array<RecentItem>;
return currentRecents.filter((recent) => sourceIDs.includes(recent.sourceID));
}
function sortUses(itemA: RecentItem, itemB: RecentItem): number {
if (itemA.uses.length > itemB.uses.length) return -1;
if (itemB.uses.length > itemA.uses.length) return 1;
return 0;
}
function stripOldUses(items: Array<RecentItem>): Array<RecentItem> {
const earliestTs = Date.now() - MAX_USE_AGE;
return items.reduce((output: Array<RecentItem>, item: RecentItem) => {
const hasRecentUse = item.uses.some((ts) => ts >= earliestTs);
if (!hasRecentUse) return output;
return [
...output,
{
...item,
uses: item.uses.filter((ts) => ts >= earliestTs)
}
];
}, []);
}
export async function trackRecentUsage(sourceID: VaultSourceID, entryID: EntryID): Promise<void> {
const channel = getQueue().channel("write");
await channel.enqueue(
async () => {
const currentRecentsRaw = await getSyncValue(SyncStorageItem.RecentItems);
let currentRecents = currentRecentsRaw ? (JSON.parse(currentRecentsRaw) as Array<RecentItem>) : [];
let existingResult = currentRecents.find(
(recent) => recent.sourceID === sourceID && recent.entryID === entryID
);
if (existingResult) {
existingResult.uses.unshift(Date.now());
} else {
existingResult = {
entryID,
sourceID,
uses: [Date.now()]
};
currentRecents.push(existingResult);
}
currentRecents = stripOldUses(currentRecents);
currentRecents.sort(sortUses);
await setSyncValue(SyncStorageItem.RecentItems, JSON.stringify(currentRecents));
},
TaskPriority.Normal,
`${sourceID}-${entryID}`
);
}
================================================
FILE: source/background/services/storage/BrowserStorageInterface.ts
================================================
import { StorageInterface } from "buttercup";
import { getExtensionAPI } from "../../../shared/extension.js";
export function getSyncStorage() {
return getExtensionAPI().storage.sync;
}
export function getNonSyncStorage() {
return getExtensionAPI().storage.local;
}
export class BrowserStorageInterface extends StorageInterface {
protected _storage: chrome.storage.StorageArea;
constructor(storage: chrome.storage.StorageArea = getSyncStorage()) {
super();
this._storage = storage;
}
get storage() {
return this._storage;
}
async getAllKeys() {
return new Promise<Array<string>>((resolve) => {
this.storage.get(null, (allItems) => {
resolve(Object.keys(allItems));
});
});
}
async getValue(name: string) {
return new Promise<string>((resolve) => {
this.storage.get(name, (items) => {
resolve(items[name]);
});
});
}
async removeKey(name: string) {
return new Promise<void>((resolve) => {
this.storage.remove(name, () => resolve());
});
}
async setValue(name: string, value: any) {
return new Promise<void>((resolve) => {
this.storage.set({ [name]: value }, () => resolve());
});
}
}
================================================
FILE: source/background/services/storage.ts
================================================
import { log } from "./log.js";
import { BrowserStorageInterface, getNonSyncStorage, getSyncStorage } from "./storage/BrowserStorageInterface.js";
import { LocalStorageItem, SyncStorageItem } from "../types.js";
const VALID_LOCAL_KEYS = Object.values(LocalStorageItem);
const VALID_SYNC_KEYS = Object.values(SyncStorageItem);
export async function clearLocalStorage(): Promise<void> {
const localStorage = getLocalStorage();
const syncStorage = getSynchronisedStorage();
const keys = await localStorage.getAllKeys();
for (const key of keys) {
log(`clearing local storage key: ${key}`);
await localStorage.removeKey(key);
}
await syncStorage.removeKey(SyncStorageItem.Notifications);
}
function getLocalStorage(): BrowserStorageInterface {
return new BrowserStorageInterface(getNonSyncStorage());
}
export async function getLocalValue(key: LocalStorageItem): Promise<string | null> {
return getLocalStorage().getValue(key) ?? null;
}
export async function getSyncValue(key: SyncStorageItem): Promise<string | null> {
return getSynchronisedStorage().getValue(key) ?? null;
}
function getSynchronisedStorage(): BrowserStorageInterface {
return new BrowserStorageInterface(getSyncStorage());
}
export async function initialise() {
const localStorage = getLocalStorage();
{
const keys = await localStorage.getAllKeys();
for (const key of keys) {
const valid = VALID_LOCAL_KEYS.find((local) => key === local || key.indexOf(local) === 0);
if (!valid) {
log(`remove unrecognised local storage key: ${key}`);
await localStorage.removeKey(key);
}
}
}
const syncStorage = getSynchronisedStorage();
{
const keys = await syncStorage.getAllKeys();
for (const key of keys) {
const valid = VALID_SYNC_KEYS.find((local) => key === local || key.indexOf(local) === 0);
if (!valid) {
log(`remove unrecognised sync storage key: ${key}`);
await syncStorage.removeKey(key);
}
}
}
}
export async function removeLocalValue(key: LocalStorageItem): Promise<void> {
await getLocalStorage().removeKey(key);
}
export async function removeSyncValue(key: SyncStorageItem): Promise<void> {
await getSynchronisedStorage().removeKey(key);
}
export async function setLocalValue(key: LocalStorageItem, value: string): Promise<void> {
return getLocalStorage().setValue(key, value);
}
export async function setSyncValue(key: SyncStorageItem, value: string): Promise<void> {
return getSynchronisedStorage().setValue(key, value);
}
================================================
FILE: source/background/services/tabs.ts
================================================
import { getExtensionAPI } from "../../shared/extension.js";
import { TabEvent } from "../types.js";
export async function sendTabsMessage(payload: TabEvent, tabIDs: Array<number> | null = null): Promise<void> {
const browser = getExtensionAPI();
const targetTabIDs = Array.isArray(tabIDs)
? tabIDs
: (
await browser.tabs.query({
status: "complete"
})
).reduce((output: Array<number>, tab) => {
if (!tab.id) return output;
return [...output, tab.id];
}, []);
await Promise.all(
targetTabIDs.map(async (tabID) => {
browser.tabs.sendMessage(tabID, payload);
})
);
}
================================================
FILE: source/background/types.ts
================================================
export * from "../shared/types.js";
export enum LocalStorageItem {
APIClientID = "bcup:api:clientID",
APIPrivateKey = "bcup:api:privateKey",
APIPublicKey = "bcup:api:publicKey",
APIServerPublicKey = "bcup:api:serverPublicKey"
}
export enum SyncStorageItem {
Configuration = "bcup:configuration",
DisabledDomains = "bcup:disabledDomains",
Notifications = "bcup:notifications",
RecentItems = "bcup:recents"
}
================================================
FILE: source/full/applications/full.tsx
================================================
import React from "react";
import ReactDOM from "react-dom";
import { App } from "../components/App.js";
import { initialise } from "../services/init.js";
initialise()
.then(() => {
ReactDOM.render(
<App />,
document.getElementById("root"),
);
})
.catch(err => {
console.error(err);
});
================================================
FILE: source/full/components/App.tsx
================================================
import React from "react";
import {
createHashRouter,
RouterProvider
} from "react-router-dom";
import { ThemeProvider } from "../../shared/components/ThemeProvider.jsx";
import { ConnectPage } from "./pages/connect/index.jsx";
import { AttributionsPage } from "./pages/AttributionsPage.jsx";
import { SaveCredentialsPage } from "./pages/saveCredentials/index.js";
import { useBodyThemeClass, useTheme } from "../../shared/hooks/theme.js";
import { RouteError } from "../../shared/components/RouteError.js";
import { DisabledDomainsPage } from "./pages/DisabledDomainsPage.js";
import { NotificationsPage } from "./pages/NotificationsPage.js";
const ROUTER = createHashRouter([
{
path: "/connect",
element: <ConnectPage />,
errorElement: <RouteError />
},
{
path: "/attributions",
element: <AttributionsPage />,
errorElement: <RouteError />
},
{
path: "/disabled-domains",
element: <DisabledDomainsPage />,
errorElement: <RouteError />
},
{
path: "/notifications",
element: <NotificationsPage />,
errorElement: <RouteError />,
loader: ({ request }) => {
const url = new URL(request.url);
const notifications = url.searchParams.get("notifications");
return { notifications };
}
},
{
path: "/save-credentials",
element: <SaveCredentialsPage />,
errorElement: <RouteError />
}
]);
export function App() {
const theme = useTheme();
useBodyThemeClass(theme);
return (
<ThemeProvider darkMode={theme === "dark"}>
<RouterProvider router={ROUTER} />
</ThemeProvider>
);
}
================================================
FILE: source/full/components/Layout.tsx
================================================
import React from "react";
import styled from "styled-components";
import { Divider } from "@blueprintjs/core";
import { ChildElements } from "../types.js";
import BUTTERCUP_LOGO from "../../../resources/buttercup-128.png";
interface LayoutProps {
children: ChildElements;
title: string;
}
const ContentContainer = styled.div`
padding: 1rem;
`;
const Header = styled.div`
margin: 0.5rem 1rem 0;
padding: 0.3rem 0;
display: flex;
justify-content: flex-start;
align-items: center;
`;
const MainContent = styled.div`
width: 100vw;
min-height: 100vh;
padding: 3rem 0;
`;
const Title = styled.h1`
margin: 0px 0px 4px 0px;
padding: 0;
font-size: 18px;
flex: 1;
`;
const TitleImage = styled.img`
width: 28px;
height: 28px;
margin-bottom: 3px;
margin-right: 6px;
`;
const Wrapper = styled.div`
width: 680px;
margin: 0 auto;
display: flex;
flex-direction: column;
@media screen and (max-width: 700px) {
width: 100%;
}
`;
export function Layout({ children, title }: LayoutProps) {
return (
<MainContent>
<Wrapper>
<Header>
<TitleImage src={BUTTERCUP_LOGO} />
<Title>{title}</Title>
</Header>
<Divider />
<ContentContainer>{children}</ContentContainer>
</Wrapper>
</MainContent>
);
}
================================================
FILE: source/full/components/pages/AttributionsPage.tsx
================================================
import React from "react";
import styled from "styled-components";
import { Layout } from "../Layout.js";
import { t } from "../../../shared/i18n/trans.js";
import { useTitle } from "../../hooks/document.js";
import COMPUTER_ICON from "../../../../resources/providers/local-256.png";
const AttributionLI = styled.li`
display: flex;
flex-direction: row;
align-items: center;
`;
const ImageIcon = styled.img`
width: auto;
height: 32px;
margin-right: 12px;
`;
export function AttributionsPage() {
useTitle(t("attributions-page.title"));
return (
<Layout title={t("attributions-page.title")}>
<p>Buttercup is Open Source Software and makes use of many free and openly available libraries and resources.</p>
<p>Below are a list of resource attributions that this browser extension makes use of.</p>
<ul>
<AttributionLI>
<ImageIcon src={COMPUTER_ICON} />
<a target="_blank" href="https://www.flaticon.com/free-icons/computer" title="computer icons">Computer icons created by Freepik - Flaticon</a>
</AttributionLI>
</ul>
</Layout>
);
}
================================================
FILE: source/full/components/pages/DisabledDomainsPage.tsx
================================================
import React, { Fragment, useCallback, useState } from "react";
import cn from "classnames";
import styled from "styled-components";
import { Button, Classes, Intent, NonIdealState, Spinner } from "@blueprintjs/core";
import { useTitle } from "../../hooks/document.js";
import { t } from "../../../shared/i18n/trans.js";
import { Layout } from "../Layout.js";
import { useDisabledDomains } from "../../hooks/disabledDomains.js";
import { ErrorMessage } from "../../../shared/components/ErrorMessage.js";
import { ConfirmDialog } from "../../../shared/components/ConfirmDialog.js";
import { getToaster } from "../../../shared/services/notifications.js";
import { localisedErrorMessage } from "../../../shared/library/error.js";
import { removeDisabledDomain } from "../../services/disabledDomains.js";
const ActionCell = styled.td`
vertical-align: middle !important;
`;
const CenteredContent = styled.div`
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
`;
const LoaderContainer = styled.div`
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
padding: 20px 0px;
`;
const Table = styled.table`
min-width: 80%;
table-layout: fixed;
`;
export function DisabledDomainsPage() {
useTitle(t("disabled-domains-page.title"));
const [reloadCount, setReloadCount] = useState<number>(0);
const [domains, loading, error] = useDisabledDomains([reloadCount]);
const [removeDomain, setRemoveDomain] = useState<string | null>(null);
const handleDomainRemove = useCallback(async () => {
if (!removeDomain) return;
try {
removeDisabledDomain(removeDomain);
setReloadCount(count => count += 1);
} catch (err) {
console.error(err);
getToaster().show({
intent: Intent.DANGER,
message: t("error.generic", { message: localisedErrorMessage(err) }),
timeout: 10000
});
}
setRemoveDomain(null);
}, [removeDomain]);
return (
<Layout title={t("disabled-domains-page.title")}>
<p dangerouslySetInnerHTML={{ __html: t("disabled-domains-page.description") }} />
<h3>{t("disabled-domains-page.disabled-domains.heading")}</h3>
{error && (
<ErrorMessage message={error.message} scroll={false} />
)}
{loading && (
<LoaderContainer>
<Spinner size={60} />
</LoaderContainer>
)}
{!error && !loading && Array.isArray(domains) && (
<Fragment>
{domains.length > 0 && (
<CenteredContent>
<Table className={cn(Classes.HTML_TABLE, Classes.HTML_TABLE_STRIPED)}>
<thead>
<tr>
<th>{t("disabled-domains-page.table.domain-heading")}</th>
<th>{t("disabled-domains-page.table.action-heading")}</th>
</tr>
</thead>
<tbody>
{domains.map((domain, ind) => (
<tr key={`${domain}-${ind}`}>
<td>
<pre>{domain}</pre>
</td>
<ActionCell>
<Button
icon="delete"
intent={Intent.DANGER}
minimal
onClick={() => setRemoveDomain(domain)}
title={t("disabled-domains-page.table.action.delete")}
/>
</ActionCell>
</tr>
))}
</tbody>
</Table>
</CenteredContent>
) || (
<NonIdealState
description={t("disabled-domains-page.table.empty-description")}
icon="exclude-row"
title={t("disabled-domains-page.table.empty-title")}
/>
)}
</Fragment>
)}
<ConfirmDialog
confirmIntent={Intent.DANGER}
confirmText={t("disabled-domains-page.delete-dialog.confirm")}
icon="delete"
isOpen={!!removeDomain}
onClose={() => setRemoveDomain(null)}
onConfirm={handleDomainRemove}
title={t("disabled-domains-page.delete-dialog.title")}
>
<span dangerouslySetInnerHTML={{
__html: t("disabled-domains-page.delete-dialog.description").replace("{{domain}}", removeDomain ?? "")
}} />
</ConfirmDialog>
</Layout>
);
}
================================================
FILE: source/full/components/pages/NotificationsPage.tsx
================================================
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useLoaderData } from "react-router-dom";
import { Icon, Intent, Tab, Tabs } from "@blueprintjs/core";
import { NOTIFICATIONS } from "../../../shared/notifications/index.js";
import { Layout } from "../Layout.js";
import { t } from "../../../shared/i18n/trans.js";
import { updateReadNotifications } from "../../services/notifications.js";
import { getToaster } from "../../../shared/services/notifications.js";
import { localisedErrorMessage } from "../../../shared/library/error.js";
export function NotificationsPage() {
const { notifications: notificationsRaw = "" } = useLoaderData() as {
notifications: string
};
const notificationKeys = useMemo(() => notificationsRaw.split(","), [notificationsRaw]);
const notifications = useMemo(() => notificationKeys.map(key => NOTIFICATIONS[key]), [notificationKeys]);
const [currentTab, setCurrentTab] = useState<string | null>(null);
const [readNotifications, setReadNotifications] = useState<Array<string>>([]);
const handleTabChange = useCallback((newTabID: string) => {
setReadNotifications(current => [...new Set([
...current,
newTabID
])]);
setCurrentTab(newTabID);
const key = Object.keys(NOTIFICATIONS).find(nKey => NOTIFICATIONS[nKey][0] === newTabID);
if (!key) return;
updateReadNotifications(key).catch(err => {
console.error(err);
getToaster().show({
intent: Intent.DANGER,
message: t("error.generic", { message: localisedErrorMessage(err) }),
timeout: 10000
});
});
}, []);
useEffect(() => {
if (currentTab || notifications.length <= 0) return;
handleTabChange(notifications[0][0]);
}, [currentTab, handleTabChange, notifications]);
return (
<Layout title={t("notifications.title")}>
<Tabs onChange={handleTabChange} selectedTabId={currentTab ?? undefined}>
{notifications.map(([nameKey, Component]) => (
<Tab
key={nameKey}
id={nameKey}
title={(
<span>
<Icon icon={readNotifications.includes(nameKey) ? "notifications-updated" : "notifications"} />
{t(nameKey)}
</span>
)}
panel={<Component />}
/>
))}
</Tabs>
</Layout>
);
}
================================================
FILE: source/full/components/pages/connect/CodeInput.tsx
================================================
import React, { KeyboardEvent, useCallback } from "react";
import { Button, ControlGroup, InputGroup, Intent } from "@blueprintjs/core";
import styled from "styled-components";
import { t } from "../../../../shared/i18n/trans.js";
interface CodeInputProps {
authenticating: boolean;
onChange: (newValue: string) => void;
onSubmit: () => void;
value: string;
}
const Container = styled.div`
width: 100%;
margin-top: 32px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
`;
export function CodeInput(props: CodeInputProps) {
const handleKeyPress = useCallback((event: KeyboardEvent<HTMLInputElement>) => {
if ((event.key === "Enter" || event.keyCode === 13) && !event.ctrlKey && !event.shiftKey) {
props.onSubmit();
}
}, [props.onSubmit]);
return (
<Container>
<ControlGroup>
<InputGroup
autoFocus
disabled={props.authenticating}
leftIcon="key"
large
onChange={evt => props.onChange(evt.target.value)}
onKeyDown={handleKeyPress}
placeholder={t("connect-page.code-plc")}
type="password"
value={props.value}
/>
<Button
icon="arrow-right"
intent={Intent.PRIMARY}
loading={props.authenticating}
onClick={() => props.onSubmit()}
minimal
/>
</ControlGroup>
</Container>
)
}
================================================
FILE: source/full/components/pages/connect/ConnectPage.tsx
================================================
import React, { useCallback, useState } from "react";
import { Intent } from "@blueprintjs/core";
import { Layout } from "../../Layout.js";
import { t } from "../../../../shared/i18n/trans.js";
import { CodeInput } from "./CodeInput.js";
import { sendBackgroundMessage } from "../../../../shared/services/messaging.js";
import { getToaster } from "../../../../shared/services/notifications.js";
import { closeCurrentTab } from "../../../../shared/library/extension.js";
import { localisedErrorMessage } from "../../../../shared/library/error.js";
import { BackgroundMessageType } from "../../../types.js";
export function ConnectPage() {
const [code, setCode] = useState<string>("");
const [authenticating, setAuthenticating] = useState<boolean>(false);
const handleSubmitCode = useCallback(async () => {
if (!code) return;
setAuthenticating(true);
sendBackgroundMessage({ type: BackgroundMessageType.AuthenticateDesktopConnection, code })
.then(() => {
getToaster().show({
intent: Intent.SUCCESS,
message: t("connect-page.auth-success"),
timeout: 3000
});
setTimeout(() => {
closeCurrentTab();
}, 3000);
})
.catch(err => {
console.error(err);
getToaster().show({
intent: Intent.DANGER,
message: t("connect-page.auth-error", { message: localisedErrorMessage(err) }),
timeout: 10000
});
setAuthenticating(false);
});
}, [code]);
return (
<Layout title={t("connect-page.title")}>
<p>{t("connect-page.description")}</p>
<p>{t("connect-page.instruction")}</p>
<CodeInput
authenticating={authenticating}
onChange={setCode}
onSubmit={handleSubmitCode}
value={code}
/>
</Layout>
);
}
================================================
FILE: source/full/components/pages/connect/index.tsx
================================================
import React from "react";
import { ConnectPage as InternalPage } from "./ConnectPage.js";
import { ErrorBoundary } from "../../../../shared/components/ErrorBoundary.jsx";
export function ConnectPage() {
return (
<ErrorBoundary>
<InternalPage />
</ErrorBoundary>
);
}
================================================
FILE: source/full/components/pages/saveCredentials/CredentialsSaver.tsx
================================================
import React, { Fragment, useCallback, useMemo, useState } from "react";
import { ITreeNode, NonIdealState, Tree, TreeNodeInfo } from "@blueprintjs/core";
import { EntryPropertyType, VaultFacade, VaultSourceID, fieldsToProperties } from "buttercup";
import { SiteIcon } from "@buttercup/ui";
import styled from "styled-components";
import { t } from "../../../../shared/i18n/trans.js";
import { useAllVaultsContents } from "../../../hooks/vaultContents.js";
import { BusyLoader } from "../../../../shared/components/loading/BusyLoader.js";
import { ErrorMessage } from "../../../../shared/components/ErrorMessage.js";
import { NewEntrySavePrompt } from "./NewEntrySavePrompt.js";
import { useCapturedCredentials } from "../../../hooks/credentials.js";
import { extractEntryDomain } from "../../../../shared/library/domain.js";
import { SavedCredentials, UsedCredentials, VaultsTree } from "../../../types.js";
interface CredentialsSaverProps {
mode: "existing" | "new";
onSaveNewClick: (credentials: SavedCredentials) => void;
saving: boolean;
selected: string;
}
interface NodeInfo {
id: string;
type: "group" | "entry";
}
const EntryTreeIcon = styled(SiteIcon)`
width: 18px;
height: 18px;
margin-right: 5px;
> img {
width: 100%;
height: 100%;
}
`;
function buildGroupNodes(
sourceID: VaultSourceID,
vault: VaultFacade,
parentGroupID: string | null,
expanded: Array<string>,
selected: Array<string>,
mode: "existing" | "new"
): Array<TreeNodeInfo<NodeInfo>> {
const output = vault.groups.reduce((output: Array<TreeNodeInfo<NodeInfo>>, group) => {
const groupParent = group.parentID === "0" ? null : group.parentID;
if (groupParent !== parentGroupID) return output;
const id = `group:${sourceID}:${group.id}`;
const newNode: TreeNodeInfo<NodeInfo> = {
childNodes: buildGroupNodes(sourceID, vault, group.id, expanded, selected, mode),
hasCaret: countGroupChildren(vault, group.id, mode === "existing") > 0,
icon: expanded.includes(id) ? "folder-open" : "folder-close",
id,
isExpanded: expanded.includes(id),
isSelected: selected.includes(id),
label: group.title,
nodeData: {
id,
type: "group"
}
};
return [
...output,
newNode
];
}, []);
if (mode === "existing") {
// Add entries
output.push(...vault.entries.reduce((output: Array<TreeNodeInfo<NodeInfo>>, entry) => {
if (entry.parentID !== parentGroupID) return output;
const id = `entry:${sourceID}:${parentGroupID}:${entry.id}`;
const titleField = entry.fields.find(field => field.propertyType === EntryPropertyType.Property && field.property === "title");
const title = titleField?.value ?? "(Untitled)";
const domain = extractEntryDomain(fieldsToProperties(entry.fields));
const newNode: TreeNodeInfo<NodeInfo> = {
childNodes: [],
hasCaret: false,
icon: (
<EntryTreeIcon
domain={domain}
type={entry.type}
/>
),
id,
isExpanded: false,
isSelected: selected.includes(id),
label: title,
nodeData: {
id,
type: "entry"
}
};
return [
...output,
newNode
];
}, []))
}
return output;
}
function buildVaultRootNodes(
vaultTree: VaultsTree,
expanded: Array<string>,
selected: Array<string>,
mode: "existing" | "new"
): Array<TreeNodeInfo<NodeInfo>> {
return Object.keys(vaultTree).map(sourceID => ({
childNodes: buildGroupNodes(sourceID, vaultTree[sourceID], null, expanded, selected, mode),
icon: "box",
id: `source:${sourceID}`,
isExpanded: expanded.includes(`source:${sourceID}`),
label: vaultTree[sourceID].name,
nodeData: {
id: `source:${sourceID}`,
type: "group"
}
}));
}
function countGroupChildren(
vault: VaultFacade,
parentGroupID: string | null,
includeEntries: boolean
): number {
let total = vault.groups.reduce((output: number, group) => {
const groupParent = group.parentID === "0" ? null : group.parentID;
return groupParent === parentGroupID ? output + 1 : output;
}, 0);
if (includeEntries) {
total += vault.entries.reduce((output: number, entry) => {
return entry.parentID === parentGroupID ? output + 1 : output;
}, 0);
}
return total;
}
export function CredentialsSaver(props: CredentialsSaverProps) {
const { mode, onSaveNewClick, saving, selected: selectedCredentialsID } = props;
const {
error: contentsError,
loading: contentsLoading,
tree
} = useAllVaultsContents();
const [credentials, credentialsLoading, credentialsError] = useCapturedCredentials();
const [selectedNodes, setSelectedNodes] = useState<Array<string>>([]);
const [expandedNodes, setExpandedNodes] = useState<Array<string>>([]);
const selectedUsedCredentials = useMemo(() => credentials.find(cred => cred?.id === selectedCredentialsID), [credentials, selectedCredentialsID]);
const selectedGroupURI = useMemo(() => {
return selectedNodes.length === 1 && /^group:/.test(selectedNodes[0])
? selectedNodes[0]
: null;
}, [selectedNodes]);
const selectedEntryURI = useMemo(() => {
return selectedNodes.length === 1 && /^entry:/.test(selectedNodes[0])
? selectedNodes[0]
: null;
}, [selectedNodes]);
const contents = useMemo(
() => tree ? buildVaultRootNodes(tree, expandedNodes, selectedNodes, mode) : [],
[expandedNodes, mode, selectedNodes, tree]
);
const handleNodeClick = useCallback((node: TreeNodeInfo<NodeInfo>) => {
if (saving) return;
if (mode === "existing") {
if (node.nodeData?.type === "entry") {
// Only select entries
setSelectedNodes([node.nodeData.id]);
}
} else if (mode === "new" && node.nodeData) {
// Groups only, select immediately
setSelectedNodes([node.nodeData.id]);
}
}, [mode, saving]);
const handleSaveClick = useCallback((credentials: UsedCredentials) => {
if (mode === "new" && selectedGroupURI) {
const [, sourceID, groupID] = selectedGroupURI.split(":");
onSaveNewClick({
...credentials,
groupID,
sourceID
});
} else if (mode === "existing" && selectedEntryURI) {
const [, sourceID, groupID, entryID] = selectedEntryURI.split(":");
onSaveNewClick({
...credentials,
groupID,
sourceID,
entryID
});
}
}, [mode, onSaveNewClick, selectedEntryURI, selectedGroupURI]);
return (
<div>
{contentsError && (
<ErrorMessage message={contentsError.message} scroll={false} />
)}
{credentialsError && (
<ErrorMessage message={credentialsError.message} scroll={false} />
)}
{(contentsLoading || credentialsLoading) && (
<BusyLoader
title={t("save-credentials-page.credentials-saver.create-new.loader.title")}
description={t("save-credentials-page.credentials-saver.create-new.loader.description")}
/>
)}
{tree && (
<Fragment>
{contents.length > 0 && (
<Tree
contents={contents}
onNodeClick={handleNodeClick}
onNodeCollapse={node => setExpandedNodes(
current => current.filter(id => id !== node.nodeData?.id)
)}
onNodeExpand={node => {
if (!node.nodeData) return;
setExpandedNodes(current => [
...current,
(node.nodeData as NodeInfo).id
]);
}}
/>
) || (
<NonIdealState
icon="inbox"
title={t("save-credentials-page.credentials-saver.no-vaults.title")}
description={t("save-credentials-page.credentials-saver.no-vaults.description")}
/>
)}
{mode === "new" && contents.length > 0 && selectedGroupURI && selectedUsedCredentials && (
<NewEntrySavePrompt
credentials={selectedUsedCredentials}
onSaveClick={handleSaveClick}
saving={saving}
/>
)}
{mode === "existing" && contents.length > 0 && selectedEntryURI && selectedUsedCredentials && (
<NewEntrySavePrompt
credentials={selectedUsedCredentials}
onSaveClick={handleSaveClick}
saving={saving}
/>
)}
</Fragment>
)}
</div>
);
}
================================================
FILE: source/full/components/pages/saveCredentials/CredentialsSelector.tsx
================================================
import React, { Fragment, useCallback, useMemo } from "react";
import styled from "styled-components";
import { Card, Elevation } from "@blueprintjs/core";
import { SiteIcon } from "@buttercup/ui";
import { EntryType } from "buttercup";
import { useCapturedCredentials } from "../../../hooks/credentials.js";
import { extractDomain } from "../../../../shared/library/domain.js";
import { ErrorMessage } from "../../../../shared/components/ErrorMessage.js";
import { UsedCredentials } from "../../../types.js";
interface CredentialsSelectorProps {
disabled?: boolean;
onSelect: (id: string) => void;
selected: string | null;
}
const Credential = styled.h5`
margin: 0;
${p => p.monospace ? "font-family: monospace;" : ""}
&:not(:last-child) {
margin-bottom: 4px;
}
`;
const CredentialsCard = styled(Card)`
min-width: 280px;
padding: 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&:not(:last-child) {
margin-right: 8px;
}
`;
const CredentialsHeading = styled.h4`
margin: 0 0 5px 0;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
`;
const HorizontalScroller = styled.div`
width: 100%;
overflow-y: hidden;
overflow-x: scroll;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: stretch;
padding: 12px;
`;
const CredentialsIcon = styled(SiteIcon)`
width: 24px;
height: 24px;
margin-right: 6px;
> img {
width: 100%;
height: 100%;
}
`;
const URL = styled.span`
font-size: 12px;
`;
export function CredentialsSelector(props: CredentialsSelectorProps) {
const { disabled: parentDisabled = false, onSelect, selected } = props;
const [credentialsInitial, loading, error] = useCapturedCredentials();
const credentials = useMemo(() => credentialsInitial.filter(cred => !!cred) as Array<UsedCredentials>, [credentialsInitial]);
const disabled = parentDisabled || loading;
const handleItemClick = useCallback((credential: UsedCredentials) => {
if (disabled) return;
onSelect(credential.id);
}, [disabled, onSelect]);
const credentialDomains = useMemo(
() => credentials.map(cred => extractDomain(cred.url)),
[credentials]
);
return (
<Fragment>
{error && (
<ErrorMessage message={error.message} scroll={false} />
)}
<HorizontalScroller>
{credentials.map((cred, ind) => (
<CredentialsCard
disabled={disabled}
key={cred.id}
interactive={cred.id !== selected}
elevation={cred.id === selected ? Elevation.ZERO : Elevation.THREE}
onClick={() => handleItemClick(cred)}
>
<CredentialsHeading>
<CredentialsIcon
domain={credentialDomains[ind]}
type={EntryType.Website}
/>
<span>{cred.title}</span>
</CredentialsHeading>
<Credential monospace>{cred.username}</Credential>
<Credential monospace><code>*********</code></Credential>
<URL>{cred.url}</URL>
</CredentialsCard>
))}
</HorizontalScroller>
</Fragment>
);
}
================================================
FILE: source/full/components/pages/saveCredentials/NewEntrySavePrompt.tsx
================================================
import React, { Fragment, useCallback, useEffect, useState } from "react";
import { Button, Classes, Colors, FormGroup, InputGroup, Intent } from "@blueprintjs/core";
import { Tooltip2 as Tooltip } from "@blueprintjs/popover2";
import styled from "styled-components";
import { GroupID, VaultSourceID } from "buttercup";
import { t } from "../../../../shared/i18n/trans.js";
import { UsedCredentials } from "../../../types.js";
interface NewEntrySavePromptProps {
credentials: UsedCredentials;
onSaveClick: (credentials: UsedCredentials) => void;
saving: boolean;
}
const Form = styled.div`
width: 100%;
.${Classes.FORM_GROUP} {
justify-content: space-between;
}
.${Classes.LABEL} {
flex: 0 0 auto;
width: 25%;
min-width: 150px;
}
.${Classes.FORM_CONTENT} {
flex: 1 1 auto;
}
`;
const ValidityHelper = styled.span`
color: ${Colors.RED2};
`;
function isValidInput(input: string): boolean {
return input.trim().length > 0;
}
export function NewEntrySavePrompt(props: NewEntrySavePromptProps) {
const { credentials, onSaveClick, saving } = props;
const [title, setTitle] = useState<string>("");
const [username, setUsername] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [url, setURL] = useState<string>("");
const [showPassword, setShowPassword] = useState<boolean>(false);
const [invalidInput, setInvalidInput] = useState<string | null>(null);
useEffect(() => {
setShowPassword(false);
setTitle(credentials.title);
setUsername(credentials.username);
setPassword(credentials.password);
setURL(credentials.url);
}, [credentials]);
const handleSaveClick = useCallback(() => {
if (!isValidInput(title)) {
setInvalidInput("title")
return;
} else if (!isValidInput(username)) {
setInvalidInput("username")
return;
} else if (!isValidInput(password)) {
setInvalidInput("password")
return;
} else if (!isValidInput(url)) {
setInvalidInput("url")
return;
}
onSaveClick({
...credentials,
title,
username,
password,
url
});
}, [credentials, onSaveClick, password, title, url, username]);
return (
<Fragment>
<h4>{t("save-credentials-page.credentials-saver.create-new.heading")}</h4>
<Form>
<FormGroup
disabled={saving}
helperText={invalidInput === "title" && (
<ValidityHelper>
{t("form.invalid.required-non-empty")}
</ValidityHelper>
)}
inline
label={t("save-credentials-page.credentials-saver.create-new.label.title")}
labelFor="entry-title"
labelInfo={t("form.required")}
>
<InputGroup
disabled={saving}
id="entry-title"
onChange={evt => setTitle(evt.target.value)}
placeholder={t("save-credentials-page.credentials-saver.create-new.placeholder.title")}
value={title}
/>
</FormGroup>
<FormGroup
disabled={saving}
helperText={invalidInput === "username" && (
<ValidityHelper>
{t("form.invalid.required-non-empty")}
</ValidityHelper>
)}
inline
label={t("save-credentials-page.credentials-saver.create-new.label.username")}
labelFor="entry-username"
labelInfo={t("form.required")}
>
<InputGroup
disabled={saving}
id="entry-username"
onChange={evt => setUsername(evt.target.value)}
placeholder={t("save-credentials-page.credentials-saver.create-new.placeholder.username")}
value={username}
/>
</FormGroup>
<FormGroup
disabled={saving}
helperText={invalidInput === "password" && (
<ValidityHelper>
{t("form.invalid.required-non-empty")}
</ValidityHelper>
)}
inline
label={t("save-credentials-page.credentials-saver.create-new.label.password")}
labelFor="entry-password"
labelInfo={t("form.required")}
>
<InputGroup
disabled={saving}
id="entry-password"
onChange={evt => setPassword(evt.target.value)}
rightElement={
<Tooltip
content={t(`save-credentials-page.credentials-saver.create-new.password.${showPassword ? "hide" : "show"}`)}
>
<Button
icon={showPassword ? "unlock" : "lock"}
intent={Intent.WARNING}
minimal={true}
onClick={() => setShowPassword(show => !show)}
/>
</Tooltip>
}
type={showPassword ? "text" : "password"}
value={password}
/>
</FormGroup>
<FormGroup
disabled={saving}
helperText={invalidInput === "url" && (
<ValidityHelper>
{t("form.invalid.required-non-empty")}
</ValidityHelper>
)}
inline
label={t("save-credentials-page.credentials-saver.create-new.label.url")}
labelFor="entry-url"
labelInfo={t("form.required")}
>
<InputGroup
disabled={saving}
fill
id="entry-url"
onChange={evt => setURL(evt.target.value)}
placeholder={t("save-credentials-page.credentials-saver.create-new.placeholder.url")}
value={url}
/>
</FormGroup>
</Form>
<Button
loading={saving}
intent={Intent.SUCCESS}
onClick={handleSaveClick}
text={t("save-credentials-page.credentials-saver.create-new.save")}
/>
</Fragment>
);
}
================================================
FILE: source/full/components/pages/saveCredentials/index.tsx
================================================
import React, { Fragment, useCallback, useState } from "react";
import { Intent, Tab, Tabs } from "@blueprintjs/core";
import { Layout } from "../../Layout.js";
import { t } from "../../../../shared/i18n/trans.js";
import { useTitle } from "../../../hooks/document.js";
import { CredentialsSelector } from "./CredentialsSelector.js";
import { CredentialsSaver } from "./CredentialsSaver.js";
import { clearSavedCredentials, saveCredentialsToEntry } from "../../../services/credentials.js";
import { getToaster } from "../../../../shared/services/notifications.js";
import { localisedErrorMessage } from "../../../../shared/library/error.js";
import { closeCurrentTab } from "../../../../shared/library/extension.js";
import { SavedCredentials } from "../../../types.js";
enum TabID {
SaveNew = "save-new",
UpdateExisting = "update-existing"
}
export function SaveCredentialsPage() {
useTitle(t("save-credentials-page.title"));
const [selectedTabID, setSelectedTabID] = useState<TabID>(TabID.SaveNew);
const [selectedID, setSelectedID] = useState<string | null>(null);
const [saving, setSaving] = useState<boolean>(false);
const handleSaveNew = useCallback(async (credentials: SavedCredentials) => {
setSaving(true);
try {
await saveCredentialsToEntry(credentials);
await clearSavedCredentials(credentials.id);
getToaster().show({
intent: Intent.SUCCESS,
message: t("save-credentials-page.save-success", { title: credentials.title }),
timeout: 4000
});
setTimeout(() => {
closeCurrentTab();
}, 4000);
} catch (err) {
console.error(err);
getToaster().show({
intent: Intent.DANGER,
message: t("save-credentials-page.save-error", { message: localisedErrorMessage(err) }),
timeout: 10000
});
}
}, []);
return (
<Layout title={t("save-credentials-page.title")}>
<p>{t("save-credentials-page.description")}</p>
<h3>{t("save-credentials-page.detected-logins.heading")}</h3>
<CredentialsSelector
disabled={saving}
onSelect={setSelectedID}
selected={selectedID}
/>
{selectedID && (
<Fragment>
<h3>{t("save-credentials-page.credentials-saver.heading")}</h3>
<Tabs
onChange={newID => setSelectedTabID(newID as TabID)}
renderActiveTabPanelOnly
selectedTabId={selectedTabID}
>
<Tab
disabled={saving}
id={TabID.SaveNew}
title={t("save-credentials-page.credentials-saver.create-new.tab")}
panel={
<CredentialsSaver
mode="new"
onSaveNewClick={handleSaveNew}
saving={saving}
selected={selectedID}
/>
}
/>
<Tab
disabled={saving}
id={TabID.UpdateExisting}
title={t("save-credentials-page.credentials-saver.update-existing.tab")}
panel={
<CredentialsSaver
mode="existing"
onSaveNewClick={handleSaveNew}
saving={saving}
selected={selectedID}
/>
}
/>
</Tabs>
</Fragment>
)}
</Layout>
);
}
================================================
FILE: source/full/hooks/credentials.ts
================================================
import { useAsync } from "../../shared/hooks/async.js";
import { getCredentials } from "../services/credentials.js";
import { UsedCredentials } from "../types.js";
export function useCapturedCredentials(): [
credentials: Array<UsedCredentials | null>,
loading: boolean,
error: Error | null
] {
const { error, loading, value } = useAsync(getCredentials, []);
return [value || [], loading, error];
}
================================================
FILE: source/full/hooks/disabledDomains.ts
================================================
import { useAsyncWithTimer } from "../../shared/hooks/async.js";
import { getDisabledDomains } from "../services/disabledDomains.js";
const REFRESH_DELAY = 2500;
export function useDisabledDomains(
deps: React.DependencyList = []
): [domains: Array<string>, loading: boolean, error: Error | null] {
const { error, loading, value } = useAsyncWithTimer(getDisabledDomains, REFRESH_DELAY, deps);
return [value || [], loading, error];
}
================================================
FILE: source/full/hooks/document.ts
================================================
import { useEffect } from "react";
const TITLE_SEPARATOR = "⋅";
let __originalTitle: string | null = null;
export function useTitle(title: string) {
useEffect(() => {
if (!__originalTitle) {
__originalTitle = document.title;
}
document.title = `${title} ${TITLE_SEPARATOR} ${__originalTitle}`;
return () => {
if (__originalTitle) {
document.title = __originalTitle;
__originalTitle = null;
}
};
}, []);
}
================================================
FILE: source/full/hooks/vaultContents.ts
================================================
import { useAsync } from "../../shared/hooks/async.js";
import { getVaultsTree } from "../services/vaults.js";
import { VaultsTree } from "../types.js";
function vaultTreesDiffer(existingValue: VaultsTree, newValue: VaultsTree): boolean {
if (existingValue === null || newValue === null) return true;
const existingVaults = Object.keys(existingValue).sort().join(",");
const newVaults = Object.keys(newValue).sort().join(",");
if (existingVaults !== newVaults) return true;
for (const vaultID in existingValue) {
// Compare groups
const existingGroupMap = existingValue[vaultID].groups
.map((group) => `${group.parentID}-${group.id}:${group.title}`)
.sort()
.join(",");
const newGroupMap = newValue[vaultID].groups
.map((group) => `${group.parentID}-${group.id}:${group.title}`)
.sort()
.join(",");
if (existingGroupMap !== newGroupMap) return true;
}
return false;
}
export function useAllVaultsContents(): {
error: Error | null;
loading: boolean;
tree: VaultsTree | null;
} {
const { error, loading, value } = useAsync(getVaultsTree, [], {
clearOnExec: false,
updateInterval: 5000,
valuesDiffer: vaultTreesDiffer
});
return {
error,
loading,
tree: value ?? null
};
}
================================================
FILE: source/full/index.pug
================================================
doctype html
html
head
title Buttercup
meta(charset="utf-8")
link(rel="icon", type="image/png", href=require("../../resources/buttercup-256.png").default)
link(rel="stylesheet" href="./styles/full.sass")
script(defer, src="./applications/full.tsx")
body
div#root
================================================
FILE: source/full/services/credentials.ts
================================================
import { Layerr } from "layerr";
import { EntryType } from "buttercup";
import { sendBackgroundMessage } from "../../shared/services/messaging.js";
import { BackgroundMessageType, SavedCredentials, UsedCredentials } from "../types.js";
export async function clearSavedCredentials(id: string): Promise<void> {
const resp = await sendBackgroundMessage({
credentialsID: id,
type: BackgroundMessageType.ClearSavedCredentials
});
if (resp.error) {
throw new Layerr(resp.error, "Failed clearing saved credentials");
}
}
export async function getCredentials(): Promise<Array<UsedCredentials | null>> {
const resp = await sendBackgroundMessage({
type: BackgroundMessageType.GetSavedCredentials
});
if (resp.error) {
throw new Layerr(resp.error, "Failed fetching saved credentials");
}
return resp.credentials ?? [];
}
export async function saveCredentialsToEntry(credentials: SavedCredentials): Promise<void> {
const { entryID = null } = await sendBackgroundMessage({
sourceID: credentials.sourceID,
groupID: credentials.groupID,
entryID: credentials.entryID ?? undefined,
entryProperties: {
password: credentials.password,
title: credentials.title,
url: credentials.url,
username: credentials.username
},
entryType: EntryType.Website,
type: BackgroundMessageType.SaveCredentialsToVault
});
}
================================================
FILE: source/full/services/disabledDomains.ts
================================================
import { Layerr } from "layerr";
import { sendBackgroundMessage } from "../../shared/services/messaging.js";
import { BackgroundMessageType } from "../types.js";
export async function getDisabledDomains(): Promise<Array<string>> {
const resp = await sendBackgroundMessage({
type: BackgroundMessageType.GetDisabledDomains
});
if (resp.error) {
throw new Layerr(resp.error, "Failed fetching disabled domains");
}
return resp.domains ?? [];
}
export async function removeDisabledDomain(domain: string): Promise<void> {
const resp = await sendBackgroundMessage({
domains: [domain],
type: BackgroundMessageType.DeleteDisabledDomains
});
if (resp.error) {
throw new Layerr(resp.error, "Failed removing disabled domains");
}
}
================================================
FILE: source/full/services/init.ts
================================================
import { initialise as initialiseI18n } from "../../shared/i18n/trans.js";
import { getLanguage } from "../../shared/library/i18n.js";
import { log } from "./log.js";
export async function initialise() {
log("initialising");
await initialiseI18n(getLanguage());
log("initialisation complete");
}
================================================
FILE: source/full/services/log.ts
================================================
import { createLog } from "../../shared/library/log.js";
const LOG_NAME = "buttercup:browser:page";
let __logger: ReturnType<typeof createLog>;
export function log(...args: Array<any>): void {
if (!__logger) {
__logger = createLog(LOG_NAME, true);
}
return __logger(...args);
}
================================================
FILE: source/full/services/notifications.ts
================================================
import { Layerr } from "layerr";
import { sendBackgroundMessage } from "../../shared/services/messaging.js";
import { BackgroundMessageType } from "../types.js";
export async function updateReadNotifications(notificationName: string): Promise<void> {
const resp = await sendBackgroundMessage({
notification: notificationName,
type: BackgroundMessageType.MarkNotificationRead
});
if (resp.error) {
throw new Layerr(resp.error, "Failed updating read notifications");
}
}
================================================
FILE: source/full/services/vaults.ts
================================================
import { Layerr } from "layerr";
import { sendBackgroundMessage } from "../../shared/services/messaging.js";
import { BackgroundMessageType, VaultsTree } from "../types.js";
export async function getVaultsTree(): Promise<VaultsTree> {
const resp = await sendBackgroundMessage({
type: BackgroundMessageType.GetDesktopVaultsTree
});
if (resp.error) {
throw new Layerr(resp.error, "Failed fetching vaults tree");
}
if (!resp.vaultsTree) {
throw new Error("No vaults tree returned");
}
return resp.vaultsTree;
}
================================================
FILE: source/full/styles/full.sass
================================================
html, body
margin: 0
padding: 0
height: 100%
#root
display: flex
justify-content: center
min-height: 100%
height: 100%
@import ../../shared/styles/base
================================================
FILE: source/full/types.ts
================================================
export * from "../shared/types.js";
================================================
FILE: source/popup/applications/popup.tsx
================================================
import React from "react";
import ReactDOM from "react-dom";
import { App } from "../components/App.js";
import { initialise } from "../services/init.js";
initialise()
.then(() => {
ReactDOM.render(
<App />,
document.getElementById("root")
);
})
.catch(err => {
console.error(err);
});
================================================
FILE: source/popup/components/App.tsx
================================================
import React, { useEffect, useState } from "react";
import {
createHashRouter,
RouterProvider,
useLoaderData
} from "react-router-dom";
import { useSingleState } from "react-obstate";
import { Navigator } from "./navigation/Navigator.js";
import { APP_STATE } from "../state/app.js";
import { ThemeProvider } from "../../shared/components/ThemeProvider.js";
import { useBodyClass } from "../hooks/document.js";
import { LaunchContextProvider } from "./contexts/LaunchContext.js";
import { useBodyThemeClass, useTheme } from "../../shared/hooks/theme.js";
import { SaveDialogPage } from "./pages/SaveDialogPage.js";
import { useCurrentTabURL } from "../hooks/tab.js";
import { PopupPage } from "../types.js";
const ROUTER = createHashRouter([
{
path: "/",
element: <ToolbarApp />
},
{
path: "/dialog",
element: <InPageApp />,
loader: ({ request }) => {
const url = new URL(request.url);
const pageURL = url.searchParams.get("page");
const formID = url.searchParams.get("form");
const initialTab = url.searchParams.get("initial");
return { formID, url: pageURL, initialTab };
}
},
{
path: "/save-dialog",
element: <SavePromptApp />,
loader: ({ request }) => {
const url = new URL(request.url);
const loginID = url.searchParams.get("login");
return { loginID };
}
}
]);
export function App() {
const theme = useTheme();
useBodyThemeClass(theme);
return (
<ThemeProvider darkMode={theme === "dark"}>
<RouterProvider router={ROUTER} />
</ThemeProvider>
);
}
function InPageApp() {
const [tab, setTab] = useSingleState(APP_STATE, "tab");
useBodyClass("in-page");
const { formID = "", initialTab, url = null } = useLoaderData() as {
formID?: string;
initialTab: PopupPage,
url: string;
};
useEffect(() => {
setTab(initialTab);
}, [initialTab]);
return (
<LaunchContextProvider source="page" formID={formID || null} url={url}>
<Navigator
activeTab={tab}
onChangeTab={setTab}
tabs={[
PopupPage.Entries,
PopupPage.OTPs
]}
/>
</LaunchContextProvider>
);
}
function SavePromptApp() {
const { loginID = null } = useLoaderData() as {
loginID: string;
};
useBodyClass("in-page");
return (
<LaunchContextProvider source="page" loginID={loginID}>
<SaveDialogPage />
</LaunchContextProvider>
);
}
function ToolbarApp() {
const [tab, setTab] = useSingleState(APP_STATE, "tab");
const [loadingURL, url] = useCurrentTabURL();
const [hasLoadedURL, setHasLoadedURL] = useState<boolean>(false);
useEffect(() => {
if (!loadingURL) {
setHasLoadedURL(true);
}
}, [loadingURL]);
if (!hasLoadedURL) return null;
return (
<LaunchContextProvider source="popup" url={url}>
<Navigator
activeTab={tab}
onChangeTab={setTab}
tabs={[
PopupPage.About,
PopupPage.Entries,
PopupPage.Vaults,
PopupPage.OTPs,
PopupPage.Settings
]}
/>
</LaunchContextProvider>
);
}
================================================
FILE: source/popup/components/contexts/LaunchContext.tsx
================================================
import React, { ReactNode, createContext } from "react";
interface LaunchContextProps {
children: ReactNode;
formID?: string | null;
loginID?: string | null;
source: "popup" | "page";
url?: string | null;
}
interface LaunchContextDefaultValue {
formID: string | null;
loginID: string | null;
source: "popup" | "page";
url: string | null;
}
export const LaunchContext = createContext<LaunchContextDefaultValue>({} as LaunchContextDefaultValue);
LaunchContext.displayName = "LaunchContext";
export function LaunchContextProvider(props: LaunchContextProps) {
return (
<LaunchContext.Provider value={{
formID: props.formID ?? null,
loginID: props.loginID ?? null,
source: props.source,
url: props.url ?? null
}}>
{props.children}
</LaunchContext.Provider>
);
}
================================================
FILE: source/popup/components/entries/EntryInfoDialog.tsx
================================================
import React, { useCallback, useMemo } from "react";
import { Button, Classes, Dialog, DialogBody, InputGroup, Intent } from "@blueprintjs/core";
import { SearchResult } from "buttercup";
import cn from "classnames";
import styled from "styled-components";
import { t } from "../../../shared/i18n/trans.js";
import { copyTextToClipboard } from "../../services/clipboard.js";
import { getToaster } from "../../../shared/services/notifications.js";
import { localisedErrorMessage } from "../../../shared/library/error.js";
interface EntryInfoDialogProps {
entry: SearchResult | null;
onClose: () => void;
}
interface EntryProperty {
key: string;
sensitive: boolean;
title: string;
value: string;
}
const InfoDialog = styled(Dialog)`
max-width: 90%;
`;
const InfoDialogBody = styled(DialogBody)`
display: flex;
flex-direction: row;
justify-content: stretch;
align-items: flex-start;
overflow-x: hidden;
`;
const InfoTable = styled.table`
table-layout: fixed;
width: 100%;
`;
export function EntryInfoDialog(props: EntryInfoDialogProps) {
const { entry, onClose } = props;
const properties = useMemo(() => entry ? orderProperties(entry.properties) : [], [entry]);
const handleCopyClick = useCallback(async (property: string, value: string) => {
try {
await copyTextToClipboard(value);
getToaster().show({
intent: Intent.SUCCESS,
message: t("popup.entries.info.copy-success", { property }),
timeout: 4000
});
} catch (err) {
getToaster().show({
intent: Intent.DANGER,
message: t("popup.entries.info.copy-error", { message: localisedErrorMessage(err) }),
timeout: 10000
});
}
}, []);
return (
<InfoDialog
icon="info-sign"
isCloseButtonShown
isOpen={!!entry}
onClose={onClose}
title={entry?.properties.title ?? "Untitled Entry"}
>
<InfoDialogBody>
<InfoTable className={cn(Classes.HTML_TABLE, Classes.COMPACT, Classes.HTML_TABLE_STRIPED)}>
<tbody>
{properties.map(property => (
<tr key={property.key}>
<td style={{ width: "100%" }}>
{property.title}<br />
<InputGroup
type={property.sensitive ? "password" : "text"}
value={property.value}
readOnly
rightElement={
<Button
icon="clipboard"
minimal
onClick={() => handleCopyClick(property.title, property.value)}
title={t("popup.entries.info.copy-tooltip")}
/>
}
/>
</td>
</tr>
))}
</tbody>
</InfoTable>
</InfoDialogBody>
</InfoDialog>
);
}
function orderProperties(properties: Record<string, string>): Array<EntryProperty> {
const working = { ...properties };
delete working["title"];
const output: Array<EntryProperty> = [];
if (working["username"]) {
output.push({
key: "username",
sensitive: false,
title: "Username",
value: properties["username"]
});
delete working["username"];
}
if (working["password"]) {
output.push({
key: "password",
sensitive: true,
title: "Password",
value: properties["password"]
});
delete working["password"];
}
for (const prop in working) {
output.push({
key: prop,
sensitive: false,
title: prop,
value: working[prop]
});
}
return output;
}
================================================
FILE: source/popup/components/entries/EntryItem.tsx
================================================
import React, { MouseEvent, useCallback, useContext, useMemo } from "react";
import styled from "styled-components";
import cn from "classnames";
import { Button, ButtonGroup, Classes, Text } from "@blueprintjs/core";
import { SearchResult, VaultSourceStatus } from "buttercup";
import { SiteIcon } from "@buttercup/ui";
import { LaunchContext } from "../contexts/LaunchContext.js";
import { extractEntryDomain } from "../../../shared/library/domain.js";
import { Tooltip2 } from "@blueprintjs/popover2";
import { t } from "../../../shared/i18n/trans.js";
interface EntryItemProps {
entry: SearchResult;
fetchIcons: boolean;
onAutoClick: () => void;
onClick: () => void;
onInfoClick: () => void;
}
const CenteredText = styled(Text)`
display: flex;
align-items: center;
`;
const Container = styled.div`
border-radius: 3px;
padding: 0.5rem;
background-color: ${p => (p.isActive ? p.theme.listItemHover : null)};
position: relative;
&:hover {
background-color: ${p => p.theme.listItemHover};
}
`;
const DetailRow = styled.div`
margin-left: 0.5rem;
overflow: hidden;
flex: 1;
`;
const EntryIcon = styled(SiteIcon)`
width: 100%;
height: 100%;
> img {
width: 100%;
height: 100%;
}
`;
const Title = styled(Text)`
margin-bottom: 0.3rem;
`;
const EntryIconBackground = styled.div`
width: 2.5rem;
height: 2.5rem;
flex: 0 0 auto;
background-color: ${p => p.theme.backgroundColor};
border-radius: 3px;
border: 1px solid ${p => p.theme.listItemHover};
`;
const EntryRow = styled.div`
flex: 1;
width: 100%;
display: flex;
cursor: pointer;
align-items: center;
`;
export function EntryItem(props: EntryItemProps) {
const {
entry,
fetchIcons,
onAutoClick,
onClick,
onInfoClick
} = props;
const { source: popupSource } = useContext(LaunchContext);
const entryDomain = useMemo(() => {
if (!fetchIcons) {
return null;
}
return extractEntryDomain(entry.properties);
}, [entry, fetchIcons]);
const handleEntryClick = useCallback(
(evt: MouseEvent) => {
evt.preventDefault();
evt.stopPropagation();
onClick();
},
[onClick]
);
const handleEntryLoginClick = useCallback(
(evt: MouseEvent) => {
evt.preventDefault();
evt.stopPropagation();
onAutoClick();
},
[onAutoClick]
);
const handleEntryInfoClick = useCallback((evt: MouseEvent) => {
evt.preventDefault();
evt.stopPropagation();
onInfoClick();
}, [onInfoClick]);
return (
<Container isActive={false} onClick={handleEntryClick}>
<EntryRow>
<EntryIconBackground>
<EntryIcon
domain={entryDomain}
type={entry.entryType}
/>
</EntryIconBackground>
<DetailRow>
<Title title={entry.properties.title}>
<Text ellipsize>{entry.properties.title}</Text>
</Title>
<CenteredText ellipsize className={cn(Classes.TEXT_SMALL, Classes.TEXT_MUTED)}>
{entry.properties.username} {entry.properties.url && `@ ${entry.properties.url}` || ""}
</CenteredText>
</DetailRow>
{popupSource === "popup" && (
<ButtonGroup>
<Tooltip2
content={t("popup.entries.auto-login.tooltip")}
>
<Button
icon="text-highlight"
minimal
onClick={handleEntryLoginClick}
/>
</Tooltip2>
<Tooltip2
content={t("popup.entries.info.tooltip")}
>
<Button
icon="info-sign"
minimal
onClick={handleEntryInfoClick}
/>
</Tooltip2>
</ButtonGroup>
)}
</EntryRow>
</Container>
);
}
================================================
FILE: source/popup/components/entries/EntryItemList.tsx
================================================
import React, { Fragment } from "react";
import styled from "styled-components";
import { SearchResult } from "buttercup";
import { Divider, H4} from "@blueprintjs/core";
import { EntryItem } from "./EntryItem.js";
import { useConfig } from "../../../shared/hooks/config.js";
import { t } from "../../../shared/i18n/trans.js";
interface EntryItemListProps {
entries: Array<SearchResult> | Record<string, Array<SearchResult>>;
onEntryAutoClick: (entry: SearchResult) => void;
onEntryClick: (entry: SearchResult) => void;
onEntryInfoClick: (entry: SearchResult) => void;
}
const ScrollList = styled.div`
max-height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
`;
export function EntryItemList(props: EntryItemListProps) {
const { entries, onEntryAutoClick, onEntryClick, onEntryInfoClick } = props;
const [config] = useConfig();
if (!config) return null;
return (
<ScrollList>
{Array.isArray(entries) && (
<>
{entries.map((entry) => (
<Fragment key={entry.id}>
<EntryItem
entry={entry}
fetchIcons={config.entryIcons}
onAutoClick={() => onEntryAutoClick(entry)}
onClick={() => onEntryClick(entry)}
onInfoClick={() => onEntryInfoClick(entry)}
/>
<Divider />
</Fragment>
))}
</>
) || (
<>
{Object.keys(entries).map(sectionName => (
<Fragment key={sectionName}>
{entries[sectionName].length > 0 && (
<Fragment key={`en-${sectionName}`}>
<H4>{t(sectionName)}</H4>
{entries[sectionName].map((entry: SearchResult) => (
<Fragment key={entry.id}>
<EntryItem
entry={entry}
fetchIcons={config.entryIcons}
onAutoClick={() => onEntryAutoClick(entry)}
onClick={() => onEntryClick(entry)}
onInfoClick={() => onEntryInfoClick(entry)}
/>
<Divider />
</Fragment>
))}
</Fragment>
)}
</Fragment>
))}
</>
)}
</ScrollList>
);
}
================================================
FILE: source/popup/components/navigation/Navigator.tsx
================================================
import React, { useCallback, useState } from "react";
import styled from "styled-components";
import { Classes, Divider, Icon, Intent, Tab, Tabs } from "@blueprintjs/core";
import { VaultsPage, VaultsPageControls } from "../pages/VaultsPage.js";
import { EntriesPage, EntriesPageControls } from "../pages/EntriesPage.js";
import { getToaster } from "../../../shared/services/notifications.js";
import { localisedErrorMessage } from "../../../shared/library/error.js";
import { t } from "../../../shared/i18n/trans.js";
import { clearDesktopConnectionAuth, initiateDesktopConnectionRequest } from "../../queries/desktop.js";
import { OTPsPage } from "../pages/OTPsPage.js";
import { SettingsPage } from "../pages/SettingsPage.js";
import { AboutPage } from "../pages/AboutPage.js";
import { PopupPage } from "../../types.js";
import BUTTERCUP_LOGO from "../../../../resources/buttercup-128.png";
interface NavigatorProps {
activeTab: PopupPage;
onChangeTab: (tab: PopupPage) => void;
tabs: Array<PopupPage>;
}
const ButtercupIconImg = styled.img`
width: 20px;
height: 20px;
`;
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: stretch;
padding: 3px;
overflow: hidden;
max-height: 100%;
.${Classes.TAB} {
outline: none;
user-select: none;
}
.${Classes.TABS} {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: stretch;
}
.${Classes.TAB_PANEL} {
margin-top: 8px;
overflow-x: hidden;
overflow-y: scroll;
}
.${Classes.TAB_LIST} {
padding: 0px 5px;
height: 30px;
}
`;
export function Navigator(props: NavigatorProps) {
const [entriesSearch, setEntriesSearch] = useState<string>("");
const handleConnectClick = useCallback(async () => {
try {
await initiateDesktopConnectionRequest();
} catch (err) {
console.error(err);
getToaster().show({
intent: Intent.DANGER,
message: t("popup.connection.open-error", { message: localisedErrorMessage(err) }),
timeout: 10000
});
}
}, []);
const handleReconnectClick = useCallback(async () => {
try {
await clearDesktopConnectionAuth();
} catch (err) {
console.error(err);
getToaster().show({
intent: Intent.DANGER,
message: t("popup.connection.reauth-error", { message: localisedErrorMessage(err) }),
timeout: 10000
});
return;
}
await handleConnectClick();
}, [handleConnectClick]);
return (
<Container>
<Tabs
onChange={(newTab: PopupPage) => props.onChangeTab(newTab)}
selectedTabId={props.activeTab}
>
{props.tabs.map((tabType: PopupPage, ind: number) => (
(tabType === PopupPage.Entries && (
<Tab
key={`tab-${ind}-${tabType}`}
id={PopupPage.Entries}
panel={(
<>
<Divider />
<EntriesPage
onConnectClick={handleConnectClick}
onReconnectClick={handleReconnectClick}
searchTerm={entriesSearch}
/>
</>
)}
>
<Icon icon="label" title={t("popup.tab.entries.title")} />
</Tab>
)) ||
(tabType === PopupPage.Vaults && (
<Tab
key={`tab-${ind}-${tabType}`}
id={PopupPage.Vaults}
panel={(
<>
<Divider />
<VaultsPage
onConnectClick={handleConnectClick}
onReconnectClick={handleReconnectClick}
/>
</>
)}
>
<Icon icon="projects" title={t("popup.tab.vaults.title")} />
</Tab>
)) ||
(tabType === PopupPage.OTPs && (
<Tab
key={`tab-${ind}-${tabType}`}
id={PopupPage.OTPs}
panel={(
<>
<Divider />
<OTPsPage
onConnectClick={handleConnectClick}
onReconnectClick={handleReconnectClick}
/>
</>
)}
>
<Icon icon="array-timestamp" title={t("popup.tab.otps.title")} />
</Tab>
)) ||
(tabType === PopupPage.Settings && (
<Tab
key={`tab-${ind}-${tabType}`}
id={PopupPage.Settings}
panel={(
<>
<Divider />
<SettingsPage />
</>
)}
>
<Icon icon="cog" title={t("popup.tab.settings.title")} />
</Tab>
)) ||
(tabType === PopupPage.About && (
<Tab
key={`tab-${ind}-${tabType}`}
id={PopupPage.About}
panel={(
<>
<Divider />
<AboutPage />
</>
)}
>
<ButtercupIconImg src={BUTTERCUP_LOGO} alt={t("popup.tab.about.title")} />
</Tab>
))
))}
<Tabs.Expander />
{props.activeTab === PopupPage.Entries && (
<EntriesPageControls
onSearchTermChange={setEntriesSearch}
searchTerm={entriesSearch}
/>
)}
{props.activeTab === PopupPage.Vaults && (
<VaultsPageControls />
)}
</Tabs>
</Container>
);
}
================================================
FILE: source/popup/components/otps/OTPItem.tsx
================================================
import React, { useCallback, useMemo } from "react";
import styled from "styled-components";
import cn from "classnames";
import { Classes, Intent, Spinner, Text } from "@blueprintjs/core";
import { SiteIcon } from "@buttercup/ui";
import { extractDomain } from "../../../shared/library/domain.js";
import { PreparedOTP } from "../../hooks/otp.js";
interface OTPItemProps {
otp: PreparedOTP;
onClick: () => void;
}
const CenteredText = styled(Text)`
display: flex;
align-items: center;
`;
const Container = styled.div`
border-radius: 3px;
padding: 0.5rem;
background-color: ${p => (p.isActive ? p.theme.listItemHover : null)};
position: relative;
&:hover {
background-color: ${p => p.theme.listItemHover};
}
`;
const DetailRow = styled.div`
margin-left: 0.5rem;
overflow: hidden;
flex: 1;
`;
const EntryIcon = styled(SiteIcon)`
width: 100%;
height: 100%;
> img {
width: 100%;
height: 100%;
}
`;
const Title = styled(Text)`
margin-bottom: 0.3rem;
`;
const OTPIconBackground = styled.div`
width: 2.5rem;
height: 2.5rem;
flex: 0 0 auto;
background-color: ${p => p.theme.backgroundColor};
border-radius: 3px;
border: 1px solid ${p => p.theme.listItemHover};
`;
const OTPCode = styled.div`
flex: 0 0 auto;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding-left: 3px;
.${Classes.SPINNER} {
margin-right: 4px;
}
`;
const OTPCodePart = styled.div`
font-family: monospace;
font-size: 22px;
margin-right: 3px;
`;
const OTPRow = styled.div`
flex: 1;
width: 100%;
display: flex;
cursor: pointer;
align-items: center;
`;
export function OTPItem(props: OTPItemProps) {
const {
otp,
onClick
} = props;
const entryDomain = useMemo(() => otp.loginURL ? extractDomain(otp.loginURL) : null, [otp]);
const handleOTPClick = useCallback(() => {
onClick();
}, [onClick]);
const [codeFirst, codeSecond] = useMemo(() => {
if (otp.errored) return [otp.digits, ""];
return otp.digits.length === 8
? [otp.digits.substring(0, 4), otp.digits.substring(4)]
: [otp.digits.substring(0, 3), otp.digits.substring(3)]
}, [otp.digits]);
const spinnerLeft = useMemo(() => {
if (otp.errored) return 1;
return otp.remaining / otp.period;
}, [otp]);
const spinnerIntent = useMemo(() => {
if (otp.errored) return Intent.DANGER;
return spinnerLeft < 0.15 ? Intent.DANGER : spinnerLeft < 0.35 ? Intent.WARNING : Intent.SUCCESS;
}, [otp.errored, spinnerLeft]);
return (
<Container isActive={false} onClick={handleOTPClick}>
<OTPRow>
<OTPIconBackground>
<EntryIcon
domain={entryDomain}
/>
</OTPIconBackground>
<DetailRow onClick={() => {}}>
<Title title={otp.otpTitle ?? ""}>
<Text ellipsize>{otp.otpTitle ?? "?"}</Text>
</Title>
<CenteredText ellipsize className={cn(Classes.TEXT_SMALL, Classes.TEXT_MUTED)}>
{otp.entryTitle}
</CenteredText>
</DetailRow>
<OTPCode>
<Spinner
size={19}
value={spinnerLeft}
intent={spinnerIntent}
/>
<OTPCodePart>{codeFirst}</OTPCodePart>
<OTPCodePart>{codeSecond}</OTPCodePart>
</OTPCode>
</OTPRow>
</Container>
);
}
================================================
FILE: source/popup/components/otps/OTPItemList.tsx
================================================
import React, { Fragment } from "react";
import styled from "styled-components";
import { Divider } from "@blueprintjs/core";
import { OTPItem } from "./OTPItem.js";
import { PreparedOTP } from "../../hooks/otp.js";
import { OTP } from "../../types.js";
interface OTPItemListProps {
onOTPClick: (otp: OTP) => void;
otps: Array<PreparedOTP>;
}
const ButtonRow = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: flex-start;
> button:not(:last-child) {
margin-right: 6px;
}
`;
const ScrollList = styled.div`
max-height: 100%;
// overflow-x: hidden;
// overflow-y: scroll;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
`;
export function OTPItemList(props: OTPItemListProps) {
const {
onOTPClick,
otps
} = props;
return (
<>
<ScrollList>
{otps.map((otp) => (
<Fragment key={`${otp.entryID}:${otp.otpURL}`}>
<OTPItem
otp={otp}
onClick={() => onOTPClick(otp)}
/>
<Divider />
</Fragment>
))}
</ScrollList>
</>
);
}
================================================
FILE: source/popup/components/pages/AboutPage.tsx
================================================
import React, { MouseEvent, useCallback } from "react";
import styled from "styled-components";
import { Button, Callout, Classes, Intent } from "@blueprintjs/core";
import cn from "classnames";
import { t } from "../../../shared/i18n/trans.js";
import { BUILD_DATE, VERSION } from "../../../shared/library/version.js";
import { createNewTab, getExtensionURL } from "../../../shared/library/extension.js";
import { localisedErrorMessage } from "../../../shared/library/error.js";
import { getToaster } from "../../../shared/services/notifications.js";
import BUTTERCUP_LOGO from "../../../../resources/buttercup-256.png";
interface AboutPageProps {}
const AboutSection = styled(Callout)`
margin: 0px 12px;
width: calc(100% - 24px);
padding: 9px;
`;
const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
> .${Classes.CALLOUT}:not(:last-child) {
margin-bottom: 8px;
}
`;
const FooterSection = styled.div`
width: 100%;
padding: 8px 12px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
`;
const Heading = styled.h3`
margin-top: 3px;
margin-bottom: 5px;
`;
const HeadingLogo = styled.img`
width: 32px;
height: auto;
`;
const HeadingSection = styled.div`
width: 100%;
padding: 8px 12px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
`;
const InfoTable = styled.table`
width: 100%;
`;
export function AboutPage(_: AboutPageProps) {
const handleAttributionsClick = useCallback(async (event: MouseEvent) => {
try {
await createNewTab(getExtensionURL("full.html#/attributions"));
} catch (err) {
console.error(err);
getToaster().show({
intent: Intent.DANGER,
message: t("error.generic", { message: localisedErrorMessage(err) }),
timeout: 10000
});
}
}, []);
return (
<Container>
<HeadingSection>
<HeadingLogo src={BUTTERCUP_LOGO} alt="Buttercup logo" />
<Heading>Buttercup Password Manager</Heading>
</HeadingSection>
<AboutSection>
<InfoTable className={cn(Classes.HTML_TABLE, Classes.COMPACT)}>
<thead>
<tr>
<th>{t("about.info.title")}</th>
<th> </th>
</tr>
</thead>
<tbody>
<tr>
<td>{t("about.info.version")}</td>
<td>{VERSION}</td>
</tr>
<tr>
<td>{t("about.info.build-date")}</td>
<td>{BUILD_DATE}</td>
</tr>
</tbody>
</InfoTable>
</AboutSection>
<FooterSection>
<Button
onClick={handleAttributionsClick}
>
{t("about.attributions")}
</Button>
</FooterSection>
</Container>
);
}
================================================
FILE: source/popup/components/pages/EntriesPage.tsx
================================================
import React, { useCallback, useContext, useMemo, useState } from "react";
import styled from "styled-components";
import { Button, InputGroup, Intent, NonIdealState, Spinner } from "@blueprintjs/core";
import { SearchResult, VaultSourceStatus } from "buttercup";
import { t } from "../../../shared/i18n/trans.js";
import { useDesktopConnectionState, useEntriesForURL, useRecentEntries, useSearchedEntries, useVaultSources } from "../../hooks/desktop.js";
import { EntryItemList } from "../entries/EntryItemList.js";
import { LaunchContext } from "../contexts/LaunchContext.js";
import { sendEntryResultToTabForInput } from "../../services/tab.js";
import { trackEntryRecentUse } from "../../services/recents.js";
import { getToaster } from "../../../shared/services/notifications.js";
import { localisedErrorMessage } from "../../../shared/library/error.js";
import { DesktopConnectionState } from "../../types.js";
import { openPageForEntry } from "../../services/entry.js";
import { EntryInfoDialog } from "../entries/EntryInfoDialog.js";
interface EntriesPageProps {
onConnectClick: () => Promise<void>;
onReconnectClick: () => Promise<void>;
searchTerm: string;
}
interface EntriesPageControlsProps {
onSearchTermChange: (term: string) => void;
searchTerm: string;
}
const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
`;
const Input = styled(InputGroup)`
margin-right: 2px !important;
`;
const InvalidState = styled(NonIdealState)`
margin-top: 28px;
`;
export function EntriesPage(props: EntriesPageProps) {
const desktopState = useDesktopConnectionState();
return (
<Container>
{desktopState === DesktopConnectionState.NotConnected && (
<InvalidState
title={t("popup.vaults.no-connection.title")}
description={t("popup.vaults.no-connection.description")}
icon="offline"
action={(
<Button
icon="link"
onClick={props.onConnectClick}
text={t("popup.vaults.no-connection.action-text")}
/>
)}
/>
)}
{desktopState === DesktopConnectionState.Connected && (
<EntriesPageList {...props} />
)}
{desktopState === DesktopConnectionState.Pending && (
<Spinner size={40} />
)}
{desktopState === DesktopConnectionState.Error && (
<InvalidState
title={t("popup.connection.check-error.title")}
description={t("popup.connection.check-error.description")}
icon="error"
intent={Intent.DANGER}
action={(
<Button
icon="link"
onClick={props.onReconnectClick}
text={t("popup.vaults.no-connection.action-text")}
/>
)}
/>
)}
</Container>
);
}
function EntriesPageList(props: EntriesPageProps) {
const sources = useVaultSources();
const unlockedCount = useMemo(
() => sources.reduce(
(count, source) => source.state === VaultSourceStatus.Unlocked ? count + 1 : count,
0
),
[sources]
);
const searchedEntries = useSearchedEntries(props.searchTerm);
const { formID, source: popupSource, url } = useContext(LaunchContext);
const [selectedEntryInfo, setSelectedEntryInfo] = useState<SearchResult | null>(null);
const urlEntries = useEntriesForURL(url);
const recentEntries = useRecentEntries();
const handleEntryClick = useCallback((entry: SearchResult, autoLogin: boolean) => {
if (popupSource === "page" && formID) {
sendEntryResultToTabForInput(formID, entry);
} else if (popupSource === "popup") {
openPageForEntry(entry, autoLogin)
.then(opened => {
if (!opened) {
getToaster().show({
intent: Intent.PRIMARY,
message: t("popup.entries.click.no-url-available"),
timeout: 3000
});
}
})
.catch(err => {
console.error(err);
getToaster().show({
intent: Intent.DANGER,
message: t("popup.entries.click.open-error", { message: localisedErrorMessage(err) }),
timeout: 10000
});
});
}
trackEntryRecentUse(entry).catch(err => {
console.error(err);
getToaster().show({
intent: Intent.DANGER,
message: t("popup.entries.click.recent-set-error", { message: localisedErrorMessage(err) }),
timeout: 10000
});
});
}, [popupSource]);
const handleEntryAutoLoginClick = useCallback((entry: SearchResult) => {
handleEntryClick(entry, true);
}, [handleEntryClick]);
const handleEntryBodyClick = useCallback((entry: SearchResult) => {
handleEntryClick(entry, false);
}, [handleEntryClick]);
const handleEntryInfoClick = useCallback((entry: SearchResult) => {
setSelectedEntryInfo(entry);
}, []);
// Render
return (
<>
{unlockedCount === 0 && (
<InvalidState
title={t("popup.all-locked.title")}
description={t("popup.all-locked.description")}
icon="folder-close"
/>
) || searchedEntries.length > 0 && (
<EntryItemList
entries={searchedEntries}
onEntryAutoClick={handleEntryAutoLoginClick}
onEntryClick={handleEntryBodyClick}
onEntryInfoClick={handleEntryInfoClick}
/>
) || (urlEntries.length <= 0 && recentEntries.length <= 0) && (
<InvalidState
title={t("popup.no-entries.title")}
description={t("popup.no-entries.description")}
icon="clean"
/>
) || (
<EntryItemList
entries={{
"URL Entries": urlEntries,
"Recents": recentEntries
}}
onEntryAutoClick={handleEntryAutoLoginClick}
onEntryClick={handleEntryBodyClick}
onEntryInfoClick={handleEntryInfoClick}
/>
)}
<EntryInfoDialog entry={selectedEntryInfo} onClose={() => setSelectedEntryInfo(null)} />
</>
);
}
export function EntriesPageControls(props: EntriesPageControlsProps) {
const desktopState = useDesktopConnectionState();
return (
<>
<Input
disabled={desktopState !== DesktopConnectionState.Connected}
onChange={evt => props.onSearchTermChange(evt.target.value)}
placeholder={t("popup.entries.search.placeholder")}
round
value={props.searchTerm}
/>
<Button
disabled={desktopState !== DesktopConnectionState.Connected}
icon="search"
minimal
title={t("popup.entries.search.button")}
/>
</>
);
}
================================================
FILE: source/popup/components/pages/OTPsPage.tsx
================================================
import React, { useCallback, useContext, useMemo } from "react";
import styled from "styled-components";
import { Button, Intent, NonIdealState, Spinner } from "@blueprintjs/core";
import { VaultSourceStatus } from "buttercup";
import { t } from "../../../shared/i18n/trans.js";
import { useDesktopConnectionState, useOTPs, useVaultSources } from "../../hooks/desktop.js";
import { OTPItemList } from "../otps/OTPItemList.js";
import { LaunchContext } from "../contexts/LaunchContext.js";
import { sendOTPToTabForInput } from "../../services/tab.js";
import { usePreparedOTPs } from "../../hooks/otp.js";
import { DesktopConnectionState, OTP } from "../../types.js";
import { getToaster } from "../../../shared/services/notifications.js";
import { createNewTab } from "../../../shared/library/extension.js";
import { formatURL } from "../../../shared/library/url.js";
import { localisedErrorMessage } from "../../../shared/library/error.js";
interface OTPsPageProps {
onConnectClick: () => Promise<void>;
onReconnectClick: () => Promise<void>;
}
interface OTPsPageControlsProps {}
const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
`;
const InvalidState = styled(NonIdealState)`
margin-top: 28px;
`;
export function OTPsPage(props: OTPsPageProps) {
const desktopState = useDesktopConnectionState();
return (
<Container>
{desktopState === DesktopConnectionState.NotConnected && (
<InvalidState
title={t("popup.vaults.no-connection.title")}
description={t("popup.vaults.no-connection.description")}
icon="offline"
action={(
<Button
icon="link"
onClick={props.onConnectClick}
text={t("popup.vaults.no-connection.action-text")}
/>
)}
/>
)}
{desktopState === DesktopConnectionState.Connected && (
<OTPsPageList {...props} />
)}
{desktopState === DesktopConnectionState.Pending && (
<Spinner size={40} />
)}
{desktopState === DesktopConnectionState.Error && (
<InvalidState
title={t("popup.connection.check-error.title")}
description={t("popup.connection.check-error.description")}
icon="error"
intent={Intent.DANGER}
action={(
<Button
icon="link"
onClick={props.onReconnectClick}
text={t("popup.vaults.no-connection.action-text")}
/>
)}
/>
)}
</Container>
);
}
function OTPsPageList(props: OTPsPageProps) {
const { formID, source: popupSource } = useContext(LaunchContext);
const sources = useVaultSources();
const unlockedCount = useMemo(
() => sources.reduce(
(count, source) => source.state === VaultSourceStatus.Unlocked ? count + 1 : count,
0
),
[sources]
);
const [otps, loadingOTPs] = useOTPs();
const preparedOTPs = usePreparedOTPs(otps);
const handleOTPClick = useCallback((otp: OTP) => {
if (popupSource === "page" && formID) {
sendOTPToTabForInput(formID, otp);
} else if (popupSource === "popup") {
if (!otp.loginURL) {
getToaster().show({
intent: Intent.PRIMARY,
message: t("popup.otps.click.no-url-available"),
timeout: 3000
});
return;
}
createNewTab(formatURL(otp.loginURL))
.catch(err => {
console.error(err);
getToaster().show({
intent: Intent.DANGER,
message: t("popup.otps.click.open-error", { message: localisedErrorMessage(err) }),
timeout: 10000
});
});
}
}, [popupSource]);
if (loadingOTPs || (unlockedCount === 0 && otps.length > 0)) {
return (
<Spinner size={40} />
);
}
if (unlockedCount === 0) {
return (
<InvalidState
title={t("popup.all-locked.title")}
description={t("popup.all-locked.description")}
icon="folder-close"
/>
);
} else if (preparedOTPs.length <= 0) {
return (
<InvalidState
title={t("popup.no-otps.title")}
description={t("popup.no-otps.description")}
icon="array-numeric"
/>
);
}
return (
<OTPItemList
onOTPClick={handleOTPClick}
otps={preparedOTPs}
/>
);
}
export function OTPsPageControls(props: OTPsPageControlsProps) {
return (
<>
</>
);
}
================================================
FILE: source/popup/components/pages/SaveDialogPage.tsx
================================================
import React, { useCallback, useContext, useEffect, useRef, useState } from "react";
import styled from "styled-components";
import { EntryType } from "buttercup";
import { SiteIcon } from "@buttercup/ui";
import { Button, Card, H5, Intent, NonIdealState, Spinner } from "@blueprintjs/core";
import { LaunchContext } from "../contexts/LaunchContext.js";
import { t } from "../../../shared/i18n/trans.js";
import { useLoginCredentials } from "../../hooks/credentials.js";
import { getToaster } from "../../../shared/services/notifications.js";
import { clearSavedLoginPrompt } from "../../queries/loginMemory.js";
import { localisedErrorMessage } from "../../../shared/library/error.js";
import { extractDomain } from "../../../shared/library/domain.js";
import { BackgroundMessageType } from "../../types.js";
import { disableDomainForLogin } from "../../queries/disabledDomains.js";
import { sendBackgroundMessage } from "../../../shared/services/messaging.js";
const Buttons = styled.div`
width: 100%;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
flex: 0 0 auto;
> button:not(:last-child) {
margin-right: 6px;
}
`;
const Container = styled.div`
width: 100%;
height: 100%;
overflow: hidden;
padding: 10px;
display: flex;
flex-direction: column;
`;
const CredentialsCard = styled(Card)`
min-width: 280px;
padding: 10px;
margin-right: 0px !important;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&:not(:last-child) {
margin-right: 8px;
}
`;
const CredentialsHeading = styled.h5`
margin: 0 0 5px 0;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
`;
const CredentialsIcon = styled(SiteIcon)`
width: 24px;
height: 24px;
margin-right: 6px;
> img {
width: 100%;
height: 100%;
}
`;
const Heading = styled(H5)`
flex: 0 0 auto;
`;
const Scroller = styled.div`
width: 100%;
overflow-x: hidden;
overflow-y: scroll;
margin-bottom: 8px;
flex: 1 1 auto;
`;
const SpinnerContainer = styled.div`
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex: 1 1 auto;
`;
const URL = styled.span`
font-size: 12px;
`;
export function SaveDialogPage() {
const { loginID } = useContext(LaunchContext);
const credentials = useLoginCredentials(loginID);
const errorShownRef = useRef<boolean>(false);
const [disableConfirm, setDisableConfirm] = useState<boolean>(false);
const handleViewClick = useCallback(async () => {
try {
// Open save page and close dialog
await sendBackgroundMessage({
type: BackgroundMessageType.OpenSaveCredentialsPage
});
} catch (err) {
console.error(err);
getToaster().show({
intent: Intent.DANGER,
message: t("error.generic", { message: localisedErrorMessage(err) }),
timeout: 10000
});
}
}, [loginID]);
const handleCloseClick = useCallback(async () => {
if (!loginID) return;
try {
// Clear prompt and close dialog
await clearSavedLoginPrompt(loginID);
} catch (err) {
console.error(err);
getToaster().show({
intent: Intent.DANGER,
message: t("error.generic", { message: localisedErrorMessage(err) }),
timeout: 10000
});
}
}, [loginID]);
const handleDisableClick = useCallback(async () => {
if (!loginID) return;
if (!disableConfirm) {
setDisableConfirm(true);
return;
}
try {
// Disable domain and close
gitextract_eo89ckcu/ ├── .editorconfig ├── .github/ │ └── workflows/ │ └── test.yml ├── .gitignore ├── .prettierrc ├── .vscode/ │ └── settings.json ├── CHANGELOG.md ├── LICENSE ├── PRIVACY_POLICY.md ├── README.md ├── package.json ├── resources/ │ ├── full.pug │ ├── manifest.v2.json │ ├── manifest.v3.json │ └── popup.pug ├── scripts/ │ └── version.js ├── source/ │ ├── background/ │ │ ├── index.ts │ │ ├── library/ │ │ │ └── domain.ts │ │ ├── services/ │ │ │ ├── autoLogin.ts │ │ │ ├── config.ts │ │ │ ├── crypto.ts │ │ │ ├── cryptoKeys.ts │ │ │ ├── desktop/ │ │ │ │ ├── actions.ts │ │ │ │ ├── header.ts │ │ │ │ └── request.ts │ │ │ ├── disabledDomains.ts │ │ │ ├── entry.ts │ │ │ ├── init.ts │ │ │ ├── log.ts │ │ │ ├── loginMemory.ts │ │ │ ├── messaging.ts │ │ │ ├── notifications.ts │ │ │ ├── recents.ts │ │ │ ├── storage/ │ │ │ │ └── BrowserStorageInterface.ts │ │ │ ├── storage.ts │ │ │ └── tabs.ts │ │ └── types.ts │ ├── full/ │ │ ├── applications/ │ │ │ └── full.tsx │ │ ├── components/ │ │ │ ├── App.tsx │ │ │ ├── Layout.tsx │ │ │ └── pages/ │ │ │ ├── AttributionsPage.tsx │ │ │ ├── DisabledDomainsPage.tsx │ │ │ ├── NotificationsPage.tsx │ │ │ ├── connect/ │ │ │ │ ├── CodeInput.tsx │ │ │ │ ├── ConnectPage.tsx │ │ │ │ └── index.tsx │ │ │ └── saveCredentials/ │ │ │ ├── CredentialsSaver.tsx │ │ │ ├── CredentialsSelector.tsx │ │ │ ├── NewEntrySavePrompt.tsx │ │ │ └── index.tsx │ │ ├── hooks/ │ │ │ ├── credentials.ts │ │ │ ├── disabledDomains.ts │ │ │ ├── document.ts │ │ │ └── vaultContents.ts │ │ ├── index.pug │ │ ├── services/ │ │ │ ├── credentials.ts │ │ │ ├── disabledDomains.ts │ │ │ ├── init.ts │ │ │ ├── log.ts │ │ │ ├── notifications.ts │ │ │ └── vaults.ts │ │ ├── styles/ │ │ │ └── full.sass │ │ └── types.ts │ ├── popup/ │ │ ├── applications/ │ │ │ └── popup.tsx │ │ ├── components/ │ │ │ ├── App.tsx │ │ │ ├── contexts/ │ │ │ │ └── LaunchContext.tsx │ │ │ ├── entries/ │ │ │ │ ├── EntryInfoDialog.tsx │ │ │ │ ├── EntryItem.tsx │ │ │ │ └── EntryItemList.tsx │ │ │ ├── navigation/ │ │ │ │ └── Navigator.tsx │ │ │ ├── otps/ │ │ │ │ ├── OTPItem.tsx │ │ │ │ └── OTPItemList.tsx │ │ │ ├── pages/ │ │ │ │ ├── AboutPage.tsx │ │ │ │ ├── EntriesPage.tsx │ │ │ │ ├── OTPsPage.tsx │ │ │ │ ├── SaveDialogPage.tsx │ │ │ │ ├── SettingsPage.tsx │ │ │ │ └── VaultsPage.tsx │ │ │ └── vaults/ │ │ │ ├── VaultItem.tsx │ │ │ ├── VaultItemList.tsx │ │ │ └── VaultStateIndicator.tsx │ │ ├── hooks/ │ │ │ ├── credentials.ts │ │ │ ├── desktop.ts │ │ │ ├── document.ts │ │ │ ├── otp.ts │ │ │ └── tab.ts │ │ ├── index.pug │ │ ├── queries/ │ │ │ ├── desktop.ts │ │ │ ├── disabledDomains.ts │ │ │ └── loginMemory.ts │ │ ├── services/ │ │ │ ├── clipboard.ts │ │ │ ├── entry.ts │ │ │ ├── init.ts │ │ │ ├── log.ts │ │ │ ├── recents.ts │ │ │ ├── reset.ts │ │ │ └── tab.ts │ │ ├── state/ │ │ │ └── app.ts │ │ ├── styles/ │ │ │ └── popup.sass │ │ └── types.ts │ ├── shared/ │ │ ├── components/ │ │ │ ├── ConfirmDialog.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── ErrorMessage.tsx │ │ │ ├── RouteError.tsx │ │ │ ├── ThemeProvider.tsx │ │ │ └── loading/ │ │ │ └── BusyLoader.tsx │ │ ├── extension.ts │ │ ├── hooks/ │ │ │ ├── async.ts │ │ │ ├── config.ts │ │ │ ├── global.ts │ │ │ ├── theme.ts │ │ │ └── timer.ts │ │ ├── i18n/ │ │ │ ├── trans.ts │ │ │ └── translations/ │ │ │ ├── en.json │ │ │ └── nl.json │ │ ├── library/ │ │ │ ├── buffer.ts │ │ │ ├── clone.ts │ │ │ ├── domain.ts │ │ │ ├── error.ts │ │ │ ├── extension.ts │ │ │ ├── i18n.ts │ │ │ ├── log.ts │ │ │ ├── otp.ts │ │ │ ├── url.ts │ │ │ ├── vaultTypes.ts │ │ │ └── version.ts │ │ ├── notifications/ │ │ │ ├── index.ts │ │ │ └── pages/ │ │ │ └── WelcomeV3.tsx │ │ ├── queries/ │ │ │ └── config.ts │ │ ├── services/ │ │ │ ├── messaging.ts │ │ │ └── notifications.ts │ │ ├── styles/ │ │ │ ├── base.sass │ │ │ └── fonts.sass │ │ ├── symbols.ts │ │ ├── themes.ts │ │ └── types.ts │ ├── tab/ │ │ ├── index.ts │ │ ├── library/ │ │ │ ├── disable.ts │ │ │ ├── dismount.ts │ │ │ ├── frames.ts │ │ │ ├── page.ts │ │ │ ├── position.ts │ │ │ ├── resize.ts │ │ │ ├── styles.ts │ │ │ └── zIndex.ts │ │ ├── services/ │ │ │ ├── LoginTracker.ts │ │ │ ├── autoLogin.ts │ │ │ ├── config.ts │ │ │ ├── form.ts │ │ │ ├── formDetection.ts │ │ │ ├── init.ts │ │ │ ├── log.ts │ │ │ ├── logins/ │ │ │ │ ├── disabled.ts │ │ │ │ ├── saving.ts │ │ │ │ └── watcher.ts │ │ │ └── messaging.ts │ │ ├── state/ │ │ │ ├── form.ts │ │ │ └── frame.ts │ │ ├── types.ts │ │ └── ui/ │ │ ├── launch.ts │ │ ├── popup.ts │ │ └── saveDialog.ts │ └── typings/ │ ├── assets.d.ts │ └── globals.d.ts ├── tsconfig.json └── webpack.config.js
SYMBOL INDEX (401 symbols across 133 files)
FILE: source/background/library/domain.ts
function extractDomainFromCredentials (line 3) | function extractDomainFromCredentials(credentials: UsedCredentials): str...
FILE: source/background/services/autoLogin.ts
type RegisteredItem (line 4) | interface RegisteredItem {
constant REGISTER_MAX_AGE (line 9) | const REGISTER_MAX_AGE = 30 * 1000;
function getAutoLoginForTab (line 13) | function getAutoLoginForTab(tabID: number): SearchResult | null {
function getRegister (line 24) | function getRegister(): ExpiryMap<string, RegisteredItem> {
function registerAutoLogin (line 31) | function registerAutoLogin(entry: SearchResult, tabID: number): void {
FILE: source/background/services/config.ts
constant DEFAULTS (line 5) | const DEFAULTS: Configuration = {
function getConfig (line 15) | function getConfig(): Configuration {
function initialise (line 22) | async function initialise() {
function updateConfigValue (line 26) | async function updateConfigValue<T extends keyof Configuration>(key: T, ...
function updateConfigWithDefaults (line 34) | async function updateConfigWithDefaults(): Promise<Configuration> {
FILE: source/background/services/crypto.ts
function decryptPayload (line 4) | async function decryptPayload(
function encryptPayload (line 15) | async function encryptPayload(
FILE: source/background/services/cryptoKeys.ts
function createKeys (line 9) | async function createKeys(): Promise<{
function deriveSecretKey (line 31) | async function deriveSecretKey(privateKey: CryptoKey, publicKey: CryptoK...
function exportECDHKey (line 54) | async function exportECDHKey(key: CryptoKey): Promise<string> {
function generateKeys (line 63) | async function generateKeys(): Promise<void> {
function importECDHKey (line 79) | async function importECDHKey(key: string): Promise<CryptoKey> {
FILE: source/background/services/desktop/actions.ts
function authenticateBrowserAccess (line 8) | async function authenticateBrowserAccess(code: string): Promise<string> {
function getEntrySearchResults (line 32) | async function getEntrySearchResults(
function getOTPs (line 49) | async function getOTPs(): Promise<Array<OTP>> {
function getVaultSources (line 61) | async function getVaultSources(): Promise<Array<VaultSourceDescription>> {
function getVaultsTree (line 73) | async function getVaultsTree(): Promise<VaultsTree> {
function hasConnection (line 94) | async function hasConnection(): Promise<boolean> {
function initiateConnection (line 99) | async function initiateConnection(): Promise<void> {
function promptSourceLock (line 111) | async function promptSourceLock(sourceID: VaultSourceID): Promise<boolea...
function promptSourceUnlock (line 122) | async function promptSourceUnlock(sourceID: VaultSourceID): Promise<void> {
function saveExistingEntry (line 131) | async function saveExistingEntry(
function saveNewEntry (line 148) | async function saveNewEntry(
function searchEntriesByURL (line 169) | async function searchEntriesByURL(url: string): Promise<Array<SearchResu...
function searchEntriesByTerm (line 185) | async function searchEntriesByTerm(term: string): Promise<Array<SearchRe...
function testAuth (line 201) | async function testAuth(): Promise<void> {
FILE: source/background/services/desktop/header.ts
function generateAuthHeader (line 5) | async function generateAuthHeader(): Promise<string> {
FILE: source/background/services/desktop/request.ts
type OutputType (line 8) | type OutputType = "body" | "status" | undefined;
type DesktopRequestConfig (line 10) | interface DesktopRequestConfig<O extends OutputType> {
constant DESKTOP_URL_BASE (line 18) | const DESKTOP_URL_BASE = `http://localhost:${DESKTOP_API_PORT}`;
function sendDesktopRequest (line 27) | async function sendDesktopRequest<O extends OutputType>(
FILE: source/background/services/disabledDomains.ts
function disableLoginsOnDomain (line 4) | async function disableLoginsOnDomain(domain: string): Promise<void> {
function getDisabledDomains (line 10) | async function getDisabledDomains(): Promise<Array<string>> {
function removeDisabledFlagForDomain (line 15) | async function removeDisabledFlagForDomain(domain: string): Promise<void> {
FILE: source/background/services/entry.ts
function openEntryPageInNewTab (line 5) | async function openEntryPageInNewTab(_: SearchResult, url: string): Prom...
FILE: source/background/services/init.ts
type Initialisation (line 11) | enum Initialisation {
function initialise (line 20) | async function initialise(): Promise<void> {
function resetInitialisation (line 35) | async function resetInitialisation(): Promise<void> {
function waitForInitialisation (line 41) | async function waitForInitialisation(): Promise<void> {
FILE: source/background/services/log.ts
constant LOG_NAME (line 3) | const LOG_NAME = "buttercup:browser:background";
function log (line 7) | function log(...args: Array<any>): void {
FILE: source/background/services/loginMemory.ts
type LoginMemoryItem (line 6) | interface LoginMemoryItem {
constant LOGIN_MAX_AGE (line 11) | const LOGIN_MAX_AGE = 15 * 60 * 1000;
function clearCredentials (line 15) | function clearCredentials(id: string): void {
function credentialsAlreadyStored (line 28) | async function credentialsAlreadyStored(credentials: UsedCredentials): P...
function getAllCredentials (line 49) | function getAllCredentials(): Array<UsedCredentials> {
function getCredentialsForID (line 59) | function getCredentialsForID(id: string): UsedCredentials | null {
function getLastCredentials (line 64) | function getLastCredentials(tabID: number): UsedCredentials | null {
function getLoginMemory (line 71) | function getLoginMemory(): ExpiryMap<string, LoginMemoryItem> {
function stopPromptForID (line 78) | function stopPromptForID(id: string): void {
function updateUsedCredentials (line 102) | function updateUsedCredentials(credentials: UsedCredentials, tabID: numb...
FILE: source/background/services/messaging.ts
function handleMessage (line 50) | async function handleMessage(
function initialise (line 403) | function initialise() {
FILE: source/background/services/notifications.ts
function getPendingNotifications (line 6) | async function getPendingNotifications(): Promise<Array<string>> {
function markNotificationRead (line 12) | async function markNotificationRead(name: string): Promise<void> {
function showPendingNotifications (line 21) | async function showPendingNotifications(): Promise<void> {
FILE: source/background/services/recents.ts
type RecentItem (line 7) | interface RecentItem {
constant MAX_USE_AGE (line 13) | const MAX_USE_AGE = ms("30d");
function getQueue (line 17) | function getQueue(): ChannelQueue {
function getRecents (line 24) | async function getRecents(sourceIDs: Array<VaultSourceID>): Promise<Arra...
function sortUses (line 31) | function sortUses(itemA: RecentItem, itemB: RecentItem): number {
function stripOldUses (line 37) | function stripOldUses(items: Array<RecentItem>): Array<RecentItem> {
function trackRecentUsage (line 52) | async function trackRecentUsage(sourceID: VaultSourceID, entryID: EntryI...
FILE: source/background/services/storage.ts
constant VALID_LOCAL_KEYS (line 5) | const VALID_LOCAL_KEYS = Object.values(LocalStorageItem);
constant VALID_SYNC_KEYS (line 6) | const VALID_SYNC_KEYS = Object.values(SyncStorageItem);
function clearLocalStorage (line 8) | async function clearLocalStorage(): Promise<void> {
function getLocalStorage (line 19) | function getLocalStorage(): BrowserStorageInterface {
function getLocalValue (line 23) | async function getLocalValue(key: LocalStorageItem): Promise<string | nu...
function getSyncValue (line 27) | async function getSyncValue(key: SyncStorageItem): Promise<string | null> {
function getSynchronisedStorage (line 31) | function getSynchronisedStorage(): BrowserStorageInterface {
function initialise (line 35) | async function initialise() {
function removeLocalValue (line 60) | async function removeLocalValue(key: LocalStorageItem): Promise<void> {
function removeSyncValue (line 64) | async function removeSyncValue(key: SyncStorageItem): Promise<void> {
function setLocalValue (line 68) | async function setLocalValue(key: LocalStorageItem, value: string): Prom...
function setSyncValue (line 72) | async function setSyncValue(key: SyncStorageItem, value: string): Promis...
FILE: source/background/services/storage/BrowserStorageInterface.ts
function getSyncStorage (line 4) | function getSyncStorage() {
function getNonSyncStorage (line 8) | function getNonSyncStorage() {
class BrowserStorageInterface (line 12) | class BrowserStorageInterface extends StorageInterface {
method constructor (line 15) | constructor(storage: chrome.storage.StorageArea = getSyncStorage()) {
method storage (line 20) | get storage() {
method getAllKeys (line 24) | async getAllKeys() {
method getValue (line 32) | async getValue(name: string) {
method removeKey (line 40) | async removeKey(name: string) {
method setValue (line 46) | async setValue(name: string, value: any) {
FILE: source/background/services/tabs.ts
function sendTabsMessage (line 4) | async function sendTabsMessage(payload: TabEvent, tabIDs: Array<number> ...
FILE: source/background/types.ts
type LocalStorageItem (line 3) | enum LocalStorageItem {
type SyncStorageItem (line 10) | enum SyncStorageItem {
FILE: source/full/components/App.tsx
constant ROUTER (line 15) | const ROUTER = createHashRouter([
function App (line 48) | function App() {
FILE: source/full/components/Layout.tsx
type LayoutProps (line 8) | interface LayoutProps {
function Layout (line 50) | function Layout({ children, title }: LayoutProps) {
FILE: source/full/components/pages/AttributionsPage.tsx
function AttributionsPage (line 19) | function AttributionsPage() {
FILE: source/full/components/pages/DisabledDomainsPage.tsx
function DisabledDomainsPage (line 36) | function DisabledDomainsPage() {
FILE: source/full/components/pages/NotificationsPage.tsx
function NotificationsPage (line 11) | function NotificationsPage() {
FILE: source/full/components/pages/connect/CodeInput.tsx
type CodeInputProps (line 6) | interface CodeInputProps {
function CodeInput (line 22) | function CodeInput(props: CodeInputProps) {
FILE: source/full/components/pages/connect/ConnectPage.tsx
function ConnectPage (line 12) | function ConnectPage() {
FILE: source/full/components/pages/connect/index.tsx
function ConnectPage (line 5) | function ConnectPage() {
FILE: source/full/components/pages/saveCredentials/CredentialsSaver.tsx
type CredentialsSaverProps (line 15) | interface CredentialsSaverProps {
type NodeInfo (line 22) | interface NodeInfo {
function buildGroupNodes (line 38) | function buildGroupNodes(
function buildVaultRootNodes (line 103) | function buildVaultRootNodes(
function countGroupChildren (line 122) | function countGroupChildren(
function CredentialsSaver (line 139) | function CredentialsSaver(props: CredentialsSaverProps) {
FILE: source/full/components/pages/saveCredentials/CredentialsSelector.tsx
type CredentialsSelectorProps (line 11) | interface CredentialsSelectorProps {
constant URL (line 64) | const URL = styled.span`
function CredentialsSelector (line 68) | function CredentialsSelector(props: CredentialsSelectorProps) {
FILE: source/full/components/pages/saveCredentials/NewEntrySavePrompt.tsx
type NewEntrySavePromptProps (line 9) | interface NewEntrySavePromptProps {
function isValidInput (line 37) | function isValidInput(input: string): boolean {
function NewEntrySavePrompt (line 41) | function NewEntrySavePrompt(props: NewEntrySavePromptProps) {
FILE: source/full/components/pages/saveCredentials/index.tsx
type TabID (line 14) | enum TabID {
function SaveCredentialsPage (line 19) | function SaveCredentialsPage() {
FILE: source/full/hooks/credentials.ts
function useCapturedCredentials (line 5) | function useCapturedCredentials(): [
FILE: source/full/hooks/disabledDomains.ts
constant REFRESH_DELAY (line 4) | const REFRESH_DELAY = 2500;
function useDisabledDomains (line 6) | function useDisabledDomains(
FILE: source/full/hooks/document.ts
constant TITLE_SEPARATOR (line 3) | const TITLE_SEPARATOR = "⋅";
function useTitle (line 7) | function useTitle(title: string) {
FILE: source/full/hooks/vaultContents.ts
function vaultTreesDiffer (line 5) | function vaultTreesDiffer(existingValue: VaultsTree, newValue: VaultsTre...
function useAllVaultsContents (line 25) | function useAllVaultsContents(): {
FILE: source/full/services/credentials.ts
function clearSavedCredentials (line 6) | async function clearSavedCredentials(id: string): Promise<void> {
function getCredentials (line 16) | async function getCredentials(): Promise<Array<UsedCredentials | null>> {
function saveCredentialsToEntry (line 26) | async function saveCredentialsToEntry(credentials: SavedCredentials): Pr...
FILE: source/full/services/disabledDomains.ts
function getDisabledDomains (line 5) | async function getDisabledDomains(): Promise<Array<string>> {
function removeDisabledDomain (line 15) | async function removeDisabledDomain(domain: string): Promise<void> {
FILE: source/full/services/init.ts
function initialise (line 5) | async function initialise() {
FILE: source/full/services/log.ts
constant LOG_NAME (line 3) | const LOG_NAME = "buttercup:browser:page";
function log (line 7) | function log(...args: Array<any>): void {
FILE: source/full/services/notifications.ts
function updateReadNotifications (line 5) | async function updateReadNotifications(notificationName: string): Promis...
FILE: source/full/services/vaults.ts
function getVaultsTree (line 5) | async function getVaultsTree(): Promise<VaultsTree> {
FILE: source/popup/components/App.tsx
constant ROUTER (line 18) | const ROUTER = createHashRouter([
function App (line 45) | function App() {
function InPageApp (line 55) | function InPageApp() {
function SavePromptApp (line 80) | function SavePromptApp() {
function ToolbarApp (line 92) | function ToolbarApp() {
FILE: source/popup/components/contexts/LaunchContext.tsx
type LaunchContextProps (line 3) | interface LaunchContextProps {
type LaunchContextDefaultValue (line 11) | interface LaunchContextDefaultValue {
function LaunchContextProvider (line 21) | function LaunchContextProvider(props: LaunchContextProps) {
FILE: source/popup/components/entries/EntryInfoDialog.tsx
type EntryInfoDialogProps (line 11) | interface EntryInfoDialogProps {
type EntryProperty (line 16) | interface EntryProperty {
function EntryInfoDialog (line 38) | function EntryInfoDialog(props: EntryInfoDialogProps) {
function orderProperties (line 95) | function orderProperties(properties: Record<string, string>): Array<Entr...
FILE: source/popup/components/entries/EntryItem.tsx
type EntryItemProps (line 12) | interface EntryItemProps {
function EntryItem (line 65) | function EntryItem(props: EntryItemProps) {
FILE: source/popup/components/entries/EntryItemList.tsx
type EntryItemListProps (line 9) | interface EntryItemListProps {
function EntryItemList (line 24) | function EntryItemList(props: EntryItemListProps) {
FILE: source/popup/components/navigation/Navigator.tsx
type NavigatorProps (line 16) | interface NavigatorProps {
function Navigator (line 60) | function Navigator(props: NavigatorProps) {
FILE: source/popup/components/otps/OTPItem.tsx
type OTPItemProps (line 9) | interface OTPItemProps {
function OTPItem (line 76) | function OTPItem(props: OTPItemProps) {
FILE: source/popup/components/otps/OTPItemList.tsx
type OTPItemListProps (line 8) | interface OTPItemListProps {
function OTPItemList (line 33) | function OTPItemList(props: OTPItemListProps) {
FILE: source/popup/components/pages/AboutPage.tsx
type AboutPageProps (line 12) | interface AboutPageProps {}
function AboutPage (line 57) | function AboutPage(_: AboutPageProps) {
FILE: source/popup/components/pages/EntriesPage.tsx
type EntriesPageProps (line 17) | interface EntriesPageProps {
type EntriesPageControlsProps (line 23) | interface EntriesPageControlsProps {
function EntriesPage (line 41) | function EntriesPage(props: EntriesPageProps) {
function EntriesPageList (line 84) | function EntriesPageList(props: EntriesPageProps) {
function EntriesPageControls (line 177) | function EntriesPageControls(props: EntriesPageControlsProps) {
FILE: source/popup/components/pages/OTPsPage.tsx
type OTPsPageProps (line 17) | interface OTPsPageProps {
type OTPsPageControlsProps (line 22) | interface OTPsPageControlsProps {}
function OTPsPage (line 34) | function OTPsPage(props: OTPsPageProps) {
function OTPsPageList (line 77) | function OTPsPageList(props: OTPsPageProps) {
function OTPsPageControls (line 142) | function OTPsPageControls(props: OTPsPageControlsProps) {
FILE: source/popup/components/pages/SaveDialogPage.tsx
constant URL (line 85) | const URL = styled.span`
function SaveDialogPage (line 89) | function SaveDialogPage() {
FILE: source/popup/components/pages/SettingsPage.tsx
type InputButtonTypeItem (line 15) | interface InputButtonTypeItem {
function renderInputButtonTypeItem (line 35) | function renderInputButtonTypeItem(item: InputButtonTypeItem, props: Ite...
function SettingsPage (line 51) | function SettingsPage() {
function SettingsPageControls (line 192) | function SettingsPageControls() {
FILE: source/popup/components/pages/VaultsPage.tsx
type VaultsPageProps (line 9) | interface VaultsPageProps {
function VaultsPage (line 24) | function VaultsPage(props: VaultsPageProps) {
function VaultsPageList (line 67) | function VaultsPageList() {
function VaultsPageControls (line 85) | function VaultsPageControls() {
FILE: source/popup/components/vaults/VaultItem.tsx
type VaultItemProps (line 11) | interface VaultItemProps {
function VaultItem (line 63) | function VaultItem(props: VaultItemProps) {
FILE: source/popup/components/vaults/VaultItemList.tsx
type VaultItemListProps (line 11) | interface VaultItemListProps {
function VaultItemList (line 23) | function VaultItemList(props: VaultItemListProps) {
FILE: source/popup/components/vaults/VaultStateIndicator.tsx
type VaultStateIndicatorProps (line 6) | interface VaultStateIndicatorProps {
function VaultStateIndicator (line 14) | function VaultStateIndicator(props: VaultStateIndicatorProps) {
FILE: source/popup/hooks/credentials.ts
function getAllCredentials (line 7) | async function getAllCredentials(): Promise<Array<UsedCredentials | null...
function getCredentialsForID (line 17) | async function getCredentialsForID(id: string): Promise<UsedCredentials ...
function useAllLoginCredentials (line 28) | function useAllLoginCredentials(): AsyncResult<Array<UsedCredentials | n...
function useLoginCredentials (line 34) | function useLoginCredentials(loginID: string | null): AsyncResult<UsedCr...
FILE: source/popup/hooks/desktop.ts
constant OTPS_UPDATE_DELAY (line 18) | const OTPS_UPDATE_DELAY = 7500;
constant SEARCH_DEBOUNCE (line 19) | const SEARCH_DEBOUNCE = 600;
constant SOURCES_UPDATE_DELAY (line 20) | const SOURCES_UPDATE_DELAY = 3500;
function useDesktopConnectionState (line 22) | function useDesktopConnectionState(): DesktopConnectionState {
function useEntriesForURL (line 51) | function useEntriesForURL(url: string | null): Array<SearchResult> {
function useOTPs (line 74) | function useOTPs(): [Array<OTP>, boolean] {
function useRecentEntries (line 111) | function useRecentEntries(): Array<SearchResult> {
function useSearchedEntries (line 126) | function useSearchedEntries(term: string): Array<SearchResult> {
function useVaultSources (line 154) | function useVaultSources(): Array<VaultSourceDescription> {
FILE: source/popup/hooks/document.ts
function useBodyClass (line 3) | function useBodyClass(className: string): void {
FILE: source/popup/hooks/otp.ts
type PreparedOTP (line 8) | interface PreparedOTP extends OTP {
function getPeriodTimeLeft (line 15) | function getPeriodTimeLeft(period: number): number {
function usePreparedOTPs (line 19) | function usePreparedOTPs(otps: Array<OTP>): Array<PreparedOTP> {
FILE: source/popup/hooks/tab.ts
function useCurrentTabURL (line 5) | function useCurrentTabURL(): [loading: boolean, url: string | null] {
FILE: source/popup/queries/desktop.ts
function clearDesktopConnectionAuth (line 6) | async function clearDesktopConnectionAuth(): Promise<void> {
function getDesktopConnectionAvailable (line 15) | async function getDesktopConnectionAvailable(): Promise<boolean> {
function getOTPs (line 25) | async function getOTPs(): Promise<Array<OTP>> {
function getRecentEntries (line 35) | async function getRecentEntries(): Promise<Array<SearchResult>> {
function getVaultSources (line 46) | async function getVaultSources(): Promise<Array<VaultSourceDescription>> {
function initiateDesktopConnectionRequest (line 56) | async function initiateDesktopConnectionRequest(): Promise<void> {
function promptLockVault (line 65) | async function promptLockVault(sourceID: VaultSourceID): Promise<boolean> {
function promptUnlockVault (line 76) | async function promptUnlockVault(sourceID: VaultSourceID): Promise<void> {
function searchEntriesByTerm (line 86) | async function searchEntriesByTerm(term: string): Promise<Array<SearchRe...
function searchEntriesByURL (line 97) | async function searchEntriesByURL(url: string): Promise<Array<SearchResu...
FILE: source/popup/queries/disabledDomains.ts
function disableDomainForLogin (line 5) | async function disableDomainForLogin(loginID: string): Promise<void> {
FILE: source/popup/queries/loginMemory.ts
function clearSavedLoginPrompt (line 5) | async function clearSavedLoginPrompt(loginID: string): Promise<void> {
FILE: source/popup/services/clipboard.ts
function copyTextToClipboard (line 3) | async function copyTextToClipboard(text: string): Promise<void> {
FILE: source/popup/services/entry.ts
function openPageForEntry (line 6) | async function openPageForEntry(item: SearchResult, autoLogin: boolean):...
FILE: source/popup/services/init.ts
function initialise (line 5) | async function initialise() {
FILE: source/popup/services/log.ts
constant LOG_NAME (line 3) | const LOG_NAME = "buttercup:browser:popup";
function log (line 7) | function log(...args: Array<any>): void {
FILE: source/popup/services/recents.ts
function trackEntryRecentUse (line 6) | async function trackEntryRecentUse(item: SearchResult): Promise<void> {
FILE: source/popup/services/reset.ts
function resetApplicationSettings (line 5) | async function resetApplicationSettings(): Promise<void> {
FILE: source/popup/services/tab.ts
function sendEntryResultToTabForInput (line 9) | function sendEntryResultToTabForInput(formID: string, entry: SearchResul...
function sendOTPToTabForInput (line 23) | function sendOTPToTabForInput(formID: string, otp: OTP): void {
function sendTabEvent (line 47) | function sendTabEvent(event: TabEvent, target: MessageEventSource = wind...
FILE: source/popup/state/app.ts
constant APP_STATE (line 4) | const APP_STATE = createStateObject<{
FILE: source/popup/types.ts
type DesktopConnectionState (line 1) | enum DesktopConnectionState {
FILE: source/shared/components/ConfirmDialog.tsx
type ConfirmDialogProps (line 7) | interface ConfirmDialogProps {
function ConfirmDialog (line 21) | function ConfirmDialog(props: ConfirmDialogProps) {
FILE: source/shared/components/ErrorBoundary.tsx
function stripBlanks (line 17) | function stripBlanks(txt = "") {
class ErrorBoundary (line 24) | class ErrorBoundary extends Component {
method getDerivedStateFromError (line 25) | static getDerivedStateFromError(error: Error) {
method componentDidCatch (line 37) | componentDidCatch(error: Error, errorInfo) {
method render (line 41) | render() {
FILE: source/shared/components/ErrorMessage.tsx
type ErrorMessageProps (line 5) | interface ErrorMessageProps {
function ErrorMessage (line 18) | function ErrorMessage(props: ErrorMessageProps) {
FILE: source/shared/components/RouteError.tsx
function stripBlanks (line 18) | function stripBlanks(txt = "") {
function RouteError (line 25) | function RouteError() {
FILE: source/shared/components/ThemeProvider.tsx
type ThemeProviderProps (line 6) | interface ThemeProviderProps {
function ThemeProvider (line 11) | function ThemeProvider(props: ThemeProviderProps) {
FILE: source/shared/components/loading/BusyLoader.tsx
type BusyLoaderProps (line 6) | interface BusyLoaderProps {
function BusyLoader (line 23) | function BusyLoader(props: BusyLoaderProps) {
FILE: source/shared/extension.ts
function getExtensionAPI (line 1) | function getExtensionAPI(): typeof chrome {
FILE: source/shared/hooks/async.ts
type AsyncResult (line 4) | interface AsyncResult<T extends any> {
function useAsync (line 10) | function useAsync<T extends any>(
function useAsyncWithTimer (line 93) | function useAsyncWithTimer<T extends any>(
FILE: source/shared/hooks/config.ts
function useConfig (line 8) | function useConfig(): [
FILE: source/shared/hooks/global.ts
type Globals (line 4) | interface Globals {
function useGlobal (line 13) | function useGlobal<K extends keyof Globals>(key: K): [Globals[K], (value...
FILE: source/shared/hooks/theme.ts
function useBodyThemeClass (line 7) | function useBodyThemeClass(theme: "dark" | "light"): void {
function useTheme (line 25) | function useTheme(): "dark" | "light" {
FILE: source/shared/hooks/timer.ts
function useTimer (line 4) | function useTimer(callback: () => void, delay: number, dependencies: Dep...
FILE: source/shared/i18n/trans.ts
constant DEFAULT_LANGUAGE (line 6) | const DEFAULT_LANGUAGE = "en";
constant TRANSLATIONS (line 7) | const TRANSLATIONS = {
function changeLanguage (line 13) | async function changeLanguage(lang: string) {
function initialise (line 17) | async function initialise(lang: string) {
function onLanguageChanged (line 34) | function onLanguageChanged(callback: (lang: string) => void): () => void {
function t (line 42) | function t(key: string, options?: TOptions) {
FILE: source/shared/library/buffer.ts
function arrayBufferToHex (line 1) | function arrayBufferToHex(buffer: ArrayBuffer): string {
function arrayBufferToString (line 5) | function arrayBufferToString(buffer: ArrayBuffer): string {
function base64DecodeUnicode (line 9) | function base64DecodeUnicode(str: string): string {
function base64EncodeUnicode (line 13) | function base64EncodeUnicode(str: string): string {
function stringToArrayBuffer (line 17) | function stringToArrayBuffer(str: string): ArrayBuffer {
FILE: source/shared/library/clone.ts
function naiveClone (line 1) | function naiveClone<T extends Array<any> | Record<string, any>>(item: T)...
function naiveCloneArray (line 8) | function naiveCloneArray<T extends Array<any>>(arr: T): T {
function naiveCloneObject (line 20) | function naiveCloneObject<T extends Record<string, any>>(obj: T): T {
FILE: source/shared/library/domain.ts
function domainsReferToSameParent (line 4) | function domainsReferToSameParent(domain1: string, domain2: string): boo...
function extractDomain (line 16) | function extractDomain(str: string): string {
function extractEntryDomain (line 24) | function extractEntryDomain(entryProperties: PropertyKeyValueObject): st...
FILE: source/shared/library/error.ts
function errorToString (line 4) | function errorToString(error: Error | Layerr): string {
function localisedErrorMessage (line 8) | function localisedErrorMessage(error: Error | Layerr): string {
function stringToError (line 20) | function stringToError(error: Error | Layerr | string): Layerr | Error {
FILE: source/shared/library/extension.ts
function createNewTab (line 5) | async function createNewTab(url: string): Promise<chrome.tabs.Tab | null> {
function closeCurrentTab (line 15) | function closeCurrentTab() {
function getAllTabs (line 23) | async function getAllTabs(): Promise<Array<chrome.tabs.Tab>> {
function getCurrentTab (line 32) | async function getCurrentTab(): Promise<chrome.tabs.Tab> {
function getExtensionURL (line 41) | function getExtensionURL(path: string): string {
function sendTabMessage (line 45) | async function sendTabMessage(tabID: number, message: any) {
FILE: source/shared/library/i18n.ts
function getLanguage (line 1) | function getLanguage(/*preferences: Preferences, locale: string*/): stri...
FILE: source/shared/library/log.ts
type Logger (line 3) | type Logger = ReturnType<typeof createLog>;
function createLog (line 5) | function createLog(name: string, force: boolean = false): (...args: Arra...
FILE: source/shared/library/otp.ts
function extractFirstOTPURI (line 5) | function extractFirstOTPURI(entry: SearchResult): string | null {
function otpURIToDigits (line 18) | function otpURIToDigits(uri: string): string {
function searchResultToOTP (line 27) | function searchResultToOTP(entry: SearchResult): string | null {
FILE: source/shared/library/url.ts
function formatURL (line 1) | function formatURL(base: string): string {
FILE: source/shared/library/vaultTypes.ts
type VaultTypeDescription (line 7) | interface VaultTypeDescription {
constant VAULT_TYPES (line 12) | const VAULT_TYPES: Record<VaultType, VaultTypeDescription> = {
FILE: source/shared/library/version.ts
constant BUILD_DATE (line 3) | const BUILD_DATE = "2024-04-09";
constant VERSION (line 4) | const VERSION = "3.2.0";
FILE: source/shared/notifications/index.ts
constant NOTIFICATIONS (line 3) | const NOTIFICATIONS: Record<string, [string, () => JSX.Element]> = {
constant NOTIFICATION_NAMES (line 7) | const NOTIFICATION_NAMES = Object.keys(NOTIFICATIONS);
FILE: source/shared/notifications/pages/WelcomeV3.tsx
constant TITLE (line 4) | const TITLE = "notifications.page.welcome-v3.title";
function Page (line 6) | function Page() {
FILE: source/shared/queries/config.ts
function getConfig (line 5) | async function getConfig(): Promise<Configuration> {
function setConfigValue (line 18) | async function setConfigValue<T extends keyof Configuration>(
FILE: source/shared/services/messaging.ts
function sendBackgroundMessage (line 6) | async function sendBackgroundMessage(
FILE: source/shared/services/notifications.ts
function getToaster (line 7) | function getToaster(): ToasterInstance {
FILE: source/shared/symbols.ts
constant API_KEY_ALGO (line 1) | const API_KEY_ALGO = "ECDH";
constant API_KEY_CURVE (line 2) | const API_KEY_CURVE = "P-256";
constant BRAND_COLOUR (line 4) | const BRAND_COLOUR = "#00B7AC";
constant BRAND_COLOUR_DARK (line 5) | const BRAND_COLOUR_DARK = "#179E94";
constant DESKTOP_API_PORT (line 7) | const DESKTOP_API_PORT = 12822;
constant MESSAGE_DEFAULT_TIMEOUT (line 9) | const MESSAGE_DEFAULT_TIMEOUT = 15000;
FILE: source/shared/types.ts
type AddVaultPayload (line 13) | interface AddVaultPayload {
type BackgroundMessage (line 22) | interface BackgroundMessage {
type BackgroundMessageType (line 45) | enum BackgroundMessageType {
type BackgroundResponse (line 78) | interface BackgroundResponse {
type ChildElement (line 94) | type ChildElement = ReactChild | ReactChildren | false | null;
type ChildElements (line 95) | type ChildElements = ChildElement | Array<ChildElement>;
type Configuration (line 97) | interface Configuration {
type ElementRect (line 105) | interface ElementRect {
type InputButtonType (line 112) | enum InputButtonType {
type InputType (line 117) | enum InputType {
type OTP (line 122) | interface OTP {
type PopupPage (line 132) | enum PopupPage {
type SavedCredentials (line 140) | interface SavedCredentials extends UsedCredentials {
type TabEvent (line 146) | interface TabEvent {
type TabEventType (line 160) | enum TabEventType {
type UsedCredentials (line 167) | interface UsedCredentials {
type VaultSourceDescription (line 178) | interface VaultSourceDescription {
type VaultsTree (line 187) | interface VaultsTree {
type VaultsTreeItem (line 191) | interface VaultsTreeItem extends VaultFacade {
type VaultType (line 195) | enum VaultType {
FILE: source/tab/library/disable.ts
function itemIsIgnored (line 1) | function itemIsIgnored(element: HTMLElement): boolean {
FILE: source/tab/library/dismount.ts
function onElementDismount (line 1) | function onElementDismount(el: HTMLElement, callback: () => void): void {
FILE: source/tab/library/frames.ts
function findIframeForWindow (line 1) | function findIframeForWindow(url: string): HTMLIFrameElement | null {
FILE: source/tab/library/page.ts
function currentDomainDisabled (line 3) | function currentDomainDisabled(
function getCurrentDomain (line 13) | function getCurrentDomain(): string {
function getCurrentTitle (line 17) | function getCurrentTitle(): string {
function getCurrentURL (line 21) | function getCurrentURL(): string {
FILE: source/tab/library/position.ts
function getElementRectInDocument (line 3) | function getElementRectInDocument(el: HTMLElement): ElementRect {
function recalculateRectForIframe (line 13) | function recalculateRectForIframe(rect: ElementRect, iframe: HTMLIFrameE...
FILE: source/tab/library/resize.ts
function onBodyResize (line 1) | function onBodyResize(
function onBodyWidthResize (line 20) | function onBodyWidthResize(callback: (newWidth: number, lastWidth: numbe...
FILE: source/tab/library/styles.ts
constant CLEAR_STYLES (line 1) | const CLEAR_STYLES = {
function findBestZIndexInContainer (line 8) | function findBestZIndexInContainer(parentElement: HTMLElement) {
FILE: source/tab/library/zIndex.ts
function findBestZIndexInContainer (line 1) | function findBestZIndexInContainer(parentElement: HTMLElement): number {
FILE: source/tab/services/LoginTracker.ts
type Connection (line 6) | interface Connection {
type LoginTrackerEvents (line 16) | interface LoginTrackerEvents {
class LoginTracker (line 22) | class LoginTracker extends EventEmitter<LoginTrackerEvents> {
method title (line 27) | get title() {
method url (line 31) | get url() {
method getConnection (line 35) | getConnection(loginTarget: LoginTarget): Connection | null {
method registerConnection (line 43) | registerConnection(loginTarget: LoginTarget) {
function getSharedTracker (line 80) | function getSharedTracker(): LoginTracker {
FILE: source/tab/services/autoLogin.ts
function getAutoLogin (line 8) | async function getAutoLogin(): Promise<SearchResult | null> {
function processTargetAutoLogin (line 18) | async function processTargetAutoLogin(loginTarget: LoginTarget): Promise...
FILE: source/tab/services/config.ts
function getConfig (line 5) | async function getConfig(): Promise<Configuration> {
FILE: source/tab/services/form.ts
function fillFormDetails (line 11) | function fillFormDetails(frameEvent: FrameEvent) {
function initialise (line 34) | async function initialise() {
FILE: source/tab/services/formDetection.ts
constant TARGET_SEARCH_INTERVAL (line 8) | const TARGET_SEARCH_INTERVAL = 1000;
function filterLoginTarget (line 10) | function filterLoginTarget(_: LoginTargetFeature, element: HTMLElement):...
function onIdentifiedTarget (line 17) | function onIdentifiedTarget(callback: (target: LoginTarget) => void) {
function waitAndAttachLaunchButtons (line 39) | async function waitAndAttachLaunchButtons(
FILE: source/tab/services/init.ts
function initialise (line 5) | async function initialise() {
FILE: source/tab/services/log.ts
constant LOG_NAME (line 3) | const LOG_NAME = "buttercup:browser:tab";
function log (line 7) | function log(...args: Array<any>): void {
FILE: source/tab/services/logins/disabled.ts
function getDisabledDomains (line 5) | async function getDisabledDomains(): Promise<Array<string>> {
FILE: source/tab/services/logins/saving.ts
function getCredentialsForID (line 5) | async function getCredentialsForID(id: string, excludeSaved: boolean = f...
function getLastSavedCredentials (line 17) | async function getLastSavedCredentials(excludeSaved: boolean = false): P...
function transferLoginCredentials (line 28) | function transferLoginCredentials(details: UsedCredentials) {
FILE: source/tab/services/logins/watcher.ts
function checkForLoginSaveAbility (line 11) | async function checkForLoginSaveAbility(loginID?: string) {
function initialise (line 27) | async function initialise() {
function watchCredentialsOnTarget (line 44) | function watchCredentialsOnTarget(loginTarget: LoginTarget): void {
function watchLogin (line 73) | function watchLogin(
FILE: source/tab/services/messaging.ts
function broadcastFrameMessage (line 9) | function broadcastFrameMessage(event: FrameEvent): void {
function initialise (line 13) | async function initialise() {
function handleFramesBroadcast (line 20) | function handleFramesBroadcast(event: MessageEvent<FrameEvent>) {
function handleTabMessage (line 30) | function handleTabMessage(payload: unknown) {
function listenForTabEvents (line 44) | function listenForTabEvents(callback: (event: TabEvent) => void) {
function sendTabEvent (line 55) | function sendTabEvent(event: TabEvent, destination: MessageEventSource):...
FILE: source/tab/state/form.ts
constant FORM (line 4) | const FORM = createStateObject<{
FILE: source/tab/state/frame.ts
constant FRAME (line 3) | const FRAME = createStateObject<{
FILE: source/tab/types.ts
type FrameEvent (line 5) | interface FrameEvent {
type FrameEventType (line 16) | enum FrameEventType {
FILE: source/tab/ui/launch.ts
constant BUTTON_BACKGROUND_IMAGE (line 11) | const BUTTON_BACKGROUND_IMAGE = getExtensionURL(BUTTON_BACKGROUND_IMAGE_...
constant INPUT_BACKGROUND_IMAGE (line 12) | const INPUT_BACKGROUND_IMAGE = getExtensionURL(INPUT_BACKGROUND_IMAGE_RES);
function attachLaunchButton (line 14) | function attachLaunchButton(
function renderInternalStyle (line 41) | function renderInternalStyle(input: HTMLInputElement, onClick: () => voi...
function renderButtonStyle (line 81) | function renderButtonStyle(input: HTMLInputElement, onClick: () => void,...
function updateOffsetParentPositioning (line 154) | function updateOffsetParentPositioning(offsetParent: HTMLElement): void {
FILE: source/tab/ui/popup.ts
type LastPopup (line 9) | interface LastPopup {
constant CLEAR_STYLES (line 15) | const CLEAR_STYLES = {
constant POPUP_HEIGHT (line 21) | const POPUP_HEIGHT = 300;
constant POPUP_WIDTH (line 22) | const POPUP_WIDTH = 320;
function buildNewPopup (line 26) | function buildNewPopup(inputRect: ElementRect, forInputType: InputType) {
function closePopup (line 73) | function closePopup() {
function togglePopup (line 81) | function togglePopup(inputRect: ElementRect, forInputType: InputType) {
function updatePopupPosition (line 90) | function updatePopupPosition(inputRect: ElementRect): void {
FILE: source/tab/ui/saveDialog.ts
type LastSaveDialog (line 5) | interface LastSaveDialog {
constant CLEAR_STYLES (line 11) | const CLEAR_STYLES = {
constant DIALOG_WIDTH (line 17) | const DIALOG_WIDTH = 380;
constant DIALOG_HEIGHT (line 18) | const DIALOG_HEIGHT = 230;
function buildNewSaveDialog (line 22) | function buildNewSaveDialog(loginID: string) {
function closeDialog (line 60) | function closeDialog() {
function openDialog (line 67) | function openDialog(loginID: string) {
FILE: webpack.config.js
constant V3_BROWSERS (line 18) | const V3_BROWSERS = ["chrome", "edge"];
constant DIST (line 21) | const DIST = path.resolve(__dirname, "dist");
constant ICONS_PATH (line 22) | const ICONS_PATH = path.join(path.dirname(require.resolve("@buttercup/ui...
function buildManifest (line 28) | function buildManifest(assetNames, manifest) {
function getBaseConfig (line 44) | function getBaseConfig() {
Condensed preview — 165 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (368K chars).
[
{
"path": ".editorconfig",
"chars": 172,
"preview": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 4\n\n[{.prettierrc,packag"
},
{
"path": ".github/workflows/test.yml",
"chars": 864,
"preview": "name: Tests\n\non: push\n\njobs:\n lint:\n runs-on: ubuntu-latest\n strategy:\n matrix:\n "
},
{
"path": ".gitignore",
"chars": 88,
"preview": "*.log\n.DS_Store\n.history\nnode_modules\n/dist\n/release\nArchive.zip\ndist.zip\n/secrets.json\n"
},
{
"path": ".prettierrc",
"chars": 68,
"preview": "{\n \"printWidth\": 120,\n \"tabWidth\": 4,\n \"trailingComma\": \"none\"\n}\n"
},
{
"path": ".vscode/settings.json",
"chars": 205,
"preview": "{\n \"vsicons.presets.angular\": false,\n \"eslint.enable\": false,\n \"files.associations\": {\n \"**/*.js\": \"java"
},
{
"path": "CHANGELOG.md",
"chars": 18943,
"preview": "# Buttercup browser extension changelog\n\n## v3.2.0\n_2024-04-09_\n\n * Input button customisation (global)\n * Dutch languag"
},
{
"path": "LICENSE",
"chars": 1082,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Perry Mitchell\n\nPermission is hereby granted, free of charge, to any person ob"
},
{
"path": "PRIVACY_POLICY.md",
"chars": 5454,
"preview": "# Privacy Policy\n\nThis privacy policy concerns the Browser Extension for Buttercup, its use and the data it makes use of"
},
{
"path": "README.md",
"chars": 6573,
"preview": "<h1 align=\"center\">\n <br/>\n <img src=\"https://cdn.rawgit.com/buttercup-pw/buttercup-assets/4bbfd317/badge/browsers.svg"
},
{
"path": "package.json",
"chars": 4392,
"preview": "{\n \"name\": \"buttercup-browser-extension\",\n \"version\": \"3.2.0\",\n \"description\": \"Buttercup browser extension\",\n \"expo"
},
{
"path": "resources/full.pug",
"chars": 369,
"preview": "doctype html\nhtml\n head\n title Buttercup\n meta(charset=\"utf-8\")\n link(rel=\"icon\", type=\"image/pn"
},
{
"path": "resources/manifest.v2.json",
"chars": 1173,
"preview": "{\n \"manifest_version\": 2,\n\n \"name\": \"Buttercup\",\n \"description\": \"Browser extension for Buttercup, the secure a"
},
{
"path": "resources/manifest.v3.json",
"chars": 1224,
"preview": "{\n \"manifest_version\": 3,\n\n \"name\": \"Buttercup\",\n \"description\": \"Browser extension for Buttercup, the secure a"
},
{
"path": "resources/popup.pug",
"chars": 378,
"preview": "doctype html\nhtml\n head\n title Menu ⋅ Buttercup\n meta(charset=\"utf-8\")\n link(rel=\"icon\", type=\"i"
},
{
"path": "scripts/version.js",
"chars": 745,
"preview": "import { readFileSync, writeFileSync } from \"fs\";\nimport { resolve } from \"path\";\nimport { fileURLToPath } from \"url\";\n\n"
},
{
"path": "source/background/index.ts",
"chars": 183,
"preview": "import { initialise } from \"./services/init.js\";\nimport { log } from \"./services/log.js\";\n\ninitialise().catch((err) => {"
},
{
"path": "source/background/library/domain.ts",
"chars": 242,
"preview": "import { UsedCredentials } from \"../types.js\";\n\nexport function extractDomainFromCredentials(credentials: UsedCredential"
},
{
"path": "source/background/services/autoLogin.ts",
"chars": 920,
"preview": "import { SearchResult } from \"buttercup\";\nimport ExpiryMap from \"expiry-map\";\n\ninterface RegisteredItem {\n entry: Sea"
},
{
"path": "source/background/services/config.ts",
"chars": 1539,
"preview": "import { getSyncValue, setSyncValue } from \"./storage.js\";\nimport { Configuration, InputButtonType, SyncStorageItem } fr"
},
{
"path": "source/background/services/crypto.ts",
"chars": 1020,
"preview": "import { EncryptionAlgorithm, createAdapter } from \"iocane\";\nimport { deriveSecretKey, importECDHKey } from \"./cryptoKey"
},
{
"path": "source/background/services/cryptoKeys.ts",
"chars": 3157,
"preview": "import { ulid } from \"ulidx\";\nimport { log } from \"./log.js\";\nimport { getLocalValue, setLocalValue } from \"./storage.js"
},
{
"path": "source/background/services/desktop/actions.ts",
"chars": 6314,
"preview": "import { Layerr } from \"layerr\";\nimport { EntryID, EntryType, GroupID, SearchResult, VaultFacade, VaultSourceID } from \""
},
{
"path": "source/background/services/desktop/header.ts",
"chars": 544,
"preview": "import { Layerr } from \"layerr\";\nimport { LocalStorageItem } from \"../../types.js\";\nimport { getLocalValue } from \"../st"
},
{
"path": "source/background/services/desktop/request.ts",
"chars": 4625,
"preview": "import { Layerr } from \"layerr\";\nimport joinURL from \"url-join\";\nimport { DESKTOP_API_PORT } from \"../../../shared/symbo"
},
{
"path": "source/background/services/disabledDomains.ts",
"chars": 877,
"preview": "import { getSyncValue, setSyncValue } from \"./storage.js\";\nimport { SyncStorageItem } from \"../types.js\";\n\nexport async "
},
{
"path": "source/background/services/entry.ts",
"chars": 431,
"preview": "import { SearchResult } from \"buttercup\";\nimport { createNewTab } from \"../../shared/library/extension.js\";\nimport { for"
},
{
"path": "source/background/services/init.ts",
"chars": 1617,
"preview": "import { EventEmitter } from \"eventemitter3\";\nimport { log } from \"./log.js\";\nimport { initialise as initialiseMessaging"
},
{
"path": "source/background/services/log.ts",
"chars": 307,
"preview": "import { createLog } from \"../../shared/library/log.js\";\n\nconst LOG_NAME = \"buttercup:browser:background\";\n\nlet __logger"
},
{
"path": "source/background/services/loginMemory.ts",
"chars": 3587,
"preview": "import ExpiryMap from \"expiry-map\";\nimport { searchEntriesByTerm } from \"./desktop/actions.js\";\nimport { domainsReferToS"
},
{
"path": "source/background/services/messaging.ts",
"chars": 15060,
"preview": "import { Layerr } from \"layerr\";\nimport { EntryType, EntryURLType, VaultSourceID, VaultSourceStatus, getEntryURLs } from"
},
{
"path": "source/background/services/notifications.ts",
"chars": 1313,
"preview": "import { getSyncValue, setSyncValue } from \"./storage.js\";\nimport { SyncStorageItem } from \"../types.js\";\nimport { NOTIF"
},
{
"path": "source/background/services/recents.ts",
"chars": 2784,
"preview": "import { EntryID, VaultSourceID } from \"buttercup\";\nimport { ChannelQueue, TaskPriority } from \"@buttercup/channel-queue"
},
{
"path": "source/background/services/storage/BrowserStorageInterface.ts",
"chars": 1344,
"preview": "import { StorageInterface } from \"buttercup\";\nimport { getExtensionAPI } from \"../../../shared/extension.js\";\n\nexport fu"
},
{
"path": "source/background/services/storage.ts",
"chars": 2682,
"preview": "import { log } from \"./log.js\";\nimport { BrowserStorageInterface, getNonSyncStorage, getSyncStorage } from \"./storage/Br"
},
{
"path": "source/background/services/tabs.ts",
"chars": 719,
"preview": "import { getExtensionAPI } from \"../../shared/extension.js\";\nimport { TabEvent } from \"../types.js\";\n\nexport async funct"
},
{
"path": "source/background/types.ts",
"chars": 441,
"preview": "export * from \"../shared/types.js\";\n\nexport enum LocalStorageItem {\n APIClientID = \"bcup:api:clientID\",\n APIPrivat"
},
{
"path": "source/full/applications/full.tsx",
"chars": 352,
"preview": "import React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport { App } from \"../components/App.js\";\nimport { initia"
},
{
"path": "source/full/components/App.tsx",
"chars": 1730,
"preview": "import React from \"react\";\nimport {\n createHashRouter,\n RouterProvider\n} from \"react-router-dom\";\nimport { ThemePr"
},
{
"path": "source/full/components/Layout.tsx",
"chars": 1437,
"preview": "import React from \"react\";\nimport styled from \"styled-components\";\nimport { Divider } from \"@blueprintjs/core\";\nimport {"
},
{
"path": "source/full/components/pages/AttributionsPage.tsx",
"chars": 1204,
"preview": "import React from \"react\";\nimport styled from \"styled-components\";\nimport { Layout } from \"../Layout.js\";\nimport { t } f"
},
{
"path": "source/full/components/pages/DisabledDomainsPage.tsx",
"chars": 5412,
"preview": "import React, { Fragment, useCallback, useState } from \"react\";\nimport cn from \"classnames\";\nimport styled from \"styled-"
},
{
"path": "source/full/components/pages/NotificationsPage.tsx",
"chars": 2662,
"preview": "import React, { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useLoaderData } from \"react-router-do"
},
{
"path": "source/full/components/pages/connect/CodeInput.tsx",
"chars": 1671,
"preview": "import React, { KeyboardEvent, useCallback } from \"react\";\nimport { Button, ControlGroup, InputGroup, Intent } from \"@bl"
},
{
"path": "source/full/components/pages/connect/ConnectPage.tsx",
"chars": 2070,
"preview": "import React, { useCallback, useState } from \"react\";\nimport { Intent } from \"@blueprintjs/core\";\nimport { Layout } from"
},
{
"path": "source/full/components/pages/connect/index.tsx",
"chars": 305,
"preview": "import React from \"react\";\nimport { ConnectPage as InternalPage } from \"./ConnectPage.js\";\nimport { ErrorBoundary } from"
},
{
"path": "source/full/components/pages/saveCredentials/CredentialsSaver.tsx",
"chars": 9878,
"preview": "import React, { Fragment, useCallback, useMemo, useState } from \"react\";\nimport { ITreeNode, NonIdealState, Tree, TreeNo"
},
{
"path": "source/full/components/pages/saveCredentials/CredentialsSelector.tsx",
"chars": 3580,
"preview": "import React, { Fragment, useCallback, useMemo } from \"react\";\nimport styled from \"styled-components\";\nimport { Card, El"
},
{
"path": "source/full/components/pages/saveCredentials/NewEntrySavePrompt.tsx",
"chars": 7175,
"preview": "import React, { Fragment, useCallback, useEffect, useState } from \"react\";\nimport { Button, Classes, Colors, FormGroup, "
},
{
"path": "source/full/components/pages/saveCredentials/index.tsx",
"chars": 4072,
"preview": "import React, { Fragment, useCallback, useState } from \"react\";\nimport { Intent, Tab, Tabs } from \"@blueprintjs/core\";\ni"
},
{
"path": "source/full/hooks/credentials.ts",
"chars": 419,
"preview": "import { useAsync } from \"../../shared/hooks/async.js\";\nimport { getCredentials } from \"../services/credentials.js\";\nimp"
},
{
"path": "source/full/hooks/disabledDomains.ts",
"chars": 447,
"preview": "import { useAsyncWithTimer } from \"../../shared/hooks/async.js\";\nimport { getDisabledDomains } from \"../services/disable"
},
{
"path": "source/full/hooks/document.ts",
"chars": 524,
"preview": "import { useEffect } from \"react\";\n\nconst TITLE_SEPARATOR = \"⋅\";\n\nlet __originalTitle: string | null = null;\n\nexport fun"
},
{
"path": "source/full/hooks/vaultContents.ts",
"chars": 1379,
"preview": "import { useAsync } from \"../../shared/hooks/async.js\";\nimport { getVaultsTree } from \"../services/vaults.js\";\nimport { "
},
{
"path": "source/full/index.pug",
"chars": 319,
"preview": "doctype html\nhtml\n head\n title Buttercup\n meta(charset=\"utf-8\")\n link(rel=\"icon\", type=\"image/pn"
},
{
"path": "source/full/services/credentials.ts",
"chars": 1476,
"preview": "import { Layerr } from \"layerr\";\nimport { EntryType } from \"buttercup\";\nimport { sendBackgroundMessage } from \"../../sha"
},
{
"path": "source/full/services/disabledDomains.ts",
"chars": 798,
"preview": "import { Layerr } from \"layerr\";\nimport { sendBackgroundMessage } from \"../../shared/services/messaging.js\";\nimport { Ba"
},
{
"path": "source/full/services/init.ts",
"chars": 309,
"preview": "import { initialise as initialiseI18n } from \"../../shared/i18n/trans.js\";\nimport { getLanguage } from \"../../shared/lib"
},
{
"path": "source/full/services/log.ts",
"chars": 301,
"preview": "import { createLog } from \"../../shared/library/log.js\";\n\nconst LOG_NAME = \"buttercup:browser:page\";\n\nlet __logger: Retu"
},
{
"path": "source/full/services/notifications.ts",
"chars": 510,
"preview": "import { Layerr } from \"layerr\";\nimport { sendBackgroundMessage } from \"../../shared/services/messaging.js\";\nimport { Ba"
},
{
"path": "source/full/services/vaults.ts",
"chars": 561,
"preview": "import { Layerr } from \"layerr\";\nimport { sendBackgroundMessage } from \"../../shared/services/messaging.js\";\nimport { Ba"
},
{
"path": "source/full/styles/full.sass",
"chars": 182,
"preview": "html, body\n margin: 0\n padding: 0\n height: 100%\n\n#root\n display: flex\n justify-content: center\n min-he"
},
{
"path": "source/full/types.ts",
"chars": 36,
"preview": "export * from \"../shared/types.js\";\n"
},
{
"path": "source/popup/applications/popup.tsx",
"chars": 351,
"preview": "import React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport { App } from \"../components/App.js\";\nimport { initia"
},
{
"path": "source/popup/components/App.tsx",
"chars": 3511,
"preview": "import React, { useEffect, useState } from \"react\";\nimport {\n createHashRouter,\n RouterProvider,\n useLoaderData"
},
{
"path": "source/popup/components/contexts/LaunchContext.tsx",
"chars": 889,
"preview": "import React, { ReactNode, createContext } from \"react\";\n\ninterface LaunchContextProps {\n children: ReactNode;\n fo"
},
{
"path": "source/popup/components/entries/EntryInfoDialog.tsx",
"chars": 4363,
"preview": "import React, { useCallback, useMemo } from \"react\";\nimport { Button, Classes, Dialog, DialogBody, InputGroup, Intent } "
},
{
"path": "source/popup/components/entries/EntryItem.tsx",
"chars": 4470,
"preview": "import React, { MouseEvent, useCallback, useContext, useMemo } from \"react\";\nimport styled from \"styled-components\";\nimp"
},
{
"path": "source/popup/components/entries/EntryItemList.tsx",
"chars": 3031,
"preview": "import React, { Fragment } from \"react\";\nimport styled from \"styled-components\";\nimport { SearchResult } from \"buttercup"
},
{
"path": "source/popup/components/navigation/Navigator.tsx",
"chars": 7185,
"preview": "import React, { useCallback, useState } from \"react\";\nimport styled from \"styled-components\";\nimport { Classes, Divider,"
},
{
"path": "source/popup/components/otps/OTPItem.tsx",
"chars": 3774,
"preview": "import React, { useCallback, useMemo } from \"react\";\nimport styled from \"styled-components\";\nimport cn from \"classnames\""
},
{
"path": "source/popup/components/otps/OTPItemList.tsx",
"chars": 1340,
"preview": "import React, { Fragment } from \"react\";\nimport styled from \"styled-components\";\nimport { Divider } from \"@blueprintjs/c"
},
{
"path": "source/popup/components/pages/AboutPage.tsx",
"chars": 3312,
"preview": "import React, { MouseEvent, useCallback } from \"react\";\nimport styled from \"styled-components\";\nimport { Button, Callout"
},
{
"path": "source/popup/components/pages/EntriesPage.tsx",
"chars": 7777,
"preview": "import React, { useCallback, useContext, useMemo, useState } from \"react\";\nimport styled from \"styled-components\";\nimpor"
},
{
"path": "source/popup/components/pages/OTPsPage.tsx",
"chars": 5212,
"preview": "import React, { useCallback, useContext, useMemo } from \"react\";\nimport styled from \"styled-components\";\nimport { Button"
},
{
"path": "source/popup/components/pages/SaveDialogPage.tsx",
"chars": 7077,
"preview": "import React, { useCallback, useContext, useEffect, useRef, useState } from \"react\";\nimport styled from \"styled-componen"
},
{
"path": "source/popup/components/pages/SettingsPage.tsx",
"chars": 8474,
"preview": "import React, { Fragment, useCallback, useMemo, useState } from \"react\";\nimport styled from \"styled-components\";\nimport "
},
{
"path": "source/popup/components/pages/VaultsPage.tsx",
"chars": 3366,
"preview": "import React, { useCallback } from \"react\";\nimport styled from \"styled-components\";\nimport { Button, ButtonGroup, Intent"
},
{
"path": "source/popup/components/vaults/VaultItem.tsx",
"chars": 4015,
"preview": "import React, { useCallback } from \"react\";\nimport styled from \"styled-components\";\nimport cn from \"classnames\";\nimport "
},
{
"path": "source/popup/components/vaults/VaultItemList.tsx",
"chars": 2547,
"preview": "import React, { Fragment, useCallback } from \"react\";\nimport styled from \"styled-components\";\nimport { Divider, Intent }"
},
{
"path": "source/popup/components/vaults/VaultStateIndicator.tsx",
"chars": 863,
"preview": "import React, { useMemo } from \"react\";\nimport { VaultSourceStatus } from \"buttercup\";\nimport { Colors, Icon, IconName }"
},
{
"path": "source/popup/hooks/credentials.ts",
"chars": 1532,
"preview": "import { Layerr } from \"layerr\";\nimport { AsyncResult, useAsync } from \"../../shared/hooks/async.js\";\nimport { sendBackg"
},
{
"path": "source/popup/hooks/desktop.ts",
"chars": 5879,
"preview": "import { Intent } from \"@blueprintjs/core\";\nimport { SearchResult } from \"buttercup\";\nimport { useCallback, useEffect, u"
},
{
"path": "source/popup/hooks/document.ts",
"chars": 288,
"preview": "import { useEffect } from \"react\";\n\nexport function useBodyClass(className: string): void {\n const body = document.bo"
},
{
"path": "source/popup/hooks/otp.ts",
"chars": 3770,
"preview": "import * as OTPAuth from \"otpauth\";\nimport { Layerr } from \"layerr\";\nimport { useEffect, useMemo, useState } from \"react"
},
{
"path": "source/popup/hooks/tab.ts",
"chars": 511,
"preview": "import { useMemo } from \"react\";\nimport { useAsync } from \"../../shared/hooks/async.js\";\nimport { getCurrentTab } from \""
},
{
"path": "source/popup/index.pug",
"chars": 328,
"preview": "doctype html\nhtml\n head\n title Menu ⋅ Buttercup\n meta(charset=\"utf-8\")\n link(rel=\"icon\", type=\"i"
},
{
"path": "source/popup/queries/desktop.ts",
"chars": 3606,
"preview": "import { SearchResult, VaultSourceID } from \"buttercup\";\nimport { Layerr } from \"layerr\";\nimport { sendBackgroundMessage"
},
{
"path": "source/popup/queries/disabledDomains.ts",
"chars": 506,
"preview": "import { Layerr } from \"layerr\";\nimport { sendBackgroundMessage } from \"../../shared/services/messaging.js\";\nimport { Ba"
},
{
"path": "source/popup/queries/loginMemory.ts",
"chars": 504,
"preview": "import { Layerr } from \"layerr\";\nimport { sendBackgroundMessage } from \"../../shared/services/messaging.js\";\nimport { Ba"
},
{
"path": "source/popup/services/clipboard.ts",
"chars": 875,
"preview": "import { Layerr } from \"layerr\";\n\nexport async function copyTextToClipboard(text: string): Promise<void> {\n // Naviga"
},
{
"path": "source/popup/services/entry.ts",
"chars": 573,
"preview": "import { SearchResult } from \"buttercup\";\nimport { Layerr } from \"layerr\";\nimport { sendBackgroundMessage } from \"../../"
},
{
"path": "source/popup/services/init.ts",
"chars": 334,
"preview": "import { initialise as initialiseI18n } from \"../../shared/i18n/trans.js\";\nimport { getLanguage } from \"../../shared/lib"
},
{
"path": "source/popup/services/log.ts",
"chars": 288,
"preview": "import { Logger, createLog } from \"../../shared/library/log.js\";\n\nconst LOG_NAME = \"buttercup:browser:popup\";\n\nlet __log"
},
{
"path": "source/popup/services/recents.ts",
"chars": 510,
"preview": "import { SearchResult } from \"buttercup\";\nimport { Layerr } from \"layerr\";\nimport { sendBackgroundMessage } from \"../../"
},
{
"path": "source/popup/services/reset.ts",
"chars": 431,
"preview": "import { Layerr } from \"layerr\";\nimport { sendBackgroundMessage } from \"../../shared/services/messaging.js\";\nimport { Ba"
},
{
"path": "source/popup/services/tab.ts",
"chars": 1560,
"preview": "import { SearchResult } from \"buttercup\";\nimport { Intent } from \"@blueprintjs/core\";\nimport { otpURIToDigits } from \".."
},
{
"path": "source/popup/state/app.ts",
"chars": 188,
"preview": "import { createStateObject } from \"obstate\";\nimport { PopupPage } from \"../types.js\";\n\nexport const APP_STATE = createSt"
},
{
"path": "source/popup/styles/popup.sass",
"chars": 391,
"preview": "html\n margin: 0\n height: 100%\n\nbody\n width: 350px\n height: 100%\n padding: 0px\n margin: 0\n overflow:"
},
{
"path": "source/popup/types.ts",
"chars": 185,
"preview": "export enum DesktopConnectionState {\n Connected = \"connected\",\n Error = \"error\",\n NotConnected = \"notConnected\""
},
{
"path": "source/shared/components/ConfirmDialog.tsx",
"chars": 1708,
"preview": "import React, { Fragment, useCallback } from \"react\";\nimport { IconName } from \"@blueprintjs/icons\";\nimport { ChildEleme"
},
{
"path": "source/shared/components/ErrorBoundary.tsx",
"chars": 1578,
"preview": "import React, { Component } from \"react\";\nimport styled from \"styled-components\";\nimport { Callout, Intent } from \"@blue"
},
{
"path": "source/shared/components/ErrorMessage.tsx",
"chars": 672,
"preview": "import React from \"react\";\nimport { Callout, Intent } from \"@blueprintjs/core\";\nimport styled from \"styled-components\";\n"
},
{
"path": "source/shared/components/RouteError.tsx",
"chars": 1265,
"preview": "import React from \"react\";\nimport styled from \"styled-components\";\nimport { useRouteError } from \"react-router-dom\";\nimp"
},
{
"path": "source/shared/components/ThemeProvider.tsx",
"chars": 603,
"preview": "import React from \"react\";\nimport { ThemeProvider as StyledThemeProvider } from \"styled-components\";\nimport themesIntern"
},
{
"path": "source/shared/components/loading/BusyLoader.tsx",
"chars": 1074,
"preview": "import React from \"react\";\nimport { Classes, Overlay, H4, Spinner, Text } from \"@blueprintjs/core\";\nimport styled from \""
},
{
"path": "source/shared/extension.ts",
"chars": 159,
"preview": "export function getExtensionAPI(): typeof chrome {\n if (BROWSER === \"firefox\") {\n return browser;\n }\n re"
},
{
"path": "source/shared/hooks/async.ts",
"chars": 4422,
"preview": "import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { DependencyList } from \"react\";\n"
},
{
"path": "source/shared/hooks/config.ts",
"chars": 1139,
"preview": "import { useCallback, useState } from \"react\";\nimport { useAsync } from \"./async.js\";\nimport { getConfig } from \"../quer"
},
{
"path": "source/shared/hooks/global.ts",
"chars": 1219,
"preview": "import EventEmitter from \"eventemitter3\";\nimport { useCallback, useEffect, useState } from \"react\";\n\ninterface Globals {"
},
{
"path": "source/shared/hooks/theme.ts",
"chars": 1014,
"preview": "import { useEffect } from \"react\";\nimport { useConfig } from \"./config.js\";\nimport { Classes } from \"@blueprintjs/core\";"
},
{
"path": "source/shared/hooks/timer.ts",
"chars": 311,
"preview": "import { useEffect } from \"react\";\nimport { DependencyList } from \"react\";\n\nexport function useTimer(callback: () => voi"
},
{
"path": "source/shared/i18n/trans.ts",
"chars": 1124,
"preview": "import i18next, { TOptions } from \"i18next\";\n\nimport en from \"./translations/en.json\";\nimport nl from \"./translations/nl"
},
{
"path": "source/shared/i18n/translations/en.json",
"chars": 13509,
"preview": "{\n \"_\": \"English (UK)\",\n \"about\": {\n \"attributions\": \"Attributions\",\n \"info\": {\n \"build-d"
},
{
"path": "source/shared/i18n/translations/nl.json",
"chars": 14605,
"preview": "{\n \"_\": \"Nederlands (NL)\",\n \"about\": {\n \"attributions\": \"Toeschrijvingen\",\n \"info\": {\n \"b"
},
{
"path": "source/shared/library/buffer.ts",
"chars": 755,
"preview": "export function arrayBufferToHex(buffer: ArrayBuffer): string {\n return [...new Uint8Array(buffer)].map((x) => x.toSt"
},
{
"path": "source/shared/library/clone.ts",
"chars": 963,
"preview": "export function naiveClone<T extends Array<any> | Record<string, any>>(item: T): T {\n if (Array.isArray(item)) {\n "
},
{
"path": "source/shared/library/domain.ts",
"chars": 1243,
"preview": "import { EntryURLType, PropertyKeyValueObject, getEntryURLs } from \"buttercup\";\nimport { ParseResultListed, ParseResultT"
},
{
"path": "source/shared/library/error.ts",
"chars": 786,
"preview": "import { isError, Layerr } from \"layerr\";\nimport { t } from \"../i18n/trans.js\";\n\nexport function errorToString(error: Er"
},
{
"path": "source/shared/library/extension.ts",
"chars": 1537,
"preview": "import { getExtensionAPI } from \"../extension.js\";\n\nconst NOOP = () => {};\n\nexport async function createNewTab(url: stri"
},
{
"path": "source/shared/library/i18n.ts",
"chars": 190,
"preview": "export function getLanguage(/*preferences: Preferences, locale: string*/): string {\n // return preferences.language |"
},
{
"path": "source/shared/library/log.ts",
"chars": 305,
"preview": "import { createLog as createLogger, toggleContext } from \"gle\";\n\nexport type Logger = ReturnType<typeof createLog>;\n\nexp"
},
{
"path": "source/shared/library/otp.ts",
"chars": 936,
"preview": "import { SearchResult } from \"buttercup\";\nimport { Layerr } from \"layerr\";\nimport * as OTPAuth from \"otpauth\";\n\nfunction"
},
{
"path": "source/shared/library/url.ts",
"chars": 241,
"preview": "export function formatURL(base: string): string {\n if (/^\\d+\\.\\d+\\.\\d+\\.\\d+/.test(base)) {\n return `http://${b"
},
{
"path": "source/shared/library/vaultTypes.ts",
"chars": 964,
"preview": "import { VaultType } from \"../types.js\";\nimport VAULT_TYPE_IMAGE_DROPBOX from \"../../../resources/providers/dropbox-256."
},
{
"path": "source/shared/library/version.ts",
"chars": 144,
"preview": "// Do not edit this file - it is generated automatically at build time\n\nexport const BUILD_DATE = \"2024-04-09\";\nexport c"
},
{
"path": "source/shared/notifications/index.ts",
"chars": 289,
"preview": "import { TITLE as WelcomeV3Title, Page as WelcomeV3Page } from \"./pages/WelcomeV3.jsx\";\n\nexport const NOTIFICATIONS: Rec"
},
{
"path": "source/shared/notifications/pages/WelcomeV3.tsx",
"chars": 655,
"preview": "import { Fragment } from \"react\";\nimport { t } from \"../../i18n/trans.js\";\n\nexport const TITLE = \"notifications.page.wel"
},
{
"path": "source/shared/queries/config.ts",
"chars": 1108,
"preview": "import { Layerr } from \"layerr\";\nimport { sendBackgroundMessage } from \"../services/messaging.js\";\nimport { BackgroundMe"
},
{
"path": "source/shared/services/messaging.ts",
"chars": 950,
"preview": "import { getExtensionAPI } from \"../extension.js\";\nimport { stringToError } from \"../library/error.js\";\nimport { MESSAGE"
},
{
"path": "source/shared/services/notifications.ts",
"chars": 221,
"preview": "import { Position, Toaster, ToasterInstance } from \"@blueprintjs/core\";\n\nconst __toaster = Toaster.create({\n position"
},
{
"path": "source/shared/styles/base.sass",
"chars": 722,
"preview": "@import \"~@blueprintjs/core/lib/css/blueprint.css\"\n@import \"~@blueprintjs/icons/lib/css/blueprint-icons.css\"\n@import \"~@"
},
{
"path": "source/shared/styles/fonts.sass",
"chars": 320,
"preview": "@font-face\n font-family: \"OpenSans\"\n src: url(\"../../../resources/OpenSans-Regular.woff2\") format(\"woff2\")\n fon"
},
{
"path": "source/shared/symbols.ts",
"chars": 245,
"preview": "export const API_KEY_ALGO = \"ECDH\";\nexport const API_KEY_CURVE = \"P-256\";\n\nexport const BRAND_COLOUR = \"#00B7AC\";\nexport"
},
{
"path": "source/shared/themes.ts",
"chars": 2941,
"preview": "import { Colors } from \"@blueprintjs/core\";\n\nexport default {\n dark: {\n backgroundColor: Colors.DARK_GRAY4,\n "
},
{
"path": "source/shared/types.ts",
"chars": 5181,
"preview": "import {\n EntryID,\n EntryType,\n GroupID,\n SearchResult,\n VaultFacade,\n VaultFormatID,\n VaultSourceI"
},
{
"path": "source/tab/index.ts",
"chars": 282,
"preview": "import { FRAME } from \"./state/frame.js\";\nimport { initialise } from \"./services/init.js\";\nimport { log } from \"./servic"
},
{
"path": "source/tab/library/disable.ts",
"chars": 145,
"preview": "export function itemIsIgnored(element: HTMLElement): boolean {\n return element.matches(\"[data-bcupignore=true] *, [da"
},
{
"path": "source/tab/library/dismount.ts",
"chars": 867,
"preview": "export function onElementDismount(el: HTMLElement, callback: () => void): void {\n let active = true,\n timer: R"
},
{
"path": "source/tab/library/frames.ts",
"chars": 208,
"preview": "export function findIframeForWindow(url: string): HTMLIFrameElement | null {\n const iframes = [...document.getElement"
},
{
"path": "source/tab/library/page.ts",
"chars": 635,
"preview": "import { extractDomain } from \"../../shared/library/domain.js\";\n\nexport function currentDomainDisabled(\n disabledDoma"
},
{
"path": "source/tab/library/position.ts",
"chars": 657,
"preview": "import { ElementRect } from \"../types.js\";\n\nexport function getElementRectInDocument(el: HTMLElement): ElementRect {\n "
},
{
"path": "source/tab/library/resize.ts",
"chars": 1034,
"preview": "export function onBodyResize(\n callback: (newWidth: number, newHeight: number, lastWidth: number, lastHeight: number)"
},
{
"path": "source/tab/library/styles.ts",
"chars": 525,
"preview": "export const CLEAR_STYLES = {\n margin: \"0px\",\n minWidth: \"0px\",\n minHeight: \"0px\",\n padding: \"0px\"\n};\n\nexpor"
},
{
"path": "source/tab/library/zIndex.ts",
"chars": 426,
"preview": "export function findBestZIndexInContainer(parentElement: HTMLElement): number {\n let highest: number = 0;\n [...par"
},
{
"path": "source/tab/services/LoginTracker.ts",
"chars": 2474,
"preview": "import { ulid } from \"ulidx\";\nimport EventEmitter from \"eventemitter3\";\nimport { getCurrentTitle, getCurrentURL } from \""
},
{
"path": "source/tab/services/autoLogin.ts",
"chars": 1187,
"preview": "import { LoginTarget } from \"@buttercup/locust\";\nimport { SearchResult } from \"buttercup\";\nimport { Layerr } from \"layer"
},
{
"path": "source/tab/services/config.ts",
"chars": 564,
"preview": "import { Layerr } from \"layerr\";\nimport { sendBackgroundMessage } from \"../../shared/services/messaging.js\";\nimport { Ba"
},
{
"path": "source/tab/services/form.ts",
"chars": 4405,
"preview": "import { ulid } from \"ulidx\";\nimport { getElementRectInDocument, recalculateRectForIframe } from \"../library/position.js"
},
{
"path": "source/tab/services/formDetection.ts",
"chars": 2359,
"preview": "import { LoginTarget, LoginTargetFeature, getLoginTargets } from \"@buttercup/locust\";\nimport { attachLaunchButton } from"
},
{
"path": "source/tab/services/init.ts",
"chars": 355,
"preview": "import { initialise as initialiseMessaging } from \"./messaging.js\";\nimport { initialise as initialiseForms } from \"./for"
},
{
"path": "source/tab/services/log.ts",
"chars": 286,
"preview": "import { Logger, createLog } from \"../../shared/library/log.js\";\n\nconst LOG_NAME = \"buttercup:browser:tab\";\n\nlet __logge"
},
{
"path": "source/tab/services/logins/disabled.ts",
"chars": 489,
"preview": "import { Layerr } from \"layerr\";\nimport { sendBackgroundMessage } from \"../../../shared/services/messaging.js\";\nimport {"
},
{
"path": "source/tab/services/logins/saving.ts",
"chars": 1246,
"preview": "import { Layerr } from \"layerr\";\nimport { sendBackgroundMessage } from \"../../../shared/services/messaging.js\";\nimport {"
},
{
"path": "source/tab/services/logins/watcher.ts",
"chars": 3293,
"preview": "import { LoginTarget, LoginTargetFeature } from \"@buttercup/locust\";\nimport { onNavigate } from \"on-navigate\";\nimport { "
},
{
"path": "source/tab/services/messaging.ts",
"chars": 2101,
"preview": "import { FORM } from \"../state/form.js\";\nimport { fillFormDetails } from \"./form.js\";\nimport { closeDialog } from \"../ui"
},
{
"path": "source/tab/state/form.ts",
"chars": 333,
"preview": "import { createStateObject } from \"obstate\";\nimport { LoginTarget } from \"@buttercup/locust\";\n\nexport const FORM = creat"
},
{
"path": "source/tab/state/frame.ts",
"chars": 133,
"preview": "import { createStateObject } from \"obstate\";\n\nexport const FRAME = createStateObject<{\n isTop: boolean;\n}>({\n isTo"
},
{
"path": "source/tab/types.ts",
"chars": 354,
"preview": "import { InputType } from \"../shared/types.js\";\n\nexport * from \"../shared/types.js\";\n\nexport interface FrameEvent {\n "
},
{
"path": "source/tab/ui/launch.ts",
"chars": 6158,
"preview": "import { el, mount, setStyle } from \"redom\";\nimport { itemIsIgnored } from \"../library/disable.js\";\nimport { CLEAR_STYLE"
},
{
"path": "source/tab/ui/popup.ts",
"chars": 2915,
"preview": "import { el, mount, setStyle, unmount } from \"redom\";\nimport { getExtensionURL } from \"../../shared/library/extension.js"
},
{
"path": "source/tab/ui/saveDialog.ts",
"chars": 1834,
"preview": "import { el, mount, unmount } from \"redom\";\nimport { getExtensionURL } from \"../../shared/library/extension.js\";\nimport "
},
{
"path": "source/typings/assets.d.ts",
"chars": 299,
"preview": "declare module \"*.md\" {\n const value: any;\n export default value;\n}\ndeclare module \"*.png\" {\n const value: any;"
},
{
"path": "source/typings/globals.d.ts",
"chars": 88,
"preview": "declare var BROWSER: \"chrome\" | \"edge\" | \"firefox\";\ndeclare var browser: typeof chrome;\n"
},
{
"path": "tsconfig.json",
"chars": 513,
"preview": "{\n \"compilerOptions\": {\n \"allowSyntheticDefaultImports\": true,\n \"esModuleInterop\": true,\n \"jsx\":"
},
{
"path": "webpack.config.js",
"chars": 7420,
"preview": "import { writeFileSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport path from \"node:path\";\nimport "
}
]
About this extraction
This page contains the full source code of the buttercup/buttercup-browser-extension GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 165 files (334.7 KB), approximately 79.4k tokens, and a symbol index with 401 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.