Skip to content

Commit 81f35c0

Browse files
committed
feat: send cookies
1 parent 7ca5a3c commit 81f35c0

35 files changed

Lines changed: 863 additions & 91 deletions

File tree

CLAUDE.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
Cloudhood is a cross-browser extension (Chrome & Firefox) for managing custom HTTP request headers. Users create profiles containing headers and URL filters, which are applied to web requests via the Declarative Net Request API.
8+
9+
## Commands
10+
11+
```bash
12+
# Development
13+
pnpm dev:chrome # Chrome dev with hot reload
14+
pnpm dev:firefox # Firefox dev with file watch
15+
16+
# Build
17+
pnpm build # Build both browsers
18+
pnpm build:chromium # Chrome only
19+
pnpm build:firefox # Firefox only
20+
21+
# Testing
22+
pnpm test:unit # Unit tests (Vitest)
23+
pnpm test:e2e # E2E tests interactive (Playwright)
24+
pnpm test:e2e:ci # E2E tests in CI mode
25+
pnpm test:e2e:screenshots # Visual regression tests
26+
pnpm test:e2e:screenshots:docker # Visual regression in Docker (used in CI)
27+
28+
# Linting
29+
pnpm lint # ESLint
30+
pnpm lint:css # Stylelint
31+
```
32+
33+
Run a single unit test file: `pnpm test:unit src/shared/utils/__tests__/formatHeaders.test.ts`
34+
35+
Run a single E2E test: `pnpm test:e2e tests/e2e/some-test.spec.ts`
36+
37+
## Architecture
38+
39+
### Feature-Sliced Design (FSD)
40+
41+
The codebase follows FSD with strict layer imports — each layer can only import from layers below it:
42+
43+
```
44+
app → pages → widgets → features → entities → shared
45+
```
46+
47+
### Path Aliases
48+
49+
TypeScript path aliases map to FSD layers: `#app`, `#pages/*`, `#widgets/*`, `#features/*`, `#entities/*`, `#shared/*`. Defined in `tsconfig.json`.
50+
51+
### State Management — Effector
52+
53+
All state is managed with **Effector** (stores, events, effects, `sample`). Each entity/feature has a `model/` directory containing Effector units. Data persists to `browser.storage` via effects.
54+
55+
### Key Domain Model
56+
57+
A **Profile** (`src/entities/request-profile/types.ts`) contains:
58+
- `requestHeaders: RequestHeader[]` — name/value pairs applied to matching requests
59+
- `urlFilters: UrlFilter[]` — URL patterns determining which requests get headers
60+
61+
Headers are applied via Declarative Net Request in `src/shared/utils/setBrowserHeaders.ts`.
62+
63+
### Browser Compatibility
64+
65+
- `webextension-polyfill` wraps Chrome/Firefox API differences
66+
- `src/shared/utils/browserAPI.ts` provides further abstraction (action API v2/v3 fallback)
67+
- Separate manifests: `manifest.chromium.json`, `manifest.firefox.json`
68+
- Build output goes to `build/chrome/` and `build/firefox/`
69+
70+
### Build System
71+
72+
Vite with two build targets:
73+
1. **Popup** — React SPA (entry: `src/index.tsx`)
74+
2. **Background** — Service worker (entry: `src/background.ts`, config: `vite.background.config.ts`)
75+
76+
The `BROWSER` env var (`chrome` | `firefox`) controls which manifest and build output are used.
77+
78+
### UI Components
79+
80+
Uses `@snack-uikit/*` component library with `@emotion/styled` for CSS-in-JS styling.
81+
82+
## Code Style
83+
84+
- Prettier: 2-space indent, 120 print width, single quotes, trailing commas
85+
- ESLint config from `@cloud-ru/eslint-config` with `eslint-plugin-effector`
86+
- Commit messages validated by `@cloud-ru/ft-config-commit-message` (conventional commits)
87+
- Pre-commit hooks via Husky + lint-staged
88+
89+
## Tech Stack
90+
91+
- React 18, TypeScript (strict), Vite, Effector
92+
- pnpm (>=10), Node.js (>=20)
93+
- Playwright (E2E), Vitest (unit), jsdom

manifest.chromium.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"permissions": [
2424
"storage",
2525
"declarativeNetRequest",
26-
"declarativeNetRequestFeedback"
26+
"declarativeNetRequestFeedback",
27+
"cookies"
2728
],
2829
"background": {
2930
"service_worker": "background.bundle.js"

manifest.dev.json

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,8 @@
1515
"48": "img/main-icon-48.jpg",
1616
"128": "img/main-icon-128.jpg"
1717
},
18-
"host_permissions": [
19-
"<all_urls>"
20-
],
21-
"permissions": [
22-
"storage",
23-
"declarativeNetRequest",
24-
"declarativeNetRequestFeedback"
25-
],
18+
"host_permissions": ["<all_urls>"],
19+
"permissions": ["storage", "declarativeNetRequest", "declarativeNetRequestFeedback", "cookies"],
2620
"background": {
2721
"service_worker": "background.bundle.js"
2822
},

manifest.firefox.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"storage",
2121
"declarativeNetRequest",
2222
"declarativeNetRequestFeedback",
23-
"activeTab"
23+
"activeTab",
24+
"cookies"
2425
],
2526
"host_permissions": [
2627
"<all_urls>"

src/background.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import browser from 'webextension-polyfill';
22

3-
import type { Profile, RequestHeader } from '#entities/request-profile/types';
3+
import type { Profile, RequestCookie, RequestHeader, UrlFilter } from '#entities/request-profile/types';
44

55
import { BrowserStorageKey, ServiceWorkerEvent } from './shared/constants';
66
import { browserAction } from './shared/utils/browserAPI';
77
import { logger, LogLevel } from './shared/utils/logger';
8+
import { setBrowserCookies } from './shared/utils/setBrowserCookies';
89
import { setBrowserHeaders } from './shared/utils/setBrowserHeaders';
910
import { setIconBadge } from './shared/utils/setIconBadge';
1011
import { enableExtensionReload } from './utils/extension-reload';
@@ -47,8 +48,11 @@ logger.info('🔍 About to check storage contents...');
4748
// Count active headers for the badge
4849
const selectedProfile = profiles.find((p: Profile) => p.id === result[BrowserStorageKey.SelectedProfile]);
4950
if (selectedProfile) {
50-
activeHeadersCount = selectedProfile.requestHeaders?.filter((h: RequestHeader) => !h.disabled).length || 0;
51-
logger.info(` - Active headers count: ${activeHeadersCount}`);
51+
const activeHeaders = selectedProfile.requestHeaders?.filter((h: RequestHeader) => !h.disabled).length || 0;
52+
const activeCookies = selectedProfile.requestCookies?.filter((c: RequestCookie) => !c.disabled).length || 0;
53+
const activeUrlFilters = selectedProfile.urlFilters?.filter((f: UrlFilter) => !f.disabled).length || 0;
54+
activeHeadersCount = activeHeaders + activeCookies + activeUrlFilters;
55+
logger.info(` - Active rules count: ${activeHeadersCount}`);
5256
}
5357
}
5458
} catch (error) {
@@ -89,7 +93,7 @@ async function notify(message: ServiceWorkerEvent) {
8993
]);
9094

9195
logger.info('📦 Storage data for reload:', result);
92-
await setBrowserHeaders(result);
96+
await Promise.all([setBrowserHeaders(result), setBrowserCookies(result)]);
9397
}
9498
return undefined;
9599
}
@@ -128,7 +132,7 @@ browser.runtime.onStartup.addListener(async function () {
128132
if (Object.keys(result).length) {
129133
logger.info('🚀 Storage data found, setting browser headers on startup');
130134
try {
131-
await setBrowserHeaders(result);
135+
await Promise.all([setBrowserHeaders(result), setBrowserCookies(result)]);
132136
} catch (error) {
133137
logger.error('Failed to set browser headers on startup:', error);
134138
}
@@ -156,7 +160,7 @@ browser.storage.onChanged.addListener(async (changes, areaName) => {
156160
]);
157161
logger.debug('Storage changes data:', result);
158162
try {
159-
await setBrowserHeaders(result);
163+
await Promise.all([setBrowserHeaders(result), setBrowserCookies(result)]);
160164
} catch (error) {
161165
logger.error('Failed to set browser headers on storage change:', error);
162166
}
@@ -199,7 +203,7 @@ browser.runtime.onInstalled.addListener(async details => {
199203
if (Object.keys(result).length) {
200204
logger.info('🔧 Storage data found, initializing browser headers on install/update');
201205
try {
202-
await setBrowserHeaders(result);
206+
await Promise.all([setBrowserHeaders(result), setBrowserCookies(result)]);
203207
} catch (error) {
204208
logger.error('Failed to set browser headers on install/update:', error);
205209
}

src/entities/profile-actions/model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createEvent, createStore } from 'effector';
22

33
import { selectedRequestProfileIdChanged } from '#entities/request-profile/model/selected-request-profile';
44

5-
export type ProfileActionsTab = 'headers' | 'url-filters';
5+
export type ProfileActionsTab = 'headers' | 'cookies' | 'url-filters';
66

77
export const profileActionsTabChanged = createEvent<ProfileActionsTab>();
88

src/entities/request-profile/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ export const DEFAULT_REQUEST_HEADERS: Profile[] = [
1313
value: '',
1414
},
1515
],
16+
requestCookies: [
17+
{
18+
id: generateId(),
19+
disabled: false,
20+
name: '',
21+
value: '',
22+
},
23+
],
1624
urlFilters: [
1725
{
1826
id: generateId(),
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './request-profiles';
22
export * from './selected-profile-url-filters';
3+
export * from './selected-request-cookies';
34
export * from './selected-request-headers';
45
export * from './selected-request-profile';

src/entities/request-profile/model/request-profiles.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const profileAddedFx = attach({
6767
{
6868
id: addedHeaderId,
6969
requestHeaders: [{ id: generateId(), name: '', value: '', disabled: false }],
70+
requestCookies: [{ id: generateId(), name: '', value: '', disabled: false }],
7071
urlFilters: [{ id: generateId(), value: '', disabled: false }],
7172
},
7273
],
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { combine } from 'effector';
2+
3+
import { validateCookie } from '#shared/utils/cookies';
4+
5+
import { $requestProfiles } from './request-profiles';
6+
import { $selectedRequestProfile } from './selected-request-profile';
7+
8+
export const $selectedProfileRequestCookies = combine(
9+
$selectedRequestProfile,
10+
$requestProfiles,
11+
(selectedProfileId, profiles) => profiles.find(p => p.id === selectedProfileId)?.requestCookies ?? [],
12+
{ skipVoid: false },
13+
);
14+
15+
export const $selectedProfileActiveRequestCookiesCount = combine(
16+
$selectedProfileRequestCookies,
17+
cookies => cookies.filter(c => !c.disabled && validateCookie(c.name, c.value)).length,
18+
{ skipVoid: false },
19+
);

0 commit comments

Comments
 (0)