Repository: jackluson/sync-your-cookie Branch: main Commit: faf31f67eafa Files: 204 Total size: 370.7 KB Directory structure: gitextract_67wvu_tp/ ├── .eslintignore ├── .eslintrc ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── auto_assign.yml │ ├── dependabot.yml │ ├── stale.yml │ └── workflows/ │ ├── auto-assign.yml │ ├── build-zip.yml │ ├── claude.yml │ └── greetings.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode/ │ └── settings.json ├── LICENSE ├── README.md ├── README_ZH.md ├── chrome-extension/ │ ├── lib/ │ │ └── background/ │ │ ├── badge.ts │ │ ├── contextMenu.ts │ │ ├── index.ts │ │ ├── listen.ts │ │ └── subscribe.ts │ ├── manifest.js │ ├── package.json │ ├── public/ │ │ ├── _locales/ │ │ │ └── en/ │ │ │ └── messages.json │ │ └── content.css │ ├── tsconfig.json │ ├── utils/ │ │ └── plugins/ │ │ └── make-manifest-plugin.ts │ └── vite.config.ts ├── googlef759ff453695209f.html ├── how-to-use.md ├── package.json ├── packages/ │ ├── dev-utils/ │ │ ├── index.ts │ │ ├── lib/ │ │ │ ├── logger.ts │ │ │ └── manifest-parser/ │ │ │ ├── impl.ts │ │ │ ├── index.ts │ │ │ └── type.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── hmr/ │ │ ├── index.ts │ │ ├── lib/ │ │ │ ├── constant.ts │ │ │ ├── debounce.ts │ │ │ ├── initClient.ts │ │ │ ├── initReloadServer.ts │ │ │ ├── injections/ │ │ │ │ ├── refresh.ts │ │ │ │ └── reload.ts │ │ │ ├── interpreter/ │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ └── plugins/ │ │ │ ├── index.ts │ │ │ ├── make-entry-point-plugin.ts │ │ │ └── watch-rebuild-plugin.ts │ │ ├── package.json │ │ ├── rollup.config.mjs │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── protobuf/ │ │ ├── README.md │ │ ├── index.ts │ │ ├── lib/ │ │ │ └── protobuf/ │ │ │ ├── code.ts │ │ │ ├── index.ts │ │ │ └── proto/ │ │ │ ├── cookie.d.ts │ │ │ └── cookie.js │ │ ├── package.json │ │ ├── proto/ │ │ │ └── cookie.proto │ │ ├── tsconfig.json │ │ ├── tsup.config.ts │ │ ├── utils/ │ │ │ ├── base64.ts │ │ │ ├── compress.ts │ │ │ ├── encryption.test.ts │ │ │ ├── encryption.ts │ │ │ └── index.ts │ │ └── vitest.config.ts │ ├── shared/ │ │ ├── README.md │ │ ├── index.ts │ │ ├── lib/ │ │ │ ├── Providers/ │ │ │ │ ├── ThemeProvider.tsx │ │ │ │ ├── hooks/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── useTheme.ts │ │ │ │ └── index.tsx │ │ │ ├── cloudflare/ │ │ │ │ ├── api.ts │ │ │ │ ├── enum.ts │ │ │ │ └── index.ts │ │ │ ├── cookie/ │ │ │ │ ├── index.ts │ │ │ │ ├── withCloudflare.ts │ │ │ │ └── withStorage.ts │ │ │ ├── github/ │ │ │ │ ├── api.ts │ │ │ │ └── index.ts │ │ │ ├── hoc/ │ │ │ │ ├── index.ts │ │ │ │ ├── withErrorBoundary.tsx │ │ │ │ └── withSuspense.tsx │ │ │ ├── hooks/ │ │ │ │ ├── index.ts │ │ │ │ ├── useCookieAction.ts │ │ │ │ ├── useStorage.ts │ │ │ │ └── useStorageSuspense.tsx │ │ │ ├── message/ │ │ │ │ └── index.ts │ │ │ └── utils/ │ │ │ └── index.ts │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── storage/ │ │ ├── index.ts │ │ ├── lib/ │ │ │ ├── accountStorage.ts │ │ │ ├── base.ts │ │ │ ├── cookieStorage.ts │ │ │ ├── domainConfigStorage.ts │ │ │ ├── domainStatusStorage.ts │ │ │ ├── index.ts │ │ │ ├── settingsStorage.ts │ │ │ └── themeStorage.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── tailwind-config/ │ │ ├── package.json │ │ └── tailwind.config.js │ ├── tsconfig/ │ │ ├── app.json │ │ ├── base.json │ │ ├── package.json │ │ └── utils.json │ ├── ui/ │ │ ├── components.json │ │ ├── globals.css │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── DateTable/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Image/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Spinner/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ThemeDropdown/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Tooltip/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.ts │ │ │ │ └── ui/ │ │ │ │ ├── alert-dialog.tsx │ │ │ │ ├── alert.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── input.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── select.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── table.tsx │ │ │ │ ├── toggle.tsx │ │ │ │ └── tooltip.tsx │ │ │ ├── index.ts │ │ │ └── libs/ │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ └── zipper/ │ ├── index.mts │ ├── lib/ │ │ └── index.ts │ ├── package.json │ └── tsconfig.json ├── pages/ │ ├── content/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── listener.ts │ │ │ ├── localStorage.ts │ │ │ └── observer.ts │ │ ├── tsconfig.json │ │ └── vite.config.mts │ ├── options/ │ │ ├── index.html │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── src/ │ │ │ ├── Options.tsx │ │ │ ├── components/ │ │ │ │ ├── SettingsPopover.tsx │ │ │ │ └── StorageSelect.tsx │ │ │ ├── hooks/ │ │ │ │ └── useGithub.ts │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── tailwind.config.js │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── popup/ │ │ ├── index.html │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── src/ │ │ │ ├── Popup.tsx │ │ │ ├── components/ │ │ │ │ └── AutoSwtich/ │ │ │ │ └── index.tsx │ │ │ ├── hooks/ │ │ │ │ └── useDomainConfig.ts │ │ │ ├── index.css │ │ │ ├── index.tsx │ │ │ └── utils/ │ │ │ └── index.ts │ │ ├── tailwind.config.js │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── sidepanel/ │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── src/ │ │ ├── SidePanel.tsx │ │ ├── components/ │ │ │ └── CookieTable/ │ │ │ ├── SearchInput.tsx │ │ │ ├── hooks/ │ │ │ │ ├── useAction.ts │ │ │ │ ├── useCookieItem.ts │ │ │ │ └── useSelected.tsx │ │ │ └── index.tsx │ │ ├── index.css │ │ ├── index.html │ │ └── index.tsx │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-workspace.yaml ├── private-policy.md └── turbo.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ dist node_modules tailwind.config.js ================================================ FILE: .eslintrc ================================================ { "env": { "browser": true, "es6": true, "node": true }, "extends": [ "eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended", "plugin:import/recommended", "plugin:jsx-a11y/recommended", "prettier" ], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaFeatures": { "jsx": true }, "ecmaVersion": "latest", "sourceType": "module" }, "plugins": ["react", "@typescript-eslint", "react-hooks", "import", "jsx-a11y", "prettier"], "settings": { "react": { "version": "detect" } }, "rules": { "prettier/prettier": "error", "react/react-in-jsx-scope": "off", "import/no-unresolved": "off", "jsx-a11y/click-events-have-key-events": "warn" }, "globals": { "chrome": "readonly" }, "ignorePatterns": ["watch.js", "dist/**"] } ================================================ FILE: .github/CODEOWNERS ================================================ * @jackluson ================================================ FILE: .github/FUNDING.yml ================================================ github: jackluson ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: jackluson --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. Mac, Window, Linux] - Browser [e.g. chrome, firefox] - Node Version [e.g. 18.12.0] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: jackluson --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/auto_assign.yml ================================================ # Set to true to add reviewers to pull requests addReviewers: true # Set to true to add assignees to pull requests addAssignees: author # A list of reviewers to be added to pull requests (GitHub user name) reviewers: - jackluson # A number of reviewers added to the pull request # Set 0 to add all the reviewers (default: 0) numberOfReviewers: 0 # A list of assignees, overrides reviewers if set # assignees: # - assigneeA # A number of assignees to add to the pull request # Set to 0 to add all of the assignees. # Uses numberOfReviewers if unset. # numberOfAssignees: 2 # A list of keywords to be skipped the process that add reviewers if pull requests include it # skipKeywords: # - wip filterLabels: exclude: - dependencies ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "npm" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" ================================================ FILE: .github/stale.yml ================================================ # Number of days of inactivity before an Issue or Pull Request becomes stale daysUntilStale: 90 # Number of days of inactivity before a stale Issue or Pull Request is closed daysUntilClose: 30 # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable exemptLabels: - pinned - security # Label to use when marking as stale staleLabel: stale # Comment to post when marking as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when removing the stale label. Set to `false` to disable unmarkComment: false # Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable closeComment: true # Limit to only `issues` or `pulls` only: issues ================================================ FILE: .github/workflows/auto-assign.yml ================================================ name: 'Auto Assign' on: pull_request: types: [opened, ready_for_review] jobs: add-reviews: runs-on: ubuntu-latest steps: - uses: kentaro-m/auto-assign-action@v1.2.5 with: configuration-path: '.github/auto_assign.yml' ================================================ FILE: .github/workflows/build-zip.yml ================================================ name: Build And Upload Extension Zip Via Artifact on: push: branches: [ main ] pull_request: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version-file: ".nvmrc" - uses: actions/cache@v3 with: path: node_modules key: ${{ runner.OS }}-build-${{ hashFiles('**/pnpm-lock.yaml') }} - uses: pnpm/action-setup@v4 - run: pnpm install --frozen-lockfile - run: pnpm build - uses: actions/upload-artifact@v4 with: path: dist/* ================================================ FILE: .github/workflows/claude.yml ================================================ name: Claude Code Integration on: # 当 Issue 或 PR 中有新评论时触发(支持 @claude 唤醒) issue_comment: types: [created] # 当代码推送或创建拉取请求时自动触发功能 pull_request: types: [opened, synchronize, reopened] permissions: id-token: write # 必须添加这个权限,才能生成 ID Token 并消除报错 contents: write # 允许工作流读写代码(Claude Code 常用) pull-requests: write # 允许在 PR 下回复评论 issues: write # 如果你在 issue comment 里触发,需要这个 jobs: claude-code-action: runs-on: ubuntu-latest # 限制仅在需要时运行,节省资源 if: > github.event_name == 'pull_request' || (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) steps: # 1. 检出代码 - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 # 获取完整历史记录以便 Claude 分析上下文 # 2. 调用 Claude Code 官方 Action - name: Run Claude Code uses: anthropics/claude-code-action@v1 # 请以实际官方最新版本号为准 with: # 提供 GitHub token 以便 Claude 能回复评论和修改代码 github-token: ${{ secrets.GITHUB_TOKEN }} claude_code_oauth_token: ${{ secrets.ANTHROPIC_AUTH_TOKEN }} env: # 以下环境变量在 Github Secrets 中提前配置好 ANTHROPIC_BASE_URL: ${{ secrets.ANTHROPIC_BASE_URL }} # 例如填写 OpenRouter 地址 ANTHROPIC_AUTH_TOKEN: ${{ secrets.ANTHROPIC_AUTH_TOKEN }} # 填写中转 Key ANTHROPIC_API_KEY: "" # 必须为空 ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-5" ANTHROPIC_DEFAULT_HAIKU_MODEL: "glm-5" ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-5" ================================================ FILE: .github/workflows/greetings.yml ================================================ name: Greetings on: [pull_request_target, issues] jobs: greeting: runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - uses: actions/first-interaction@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} issue-message: 'Thank you for your contribution. We will check and reply to you as soon as possible.' pr-message: 'Thank you for your contribution. We will check and reply to you as soon as possible.' ================================================ FILE: .gitignore ================================================ # dependencies **/node_modules # testing **/coverage # build **/dist **/dist-zip **/build # env **/.env.local **/.env # etc .DS_Store .idea **/.turbo # compiled apps/chrome-extension/public/manifest.json ================================================ FILE: .npmrc ================================================ public-hoist-pattern[]=@testing-library/dom ================================================ FILE: .nvmrc ================================================ 20.13.1 ================================================ FILE: .prettierignore ================================================ dist node_modules proto .gitignore .github .eslintignore .husky .nvmrc .prettierignore LICENSE *.md pnpm-lock.yaml ================================================ FILE: .prettierrc ================================================ { "trailingComma": "all", "semi": true, "singleQuote": true, "arrowParens": "avoid", "printWidth": 120, "bracketSameLine": true, "htmlWhitespaceSensitivity": "strict" } ================================================ FILE: .vscode/settings.json ================================================ { "eslint.validate": [ "javascript", "javascriptreact", "typescript", "typescriptreact" ], "editor.codeActionsOnSave": { "source.formatDocument": "explicit", "source.fixAll.eslint": "explicit", "source.organizeImports": "explicit" }, "files.insertFinalNewline": true, "deepscan.enable": true } ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2024 Jack Lu 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: README.md ================================================
logo

Sync your cookie to Your Cloudflare or Github Gist

![](https://img.shields.io/badge/React-61DAFB?style=flat-square&logo=react&logoColor=black) ![](https://img.shields.io/badge/Typescript-3178C6?style=flat-square&logo=typescript&logoColor=white) ![](https://badges.aleen42.com/src/vitejs.svg) ![GitHub action badge](https://github.com/jackluson/sync-your-cookie/actions/workflows/build-zip.yml/badge.svg)
[English](./README.md) | [中文](./README_ZH.md) `Sync your cookie` is a chrome extension that helps you to sync your cookie to Cloudflare or Github Gist. It's a useful tool for web developers to share cookies between different devices. ### Install Chrome: [Sync Your Cookie](https://chromewebstore.google.com/detail/sync-your-cookie/bcegpckmgklcpcapnbigfdadedcneopf) Edge: [Sync Your Cookie](https://microsoftedge.microsoft.com/addons/detail/sync-your-cookie/ohlcghldllgnmkegocpcphdbbphikgfm) ### Features - Supports syncing cookies to Cloudflare or Github Gist (Include LocalStorage) - Supports configuring `Auto Merge` and `Auto Push` rules for different sites - Cookie data is transmitted via protobuf encoding - Provides a management panel to facilitate viewing, copying, and managing synchronized cookie data - Multi-account synchronization based on Storage-key ### Project Screenshots Account Settings Page account settings Cookie Sync Popup Page cookie sync popup Cookie Manager Sidebar Panel cookie manager sidebar panel Cookie Detail cookie manager sidebar panel LocalStorage Detail cookie manager sidebar panel Pushed Cookie on Github Gist Pushed Cookie on Github Gist Pushed Cookie on Cloudflare Pushed Cookie on Cloudflare ### Usage [How to use](./how-to-use.md) ### TODO - [x] Custom Save Configure - [x] Multi-account synchronization based on Storage-key - [x] Sync LocalStorage - [x] More Cloud Platform (First github gist) ### Privacy Policy Please refer to [Privacy Policy](./private-policy.md) for more information. ### Support If you find this project helpful, you can support the development by: - Starring the repository ⭐ - [Sponsoring via Ko-fi](https://ko-fi.com/jacklu) 💖 - Sponsor via Wechat
微信支付
- Sharing it with others 🚀 ================================================ FILE: README_ZH.md ================================================
logo

Sync your cookie to Cloudflare or Github Gist

[English](./README.md) | [中文](./README_ZH.md) `Sync your cookie` 是一个 Chrome 扩展程序,它可以帮助您将 Cookie 同步到 Cloudflare。它是一个有用的工具,用于在不同设备之间共享 Cookie, 免去了登录流程的烦恼,此外也提供了cookie管理面板查看,管理已经过同步的 cookie。 ### 安装 Chrome: [Sync Your Cookie](https://chromewebstore.google.com/detail/sync-your-cookie/bcegpckmgklcpcapnbigfdadedcneopf) Edge: [Sync Your Cookie](https://microsoftedge.microsoft.com/addons/detail/sync-your-cookie/ohlcghldllgnmkegocpcphdbbphikgfm) ### 功能 - 支持同步 Cookie 到 Cloudflare 或者 github Gist (支持LocalStorage) - 支持为不同站点配置`Auto Merge`和`Auto Push`规则 - Cookie数据经过 protobuf 编码传输 - 提供了一个管理面板,方便查看、复制、管理已经同步的 Cookie 数据 - 可配置多个Key,支持多账户同步 ### 项目截图 账号设置页面 account settings Cookie 同步页面 cookie sync popup Cookie 管理侧边栏面板 cookie manager sidebar panel Cookie 详情 cookie manager sidebar panel Github Gist上传的cookie Pushed Cookie on Github Gist Cloudflare上传的cookie Pushed Cookie on Cloudflare ### 使用指引 [How to use](./how-to-use.md) ### Privacy Policy Please refer to [Privacy Policy](./private-policy.md) for more information. ### 赞赏 如果你觉得这个项目对你有帮助,欢迎通过以下方式支持我: - 给项目点个 Star ⭐ - [通过 Ko-fi 赞助](https://ko-fi.com/jacklu) 💖 - 通过 微信支付 赞助
微信支付
- 分享给更多需要的人 🚀 ================================================ FILE: chrome-extension/lib/background/badge.ts ================================================ export function setBadge(text: string, color: string = '#7246e4') { chrome.action.setBadgeText({ text }); chrome.action.setBadgeBackgroundColor({ color }); } export function clearBadge() { chrome.action.setBadgeText({ text: '' }); } export function setPullingBadge() { setBadge('↓'); } export function setPushingBadge() { setBadge('↑'); } export function setPushingAndPullingBadge() { // badge('↓↑'); setBadge('⇅'); } ================================================ FILE: chrome-extension/lib/background/contextMenu.ts ================================================ let globalMenuId: number | string = ''; export const initContextMenu = () => { globalMenuId = chrome.contextMenus.create({ id: 'openSidePanel', title: 'Open Cookie Manager', contexts: ['all'], }); chrome.contextMenus.onClicked.addListener((info, tab) => { if (info.menuItemId === 'openSidePanel' && tab?.windowId) { // This will open the panel in all the pages on the current window. console.log('openSidePanel->tab', tab); chrome.sidePanel.open({ windowId: tab.windowId }); } }); }; export const removeContextMenu = () => { if (globalMenuId) { chrome.contextMenus.remove(globalMenuId); } }; ================================================ FILE: chrome-extension/lib/background/index.ts ================================================ // sort-imports-ignore import 'webextension-polyfill'; import { initGithubApi, pullAndSetCookies, pullCookies, pushMultipleDomainCookies } from '@sync-your-cookie/shared'; import { cookieStorage } from '@sync-your-cookie/storage/lib/cookieStorage'; import { domainConfigStorage } from '@sync-your-cookie/storage/lib/domainConfigStorage'; import { domainStatusStorage } from '@sync-your-cookie/storage/lib/domainStatusStorage'; import { settingsStorage } from '@sync-your-cookie/storage/lib/settingsStorage'; import { initContextMenu } from './contextMenu'; import { refreshListen } from './listen'; import { initSubscribe } from './subscribe'; const ping = () => { chrome.tabs.query({ active: true, currentWindow: true }, async function (tabs) { if (tabs.length === 0) { // const allOpendTabs = await chrome.tabs.query({}); console.log('No active tab found, try alternative way'); // reject({ isOk: false, msg: 'No active tab found' } as SendResponse); return; } chrome.tabs.sendMessage(tabs[0].id!, 'ping', function (result) { console.log('result->', result); }); }); // setTimeout(ping, 4000); }; const init = async () => { try { await refreshListen(); console.log('initListen finish'); await initSubscribe(); // await state reset finish console.log('initSubscribe finish'); await pullCookies(true); console.log('initPullCookies finish'); // ping(); } catch (error) { console.log('init-->error', error); } }; chrome.runtime.onInstalled.addListener(async () => { init(); console.log('onInstalled'); chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); const settingsSnapShot = await settingsStorage.get(); if (settingsSnapShot?.contextMenu) { initContextMenu(); } }); let delayTimer: NodeJS.Timeout | null = null; let checkDelayTimer: NodeJS.Timeout | null = null; let timeoutFlag = false; const changedDomainSet = new Set(); chrome.cookies.onChanged.addListener(async changeInfo => { const domainConfigSnapShot = await domainConfigStorage.getSnapshot(); const domain = changeInfo.cookie.domain; const domainMap = domainConfigSnapShot?.domainMap || {}; const removeHeadDomain = domain.startsWith('.') ? domain.slice(1) : domain; let flag = false; for (const key in domainMap) { if (key.endsWith(removeHeadDomain) && domainMap[key]?.autoPush) { flag = true; break; } } if (!flag) return; if (delayTimer && timeoutFlag) { return; } delayTimer && clearTimeout(delayTimer); changedDomainSet.add(removeHeadDomain); delayTimer = setTimeout(async () => { timeoutFlag = false; if (checkDelayTimer) { clearTimeout(checkDelayTimer); } const domainConfig = await domainConfigStorage.get(); // const pushDomainSet = new Set(); const pushDomainHostMap = new Map(); for (const domain of changedDomainSet) { for (const key in domainConfig.domainMap) { if (key.endsWith(domain) && domainConfig.domainMap[key]?.autoPush) { // pushDomainSet.add(domain); const existedHost = pushDomainHostMap.get(domain) || []; pushDomainHostMap.set(domain, [key, ...existedHost]); } } } const uploadDomainCookies = []; const cookieMap = await cookieStorage.getSnapshot(); const userAgent = navigator?.userAgent || ''; console.log('pushDomainHostMap', pushDomainHostMap); for (const domain of pushDomainHostMap.keys()) { const hosts = pushDomainHostMap.get(domain) || []; // const [domain] = await extractDomainAndPort(host); const cookies = await chrome.cookies.getAll({ domain: domain, }); for (const host of hosts) { uploadDomainCookies.push({ domain: host, cookies, localStorageItems: cookieMap?.domainCookieMap?.[host]?.localStorageItems || [], userAgent, }); } } if (uploadDomainCookies.length) { await pushMultipleDomainCookies(uploadDomainCookies); changedDomainSet.clear(); } }, 10000); if (!checkDelayTimer) { checkDelayTimer = setTimeout(() => { if (delayTimer) { console.info('checkDelayTimer timeout'); timeoutFlag = true; delayTimer = null; } checkDelayTimer = null; }, 30000); } }); let previousActiveTabList: chrome.tabs.Tab[] = []; chrome.tabs.onUpdated.addListener(async function (tabId, changeInfo, tab) { // 1. current tab not exist in the tabMap // read changeInfo data and do something with it (like read the url) if (changeInfo.status === 'loading' && changeInfo.url) { const domainConfig = await domainConfigStorage.get(); let pullDomain = ''; let needPull = false; for (const key in domainConfig.domainMap) { if (new URL(changeInfo.url).host.endsWith(key) && domainConfig.domainMap[key]?.autoPull) { needPull = true; pullDomain = key; break; // await pullCookies(); } } if (needPull) { const allOpendTabs = await chrome.tabs.query({}); const otherExistedTabs = allOpendTabs.filter(itemTab => tab.id !== itemTab.id); for (const itemTab of otherExistedTabs) { if (itemTab.url && new URL(itemTab.url).host === new URL(changeInfo.url).host) { needPull = false; break; } } } if (needPull) { for (const itemTab of previousActiveTabList) { if (itemTab.url && new URL(itemTab.url).host === new URL(changeInfo.url).host) { needPull = false; break; } } } if (needPull) { await pullAndSetCookies(changeInfo.url, pullDomain); } const allActiveTabs = await chrome.tabs.query({ active: true, }); previousActiveTabList = allActiveTabs; } }); // let previousUrl = ''; // chrome.webNavigation?.onBeforeNavigate.addListener(function (object) { // chrome.tabs.get(object.tabId, function (tab) { // previousUrl = tab.url || ''; // console.log('previousUrl', previousUrl); // }); // }); // chrome.tabs.onRemoved.addListener(async function (tabId, removeInfo) { // const allActiveTabs = await chrome.tabs.query({ // active: true, // }); // previousActiveTabList = allActiveTabs; // }); chrome.tabs.onActivated.addListener(async function () { const allActiveTabs = await chrome.tabs.query({ active: true, }); previousActiveTabList = allActiveTabs; console.log('refreshListen', previousActiveTabList); const domainStatus = await domainStatusStorage.get(); const settingsStorageInfo = await settingsStorage.get(); if (!domainStatus.pulling && !domainStatus.pushing && !settingsStorageInfo.localStorageGetting) { refreshListen(); } }); initGithubApi(true); ================================================ FILE: chrome-extension/lib/background/listen.ts ================================================ import { check, checkResponseAndCallback, CookieOperator, extractDomainAndPort, ICookie, Message, MessageType, pullAndSetCookies, PushCookieMessagePayload, pushCookies, removeCookieItem, removeCookies, sendGetLocalStorageMessage, SendResponse, } from '@sync-your-cookie/shared'; import { cookieStorage } from '@sync-your-cookie/storage/lib/cookieStorage'; import { settingsStorage } from '@sync-your-cookie/storage/lib/settingsStorage'; import { domainConfigStorage } from '@sync-your-cookie/storage/lib/domainConfigStorage'; import { domainStatusStorage } from '@sync-your-cookie/storage/lib/domainStatusStorage'; type HandleCallback = (response?: SendResponse) => void; const handlePush = async (payload: PushCookieMessagePayload, callback: HandleCallback) => { const { sourceUrl, host, favIconUrl } = payload || {}; const userAgent = navigator?.userAgent || ''; try { await check(); await domainConfigStorage.updateItem(host, { sourceUrl: sourceUrl, favIconUrl, }); await domainStatusStorage.updateItem(host, { pushing: true, }); const [domain, port, hostname] = await extractDomainAndPort(host); const condition = sourceUrl ? { url: sourceUrl } : { domain: domain }; const cookies = await chrome.cookies.getAll(condition); let localStorageItems: NonNullable[2]> = []; const includeLocalStorage = settingsStorage.getSnapshot()?.includeLocalStorage; if (includeLocalStorage) { try { const hostname = sourceUrl ? new URL(sourceUrl).hostname : host; localStorageItems = await sendGetLocalStorageMessage(hostname); } catch (error) { console.error('sendGetLocalStorageMessage error', error); const cookieMap = await cookieStorage.getSnapshot(); localStorageItems = cookieMap?.domainCookieMap?.[host]?.localStorageItems || []; } } else { const cookieMap = await cookieStorage.getSnapshot(); localStorageItems = cookieMap?.domainCookieMap?.[host]?.localStorageItems || []; } if (cookies?.length || localStorageItems.length) { const res = await pushCookies(host, cookies, localStorageItems, userAgent); checkResponseAndCallback(res, 'push', callback); } else { callback({ isOk: false, msg: 'no cookies and localStorageItems found', result: cookies }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { checkResponseAndCallback(err, 'push', callback); } finally { await domainStatusStorage.togglePushingState(host, false); } }; const handlePull = async (activeTabUrl: string, domain: string, isReload: boolean, callback: HandleCallback) => { try { await check(); await domainStatusStorage.togglePullingState(domain, true); const cookieMap = await pullAndSetCookies(activeTabUrl, domain, isReload); console.log('handlePull->cookieMap', cookieMap); callback({ isOk: true, msg: 'Pull success', result: cookieMap }); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { checkResponseAndCallback(err, 'pull', callback); } finally { await domainStatusStorage.togglePullingState(domain, false); } }; const handleRemove = async (domain: string, callback: HandleCallback) => { try { await check(); const res = await removeCookies(domain); checkResponseAndCallback(res, 'remove', callback); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { checkResponseAndCallback(err, 'remove', callback); // callback({ isOk: false, msg: (err as Error).message || 'remove fail, please try again ', result: err }); } }; const handleRemoveItem = async (domain: string, id: string, callback: HandleCallback) => { try { await check(); const res = await removeCookieItem(domain, id); checkResponseAndCallback(res, 'delete', callback); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { checkResponseAndCallback(err, 'delete', callback); } }; const handleEditItem = async (domain: string, oldItem: ICookie, newItem: ICookie, callback: HandleCallback) => { try { await check(); const res = await CookieOperator.editCookieItem(domain, oldItem, newItem); checkResponseAndCallback(res, 'edit', callback); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { checkResponseAndCallback(err, 'edit', callback); } }; function handleMessage( message: Message, sender: chrome.runtime.MessageSender, callback: (response?: SendResponse) => void, ) { const type = message.type; switch (type) { case MessageType.PushCookie: handlePush(message.payload, callback); break; case MessageType.PullCookie: // eslint-disable-next-line no-case-declarations, @typescript-eslint/no-non-null-asserted-optional-chain const activeTabUrl = message.payload.activeTabUrl || sender.tab?.url!; handlePull(activeTabUrl!, message.payload.domain, message.payload.reload, callback); break; case MessageType.RemoveCookie: handleRemove(message.payload.domain, callback); break; case MessageType.RemoveCookieItem: handleRemoveItem(message.payload.domain, message.payload.id, callback); break; case MessageType.EditCookieItem: handleEditItem(message.payload.domain, message.payload.oldItem, message.payload.newItem, callback); break; default: break; } return true; } export const refreshListen = async () => { chrome.runtime.onMessage.removeListener(handleMessage); chrome.runtime.onMessage.addListener(handleMessage); }; ================================================ FILE: chrome-extension/lib/background/subscribe.ts ================================================ import { accountStorage } from '@sync-your-cookie/storage/lib/accountStorage'; import { domainStatusStorage } from '@sync-your-cookie/storage/lib/domainStatusStorage'; import { pullCookies } from '@sync-your-cookie/shared'; import { cookieStorage } from '@sync-your-cookie/storage/lib/cookieStorage'; import { settingsStorage } from '@sync-your-cookie/storage/lib/settingsStorage'; import { clearBadge, setPullingBadge, setPushingAndPullingBadge, setPushingBadge } from './badge'; import { initContextMenu, removeContextMenu } from './contextMenu'; export const initSubscribe = async () => { await domainStatusStorage.resetState(); domainStatusStorage.subscribe(async () => { const domainStatus = await domainStatusStorage.get(); if (domainStatus?.pulling && domainStatus.pushing) { setPushingAndPullingBadge(); } else if (domainStatus?.pushing) { setPushingBadge(); } else if (domainStatus?.pulling) { setPullingBadge(); } else { clearBadge(); } }); accountStorage.subscribe(async () => { await domainStatusStorage.resetState(); await cookieStorage.reset(); await pullCookies(); console.log('reset finished'); }); let previousContextMenu: boolean | undefined = undefined; settingsStorage.subscribe(async () => { const settingsSnapShot = await settingsStorage.getSnapshot(); if (previousContextMenu === settingsSnapShot?.contextMenu) { return; } previousContextMenu = settingsSnapShot?.contextMenu; if (settingsSnapShot?.contextMenu) { initContextMenu(); } else { removeContextMenu(); } }); }; ================================================ FILE: chrome-extension/manifest.js ================================================ import fs from 'node:fs'; const packageJson = JSON.parse(fs.readFileSync('../package.json', 'utf8')); const isFirefox = process.env.__FIREFOX__ === 'true'; const sidePanelConfig = { side_panel: { default_path: 'sidepanel/index.html', }, permissions: !isFirefox ? ['sidePanel', 'contextMenus'] : [], }; /** * After changing, please reload the extension at `chrome://extensions` * @type {chrome.runtime.ManifestV3} */ const manifest = Object.assign( { manifest_version: 3, default_locale: 'en', /** * if you want to support multiple languages, you can use the following reference * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Internationalization */ name: 'Sync Your Cookie', version: packageJson.version, description: 'A browser extension for syncing cookies and localStorage to Cloudflare KV or GitHub Gist', permissions: ['cookies', 'activeTab', 'tabs', 'storage', 'identity'].concat(sidePanelConfig.permissions), host_permissions: [''], options_page: 'options/index.html', background: { service_worker: 'background.iife.js', type: 'module', }, action: { default_popup: 'popup/index.html', default_icon: 'icon-34.png', }, // key: key, // chrome_url_overrides: { // newtab: 'newtab/index.html', // }, icons: { 128: 'icon-128.png', }, content_scripts: [ { matches: ['*://*/*'], js: ['content/index.iife.js'], run_at: 'document_start', }, ], // devtools_page: 'devtools/index.html', web_accessible_resources: [ { resources: ['*.js', '*.css', '*.svg', 'icon-128.png', 'icon-34.png'], matches: ['*://*/*'], }, ], }, !isFirefox && { side_panel: { ...sidePanelConfig.side_panel } }, ); export default manifest; ================================================ FILE: chrome-extension/package.json ================================================ { "name": "chrome-extension", "version": "1.4.2", "description": "chrome extension", "scripts": { "clean": "rimraf ../../dist && rimraf .turbo", "build": "tsc --noEmit && vite build", "build:firefox": "tsc --noEmit && cross-env __FIREFOX__=true vite build", "build:watch": "cross-env __DEV__=true vite build -w --mode development", "build:firefox:watch": "cross-env __DEV__=true __FIREFOX__=true vite build -w --mode development", "dev": "pnpm build:watch", "dev:firefox": "pnpm build:firefox:watch", "test": "vitest run", "lint": "eslint ./ --ext .ts,.js,.tsx,.jsx", "lint:fix": "pnpm lint --fix", "prettier": "prettier . --write", "type-check": "tsc --noEmit" }, "type": "module", "dependencies": { "@sync-your-cookie/shared": "workspace:*", "@sync-your-cookie/storage": "workspace:*", "p-timeout": "^6.1.4", "webextension-polyfill": "^0.12.0" }, "devDependencies": { "@laynezh/vite-plugin-lib-assets": "^0.5.21", "@sync-your-cookie/dev-utils": "workspace:*", "@sync-your-cookie/hmr": "workspace:*", "@sync-your-cookie/tsconfig": "workspace:*", "@types/ws": "^8.5.10", "magic-string": "^0.30.12", "ts-loader": "^9.5.1" } } ================================================ FILE: chrome-extension/public/_locales/en/messages.json ================================================ { "extensionDescription": { "description": "sync your cookie free", "message": "Sync your cookies to cloudlfare " }, "extensionName": { "description": "Sync your cookie", "message": "Sync your cookie" } } ================================================ FILE: chrome-extension/public/content.css ================================================ ================================================ FILE: chrome-extension/tsconfig.json ================================================ { "extends": "@sync-your-cookie/tsconfig/app.json", "compilerOptions": { "baseUrl": "./", "declaration": true, "declarationMap": true, "types": ["vite/client", "node", "chrome"], "paths": { "@root/*": ["./*"], "@lib/*": ["lib/*"] } }, "include": ["lib", "utils", "vite.config.ts", "node_modules/@types"] } ================================================ FILE: chrome-extension/utils/plugins/make-manifest-plugin.ts ================================================ import * as fs from 'fs'; import * as path from 'path'; import { ManifestParser, colorLog } from '@sync-your-cookie/dev-utils'; import type { PluginOption } from 'vite'; import { pathToFileURL } from 'url'; import * as process from 'process'; const { resolve } = path; const rootDir = resolve(__dirname, '..', '..'); const manifestFile = resolve(rootDir, 'manifest.js'); const getManifestWithCacheBurst = (): Promise<{ default: chrome.runtime.ManifestV3 }> => { const withCacheBurst = (path: string) => `${path}?${Date.now().toString()}`; /** * In Windows, import() doesn't work without file:// protocol. * So, we need to convert path to file:// protocol. (url.pathToFileURL) */ if (process.platform === 'win32') { return import(withCacheBurst(pathToFileURL(manifestFile).href)); } return import(withCacheBurst(manifestFile)); }; export default function makeManifestPlugin(config: { outDir: string }): PluginOption { function makeManifest(manifest: chrome.runtime.ManifestV3, to: string) { if (!fs.existsSync(to)) { fs.mkdirSync(to); } const manifestPath = resolve(to, 'manifest.json'); const isFirefox = process.env.__FIREFOX__; fs.writeFileSync(manifestPath, ManifestParser.convertManifestToString(manifest, isFirefox ? 'firefox' : 'chrome')); colorLog(`Manifest file copy complete: ${manifestPath}`, 'success'); } return { name: 'make-manifest', buildStart() { this.addWatchFile(manifestFile); }, async writeBundle() { const outDir = config.outDir; const manifest = await getManifestWithCacheBurst(); makeManifest(manifest.default, outDir); }, }; } ================================================ FILE: chrome-extension/vite.config.ts ================================================ import libAssetsPlugin from '@laynezh/vite-plugin-lib-assets'; import { watchRebuildPlugin } from '@sync-your-cookie/hmr'; import { resolve } from 'path'; import { defineConfig } from 'vite'; import makeManifestPlugin from './utils/plugins/make-manifest-plugin'; const rootDir = resolve(__dirname); const libDir = resolve(rootDir, 'lib'); const isDev = process.env.__DEV__ === 'true'; const isProduction = !isDev; const outDir = resolve(rootDir, '..', 'dist'); export default defineConfig({ resolve: { alias: { '@root': rootDir, '@lib': libDir, '@assets': resolve(libDir, 'assets'), }, }, define: { 'process.env.NODE_ENV': isDev ? `"development"` : `"production"`, }, plugins: [ libAssetsPlugin({ outputPath: outDir, }), makeManifestPlugin({ outDir }), isDev && watchRebuildPlugin({ reload: true }), ], publicDir: resolve(rootDir, 'public'), build: { lib: { formats: ['iife'], entry: resolve(__dirname, 'lib/background/index.ts'), name: 'BackgroundScript', fileName: 'background', }, outDir, sourcemap: isDev, minify: isProduction, reportCompressedSize: isProduction, modulePreload: true, rollupOptions: { external: ['chrome'], output: { inlineDynamicImports: true, }, }, }, }); ================================================ FILE: googlef759ff453695209f.html ================================================ google-site-verification: googlef759ff453695209f.html ================================================ FILE: how-to-use.md ================================================ ## How to use `Sync-Your-Cookie` uses Cloudflare [KV](https://developers.cloudflare.com/kv/) to store cookie data. Here is a tutorial on how to configure KV and Token: ## Create Namespace ![create_namespace](./screenshots/kv//create_namepace.png) Input ![created_namespace](./screenshots/kv/input_name.png) Your NamespaceId ![namespaceId](./screenshots/kv/namespaceId.png) ## Your AccountId ![your_account_id](./screenshots/kv//account-id.png) ## Create Token 1. Enter Profile Page ![token_page](./screenshots/kv//create_token.png) 2. Custom Permission ![setting-up](./screenshots/kv//custom_token.png) 3. Select KV Read and Write Permission ![select-permission](./screenshots/kv/setting-permission.png) 4. Confirm Create ![confirm-create](./screenshots/kv/finish_create_token.png) 5. Copy Token ![copy-token](./screenshots/kv/copy_token.png) 6. Your Token List ![your-token-list](./screenshots/kv/created_token_list.png) 7. Paste Your Account Info And Save ![paste-and-save](./screenshots/kv/paste.png) 8. Push Your Cookie ![push-cookie](./screenshots/kv/push_cookie.png) 9. Check Your Cookie The uploaded cookie is a protobuf-encoded string ![check your cookie](./screenshots/kv/reload_page.png) ## Reference - [create-token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) - [account-owned-tokens](https://developers.cloudflare.com/fundamentals/api/get-started/account-owned-tokens/) ================================================ FILE: package.json ================================================ { "name": "sync-your-cookie", "version": "1.4.2", "description": "sync your cookie extension monorepo", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/jackluson/sync-your-cookie.git" }, "scripts": { "clean": "rimraf dist && rimraf .turbo && turbo clean", "build": "turbo build", "build:firefox": "cross-env __FIREFOX__=true turbo build", "dev-server": "pnpm -F hmr build && pnpm -F hmr dev-server", "dev:apps": "cross-env __DEV__=true turbo run dev --filter=./pages/* --filter=chrome-extension --filter=@sync-your-cookie/hmr --concurrency 15", "dev": "cross-env __DEV__=true turbo run dev --concurrency 25", "dev:firefox": "cross-env __DEV__=true __FIREFOX__=true turbo dev --concurrency 20", "zip": "pnpm build && pnpm -F zipper zip", "test": "turbo test", "type-check": "turbo type-check", "lint": "turbo lint", "lint:fix": "turbo lint:fix", "prettier": "turbo prettier" }, "type": "module", "dependencies": { "react": "18.2.0", "react-dom": "18.2.0" }, "devDependencies": { "@types/chrome": "^0.0.268", "@types/node": "^20.12.11", "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "@vitejs/plugin-react-swc": "^3.6.0", "autoprefixer": "^10.4.19", "concurrently": "^8.2.2", "cross-env": "^7.0.3", "deepmerge": "^4.3.1", "eslint": "8.56.0", "eslint-config-airbnb-typescript": "17.1.0", "eslint-config-prettier": "9.0.0", "eslint-plugin-import": "2.29.1", "eslint-plugin-jsx-a11y": "6.8.0", "eslint-plugin-prettier": "5.1.3", "eslint-plugin-react": "7.33.2", "eslint-plugin-react-hooks": "4.6.2", "postcss": "^8.4.38", "prettier": "^3.2.5", "rimraf": "^5.0.7", "tailwindcss": "^3.4.3", "tslib": "^2.6.2", "turbo": "^2.0.3", "typescript": "5.2.2", "vite": "^5.2.11" }, "packageManager": "pnpm@9.1.1", "engines": { "node": ">=20.12.0" } } ================================================ FILE: packages/dev-utils/index.ts ================================================ export * from './lib/manifest-parser'; export * from './lib/logger'; ================================================ FILE: packages/dev-utils/lib/logger.ts ================================================ type ColorType = 'success' | 'info' | 'error' | 'warning' | keyof typeof COLORS; type ValueOf = T[keyof T]; export function colorLog(message: string, type: ColorType) { let color: ValueOf; switch (type) { case 'success': color = COLORS.FgGreen; break; case 'info': color = COLORS.FgBlue; break; case 'error': color = COLORS.FgRed; break; case 'warning': color = COLORS.FgYellow; break; default: color = COLORS[type]; break; } console.log(color, message); } const COLORS = { Reset: '\x1b[0m', Bright: '\x1b[1m', Dim: '\x1b[2m', Underscore: '\x1b[4m', Blink: '\x1b[5m', Reverse: '\x1b[7m', Hidden: '\x1b[8m', FgBlack: '\x1b[30m', FgRed: '\x1b[31m', FgGreen: '\x1b[32m', FgYellow: '\x1b[33m', FgBlue: '\x1b[34m', FgMagenta: '\x1b[35m', FgCyan: '\x1b[36m', FgWhite: '\x1b[37m', BgBlack: '\x1b[40m', BgRed: '\x1b[41m', BgGreen: '\x1b[42m', BgYellow: '\x1b[43m', BgBlue: '\x1b[44m', BgMagenta: '\x1b[45m', BgCyan: '\x1b[46m', BgWhite: '\x1b[47m', } as const; ================================================ FILE: packages/dev-utils/lib/manifest-parser/impl.ts ================================================ import { ManifestParserInterface, Manifest } from './type'; export const ManifestParserImpl: ManifestParserInterface = { convertManifestToString: (manifest, env) => { if (env === 'firefox') { manifest = convertToFirefoxCompatibleManifest(manifest); } return JSON.stringify(manifest, null, 2); }, }; function convertToFirefoxCompatibleManifest(manifest: Manifest) { const manifestCopy = { ...manifest, } as { [key: string]: unknown }; manifestCopy.background = { scripts: [manifest.background?.service_worker], type: 'module', }; manifestCopy.options_ui = { page: manifest.options_page, browser_style: false, }; manifestCopy.content_security_policy = { extension_pages: "script-src 'self'; object-src 'self'", }; delete manifestCopy.options_page; return manifestCopy as Manifest; } ================================================ FILE: packages/dev-utils/lib/manifest-parser/index.ts ================================================ import { ManifestParserImpl } from './impl'; export const ManifestParser = ManifestParserImpl; ================================================ FILE: packages/dev-utils/lib/manifest-parser/type.ts ================================================ export type Manifest = chrome.runtime.ManifestV3; export interface ManifestParserInterface { convertManifestToString: (manifest: Manifest, env: 'chrome' | 'firefox') => string; } ================================================ FILE: packages/dev-utils/package.json ================================================ { "name": "@sync-your-cookie/dev-utils", "version": "0.0.1", "description": "chrome extension dev utils", "private": true, "sideEffects": false, "files": [ "dist/**" ], "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "clean": "rimraf ./dist && rimraf ./build && rimraf .turbo", "build": "pnpm run clean && tsc", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "pnpm lint --fix", "prettier": "prettier . --write", "type-check": "tsc --noEmit" }, "dependencies": {}, "devDependencies": { "@sync-your-cookie/tsconfig": "workspace:*" } } ================================================ FILE: packages/dev-utils/tsconfig.json ================================================ { "extends": "@sync-your-cookie/tsconfig/utils", "compilerOptions": { "outDir": "dist", "types": ["chrome"] }, "include": ["index.ts", "lib"] } ================================================ FILE: packages/hmr/index.ts ================================================ export * from './lib/plugins'; ================================================ FILE: packages/hmr/lib/constant.ts ================================================ export const LOCAL_RELOAD_SOCKET_PORT = 8081; export const LOCAL_RELOAD_SOCKET_URL = `ws://localhost:${LOCAL_RELOAD_SOCKET_PORT}`; ================================================ FILE: packages/hmr/lib/debounce.ts ================================================ export function debounce(callback: (...args: A) => void, delay: number) { let timer: NodeJS.Timeout; return function (...args: A) { clearTimeout(timer); timer = setTimeout(() => callback(...args), delay); }; } ================================================ FILE: packages/hmr/lib/initClient.ts ================================================ import { LOCAL_RELOAD_SOCKET_URL } from './constant'; import MessageInterpreter from './interpreter'; export default function initReloadClient({ id, onUpdate }: { id: string; onUpdate: () => void }) { let ws: WebSocket | null = null; try { ws = new WebSocket(LOCAL_RELOAD_SOCKET_URL); ws.onopen = () => { ws?.addEventListener('message', event => { const message = MessageInterpreter.receive(String(event.data)); if (message.type === 'ping') { console.log('[HMR] Client OK'); } if (message.type === 'do_update' && message.id === id) { sendUpdateCompleteMessage(); onUpdate(); return; } }); }; ws.onclose = () => { console.log( `Reload server disconnected.\nPlease check if the WebSocket server is running properly on ${LOCAL_RELOAD_SOCKET_URL}. This feature detects changes in the code and helps the browser to reload the extension or refresh the current tab.`, ); setTimeout(() => { initReloadClient({ onUpdate, id }); }, 1000); }; } catch (e) { setTimeout(() => { initReloadClient({ onUpdate, id }); }, 1000); } function sendUpdateCompleteMessage() { ws?.send(MessageInterpreter.send({ type: 'done_update' })); } } ================================================ FILE: packages/hmr/lib/initReloadServer.ts ================================================ #!/usr/bin/env node import { WebSocket, WebSocketServer } from 'ws'; import { LOCAL_RELOAD_SOCKET_PORT, LOCAL_RELOAD_SOCKET_URL } from './constant'; import MessageInterpreter from './interpreter'; const clientsThatNeedToUpdate: Set = new Set(); function initReloadServer() { try { const wss = new WebSocketServer({ port: LOCAL_RELOAD_SOCKET_PORT }); wss.on('listening', () => console.log(`[HMR] Server listening at ${LOCAL_RELOAD_SOCKET_URL}`)); wss.on('connection', ws => { clientsThatNeedToUpdate.add(ws); ws.addEventListener('close', () => clientsThatNeedToUpdate.delete(ws)); ws.addEventListener('message', event => { if (typeof event.data !== 'string') return; const message = MessageInterpreter.receive(event.data); if (message.type === 'done_update') { ws.close(); } if (message.type === 'build_complete') { clientsThatNeedToUpdate.forEach((ws: WebSocket) => ws.send(MessageInterpreter.send({ type: 'do_update', id: message.id })), ); } }); }); ping(); } catch { console.error(`[HMR] Failed to start server at ${LOCAL_RELOAD_SOCKET_URL}`); console.error('PLEASE MAKE SURE YOU ARE RUNNING `pnpm dev-server`'); } } initReloadServer(); function ping() { clientsThatNeedToUpdate.forEach(ws => ws.send(MessageInterpreter.send({ type: 'ping' }))); setTimeout(() => { ping(); }, 15_000); } ================================================ FILE: packages/hmr/lib/injections/refresh.ts ================================================ import initClient from '../initClient'; function addRefresh() { let pendingReload = false; initClient({ // eslint-disable-next-line // @ts-ignore id: __HMR_ID, onUpdate: () => { // disable reload when tab is hidden if (document.hidden) { pendingReload = true; return; } reload(); }, }); // reload function reload(): void { pendingReload = false; window.location.reload(); } // reload when tab is visible function reloadWhenTabIsVisible(): void { !document.hidden && pendingReload && reload(); } document.addEventListener('visibilitychange', reloadWhenTabIsVisible); } addRefresh(); ================================================ FILE: packages/hmr/lib/injections/reload.ts ================================================ import initClient from '../initClient'; function addReload() { const reload = () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore chrome.runtime.reload(); }; initClient({ // eslint-disable-next-line // @ts-ignore id: __HMR_ID, onUpdate: reload, }); } addReload(); ================================================ FILE: packages/hmr/lib/interpreter/index.ts ================================================ import type { WebSocketMessage, SerializedMessage } from './types'; export default class MessageInterpreter { // eslint-disable-next-line @typescript-eslint/no-empty-function private constructor() {} static send(message: WebSocketMessage): SerializedMessage { return JSON.stringify(message); } static receive(serializedMessage: SerializedMessage): WebSocketMessage { return JSON.parse(serializedMessage); } } ================================================ FILE: packages/hmr/lib/interpreter/types.ts ================================================ type UpdateRequestMessage = { type: 'do_update'; id: string; }; type UpdateCompleteMessage = { type: 'done_update' }; type PingMessage = { type: 'ping' }; type BuildCompletionMessage = { type: 'build_complete'; id: string }; export type SerializedMessage = string; export type WebSocketMessage = UpdateCompleteMessage | UpdateRequestMessage | BuildCompletionMessage | PingMessage; ================================================ FILE: packages/hmr/lib/plugins/index.ts ================================================ export * from './watch-rebuild-plugin'; export * from './make-entry-point-plugin'; ================================================ FILE: packages/hmr/lib/plugins/make-entry-point-plugin.ts ================================================ import * as fs from 'fs'; import path from 'path'; import type { PluginOption } from 'vite'; /** * make entry point file for content script cache busting */ export function makeEntryPointPlugin(): PluginOption { const cleanupTargets = new Set(); const isFirefox = process.env.__FIREFOX__ === 'true'; return { name: 'make-entry-point-plugin', generateBundle(options, bundle) { const outputDir = options.dir; if (!outputDir) { throw new Error('Output directory not found'); } for (const module of Object.values(bundle)) { const fileName = path.basename(module.fileName); const newFileName = fileName.replace('.js', '_dev.js'); switch (module.type) { case 'asset': // map file if (fileName.endsWith('.map')) { cleanupTargets.add(path.resolve(outputDir, fileName)); const originalFileName = fileName.replace('.map', ''); const replacedSource = String(module.source).replaceAll(originalFileName, newFileName); module.source = ''; fs.writeFileSync(path.resolve(outputDir, newFileName), replacedSource); break; } break; case 'chunk': { fs.writeFileSync(path.resolve(outputDir, newFileName), module.code); console.log('newFileName', newFileName); if (isFirefox) { const contentDirectory = extractContentDir(outputDir); module.code = `import(browser.runtime.getURL("${contentDirectory}/${newFileName}"));`; } else { module.code = `import('./${newFileName}');`; } break; } } } }, closeBundle() { cleanupTargets.forEach(target => { fs.unlinkSync(target); }); }, }; } /** * Extract content directory from output directory for Firefox * @param outputDir */ function extractContentDir(outputDir: string) { const parts = outputDir.split(path.sep); const distIndex = parts.indexOf('dist'); if (distIndex !== -1 && distIndex < parts.length - 1) { return parts.slice(distIndex + 1); } throw new Error('Output directory does not contain "dist"'); } ================================================ FILE: packages/hmr/lib/plugins/watch-rebuild-plugin.ts ================================================ import type { PluginOption } from 'vite'; import { WebSocket } from 'ws'; import MessageInterpreter from '../interpreter'; import { LOCAL_RELOAD_SOCKET_URL } from '../constant'; import * as fs from 'fs'; import path from 'path'; type PluginConfig = { onStart?: () => void; reload?: boolean; refresh?: boolean; }; const injectionsPath = path.resolve(__dirname, '..', '..', '..', 'build', 'injections'); const refreshCode = fs.readFileSync(path.resolve(injectionsPath, 'refresh.js'), 'utf-8'); const reloadCode = fs.readFileSync(path.resolve(injectionsPath, 'reload.js'), 'utf-8'); export function watchRebuildPlugin(config: PluginConfig): PluginOption { let ws: WebSocket | null = null; const id = Math.random().toString(36); const { refresh, reload } = config; const hmrCode = (refresh ? refreshCode : '') + (reload ? reloadCode : ''); function initializeWebSocket() { if (!ws) { ws = new WebSocket(LOCAL_RELOAD_SOCKET_URL); ws.onopen = () => { console.log(`[HMR] Connected to dev-server at ${LOCAL_RELOAD_SOCKET_URL}`); }; ws.onerror = () => { console.error(`[HMR] Failed to start server at ${LOCAL_RELOAD_SOCKET_URL}`); console.error('PLEASE MAKE SURE YOU ARE RUNNING `pnpm dev-server`'); console.warn('Retrying in 5 seconds...'); ws = null; setTimeout(() => initializeWebSocket(), 5_000); }; } } return { name: 'watch-rebuild', writeBundle() { config.onStart?.(); if (!ws) { initializeWebSocket(); return; } /** * When the build is complete, send a message to the reload server. * The reload server will send a message to the client to reload or refresh the extension. */ if (!ws) { throw new Error('WebSocket is not initialized'); } ws.send(MessageInterpreter.send({ type: 'build_complete', id })); }, generateBundle(_options, bundle) { for (const module of Object.values(bundle)) { if (module.type === 'chunk') { module.code = `(function() {let __HMR_ID = "${id}";\n` + hmrCode + '\n' + '})();' + '\n' + module.code; } } }, }; } ================================================ FILE: packages/hmr/package.json ================================================ { "name": "@sync-your-cookie/hmr", "version": "0.0.1", "description": "chrome extension hot module reload or refresh", "private": true, "sideEffects": true, "files": [ "dist/**" ], "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "clean": "rimraf ./dist && rimraf ./build && rimraf .turbo", "build:tsc": "tsc -b tsconfig.build.json", "build:rollup": "rollup --config rollup.config.mjs", "build": "pnpm run build:tsc && pnpm run build:rollup", "dev": "node dist/lib/initReloadServer.js", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "pnpm lint --fix", "prettier": "prettier . --write", "type-check": "tsc --noEmit" }, "dependencies": { "ws": "8.17.0" }, "devDependencies": { "@sync-your-cookie/tsconfig": "workspace:*", "@rollup/plugin-sucrase": "^5.0.2", "@types/ws": "^8.5.10", "esm": "^3.2.25", "rollup": "^4.17.2", "ts-node": "^10.9.2" } } ================================================ FILE: packages/hmr/rollup.config.mjs ================================================ import sucrase from '@rollup/plugin-sucrase'; const plugins = [ sucrase({ exclude: ['node_modules/**'], transforms: ['typescript'], }), ]; /** * @type {import("rollup").RollupOptions[]} */ export default [ { plugins, input: 'lib/injections/reload.ts', output: { format: 'iife', file: 'build/injections/reload.js', }, }, { plugins, input: 'lib/injections/refresh.ts', output: { format: 'iife', file: 'build/injections/refresh.js', }, }, ]; ================================================ FILE: packages/hmr/tsconfig.build.json ================================================ { "extends": "@sync-your-cookie/tsconfig/utils", "compilerOptions": { "outDir": "dist" }, "exclude": ["lib/injections/**/*"], "include": ["lib", "index.ts"] } ================================================ FILE: packages/hmr/tsconfig.json ================================================ { "extends": "@sync-your-cookie/tsconfig/utils", "compilerOptions": { "outDir": "dist" }, "include": ["lib", "index.ts", "rollup.config.mjs"] } ================================================ FILE: packages/protobuf/README.md ================================================ # Shared Package This package contains code shared with other packages. To use the code in the package, you need to add the following to the package.json file. ```json { "dependencies": { "@sync-your-cookie/shared": "workspace:*" } } ``` After building this package, real-time cache busting does not occur in the code of other packages that reference this package. You need to rerun it from the root path with `pnpm dev`, etc. (This will be improved in the future.) If the type does not require compilation, there is no problem, but if the implementation requiring compilation is changed, a problem may occur. Therefore, it is recommended to extract and use it in each context if it is easier to manage by extracting overlapping or business logic from the code that changes frequently in this package. ================================================ FILE: packages/protobuf/index.ts ================================================ export * from './lib/protobuf'; export * from './utils'; import pako from 'pako'; export { pako }; ================================================ FILE: packages/protobuf/lib/protobuf/code.ts ================================================ import pako from 'pako'; import { compress, decompress } from './../../utils/compress'; import type { ICookiesMap } from './proto/cookie'; import { CookiesMap } from './proto/cookie'; export const encodeCookiesMap = async ( cookiesMap: ICookiesMap = {}, isCompress: boolean = true, ): Promise => { // verify 只会校验数据的类型是否合法,并不会校验是否缺少或增加了数据项。 const invalid = CookiesMap.verify(cookiesMap); if (invalid) { throw Error(invalid); } const message = CookiesMap.create(cookiesMap); const buffer = CookiesMap.encode(message).finish(); if (isCompress) { const compressedBuf = pako.deflate(buffer); return await compress(compressedBuf); } return buffer; }; export const decodeCookiesMap = async (buffer: Uint8Array, isDeCompress: boolean = true) => { let buf = buffer; if (isDeCompress) { buf = await decompress(buf); buf = pako.inflate(buf); } const message = CookiesMap.decode(buf); return message; }; export type { ICookie, ICookiesMap, ILocalStorageItem } from './proto/cookie'; ================================================ FILE: packages/protobuf/lib/protobuf/index.ts ================================================ export * from './code'; ================================================ FILE: packages/protobuf/lib/protobuf/proto/cookie.d.ts ================================================ import * as $protobuf from "protobufjs"; import Long = require("long"); /** Properties of a Cookie. */ export interface ICookie { /** Cookie domain */ domain?: (string|null); /** Cookie name */ name?: (string|null); /** Cookie storeId */ storeId?: (string|null); /** Cookie value */ value?: (string|null); /** Cookie session */ session?: (boolean|null); /** Cookie hostOnly */ hostOnly?: (boolean|null); /** Cookie expirationDate */ expirationDate?: (number|null); /** Cookie path */ path?: (string|null); /** Cookie httpOnly */ httpOnly?: (boolean|null); /** Cookie secure */ secure?: (boolean|null); /** Cookie sameSite */ sameSite?: (string|null); } /** Represents a Cookie. */ export class Cookie implements ICookie { /** * Constructs a new Cookie. * @param [properties] Properties to set */ constructor(properties?: ICookie); /** Cookie domain. */ public domain: string; /** Cookie name. */ public name: string; /** Cookie storeId. */ public storeId: string; /** Cookie value. */ public value: string; /** Cookie session. */ public session: boolean; /** Cookie hostOnly. */ public hostOnly: boolean; /** Cookie expirationDate. */ public expirationDate: number; /** Cookie path. */ public path: string; /** Cookie httpOnly. */ public httpOnly: boolean; /** Cookie secure. */ public secure: boolean; /** Cookie sameSite. */ public sameSite: string; /** * Creates a new Cookie instance using the specified properties. * @param [properties] Properties to set * @returns Cookie instance */ public static create(properties?: ICookie): Cookie; /** * Encodes the specified Cookie message. Does not implicitly {@link Cookie.verify|verify} messages. * @param message Cookie message or plain object to encode * @param [writer] Writer to encode to * @returns Writer */ public static encode(message: ICookie, writer?: $protobuf.Writer): $protobuf.Writer; /** * Encodes the specified Cookie message, length delimited. Does not implicitly {@link Cookie.verify|verify} messages. * @param message Cookie message or plain object to encode * @param [writer] Writer to encode to * @returns Writer */ public static encodeDelimited(message: ICookie, writer?: $protobuf.Writer): $protobuf.Writer; /** * Decodes a Cookie message from the specified reader or buffer. * @param reader Reader or buffer to decode from * @param [length] Message length if known beforehand * @returns Cookie * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): Cookie; /** * Decodes a Cookie message from the specified reader or buffer, length delimited. * @param reader Reader or buffer to decode from * @returns Cookie * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): Cookie; /** * Verifies a Cookie message. * @param message Plain object to verify * @returns `null` if valid, otherwise the reason why it is not */ public static verify(message: { [k: string]: any }): (string|null); /** * Creates a Cookie message from a plain object. Also converts values to their respective internal types. * @param object Plain object * @returns Cookie */ public static fromObject(object: { [k: string]: any }): Cookie; /** * Creates a plain object from a Cookie message. Also converts values to other types if specified. * @param message Cookie * @param [options] Conversion options * @returns Plain object */ public static toObject(message: Cookie, options?: $protobuf.IConversionOptions): { [k: string]: any }; /** * Converts this Cookie to JSON. * @returns JSON object */ public toJSON(): { [k: string]: any }; /** * Gets the default type url for Cookie * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") * @returns The default type url */ public static getTypeUrl(typeUrlPrefix?: string): string; } /** Properties of a LocalStorageItem. */ export interface ILocalStorageItem { /** LocalStorageItem key */ key?: (string|null); /** LocalStorageItem value */ value?: (string|null); } /** Represents a LocalStorageItem. */ export class LocalStorageItem implements ILocalStorageItem { /** * Constructs a new LocalStorageItem. * @param [properties] Properties to set */ constructor(properties?: ILocalStorageItem); /** LocalStorageItem key. */ public key: string; /** LocalStorageItem value. */ public value: string; /** * Creates a new LocalStorageItem instance using the specified properties. * @param [properties] Properties to set * @returns LocalStorageItem instance */ public static create(properties?: ILocalStorageItem): LocalStorageItem; /** * Encodes the specified LocalStorageItem message. Does not implicitly {@link LocalStorageItem.verify|verify} messages. * @param message LocalStorageItem message or plain object to encode * @param [writer] Writer to encode to * @returns Writer */ public static encode(message: ILocalStorageItem, writer?: $protobuf.Writer): $protobuf.Writer; /** * Encodes the specified LocalStorageItem message, length delimited. Does not implicitly {@link LocalStorageItem.verify|verify} messages. * @param message LocalStorageItem message or plain object to encode * @param [writer] Writer to encode to * @returns Writer */ public static encodeDelimited(message: ILocalStorageItem, writer?: $protobuf.Writer): $protobuf.Writer; /** * Decodes a LocalStorageItem message from the specified reader or buffer. * @param reader Reader or buffer to decode from * @param [length] Message length if known beforehand * @returns LocalStorageItem * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): LocalStorageItem; /** * Decodes a LocalStorageItem message from the specified reader or buffer, length delimited. * @param reader Reader or buffer to decode from * @returns LocalStorageItem * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): LocalStorageItem; /** * Verifies a LocalStorageItem message. * @param message Plain object to verify * @returns `null` if valid, otherwise the reason why it is not */ public static verify(message: { [k: string]: any }): (string|null); /** * Creates a LocalStorageItem message from a plain object. Also converts values to their respective internal types. * @param object Plain object * @returns LocalStorageItem */ public static fromObject(object: { [k: string]: any }): LocalStorageItem; /** * Creates a plain object from a LocalStorageItem message. Also converts values to other types if specified. * @param message LocalStorageItem * @param [options] Conversion options * @returns Plain object */ public static toObject(message: LocalStorageItem, options?: $protobuf.IConversionOptions): { [k: string]: any }; /** * Converts this LocalStorageItem to JSON. * @returns JSON object */ public toJSON(): { [k: string]: any }; /** * Gets the default type url for LocalStorageItem * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") * @returns The default type url */ public static getTypeUrl(typeUrlPrefix?: string): string; } /** Properties of a DomainCookie. */ export interface IDomainCookie { /** DomainCookie createTime */ createTime?: (number|Long|null); /** DomainCookie updateTime */ updateTime?: (number|Long|null); /** DomainCookie cookies */ cookies?: (ICookie[]|null); /** DomainCookie localStorageItems */ localStorageItems?: (ILocalStorageItem[]|null); /** DomainCookie userAgent */ userAgent?: (string|null); } /** Represents a DomainCookie. */ export class DomainCookie implements IDomainCookie { /** * Constructs a new DomainCookie. * @param [properties] Properties to set */ constructor(properties?: IDomainCookie); /** DomainCookie createTime. */ public createTime: (number|Long); /** DomainCookie updateTime. */ public updateTime: (number|Long); /** DomainCookie cookies. */ public cookies: ICookie[]; /** DomainCookie localStorageItems. */ public localStorageItems: ILocalStorageItem[]; /** DomainCookie userAgent. */ public userAgent: string; /** * Creates a new DomainCookie instance using the specified properties. * @param [properties] Properties to set * @returns DomainCookie instance */ public static create(properties?: IDomainCookie): DomainCookie; /** * Encodes the specified DomainCookie message. Does not implicitly {@link DomainCookie.verify|verify} messages. * @param message DomainCookie message or plain object to encode * @param [writer] Writer to encode to * @returns Writer */ public static encode(message: IDomainCookie, writer?: $protobuf.Writer): $protobuf.Writer; /** * Encodes the specified DomainCookie message, length delimited. Does not implicitly {@link DomainCookie.verify|verify} messages. * @param message DomainCookie message or plain object to encode * @param [writer] Writer to encode to * @returns Writer */ public static encodeDelimited(message: IDomainCookie, writer?: $protobuf.Writer): $protobuf.Writer; /** * Decodes a DomainCookie message from the specified reader or buffer. * @param reader Reader or buffer to decode from * @param [length] Message length if known beforehand * @returns DomainCookie * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): DomainCookie; /** * Decodes a DomainCookie message from the specified reader or buffer, length delimited. * @param reader Reader or buffer to decode from * @returns DomainCookie * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): DomainCookie; /** * Verifies a DomainCookie message. * @param message Plain object to verify * @returns `null` if valid, otherwise the reason why it is not */ public static verify(message: { [k: string]: any }): (string|null); /** * Creates a DomainCookie message from a plain object. Also converts values to their respective internal types. * @param object Plain object * @returns DomainCookie */ public static fromObject(object: { [k: string]: any }): DomainCookie; /** * Creates a plain object from a DomainCookie message. Also converts values to other types if specified. * @param message DomainCookie * @param [options] Conversion options * @returns Plain object */ public static toObject(message: DomainCookie, options?: $protobuf.IConversionOptions): { [k: string]: any }; /** * Converts this DomainCookie to JSON. * @returns JSON object */ public toJSON(): { [k: string]: any }; /** * Gets the default type url for DomainCookie * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") * @returns The default type url */ public static getTypeUrl(typeUrlPrefix?: string): string; } /** Properties of a CookiesMap. */ export interface ICookiesMap { /** CookiesMap createTime */ createTime?: (number|Long|null); /** CookiesMap updateTime */ updateTime?: (number|Long|null); /** CookiesMap domainCookieMap */ domainCookieMap?: ({ [k: string]: IDomainCookie }|null); } /** Represents a CookiesMap. */ export class CookiesMap implements ICookiesMap { /** * Constructs a new CookiesMap. * @param [properties] Properties to set */ constructor(properties?: ICookiesMap); /** CookiesMap createTime. */ public createTime: (number|Long); /** CookiesMap updateTime. */ public updateTime: (number|Long); /** CookiesMap domainCookieMap. */ public domainCookieMap: { [k: string]: IDomainCookie }; /** * Creates a new CookiesMap instance using the specified properties. * @param [properties] Properties to set * @returns CookiesMap instance */ public static create(properties?: ICookiesMap): CookiesMap; /** * Encodes the specified CookiesMap message. Does not implicitly {@link CookiesMap.verify|verify} messages. * @param message CookiesMap message or plain object to encode * @param [writer] Writer to encode to * @returns Writer */ public static encode(message: ICookiesMap, writer?: $protobuf.Writer): $protobuf.Writer; /** * Encodes the specified CookiesMap message, length delimited. Does not implicitly {@link CookiesMap.verify|verify} messages. * @param message CookiesMap message or plain object to encode * @param [writer] Writer to encode to * @returns Writer */ public static encodeDelimited(message: ICookiesMap, writer?: $protobuf.Writer): $protobuf.Writer; /** * Decodes a CookiesMap message from the specified reader or buffer. * @param reader Reader or buffer to decode from * @param [length] Message length if known beforehand * @returns CookiesMap * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): CookiesMap; /** * Decodes a CookiesMap message from the specified reader or buffer, length delimited. * @param reader Reader or buffer to decode from * @returns CookiesMap * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): CookiesMap; /** * Verifies a CookiesMap message. * @param message Plain object to verify * @returns `null` if valid, otherwise the reason why it is not */ public static verify(message: { [k: string]: any }): (string|null); /** * Creates a CookiesMap message from a plain object. Also converts values to their respective internal types. * @param object Plain object * @returns CookiesMap */ public static fromObject(object: { [k: string]: any }): CookiesMap; /** * Creates a plain object from a CookiesMap message. Also converts values to other types if specified. * @param message CookiesMap * @param [options] Conversion options * @returns Plain object */ public static toObject(message: CookiesMap, options?: $protobuf.IConversionOptions): { [k: string]: any }; /** * Converts this CookiesMap to JSON. * @returns JSON object */ public toJSON(): { [k: string]: any }; /** * Gets the default type url for CookiesMap * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") * @returns The default type url */ public static getTypeUrl(typeUrlPrefix?: string): string; } ================================================ FILE: packages/protobuf/lib/protobuf/proto/cookie.js ================================================ /*eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars*/ import * as $protobuf from "protobufjs/minimal"; // Common aliases const $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util; // Exported root namespace const $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {}); export const Cookie = $root.Cookie = (() => { /** * Properties of a Cookie. * @exports ICookie * @interface ICookie * @property {string|null} [domain] Cookie domain * @property {string|null} [name] Cookie name * @property {string|null} [storeId] Cookie storeId * @property {string|null} [value] Cookie value * @property {boolean|null} [session] Cookie session * @property {boolean|null} [hostOnly] Cookie hostOnly * @property {number|null} [expirationDate] Cookie expirationDate * @property {string|null} [path] Cookie path * @property {boolean|null} [httpOnly] Cookie httpOnly * @property {boolean|null} [secure] Cookie secure * @property {string|null} [sameSite] Cookie sameSite */ /** * Constructs a new Cookie. * @exports Cookie * @classdesc Represents a Cookie. * @implements ICookie * @constructor * @param {ICookie=} [properties] Properties to set */ function Cookie(properties) { if (properties) for (let keys = Object.keys(properties), i = 0; i < keys.length; ++i) if (properties[keys[i]] != null) this[keys[i]] = properties[keys[i]]; } /** * Cookie domain. * @member {string} domain * @memberof Cookie * @instance */ Cookie.prototype.domain = ""; /** * Cookie name. * @member {string} name * @memberof Cookie * @instance */ Cookie.prototype.name = ""; /** * Cookie storeId. * @member {string} storeId * @memberof Cookie * @instance */ Cookie.prototype.storeId = ""; /** * Cookie value. * @member {string} value * @memberof Cookie * @instance */ Cookie.prototype.value = ""; /** * Cookie session. * @member {boolean} session * @memberof Cookie * @instance */ Cookie.prototype.session = false; /** * Cookie hostOnly. * @member {boolean} hostOnly * @memberof Cookie * @instance */ Cookie.prototype.hostOnly = false; /** * Cookie expirationDate. * @member {number} expirationDate * @memberof Cookie * @instance */ Cookie.prototype.expirationDate = 0; /** * Cookie path. * @member {string} path * @memberof Cookie * @instance */ Cookie.prototype.path = ""; /** * Cookie httpOnly. * @member {boolean} httpOnly * @memberof Cookie * @instance */ Cookie.prototype.httpOnly = false; /** * Cookie secure. * @member {boolean} secure * @memberof Cookie * @instance */ Cookie.prototype.secure = false; /** * Cookie sameSite. * @member {string} sameSite * @memberof Cookie * @instance */ Cookie.prototype.sameSite = ""; /** * Creates a new Cookie instance using the specified properties. * @function create * @memberof Cookie * @static * @param {ICookie=} [properties] Properties to set * @returns {Cookie} Cookie instance */ Cookie.create = function create(properties) { return new Cookie(properties); }; /** * Encodes the specified Cookie message. Does not implicitly {@link Cookie.verify|verify} messages. * @function encode * @memberof Cookie * @static * @param {ICookie} message Cookie message or plain object to encode * @param {$protobuf.Writer} [writer] Writer to encode to * @returns {$protobuf.Writer} Writer */ Cookie.encode = function encode(message, writer) { if (!writer) writer = $Writer.create(); if (message.domain != null && Object.hasOwnProperty.call(message, "domain")) writer.uint32(/* id 1, wireType 2 =*/10).string(message.domain); if (message.name != null && Object.hasOwnProperty.call(message, "name")) writer.uint32(/* id 2, wireType 2 =*/18).string(message.name); if (message.storeId != null && Object.hasOwnProperty.call(message, "storeId")) writer.uint32(/* id 3, wireType 2 =*/26).string(message.storeId); if (message.value != null && Object.hasOwnProperty.call(message, "value")) writer.uint32(/* id 4, wireType 2 =*/34).string(message.value); if (message.session != null && Object.hasOwnProperty.call(message, "session")) writer.uint32(/* id 5, wireType 0 =*/40).bool(message.session); if (message.hostOnly != null && Object.hasOwnProperty.call(message, "hostOnly")) writer.uint32(/* id 6, wireType 0 =*/48).bool(message.hostOnly); if (message.expirationDate != null && Object.hasOwnProperty.call(message, "expirationDate")) writer.uint32(/* id 7, wireType 5 =*/61).float(message.expirationDate); if (message.path != null && Object.hasOwnProperty.call(message, "path")) writer.uint32(/* id 8, wireType 2 =*/66).string(message.path); if (message.httpOnly != null && Object.hasOwnProperty.call(message, "httpOnly")) writer.uint32(/* id 9, wireType 0 =*/72).bool(message.httpOnly); if (message.secure != null && Object.hasOwnProperty.call(message, "secure")) writer.uint32(/* id 10, wireType 0 =*/80).bool(message.secure); if (message.sameSite != null && Object.hasOwnProperty.call(message, "sameSite")) writer.uint32(/* id 11, wireType 2 =*/90).string(message.sameSite); return writer; }; /** * Encodes the specified Cookie message, length delimited. Does not implicitly {@link Cookie.verify|verify} messages. * @function encodeDelimited * @memberof Cookie * @static * @param {ICookie} message Cookie message or plain object to encode * @param {$protobuf.Writer} [writer] Writer to encode to * @returns {$protobuf.Writer} Writer */ Cookie.encodeDelimited = function encodeDelimited(message, writer) { return this.encode(message, writer).ldelim(); }; /** * Decodes a Cookie message from the specified reader or buffer. * @function decode * @memberof Cookie * @static * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from * @param {number} [length] Message length if known beforehand * @returns {Cookie} Cookie * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ Cookie.decode = function decode(reader, length) { if (!(reader instanceof $Reader)) reader = $Reader.create(reader); let end = length === undefined ? reader.len : reader.pos + length, message = new $root.Cookie(); while (reader.pos < end) { let tag = reader.uint32(); switch (tag >>> 3) { case 1: { message.domain = reader.string(); break; } case 2: { message.name = reader.string(); break; } case 3: { message.storeId = reader.string(); break; } case 4: { message.value = reader.string(); break; } case 5: { message.session = reader.bool(); break; } case 6: { message.hostOnly = reader.bool(); break; } case 7: { message.expirationDate = reader.float(); break; } case 8: { message.path = reader.string(); break; } case 9: { message.httpOnly = reader.bool(); break; } case 10: { message.secure = reader.bool(); break; } case 11: { message.sameSite = reader.string(); break; } default: reader.skipType(tag & 7); break; } } return message; }; /** * Decodes a Cookie message from the specified reader or buffer, length delimited. * @function decodeDelimited * @memberof Cookie * @static * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from * @returns {Cookie} Cookie * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ Cookie.decodeDelimited = function decodeDelimited(reader) { if (!(reader instanceof $Reader)) reader = new $Reader(reader); return this.decode(reader, reader.uint32()); }; /** * Verifies a Cookie message. * @function verify * @memberof Cookie * @static * @param {Object.} message Plain object to verify * @returns {string|null} `null` if valid, otherwise the reason why it is not */ Cookie.verify = function verify(message) { if (typeof message !== "object" || message === null) return "object expected"; if (message.domain != null && message.hasOwnProperty("domain")) if (!$util.isString(message.domain)) return "domain: string expected"; if (message.name != null && message.hasOwnProperty("name")) if (!$util.isString(message.name)) return "name: string expected"; if (message.storeId != null && message.hasOwnProperty("storeId")) if (!$util.isString(message.storeId)) return "storeId: string expected"; if (message.value != null && message.hasOwnProperty("value")) if (!$util.isString(message.value)) return "value: string expected"; if (message.session != null && message.hasOwnProperty("session")) if (typeof message.session !== "boolean") return "session: boolean expected"; if (message.hostOnly != null && message.hasOwnProperty("hostOnly")) if (typeof message.hostOnly !== "boolean") return "hostOnly: boolean expected"; if (message.expirationDate != null && message.hasOwnProperty("expirationDate")) if (typeof message.expirationDate !== "number") return "expirationDate: number expected"; if (message.path != null && message.hasOwnProperty("path")) if (!$util.isString(message.path)) return "path: string expected"; if (message.httpOnly != null && message.hasOwnProperty("httpOnly")) if (typeof message.httpOnly !== "boolean") return "httpOnly: boolean expected"; if (message.secure != null && message.hasOwnProperty("secure")) if (typeof message.secure !== "boolean") return "secure: boolean expected"; if (message.sameSite != null && message.hasOwnProperty("sameSite")) if (!$util.isString(message.sameSite)) return "sameSite: string expected"; return null; }; /** * Creates a Cookie message from a plain object. Also converts values to their respective internal types. * @function fromObject * @memberof Cookie * @static * @param {Object.} object Plain object * @returns {Cookie} Cookie */ Cookie.fromObject = function fromObject(object) { if (object instanceof $root.Cookie) return object; let message = new $root.Cookie(); if (object.domain != null) message.domain = String(object.domain); if (object.name != null) message.name = String(object.name); if (object.storeId != null) message.storeId = String(object.storeId); if (object.value != null) message.value = String(object.value); if (object.session != null) message.session = Boolean(object.session); if (object.hostOnly != null) message.hostOnly = Boolean(object.hostOnly); if (object.expirationDate != null) message.expirationDate = Number(object.expirationDate); if (object.path != null) message.path = String(object.path); if (object.httpOnly != null) message.httpOnly = Boolean(object.httpOnly); if (object.secure != null) message.secure = Boolean(object.secure); if (object.sameSite != null) message.sameSite = String(object.sameSite); return message; }; /** * Creates a plain object from a Cookie message. Also converts values to other types if specified. * @function toObject * @memberof Cookie * @static * @param {Cookie} message Cookie * @param {$protobuf.IConversionOptions} [options] Conversion options * @returns {Object.} Plain object */ Cookie.toObject = function toObject(message, options) { if (!options) options = {}; let object = {}; if (options.defaults) { object.domain = ""; object.name = ""; object.storeId = ""; object.value = ""; object.session = false; object.hostOnly = false; object.expirationDate = 0; object.path = ""; object.httpOnly = false; object.secure = false; object.sameSite = ""; } if (message.domain != null && message.hasOwnProperty("domain")) object.domain = message.domain; if (message.name != null && message.hasOwnProperty("name")) object.name = message.name; if (message.storeId != null && message.hasOwnProperty("storeId")) object.storeId = message.storeId; if (message.value != null && message.hasOwnProperty("value")) object.value = message.value; if (message.session != null && message.hasOwnProperty("session")) object.session = message.session; if (message.hostOnly != null && message.hasOwnProperty("hostOnly")) object.hostOnly = message.hostOnly; if (message.expirationDate != null && message.hasOwnProperty("expirationDate")) object.expirationDate = options.json && !isFinite(message.expirationDate) ? String(message.expirationDate) : message.expirationDate; if (message.path != null && message.hasOwnProperty("path")) object.path = message.path; if (message.httpOnly != null && message.hasOwnProperty("httpOnly")) object.httpOnly = message.httpOnly; if (message.secure != null && message.hasOwnProperty("secure")) object.secure = message.secure; if (message.sameSite != null && message.hasOwnProperty("sameSite")) object.sameSite = message.sameSite; return object; }; /** * Converts this Cookie to JSON. * @function toJSON * @memberof Cookie * @instance * @returns {Object.} JSON object */ Cookie.prototype.toJSON = function toJSON() { return this.constructor.toObject(this, $protobuf.util.toJSONOptions); }; /** * Gets the default type url for Cookie * @function getTypeUrl * @memberof Cookie * @static * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") * @returns {string} The default type url */ Cookie.getTypeUrl = function getTypeUrl(typeUrlPrefix) { if (typeUrlPrefix === undefined) { typeUrlPrefix = "type.googleapis.com"; } return typeUrlPrefix + "/Cookie"; }; return Cookie; })(); export const LocalStorageItem = $root.LocalStorageItem = (() => { /** * Properties of a LocalStorageItem. * @exports ILocalStorageItem * @interface ILocalStorageItem * @property {string|null} [key] LocalStorageItem key * @property {string|null} [value] LocalStorageItem value */ /** * Constructs a new LocalStorageItem. * @exports LocalStorageItem * @classdesc Represents a LocalStorageItem. * @implements ILocalStorageItem * @constructor * @param {ILocalStorageItem=} [properties] Properties to set */ function LocalStorageItem(properties) { if (properties) for (let keys = Object.keys(properties), i = 0; i < keys.length; ++i) if (properties[keys[i]] != null) this[keys[i]] = properties[keys[i]]; } /** * LocalStorageItem key. * @member {string} key * @memberof LocalStorageItem * @instance */ LocalStorageItem.prototype.key = ""; /** * LocalStorageItem value. * @member {string} value * @memberof LocalStorageItem * @instance */ LocalStorageItem.prototype.value = ""; /** * Creates a new LocalStorageItem instance using the specified properties. * @function create * @memberof LocalStorageItem * @static * @param {ILocalStorageItem=} [properties] Properties to set * @returns {LocalStorageItem} LocalStorageItem instance */ LocalStorageItem.create = function create(properties) { return new LocalStorageItem(properties); }; /** * Encodes the specified LocalStorageItem message. Does not implicitly {@link LocalStorageItem.verify|verify} messages. * @function encode * @memberof LocalStorageItem * @static * @param {ILocalStorageItem} message LocalStorageItem message or plain object to encode * @param {$protobuf.Writer} [writer] Writer to encode to * @returns {$protobuf.Writer} Writer */ LocalStorageItem.encode = function encode(message, writer) { if (!writer) writer = $Writer.create(); if (message.key != null && Object.hasOwnProperty.call(message, "key")) writer.uint32(/* id 1, wireType 2 =*/10).string(message.key); if (message.value != null && Object.hasOwnProperty.call(message, "value")) writer.uint32(/* id 2, wireType 2 =*/18).string(message.value); return writer; }; /** * Encodes the specified LocalStorageItem message, length delimited. Does not implicitly {@link LocalStorageItem.verify|verify} messages. * @function encodeDelimited * @memberof LocalStorageItem * @static * @param {ILocalStorageItem} message LocalStorageItem message or plain object to encode * @param {$protobuf.Writer} [writer] Writer to encode to * @returns {$protobuf.Writer} Writer */ LocalStorageItem.encodeDelimited = function encodeDelimited(message, writer) { return this.encode(message, writer).ldelim(); }; /** * Decodes a LocalStorageItem message from the specified reader or buffer. * @function decode * @memberof LocalStorageItem * @static * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from * @param {number} [length] Message length if known beforehand * @returns {LocalStorageItem} LocalStorageItem * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ LocalStorageItem.decode = function decode(reader, length) { if (!(reader instanceof $Reader)) reader = $Reader.create(reader); let end = length === undefined ? reader.len : reader.pos + length, message = new $root.LocalStorageItem(); while (reader.pos < end) { let tag = reader.uint32(); switch (tag >>> 3) { case 1: { message.key = reader.string(); break; } case 2: { message.value = reader.string(); break; } default: reader.skipType(tag & 7); break; } } return message; }; /** * Decodes a LocalStorageItem message from the specified reader or buffer, length delimited. * @function decodeDelimited * @memberof LocalStorageItem * @static * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from * @returns {LocalStorageItem} LocalStorageItem * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ LocalStorageItem.decodeDelimited = function decodeDelimited(reader) { if (!(reader instanceof $Reader)) reader = new $Reader(reader); return this.decode(reader, reader.uint32()); }; /** * Verifies a LocalStorageItem message. * @function verify * @memberof LocalStorageItem * @static * @param {Object.} message Plain object to verify * @returns {string|null} `null` if valid, otherwise the reason why it is not */ LocalStorageItem.verify = function verify(message) { if (typeof message !== "object" || message === null) return "object expected"; if (message.key != null && message.hasOwnProperty("key")) if (!$util.isString(message.key)) return "key: string expected"; if (message.value != null && message.hasOwnProperty("value")) if (!$util.isString(message.value)) return "value: string expected"; return null; }; /** * Creates a LocalStorageItem message from a plain object. Also converts values to their respective internal types. * @function fromObject * @memberof LocalStorageItem * @static * @param {Object.} object Plain object * @returns {LocalStorageItem} LocalStorageItem */ LocalStorageItem.fromObject = function fromObject(object) { if (object instanceof $root.LocalStorageItem) return object; let message = new $root.LocalStorageItem(); if (object.key != null) message.key = String(object.key); if (object.value != null) message.value = String(object.value); return message; }; /** * Creates a plain object from a LocalStorageItem message. Also converts values to other types if specified. * @function toObject * @memberof LocalStorageItem * @static * @param {LocalStorageItem} message LocalStorageItem * @param {$protobuf.IConversionOptions} [options] Conversion options * @returns {Object.} Plain object */ LocalStorageItem.toObject = function toObject(message, options) { if (!options) options = {}; let object = {}; if (options.defaults) { object.key = ""; object.value = ""; } if (message.key != null && message.hasOwnProperty("key")) object.key = message.key; if (message.value != null && message.hasOwnProperty("value")) object.value = message.value; return object; }; /** * Converts this LocalStorageItem to JSON. * @function toJSON * @memberof LocalStorageItem * @instance * @returns {Object.} JSON object */ LocalStorageItem.prototype.toJSON = function toJSON() { return this.constructor.toObject(this, $protobuf.util.toJSONOptions); }; /** * Gets the default type url for LocalStorageItem * @function getTypeUrl * @memberof LocalStorageItem * @static * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") * @returns {string} The default type url */ LocalStorageItem.getTypeUrl = function getTypeUrl(typeUrlPrefix) { if (typeUrlPrefix === undefined) { typeUrlPrefix = "type.googleapis.com"; } return typeUrlPrefix + "/LocalStorageItem"; }; return LocalStorageItem; })(); export const DomainCookie = $root.DomainCookie = (() => { /** * Properties of a DomainCookie. * @exports IDomainCookie * @interface IDomainCookie * @property {number|Long|null} [createTime] DomainCookie createTime * @property {number|Long|null} [updateTime] DomainCookie updateTime * @property {Array.|null} [cookies] DomainCookie cookies * @property {Array.|null} [localStorageItems] DomainCookie localStorageItems * @property {string|null} [userAgent] DomainCookie userAgent */ /** * Constructs a new DomainCookie. * @exports DomainCookie * @classdesc Represents a DomainCookie. * @implements IDomainCookie * @constructor * @param {IDomainCookie=} [properties] Properties to set */ function DomainCookie(properties) { this.cookies = []; this.localStorageItems = []; if (properties) for (let keys = Object.keys(properties), i = 0; i < keys.length; ++i) if (properties[keys[i]] != null) this[keys[i]] = properties[keys[i]]; } /** * DomainCookie createTime. * @member {number|Long} createTime * @memberof DomainCookie * @instance */ DomainCookie.prototype.createTime = $util.Long ? $util.Long.fromBits(0,0,false) : 0; /** * DomainCookie updateTime. * @member {number|Long} updateTime * @memberof DomainCookie * @instance */ DomainCookie.prototype.updateTime = $util.Long ? $util.Long.fromBits(0,0,false) : 0; /** * DomainCookie cookies. * @member {Array.} cookies * @memberof DomainCookie * @instance */ DomainCookie.prototype.cookies = $util.emptyArray; /** * DomainCookie localStorageItems. * @member {Array.} localStorageItems * @memberof DomainCookie * @instance */ DomainCookie.prototype.localStorageItems = $util.emptyArray; /** * DomainCookie userAgent. * @member {string} userAgent * @memberof DomainCookie * @instance */ DomainCookie.prototype.userAgent = ""; /** * Creates a new DomainCookie instance using the specified properties. * @function create * @memberof DomainCookie * @static * @param {IDomainCookie=} [properties] Properties to set * @returns {DomainCookie} DomainCookie instance */ DomainCookie.create = function create(properties) { return new DomainCookie(properties); }; /** * Encodes the specified DomainCookie message. Does not implicitly {@link DomainCookie.verify|verify} messages. * @function encode * @memberof DomainCookie * @static * @param {IDomainCookie} message DomainCookie message or plain object to encode * @param {$protobuf.Writer} [writer] Writer to encode to * @returns {$protobuf.Writer} Writer */ DomainCookie.encode = function encode(message, writer) { if (!writer) writer = $Writer.create(); if (message.createTime != null && Object.hasOwnProperty.call(message, "createTime")) writer.uint32(/* id 1, wireType 0 =*/8).int64(message.createTime); if (message.updateTime != null && Object.hasOwnProperty.call(message, "updateTime")) writer.uint32(/* id 2, wireType 0 =*/16).int64(message.updateTime); if (message.cookies != null && message.cookies.length) for (let i = 0; i < message.cookies.length; ++i) $root.Cookie.encode(message.cookies[i], writer.uint32(/* id 5, wireType 2 =*/42).fork()).ldelim(); if (message.localStorageItems != null && message.localStorageItems.length) for (let i = 0; i < message.localStorageItems.length; ++i) $root.LocalStorageItem.encode(message.localStorageItems[i], writer.uint32(/* id 6, wireType 2 =*/50).fork()).ldelim(); if (message.userAgent != null && Object.hasOwnProperty.call(message, "userAgent")) writer.uint32(/* id 7, wireType 2 =*/58).string(message.userAgent); return writer; }; /** * Encodes the specified DomainCookie message, length delimited. Does not implicitly {@link DomainCookie.verify|verify} messages. * @function encodeDelimited * @memberof DomainCookie * @static * @param {IDomainCookie} message DomainCookie message or plain object to encode * @param {$protobuf.Writer} [writer] Writer to encode to * @returns {$protobuf.Writer} Writer */ DomainCookie.encodeDelimited = function encodeDelimited(message, writer) { return this.encode(message, writer).ldelim(); }; /** * Decodes a DomainCookie message from the specified reader or buffer. * @function decode * @memberof DomainCookie * @static * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from * @param {number} [length] Message length if known beforehand * @returns {DomainCookie} DomainCookie * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ DomainCookie.decode = function decode(reader, length) { if (!(reader instanceof $Reader)) reader = $Reader.create(reader); let end = length === undefined ? reader.len : reader.pos + length, message = new $root.DomainCookie(); while (reader.pos < end) { let tag = reader.uint32(); switch (tag >>> 3) { case 1: { message.createTime = reader.int64(); break; } case 2: { message.updateTime = reader.int64(); break; } case 5: { if (!(message.cookies && message.cookies.length)) message.cookies = []; message.cookies.push($root.Cookie.decode(reader, reader.uint32())); break; } case 6: { if (!(message.localStorageItems && message.localStorageItems.length)) message.localStorageItems = []; message.localStorageItems.push($root.LocalStorageItem.decode(reader, reader.uint32())); break; } case 7: { message.userAgent = reader.string(); break; } default: reader.skipType(tag & 7); break; } } return message; }; /** * Decodes a DomainCookie message from the specified reader or buffer, length delimited. * @function decodeDelimited * @memberof DomainCookie * @static * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from * @returns {DomainCookie} DomainCookie * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ DomainCookie.decodeDelimited = function decodeDelimited(reader) { if (!(reader instanceof $Reader)) reader = new $Reader(reader); return this.decode(reader, reader.uint32()); }; /** * Verifies a DomainCookie message. * @function verify * @memberof DomainCookie * @static * @param {Object.} message Plain object to verify * @returns {string|null} `null` if valid, otherwise the reason why it is not */ DomainCookie.verify = function verify(message) { if (typeof message !== "object" || message === null) return "object expected"; if (message.createTime != null && message.hasOwnProperty("createTime")) if (!$util.isInteger(message.createTime) && !(message.createTime && $util.isInteger(message.createTime.low) && $util.isInteger(message.createTime.high))) return "createTime: integer|Long expected"; if (message.updateTime != null && message.hasOwnProperty("updateTime")) if (!$util.isInteger(message.updateTime) && !(message.updateTime && $util.isInteger(message.updateTime.low) && $util.isInteger(message.updateTime.high))) return "updateTime: integer|Long expected"; if (message.cookies != null && message.hasOwnProperty("cookies")) { if (!Array.isArray(message.cookies)) return "cookies: array expected"; for (let i = 0; i < message.cookies.length; ++i) { let error = $root.Cookie.verify(message.cookies[i]); if (error) return "cookies." + error; } } if (message.localStorageItems != null && message.hasOwnProperty("localStorageItems")) { if (!Array.isArray(message.localStorageItems)) return "localStorageItems: array expected"; for (let i = 0; i < message.localStorageItems.length; ++i) { let error = $root.LocalStorageItem.verify(message.localStorageItems[i]); if (error) return "localStorageItems." + error; } } if (message.userAgent != null && message.hasOwnProperty("userAgent")) if (!$util.isString(message.userAgent)) return "userAgent: string expected"; return null; }; /** * Creates a DomainCookie message from a plain object. Also converts values to their respective internal types. * @function fromObject * @memberof DomainCookie * @static * @param {Object.} object Plain object * @returns {DomainCookie} DomainCookie */ DomainCookie.fromObject = function fromObject(object) { if (object instanceof $root.DomainCookie) return object; let message = new $root.DomainCookie(); if (object.createTime != null) if ($util.Long) (message.createTime = $util.Long.fromValue(object.createTime)).unsigned = false; else if (typeof object.createTime === "string") message.createTime = parseInt(object.createTime, 10); else if (typeof object.createTime === "number") message.createTime = object.createTime; else if (typeof object.createTime === "object") message.createTime = new $util.LongBits(object.createTime.low >>> 0, object.createTime.high >>> 0).toNumber(); if (object.updateTime != null) if ($util.Long) (message.updateTime = $util.Long.fromValue(object.updateTime)).unsigned = false; else if (typeof object.updateTime === "string") message.updateTime = parseInt(object.updateTime, 10); else if (typeof object.updateTime === "number") message.updateTime = object.updateTime; else if (typeof object.updateTime === "object") message.updateTime = new $util.LongBits(object.updateTime.low >>> 0, object.updateTime.high >>> 0).toNumber(); if (object.cookies) { if (!Array.isArray(object.cookies)) throw TypeError(".DomainCookie.cookies: array expected"); message.cookies = []; for (let i = 0; i < object.cookies.length; ++i) { if (typeof object.cookies[i] !== "object") throw TypeError(".DomainCookie.cookies: object expected"); message.cookies[i] = $root.Cookie.fromObject(object.cookies[i]); } } if (object.localStorageItems) { if (!Array.isArray(object.localStorageItems)) throw TypeError(".DomainCookie.localStorageItems: array expected"); message.localStorageItems = []; for (let i = 0; i < object.localStorageItems.length; ++i) { if (typeof object.localStorageItems[i] !== "object") throw TypeError(".DomainCookie.localStorageItems: object expected"); message.localStorageItems[i] = $root.LocalStorageItem.fromObject(object.localStorageItems[i]); } } if (object.userAgent != null) message.userAgent = String(object.userAgent); return message; }; /** * Creates a plain object from a DomainCookie message. Also converts values to other types if specified. * @function toObject * @memberof DomainCookie * @static * @param {DomainCookie} message DomainCookie * @param {$protobuf.IConversionOptions} [options] Conversion options * @returns {Object.} Plain object */ DomainCookie.toObject = function toObject(message, options) { if (!options) options = {}; let object = {}; if (options.arrays || options.defaults) { object.cookies = []; object.localStorageItems = []; } if (options.defaults) { if ($util.Long) { let long = new $util.Long(0, 0, false); object.createTime = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; } else object.createTime = options.longs === String ? "0" : 0; if ($util.Long) { let long = new $util.Long(0, 0, false); object.updateTime = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; } else object.updateTime = options.longs === String ? "0" : 0; object.userAgent = ""; } if (message.createTime != null && message.hasOwnProperty("createTime")) if (typeof message.createTime === "number") object.createTime = options.longs === String ? String(message.createTime) : message.createTime; else object.createTime = options.longs === String ? $util.Long.prototype.toString.call(message.createTime) : options.longs === Number ? new $util.LongBits(message.createTime.low >>> 0, message.createTime.high >>> 0).toNumber() : message.createTime; if (message.updateTime != null && message.hasOwnProperty("updateTime")) if (typeof message.updateTime === "number") object.updateTime = options.longs === String ? String(message.updateTime) : message.updateTime; else object.updateTime = options.longs === String ? $util.Long.prototype.toString.call(message.updateTime) : options.longs === Number ? new $util.LongBits(message.updateTime.low >>> 0, message.updateTime.high >>> 0).toNumber() : message.updateTime; if (message.cookies && message.cookies.length) { object.cookies = []; for (let j = 0; j < message.cookies.length; ++j) object.cookies[j] = $root.Cookie.toObject(message.cookies[j], options); } if (message.localStorageItems && message.localStorageItems.length) { object.localStorageItems = []; for (let j = 0; j < message.localStorageItems.length; ++j) object.localStorageItems[j] = $root.LocalStorageItem.toObject(message.localStorageItems[j], options); } if (message.userAgent != null && message.hasOwnProperty("userAgent")) object.userAgent = message.userAgent; return object; }; /** * Converts this DomainCookie to JSON. * @function toJSON * @memberof DomainCookie * @instance * @returns {Object.} JSON object */ DomainCookie.prototype.toJSON = function toJSON() { return this.constructor.toObject(this, $protobuf.util.toJSONOptions); }; /** * Gets the default type url for DomainCookie * @function getTypeUrl * @memberof DomainCookie * @static * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") * @returns {string} The default type url */ DomainCookie.getTypeUrl = function getTypeUrl(typeUrlPrefix) { if (typeUrlPrefix === undefined) { typeUrlPrefix = "type.googleapis.com"; } return typeUrlPrefix + "/DomainCookie"; }; return DomainCookie; })(); export const CookiesMap = $root.CookiesMap = (() => { /** * Properties of a CookiesMap. * @exports ICookiesMap * @interface ICookiesMap * @property {number|Long|null} [createTime] CookiesMap createTime * @property {number|Long|null} [updateTime] CookiesMap updateTime * @property {Object.|null} [domainCookieMap] CookiesMap domainCookieMap */ /** * Constructs a new CookiesMap. * @exports CookiesMap * @classdesc Represents a CookiesMap. * @implements ICookiesMap * @constructor * @param {ICookiesMap=} [properties] Properties to set */ function CookiesMap(properties) { this.domainCookieMap = {}; if (properties) for (let keys = Object.keys(properties), i = 0; i < keys.length; ++i) if (properties[keys[i]] != null) this[keys[i]] = properties[keys[i]]; } /** * CookiesMap createTime. * @member {number|Long} createTime * @memberof CookiesMap * @instance */ CookiesMap.prototype.createTime = $util.Long ? $util.Long.fromBits(0,0,false) : 0; /** * CookiesMap updateTime. * @member {number|Long} updateTime * @memberof CookiesMap * @instance */ CookiesMap.prototype.updateTime = $util.Long ? $util.Long.fromBits(0,0,false) : 0; /** * CookiesMap domainCookieMap. * @member {Object.} domainCookieMap * @memberof CookiesMap * @instance */ CookiesMap.prototype.domainCookieMap = $util.emptyObject; /** * Creates a new CookiesMap instance using the specified properties. * @function create * @memberof CookiesMap * @static * @param {ICookiesMap=} [properties] Properties to set * @returns {CookiesMap} CookiesMap instance */ CookiesMap.create = function create(properties) { return new CookiesMap(properties); }; /** * Encodes the specified CookiesMap message. Does not implicitly {@link CookiesMap.verify|verify} messages. * @function encode * @memberof CookiesMap * @static * @param {ICookiesMap} message CookiesMap message or plain object to encode * @param {$protobuf.Writer} [writer] Writer to encode to * @returns {$protobuf.Writer} Writer */ CookiesMap.encode = function encode(message, writer) { if (!writer) writer = $Writer.create(); if (message.createTime != null && Object.hasOwnProperty.call(message, "createTime")) writer.uint32(/* id 1, wireType 0 =*/8).int64(message.createTime); if (message.updateTime != null && Object.hasOwnProperty.call(message, "updateTime")) writer.uint32(/* id 2, wireType 0 =*/16).int64(message.updateTime); if (message.domainCookieMap != null && Object.hasOwnProperty.call(message, "domainCookieMap")) for (let keys = Object.keys(message.domainCookieMap), i = 0; i < keys.length; ++i) { writer.uint32(/* id 5, wireType 2 =*/42).fork().uint32(/* id 1, wireType 2 =*/10).string(keys[i]); $root.DomainCookie.encode(message.domainCookieMap[keys[i]], writer.uint32(/* id 2, wireType 2 =*/18).fork()).ldelim().ldelim(); } return writer; }; /** * Encodes the specified CookiesMap message, length delimited. Does not implicitly {@link CookiesMap.verify|verify} messages. * @function encodeDelimited * @memberof CookiesMap * @static * @param {ICookiesMap} message CookiesMap message or plain object to encode * @param {$protobuf.Writer} [writer] Writer to encode to * @returns {$protobuf.Writer} Writer */ CookiesMap.encodeDelimited = function encodeDelimited(message, writer) { return this.encode(message, writer).ldelim(); }; /** * Decodes a CookiesMap message from the specified reader or buffer. * @function decode * @memberof CookiesMap * @static * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from * @param {number} [length] Message length if known beforehand * @returns {CookiesMap} CookiesMap * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ CookiesMap.decode = function decode(reader, length) { if (!(reader instanceof $Reader)) reader = $Reader.create(reader); let end = length === undefined ? reader.len : reader.pos + length, message = new $root.CookiesMap(), key, value; while (reader.pos < end) { let tag = reader.uint32(); switch (tag >>> 3) { case 1: { message.createTime = reader.int64(); break; } case 2: { message.updateTime = reader.int64(); break; } case 5: { if (message.domainCookieMap === $util.emptyObject) message.domainCookieMap = {}; let end2 = reader.uint32() + reader.pos; key = ""; value = null; while (reader.pos < end2) { let tag2 = reader.uint32(); switch (tag2 >>> 3) { case 1: key = reader.string(); break; case 2: value = $root.DomainCookie.decode(reader, reader.uint32()); break; default: reader.skipType(tag2 & 7); break; } } message.domainCookieMap[key] = value; break; } default: reader.skipType(tag & 7); break; } } return message; }; /** * Decodes a CookiesMap message from the specified reader or buffer, length delimited. * @function decodeDelimited * @memberof CookiesMap * @static * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from * @returns {CookiesMap} CookiesMap * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ CookiesMap.decodeDelimited = function decodeDelimited(reader) { if (!(reader instanceof $Reader)) reader = new $Reader(reader); return this.decode(reader, reader.uint32()); }; /** * Verifies a CookiesMap message. * @function verify * @memberof CookiesMap * @static * @param {Object.} message Plain object to verify * @returns {string|null} `null` if valid, otherwise the reason why it is not */ CookiesMap.verify = function verify(message) { if (typeof message !== "object" || message === null) return "object expected"; if (message.createTime != null && message.hasOwnProperty("createTime")) if (!$util.isInteger(message.createTime) && !(message.createTime && $util.isInteger(message.createTime.low) && $util.isInteger(message.createTime.high))) return "createTime: integer|Long expected"; if (message.updateTime != null && message.hasOwnProperty("updateTime")) if (!$util.isInteger(message.updateTime) && !(message.updateTime && $util.isInteger(message.updateTime.low) && $util.isInteger(message.updateTime.high))) return "updateTime: integer|Long expected"; if (message.domainCookieMap != null && message.hasOwnProperty("domainCookieMap")) { if (!$util.isObject(message.domainCookieMap)) return "domainCookieMap: object expected"; let key = Object.keys(message.domainCookieMap); for (let i = 0; i < key.length; ++i) { let error = $root.DomainCookie.verify(message.domainCookieMap[key[i]]); if (error) return "domainCookieMap." + error; } } return null; }; /** * Creates a CookiesMap message from a plain object. Also converts values to their respective internal types. * @function fromObject * @memberof CookiesMap * @static * @param {Object.} object Plain object * @returns {CookiesMap} CookiesMap */ CookiesMap.fromObject = function fromObject(object) { if (object instanceof $root.CookiesMap) return object; let message = new $root.CookiesMap(); if (object.createTime != null) if ($util.Long) (message.createTime = $util.Long.fromValue(object.createTime)).unsigned = false; else if (typeof object.createTime === "string") message.createTime = parseInt(object.createTime, 10); else if (typeof object.createTime === "number") message.createTime = object.createTime; else if (typeof object.createTime === "object") message.createTime = new $util.LongBits(object.createTime.low >>> 0, object.createTime.high >>> 0).toNumber(); if (object.updateTime != null) if ($util.Long) (message.updateTime = $util.Long.fromValue(object.updateTime)).unsigned = false; else if (typeof object.updateTime === "string") message.updateTime = parseInt(object.updateTime, 10); else if (typeof object.updateTime === "number") message.updateTime = object.updateTime; else if (typeof object.updateTime === "object") message.updateTime = new $util.LongBits(object.updateTime.low >>> 0, object.updateTime.high >>> 0).toNumber(); if (object.domainCookieMap) { if (typeof object.domainCookieMap !== "object") throw TypeError(".CookiesMap.domainCookieMap: object expected"); message.domainCookieMap = {}; for (let keys = Object.keys(object.domainCookieMap), i = 0; i < keys.length; ++i) { if (typeof object.domainCookieMap[keys[i]] !== "object") throw TypeError(".CookiesMap.domainCookieMap: object expected"); message.domainCookieMap[keys[i]] = $root.DomainCookie.fromObject(object.domainCookieMap[keys[i]]); } } return message; }; /** * Creates a plain object from a CookiesMap message. Also converts values to other types if specified. * @function toObject * @memberof CookiesMap * @static * @param {CookiesMap} message CookiesMap * @param {$protobuf.IConversionOptions} [options] Conversion options * @returns {Object.} Plain object */ CookiesMap.toObject = function toObject(message, options) { if (!options) options = {}; let object = {}; if (options.objects || options.defaults) object.domainCookieMap = {}; if (options.defaults) { if ($util.Long) { let long = new $util.Long(0, 0, false); object.createTime = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; } else object.createTime = options.longs === String ? "0" : 0; if ($util.Long) { let long = new $util.Long(0, 0, false); object.updateTime = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; } else object.updateTime = options.longs === String ? "0" : 0; } if (message.createTime != null && message.hasOwnProperty("createTime")) if (typeof message.createTime === "number") object.createTime = options.longs === String ? String(message.createTime) : message.createTime; else object.createTime = options.longs === String ? $util.Long.prototype.toString.call(message.createTime) : options.longs === Number ? new $util.LongBits(message.createTime.low >>> 0, message.createTime.high >>> 0).toNumber() : message.createTime; if (message.updateTime != null && message.hasOwnProperty("updateTime")) if (typeof message.updateTime === "number") object.updateTime = options.longs === String ? String(message.updateTime) : message.updateTime; else object.updateTime = options.longs === String ? $util.Long.prototype.toString.call(message.updateTime) : options.longs === Number ? new $util.LongBits(message.updateTime.low >>> 0, message.updateTime.high >>> 0).toNumber() : message.updateTime; let keys2; if (message.domainCookieMap && (keys2 = Object.keys(message.domainCookieMap)).length) { object.domainCookieMap = {}; for (let j = 0; j < keys2.length; ++j) object.domainCookieMap[keys2[j]] = $root.DomainCookie.toObject(message.domainCookieMap[keys2[j]], options); } return object; }; /** * Converts this CookiesMap to JSON. * @function toJSON * @memberof CookiesMap * @instance * @returns {Object.} JSON object */ CookiesMap.prototype.toJSON = function toJSON() { return this.constructor.toObject(this, $protobuf.util.toJSONOptions); }; /** * Gets the default type url for CookiesMap * @function getTypeUrl * @memberof CookiesMap * @static * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") * @returns {string} The default type url */ CookiesMap.getTypeUrl = function getTypeUrl(typeUrlPrefix) { if (typeUrlPrefix === undefined) { typeUrlPrefix = "type.googleapis.com"; } return typeUrlPrefix + "/CookiesMap"; }; return CookiesMap; })(); export { $root as default }; ================================================ FILE: packages/protobuf/package.json ================================================ { "name": "@sync-your-cookie/protobuf", "version": "0.0.1", "description": "chrome extension protobuf code", "private": true, "sideEffects": false, "files": [ "dist/**" ], "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "scripts": { "clean": "rimraf ./dist && rimraf .turbo", "build": "tsup index.ts --format esm,cjs --dts --external react,chrome", "dev": "tsc -w", "copy:proto": "cp -r ./lib/protobuf/proto ./dist/lib/protobuf", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "pnpm lint --fix", "prettier": "prettier . --write", "type-check": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", "proto": "pbjs -o ./lib/protobuf/proto/cookie.js -w es6 -t static-module ./proto/*.proto && pbts ./lib/protobuf/proto/cookie.js -o ./lib/protobuf/proto/cookie.d.ts" }, "dependencies": { "pako": "^2.1.0", "protobufjs": "^7.3.2" }, "devDependencies": { "@sync-your-cookie/tsconfig": "workspace:*", "@types/pako": "^2.0.3", "protobufjs-cli": "^1.1.3", "tsup": "8.0.2", "tsx": "^4.19.1", "vitest": "^1.6.0" } } ================================================ FILE: packages/protobuf/proto/cookie.proto ================================================ syntax = "proto3"; message Cookie { string domain = 1; string name = 2; string storeId = 3; string value = 4; bool session = 5; bool hostOnly = 6; float expirationDate = 7; string path = 8; bool httpOnly = 9; bool secure = 10; string sameSite = 11; } message LocalStorageItem { string key = 1; string value = 2; } message DomainCookie { int64 createTime = 1; int64 updateTime = 2; repeated Cookie cookies = 5; repeated LocalStorageItem localStorageItems = 6; string userAgent = 7; } message CookiesMap { int64 createTime = 1; int64 updateTime = 2; map domainCookieMap = 5; } ================================================ FILE: packages/protobuf/tsconfig.json ================================================ { "extends": "@sync-your-cookie/tsconfig/utils", "compilerOptions": { "outDir": "dist", "jsx": "react-jsx", "checkJs": false, "allowJs": false, "baseUrl": ".", "paths": { "@lib/*": ["lib/*"] }, "types": ["chrome"] }, "exclude": ["code-test.ts"], "include": ["index.ts", "lib"], } ================================================ FILE: packages/protobuf/tsup.config.ts ================================================ import { defineConfig } from 'tsup'; export default defineConfig({ treeshake: true, format: ['cjs', 'esm'], dts: true, external: ['chrome'], }); ================================================ FILE: packages/protobuf/utils/base64.ts ================================================ export function arrayBufferToBase64(arrayBuffer: ArrayBuffer) { let base64 = ''; const encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; const bytes = new Uint8Array(arrayBuffer); const byteLength = bytes.byteLength; const byteRemainder = byteLength % 3; const mainLength = byteLength - byteRemainder; let a, b, c, d; let chunk; // Main loop deals with bytes in chunks of 3 for (let i = 0; i < mainLength; i = i + 3) { // Combine the three bytes into a single integer chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; // Use bitmasks to extract 6-bit segments from the triplet a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18 b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12 c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6 d = chunk & 63; // 63 = 2^6 - 1 // Convert the raw binary segments to the appropriate ASCII encoding base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]; } // Deal with the remaining bytes and padding if (byteRemainder == 1) { chunk = bytes[mainLength]; a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2 // Set the 4 least significant bits to zero b = (chunk & 3) << 4; // 3 = 2^2 - 1 base64 += encodings[a] + encodings[b] + '=='; } else if (byteRemainder == 2) { chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]; a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10 b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4 // Set the 2 least significant bits to zero c = (chunk & 15) << 2; // 15 = 2^4 - 1 base64 += encodings[a] + encodings[b] + encodings[c] + '='; } return base64; } export function base64ToArrayBuffer(base64: string) { const binaryString = atob(base64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes; } ================================================ FILE: packages/protobuf/utils/compress.ts ================================================ async function concatUint8Arrays(uint8arrays: ArrayBuffer[]) { const blob = new Blob(uint8arrays); const buffer = await blob.arrayBuffer(); return new Uint8Array(buffer); } /** * Compress a string into a Uint8Array. * @param byteArray * @param method * @returns Promise */ export const compress = async (byteArray: Uint8Array, method: CompressionFormat = 'gzip'): Promise => { const stream = new Blob([byteArray]).stream(); // const byteArray: Uint8Array = new TextEncoder().encode(string); const compressedStream = stream.pipeThrough(new CompressionStream(method)) as unknown as ArrayBuffer[]; const chunks: ArrayBuffer[] = []; for await (const chunk of compressedStream) { chunks.push(chunk); } return await concatUint8Arrays(chunks); }; /** * Decompress bytes into a Uint8Array. * * @param {Uint8Array} compressedBytes * @returns {Promise} */ export async function decompress(compressedBytes: Uint8Array) { // Convert the bytes to a stream. const stream = new Blob([compressedBytes]).stream(); // Create a decompressed stream. const decompressedStream = stream.pipeThrough(new DecompressionStream('gzip')) as unknown as ArrayBuffer[]; // Read all the bytes from this stream. const chunks = []; for await (const chunk of decompressedStream) { chunks.push(chunk); } const stringBytes = await concatUint8Arrays(chunks); return stringBytes; } ================================================ FILE: packages/protobuf/utils/encryption.test.ts ================================================ import { describe, it, expect } from 'vitest'; import { encrypt, decrypt, isEncrypted, encryptBase64, decryptBase64, isBase64Encrypted, } from './encryption'; // Helper to create test data function createTestData(size: number): Uint8Array { const data = new Uint8Array(size); for (let i = 0; i < size; i++) { data[i] = i % 256; } return data; } // Helper to convert string to Uint8Array function stringToUint8Array(str: string): Uint8Array { return new TextEncoder().encode(str); } // Helper to convert Uint8Array to string function uint8ArrayToString(arr: Uint8Array): string { return new TextDecoder().decode(arr); } describe('Encryption Module', () => { describe('encrypt and decrypt', () => { it('should encrypt and decrypt small data correctly', async () => { const originalData = stringToUint8Array('Hello, World!'); const password = 'test-password-123'; const encrypted = await encrypt(originalData, password); const decrypted = await decrypt(encrypted, password); expect(uint8ArrayToString(decrypted)).toBe('Hello, World!'); }); it('should encrypt and decrypt empty data', async () => { const originalData = new Uint8Array(0); const password = 'test-password'; const encrypted = await encrypt(originalData, password); const decrypted = await decrypt(encrypted, password); expect(decrypted.length).toBe(0); }); it('should encrypt and decrypt large data (1MB)', async () => { const originalData = createTestData(1024 * 1024); // 1MB const password = 'strong-password-456'; const encrypted = await encrypt(originalData, password); const decrypted = await decrypt(encrypted, password); expect(decrypted).toEqual(originalData); }); it('should encrypt and decrypt binary data', async () => { const originalData = new Uint8Array([0, 1, 2, 255, 254, 253, 128, 127]); const password = 'binary-test'; const encrypted = await encrypt(originalData, password); const decrypted = await decrypt(encrypted, password); expect(decrypted).toEqual(originalData); }); it('should produce different ciphertext for same plaintext (random IV)', async () => { const originalData = stringToUint8Array('Same message'); const password = 'same-password'; const encrypted1 = await encrypt(originalData, password); const encrypted2 = await encrypt(originalData, password); // Ciphertexts should be different due to random IV and salt expect(encrypted1).not.toEqual(encrypted2); // But both should decrypt to the same plaintext const decrypted1 = await decrypt(encrypted1, password); const decrypted2 = await decrypt(encrypted2, password); expect(decrypted1).toEqual(decrypted2); }); it('should fail decryption with wrong password', async () => { const originalData = stringToUint8Array('Secret message'); const correctPassword = 'correct-password'; const wrongPassword = 'wrong-password'; const encrypted = await encrypt(originalData, correctPassword); await expect(decrypt(encrypted, wrongPassword)).rejects.toThrow( 'Decryption failed: incorrect password or corrupted data', ); }); it('should fail decryption with corrupted data', async () => { const originalData = stringToUint8Array('Test data'); const password = 'test-password'; const encrypted = await encrypt(originalData, password); // Corrupt the ciphertext (modify bytes after the header) encrypted[encrypted.length - 1] ^= 0xff; await expect(decrypt(encrypted, password)).rejects.toThrow(); }); it('should fail decryption with invalid magic bytes', async () => { const fakeEncrypted = new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x01, ...new Array(33).fill(0)]); await expect(decrypt(fakeEncrypted, 'any-password')).rejects.toThrow( 'Invalid encrypted data: magic bytes mismatch', ); }); it('should fail decryption with unsupported version', async () => { // Create data with correct magic bytes but wrong version const fakeEncrypted = new Uint8Array([ 0x53, 0x59, 0x43, 0x45, // SYCE magic bytes 0x99, // Invalid version ...new Array(33).fill(0), ]); await expect(decrypt(fakeEncrypted, 'any-password')).rejects.toThrow('Unsupported encryption version: 153'); }); it('should handle unicode strings correctly', async () => { const originalData = stringToUint8Array('Hello 世界 🌍 Привет'); const password = 'unicode-test-密码'; const encrypted = await encrypt(originalData, password); const decrypted = await decrypt(encrypted, password); expect(uint8ArrayToString(decrypted)).toBe('Hello 世界 🌍 Привет'); }); it('should handle special characters in password', async () => { const originalData = stringToUint8Array('Test data'); const password = '!@#$%^&*()_+-=[]{}|;:,.<>?`~"\'\\'; const encrypted = await encrypt(originalData, password); const decrypted = await decrypt(encrypted, password); expect(uint8ArrayToString(decrypted)).toBe('Test data'); }); it('should handle very long passwords', async () => { const originalData = stringToUint8Array('Test data'); const password = 'a'.repeat(10000); const encrypted = await encrypt(originalData, password); const decrypted = await decrypt(encrypted, password); expect(uint8ArrayToString(decrypted)).toBe('Test data'); }); it('should handle empty password', async () => { const originalData = stringToUint8Array('Test data'); const password = ''; const encrypted = await encrypt(originalData, password); const decrypted = await decrypt(encrypted, password); expect(uint8ArrayToString(decrypted)).toBe('Test data'); }); }); describe('isEncrypted', () => { it('should return true for encrypted data', async () => { const originalData = stringToUint8Array('Test'); const encrypted = await encrypt(originalData, 'password'); expect(isEncrypted(encrypted)).toBe(true); }); it('should return false for non-encrypted data', () => { const plainData = stringToUint8Array('Plain text data'); expect(isEncrypted(plainData)).toBe(false); }); it('should return false for data shorter than magic bytes', () => { const shortData = new Uint8Array([0x53, 0x59]); // Only 2 bytes expect(isEncrypted(shortData)).toBe(false); }); it('should return false for empty data', () => { const emptyData = new Uint8Array(0); expect(isEncrypted(emptyData)).toBe(false); }); it('should return false for data with partial magic bytes match', () => { const partialMatch = new Uint8Array([0x53, 0x59, 0x43, 0x00]); // SYCA instead of SYCE expect(isEncrypted(partialMatch)).toBe(false); }); }); describe('encryptBase64 and decryptBase64', () => { it('should encrypt and decrypt base64 strings', async () => { const originalBase64 = btoa('Hello, World!'); const password = 'test-password'; const encryptedBase64 = await encryptBase64(originalBase64, password); const decryptedBase64 = await decryptBase64(encryptedBase64, password); expect(decryptedBase64).toBe(originalBase64); }); it('should handle complex base64 data', async () => { // Create some binary data and convert to base64 const binaryData = new Uint8Array([0, 127, 128, 255, 1, 2, 3]); const originalBase64 = btoa(String.fromCharCode(...binaryData)); const password = 'complex-test'; const encryptedBase64 = await encryptBase64(originalBase64, password); const decryptedBase64 = await decryptBase64(encryptedBase64, password); expect(decryptedBase64).toBe(originalBase64); }); it('should fail with wrong password', async () => { const originalBase64 = btoa('Secret data'); const encryptedBase64 = await encryptBase64(originalBase64, 'correct-password'); await expect(decryptBase64(encryptedBase64, 'wrong-password')).rejects.toThrow(); }); it('should handle empty base64 string', async () => { const originalBase64 = btoa(''); const password = 'test'; const encryptedBase64 = await encryptBase64(originalBase64, password); const decryptedBase64 = await decryptBase64(encryptedBase64, password); expect(decryptedBase64).toBe(originalBase64); }); }); describe('isBase64Encrypted', () => { it('should return true for encrypted base64 data', async () => { const originalBase64 = btoa('Test data'); const encryptedBase64 = await encryptBase64(originalBase64, 'password'); expect(isBase64Encrypted(encryptedBase64)).toBe(true); }); it('should return false for plain base64 data', () => { const plainBase64 = btoa('Plain text'); expect(isBase64Encrypted(plainBase64)).toBe(false); }); it('should return false for invalid base64 string', () => { const invalidBase64 = '!!!not-valid-base64!!!'; expect(isBase64Encrypted(invalidBase64)).toBe(false); }); it('should return false for empty string', () => { expect(isBase64Encrypted('')).toBe(false); }); it('should return false for JSON-like base64', () => { const jsonBase64 = btoa('{"key": "value"}'); expect(isBase64Encrypted(jsonBase64)).toBe(false); }); }); describe('Security properties', () => { it('encrypted data should be larger than original due to header', async () => { const originalData = stringToUint8Array('Test'); const encrypted = await encrypt(originalData, 'password'); // Header size: 4 (magic) + 1 (version) + 16 (salt) + 12 (IV) = 33 bytes // Plus authentication tag from GCM (16 bytes) expect(encrypted.length).toBeGreaterThan(originalData.length + 33); }); it('should have correct magic bytes at the start', async () => { const originalData = stringToUint8Array('Test'); const encrypted = await encrypt(originalData, 'password'); // Check SYCE magic bytes expect(encrypted[0]).toBe(0x53); // S expect(encrypted[1]).toBe(0x59); // Y expect(encrypted[2]).toBe(0x43); // C expect(encrypted[3]).toBe(0x45); // E }); it('should have version 1 in the header', async () => { const originalData = stringToUint8Array('Test'); const encrypted = await encrypt(originalData, 'password'); expect(encrypted[4]).toBe(1); // Version }); }); }); ================================================ FILE: packages/protobuf/utils/encryption.ts ================================================ /** * End-to-end encryption utilities using Web Crypto API. * Uses AES-GCM for authenticated encryption and PBKDF2 for password-based key derivation. */ // Constants for encryption const ALGORITHM = 'AES-GCM'; const KEY_LENGTH = 256; const PBKDF2_ITERATIONS = 100000; const SALT_LENGTH = 16; const IV_LENGTH = 12; // Magic bytes to identify encrypted data (ASCII: "SYCE" - Sync Your Cookie Encrypted) const MAGIC_BYTES = new Uint8Array([0x53, 0x59, 0x43, 0x45]); const VERSION = 1; /** * Derives a cryptographic key from a password using PBKDF2. */ async function deriveKey(password: string, salt: Uint8Array): Promise { const encoder = new TextEncoder(); const passwordKey = await crypto.subtle.importKey('raw', encoder.encode(password), 'PBKDF2', false, [ 'deriveKey', ]); return crypto.subtle.deriveKey( { name: 'PBKDF2', salt, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256', }, passwordKey, { name: ALGORITHM, length: KEY_LENGTH, }, false, ['encrypt', 'decrypt'], ); } /** * Encrypts data using AES-GCM with a password-derived key. * * Output format: * [MAGIC_BYTES (4)] [VERSION (1)] [SALT (16)] [IV (12)] [CIPHERTEXT (...)] * * @param data - The data to encrypt (Uint8Array) * @param password - The password to derive the encryption key from * @returns Promise - The encrypted data with header */ export async function encrypt(data: Uint8Array, password: string): Promise { // Generate random salt and IV const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH)); const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)); // Derive key from password const key = await deriveKey(password, salt); // Encrypt the data const ciphertext = await crypto.subtle.encrypt( { name: ALGORITHM, iv, }, key, data, ); // Combine: MAGIC + VERSION + SALT + IV + CIPHERTEXT const result = new Uint8Array(MAGIC_BYTES.length + 1 + SALT_LENGTH + IV_LENGTH + ciphertext.byteLength); let offset = 0; result.set(MAGIC_BYTES, offset); offset += MAGIC_BYTES.length; result[offset] = VERSION; offset += 1; result.set(salt, offset); offset += SALT_LENGTH; result.set(iv, offset); offset += IV_LENGTH; result.set(new Uint8Array(ciphertext), offset); return result; } /** * Decrypts data that was encrypted with the encrypt() function. * * @param encryptedData - The encrypted data with header (Uint8Array) * @param password - The password to derive the decryption key from * @returns Promise - The decrypted data * @throws Error if decryption fails or data is invalid */ export async function decrypt(encryptedData: Uint8Array, password: string): Promise { let offset = 0; // Verify magic bytes const magic = encryptedData.slice(offset, offset + MAGIC_BYTES.length); offset += MAGIC_BYTES.length; if (!magic.every((byte, i) => byte === MAGIC_BYTES[i])) { throw new Error('Invalid encrypted data: magic bytes mismatch'); } // Check version const version = encryptedData[offset]; offset += 1; if (version !== VERSION) { throw new Error(`Unsupported encryption version: ${version}`); } // Extract salt const salt = encryptedData.slice(offset, offset + SALT_LENGTH); offset += SALT_LENGTH; // Extract IV const iv = encryptedData.slice(offset, offset + IV_LENGTH); offset += IV_LENGTH; // Extract ciphertext const ciphertext = encryptedData.slice(offset); // Derive key from password const key = await deriveKey(password, salt); // Decrypt the data try { const decrypted = await crypto.subtle.decrypt( { name: ALGORITHM, iv, }, key, ciphertext, ); return new Uint8Array(decrypted); } catch { throw new Error('Decryption failed: incorrect password or corrupted data'); } } /** * Checks if data appears to be encrypted (starts with magic bytes). * * @param data - The data to check (Uint8Array) * @returns boolean - True if data appears to be encrypted */ export function isEncrypted(data: Uint8Array): boolean { if (data.length < MAGIC_BYTES.length) { return false; } return data.slice(0, MAGIC_BYTES.length).every((byte, i) => byte === MAGIC_BYTES[i]); } /** * Encrypts a base64 string using the provided password. * Returns a new base64 string containing the encrypted data. * * @param base64Data - The base64-encoded data to encrypt * @param password - The password to use for encryption * @returns Promise - The encrypted data as a base64 string */ export async function encryptBase64(base64Data: string, password: string): Promise { const binaryString = atob(base64Data); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } const encrypted = await encrypt(bytes, password); return arrayBufferToBase64ForEncryption(encrypted); } /** * Decrypts a base64 string that was encrypted with encryptBase64(). * Returns the original base64-encoded data. * * @param encryptedBase64 - The encrypted base64 string * @param password - The password to use for decryption * @returns Promise - The decrypted data as a base64 string */ export async function decryptBase64(encryptedBase64: string, password: string): Promise { const binaryString = atob(encryptedBase64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } const decrypted = await decrypt(bytes, password); return arrayBufferToBase64ForEncryption(decrypted); } /** * Checks if a base64 string appears to be encrypted data. * * @param base64Data - The base64 string to check * @returns boolean - True if the data appears to be encrypted */ export function isBase64Encrypted(base64Data: string): boolean { try { const binaryString = atob(base64Data); const bytes = new Uint8Array(Math.min(binaryString.length, MAGIC_BYTES.length)); for (let i = 0; i < bytes.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return isEncrypted(bytes); } catch { return false; } } // Helper function for base64 encoding (to avoid circular dependency) function arrayBufferToBase64ForEncryption(arrayBuffer: ArrayBuffer | Uint8Array) { let base64 = ''; const encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; const bytes = arrayBuffer instanceof Uint8Array ? arrayBuffer : new Uint8Array(arrayBuffer); const byteLength = bytes.byteLength; const byteRemainder = byteLength % 3; const mainLength = byteLength - byteRemainder; let a, b, c, d; let chunk; for (let i = 0; i < mainLength; i = i + 3) { chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; a = (chunk & 16515072) >> 18; b = (chunk & 258048) >> 12; c = (chunk & 4032) >> 6; d = chunk & 63; base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]; } if (byteRemainder == 1) { chunk = bytes[mainLength]; a = (chunk & 252) >> 2; b = (chunk & 3) << 4; base64 += encodings[a] + encodings[b] + '=='; } else if (byteRemainder == 2) { chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]; a = (chunk & 64512) >> 10; b = (chunk & 1008) >> 4; c = (chunk & 15) << 2; base64 += encodings[a] + encodings[b] + encodings[c] + '='; } return base64; } ================================================ FILE: packages/protobuf/utils/index.ts ================================================ export * from './base64'; export * from './encryption'; ================================================ FILE: packages/protobuf/vitest.config.ts ================================================ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, environment: 'node', include: ['**/*.test.ts'], }, }); ================================================ FILE: packages/shared/README.md ================================================ # Shared Package This package contains code shared with other packages. To use the code in the package, you need to add the following to the package.json file. ```json { "dependencies": { "@sync-your-cookie/shared": "workspace:*" } } ``` After building this package, real-time cache busting does not occur in the code of other packages that reference this package. You need to rerun it from the root path with `pnpm dev`, etc. (This will be improved in the future.) If the type does not require compilation, there is no problem, but if the implementation requiring compilation is changed, a problem may occur. Therefore, it is recommended to extract and use it in each context if it is easier to manage by extracting overlapping or business logic from the code that changes frequently in this package. ================================================ FILE: packages/shared/index.ts ================================================ export * from './lib/cloudflare'; export * from './lib/cookie'; export * from './lib/hoc'; export * from './lib/hooks'; export * from './lib/Providers'; export * from './lib/message'; export * from './lib/utils'; export * from './lib/github'; ================================================ FILE: packages/shared/lib/Providers/ThemeProvider.tsx ================================================ import { useStorageSuspense } from '@lib/hooks/useStorageSuspense'; import { themeStorage } from '@sync-your-cookie/storage/lib/themeStorage'; import { createContext, useEffect } from 'react'; type Theme = 'dark' | 'light' | 'system'; type ThemeProviderProps = { children: React.ReactNode; }; type ThemeProviderState = { theme: Theme; setTheme: (theme: Theme) => void; }; const initialState: ThemeProviderState = { theme: 'system', setTheme: () => null, }; export const ThemeProviderContext = createContext(initialState); export function ThemeProvider({ children, ...props }: ThemeProviderProps) { const theme = useStorageSuspense(themeStorage); useEffect(() => { const root = window.document.documentElement; root.classList.remove('light', 'dark'); if (theme === 'system') { const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; root.classList.add(systemTheme); return; } root.classList.add(theme); }, [theme]); const value = { theme, setTheme: (theme: Theme) => { // localStorage.setItem(storageKey, theme); themeStorage.set(theme); // setTheme(theme); }, }; return ( {children} ); } ================================================ FILE: packages/shared/lib/Providers/hooks/index.ts ================================================ export * from './useTheme'; ================================================ FILE: packages/shared/lib/Providers/hooks/useTheme.ts ================================================ import { ThemeProviderContext } from '../'; import { useContext, useEffect } from 'react'; export const useTheme = () => { const context = useContext(ThemeProviderContext); useEffect(() => { const handler = (event: MediaQueryListEvent) => { if (event.matches) { context.setTheme('dark'); } else { context.setTheme('light'); } }; window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', handler); return () => { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', handler); }; }, []); if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider'); return context; }; ================================================ FILE: packages/shared/lib/Providers/index.tsx ================================================ export * from './hooks'; export * from './ThemeProvider'; ================================================ FILE: packages/shared/lib/cloudflare/api.ts ================================================ import { settingsStorage } from '@sync-your-cookie/storage/lib/settingsStorage'; export interface WriteResponse { success: boolean; errors: { code: number; message: string; }[]; } /** * * @param value specify the value to write * @param accountId cloudflare account id * @param namespaceId cloudflare namespace id * @param token api token * @returns promise */ export const writeCloudflareKV = async (value: string, accountId: string, namespaceId: string, token: string) => { const storageKey = settingsStorage.getSnapshot()?.storageKey; const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${storageKey}`; // const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/bulk`; // const payload = [ // { // key: DEFAULT_KEY, // metadata: JSON.stringify({ // someMetadataKey: value, // }), // value: value, // }, // ]; const options = { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: value, }; return fetch(url, options).then(res => res.json()); }; /** * * @param accountId cloudflare account id * @param namespaceId cloudflare namespace id * @param token api token * @returns Promise */ export const readCloudflareKV = async (accountId: string, namespaceId: string, token: string) => { const storageKey = settingsStorage.getSnapshot()?.storageKey; const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${storageKey}`; const options = { method: 'GET', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }; return fetch(url, options).then(async res => { if (res.status === 404) { return ''; } if (res.status === 200) { const text = await res.text(); return text.trim(); } else { return Promise.reject(await res.json()); } }); }; export const verifyCloudflareAccountToken = async (accountId: string, token: string) => { const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/tokens/verify`; const options = { method: 'GET', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }; return fetch(url, options).then(async res => { if (res.status === 200) { return res.json(); } else { return Promise.reject(await res.json()); } }); }; export const verifyCloudflareToken = async (accountId: string, token: string) => { const url = `https://api.cloudflare.com/client/v4/user/tokens/verify`; const options = { method: 'GET', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }; return fetch(url, options).then(async res => { if (res.status === 200) { return res.json(); } else { return verifyCloudflareAccountToken(accountId, token); } }); }; ================================================ FILE: packages/shared/lib/cloudflare/enum.ts ================================================ export enum ErrorCode { NotFoundRoute = 7003, AuthenicationError = 10000, NamespaceIdError = 10011, } ================================================ FILE: packages/shared/lib/cloudflare/index.ts ================================================ export * from './api'; export * from './enum'; ================================================ FILE: packages/shared/lib/cookie/index.ts ================================================ export * from './withCloudflare'; export * from './withStorage'; ================================================ FILE: packages/shared/lib/cookie/withCloudflare.ts ================================================ import { accountStorage, type AccountInfo } from '@sync-your-cookie/storage/lib/accountStorage'; import { getActiveStorageItem, settingsStorage } from '@sync-your-cookie/storage/lib/settingsStorage'; import { readCloudflareKV, writeCloudflareKV, WriteResponse } from '../cloudflare/api'; import { GithubApi } from '@lib/github'; import { MessageErrorCode } from '@lib/message'; import { RestEndpointMethodTypes } from '@octokit/rest'; import { arrayBufferToBase64, base64ToArrayBuffer, decodeCookiesMap, decryptBase64, encodeCookiesMap, encryptBase64, ICookie, ICookiesMap, ILocalStorageItem, isBase64Encrypted, } from '@sync-your-cookie/protobuf'; export const check = (accountInfo?: AccountInfo) => { const cloudflareAccountInfo = accountInfo || accountStorage.getSnapshot(); if (cloudflareAccountInfo?.selectedProvider === 'github') { if (!cloudflareAccountInfo.githubAccessToken) { return Promise.reject({ message: 'GitHub Access Token is empty', code: MessageErrorCode.AccountCheck, }); } } else { if (!cloudflareAccountInfo?.accountId || !cloudflareAccountInfo.namespaceId || !cloudflareAccountInfo.token) { let message = 'Account ID is empty'; if (!cloudflareAccountInfo?.namespaceId) { message = 'NamespaceId ID is empty'; } else if (!cloudflareAccountInfo.token) { message = 'Token is empty'; } return Promise.reject({ message, code: MessageErrorCode.AccountCheck, }); } } return cloudflareAccountInfo; }; export const readCookiesMap = async (accountInfo: AccountInfo): Promise => { let content = ''; if (accountInfo.selectedProvider === 'github') { const activeStorageItem = getActiveStorageItem(); if (activeStorageItem?.rawUrl) { content = await GithubApi.instance.fetchRawContent(activeStorageItem.rawUrl); } } else { await check(accountInfo); content = await readCloudflareKV(accountInfo.accountId!, accountInfo.namespaceId!, accountInfo.token!); } if (content) { try { const settingsInfo = settingsStorage.getSnapshot(); const encryptionEnabled = settingsInfo?.encryptionEnabled; const encryptionPassword = settingsInfo?.encryptionPassword; // Check if content is encrypted and decrypt if needed let processedContent = content; const protobufEncoding = !content.startsWith('{'); if (protobufEncoding && isBase64Encrypted(content)) { if (!encryptionEnabled || !encryptionPassword) { return Promise.reject({ message: 'Failed to decrypt data. Please check your encryption password.', code: MessageErrorCode.DecryptFailed, }); } try { processedContent = await decryptBase64(content, encryptionPassword); } catch (decryptError) { console.error('Decryption failed:', decryptError); // throw new Error('Failed to decrypt data. Please check your encryption password.'); return Promise.reject({ message: 'Failed to decrypt data. Please check your encryption password.', code: MessageErrorCode.DecryptFailed, }); } } if (protobufEncoding) { const compressedBuffer = base64ToArrayBuffer(processedContent); const deMsg = await decodeCookiesMap(compressedBuffer); console.log('readCookiesMap->deMsg', deMsg); return deMsg; } else { console.log('readCookiesMap->res', JSON.parse(processedContent)); return JSON.parse(processedContent); } } catch (error) { console.log('Decode error', error, content); // return {}; return Promise.reject({ message: `Decode error: ${error}, please check your save settings`, code: MessageErrorCode.DecodeFailed, }); } } else { return {}; } }; export const writeCookiesMap = async (accountInfo: AccountInfo, cookiesMap: ICookiesMap = {}) => { const settingsInfo = settingsStorage.getSnapshot(); const protobufEncoding = settingsInfo?.protobufEncoding; const encryptionEnabled = settingsInfo?.encryptionEnabled; const encryptionPassword = settingsInfo?.encryptionPassword; let encodingStr = ''; if (protobufEncoding) { const buffered = await encodeCookiesMap(cookiesMap); // eslint-disable-next-line @typescript-eslint/no-explicit-any encodingStr = arrayBufferToBase64(buffered as any); // Encrypt the data if encryption is enabled if (encryptionEnabled && encryptionPassword) { encodingStr = await encryptBase64(encodingStr, encryptionPassword); console.log('writeCookiesMap-> data encrypted'); } } else { encodingStr = JSON.stringify(cookiesMap); console.log('writeCookiesMap->', cookiesMap); } if (accountInfo.selectedProvider === 'github') { const storageKeyGistId = settingsInfo?.storageKeyGistId; const storageKey = settingsInfo?.storageKey; return await GithubApi.instance.updateGist(storageKeyGistId!, storageKey!, encodingStr); } else { const res = await writeCloudflareKV( encodingStr, accountInfo.accountId!, accountInfo.namespaceId!, accountInfo.token!, ); return res; } }; export const mergeAndWriteCookies = async ( accountInfo: AccountInfo, domain: string, cookies: ICookie[], localStorageItems: ILocalStorageItem[] = [], userAgent = '', oldCookieMap: ICookiesMap = {}, ): Promise<[WriteResponse | RestEndpointMethodTypes['gists']['update']['response'], ICookiesMap]> => { await check(accountInfo); const cookiesMap: ICookiesMap = { updateTime: Date.now(), createTime: oldCookieMap?.createTime || Date.now(), domainCookieMap: { ...(oldCookieMap.domainCookieMap || {}), [domain]: { updateTime: Date.now(), createTime: oldCookieMap.domainCookieMap?.[domain]?.createTime || Date.now(), cookies: cookies, localStorageItems: localStorageItems, userAgent: userAgent || oldCookieMap.domainCookieMap?.[domain]?.userAgent || '', }, }, }; const res = await writeCookiesMap(accountInfo, cookiesMap); return [res, cookiesMap]; }; export const mergeAndWriteMultipleDomainCookies = async ( cloudflareAccountInfo: AccountInfo, domainCookies: { domain: string; cookies: ICookie[]; localStorageItems: ILocalStorageItem[]; userAgent?: string }[], oldCookieMap: ICookiesMap = {}, ): Promise<[WriteResponse, ICookiesMap]> => { await check(cloudflareAccountInfo); const newDomainCookieMap = { ...(oldCookieMap.domainCookieMap || {}), }; for (const { domain, cookies, localStorageItems, userAgent } of domainCookies) { newDomainCookieMap[domain] = { updateTime: Date.now(), createTime: oldCookieMap.domainCookieMap?.[domain]?.createTime || Date.now(), cookies: cookies, localStorageItems: localStorageItems || [], userAgent: userAgent || oldCookieMap.domainCookieMap?.[domain]?.userAgent || '', }; } const cookiesMap: ICookiesMap = { updateTime: Date.now(), createTime: oldCookieMap?.createTime || Date.now(), domainCookieMap: newDomainCookieMap, }; const res = await writeCookiesMap(cloudflareAccountInfo, cookiesMap); return [res, cookiesMap]; }; export const removeAndWriteCookies = async ( cloudflareAccountInfo: AccountInfo, domain: string, oldCookieMap: ICookiesMap = {}, id?: string, ): Promise<[WriteResponse, ICookiesMap]> => { await check(cloudflareAccountInfo); const cookiesMap: ICookiesMap = { updateTime: Date.now(), createTime: oldCookieMap?.createTime || Date.now(), domainCookieMap: { ...(oldCookieMap.domainCookieMap || {}), }, }; if (cookiesMap.domainCookieMap) { if (id !== undefined) { if (cookiesMap.domainCookieMap[domain]?.cookies) { const oldLength = cookiesMap.domainCookieMap[domain]?.cookies?.length || 0; cookiesMap.domainCookieMap[domain].cookies = cookiesMap.domainCookieMap[domain].cookies?.filter( // eslint-disable-next-line @typescript-eslint/no-explicit-any (cookie: any) => `${cookie.domain}_${cookie.name}` !== id, ) || []; const newLength = cookiesMap.domainCookieMap[domain]?.cookies?.length || 0; if (oldLength === newLength) { throw new Error(`${id}: cookie not found`); } } } else { delete cookiesMap.domainCookieMap[domain]; } } const res = await writeCookiesMap(cloudflareAccountInfo, cookiesMap); return [res, cookiesMap]; }; export const editAndWriteCookies = async ( cloudflareAccountInfo: AccountInfo, host: string, oldCookieMap: ICookiesMap = {}, oldItem: ICookie, newItem: ICookie, ): Promise<[WriteResponse, ICookiesMap]> => { await check(cloudflareAccountInfo); const cookiesMap: ICookiesMap = { updateTime: Date.now(), createTime: oldCookieMap?.createTime || Date.now(), domainCookieMap: { ...(oldCookieMap.domainCookieMap || {}), }, }; if (cookiesMap.domainCookieMap) { const cookieLength = cookiesMap.domainCookieMap[host]?.cookies?.length || 0; for (let i = 0; i < cookieLength; i++) { const cookieItem = cookiesMap.domainCookieMap[host]?.cookies?.[i]; if (cookieItem?.name === oldItem.name && cookieItem?.domain === oldItem.domain) { // eslint-disable-next-line @typescript-eslint/no-explicit-any (cookiesMap.domainCookieMap[host].cookies as any)[i] = { ...cookieItem, ...newItem, }; break; } } } const res = await writeCookiesMap(cloudflareAccountInfo, cookiesMap); return [res, cookiesMap]; }; ================================================ FILE: packages/shared/lib/cookie/withStorage.ts ================================================ import { ICookie, ICookiesMap, ILocalStorageItem } from '@sync-your-cookie/protobuf'; import { Cookie, cookieStorage } from '@sync-your-cookie/storage/lib/cookieStorage'; import { domainStatusStorage } from '@sync-your-cookie/storage/lib/domainStatusStorage'; import { AccountInfo, accountStorage } from '@sync-your-cookie/storage/lib/accountStorage'; import { MessageType, sendMessage } from '@lib/message'; import { RestEndpointMethodTypes } from '@octokit/rest'; import { OctokitResponse } from '@octokit/types'; import { WriteResponse } from '../cloudflare'; import { editAndWriteCookies, mergeAndWriteCookies, mergeAndWriteMultipleDomainCookies, readCookiesMap, removeAndWriteCookies, } from './withCloudflare'; export const readCookiesMapWithStatus = async (cloudflareInfo: AccountInfo) => { let cookieMap: Cookie | null = null; const domainStatus = await domainStatusStorage.get(); if (domainStatus.pushing) { cookieMap = await cookieStorage.getSnapshot(); } if (cookieMap && Object.keys(cookieMap.domainCookieMap || {}).length > 0) { return cookieMap; } return await readCookiesMap(cloudflareInfo); }; export const pullCookies = async (isInit = false): Promise => { const cloudflareInfo = await accountStorage.get(); if (isInit && (!cloudflareInfo.accountId || !cloudflareInfo.namespaceId || !cloudflareInfo.token)) { return {}; } try { const domainStatus = await domainStatusStorage.get(); if (domainStatus.pulling) { const cookieMap = await cookieStorage.getSnapshot(); if (cookieMap && Object.keys(cookieMap.domainCookieMap || {}).length > 0) { return cookieMap; } } await domainStatusStorage.update({ pulling: true, }); const cookieMap = await readCookiesMapWithStatus(cloudflareInfo); const res = await cookieStorage.update(cookieMap, isInit); await domainStatusStorage.update({ pulling: false, }); return res; } catch (e) { console.error('pullCookies fail', e); await domainStatusStorage.update({ pulling: false, }); return Promise.reject(e); } }; function extractPortRegex(host: string) { const match = host.match(/:(\d+)$/); return match ? match[1] : null; } export const pullAndSetCookies = async (activeTabUrl: string, host: string, isReload = true): Promise => { const cookieMap = await pullCookies(); const cookieDetails = cookieMap?.domainCookieMap?.[host]?.cookies || []; const localStorageItems = cookieMap?.domainCookieMap?.[host]?.localStorageItems || []; if (cookieDetails.length === 0 && localStorageItems.length === 0) { console.warn('no cookies to pull, push first please', host, cookieMap); throw new Error('No cookies to pull, push first please'); } else { const cookiesPromiseList: Promise[] = []; for (const cookie of cookieDetails) { let removeWWWHost = host.replace('www.', ''); const port = extractPortRegex(removeWWWHost); if (port) { removeWWWHost = removeWWWHost.replace(':' + port, ''); } if (cookie.domain?.includes(removeWWWHost)) { let url = activeTabUrl; if (cookie.domain) { const urlObj = new URL(activeTabUrl); const protocol = activeTabUrl ? urlObj.protocol : 'http:'; const itemHost = cookie.domain.startsWith('.') ? cookie.domain.slice(1) : cookie.domain; url = `${protocol}//${itemHost}`; } const cookieDetail: chrome.cookies.SetDetails = { domain: cookie.domain.startsWith('.') || !url ? cookie.domain : undefined, name: cookie.name ?? undefined, url: url, storeId: cookie.storeId ?? undefined, value: cookie.value ?? undefined, expirationDate: cookie.expirationDate ?? undefined, path: cookie.path ?? undefined, httpOnly: cookie.httpOnly ?? undefined, secure: cookie.secure ?? undefined, sameSite: (cookie.sameSite ?? undefined) as chrome.cookies.SameSiteStatus, }; const promise = new Promise((resolve, reject) => { try { chrome.cookies.set(cookieDetail, res => { resolve(res); }); } catch (error) { console.error('cookie set error', cookieDetail, error); reject(error); } }); cookiesPromiseList.push(promise); } } // reload window after set cookies // await new Promise(resolve => { // setTimeout(resolve, 5000); // }); await sendMessage( { type: MessageType.SetLocalStorage, payload: { domain: host, value: localStorageItems, }, }, true, ) .then(res => { console.log('set local storage', res); }) .catch(err => { console.error('set local storage error', err); }); if (cookiesPromiseList.length === 0 && localStorageItems.length === 0) { console.warn('no matched cookies and localStorageItems to pull, push first please', host, cookieMap); throw new Error('No matched cookies and localStorageItems to pull, push first please'); } await Promise.allSettled(cookiesPromiseList); if (isReload) { chrome.tabs.query({}, function (tabs) { tabs.forEach(function (tab) { // 使用字符串匹配 if (tab.url && tab.url.includes(host) && tab.id) { console.log('tab', tab); chrome.tabs.reload(tab.id); } }); }); } } return cookieMap; }; export type GistUpdateResponse = RestEndpointMethodTypes['gists']['update']['response']; export type PushCookiesResponse = WriteResponse | GistUpdateResponse; const checkSuccessAndUpdate = async ( accountInfo: AccountInfo, // eslint-disable-next-line @typescript-eslint/no-explicit-any res: WriteResponse | OctokitResponse, cookieMap: ICookiesMap, ) => { if (accountInfo.selectedProvider === 'github') { if ((res as unknown as GistUpdateResponse)?.status?.toString()?.startsWith('2')) { await cookieStorage.update(cookieMap); } } else { if ((res as WriteResponse).success) { await cookieStorage.update(cookieMap); } } }; export const pushCookies = async ( domain: string, cookies: ICookie[], localStorageItems: ILocalStorageItem[] = [], userAgent = '', ): Promise => { const accountInfo = await accountStorage.get(); try { const domainStatus = await domainStatusStorage.get(); if (domainStatus.pushing) return Promise.reject('the cookie is pushing'); await domainStatusStorage.update({ pushing: true, }); const oldCookie = await readCookiesMapWithStatus(accountInfo); const [res, cookieMap] = await mergeAndWriteCookies( accountInfo, domain, cookies, localStorageItems, userAgent, oldCookie, ); console.log('res->pushCookies', res); await checkSuccessAndUpdate(accountInfo, res, cookieMap); await domainStatusStorage.update({ pushing: false, }); return res; } catch (e) { console.error('pushCookies fail err', e); await domainStatusStorage.update({ pushing: false, }); return Promise.reject(e); } }; export const pushMultipleDomainCookies = async ( domainCookies: { domain: string; cookies: ICookie[]; localStorageItems: ILocalStorageItem[]; userAgent?: string }[], ): Promise => { const accountInfo = await accountStorage.get(); try { const domainStatus = await domainStatusStorage.get(); if (domainStatus.pushing) return Promise.reject('cookie is pushing'); await domainStatusStorage.update({ pushing: true, }); const oldCookie = await readCookiesMapWithStatus(accountInfo); const [res, cookieMap] = await mergeAndWriteMultipleDomainCookies(accountInfo, domainCookies, oldCookie); await domainStatusStorage.update({ pushing: false, }); await checkSuccessAndUpdate(accountInfo, res, cookieMap); return res; } catch (e) { console.error('pushMultipleDomainCookies fail err', e); await domainStatusStorage.update({ pushing: false, }); return Promise.reject(e); } }; export const removeCookies = async (domain: string): Promise => { const accountInfo = await accountStorage.get(); try { const domainStatus = await domainStatusStorage.get(); if (domainStatus.pushing) return Promise.reject('the cookie is pushing'); await domainStatusStorage.update({ pushing: true, }); // const oldCookie = await cookieStorage.get(); const oldCookie = await readCookiesMapWithStatus(accountInfo); const [res, cookieMap] = await removeAndWriteCookies(accountInfo, domain, oldCookie); await domainStatusStorage.update({ pushing: false, }); await checkSuccessAndUpdate(accountInfo, res, cookieMap); return res; } catch (e) { console.error('removeCookies fail err', e); await domainStatusStorage.update({ pushing: false, }); return Promise.reject(e); } }; export const removeCookieItem = async (domain: string, id: string): Promise => { const accountInfo = await accountStorage.get(); try { const domainStatus = await domainStatusStorage.get(); if (domainStatus.pushing) return Promise.reject('the cookie is pushing'); await domainStatusStorage.update({ pushing: true, }); // const oldCookie = await cookieStorage.get(); const oldCookie = await readCookiesMapWithStatus(accountInfo); const [res, cookieMap] = await removeAndWriteCookies(accountInfo, domain, oldCookie, id); await domainStatusStorage.update({ pushing: false, }); await checkSuccessAndUpdate(accountInfo, res, cookieMap); return res; } catch (e) { console.error('removeCookieItem fail err', e); await domainStatusStorage.update({ pushing: false, }); return Promise.reject(e); } }; export const editCookieItem = async (domain: string, name: string): Promise => { const accountInfo = await accountStorage.get(); try { const domainStatus = await domainStatusStorage.get(); if (domainStatus.pushing) return Promise.reject('the cookie is pushing'); await domainStatusStorage.update({ pushing: true, }); // const oldCookie = await cookieStorage.get(); const oldCookie = await readCookiesMapWithStatus(accountInfo); const [res, cookieMap] = await removeAndWriteCookies(accountInfo, domain, oldCookie, name); await domainStatusStorage.update({ pushing: false, }); await checkSuccessAndUpdate(accountInfo, res, cookieMap); return res; } catch (e) { console.error('removeCookieItem fail err', e); await domainStatusStorage.update({ pushing: false, }); return Promise.reject(e); } }; export class CookieOperator { static async prepare() { const cloudflareInfo = await accountStorage.get(); const domainStatus = await domainStatusStorage.get(); if (domainStatus.pushing) return Promise.reject('the cookie is pushing'); return { cloudflareInfo }; } static async setPushing(open: boolean) { await domainStatusStorage.update({ pushing: open, }); } static async editCookieItem(host: string, oldItem: ICookie, newItem: ICookie) { try { const { cloudflareInfo } = await this.prepare(); await this.setPushing(true); const oldCookie = await readCookiesMapWithStatus(cloudflareInfo); const [res, cookieMap] = await editAndWriteCookies(cloudflareInfo, host, oldCookie, oldItem, newItem); await this.setPushing(false); await checkSuccessAndUpdate(cloudflareInfo, res, cookieMap); return res; } catch (e) { console.error('removeCookieItem fail err', e); await this.setPushing(false); return Promise.reject(e); } } } ================================================ FILE: packages/shared/lib/github/api.ts ================================================ import { Octokit, RestEndpointMethodTypes } from '@octokit/rest'; import { arrayBufferToBase64, encodeCookiesMap, ICookiesMap } from '@sync-your-cookie/protobuf'; import { accountStorage } from '@sync-your-cookie/storage/lib/accountStorage'; import { IStorageItem, settingsStorage } from '@sync-your-cookie/storage/lib/settingsStorage'; export class GithubApi { private clientId: string; private clientSecret: string; private accessToken?: string | null = null; private prefix = 'sync-your-cookie_'; static instance: GithubApi; octokit!: Octokit; private inited = false; private initDefault = false; constructor(clientId: string, clientSecret: string, initDefault: boolean = false) { this.clientId = clientId; this.clientSecret = clientSecret; this.accessToken = accountStorage.getSnapshot()?.githubAccessToken; this.initDefault = initDefault; this.subscribe(); this.init(); } public static getInstance(clientId: string, clientSecret: string, initDefault: boolean = false): GithubApi { if (!GithubApi.instance) { GithubApi.instance = new GithubApi(clientId, clientSecret, initDefault); } return GithubApi.instance; } async newOctokit() { if (this.accessToken) { this.octokit = new Octokit({ auth: this.accessToken }); // this.octokit.hook.before('request', options => { // console.log('请求 URL:', options.url); // console.log('请求头:', options.headers); // }); } } async init() { if (this.inited) { return; } this.reload(); this.inited = true; } reload() { this.newOctokit(); this.initStorageKeyList(); } async getSyncGists() { const res = await this.listGists(); const fullList = res.data; const syncGist = fullList.find(gist => { const files = gist.files; const keys = Object.keys(files); const hasSyncFile = keys.some(key => key.startsWith(this.prefix)); return hasSyncFile; }); return syncGist; } async initStorageKeyList() { if (!this.octokit) { return; } let syncGist = await this.getSyncGists(); if (!syncGist && this.initDefault) { console.log('No sync gists found, creating one...'); const content = await this.initContent(); await this.createGist('Sync Your Cookie Gist', `${this.prefix}Default`, content, false); // syncGists.push(newGist.data); syncGist = await this.getSyncGists(); } if (syncGist) { await this.setStorageKeyList(syncGist); } } async initContent() { const cookiesMap: ICookiesMap = { updateTime: Date.now(), createTime: Date.now(), domainCookieMap: {}, }; let encodingStr = ''; const protobufEncoding = settingsStorage.getSnapshot()?.protobufEncoding; if (protobufEncoding) { const buffered = await encodeCookiesMap(cookiesMap); // eslint-disable-next-line @typescript-eslint/no-explicit-any encodingStr = arrayBufferToBase64(buffered as any); } else { encodingStr = JSON.stringify(cookiesMap); } return encodingStr; } async setStorageKeyList(gist: RestEndpointMethodTypes['gists']['list']['response']['data'][number]) { const files = gist.files; const storageKeys: IStorageItem[] = []; if (files) { const currentStorageKey = settingsStorage.getSnapshot()?.storageKey; let currentStorageExist = false; for (const filename in files) { const file = files[filename]; if (filename.startsWith(this.prefix)) { const tempValue = filename.replace(this.prefix, ''); if (!currentStorageExist) { if (currentStorageKey && tempValue === currentStorageKey) { currentStorageExist = true; } } storageKeys.push({ value: tempValue, label: tempValue, rawUrl: file.raw_url, gistId: gist.id, }); // storageKeys.push(file[0].replace(this.prefix, '')); } } console.log('storageKeys', storageKeys, gist.id); settingsStorage.update({ storageKeyList: storageKeys, storageKey: currentStorageExist ? currentStorageKey : storageKeys[0]?.value, storageKeyGistId: gist.id, gistHtmlUrl: gist.html_url, }); } else { console.log('No files found in gist', gist.id); } // const keys = Object.keys(files); // const storageKeys = keys.filter(key => key.startsWith(this.prefix)).map(key => key.replace(this.prefix, '')); // console.log('storageKeys', storageKeys); // return storageKeys; } subscribe() { accountStorage.subscribe(async () => { const accessToken = accountStorage.getSnapshot()?.githubAccessToken; if (this.accessToken === accessToken || !accessToken) { return; } this.accessToken = accessToken; console.log('GithubApi accountStorage changed -> this.accessToken', this.accessToken); this.inited = false; this.init(); }); } // 用 code 换取 access_token async fetchAccessToken(code: string): Promise { const url = 'https://github.com/login/oauth/access_token'; const params = { client_id: this.clientId, client_secret: this.clientSecret, code, }; const headers = { Accept: 'application/json', 'Content-Type': 'application/json' }; const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(params), }); const data = await res.json(); if (data.access_token) { this.accessToken = data.access_token; return this.accessToken || ''; } throw new Error('获取 access_token 失败'); } // 用 access_token 获取用户信息 async fetchUser() { this.ensureToken(); // const res = await this.octokit.users.getById(); const res = await fetch('https://api.github.com/user', { headers: { Authorization: `token ${this.accessToken}` }, }); return res.json() as Promise; } async request(method: string, path: string, payload: Record = {}) { this.ensureToken(); const res = await this.octokit.request(`${method} ${path}`, { ...payload, headers: { // Authorization: `Bearer ${this.accessToken as string}`, 'X-GitHub-Api-Version': '2022-11-28', }, }); if (res.status !== 200) { return Promise.reject(res); } return res.data; } async get(path: string) { return this.request('GET', path); } async post(path: string, payload: Record = {}) { return this.request('POST', path, payload); } async patch(path: string, payload: Record = {}) { return this.request('PATCH', path, payload); } // 获取 gist 列表 async listGists() { const res = await this.octokit.gists.list(); return res; } // 创建 gist async createGist(description: string, filename: string, content: string, publicGist = false) { return this.octokit.gists.create({ description: description, public: publicGist, files: { [filename]: { content: content, }, }, }); } async getGist(gistId: string) { return this.octokit.gists.get({ gist_id: gistId }); } // 更新 gist async updateGist(gistId: string, filename: string, content: string) { // const { data: gist } = await this.octokit.gists.get({ // gist_id: gistId, // }); // console.log('files', gist.files); // const existFiles = gist.files || {}; const syncFileName = filename.startsWith(this.prefix) ? filename : this.prefix + filename; const res = await this.octokit.gists.update({ gist_id: gistId, files: { [syncFileName]: { content: content, }, }, }); // update settingsStorage based result this.setStorageKeyList(res.data as unknown as RestEndpointMethodTypes['gists']['list']['response']['data'][number]); return res; // return this.patch(`/gists/${gistId}`, { // files: { // [filename]: { // content: content, // }, // }, // }); } async addGistFile(gistId: string, filename: string) { return this.updateGist(gistId, filename, await this.initContent()); } async deleteGistFile(gistId: string, filename: string) { const syncFileName = filename.startsWith(this.prefix) ? filename : this.prefix + filename; return this.octokit.gists.update({ gist_id: gistId, files: { [syncFileName]: null, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any, }); } // 删除 gist async deleteGist(gistId: string) { return this.octokit.gists.delete({ gist_id: gistId }); } private ensureToken() { if (!this.accessToken) { this.accessToken = accountStorage.getSnapshot()?.githubAccessToken; console.log('this.accessToken', this.accessToken); this.newOctokit(); } if (!this.accessToken) { throw new Error('请先获取 access_token'); } } async fetchRawContent(rawUrl: string) { const content = await fetch(rawUrl).then(res => res.text()); return content; } } export const scope = 'gist'; export const clientId = 'Ov23liyhOkJsj8FzPlm0'; const clientSecret = ''; // export const githubApi = new GithubApi(clientId, clientSecret); export const initGithubApi = async (initDefault = false) => { console.log('initGithubApi finish'); GithubApi.getInstance(clientId, clientSecret, initDefault); }; ================================================ FILE: packages/shared/lib/github/index.ts ================================================ export * from './api'; ================================================ FILE: packages/shared/lib/hoc/index.ts ================================================ import { withSuspense } from './withSuspense'; import { withErrorBoundary } from './withErrorBoundary'; export { withSuspense, withErrorBoundary }; ================================================ FILE: packages/shared/lib/hoc/withErrorBoundary.tsx ================================================ import type { ComponentType, ErrorInfo, ReactElement } from 'react'; import { Component } from 'react'; class ErrorBoundary extends Component< { children: ReactElement; fallback: ReactElement; }, { hasError: boolean; } > { state = { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error(error, errorInfo); } render() { if (this.state.hasError) { return this.props.fallback; } return this.props.children; } } export function withErrorBoundary>( Component: ComponentType, ErrorComponent: ReactElement, ) { return function WithErrorBoundary(props: T) { return ( ); }; } ================================================ FILE: packages/shared/lib/hoc/withSuspense.tsx ================================================ import { ComponentType, ReactElement, Suspense } from 'react'; export function withSuspense>( Component: ComponentType, SuspenseComponent: ReactElement, ) { return function WithSuspense(props: T) { return ( ); }; } ================================================ FILE: packages/shared/lib/hooks/index.ts ================================================ import { catchHandler, useCookieAction } from './useCookieAction'; import { useStorage } from './useStorage'; import { useStorageSuspense } from './useStorageSuspense'; export { catchHandler, useCookieAction, useStorage, useStorageSuspense }; ================================================ FILE: packages/shared/lib/hooks/useCookieAction.ts ================================================ import { MessageErrorCode, pullCookieUsingMessage, pushCookieUsingMessage, removeCookieUsingMessage, } from '@lib/message'; import { domainConfigStorage } from '@sync-your-cookie/storage/lib/domainConfigStorage'; import { domainStatusStorage } from '@sync-your-cookie/storage/lib/domainStatusStorage'; import { toast as Toast } from 'sonner'; import { useStorageSuspense } from './index'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const catchHandler = (err: any, scene: 'push' | 'pull' | 'remove' | 'delete' | 'edit', toast: typeof Toast) => { const defaultMsg = `${scene} fail`; const code = err?.code; const settingErrors = [ MessageErrorCode.AccountCheck, MessageErrorCode.CloudflareNotFoundRoute, MessageErrorCode.DecodeFailed, MessageErrorCode.DecryptFailed, ]; if (settingErrors.includes(code)) { toast.error(err?.msg || err?.result?.message || defaultMsg, { action: { label: 'go to settings', onClick: () => { chrome.runtime.openOptionsPage(); }, }, }); } else { toast.error(err?.msg || defaultMsg); } console.log('err', err); }; export const useCookieAction = (host: string, toast: typeof Toast) => { const domainStatus = useStorageSuspense(domainStatusStorage); const domainConfig = useStorageSuspense(domainConfigStorage); const handlePush = async (selectedHost = host, sourceUrl?: string, favIconUrl?: string) => { return pushCookieUsingMessage({ host: selectedHost, sourceUrl, favIconUrl, }) .then(res => { if (res.isOk) { toast.success('Pushed success'); } else { toast.error(res.msg || 'Pushed fail'); } console.log('res', res); }) .catch(err => { catchHandler(err, 'push', toast); }); }; const handlePull = async (activeTabUrl: string, selectedDomain = host, reload = true) => { return pullCookieUsingMessage({ activeTabUrl: activeTabUrl, domain: selectedDomain, reload, }) .then(res => { console.log('res', res); if (res.isOk) { toast.success('Pull success'); } else { toast.error(res.msg || 'Pull fail'); } }) .catch(err => { catchHandler(err, 'pull', toast); }); }; const handleRemove = async (selectedDomain = host) => { return removeCookieUsingMessage({ domain: selectedDomain, }) .then(async res => { console.log('res', res); if (res.isOk) { toast.success(res.msg || 'success'); await domainConfigStorage.removeItem(host); } else { toast.error(res.msg || 'Removed fail'); } console.log('res', res); }) .catch(err => { catchHandler(err, 'remove', toast); }); }; return { // domainConfig: domainConfig as typeof domainConfig, pulling: domainStatus.pulling, pushing: domainStatus.pushing, domainItemConfig: domainConfig.domainMap[host] || {}, domainItemStatus: domainStatus.domainMap[host] || {}, getDomainItemConfig: (selectedDomain: string) => { return domainConfig.domainMap[selectedDomain] || {}; }, getDomainItemStatus: (selectedDomain: string) => { return domainStatus.domainMap[selectedDomain] || {}; }, toggleAutoPullState: domainConfigStorage.toggleAutoPullState, toggleAutoPushState: domainConfigStorage.toggleAutoPushState, togglePullingState: domainStatusStorage.togglePullingState, togglePushingState: domainStatusStorage.togglePushingState, handlePush, handlePull, handleRemove, }; }; ================================================ FILE: packages/shared/lib/hooks/useStorage.ts ================================================ import { useSyncExternalStore } from 'react'; import { BaseStorage } from '@sync-your-cookie/storage'; export function useStorage< Storage extends BaseStorage, Data = Storage extends BaseStorage ? Data : unknown, >(storage: Storage) { const _data = useSyncExternalStore(storage.subscribe, storage.getSnapshot); // eslint-disable-next-line // @ts-ignore if (!storageMap.has(storage)) { // eslint-disable-next-line // @ts-ignore storageMap.set(storage, wrapPromise(storage.get())); } if (_data !== null) { // eslint-disable-next-line // @ts-ignore storageMap.set(storage, { read: () => _data }); } // eslint-disable-next-line // @ts-ignore return _data ?? (storageMap.get(storage)!.read() as Data); } ================================================ FILE: packages/shared/lib/hooks/useStorageSuspense.tsx ================================================ import { BaseStorage } from '@sync-your-cookie/storage'; import { useSyncExternalStore } from 'react'; type WrappedPromise = ReturnType; // eslint-disable-next-line @typescript-eslint/no-explicit-any const storageMap: Map, WrappedPromise> = new Map(); export function useStorageSuspense< Storage extends BaseStorage, Data = Storage extends BaseStorage ? Data : unknown, >(storage: Storage) { const _data = useSyncExternalStore(storage.subscribe, storage.getSnapshot); if (!storageMap.has(storage)) { storageMap.set(storage, wrapPromise(storage.get())); } if (_data !== null) { storageMap.set(storage, { read: () => _data }); } return _data ?? (storageMap.get(storage)!.read() as Data); } function wrapPromise(promise: Promise) { let status = 'pending'; let result: R; const suspender = promise.then( r => { status = 'success'; result = r; }, e => { status = 'error'; result = e; }, ); return { read() { switch (status) { case 'pending': throw suspender; case 'error': throw result; default: return result; } }, }; } ================================================ FILE: packages/shared/lib/message/index.ts ================================================ import { pushCookies } from '@lib/cookie'; import { ICookie, ILocalStorageItem } from '@sync-your-cookie/protobuf'; import { settingsStorage } from '@sync-your-cookie/storage/lib/settingsStorage'; import pTimeout from 'p-timeout'; export type { ICookie }; export enum MessageType { PushCookie = 'PushCookie', PullCookie = 'PullCookie', RemoveCookie = 'RemoveCookie', RemoveCookieItem = 'RemoveCookieItem', EditCookieItem = 'EditCookieItem', // LocalStorage GetLocalStorage = 'GetLocalStorage', SetLocalStorage = 'SetLocalStorage', } export enum MessageErrorCode { AccountCheck = 'AccountCheck', CloudflareNotFoundRoute = 'CloudflareNotFoundRoute', DecryptFailed = 'DecryptFailed', DecodeFailed = 'DecodeFailed', } export type PushCookieMessagePayload = { host: string; sourceUrl?: string; favIconUrl?: string; }; export type DomainPayload = { domain: string; }; export type RemoveCookieMessagePayload = { domain: string; }; export type RemoveCookieItemMessagePayload = { domain: string; id: string; }; export type PullCookieMessagePayload = { domain: string; activeTabUrl: string; reload: boolean; }; export type EditCookieItemMessagePayload = { domain: string; oldItem: ICookie; newItem: ICookie; }; export type SetLocalStorageMessagePayload = { domain: string; value: ILocalStorageItem[]; onlyKey?: string; }; export type MessageMap = { [MessageType.PushCookie]: { type: MessageType.PushCookie; payload: PushCookieMessagePayload; }; [MessageType.RemoveCookie]: { type: MessageType.RemoveCookie; payload: RemoveCookieMessagePayload; }; [MessageType.PullCookie]: { type: MessageType.PullCookie; payload: PullCookieMessagePayload; }; [MessageType.RemoveCookieItem]: { type: MessageType.RemoveCookieItem; payload: RemoveCookieItemMessagePayload; }; [MessageType.EditCookieItem]: { type: MessageType.EditCookieItem; payload: EditCookieItemMessagePayload; }; // LocalStorage [MessageType.GetLocalStorage]: { type: MessageType.GetLocalStorage; payload: DomainPayload; }; [MessageType.SetLocalStorage]: { type: MessageType.SetLocalStorage; payload: SetLocalStorageMessagePayload; }; }; // export type Message = { // type: T; // payload: MessagePayloadMap[T]; // }; export type Message = MessageMap[T]; export type SendResponse = { isOk: boolean; msg: string; result?: unknown; code?: MessageErrorCode; }; export function sendMessage(message: Message, isTab = false, useTimeout: boolean = false) { console.log('message', message); const send = (resolve: (value: SendResponse | PromiseLike) => void, reject: (reason?: any) => void) => { chrome.runtime.sendMessage(message, function (result: SendResponse) { console.log('sendMessage->message', message); if (result?.isOk) { resolve(result); } else { reject(result as SendResponse); } }); }; const fn = () => { if (isTab) { return new Promise((resolve, reject) => { chrome.tabs.query({ active: true, currentWindow: true }, async function (tabs) { if (tabs.length === 0) { // const allOpendTabs = await chrome.tabs.query({}); console.log('No active tab found, try alternative way'); // reject({ isOk: false, msg: 'No active tab found' } as SendResponse); send(resolve, reject); return; } chrome.tabs.sendMessage(tabs[0].id!, message, function (result) { console.log('isTab', isTab, 'result->', result); if (result?.isOk) { resolve(result); } else { reject(result as SendResponse); } }); }); }); } return new Promise((resolve, reject) => { send(resolve, reject); }); }; if (useTimeout) { return pTimeout(fn(), { milliseconds: 5000, fallback: () => { return { isOk: false, msg: 'Timeout' } as SendResponse; }, }); } return fn(); } export function pushCookieUsingMessage(payload: PushCookieMessagePayload) { return sendMessage({ payload, type: MessageType.PushCookie, }); } export function removeCookieUsingMessage(payload: RemoveCookieMessagePayload) { return sendMessage({ payload, type: MessageType.RemoveCookie, }); } export function pullCookieUsingMessage(payload: PullCookieMessagePayload) { return sendMessage({ payload, type: MessageType.PullCookie, }); } export function removeCookieItemUsingMessage(payload: RemoveCookieItemMessagePayload) { const sendType = MessageType.RemoveCookieItem; return sendMessage({ payload, type: sendType, }); } export function editCookieItemUsingMessage(payload: EditCookieItemMessagePayload) { const sendType = MessageType.EditCookieItem; return sendMessage({ payload, type: sendType, }); } export const getTabsByHost = async (host: string) => { return new Promise((resolve, reject) => { try { chrome.tabs.query({}, function (tabs) { const matchedTabs = tabs.filter(tab => tab.url && tab.id && tab.url.includes(host)); resolve(matchedTabs); }); } catch (error) { reject(error); } }); }; export const sendGetLocalStorageMessage = async (host: string, isTry = true) => { return new Promise[2]>>(async (resolve, reject) => { const myResolve = (args: any) => { settingsStorage.update({ localStorageGetting: false, }); resolve(args); }; const myReject = (err: any) => { settingsStorage.update({ localStorageGetting: false, }); reject(err); }; settingsStorage.update({ localStorageGetting: true, }); await sendMessage( { type: MessageType.GetLocalStorage, payload: { domain: host, }, }, true, ) .then(res => { if (res.isOk) { const localStorageItems = (res.result as any[]) || []; myResolve(localStorageItems); } else { throw new Error(res.msg || 'getLocalStorage fail'); } }) .catch(async (err: any) => { if (isTry == false) { myReject(err); return; } console.error('getLocalStorage and try reload fetch again', err); const matchedTabs = await getTabsByHost(host); if (matchedTabs.length === 0) { await chrome.tabs.create({ url: 'https://' + host, }); // window.open(host, '_blank'); } else { const activeTab = matchedTabs.find(tab => tab.active); if (activeTab) { chrome.tabs.reload(activeTab.id!); } else { matchedTabs.forEach(function (tab) { chrome.tabs.reload(tab.id!); }); } } setTimeout(async () => { try { const localStorageItems = await sendGetLocalStorageMessage(host, false); myResolve(localStorageItems); } catch (error) { console.log('error', error); myReject(error); } }, 500); }) .catch(err => { myReject(err); }); }); }; ================================================ FILE: packages/shared/lib/utils/index.ts ================================================ import { ErrorCode, WriteResponse } from '@lib/cloudflare'; import { MessageErrorCode, SendResponse } from '@lib/message'; import { accountStorage } from '@sync-your-cookie/storage/lib/accountStorage'; import { settingsStorage } from '@sync-your-cookie/storage/lib/settingsStorage'; export function debounce(func: (...args: T[]) => void, timeout = 300) { let timer: number | null | NodeJS.Timeout = null; return (...args: T[]) => { timer && clearTimeout(timer); timer = setTimeout(() => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore func.apply(this, args); }, timeout); }; } const successSceneMap = { push: 'Pushed', pull: 'Pulled', remove: 'Removed', delete: 'Deleted', edit: 'Edited', }; export function checkResponseAndCallback( // eslint-disable-next-line @typescript-eslint/no-explicit-any res: WriteResponse | Error | any, scene: 'push' | 'pull' | 'remove' | 'delete' | 'edit', callback: (response?: SendResponse) => void, ) { const accountInfo = accountStorage.getSnapshot(); if (accountInfo?.selectedProvider === 'github') { const statusCode = res?.status; if (statusCode === 200 || statusCode === 201 || statusCode === 204) { callback({ isOk: true, msg: `${successSceneMap[scene]} success` }); } else { const defaultErrMsg = res?.message?.toLowerCase().includes?.(scene) || ((statusCode || res.code) && res?.message) ? res?.message : `${scene} fail (status:${statusCode || res.code}), please try again.`; callback({ isOk: false, code: res?.code, msg: defaultErrMsg, result: res }); } } else { if ((res as WriteResponse)?.success) { callback({ isOk: true, msg: `${successSceneMap[scene]} success` }); } else { const cloudFlareErrors = [ErrorCode.NotFoundRoute, ErrorCode.NamespaceIdError, ErrorCode.AuthenicationError]; const isAccountError = res?.errors?.length && cloudFlareErrors.includes(res.errors[0].code); if (isAccountError) { callback({ isOk: false, msg: res.errors[0].code === ErrorCode.NamespaceIdError ? 'cloudflare namespace Id info is incorrect.' : 'cloudflare account info is incorrect.', code: MessageErrorCode.CloudflareNotFoundRoute, result: res, }); } else { const defaultErrMsg = res?.message?.toLowerCase().includes?.(scene) || (res?.code && res?.message) ? res?.message : `${scene} fail, please try again.`; callback({ isOk: false, code: res?.code, msg: defaultErrMsg, result: res }); } } } } function addProtocol(uri: string) { return uri.startsWith('http') ? uri : `http://${uri}`; } export async function extractDomainAndPort(url: string, isRemoveWWW = true): Promise<[string, string, string]> { let urlObj: URL; try { const maybeValidUrl = addProtocol(url); urlObj = new URL(maybeValidUrl); } catch (error) { return [url, '', url]; } let hostname = urlObj.hostname; const port = urlObj.port; hostname = hostname.replace('http://', '').replace('https://', ''); if (isRemoveWWW) { hostname = hostname.replace('www.', ''); } // match ip address if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) { return [hostname, port, hostname]; } if (hostname.split('.').length <= 2) { return [hostname, port, hostname]; } const includeLocalStorage = settingsStorage.getSnapshot()?.includeLocalStorage; if (includeLocalStorage) { return [hostname, port, hostname]; } return new Promise(resolve => { try { chrome.cookies.getAll( { url, }, async cookies => { console.log('cookies', cookies); if (cookies) { const hasHostCookie = cookies.find(item => item.domain.includes(hostname)); if (hasHostCookie) { resolve([hostname, port, hostname]); } else { const domain = cookies[0].domain; if (domain.startsWith('.')) { resolve([domain.slice(1), port, hostname]); } else { resolve([domain, port, hostname]); } } } else { // const match = hostname.match(/([^.]+\.[^.]+)$/); resolve([hostname, port, hostname]); } }, ); } catch (error) { console.error('error', error); resolve([hostname, port, hostname]); } }); } ================================================ FILE: packages/shared/package.json ================================================ { "name": "@sync-your-cookie/shared", "version": "0.0.1", "description": "chrome extension shared code", "private": true, "sideEffects": false, "files": [ "dist/**" ], "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "scripts": { "clean": "rimraf ./dist && rimraf .turbo", "build": "tsup index.ts --format esm,cjs --dts --external react,chrome", "dev": "tsc -w", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "pnpm lint --fix", "prettier": "prettier . --write", "type-check": "tsc --noEmit", "proto": "pbjs -o ./lib/protobuf/proto/cookie.js -w es6 -t static-module ./lib/protobuf/proto/*.proto && pbts ./lib/protobuf/proto/cookie.js -o ./lib/protobuf/proto/cookie.d.ts" }, "dependencies": { "@octokit/rest": "^22.0.0", "@octokit/types": "^15.0.1", "p-timeout": "^6.1.4", "pako": "^2.1.0", "protobufjs": "^7.3.2" }, "devDependencies": { "@sync-your-cookie/protobuf": "workspace:*", "@sync-your-cookie/storage": "workspace:*", "@sync-your-cookie/tsconfig": "workspace:*", "@types/pako": "^2.0.3", "sonner": "^1.5.0", "tsup": "8.0.2" } } ================================================ FILE: packages/shared/tsconfig.json ================================================ { "extends": "@sync-your-cookie/tsconfig/utils", "compilerOptions": { "outDir": "dist", "jsx": "react-jsx", "checkJs": false, "allowJs": false, "baseUrl": ".", "declarationMap": true, "sourceMap": true, "paths": { "@lib/*": ["lib/*"] }, "types": ["chrome", "node", "@octokit/types"] }, "include": ["index.ts", "lib"], } ================================================ FILE: packages/shared/tsup.config.ts ================================================ import { defineConfig } from 'tsup'; export default defineConfig({ treeshake: true, splitting: false, format: ['cjs', 'esm'], dts: true, external: ['chrome'], }); ================================================ FILE: packages/storage/index.ts ================================================ export * from './lib'; ================================================ FILE: packages/storage/lib/accountStorage.ts ================================================ import { BaseStorage, createStorage, StorageType } from './base'; export interface AccountInfo { accountId?: string; namespaceId?: string; token?: string; selectedProvider?: 'cloudflare' | 'github'; githubAccessToken?: string; avatarUrl?: string; name?: string | null; bio?: string | null; email?: string | null; } const key = 'cloudflare-account-storage-key'; const cacheStorageMap = new Map(); const initStorage = (): BaseStorage => { if (cacheStorageMap.has(key)) { return cacheStorageMap.get(key); } const storage = createStorage( key, { selectedProvider: 'cloudflare', }, { storageType: StorageType.Sync, liveUpdate: true, }, ); cacheStorageMap.set(key, storage); return storage; }; const storage = initStorage(); type AccountInfoStorage = BaseStorage & { update: (updateInfo: AccountInfo) => Promise; }; export const accountStorage: AccountInfoStorage = { ...storage, update: async (updateInfo: AccountInfo) => { await storage.set(currentInfo => { return { ...currentInfo, ...updateInfo }; }); }, }; ================================================ FILE: packages/storage/lib/base.ts ================================================ /** * Storage area type for persisting and exchanging data. * @see https://developer.chrome.com/docs/extensions/reference/storage/#overview */ export enum StorageType { /** * Persist data locally against browser restarts. Will be deleted by uninstalling the extension. * @default */ Local = 'local', /** * Uploads data to the users account in the cloud and syncs to the users browsers on other devices. Limits apply. */ Sync = 'sync', /** * Requires an [enterprise policy](https://www.chromium.org/administrators/configuring-policy-for-extensions) with a * json schema for company wide config. */ Managed = 'managed', /** * Only persist data until the browser is closed. Recommended for service workers which can shutdown anytime and * therefore need to restore their state. Set {@link SessionAccessLevel} for permitting content scripts access. * @implements Chromes [Session Storage](https://developer.chrome.com/docs/extensions/reference/storage/#property-session) */ Session = 'session', } /** * Global access level requirement for the {@link StorageType.Session} Storage Area. * @implements Chromes [Session Access Level](https://developer.chrome.com/docs/extensions/reference/storage/#method-StorageArea-setAccessLevel) */ export enum SessionAccessLevel { /** * Storage can only be accessed by Extension pages (not Content scripts). * @default */ ExtensionPagesOnly = 'TRUSTED_CONTEXTS', /** * Storage can be accessed by both Extension pages and Content scripts. */ ExtensionPagesAndContentScripts = 'TRUSTED_AND_UNTRUSTED_CONTEXTS', } type ValueOrUpdate = D | ((prev: D) => Promise | D); export type BaseStorage = { get: () => Promise; set: (value: ValueOrUpdate) => Promise; getSnapshot: () => D | null; subscribe: (listener: () => void) => () => void; }; type StorageConfig = { /** * Assign the {@link StorageType} to use. * @default Local */ storageType?: StorageType; /** * Only for {@link StorageType.Session}: Grant Content scripts access to storage area? * @default false */ sessionAccessForContentScripts?: boolean; /** * Keeps state live in sync between all instances of the extension. Like between popup, side panel and content scripts. * To allow chrome background scripts to stay in sync as well, use {@link StorageType.Session} storage area with * {@link StorageConfig.sessionAccessForContentScripts} potentially also set to true. * @see https://stackoverflow.com/a/75637138/2763239 * @default false */ liveUpdate?: boolean; /** * An optional props for converting values from storage and into it. * @default undefined */ serialization?: { /** * convert non-native values to string to be saved in storage */ serialize: (value: D) => string; /** * convert string value from storage to non-native values */ deserialize: (text: string) => D; }; }; /** * Sets or updates an arbitrary cache with a new value or the result of an update function. */ async function updateCache(valueOrUpdate: ValueOrUpdate, cache: D | null): Promise { // Type guard to check if our value or update is a function function isFunction(value: ValueOrUpdate): value is (prev: D) => D | Promise { return typeof value === 'function'; } // Type guard to check in case of a function, if its a Promise function returnsPromise(func: (prev: D) => D | Promise): func is (prev: D) => Promise { // Use ReturnType to infer the return type of the function and check if it's a Promise return (func as (prev: D) => Promise) instanceof Promise; } if (isFunction(valueOrUpdate)) { // Check if the function returns a Promise if (returnsPromise(valueOrUpdate)) { return await valueOrUpdate(cache as D); } else { return valueOrUpdate(cache as D); } } else { return valueOrUpdate; } } /** * If one session storage needs access from content scripts, we need to enable it globally. * @default false */ let globalSessionAccessLevelFlag: StorageConfig['sessionAccessForContentScripts'] = false; /** * Checks if the storage permission is granted in the manifest.json. */ function checkStoragePermission(storageType: StorageType): void { if (chrome.storage[storageType] === undefined) { throw new Error(`Check your storage permission in manifest.json: ${storageType} is not defined`); } } /** * Creates a storage area for persisting and exchanging data. */ export function createStorage(key: string, fallback: D, config?: StorageConfig): BaseStorage { let cache: D | null = null; let initedCache = false; let listeners: Array<() => void> = []; const storageType = config?.storageType ?? StorageType.Local; const liveUpdate = config?.liveUpdate ?? false; const serialize = config?.serialization?.serialize ?? ((v: D) => v); const deserialize = config?.serialization?.deserialize ?? (v => v as D); // Set global session storage access level for StoryType.Session, only when not already done but needed. if ( globalSessionAccessLevelFlag === false && storageType === StorageType.Session && config?.sessionAccessForContentScripts === true ) { checkStoragePermission(storageType); chrome.storage[storageType] .setAccessLevel({ accessLevel: SessionAccessLevel.ExtensionPagesAndContentScripts, }) .catch(error => { console.warn(error); console.warn('Please call setAccessLevel into different context, like a background script.'); }); globalSessionAccessLevelFlag = true; } // Register life cycle methods const _getDataFromStorage = async (): Promise => { checkStoragePermission(storageType); const value = await chrome.storage[storageType].get([key]); return deserialize(value[key]) ?? fallback; }; const _emitChange = () => { listeners.forEach(listener => listener()); }; let setUpdatePromise: Promise | null = null; let setStoragePromise: Promise | null = null; const set = async (valueOrUpdate: ValueOrUpdate) => { if (initedCache === false) { cache = await _getDataFromStorage(); } await Promise.allSettled([setUpdatePromise, setStoragePromise]); setUpdatePromise = updateCache(valueOrUpdate, cache); cache = await setUpdatePromise.then(val => { setUpdatePromise = null; return val; }); // if (!liveUpdate) { // } _emitChange(); if (cache) { //FIXME: 存在 set 执行完之后,onChanged 尚没有执行,,如果在 onchange 中再次改变 cache 值,在连续段时间内操作多次 set 操作,最终结果会不符合预期 setStoragePromise = chrome.storage[storageType].set({ [key]: serialize(cache) }); await setStoragePromise.then(async () => { setStoragePromise = null; return await new Promise(resolve => { setTimeout(() => { resolve(undefined); }, 200); }); }); } }; const subscribe = (listener: () => void) => { listeners = [...listeners, listener]; return () => { listeners = listeners.filter(l => l !== listener); }; }; const getSnapshot = () => { return cache; }; _getDataFromStorage().then(data => { cache = data; initedCache = true; _emitChange(); }); // Listener for live updates from the browser async function _updateFromStorageOnChanged(changes: { [key: string]: chrome.storage.StorageChange }) { // Check if the key we are listening for is in the changes object if (changes[key] === undefined) return; const valueOrUpdate: ValueOrUpdate = deserialize(changes[key].newValue); if (cache === valueOrUpdate) return; cache = await updateCache(valueOrUpdate, cache); _emitChange(); } // Register listener for live updates for our storage area if (liveUpdate) { chrome.storage[storageType].onChanged.addListener(_updateFromStorageOnChanged); } return { get: _getDataFromStorage, set, getSnapshot, subscribe, }; } ================================================ FILE: packages/storage/lib/cookieStorage.ts ================================================ import { ICookie, ICookiesMap, ILocalStorageItem } from '@sync-your-cookie/protobuf'; import { BaseStorage, createStorage, StorageType } from './base'; export interface Cookie extends ICookiesMap {} const cacheStorageMap = new Map(); const key = 'cookie-storage-key'; const initStorage = (): BaseStorage => { if (cacheStorageMap.has(key)) { return cacheStorageMap.get(key); } const storage: BaseStorage = createStorage( key, {}, { storageType: StorageType.Local, liveUpdate: true, }, ); cacheStorageMap.set(key, storage); return storage; }; const storage = initStorage(); export const cookieStorage = { ...storage, reset: async () => { await storage.set(() => { return {}; }); }, updateItem: async (domain: string, updateCookies: ICookie[], items: ILocalStorageItem[] =[]) => { let newVal: Cookie = {}; await storage.set(currentInfo => { const domainCookieMap = currentInfo.domainCookieMap || {}; currentInfo.createTime = currentInfo.createTime || Date.now(); currentInfo.updateTime = Date.now(); domainCookieMap[domain] = { ...domainCookieMap[domain], cookies: updateCookies, localStorageItems: items }; newVal = { ...currentInfo, domainCookieMap }; return newVal; }); return newVal; }, update: async (updateInfo: Cookie, isInit = false) => { let newVal: Cookie = {}; await storage.set(currentInfo => { newVal = isInit ? updateInfo : { ...currentInfo, ...updateInfo }; return newVal; }); return newVal; }, removeItem: async (domain: string) => { let newVal: Cookie = {}; await storage.set(currentInfo => { const domainCookieMap = currentInfo.domainCookieMap || {}; delete domainCookieMap[domain]; newVal = { ...currentInfo, domainCookieMap }; return newVal; }); return newVal; }, removeDomainItem: async (domain: string, name: string) => { let newVal: Cookie = {}; await storage.set(currentInfo => { const domainCookieMap = currentInfo.domainCookieMap || {}; const domainCookies = domainCookieMap[domain] || {}; const cookies = domainCookies.cookies || []; const newCookies = cookies.filter(cookie => cookie.name !== name); domainCookieMap[domain] = { ...domainCookies, cookies: newCookies, }; newVal = { ...currentInfo, domainCookieMap }; return newVal; }); return newVal; }, }; ================================================ FILE: packages/storage/lib/domainConfigStorage.ts ================================================ import { BaseStorage, createStorage, StorageType } from './base'; type DomainItemConfig = { autoPull?: boolean; autoPush?: boolean; favIconUrl?: string; sourceUrl?: string; }; interface DomainConfig { domainMap: { [host: string]: DomainItemConfig; }; } const key = 'domainConfig-storage-key'; const cacheStorageMap = new Map(); const initStorage = (): BaseStorage => { if (cacheStorageMap.has(key)) { return cacheStorageMap.get(key); } const storage: BaseStorage = createStorage( key, { domainMap: {}, }, { storageType: StorageType.Local, liveUpdate: true, // onLoad: onLoad, }, ); cacheStorageMap.set(key, storage); return storage; }; const storage = initStorage(); export const domainConfigStorage = { ...storage, updateItem: async (host: string, updateConf: DomainItemConfig) => { return await storage.set(currentInfo => { const domainMap = currentInfo?.domainMap || {}; domainMap[host] = { ...domainMap[host], ...updateConf, }; return { ...(currentInfo || {}), domainMap }; }); }, update: async (updateInfo: Partial) => { return await storage.set(currentInfo => { return { ...currentInfo, ...updateInfo }; }); }, removeItem: async (domain: string) => { await storage.set(currentInfo => { const domainCookieMap = currentInfo.domainMap || {}; delete domainCookieMap[domain]; return { ...currentInfo, domainCookieMap }; }); }, toggleAutoPullState: async (domain: string, checked?: boolean) => { return await storage.set(currentInfo => { const domainMap = currentInfo?.domainMap || {}; domainMap[domain] = { ...domainMap[domain], autoPull: checked ?? !domainMap[domain]?.autoPull, }; return { ...(currentInfo || {}), domainMap }; }); }, toggleAutoPushState: async (domain: string, checked?: boolean) => { return await storage.set(currentInfo => { const domainMap = currentInfo?.domainMap || {}; domainMap[domain] = { ...domainMap[domain], autoPush: checked ?? !domainMap[domain]?.autoPush, }; return { ...(currentInfo || {}), domainMap }; }); }, }; ================================================ FILE: packages/storage/lib/domainStatusStorage.ts ================================================ import { BaseStorage, createStorage, StorageType } from './base'; type DomainItemConfig = { pulling?: boolean; pushing?: boolean; }; interface DomainConfig { pulling: boolean; pushing: boolean; domainMap: { [host: string]: DomainItemConfig; }; } const key = 'domainStatus-storage-key'; const cacheStorageMap = new Map(); const initStorage = (): BaseStorage => { if (cacheStorageMap.has(key)) { return cacheStorageMap.get(key); } const storage: BaseStorage = createStorage( key, { pulling: false, pushing: false, domainMap: {}, }, { storageType: StorageType.Session, liveUpdate: true, // onLoad: onLoad, }, ); cacheStorageMap.set(key, storage); return storage; }; const storage = initStorage(); export const domainStatusStorage = { ...storage, resetState: async () => { return await storage.set(currentInfo => { const domainMap = currentInfo?.domainMap || {}; for (const domain in domainMap) { if (domain) { domainMap[domain] = { ...domainMap[domain], pulling: false, pushing: false, }; } else { delete domainMap[domain]; } } const resetInfo = { pulling: false, pushing: false, domainMap: domainMap, }; return resetInfo; }); }, updateItem: async (host: string, updateConf: DomainItemConfig) => { return await storage.set(currentInfo => { const domainMap = currentInfo?.domainMap || {}; domainMap[host] = { ...domainMap[host], ...updateConf, }; return { ...(currentInfo || {}), domainMap }; }); }, update: async (updateInfo: Partial) => { return await storage.set(currentInfo => { return { ...currentInfo, ...updateInfo }; }); }, removeItem: async (domain: string) => { await storage.set(currentInfo => { const domainCookieMap = currentInfo.domainMap || {}; delete domainCookieMap[domain]; return { ...currentInfo, domainCookieMap }; }); }, togglePullingState: async (domain: string, checked?: boolean) => { return await storage.set(currentInfo => { const domainMap = currentInfo?.domainMap || {}; domainMap[domain] = { ...domainMap[domain], pulling: checked ?? !domainMap[domain]?.pulling, }; return { ...(currentInfo || {}), domainMap }; }); }, togglePushingState: async (domain: string, checked?: boolean) => { return await storage.set(currentInfo => { const domainMap = currentInfo?.domainMap || {}; domainMap[domain] = { ...domainMap[domain], pushing: checked ?? !domainMap[domain]?.pushing, }; return { ...(currentInfo || {}), domainMap }; }); }, }; ================================================ FILE: packages/storage/lib/index.ts ================================================ import { SessionAccessLevel, StorageType, createStorage, type BaseStorage } from './base'; // export * from './accountStorage'; // export * from './cookieStorage'; // export * from './domainConfigStorage'; // export * from './themeStorage'; export { BaseStorage, SessionAccessLevel, StorageType, createStorage }; ================================================ FILE: packages/storage/lib/settingsStorage.ts ================================================ import { BaseStorage, createStorage, StorageType } from './base'; export interface IStorageItem { value: string; label: string; rawUrl?: string; [key: string]: unknown; } export interface ISettings { storageKeyList: IStorageItem[]; storageKey?: string; storageKeyGistId?: string; gistHtmlUrl?: string; protobufEncoding?: boolean; includeLocalStorage?: boolean; localStorageGetting?: boolean; contextMenu?: boolean; encryptionEnabled?: boolean; encryptionPassword?: string; } const key = 'settings-storage-key'; const cacheStorageMap = new Map(); export const defaultKey = 'sync-your-cookie'; const initStorage = (): BaseStorage => { if (cacheStorageMap.has(key)) { return cacheStorageMap.get(key); } const storage = createStorage( key, { storageKeyList: [{ value: defaultKey, label: defaultKey }], storageKey: defaultKey, protobufEncoding: false, includeLocalStorage: true, contextMenu: false, }, { storageType: StorageType.Sync, liveUpdate: true, }, ); cacheStorageMap.set(key, storage); return storage; }; const storage = initStorage(); type TSettingsStorage = BaseStorage & { update: (updateInfo: Partial) => Promise; addStorageKey: (key: string) => Promise; removeStorageKey: (key: string) => Promise; // getStorageKeyList: () => Promise; }; export const settingsStorage: TSettingsStorage = { ...storage, update: async (updateInfo: Partial) => { await storage.set(currentInfo => { return { ...currentInfo, ...updateInfo }; }); }, addStorageKey: async (key: string) => { await storage.set(currentInfo => { const exists = currentInfo.storageKeyList.find(item => item.value === key); if (exists) { return currentInfo; } return { ...currentInfo, storageKeyList: [...currentInfo.storageKeyList, { value: key, label: key }], }; }); }, removeStorageKey: async (key: string) => { await storage.set(currentInfo => { const exists = currentInfo.storageKeyList.find(item => item.value === key); if (!exists) { return currentInfo; } return { ...currentInfo, storageKeyList: currentInfo.storageKeyList.filter(item => item.value !== key), }; }); }, }; export const getActiveStorageItem = (): IStorageItem | undefined => { const snapshot = settingsStorage.getSnapshot(); const storageKey = snapshot?.storageKey; return snapshot?.storageKeyList.find(item => item.value === storageKey); }; export const initStorageKey = () => { settingsStorage.update({ storageKeyList: [{ value: defaultKey, label: defaultKey }], storageKey: defaultKey, }); }; ================================================ FILE: packages/storage/lib/themeStorage.ts ================================================ import { BaseStorage, createStorage, StorageType } from './base'; type Theme = 'light' | 'dark' | 'system'; type ThemeStorage = BaseStorage & { toggle: () => Promise; }; const cacheStorageMap = new Map(); const key = 'theme-storage-key'; const initStorage = (): BaseStorage => { if (cacheStorageMap.has(key)) { console.log('key', key); return cacheStorageMap.get(key); } const storage = createStorage(key, 'light', { storageType: StorageType.Local, liveUpdate: true, }); cacheStorageMap.set(key, storage); return storage; }; const storage = initStorage(); export const themeStorage: ThemeStorage = { ...storage, toggle: async () => { await storage.set((currentTheme: string) => { return currentTheme === 'light' ? 'dark' : 'light'; }); }, }; ================================================ FILE: packages/storage/package.json ================================================ { "name": "@sync-your-cookie/storage", "version": "0.0.1", "description": "chrome extension storage", "private": true, "sideEffects": false, "files": [ "dist/**" ], "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "scripts": { "clean": "rimraf ./dist && rimraf .turbo", "build": "tsup index.ts --format esm,cjs --dts", "dev": "tsc -w", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "pnpm lint --fix", "prettier": "prettier . --write", "type-check": "tsc --noEmit" }, "dependencies": {}, "devDependencies": { "@sync-your-cookie/tsconfig": "workspace:*", "@sync-your-cookie/protobuf": "workspace:*", "tsup": "8.0.2" } } ================================================ FILE: packages/storage/tsconfig.json ================================================ { "extends": "@sync-your-cookie/tsconfig/utils", "compilerOptions": { "outDir": "dist", "jsx": "react-jsx", "baseUrl": ".", "paths": { "@lib/*": ["lib/*"] }, "types": ["chrome"] }, "include": ["index.ts", "lib"] } ================================================ FILE: packages/tailwind-config/package.json ================================================ { "name": "@sync-your-cookie/tailwindcss-config", "version": "1.0.0", "description": "Tailwind CSS configuration for boilerplate", "main": "./tailwind.config.js", "private": true } ================================================ FILE: packages/tailwind-config/tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ module.exports = { /** shared theme configuration */ theme: { extend: {}, }, /** shared plugins configuration */ plugins: [], }; ================================================ FILE: packages/tsconfig/app.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig", "display": "Chrome Extension App", "extends": "./base.json" } ================================================ FILE: packages/tsconfig/base.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig", "display": "Base", "compilerOptions": { "outDir": "dist", "allowJs": true, "noEmit": true, "downlevelIteration": true, "isolatedModules": true, "strict": true, "noImplicitAny": true, "strictNullChecks": true, "moduleResolution": "node", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "noImplicitReturns": true, "jsx": "react-jsx", "lib": [ "DOM", "ESNext" ], "plugins": [ { "transform": "typescript-transform-paths" }, ] } } ================================================ FILE: packages/tsconfig/package.json ================================================ { "name": "@sync-your-cookie/tsconfig", "version": "1.0.0", "description": "tsconfig for chrome extension", "private": true, "scripts": { "prepare": "ts-patch install -s" }, "devDependencies": { "ts-patch": "^3.2.1", "typescript-transform-paths": "^3.4.10" } } ================================================ FILE: packages/tsconfig/utils.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig", "display": "Chrome Extension Utils", "extends": "./base.json", "compilerOptions": { "noEmit": false, "declaration": true, "module": "CommonJS", "moduleResolution": "node", "declarationMap": true, "target": "ES6", "types": ["node"], "plugins": [ { "transform": "typescript-transform-paths" }, ] } } ================================================ FILE: packages/ui/components.json ================================================ { "$schema": "https://ui.shadcn.com/schema.json", "style": "default", "rsc": false, "tsx": true, "tailwind": { "config": "tailwind.config.js", "css": "./globals.css", "baseColor": "slate", "cssVariables": true, "prefix": "" }, "aliases": { "components": "src/components", "utils": "@libs/utils" } } ================================================ FILE: packages/ui/globals.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 224 71.4% 4.1%; --card: 0 0% 100%; --card-foreground: 224 71.4% 4.1%; --popover: 0 0% 100%; --popover-foreground: 224 71.4% 4.1%; --primary: 262.1 83.3% 57.8%; --primary-foreground: 210 20% 98%; --secondary: 220 14.3% 95.9%; --secondary-foreground: 220.9 39.3% 11%; --muted: 220 14.3% 95.9%; --muted-foreground: 220 8.9% 46.1%; --accent: 220 14.3% 95.9%; --accent-foreground: 220.9 39.3% 11%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 20% 98%; --border: 220 13% 91%; --input: 220 13% 91%; --ring: 262.1 83.3% 57.8%; --radius: 0.5rem; --chart-1: 12 76% 61%; --chart-2: 173 58% 39%; --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; } .dark { --background: 224 71.4% 4.1%; --foreground: 210 20% 98%; --card: 224 71.4% 4.1%; --card-foreground: 210 20% 98%; --popover: 224 71.4% 4.1%; --popover-foreground: 210 20% 98%; --primary: 263.4 70% 50.4%; --primary-foreground: 210 20% 98%; --secondary: 215 27.9% 16.9%; --secondary-foreground: 210 20% 98%; --muted: 215 27.9% 16.9%; --muted-foreground: 217.9 10.6% 64.9%; --accent: 215 27.9% 16.9%; --accent-foreground: 210 20% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 20% 98%; --border: 215 27.9% 16.9%; --input: 215 27.9% 16.9%; --ring: 263.4 70% 50.4%; --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } } ================================================ FILE: packages/ui/package.json ================================================ { "name": "@sync-your-cookie/ui", "version": "0.0.1", "description": "chrome extension ui", "private": true, "sideEffects": false, "type": "module", "files": [ "dist/**", "dist/**/*.css" ], "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.js" }, "./css": "./globals.css", "./tailwind.config": "./tailwind.config.js" }, "scripts": { "clean": "rimraf ./dist && rimraf .turbo", "dev:tsup": "tsup src/index.ts --format esm,cjs --dts --external react,chrome --watch", "build:tsup": "tsup src/index.ts --format esm,cjs --dts --external react,chrome --watch", "dev": "tsc -w", "build": "tsc", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "pnpm lint --fix", "prettier": "prettier . --write", "type-check": "tsc --noEmit" }, "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-tooltip": "^1.1.3", "@tanstack/react-table": "^8.20.5", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "lucide-react": "^0.394.0", "next-themes": "^0.3.0", "sonner": "^1.5.0", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@sync-your-cookie/tailwindcss-config": "workspace:*", "@sync-your-cookie/tsconfig": "workspace:*", "tsup": "8.0.2", "typescript-transform-paths": "^3.4.10" } } ================================================ FILE: packages/ui/postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ================================================ FILE: packages/ui/src/components/DateTable/index.tsx ================================================ import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'; import type { ColumnDef } from '@tanstack/react-table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; interface DataTableProps { columns: ColumnDef[]; data: TData[]; } // export * from '@tanstack/react-table'; export { ColumnDef }; export function DataTable({ columns, data }: DataTableProps) { const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), }); return (
{table.getHeaderGroups().map(headerGroup => ( {headerGroup.headers.map(header => { return ( {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} ); })} ))} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map(row => ( {row.getVisibleCells().map(cell => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} )) ) : ( No results. )}
); } ================================================ FILE: packages/ui/src/components/Image/index.tsx ================================================ import { FC } from 'react'; import { Avatar, AvatarFallback, AvatarImage } from '../ui'; const randomBgColor = [ '#ec6a5e', '#f5bd4f', '#61c455', '#3a82f7', '#7246e4', '#bef653', '#e97a35', '#4c9f54', '#3266e3', ]; interface ImageProps { src: string; value?: string; index?: number; } export const Image: FC = ({ index, src, value }) => { const randomIndex = typeof index === 'number' ? index % randomBgColor.length : 0; return (
{value && typeof randomIndex === 'number' && ( {value?.slice(0, 1).toLocaleUpperCase()} )}
); }; ================================================ FILE: packages/ui/src/components/Spinner/index.tsx ================================================ import { cn } from '@libs/utils'; import { VariantProps, cva } from 'class-variance-authority'; import { Loader } from 'lucide-react'; import React from 'react'; const spinnerVariants = cva( 'absolute bg-white/60 w-full h-full top-0 bottom-0 left-0 right-0 flex-col items-center justify-center', { variants: { show: { true: 'flex', false: 'hidden', }, }, defaultVariants: { show: true, }, }, ); const loaderVariants = cva('animate-spin text-primary', { variants: { size: { small: 'size-6', medium: 'size-8', large: 'size-12', }, }, defaultVariants: { size: 'medium', }, }); interface SpinnerContentProps extends VariantProps, VariantProps { className?: string; children?: React.ReactNode; } export function Spinner({ size, show = true, children, className }: SpinnerContentProps) { return ( <> {children}
{show ? : null}
); } ================================================ FILE: packages/ui/src/components/ThemeDropdown/index.tsx ================================================ import { Moon, Sun } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; // import { useTheme } from "@/components/theme-provider" interface ThemeDropdownProps { setTheme: (theme: 'dark' | 'light' | 'system') => void; } export function ThemeDropdown(props: ThemeDropdownProps) { const { setTheme } = props; return ( setTheme('light')}>Light setTheme('dark')}>Dark setTheme('system')}>System ); } ================================================ FILE: packages/ui/src/components/Tooltip/index.tsx ================================================ import { Tooltip as STooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; interface TooltipProps { children: React.ReactNode; title?: string | React.ReactNode; align?: 'start' | 'center' | 'end' | undefined; alignOffset?: number; } const Tooltip = (props: TooltipProps) => { const { children, title, alignOffset, align } = props; return ( {children}

{title}

); }; export default Tooltip; ================================================ FILE: packages/ui/src/components/index.ts ================================================ export * from './DateTable'; export * from './Image'; export * from './Spinner'; export * from './ThemeDropdown'; export * from './ui'; import { default as SyncTooltip } from './Tooltip'; // import from './Tooltip'; export { SyncTooltip }; ================================================ FILE: packages/ui/src/components/ui/alert-dialog.tsx ================================================ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; import * as React from 'react'; import { cn } from '@libs/utils'; import { buttonVariants } from 'src/components/ui/button'; const AlertDialog = AlertDialogPrimitive.Root; const AlertDialogTrigger = AlertDialogPrimitive.Trigger; const AlertDialogPortal = AlertDialogPrimitive.Portal; const AlertDialogOverlay = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; const AlertDialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
); AlertDialogHeader.displayName = 'AlertDialogHeader'; const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
); AlertDialogFooter.displayName = 'AlertDialogFooter'; const AlertDialogTitle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; const AlertDialogDescription = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; const AlertDialogAction = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; const AlertDialogCancel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; export { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger }; ================================================ FILE: packages/ui/src/components/ui/alert.tsx ================================================ import { cva, type VariantProps } from 'class-variance-authority'; import * as React from 'react'; import { cn } from '@libs/utils'; const alertVariants = cva( 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', { variants: { variant: { default: 'bg-background text-foreground', destructive: 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', }, }, defaultVariants: { variant: 'default', }, }, ); const Alert = React.forwardRef< HTMLDivElement, React.HTMLAttributes & VariantProps >(({ className, variant, ...props }, ref) => (
)); Alert.displayName = 'Alert'; const AlertTitle = React.forwardRef>( ({ className, ...props }, ref) => (
), ); AlertTitle.displayName = 'AlertTitle'; const AlertDescription = React.forwardRef>( ({ className, ...props }, ref) => (
), ); AlertDescription.displayName = 'AlertDescription'; export { Alert, AlertDescription, AlertTitle }; ================================================ FILE: packages/ui/src/components/ui/avatar.tsx ================================================ import * as AvatarPrimitive from '@radix-ui/react-avatar'; import * as React from 'react'; import { cn } from '@libs/utils'; const Avatar = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); Avatar.displayName = AvatarPrimitive.Root.displayName; const AvatarImage = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AvatarImage.displayName = AvatarPrimitive.Image.displayName; const AvatarFallback = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; export { Avatar, AvatarFallback, AvatarImage }; ================================================ FILE: packages/ui/src/components/ui/button.tsx ================================================ import { Slot } from '@radix-ui/react-slot'; import { cva, type VariantProps } from 'class-variance-authority'; import * as React from 'react'; import { cn } from '@libs/utils'; const buttonVariants = cva( 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', }, size: { default: 'h-10 px-4 py-2', sm: 'h-9 rounded-md px-3', lg: 'h-11 rounded-md px-8', icon: 'h-10 w-10', }, }, defaultVariants: { variant: 'default', size: 'default', }, }, ); export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : 'button'; return ; }, ); Button.displayName = 'Button'; export { Button, buttonVariants }; ================================================ FILE: packages/ui/src/components/ui/card.tsx ================================================ import * as React from 'react'; import { cn } from '@libs/utils'; const Card = React.forwardRef>(({ className, ...props }, ref) => (
)); Card.displayName = 'Card'; const CardHeader = React.forwardRef>( ({ className, ...props }, ref) => (
), ); CardHeader.displayName = 'CardHeader'; const CardTitle = React.forwardRef>( ({ className, ...props }, ref) => (

), ); CardTitle.displayName = 'CardTitle'; const CardDescription = React.forwardRef>( ({ className, ...props }, ref) => (

), ); CardDescription.displayName = 'CardDescription'; const CardContent = React.forwardRef>( ({ className, ...props }, ref) =>

, ); CardContent.displayName = 'CardContent'; const CardFooter = React.forwardRef>( ({ className, ...props }, ref) => (
), ); CardFooter.displayName = 'CardFooter'; export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; ================================================ FILE: packages/ui/src/components/ui/dropdown-menu.tsx ================================================ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; import { Check, ChevronRight, Circle } from 'lucide-react'; import * as React from 'react'; import { cn } from '@libs/utils'; const DropdownMenu = DropdownMenuPrimitive.Root; const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; const DropdownMenuGroup = DropdownMenuPrimitive.Group; const DropdownMenuPortal = DropdownMenuPrimitive.Portal; const DropdownMenuSub = DropdownMenuPrimitive.Sub; const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; const DropdownMenuSubTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { inset?: boolean; } >(({ className, inset, children, ...props }, ref) => ( {children} )); DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; const DropdownMenuSubContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; const DropdownMenuContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, sideOffset = 4, ...props }, ref) => ( )); DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; const DropdownMenuItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { inset?: boolean; } >(({ className, inset, ...props }, ref) => ( )); DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; const DropdownMenuCheckboxItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, checked, ...props }, ref) => ( {children} )); DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; const DropdownMenuRadioItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( {children} )); DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; const DropdownMenuLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { inset?: boolean; } >(({ className, inset, ...props }, ref) => ( )); DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; const DropdownMenuSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { return ; }; DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'; export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger }; ================================================ FILE: packages/ui/src/components/ui/index.ts ================================================ export * from './alert'; export * from './button'; export * from './avatar'; export * from './card'; export * from './dropdown-menu'; export * from './input'; export * from './label'; export * from './sonner'; export * from './switch'; // export * from './table'; export * from './alert-dialog'; export * from './popover'; export * from './tooltip'; export * from './select'; export * from './toggle'; ================================================ FILE: packages/ui/src/components/ui/input.tsx ================================================ import * as React from 'react'; import { cn } from '@libs/utils'; export interface InputProps extends React.InputHTMLAttributes {} const Input = React.forwardRef(({ className, type, ...props }, ref) => { return ( ); }); Input.displayName = 'Input'; export { Input }; ================================================ FILE: packages/ui/src/components/ui/label.tsx ================================================ import * as LabelPrimitive from '@radix-ui/react-label'; import { cva, type VariantProps } from 'class-variance-authority'; import * as React from 'react'; import { cn } from '@libs/utils'; const labelVariants = cva('text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'); const Label = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & VariantProps >(({ className, ...props }, ref) => ( )); Label.displayName = LabelPrimitive.Root.displayName; export { Label }; ================================================ FILE: packages/ui/src/components/ui/popover.tsx ================================================ import * as PopoverPrimitive from '@radix-ui/react-popover'; import * as React from 'react'; import { cn } from '@libs/utils'; const Popover = PopoverPrimitive.Root; const PopoverTrigger = PopoverPrimitive.Trigger; const PopoverContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( )); PopoverContent.displayName = PopoverPrimitive.Content.displayName; export { Popover, PopoverContent, PopoverTrigger }; ================================================ FILE: packages/ui/src/components/ui/select.tsx ================================================ import * as SelectPrimitive from '@radix-ui/react-select'; import { Check, ChevronDown, ChevronUp } from 'lucide-react'; import * as React from 'react'; import { cn } from '@libs/utils'; const Select = SelectPrimitive.Root; const SelectPortal = SelectPrimitive.Portal; const SelectGroup = SelectPrimitive.Group; const SelectValue = SelectPrimitive.Value; const SelectTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( span]:line-clamp-1', className, )} {...props}> {children} )); SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; const SelectScrollUpButton = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; const SelectScrollDownButton = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; const SelectContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, position = 'popper', ...props }, ref) => ( {children} )); SelectContent.displayName = SelectPrimitive.Content.displayName; const SelectLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); SelectLabel.displayName = SelectPrimitive.Label.displayName; const SelectItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( {children} )); SelectItem.displayName = SelectPrimitive.Item.displayName; const SelectSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); SelectSeparator.displayName = SelectPrimitive.Separator.displayName; export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectPortal, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue }; ================================================ FILE: packages/ui/src/components/ui/sonner.tsx ================================================ import { useTheme } from "next-themes" import { Toaster as Sonner } from "sonner" type ToasterProps = React.ComponentProps const Toaster = ({ ...props }: ToasterProps) => { const { theme = "system" } = useTheme() return ( ) } export { Toaster } ================================================ FILE: packages/ui/src/components/ui/switch.tsx ================================================ import * as SwitchPrimitives from '@radix-ui/react-switch'; import * as React from 'react'; import { cn } from '@libs/utils'; const Switch = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); Switch.displayName = SwitchPrimitives.Root.displayName; export { Switch }; ================================================ FILE: packages/ui/src/components/ui/table.tsx ================================================ import * as React from 'react'; import { cn } from '@libs/utils'; const Table = React.forwardRef>( ({ className, ...props }, ref) => (
), ); Table.displayName = 'Table'; const TableHeader = React.forwardRef>( ({ className, ...props }, ref) => , ); TableHeader.displayName = 'TableHeader'; const TableBody = React.forwardRef>( ({ className, ...props }, ref) => ( ), ); TableBody.displayName = 'TableBody'; const TableFooter = React.forwardRef>( ({ className, ...props }, ref) => ( tr]:last:border-b-0', className)} {...props} /> ), ); TableFooter.displayName = 'TableFooter'; const TableRow = React.forwardRef>( ({ className, ...props }, ref) => ( ), ); TableRow.displayName = 'TableRow'; const TableHead = React.forwardRef>( ({ className, ...props }, ref) => (
), ); TableHead.displayName = 'TableHead'; const TableCell = React.forwardRef>( ({ className, ...props }, ref) => ( ), ); TableCell.displayName = 'TableCell'; const TableCaption = React.forwardRef>( ({ className, ...props }, ref) => (
), ); TableCaption.displayName = 'TableCaption'; export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow }; ================================================ FILE: packages/ui/src/components/ui/toggle.tsx ================================================ "use client" import * as TogglePrimitive from "@radix-ui/react-toggle" import { cva, type VariantProps } from "class-variance-authority" import * as React from "react" import { cn } from "@libs/utils" const toggleVariants = cva( "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap", { variants: { variant: { default: "bg-transparent", outline: "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground", }, size: { default: "h-9 px-2 min-w-9", sm: "h-8 px-1.5 min-w-8", lg: "h-10 px-2.5 min-w-10", }, }, defaultVariants: { variant: "default", size: "default", }, } ) function Toggle({ className, variant, size, ...props }: React.ComponentProps & VariantProps) { return ( ) } export { Toggle, toggleVariants } ================================================ FILE: packages/ui/src/components/ui/tooltip.tsx ================================================ import * as TooltipPrimitive from '@radix-ui/react-tooltip'; import * as React from 'react'; import { cn } from '@libs/utils'; const TooltipProvider = TooltipPrimitive.Provider; const Tooltip = TooltipPrimitive.Root; const TooltipTrigger = TooltipPrimitive.Trigger; const TooltipContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, sideOffset = 4, ...props }, ref) => ( )); TooltipContent.displayName = TooltipPrimitive.Content.displayName; export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; ================================================ FILE: packages/ui/src/index.ts ================================================ export * from './components'; export * from './libs/index'; ================================================ FILE: packages/ui/src/libs/index.ts ================================================ export * from './utils'; ================================================ FILE: packages/ui/src/libs/utils.ts ================================================ import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } ================================================ FILE: packages/ui/tailwind.config.js ================================================ const baseConfig = require('@sync-your-cookie/tailwindcss-config'); /** @type {import('tailwindcss').Config} */ module.exports = { ...baseConfig, darkMode: ["class"], content: [ './pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}', `node_modules/@sync-your-cookie/ui/**/*.{js,ts,jsx,tsx,mdx}` ], prefix: "", theme: { container: { center: true, padding: "2rem", screens: { "2xl": "1400px", }, }, extend: { colors: { border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))", }, secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))", }, destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))", }, muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))", }, accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))", }, popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))", }, card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, keyframes: { "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" }, }, "accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", }, }, }, plugins: [require("tailwindcss-animate")], } ================================================ FILE: packages/ui/tsconfig.json ================================================ { "extends": "@sync-your-cookie/tsconfig/utils", "compilerOptions": { "outDir": "dist", "target": "ESNext", "module": "ESNext", "jsx": "react-jsx", "baseUrl": ".", "paths": { "src/*": ["./src/*"], "@components/*": ["./src/components/*"], "@/components/*": ["./src/components/*"], "@libs/*": ["./src/libs/*"] }, "types": ["chrome"], }, "include": ["src/**/*"] } ================================================ FILE: packages/ui/tsup.config.ts ================================================ import { defineConfig } from 'tsup'; export default defineConfig({ treeshake: true, splitting: true, entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, clean: true, external: ['chrome'], }); ================================================ FILE: packages/zipper/index.mts ================================================ import fs from 'node:fs'; import { resolve } from 'node:path'; import { zipBundle } from './lib/index.js'; const IS_FIREFOX = process.env.BROWSER === 'firefox'; const packageJson = JSON.parse(fs.readFileSync('../../package.json', 'utf8')); const YYYY_MM_DD = new Date().toISOString().slice(0, 10).replace(/-/g, ''); const HH_mm_ss = new Date().toISOString().slice(11, 19).replace(/:/g, ''); const fileName = `extension-${packageJson.version}-${YYYY_MM_DD}-${HH_mm_ss}`; await zipBundle({ distDirectory: resolve(import.meta.dirname, '..', '..', '..', 'dist'), buildDirectory: resolve(import.meta.dirname, '..', '..', '..', 'dist-zip'), archiveName: IS_FIREFOX ? `${fileName}.xpi` : `${fileName}.zip`, }).catch(error => { console.error('Error zipping the bundle:', error); process.exit(1); }); console.log('Zipping completed'); ================================================ FILE: packages/zipper/lib/index.ts ================================================ import fg from 'fast-glob'; import { AsyncZipDeflate, Zip } from 'fflate'; import { createReadStream, createWriteStream, existsSync, mkdirSync } from 'node:fs'; import { posix, resolve } from 'node:path'; // Converts bytes to megabytes function toMB(bytes: number): number { return bytes / 1024 / 1024; } // Creates the build directory if it doesn't exist function ensureBuildDirectoryExists(buildDirectory: string): void { if (!existsSync(buildDirectory)) { mkdirSync(buildDirectory, { recursive: true }); } } // Logs the package size and duration function logPackageSize(size: number, startTime: number): void { console.log(`Zip Package size: ${toMB(size).toFixed(2)} MB in ${Date.now() - startTime}ms`); } // Handles file streaming and zipping function streamFileToZip( absPath: string, relPath: string, zip: Zip, onAbort: () => void, onError: (error: Error) => void, ): void { const data = new AsyncZipDeflate(relPath, { level: 9 }); zip.add(data); createReadStream(absPath) .on('data', (chunk: string | Buffer) => typeof chunk === 'string' ? data.push(Buffer.from(chunk), false) : data.push(chunk, false), ) .on('end', () => data.push(new Uint8Array(0), true)) .on('error', error => { onAbort(); onError(error); }); } // Zips the bundle export const zipBundle = async ( { distDirectory, buildDirectory, archiveName, }: { distDirectory: string; buildDirectory: string; archiveName: string; }, withMaps = false, ): Promise => { ensureBuildDirectoryExists(buildDirectory); const zipFilePath = resolve(buildDirectory, archiveName); const output = createWriteStream(zipFilePath); const fileList = await fg( [ '**/*', // Pick all nested files ...(!withMaps ? ['!**/(*.js.map|*.css.map)'] : []), // Exclude source maps conditionally ], { cwd: distDirectory, onlyFiles: true, }, ); return new Promise((pResolve, pReject) => { let aborted = false; let totalSize = 0; const timer = Date.now(); const zip = new Zip((err, data, final) => { if (err) { pReject(err); } else { totalSize += data.length; output.write(data); if (final) { logPackageSize(totalSize, timer); output.end(); pResolve(); } } }); // Handle file read streams for (const file of fileList) { if (aborted) return; const absPath = resolve(distDirectory, file); const absPosixPath = posix.resolve(distDirectory, file); const relPosixPath = posix.relative(distDirectory, absPosixPath); console.log(`Adding file: ${relPosixPath}`); streamFileToZip( absPath, relPosixPath, zip, () => { aborted = true; zip.terminate(); }, error => pReject(`Error reading file ${absPath}: ${error.message}`), ); } zip.end(); }); }; ================================================ FILE: packages/zipper/package.json ================================================ { "name": "@sync-your-cookie/zipper", "version": "0.8.0", "description": "chrome extension - zipper", "type": "module", "private": true, "sideEffects": false, "files": [ "dist/**" ], "types": "index.mts", "main": "dist/index.mjs", "scripts": { "clean:bundle": "rimraf dist", "clean:node_modules": "pnpx rimraf node_modules", "clean:turbo": "rimraf .turbo", "clean": "pnpm clean:bundle && pnpm clean:node_modules && pnpm clean:turbo", "zip": "npm run ready && node dist/index.mjs", "lint": "eslint .", "ready": "tsc -b", "lint:fix": "pnpm lint --fix", "prettier": "prettier . --write --ignore-path ../../.prettierignore", "type-check": "tsc --noEmit" }, "devDependencies": { "@sync-your-cookie/tsconfig": "workspace:*", "fflate": "^0.8.2", "fast-glob": "^3.3.3" } } ================================================ FILE: packages/zipper/tsconfig.json ================================================ { "extends": "@sync-your-cookie/tsconfig/utils", "compilerOptions": { "baseUrl": ".", "outDir": "dist", "module": "esnext", "target": "esnext" }, "include": ["index.mts", "lib"] } ================================================ FILE: pages/content/package.json ================================================ { "name": "@sync-your-cookie/content-script", "version": "0.3.3", "description": "chrome extension - content script", "private": true, "sideEffects": true, "files": [ "dist/**" ], "scripts": { "clean:node_modules": "pnpx rimraf node_modules", "clean:turbo": "rimraf .turbo", "clean": "pnpm clean:turbo && pnpm clean:node_modules", "build": "vite build", "build:watch": "cross-env __DEV__=true vite build -w --mode development", "dev": "pnpm build:watch", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "pnpm lint --fix", "prettier": "prettier . --write --ignore-path ../../.prettierignore", "type-check": "tsc --noEmit" }, "dependencies": { "@sync-your-cookie/shared": "workspace:*", "@sync-your-cookie/storage": "workspace:*", "eventemitter3": "^5.0.1" }, "devDependencies": { "@sync-your-cookie/hmr": "workspace:*", "@sync-your-cookie/tsconfig": "workspace:*" } } ================================================ FILE: pages/content/src/index.ts ================================================ import { init } from './localStorage'; init(); ================================================ FILE: pages/content/src/listener.ts ================================================ import { Message, MessageType, SendResponse } from '@sync-your-cookie/shared'; import EventEmitter from 'eventemitter3'; declare global { interface Window { $messageListener: MessageListener; } } /** * 消息监听器类 * 基于发布订阅模式,集中管理消息处理 */ export class MessageListener { // 声明一个事件发射器 private emitter: EventEmitter; // 声明一个单例实例 private static instance: MessageListener; // 声明一个调试器开关 public debuggerOpen = true; private timer: number | null = null; constructor() { // 初始化事件发射器 this.emitter = new EventEmitter(); // 初始化 this.init(); } /** * 获取单例实例 * @returns {MessageListener} 单例实例 */ public static getInstance(): MessageListener { // 如果单例实例不存在,则创建一个新的实例 if (!MessageListener.instance) { MessageListener.instance = new MessageListener(); } // 返回单例实例 return MessageListener.instance; } // 处理消息 handleMessage = (message: Message, sender: chrome.runtime.MessageSender, sendResponse: (response?: SendResponse) => void) => { // 打印消息类型 if(message.toString() === 'ping') { this.listen(); return } // 如果消息类型为获取本地存储 if (message.type === MessageType.GetLocalStorage) { // 发射消息 this.emit(MessageType.GetLocalStorage, message.payload, sendResponse); // console.log('拦截到 XHR 响应:', message.data); // sendResponse({}); // 如果消息类型为设置本地存储 } else if (message.type === MessageType.SetLocalStorage) { this.emit(MessageType.SetLocalStorage, message.payload, sendResponse); } return true; // keep the channel open } private init(): void { // 此处不能使用 async 函数 this.listen(false); // this.ping(); // chrome.runtime.onConnect.addListener(port => { // console.log('connected ', port); // if (port.name === 'hi') { // port.onMessage.addListener((evt) => { // console.log("evt", evt) // }); // } // }); // window.removeEventListener("load", this.listen); // window.addEventListener("load", this.listen); console.log('chrome', chrome, chrome.tabs) // var port = chrome.runtime.connect(null as any, { name: 'hi' }); // console.log("port", port); // port.onDisconnect.addListener(obj => { // console.log('disconnected port'); // }) } public ping = () => { chrome.runtime.sendMessage('ping', response => { setTimeout(this.ping, 1000); console.log('pong', response); // if (chrome.runtime.lastError) { // } else { // // Do whatever you want, background script is ready now // } }); } public listen = (initSetTimeout = true) => { if(this.timer) { clearTimeout(this.timer); } const fn = this.handleMessage; chrome.runtime.onMessage.removeListener(fn); chrome.runtime.onMessage.addListener(fn); if(initSetTimeout) { this.timer = setTimeout(() => { console.log('listen timeout reset') this.listen(); }, 6000) } } /** * 监听消息 * @param {string} event 消息类型 * @param {(...args: any[]) => void} callback 回调函数 */ public on(event: string, callback: (...args: any[]) => void): void { this.emitter.on(event, callback); } /** * 取消监听消息 * @param {string} event 消息类型 * @param {(...args: any[]) => void} sendResponse 回调函数 */ async emit(event: string, data: any, sendResponse?: (...args: any[]) => void): Promise { console.log("data", data); return new Promise((resolve, reject) => { try { if (sendResponse) { this.emitter.emit(event, data, (message: any) => { if (sendResponse) { sendResponse(message); } resolve(); }); } else { this.emitter.emit(event, data); resolve(); } } catch (error) { reject(error); } }); } } export const messageListener = MessageListener.getInstance(); window.$messageListener = messageListener ================================================ FILE: pages/content/src/localStorage.ts ================================================ import { eventHandlerInstance } from "./observer"; export const init = () => { eventHandlerInstance.init(); console.log("LocalStorage initialized"); } ================================================ FILE: pages/content/src/observer.ts ================================================ import { DomainPayload, MessageType, SendResponse, SetLocalStorageMessagePayload } from '@sync-your-cookie/shared'; import { messageListener } from './listener'; class Observer { observers: any[] = []; constructor() { } // 订阅获取本地存储 subscribeGetLocalStorage(callback: (data: any, sendResponse: (data: SendResponse) => void) => void) { messageListener.on(MessageType.GetLocalStorage, callback); } subscribeSetLocalStorage(callback: (data: any, sendResponse: (data: SendResponse) => void) => void) { messageListener.on(MessageType.SetLocalStorage, callback); } } export const observer = new Observer(); class eventHandler { constructor() { // this.init(); } init() { this.handleGetLocalStorage(); this.handleSetLocalStorage(); } handleGetLocalStorage() { observer.subscribeGetLocalStorage(async (result: DomainPayload, sendResponse: (data: SendResponse) => void) => { console.log("result", result); if (location.origin.includes(result.domain)) { try { console.log("localStorage", { ...localStorage }); const items: {key: string, value: string}[] = []; const localObject = { ...localStorage }; for(const key in localObject) { const value:string = localObject[key].toString(); items.push({key, value}); } console.log("items", items); sendResponse({ isOk: true, msg: 'get localStorage success', result: items }); // const json = JSON.parse(result.response); } catch (error) { console.log('XHR_RESPONSE->error', error); sendResponse({ isOk: false, msg: 'get localStorage error', result: error }) } } else { console.log("localStorage not match domain", result.domain); } }); } handleSetLocalStorage() { observer.subscribeSetLocalStorage(async (result: SetLocalStorageMessagePayload, sendResponse: (data: SendResponse) => void) => { console.log("result", result); if (location.origin.includes(result.domain)) { try { const values = result.value; const setKey = result.onlyKey; if(setKey){ const targetItem = values.find((item: any) => item.key === setKey); if(targetItem){ localStorage.setItem(setKey, targetItem.value || ""); } else { console.log("no target item", setKey); } } else { for(const item of values){ localStorage.setItem(item.key || '', item.value || ""); } } sendResponse({ isOk: true, msg: 'set localStorage success', }); // const json = JSON.parse(result.response); } catch (error) { console.log('XHR_RESPONSE->error', error); sendResponse({ isOk: false, msg: 'set localStorage error', result: error }) } } else { console.log("localStorage not match domain", result.domain); } }); } } export const eventHandlerInstance = new eventHandler(); ================================================ FILE: pages/content/tsconfig.json ================================================ { "extends": "@sync-your-cookie/tsconfig/utils", "compilerOptions": { "outDir": "../dist/content", "jsx": "react-jsx", "checkJs": false, "allowJs": false, "baseUrl": "./src", "declarationMap": true, "paths": { "@src/*": ["src/*"] }, "types": ["chrome"] }, "include": ["index.ts", "src"], } ================================================ FILE: pages/content/vite.config.mts ================================================ import { watchRebuildPlugin } from '@sync-your-cookie/hmr'; import react from '@vitejs/plugin-react-swc'; import { resolve } from 'path'; import { defineConfig } from 'vite'; const rootDir = resolve(__dirname); const srcDir = resolve(rootDir, 'src'); const isDev = process.env.__DEV__ === 'true'; const isProduction = !isDev; export default defineConfig({ resolve: { alias: { '@src': srcDir, }, }, base: '', plugins: [react(), isDev && watchRebuildPlugin({ refresh: true })], publicDir: resolve(rootDir, 'public'), build: { lib: { entry: resolve(__dirname, 'src/index.ts'), formats: ['iife'], name: 'ContentScript', fileName: 'index', }, sourcemap: isDev, minify: isProduction, reportCompressedSize: isProduction, rollupOptions: { external: ['chrome'], }, outDir: resolve(rootDir, '..', '..', 'dist', 'content'), }, define: { 'process.env.NODE_ENV': isDev ? `"development"` : `"production"`, }, }); ================================================ FILE: pages/options/index.html ================================================ SyncYourCookie-Options
================================================ FILE: pages/options/package.json ================================================ { "name": "@sync-your-cookie/options", "version": "0.0.1", "description": "chrome extension options", "private": true, "sideEffects": true, "files": [ "dist/**" ], "scripts": { "clean": "rimraf ./dist && rimraf .turbo", "build": "pnpm run clean && tsc --noEmit && vite build", "build:watch": "cross-env __DEV__=true vite build -w --mode development", "dev": "pnpm build:watch", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "pnpm lint --fix", "prettier": "prettier . --write", "type-check": "tsc --noEmit" }, "dependencies": { "@sync-your-cookie/shared": "workspace:*", "@sync-your-cookie/storage": "workspace:*", "lucide-react": "^0.394.0", "sonner": "^1.5.0" }, "devDependencies": { "@sync-your-cookie/hmr": "workspace:*", "@sync-your-cookie/tailwindcss-config": "workspace:*", "@sync-your-cookie/tsconfig": "workspace:*", "@sync-your-cookie/ui": "workspace:*" } } ================================================ FILE: pages/options/postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ================================================ FILE: pages/options/src/Options.tsx ================================================ import { useStorageSuspense, useTheme, verifyCloudflareToken, withErrorBoundary, withSuspense, } from '@sync-your-cookie/shared'; import { accountStorage } from '@sync-your-cookie/storage/lib/accountStorage'; import { initStorageKey } from '@sync-your-cookie/storage/lib/settingsStorage'; import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Input, Label, SyncTooltip, ThemeDropdown, Toaster, } from '@sync-your-cookie/ui'; import { Eye, EyeOff, Info, Loader2, LogOut, SlidersVertical } from 'lucide-react'; import { useState } from 'react'; import { toast } from 'sonner'; import { SettingsPopover } from './components/SettingsPopover'; import { useGithub } from './hooks/useGithub'; const Options = () => { const accountInfo = useStorageSuspense(accountStorage); const [token, setToken] = useState(accountInfo.token); const [accountId, setAccountId] = useState(accountInfo.accountId); const [namespaceId, setNamespaceId] = useState(accountInfo.namespaceId); const [openEye, setOpenEye] = useState(false); const { loading, handleLaunchAuth } = useGithub(); const [loadingSave, setLoadingSave] = useState(false); const { setTheme } = useTheme(); const handleTokenInput: React.ChangeEventHandler = evt => { setToken(evt.target.value); }; const handleAccountInput: React.ChangeEventHandler = evt => { setAccountId(evt.target.value); }; const handleNamespaceInput: React.ChangeEventHandler = evt => { setNamespaceId(evt.target.value); }; const handleSave = async () => { if (!accountId?.trim() || !token?.trim()) { toast.warning('Account ID and Token are required'); return; } else if (!namespaceId?.trim()) { toast.warning('NamespaceId are required'); return; } try { setLoadingSave(true); const res = await verifyCloudflareToken(accountId.trim(), token.trim()); if (res.success === true) { const [message] = res.messages; if (message?.message) { toast.success('Save Success (' + message.message.replace('API', '') + ')'); } else { toast.success('Save Success'); } accountStorage.update({ selectedProvider: 'cloudflare', accountId: accountId, namespaceId: namespaceId, token: token, }); } else { const [error] = res.errors; if (error?.message) { toast.error('Verify Failed: ' + error.message); } else { toast.error('Verify Failed: Unknown Error'); } } } catch (err: any) { console.log('error', err); const [error] = err?.errors || []; if (error?.message) { toast.error('Verify Failed: ' + error.message); } else { toast.error('Verify Failed: Unknown Error'); } } finally { setLoadingSave(false); } }; const handleToggleEye = () => { setOpenEye(!openEye); }; const handleLogout = () => { accountStorage.update({ githubAccessToken: '', selectedProvider: 'cloudflare', name: '', avatarUrl: '', bio: '', email: '', }); toast.success('Log out Success'); initStorageKey(); }; const renderAccount = () => { if (accountInfo.selectedProvider === 'github' && accountInfo.githubAccessToken) { return (

githubAccessToken: {accountInfo.githubAccessToken}

Your accessToken is only stored on your local device.

}>

{accountInfo.name}

{accountInfo.email || accountInfo.bio}

); } return ( <> Enter your cloudflare account Or using Github Gist

How to get it? handleToggleEye()} onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { handleToggleEye(); } }} className="cursor-pointer"> {openEye ? : }

Don’t have a cloudflare Account yet? Sign up

{namespaceId?.trim() && accountId?.trim() ? ( Go to namespace ) : null} {/* {namespaceId ? null : (
Don’t have an ID yet? Create
)} */}
Or continue with
); }; return (
logo
Settings
} />
{renderAccount()}
Built by jackluson . The source code is available on{' '} GitHub logo .
); }; export default withErrorBoundary(withSuspense(Options,
Loading ...
),
Error Occur
); ================================================ FILE: pages/options/src/components/SettingsPopover.tsx ================================================ import { GithubApi, pullCookies, useStorageSuspense } from '@sync-your-cookie/shared'; import { accountStorage } from '@sync-your-cookie/storage/lib/accountStorage'; import { cookieStorage } from '@sync-your-cookie/storage/lib/cookieStorage'; import { domainStatusStorage } from '@sync-your-cookie/storage/lib/domainStatusStorage'; import { settingsStorage } from '@sync-your-cookie/storage/lib/settingsStorage'; import { Input, Label, Popover, PopoverContent, PopoverTrigger, Switch, SyncTooltip } from '@sync-your-cookie/ui'; import { Eye, EyeOff, Info, Lock, SquareArrowOutUpRight } from 'lucide-react'; import React, { useEffect, useState } from 'react'; import { StorageSelect } from './StorageSelect'; interface SettingsPopover { trigger: React.ReactNode; } export function SettingsPopover({ trigger }: SettingsPopover) { const settingsInfo = useStorageSuspense(settingsStorage); const [selectOpen, setSelectOpen] = useState(false); const [openEye, setOpenEye] = useState(false); const handleCheckChange = ( checked: boolean, checkedKey: 'protobufEncoding' | 'includeLocalStorage' | 'contextMenu' | 'encryptionEnabled', ) => { settingsStorage.update({ [checkedKey]: checked, }); }; const handlePasswordChange = (e: React.ChangeEvent) => { settingsStorage.update({ encryptionPassword: e.target.value, }); }; const handleValueChange = (value: string) => { settingsStorage.update({ storageKey: value, }); }; const reset = async () => { await domainStatusStorage.resetState(); await cookieStorage.reset(); await pullCookies(); console.log('reset finished'); }; useEffect(() => { reset(); }, [settingsInfo.storageKey]); const handleToggleEye = () => { setOpenEye(!openEye); }; const handleOpenChange = (open: boolean) => { if (selectOpen) return; // if (open === false && (settingsInfo.storageKey !== storageKey || !storageKey)) { // console.log('open', open); // settingsStorage.update({ // storageKey: storageKey || defaultKey, // }); // domainConfigStorage.resetState(); // } }; const handleSelectOpenChange = (open: boolean) => { console.log('select open', open); setSelectOpen(open); }; const handleAddStorageKey = async (key: string) => { const accountStorageInfo = await accountStorage.getSnapshot(); if (accountStorageInfo?.selectedProvider === 'github') { const gistId = settingsInfo.storageKeyGistId; await GithubApi.instance.addGistFile(gistId!, key); await GithubApi.instance.initStorageKeyList(); } else { await settingsStorage.addStorageKey(key); } }; const handleRemoveStorageKey = async (key: string) => { const accountStorageInfo = await accountStorage.getSnapshot(); if (accountStorageInfo?.selectedProvider === 'github') { const gistId = settingsInfo.storageKeyGistId; await GithubApi.instance.deleteGistFile(gistId!, key); await GithubApi.instance.initStorageKeyList(); } else { await settingsStorage.removeStorageKey(key); } }; return ( {trigger}

Save Settings

Set the save format.

{/* */}
handleCheckChange(checked, 'protobufEncoding')} checked={settingsInfo.protobufEncoding} id="encoding" />
handleCheckChange(checked, 'includeLocalStorage')} checked={settingsInfo.includeLocalStorage} id="include" />
handleCheckChange(checked, 'contextMenu')} checked={settingsInfo.contextMenu} id="contextMenu" />
handleCheckChange(checked, 'encryptionEnabled')} checked={settingsInfo.encryptionEnabled} disabled={!settingsInfo.protobufEncoding} id="encryption" />
{settingsInfo.encryptionEnabled && settingsInfo.protobufEncoding && (
handleToggleEye()} onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { handleToggleEye(); } }} className="cursor-pointer mr-4"> {openEye ? : }
)}
); } ================================================ FILE: pages/options/src/components/StorageSelect.tsx ================================================ import { Button, Input, Select, SelectContent, SelectItem, SelectPortal, SelectTrigger, SelectValue, } from '@sync-your-cookie/ui'; import { useRef, useState } from 'react'; import { CircleX, LoaderIcon, Plus } from 'lucide-react'; import { toast } from 'sonner'; export interface IOption { value: string; label: string; [key: string]: any; } interface StorageSelectProps extends React.ComponentProps { options: IOption[]; value: string; onAdd: (key: string) => void; onRemove: (key: string) => void; } export function StorageSelect(props: StorageSelectProps) { const { value, onRemove, options, onValueChange, ...rest } = props; const [loading, setLoading] = useState(false); const [inputValue, setInputValue] = useState(''); const containerRef = useRef(null); const handleAdd = async () => { if (loading) { return; } const newKey = inputValue.trim().replaceAll(/\s+/g, ''); const exist = options.find(option => option.value === newKey); if (exist) { console.warn('Key already exists or is empty'); toast.error('Key already exists'); return; } try { setLoading(true); await props.onAdd(newKey); setInputValue(''); } catch (error) { console.log('error', error); } finally { setLoading(false); } }; const handleRemoveKey = async (option: IOption) => { // Handle removing a storage key console.log('Remove storage key', option); if (loading) { return; } try { setLoading(true); await onRemove(option.value); } catch (error) { console.log('error', error); } finally { setLoading(false); } }; return (
{ setInputValue(event?.target.value.replaceAll(/\s+/g, '')); }} onKeyDown={e => { if (e.key === 'Enter' && inputValue.replaceAll(/\s+/g, '')) { e.preventDefault(); handleAdd(); } }} className="h-8 " />
); } ================================================ FILE: pages/options/src/hooks/useGithub.ts ================================================ import { clientId, GithubApi, initGithubApi, scope } from '@sync-your-cookie/shared'; import { accountStorage } from '@sync-your-cookie/storage/lib/accountStorage'; import { useState } from 'react'; import { toast } from 'sonner'; initGithubApi(); export const useGithub = () => { const [loading, setLoading] = useState(false); const handleLaunchAuth = async () => { const state = crypto.randomUUID(); const redirectUri = chrome.identity.getRedirectURL(); const authUrl = `https://github.com/login/oauth/authorize?` + `client_id=${clientId}` + `&redirect_uri=${encodeURIComponent(redirectUri)}` + `&scope=${encodeURIComponent(scope)}` + `&state=${state}`; setLoading(true); try { chrome.identity.launchWebAuthFlow({ url: authUrl, interactive: true }, async redirectUrl => { console.log('redirectUrl', redirectUrl); const code = redirectUrl ? new URL(redirectUrl).searchParams.get('code') : ''; if (code) { try { console.log('code', code); const accessToken = await GithubApi.instance.fetchAccessToken(code); console.log('accessToken', accessToken); setLoading(false); const user = await GithubApi.instance.fetchUser(); accountStorage.update({ githubAccessToken: accessToken, selectedProvider: 'github', name: user.name, avatarUrl: user.avatar_url, bio: user.bio, email: user.email, }); GithubApi.instance.reload(); console.log('user', user); toast.success('GitHub Authorization Success'); } catch (error) { toast.error('GitHub Authorization Failed'); setLoading(false); } } else { setLoading(false); toast.error('GitHub Authorization Failed'); } }); } catch (error) { console.error('Auth error', error); toast.error('GitHub Authorization Failed'); setLoading(false); } }; return { handleLaunchAuth, loading, }; }; ================================================ FILE: pages/options/src/index.css ================================================ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } ================================================ FILE: pages/options/src/index.tsx ================================================ import Options from '@src/Options'; import '@src/index.css'; import { ThemeProvider } from '@sync-your-cookie/shared'; import '@sync-your-cookie/ui/css'; import { createRoot } from 'react-dom/client'; function init() { const appContainer = document.querySelector('#app-container'); if (!appContainer) { throw new Error('Can not find #app-container'); } const root = createRoot(appContainer); root.render( , ); } init(); ================================================ FILE: pages/options/tailwind.config.js ================================================ const uiConfig = require('@sync-your-cookie/ui/tailwind.config'); /** @type {import('tailwindcss').Config} */ module.exports = { ...uiConfig, }; ================================================ FILE: pages/options/tsconfig.json ================================================ { "extends": "@sync-your-cookie/tsconfig/base", "compilerOptions": { "baseUrl": ".", "paths": { "@src/*": ["src/*"] }, "jsx": "react-jsx", "types": ["chrome", "node"] }, "include": ["src"] } ================================================ FILE: pages/options/vite.config.ts ================================================ import { watchRebuildPlugin } from '@sync-your-cookie/hmr'; import react from '@vitejs/plugin-react-swc'; import { resolve } from 'path'; import { defineConfig, loadEnv } from 'vite'; const rootDir = resolve(__dirname); const srcDir = resolve(rootDir, 'src'); const isDev = process.env.__DEV__ === 'true'; const isProduction = !isDev; const envInfo = loadEnv(isDev ? 'development' : 'production', process.cwd(), 'SYNC'); export default defineConfig({ resolve: { alias: { '@src': srcDir, }, }, base: '', plugins: [react(), isDev && watchRebuildPlugin({ refresh: true })], publicDir: resolve(rootDir, 'public'), build: { outDir: resolve(rootDir, '..', '..', 'dist', 'options'), sourcemap: isDev, minify: isProduction, reportCompressedSize: isProduction, rollupOptions: { external: ['chrome'], }, }, define: { 'process.env.NODE_ENV': isDev ? `"development"` : `"production"`, 'process.env.CLIENT_SECRET': JSON.stringify(envInfo.SYNC_CLIENT_SECRET || ''), }, }); ================================================ FILE: pages/popup/index.html ================================================ Popup
================================================ FILE: pages/popup/package.json ================================================ { "name": "@sync-your-cookie/popup", "version": "0.0.1", "description": "chrome extension popup", "private": true, "sideEffects": true, "files": [ "dist/**" ], "scripts": { "clean": "rimraf ./dist && rimraf .turbo", "build": "pnpm run clean && tsc --noEmit && vite build", "build:watch": "cross-env __DEV__=true vite build -w --mode development", "dev": "pnpm build:watch", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "pnpm lint --fix", "prettier": "prettier . --write", "type-check": "tsc --noEmit" }, "dependencies": { "@sync-your-cookie/shared": "workspace:*", "@sync-your-cookie/storage": "workspace:*", "clsx": "^2.1.1", "lucide-react": "^0.394.0", "pako": "^2.1.0", "sonner": "^1.5.0", "tailwind-merge": "^2.3.0" }, "devDependencies": { "@sync-your-cookie/hmr": "workspace:*", "@sync-your-cookie/protobuf": "workspace:*", "@sync-your-cookie/tailwindcss-config": "workspace:*", "@sync-your-cookie/tsconfig": "workspace:*", "@sync-your-cookie/ui": "workspace:*" } } ================================================ FILE: pages/popup/postcss.config.js ================================================ module.exports = { plugins: { // 'postcss-import': {}, tailwindcss: {}, autoprefixer: {}, }, }; ================================================ FILE: pages/popup/src/Popup.tsx ================================================ import { extractDomainAndPort, useTheme, withErrorBoundary, withSuspense } from '@sync-your-cookie/shared'; import { Button, Image, Spinner, Toaster } from '@sync-your-cookie/ui'; import { CloudDownload, CloudUpload, Copyright, PanelRightOpen, RotateCw, Settings } from 'lucide-react'; import { useEffect, useState } from 'react'; import { AutoSwitch } from './components/AutoSwtich'; import { useDomainConfig } from './hooks/useDomainConfig'; const Popup = () => { const { theme } = useTheme(); const [activeTabUrl, setActiveTabUrl] = useState(''); const [favIconUrl, setFavIconUrl] = useState(''); const { pushing, toggleAutoPushState, toggleAutoPullState, domain, setDomain, domainItemConfig, domainItemStatus, handlePush, handlePull, } = useDomainConfig(); useEffect(() => { chrome.tabs.query({ active: true, lastFocusedWindow: true }, async function (tabs) { if (tabs.length > 0) { const activeTab = tabs[0]; if (activeTab.url && activeTab.url.startsWith('http')) { setFavIconUrl(activeTab?.favIconUrl || ''); setActiveTabUrl(activeTab.url); const [domain, tempPort] = await extractDomainAndPort(activeTab.url); setDomain(domain + `${tempPort ? ':' + tempPort : ''}`); } } }); }, []); const isPushingOrPulling = domainItemStatus.pushing || domainItemStatus.pulling; const handleAndReload = () => { handlePull(activeTabUrl, domain, true); }; return (
logo

SyncYourCookie

{domain ? (

{domain}

) : null}
{/* */}
toggleAutoPushState(domain)} id="autoPush" value={!!domainItemConfig.autoPush} />
toggleAutoPullState(domain)} id="autoPull" value={!!domainItemConfig.autoPull} />
); }; export default withErrorBoundary(withSuspense(Popup,
Loading ...
),
Error Occur
); ================================================ FILE: pages/popup/src/components/AutoSwtich/index.tsx ================================================ import { Label, Switch } from '@sync-your-cookie/ui'; interface AutoSwitchProps { value: boolean; onChange: (value: boolean) => void; id: string; disabled?: boolean; } export function AutoSwitch(props: AutoSwitchProps) { const { value, onChange, id, disabled } = props; return (
); } ================================================ FILE: pages/popup/src/hooks/useDomainConfig.ts ================================================ import { useCookieAction } from '@sync-your-cookie/shared'; import { useState } from 'react'; import { toast } from 'sonner'; export const useDomainConfig = () => { const [domain, setDomain] = useState(''); const cookieAction = useCookieAction(domain, toast); return { domain, setDomain, ...cookieAction, }; }; ================================================ FILE: pages/popup/src/index.css ================================================ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: relative; } code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } ================================================ FILE: pages/popup/src/index.tsx ================================================ import Popup from '@src/Popup'; import '@src/index.css'; import { initGithubApi, ThemeProvider } from '@sync-your-cookie/shared'; import '@sync-your-cookie/ui/css'; import { createRoot } from 'react-dom/client'; function init() { const appContainer = document.querySelector('#app-container'); if (!appContainer) { throw new Error('Can not find #app-container'); } const root = createRoot(appContainer); root.render( , ); } initGithubApi(); init(); ================================================ FILE: pages/popup/src/utils/index.ts ================================================ import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } ================================================ FILE: pages/popup/tailwind.config.js ================================================ const uiConfig = require('@sync-your-cookie/ui/tailwind.config'); /** @type {import('tailwindcss').Config} */ module.exports = { ...uiConfig, }; ================================================ FILE: pages/popup/tsconfig.json ================================================ { "extends": "@sync-your-cookie/tsconfig/base", "compilerOptions": { "baseUrl": ".", "paths": { "@src/*": ["src/*"] }, "jsx": "react-jsx", "types": ["chrome"] }, "include": ["src"] } ================================================ FILE: pages/popup/vite.config.ts ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react-swc'; import { resolve } from 'path'; import { watchRebuildPlugin } from '@sync-your-cookie/hmr'; const rootDir = resolve(__dirname); const srcDir = resolve(rootDir, 'src'); const isDev = process.env.__DEV__ === 'true'; const isProduction = !isDev; export default defineConfig({ resolve: { alias: { '@src': srcDir, }, }, base: '', plugins: [react(), isDev && watchRebuildPlugin({ refresh: true })], publicDir: resolve(rootDir, 'public'), build: { outDir: resolve(rootDir, '..', '..', 'dist', 'popup'), sourcemap: isDev, minify: isProduction, reportCompressedSize: isProduction, rollupOptions: { external: ['chrome'], }, }, define: { 'process.env.NODE_ENV': isDev ? `"development"` : `"production"`, }, }); ================================================ FILE: pages/sidepanel/index.html ================================================ SidePanel
================================================ FILE: pages/sidepanel/package.json ================================================ { "name": "@sync-your-cookie/sidepanel", "version": "0.0.1", "description": "chrome extension sidepanel", "private": true, "sideEffects": true, "files": [ "dist/**" ], "scripts": { "clean": "rimraf ./dist && rimraf .turbo", "build": "pnpm run clean && tsc --noEmit && vite build", "build:watch": "cross-env __DEV__=true vite build -w --mode development", "dev": "pnpm build:watch", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "pnpm lint --fix", "prettier": "prettier . --write", "type-check": "tsc --noEmit" }, "dependencies": { "@sync-your-cookie/shared": "workspace:*", "@sync-your-cookie/storage": "workspace:*", "clsx": "^2.1.1", "lucide-react": "^0.394.0", "pako": "^2.1.0", "sonner": "^1.5.0", "tailwind-merge": "^2.3.0" }, "devDependencies": { "@sync-your-cookie/protobuf": "workspace:*", "@sync-your-cookie/tailwindcss-config": "workspace:*", "@sync-your-cookie/tsconfig": "workspace:*", "@sync-your-cookie/hmr": "workspace:*", "@sync-your-cookie/ui": "workspace:*" } } ================================================ FILE: pages/sidepanel/postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ================================================ FILE: pages/sidepanel/src/SidePanel.tsx ================================================ import { useTheme, withErrorBoundary, withSuspense } from '@sync-your-cookie/shared'; import { Toaster } from '@sync-your-cookie/ui'; import { useEffect } from 'react'; import CookieTable from './components/CookieTable'; const SidePanel = () => { useEffect(() => { chrome.runtime.onMessage.addListener(message => { // Might not be as easy if there are multiple side panels open if (message === 'closeSidePanel') { window.close(); } }); }, []); const { theme } = useTheme(); return (
); }; export default withErrorBoundary(withSuspense(SidePanel,
Loading ...
),
Error Occur
); ================================================ FILE: pages/sidepanel/src/components/CookieTable/SearchInput.tsx ================================================ import { Input } from '@sync-your-cookie/ui'; import { Search, X } from 'lucide-react'; import { FC, useState } from 'react'; export interface SearchInputProps { onEnter?: (val: string) => void; } export const SearchInput: FC = props => { const { onEnter } = props; const [searchVal, setSearchVal] = useState(''); return (
{ setSearchVal(evt.target.value); }} onBlur={() => { onEnter?.(searchVal.trim()); }} onKeyDown={evt => { if (evt.key === 'Enter' || evt.code === 'Enter') { onEnter?.(searchVal.trim()); } }} className="bg-gray-100 pl-[36px]" placeholder="Filter" /> {searchVal && ( { setSearchVal(''); onEnter?.(''); }} size={16} className="absolute top-[13px] right-[10px] cursor-pointer" /> )}
); }; ================================================ FILE: pages/sidepanel/src/components/CookieTable/hooks/useAction.ts ================================================ import { useCookieAction } from '@sync-your-cookie/shared'; import type { Cookie } from '@sync-your-cookie/storage/lib/cookieStorage'; import { useEffect, useState } from 'react'; import { toast } from 'sonner'; import { CookieItem } from './../index'; import { useSelected } from './useSelected'; export const useAction = (cookie: Cookie) => { const [loading, setLoading] = useState(false); const [currentSearchStr, setCurrentSearchStr] = useState(''); const { loading: loadingWithSelected, selectedDomain, showCookiesColumns, setSelectedDomain, cookieList, renderKeyValue, localStorageItems, ...rest } = useSelected(cookie, currentSearchStr); useEffect(() => { setCurrentSearchStr(''); }, [selectedDomain]); const cookieAction = useCookieAction(selectedDomain, toast); const handleDelete = async (cookie: CookieItem) => { try { setLoading(true); await cookieAction.handleRemove(cookie.host); } finally { setLoading(false); } }; const handlePull = async (activeTabUrl: string, cookie: CookieItem) => { try { setLoading(true); await cookieAction.handlePull(activeTabUrl, cookie.host, true); } finally { setLoading(false); } }; const handlePush = async (cookie: CookieItem, sourceUrl?: string) => { try { setLoading(true); await cookieAction.handlePush(cookie.host, sourceUrl); } finally { setLoading(false); } }; const handleViewCookies = async (domain: string) => { setSelectedDomain(domain); }; const handleBack = () => { setCurrentSearchStr(''); setSelectedDomain(''); }; const handleCopy = (domain: string, isJSON: boolean = false) => { const cookies = cookie.domainCookieMap?.[domain]?.cookies || []; if (cookies.length === 0) { toast.warning('no cookie to copy, check again.'); return; } if (!navigator.clipboard) { toast.warning('please check clipboard permission settings before copy '); return; } let copyText = ''; if (isJSON) { copyText = JSON.stringify(cookies, undefined, 2); } else { const pairs = []; for (const ck of cookies) { if (ck.value) { const pair = `${ck.name}=${ck.value}`; pairs.push(pair); } } copyText = pairs.join('; '); } navigator?.clipboard?.writeText(copyText).then( () => { toast.success('Copy success'); }, err => { console.log('err', err); toast.error('Copy failed'); }, ); }; const handleSearch = (val: string) => { setCurrentSearchStr(val); }; return { handleDelete, handlePull, handlePush, handleViewCookies, loading: loading || loadingWithSelected, selectedDomain, setSelectedDomain, handleBack, showCookiesColumns, cookieAction, handleCopy, currentSearchStr, // handlePush, handleSearch, renderKeyValue, cookieList: cookieList.filter(item => { if (currentSearchStr.trim()) { return ( item.domain.includes(currentSearchStr) || item.name.includes(currentSearchStr) || item.value.includes(currentSearchStr) ); } return true; }), localStorageItems: localStorageItems.filter(item => { if (currentSearchStr.trim()) { return item.key?.includes(currentSearchStr) || item.value?.includes(currentSearchStr); } return true; }), ...rest, }; }; ================================================ FILE: pages/sidepanel/src/components/CookieTable/hooks/useCookieItem.ts ================================================ import { catchHandler, editCookieItemUsingMessage, ICookie, removeCookieItemUsingMessage, } from '@sync-your-cookie/shared'; import { useState } from 'react'; import { toast } from 'sonner'; export const useCookieItem = (selectedDomain: string) => { const [loading, setLoading] = useState(false); const handleDeleteItem = async (id: string) => { try { setLoading(true); await removeCookieItemUsingMessage({ domain: selectedDomain, id, }) .then(async res => { if (res.isOk) { toast.success(res.msg || 'success'); } else { toast.error(res.msg || 'Deleted fail'); } }) .catch(err => { catchHandler(err, 'delete', toast); }); } finally { setLoading(false); } }; const handleEditItem = async (oldItem: ICookie, newItem: ICookie) => { try { setLoading(true); await editCookieItemUsingMessage({ domain: selectedDomain, oldItem, newItem, }) .then(async res => { if (res.isOk) { toast.success(res.msg || 'success'); } else { toast.error(res.msg || 'Edited fail'); return Promise.reject(res); } }) .catch(err => { catchHandler(err, 'edit', toast); return Promise.reject(err); }); } finally { setLoading(false); } }; return { loading, handleDeleteItem, handleEditItem, }; }; ================================================ FILE: pages/sidepanel/src/components/CookieTable/hooks/useSelected.tsx ================================================ import { useStorageSuspense } from '@sync-your-cookie/shared'; import { Cookie } from '@sync-your-cookie/storage/lib/cookieStorage'; import { domainConfigStorage } from '@sync-your-cookie/storage/lib/domainConfigStorage'; import { domainStatusStorage } from '@sync-your-cookie/storage/lib/domainStatusStorage'; import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Input, Label, Popover, PopoverContent, PopoverTrigger, cn, type ColumnDef, } from '@sync-your-cookie/ui'; import { Ellipsis, PencilLine, Trash2, Wrench } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { toast } from 'sonner'; import { useCookieItem } from './useCookieItem'; export type CookieShowItem = { id: string; domain: string; name: string; value: string; expirationDate?: number | null; hostOnly?: boolean | null; httpOnly?: boolean | null; path?: string | null; sameSite?: string | null; secure?: boolean | null; session?: boolean | null; storeId?: string | null; }; export type LocalStorageShowItem = { // id: string; key: string; value: string; }; export const useSelected = (cookieMap: Cookie, currentSearchStr: string) => { const [selectedDomain, setSelectedDomain] = useState(''); const domainStatus = useStorageSuspense(domainStatusStorage); const [selectedRow, setSelectedRow] = useState | null>(null); const inputRowRef = useRef>({}); const [selectedKey, setSelectedKey] = useState(''); const [hasLocalStorage, setHasLocalStorage] = useState(false); const [localStorageMode, setLocalStorageMode] = useState(false); const handleEdit = (key: string, row: Record) => { setSelectedKey(key); setSelectedRow(row); inputRowRef.current = { ...row, }; }; const { loading, handleEditItem, handleDeleteItem } = useCookieItem(selectedDomain); const handleCancel = () => { setSelectedRow(null); }; const handleSave = async (isSet: boolean = false) => { const prevSelectedRow = selectedRow; setSelectedRow(null); if (JSON.stringify(inputRowRef.current) !== JSON.stringify(prevSelectedRow)) { await handleEditItem(prevSelectedRow!, inputRowRef.current!); } if (isSet && inputRowRef.current) { await new Promise(resolve => setTimeout(resolve, 500)); handleSet(inputRowRef.current as CookieShowItem); } }; const renderKeyValue = (value: string, key?: string) => { let nameSearchFlag = false; let startIndex = 0; let endIndex = 0; if (currentSearchStr.trim() && value.includes(currentSearchStr.trim())) { startIndex = value.indexOf(currentSearchStr); endIndex = startIndex + currentSearchStr.length; nameSearchFlag = true; } return (
{key && ( {key}: )} {nameSearchFlag ? ( {value.slice(0, startIndex)} {currentSearchStr} {value.slice(endIndex)} ) : (

{value}

)}
); }; const renderPopver = (key: keyof CookieShowItem, row: Record, nameKey = 'name') => { const keyName = nameKey; const sameId = selectedRow?.id === row.id; if (localStorageMode) { return null; } return ( { console.log('val', val); if (val === false) { handleCancel(); } // if (val) { // handleEdit(key, row); // } else { // handleCancel(); // } }}>

Edit

{ (inputRowRef.current as Record)![keyName] = evt.target.value || ''; }} className="col-span-4 h-8" />
{ if (inputRowRef.current && key) { // eslint-disable-next-line @typescript-eslint/no-explicit-any (inputRowRef.current as any)[key] = evt.target.value || ''; } }} className="col-span-4 h-8" />
); }; const renderEditCell = (key: keyof CookieShowItem, row: Record, nameKey = 'name') => { const isEdit = false && key === selectedKey && selectedRow?.id === row.id; const keyName = nameKey; const nameValue = row[keyName]; const value = row.value; const sameId = localStorageMode && 0 ? true : selectedRow?.id === row.id; return (
{isEdit ? (
{ console.log('e.target.value', e.target.value); setSelectedRow({ ...selectedRow, [key]: e.target.value, }); }} />
) : (
{renderKeyValue(nameValue, keyName)} {renderKeyValue(value, 'value')}
{renderPopver(key, row, 'name')}
)}
); }; const handleSet = async (item: Record) => { const domainCnf = await domainConfigStorage.get(); const itemDomainCnf = domainCnf.domainMap[selectedDomain]; const sourceUrl = itemDomainCnf?.sourceUrl; const protocol = sourceUrl ? new URL(sourceUrl).protocol : 'http:'; const itemHost = item.domain.startsWith('.') ? item.domain.slice(1) : item.domain; const href = `${protocol}//${itemHost || selectedDomain}`; const setVal = { domain: item.domain, name: item.name ?? undefined, url: href, storeId: item.storeId ?? undefined, value: item.value ?? undefined, expirationDate: item.expirationDate ?? undefined, path: item.path ?? undefined, httpOnly: item.httpOnly ?? undefined, secure: item.secure ?? undefined, sameSite: (item.sameSite ?? undefined) as chrome.cookies.SameSiteStatus, }; try { await chrome.cookies.set(setVal); toast.success('set success'); } catch (error) { console.log('set cookie error', setVal, error); toast.error('set failed'); } }; const handleDelete = async (item: Record) => { handleDeleteItem(`${item.domain}_${item.name}`); }; const showCookiesColumns: ColumnDef[] = [ { id: 'Domain', accessorKey: 'domain', header: 'Domain', cell: ({ row }) => { const domainValue = row.original.domain; if (currentSearchStr.trim() && domainValue.includes(currentSearchStr)) { const startIndex = domainValue.indexOf(currentSearchStr); const endIndex = startIndex + currentSearchStr.length; return (

{domainValue.slice(0, startIndex)} {currentSearchStr} {domainValue.slice(endIndex)}

); } else { return (

{domainValue}

); } }, }, // { // id: 'Name', // accessorKey: 'name', // header: 'Name', // // cell: ({ row }) => { // // return renderEditCell('name', row.original); // // }, // }, { id: 'Value', accessorKey: 'value', header: 'Key / Value', cell: ({ row }) => { return renderEditCell('value', row.original); }, }, { id: 'actions', enableHiding: false, cell: ({ row }) => { const itemStatus = domainStatus.domainMap[selectedDomain] || {}; const disabled = domainStatus.pushing || itemStatus.pulling || itemStatus.pushing; return ( handleSet(row.original)}> Apply handleDelete(row.original)}> Delete {/* handleDelete(row.original)}> Delete And Set */} ); }, }, ]; const showLocalStorageColumns: ColumnDef[] = [ { id: 'Value', accessorKey: 'value', header: 'Key / Value', cell: ({ row, cell }) => { const id = cell.id; return renderEditCell('value', { id, ...row.original }, 'key'); }, }, ]; const cookieList = cookieMap.domainCookieMap?.[selectedDomain]?.cookies?.map((item, index) => { return { ...item, id: item.name + '_' + index, domain: item.domain ?? '', name: item.name ?? '', value: item.value ?? '', // name: cookie.name ?? undefined, // url: activeTabUrl, // storeId: cookie.storeId ?? undefined, // value: cookie.value ?? undefined, // expirationDate: cookie.expirationDate ?? undefined, // path: cookie.path ?? undefined, // httpOnly: cookie.httpOnly ?? undefined, // secure: cookie.secure ?? undefined, // sameSite: (cookie.sameSite ?? undefined) as chrome.cookies.SameSiteStatus, }; }) || []; const localStorageItems = cookieMap.domainCookieMap?.[selectedDomain]?.localStorageItems || []; useEffect(() => { if (localStorageItems && localStorageItems.length > 0) { setHasLocalStorage(true); } else { setHasLocalStorage(false); setLocalStorageMode(false); } }, [localStorageItems]); return { loading, selectedDomain, showCookiesColumns, localStorageItems, showLocalStorageColumns, setSelectedDomain, cookieList, renderKeyValue, localStorageMode, hasLocalStorage, setLocalStorageMode, }; }; ================================================ FILE: pages/sidepanel/src/components/CookieTable/index.tsx ================================================ /* eslint-disable react/no-unescaped-entities */ /* eslint-disable jsx-a11y/click-events-have-key-events */ import { getTabsByHost, useStorageSuspense } from '@sync-your-cookie/shared'; import { Button, DataTable, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Image, Spinner, Switch, SyncTooltip, Toggle, } from '@sync-your-cookie/ui'; import { ArrowUpRight, ChevronLeft, ClipboardList, CloudDownload, CloudUpload, Copy, Database, Ellipsis, Info, RotateCw, Table as TableIcon, Trash, } from 'lucide-react'; import { cookieStorage } from '@sync-your-cookie/storage/lib/cookieStorage'; import { domainConfigStorage } from '@sync-your-cookie/storage/lib/domainConfigStorage'; import { domainStatusStorage } from '@sync-your-cookie/storage/lib/domainStatusStorage'; import { settingsStorage } from '@sync-your-cookie/storage/lib/settingsStorage'; import type { ColumnDef } from '@sync-your-cookie/ui'; import { useAction } from './hooks/useAction'; import { SearchInput } from './SearchInput'; export type CookieItem = { id: string; host: string; sourceUrl?: string; favIconUrl?: string; autoPush: boolean; autoPull: boolean; }; const CookieTable = () => { const domainConfig = useStorageSuspense(domainConfigStorage); const domainStatus = useStorageSuspense(domainStatusStorage); const cookieMap = useStorageSuspense(cookieStorage); const { showCookiesColumns, cookieList, handleBack, setSelectedDomain, selectedDomain, loading, cookieAction, handleDelete, handlePush, handlePull, handleCopy, handleViewCookies, handleSearch, currentSearchStr, hasLocalStorage, localStorageMode, setLocalStorageMode, showLocalStorageColumns, localStorageItems, } = useAction(cookieMap); let domainList = []; let totalCookieItem = 0; let totalLocalStorageItem = 0; for (const [key, value] of Object.entries(cookieMap?.domainCookieMap || {})) { const config = domainConfig.domainMap[key]; if (!selectedDomain && currentSearchStr.trim() && !key.includes(currentSearchStr.trim())) continue; if (value.cookies?.length) { totalCookieItem += value.cookies.length; } if (value.localStorageItems?.length) { totalLocalStorageItem += value.localStorageItems.length; } domainList.push({ id: key, host: key, sourceUrl: config?.sourceUrl, favIconUrl: config?.favIconUrl, list: value.cookies, autoPush: config?.autoPush ?? false, autoPull: config?.autoPull ?? false, createTime: value.createTime, }); } domainList = domainList.sort((a, b) => { return b.createTime - a.createTime; }); const handleAndCheckPushCookie = async (row: CookieItem) => { const protocol = row.sourceUrl ? new URL(row.sourceUrl).protocol : 'https:'; const href = `${protocol}//${row.host}`; const includeLocalStorage = settingsStorage.getSnapshot()?.includeLocalStorage; if (includeLocalStorage) { const matchedTabs = await getTabsByHost(row.host); if (matchedTabs.length === 0) { window.open(href, '_blank'); setTimeout(async () => { handlePush(row, href); }, 500); } else { handlePush(row, href); } } else { handlePush(row, href); } // console.log('handleAndCheckPushCookie->row', row); }; const columns: ColumnDef[] = [ { accessorKey: 'host', header: 'Host', cell: ({ row, getValue }) => { const value = getValue() || ''; const sourceUrl = row.original.sourceUrl; const protocol = sourceUrl ? new URL(sourceUrl).protocol : 'https:'; const href = `${protocol}//${row.original.host}`; const src = row.original.favIconUrl ?? `https://${row.original.host}/favicon.ico`; return ( ); }, id: 'host', }, { accessorKey: 'autoPush', header: 'AutoPush', id: 'autoPush', cell: record => { return (

{ await domainConfigStorage.updateItem(record.row.original.host, { autoPush: !record.row.original.autoPush, }); }} />

); }, }, { accessorKey: 'autoPull', header: 'AutoPull', id: 'autoPull', cell: record => { return (

{ await domainConfigStorage.updateItem(record.row.original.host, { autoPull: !record.row.original.autoPull, }); }} />

); }, }, { id: 'actions', enableHiding: false, cell: ({ row }) => { const itemStatus = cookieAction.getDomainItemStatus(row.original.host) || {}; const sourceUrl = row.original.sourceUrl; const protocol = sourceUrl ? new URL(sourceUrl).protocol : 'http:'; const href = `${protocol}//${row.original.host}`; const disabled = itemStatus.pushing || cookieAction.pushing; return ( Cookie Actions {/* navigator.clipboard.writeText(.id)}> Copy payment ID */} { handleAndCheckPushCookie(row.original); }}> {itemStatus.pushing ? ( ) : ( )} Push

If 'Include LocalStorage' is enabled and no 'host' tab is present,

a 'host' tab will automatically open.

}>

{ handlePull(href, row.original); }}> {itemStatus.pulling ? ( ) : ( )} Pull { handleViewCookies(row.original.host); }}> View { handleCopy(row.original.host); }}> Copy { handleCopy(row.original.host, true); }}> Copy With JSON handleDelete(row.original)}> {itemStatus.pulling ? ( ) : ( )} Delete
); }, }, ]; const selectedRow = domainConfig.domainMap[selectedDomain]; const sourceUrl = selectedRow?.sourceUrl; const protocol = sourceUrl ? new URL(sourceUrl).protocol : 'https:'; const href = `${protocol}//${selectedDomain}`; const handlePressChange = (pressed: boolean) => { setLocalStorageMode(pressed); }; const renderTable = () => { return (
{hasLocalStorage ? ( ) : null}
{localStorageMode ? ( ) : ( )}
); }; return (

Welcome back!

Here's a list of your pushed {localStorageMode ? 'localStorage items' : 'cookies'}{' '}

{selectedDomain ? ( <>{renderTable()} ) : (

Total Cookie and LocalStorage

{domainList.length} sites

{totalCookieItem} cookie items & {totalLocalStorageItem} localStorage items

)}
); }; export default CookieTable; ================================================ FILE: pages/sidepanel/src/index.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; ================================================ FILE: pages/sidepanel/src/index.html ================================================ Options
================================================ FILE: pages/sidepanel/src/index.tsx ================================================ import '@src/index.css'; import SidePanel from '@src/SidePanel'; import { initGithubApi, ThemeProvider } from '@sync-your-cookie/shared'; import '@sync-your-cookie/ui/css'; import { createRoot } from 'react-dom/client'; function init() { const appContainer = document.querySelector('#app-container'); if (!appContainer) { throw new Error('Can not find #app-container'); } const root = createRoot(appContainer); root.render( , ); } initGithubApi(); init(); ================================================ FILE: pages/sidepanel/tailwind.config.js ================================================ const uiConfig = require('@sync-your-cookie/ui/tailwind.config'); /** @type {import('tailwindcss').Config} */ module.exports = { ...uiConfig, }; ================================================ FILE: pages/sidepanel/tsconfig.json ================================================ { "extends": "@sync-your-cookie/tsconfig/base", "compilerOptions": { "baseUrl": ".", "paths": { "@src/*": ["src/*"] }, "jsx": "react-jsx", "types": ["chrome"] }, "include": ["src"] } ================================================ FILE: pages/sidepanel/vite.config.ts ================================================ import { watchRebuildPlugin } from '@sync-your-cookie/hmr'; import react from '@vitejs/plugin-react-swc'; import { resolve } from 'path'; import { defineConfig } from 'vite'; const rootDir = resolve(__dirname); const srcDir = resolve(rootDir, 'src'); const isDev = process.env.__DEV__ === 'true'; console.log("isDev", isDev); const isProduction = !isDev; export default defineConfig({ resolve: { alias: { '@src': srcDir, }, }, base: '', plugins: [react(), isDev && watchRebuildPlugin({ refresh: true })], publicDir: resolve(rootDir, 'public'), build: { outDir: resolve(rootDir, '..', '..', 'dist', 'sidepanel'), sourcemap: isDev, minify: isProduction, reportCompressedSize: isProduction, rollupOptions: { external: ['chrome'], }, }, define: { 'process.env.NODE_ENV': isDev ? `"development"` : `"production"`, }, }); ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - "chrome-extension" - "pages/*" - "packages/*" ================================================ FILE: private-policy.md ================================================ Privacy Policy `SYNC COOKIE` does not collect any personal information. `SYNC COOKIE` doesn't embed any kind of analytics in the code `SYNC COOKIE` does not track its users in any way possible `SYNC COOKIE` stores your cookie data in your browser and, after you actively configure your cloudflare account settings, transmits the cookie-encoded data to your cloudflare account according to your settings. link: [Privacy Policy](https://www.freeprivacypolicy.com/live/0744ab35-18ca-4e12-af35-524666eba493) ================================================ FILE: turbo.json ================================================ { "$schema": "https://turbo.build/schema.json", "ui": "tui", "tasks": { "dev": { "dependsOn": ["^build"], "outputs": ["dist/**", "build/**"], "persistent": true }, "build": { "dependsOn": ["^build"], "outputs": ["../../dist/**", "dist/**", "build/**"], "cache": false }, "type-check": { "cache": false }, "lint": { "cache": false }, "lint:fix": { "cache": false }, "prettier": { "cache": false }, "test": { "dependsOn": [ "^test", "^build" ], "cache": false }, "clean": { "cache": false } } }