feat: add domain rules management feature

- Introduced a new DomainRulesPage component for managing custom and global equivalent domains.
- Updated AppMainRoutes to include a route for domain rules.
- Added API functions to fetch and save domain rules.
- Enhanced localization with new strings for domain rules in multiple languages.
- Updated styles for the new domain rules interface and ensured responsiveness.
- Added types for domain rules in the TypeScript definitions.
This commit is contained in:
shuaiplus
2026-05-06 00:33:09 +08:00
parent 246c73a3d3
commit 0a001bebcc
32 changed files with 2045 additions and 32 deletions
+80
View File
@@ -0,0 +1,80 @@
import type { Env } from '../types';
import { StorageService } from '../services/storage';
import {
buildDomainsResponse,
customRulesToActiveEquivalentDomains,
normalizeCustomEquivalentDomains,
normalizeEquivalentDomains,
normalizeExcludedGlobalTypes,
} from '../services/domain-rules';
import { errorResponse, jsonResponse } from '../utils/response';
function firstPresent(payload: Record<string, unknown>, keys: string[]): unknown {
for (const key of keys) {
if (Object.prototype.hasOwnProperty.call(payload, key)) return payload[key];
}
return undefined;
}
async function readPayload(request: Request): Promise<Record<string, unknown>> {
try {
const parsed = await request.json();
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? parsed as Record<string, unknown>
: {};
} catch {
return {};
}
}
export async function handleGetDomains(env: Env, userId: string): Promise<Response> {
const storage = new StorageService(env.DB);
const settings = await storage.getUserDomainSettings(userId);
return jsonResponse(buildDomainsResponse(
settings.equivalentDomains,
settings.customEquivalentDomains,
settings.excludedGlobalEquivalentDomains
));
}
export async function handleUpdateDomains(request: Request, env: Env, userId: string): Promise<Response> {
const storage = new StorageService(env.DB);
const payload = await readPayload(request);
const current = await storage.getUserDomainSettings(userId);
const equivalentDomainsRaw = firstPresent(payload, [
'equivalentDomains',
'EquivalentDomains',
]);
const customEquivalentDomainsRaw = firstPresent(payload, [
'customEquivalentDomains',
'CustomEquivalentDomains',
]);
const excludedGlobalEquivalentDomainsRaw = firstPresent(payload, [
'excludedGlobalEquivalentDomains',
'ExcludedGlobalEquivalentDomains',
// Some older compatible clients send the excluded type list under this key.
'globalEquivalentDomains',
'GlobalEquivalentDomains',
]);
const customEquivalentDomains = customEquivalentDomainsRaw === undefined
? (equivalentDomainsRaw === undefined
? current.customEquivalentDomains
: normalizeCustomEquivalentDomains(normalizeEquivalentDomains(equivalentDomainsRaw)))
: normalizeCustomEquivalentDomains(customEquivalentDomainsRaw);
const equivalentDomains = customRulesToActiveEquivalentDomains(customEquivalentDomains);
const excludedGlobalEquivalentDomains = excludedGlobalEquivalentDomainsRaw === undefined
? current.excludedGlobalEquivalentDomains
: normalizeExcludedGlobalTypes(excludedGlobalEquivalentDomainsRaw);
await storage.saveUserDomainSettings(userId, equivalentDomains, customEquivalentDomains, excludedGlobalEquivalentDomains);
const settings = await storage.getUserDomainSettings(userId);
if (!settings) {
return errorResponse('Domain settings unavailable', 500);
}
return jsonResponse(buildDomainsResponse(
settings.equivalentDomains,
settings.customEquivalentDomains,
settings.excludedGlobalEquivalentDomains
));
}
+9 -6
View File
@@ -9,6 +9,7 @@ import {
buildUserDecryptionCompat,
buildUserDecryptionOptions,
} from '../utils/user-decryption';
import { buildDomainsResponse } from '../services/domain-rules';
function buildSyncCacheRequest(request: Request, userId: string, revisionDate: string, excludeDomains: boolean, excludeSends: boolean): Request {
const url = new URL(request.url);
@@ -50,11 +51,12 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
return cachedResponse;
}
const [ciphers, folders, sends, attachmentsByCipher] = await Promise.all([
const [ciphers, folders, sends, attachmentsByCipher, domainSettings] = await Promise.all([
storage.getAllCiphers(userId),
storage.getAllFolders(userId),
excludeSends ? Promise.resolve([]) : storage.getAllSends(userId),
storage.getAttachmentsByUserId(userId),
excludeDomains ? Promise.resolve(null) : storage.getUserDomainSettings(userId),
]);
const accountKeys = buildAccountKeys(user);
const userDecryptionOptions = buildUserDecryptionOptions(user);
@@ -111,11 +113,12 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
ciphers: cipherResponses,
domains: excludeDomains
? null
: {
equivalentDomains: [],
globalEquivalentDomains: [],
object: 'domains',
},
: buildDomainsResponse(
domainSettings?.equivalentDomains || [],
domainSettings?.customEquivalentDomains || [],
domainSettings?.excludedGlobalEquivalentDomains || [],
{ omitExcludedGlobals: true }
),
policies: [],
sends: sendResponses,
UserDecryption: {
+4 -8
View File
@@ -65,6 +65,7 @@ import {
} from './handlers/attachments';
import { handleAuthenticatedDeviceRoute } from './router-devices';
import { handleAdminRoute } from './router-admin';
import { handleGetDomains, handleUpdateDomains } from './handlers/domains';
export async function handleAuthenticatedRoute(
request: Request,
@@ -297,14 +298,9 @@ export async function handleAuthenticatedRoute(
return null;
}
if (path === '/api/settings/domains') {
if (method === 'GET' || method === 'PUT' || method === 'POST') {
return jsonResponse({
equivalentDomains: [],
globalEquivalentDomains: [],
object: 'domains',
});
}
if (path === '/api/settings/domains' || path === '/settings/domains') {
if (method === 'GET') return handleGetDomains(env, userId);
if (method === 'PUT' || method === 'POST') return handleUpdateDomains(request, env, userId);
return null;
}
+214
View File
@@ -0,0 +1,214 @@
import bitwardenGlobalDomainsRaw from '../static/global_domains.bitwarden.json';
import customGlobalDomainsRaw from '../static/global_domains.custom.json';
import type { CustomEquivalentDomain, DomainRulesResponse, GlobalEquivalentDomain } from '../types';
import { normalizeEquivalentDomain } from '../../shared/domain-normalize';
type RawGlobalDomain = Partial<GlobalEquivalentDomain> & {
Type?: unknown;
Domains?: unknown;
Excluded?: unknown;
};
function normalizeDomain(value: unknown): string {
return normalizeEquivalentDomain(value);
}
function normalizeGlobalDomain(entry: RawGlobalDomain): GlobalEquivalentDomain | null {
const type = Number(entry.type ?? entry.Type);
if (!Number.isInteger(type)) return null;
const rawDomains = entry.domains ?? entry.Domains;
if (!Array.isArray(rawDomains)) return null;
const domains = Array.from(new Set(rawDomains.map(normalizeDomain).filter(Boolean)));
if (domains.length < 2) return null;
return {
type,
domains,
excluded: Boolean(entry.excluded ?? entry.Excluded ?? false),
};
}
function normalizeGlobalDomains(input: unknown): GlobalEquivalentDomain[] {
if (!Array.isArray(input)) return [];
const seen = new Set<number>();
const out: GlobalEquivalentDomain[] = [];
for (const entry of input) {
const normalized = normalizeGlobalDomain(entry as RawGlobalDomain);
if (!normalized || seen.has(normalized.type)) continue;
seen.add(normalized.type);
out.push(normalized);
}
return out;
}
const bitwardenGlobalDomains = normalizeGlobalDomains(bitwardenGlobalDomainsRaw);
const customGlobalDomains = normalizeGlobalDomains(customGlobalDomainsRaw);
export const globalDomains: readonly GlobalEquivalentDomain[] = [
...bitwardenGlobalDomains,
...customGlobalDomains,
];
export function normalizeEquivalentDomains(input: unknown): string[][] {
if (!Array.isArray(input)) return [];
const groups: string[][] = [];
const seenGroups = new Set<string>();
for (const group of input) {
if (!Array.isArray(group)) continue;
const domains = Array.from(new Set(group.map(normalizeDomain).filter(Boolean)));
if (domains.length < 2) continue;
const key = domains.slice().sort().join('\n');
if (seenGroups.has(key)) continue;
seenGroups.add(key);
groups.push(domains);
}
return groups;
}
export function mergeEquivalentDomainGroups(input: string[][]): string[][] {
const parent = new Map<string, string>();
function find(domain: string): string {
const current = parent.get(domain);
if (!current) {
parent.set(domain, domain);
return domain;
}
if (current === domain) return domain;
const root = find(current);
parent.set(domain, root);
return root;
}
function union(a: string, b: string): void {
const rootA = find(a);
const rootB = find(b);
if (rootA !== rootB) parent.set(rootB, rootA);
}
for (const group of normalizeEquivalentDomains(input)) {
if (group.length < 2) continue;
const [first, ...rest] = group;
find(first);
for (const domain of rest) union(first, domain);
}
const components = new Map<string, string[]>();
for (const domain of parent.keys()) {
const root = find(domain);
const group = components.get(root) || [];
group.push(domain);
components.set(root, group);
}
return Array.from(components.values())
.map((group) => group.sort())
.filter((group) => group.length >= 2)
.sort((a, b) => a[0].localeCompare(b[0]));
}
export function expandCustomEquivalentDomainsWithGlobals(
customGroups: string[][],
activeGlobalGroups: string[][]
): string[][] {
const normalizedCustomGroups = normalizeEquivalentDomains(customGroups);
if (!normalizedCustomGroups.length) return [];
const customDomains = new Set(normalizedCustomGroups.flat());
return mergeEquivalentDomainGroups([
...activeGlobalGroups,
...normalizedCustomGroups,
]).filter((group) => group.some((domain) => customDomains.has(domain)));
}
function createCustomDomainId(domains: string[], index: number): string {
return `custom:${domains.slice().sort().join('|')}:${index}`;
}
export function normalizeCustomEquivalentDomains(input: unknown): CustomEquivalentDomain[] {
if (!Array.isArray(input)) return [];
const rules: CustomEquivalentDomain[] = [];
const seenGroups = new Set<string>();
for (const [index, item] of input.entries()) {
const record = Array.isArray(item)
? { domains: item, excluded: false, id: '' }
: item && typeof item === 'object'
? item as Record<string, unknown>
: null;
if (!record) continue;
const domains = normalizeEquivalentDomains([record.domains ?? record.Domains])[0];
if (!domains) continue;
const key = domains.slice().sort().join('\n');
if (seenGroups.has(key)) continue;
seenGroups.add(key);
const rawId = String(record.id ?? record.Id ?? '').trim();
rules.push({
id: rawId || createCustomDomainId(domains, index),
domains,
excluded: Boolean(record.excluded ?? record.Excluded ?? false),
});
}
return rules;
}
export function customRulesToActiveEquivalentDomains(rules: CustomEquivalentDomain[]): string[][] {
return mergeEquivalentDomainGroups(rules
.filter((rule) => !rule.excluded)
.map((rule) => rule.domains));
}
export function normalizeExcludedGlobalTypes(input: unknown): number[] {
if (!Array.isArray(input)) return [];
const validTypes = new Set(globalDomains.map((entry) => entry.type));
const seen = new Set<number>();
const out: number[] = [];
for (const item of input) {
const type = Number(typeof item === 'object' && item !== null ? (item as Record<string, unknown>).type : item);
const excluded = typeof item === 'object' && item !== null
? Boolean((item as Record<string, unknown>).excluded)
: true;
if (!excluded || !Number.isInteger(type) || !validTypes.has(type) || seen.has(type)) continue;
seen.add(type);
out.push(type);
}
return out;
}
export function buildDomainsResponse(
equivalentDomains: string[][],
customEquivalentDomains: CustomEquivalentDomain[],
excludedGlobalEquivalentDomains: number[],
options: { omitExcludedGlobals?: boolean } = {}
): DomainRulesResponse {
const excluded = new Set(excludedGlobalEquivalentDomains);
const activeGlobalDomainGroups = globalDomains
.filter((entry) => !excluded.has(entry.type))
.map((entry) => entry.domains);
const mergedEquivalentDomains = expandCustomEquivalentDomainsWithGlobals(
equivalentDomains,
activeGlobalDomainGroups
);
const globals = globalDomains
.map((entry) => ({
type: entry.type,
domains: entry.domains,
excluded: excluded.has(entry.type),
}))
.filter((entry) => !options.omitExcludedGlobals || !entry.excluded);
return {
equivalentDomains: mergedEquivalentDomains,
customEquivalentDomains,
globalEquivalentDomains: globals,
object: 'domains',
};
}
+67
View File
@@ -0,0 +1,67 @@
import type { UserDomainSettings } from '../types';
import { normalizeCustomEquivalentDomains, normalizeEquivalentDomains } from './domain-rules';
function parseJsonArray<T>(raw: string | null | undefined, fallback: T[]): T[] {
if (!raw) return fallback;
try {
const parsed = JSON.parse(raw) as unknown;
return Array.isArray(parsed) ? parsed as T[] : fallback;
} catch {
return fallback;
}
}
export async function getUserDomainSettings(db: D1Database, userId: string): Promise<UserDomainSettings> {
const row = await db
.prepare('SELECT equivalent_domains, custom_equivalent_domains, excluded_global_equivalent_domains, updated_at FROM domain_settings WHERE user_id = ?')
.bind(userId)
.first<{
equivalent_domains: string | null;
custom_equivalent_domains: string | null;
excluded_global_equivalent_domains: string | null;
updated_at: string | null;
}>();
const equivalentDomains = normalizeEquivalentDomains(parseJsonArray<string[]>(row?.equivalent_domains, []));
const storedCustomEquivalentDomains = row?.custom_equivalent_domains
? normalizeCustomEquivalentDomains(parseJsonArray<unknown>(row.custom_equivalent_domains, []))
: [];
const customEquivalentDomains = storedCustomEquivalentDomains.length
? storedCustomEquivalentDomains
: normalizeCustomEquivalentDomains(equivalentDomains);
return {
userId,
equivalentDomains,
customEquivalentDomains,
excludedGlobalEquivalentDomains: parseJsonArray<number>(row?.excluded_global_equivalent_domains, []),
updatedAt: row?.updated_at || null,
};
}
export async function saveUserDomainSettings(
db: D1Database,
userId: string,
equivalentDomains: string[][],
customEquivalentDomains: UserDomainSettings['customEquivalentDomains'],
excludedGlobalEquivalentDomains: number[],
updatedAt: string
): Promise<void> {
await db
.prepare(
'INSERT INTO domain_settings(user_id, equivalent_domains, custom_equivalent_domains, excluded_global_equivalent_domains, updated_at) ' +
'VALUES(?, ?, ?, ?, ?) ' +
'ON CONFLICT(user_id) DO UPDATE SET ' +
'equivalent_domains = excluded.equivalent_domains, ' +
'custom_equivalent_domains = excluded.custom_equivalent_domains, ' +
'excluded_global_equivalent_domains = excluded.excluded_global_equivalent_domains, ' +
'updated_at = excluded.updated_at'
)
.bind(
userId,
JSON.stringify(equivalentDomains),
JSON.stringify(customEquivalentDomains),
JSON.stringify(excludedGlobalEquivalentDomains),
updatedAt
)
.run();
}
+5
View File
@@ -15,6 +15,11 @@ const SCHEMA_STATEMENTS: readonly string[] = [
'ALTER TABLE users ADD COLUMN totp_recovery_code TEXT',
'ALTER TABLE users ADD COLUMN api_key TEXT',
'CREATE TABLE IF NOT EXISTS domain_settings (' +
'user_id TEXT PRIMARY KEY, equivalent_domains TEXT NOT NULL DEFAULT \'[]\', custom_equivalent_domains TEXT NOT NULL DEFAULT \'[]\', excluded_global_equivalent_domains TEXT NOT NULL DEFAULT \'[]\', updated_at TEXT NOT NULL, ' +
'FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE)',
'ALTER TABLE domain_settings ADD COLUMN custom_equivalent_domains TEXT NOT NULL DEFAULT \'[]\'',
'CREATE TABLE IF NOT EXISTS user_revisions (' +
'user_id TEXT PRIMARY KEY, revision_date TEXT NOT NULL, ' +
'FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE)',
+29 -2
View File
@@ -1,4 +1,4 @@
import { User, Cipher, Folder, Attachment, Device, Invite, AuditLog, Send, TrustedDeviceTokenSummary, RefreshTokenRecord } from '../types';
import { User, Cipher, Folder, Attachment, Device, Invite, AuditLog, Send, TrustedDeviceTokenSummary, RefreshTokenRecord, CustomEquivalentDomain } from '../types';
import { LIMITS } from '../config/limits';
import { ensureStorageSchema } from './storage-schema';
import {
@@ -105,10 +105,14 @@ import {
getRevisionDate as getStoredRevisionDate,
updateRevisionDate as updateStoredRevisionDate,
} from './storage-revision-repo';
import {
getUserDomainSettings as getStoredUserDomainSettings,
saveUserDomainSettings as saveStoredUserDomainSettings,
} from './storage-domain-rules-repo';
const TWO_FACTOR_REMEMBER_TTL_MS = 30 * 24 * 60 * 60 * 1000;
const STORAGE_SCHEMA_VERSION_KEY = 'schema.version';
const STORAGE_SCHEMA_VERSION = '2026-04-28';
const STORAGE_SCHEMA_VERSION = '2026-05-05-domain-rules-v2';
// D1-backed storage.
// Contract:
@@ -270,6 +274,29 @@ export class StorageService {
await createStoredAuditLog(this.db, log);
}
// --- Domain rules ---
async getUserDomainSettings(userId: string) {
return getStoredUserDomainSettings(this.db, userId);
}
async saveUserDomainSettings(
userId: string,
equivalentDomains: string[][],
customEquivalentDomains: CustomEquivalentDomain[],
excludedGlobalEquivalentDomains: number[]
): Promise<void> {
await saveStoredUserDomainSettings(
this.db,
userId,
equivalentDomains,
customEquivalentDomains,
excludedGlobalEquivalentDomains,
new Date().toISOString()
);
await this.updateRevisionDate(userId);
}
// --- Ciphers ---
async getCipher(id: string): Promise<Cipher | null> {
+93
View File
@@ -0,0 +1,93 @@
[
{ "type": 2, "domains": ["ameritrade.com", "tdameritrade.com"], "excluded": false },
{ "type": 3, "domains": ["bankofamerica.com", "bofa.com", "mbna.com", "usecfo.com"], "excluded": false },
{ "type": 4, "domains": ["sprint.com", "sprintpcs.com", "nextel.com"], "excluded": false },
{ "type": 0, "domains": ["youtube.com", "google.com", "gmail.com"], "excluded": false },
{ "type": 1, "domains": ["apple.com", "icloud.com"], "excluded": false },
{ "type": 5, "domains": ["wellsfargo.com", "wf.com", "wellsfargoadvisors.com"], "excluded": false },
{ "type": 6, "domains": ["mymerrill.com", "ml.com", "merrilledge.com"], "excluded": false },
{ "type": 7, "domains": ["accountonline.com", "citi.com", "citibank.com", "citicards.com", "citibankonline.com"], "excluded": false },
{ "type": 8, "domains": ["cnet.com", "cnettv.com", "com.com", "download.com", "news.com", "search.com", "upload.com"], "excluded": false },
{ "type": 9, "domains": ["bananarepublic.com", "gap.com", "oldnavy.com", "piperlime.com"], "excluded": false },
{ "type": 10, "domains": ["bing.com", "hotmail.com", "live.com", "microsoft.com", "msn.com", "passport.net", "windows.com", "microsoftonline.com", "office.com", "office365.com", "microsoftstore.com", "xbox.com", "azure.com", "windowsazure.com", "cloud.microsoft"], "excluded": false },
{ "type": 11, "domains": ["ua2go.com", "ual.com", "united.com", "unitedwifi.com"], "excluded": false },
{ "type": 12, "domains": ["overture.com", "yahoo.com"], "excluded": false },
{ "type": 13, "domains": ["zonealarm.com", "zonelabs.com"], "excluded": false },
{ "type": 14, "domains": ["paypal.com", "paypal-search.com"], "excluded": false },
{ "type": 15, "domains": ["avon.com", "youravon.com"], "excluded": false },
{ "type": 16, "domains": ["diapers.com", "soap.com", "wag.com", "yoyo.com", "beautybar.com", "casa.com", "afterschool.com", "vine.com", "bookworm.com", "look.com", "vinemarket.com"], "excluded": false },
{ "type": 17, "domains": ["1800contacts.com", "800contacts.com"], "excluded": false },
{ "type": 18, "domains": ["amazon.com", "amazon.com.be", "amazon.ae", "amazon.ca", "amazon.co.uk", "amazon.com.au", "amazon.com.br", "amazon.com.mx", "amazon.com.tr", "amazon.de", "amazon.es", "amazon.fr", "amazon.in", "amazon.it", "amazon.nl", "amazon.pl", "amazon.sa", "amazon.se", "amazon.sg"], "excluded": false },
{ "type": 19, "domains": ["cox.com", "cox.net", "coxbusiness.com"], "excluded": false },
{ "type": 20, "domains": ["mynortonaccount.com", "norton.com"], "excluded": false },
{ "type": 21, "domains": ["verizon.com", "verizon.net"], "excluded": false },
{ "type": 22, "domains": ["rakuten.com", "buy.com"], "excluded": false },
{ "type": 23, "domains": ["siriusxm.com", "sirius.com"], "excluded": false },
{ "type": 24, "domains": ["ea.com", "origin.com", "play4free.com", "tiberiumalliance.com"], "excluded": false },
{ "type": 25, "domains": ["37signals.com", "basecamp.com", "basecamphq.com", "highrisehq.com"], "excluded": false },
{ "type": 26, "domains": ["steampowered.com", "steamcommunity.com", "steamgames.com"], "excluded": false },
{ "type": 27, "domains": ["chart.io", "chartio.com"], "excluded": false },
{ "type": 28, "domains": ["gotomeeting.com", "citrixonline.com"], "excluded": false },
{ "type": 29, "domains": ["gogoair.com", "gogoinflight.com"], "excluded": false },
{ "type": 30, "domains": ["mysql.com", "oracle.com"], "excluded": false },
{ "type": 31, "domains": ["discover.com", "discovercard.com"], "excluded": false },
{ "type": 32, "domains": ["dcu.org", "dcu-online.org"], "excluded": false },
{ "type": 33, "domains": ["healthcare.gov", "cuidadodesalud.gov", "cms.gov"], "excluded": false },
{ "type": 34, "domains": ["pepco.com", "pepcoholdings.com"], "excluded": false },
{ "type": 35, "domains": ["century21.com", "21online.com"], "excluded": false },
{ "type": 36, "domains": ["comcast.com", "comcast.net", "xfinity.com"], "excluded": false },
{ "type": 37, "domains": ["cricketwireless.com", "aiowireless.com"], "excluded": false },
{ "type": 38, "domains": ["mandtbank.com", "mtb.com"], "excluded": false },
{ "type": 39, "domains": ["dropbox.com", "getdropbox.com"], "excluded": false },
{ "type": 40, "domains": ["snapfish.com", "snapfish.ca"], "excluded": false },
{ "type": 41, "domains": ["alibaba.com", "aliexpress.com", "aliyun.com", "net.cn"], "excluded": false },
{ "type": 42, "domains": ["playstation.com", "sonyentertainmentnetwork.com"], "excluded": false },
{ "type": 43, "domains": ["mercadolivre.com", "mercadolivre.com.br", "mercadolibre.com", "mercadolibre.com.ar", "mercadolibre.com.mx"], "excluded": false },
{ "type": 44, "domains": ["zendesk.com", "zopim.com"], "excluded": false },
{ "type": 45, "domains": ["autodesk.com", "tinkercad.com"], "excluded": false },
{ "type": 46, "domains": ["railnation.ru", "railnation.de", "rail-nation.com", "railnation.gr", "railnation.us", "trucknation.de", "traviangames.com"], "excluded": false },
{ "type": 47, "domains": ["wpcu.coop", "wpcuonline.com"], "excluded": false },
{ "type": 48, "domains": ["mathletics.com", "mathletics.com.au", "mathletics.co.uk"], "excluded": false },
{ "type": 49, "domains": ["discountbank.co.il", "telebank.co.il"], "excluded": false },
{ "type": 50, "domains": ["mi.com", "xiaomi.com"], "excluded": false },
{ "type": 52, "domains": ["postepay.it", "poste.it"], "excluded": false },
{ "type": 51, "domains": ["facebook.com", "messenger.com"], "excluded": false },
{ "type": 53, "domains": ["skysports.com", "skybet.com", "skyvegas.com"], "excluded": false },
{ "type": 54, "domains": ["disneymoviesanywhere.com", "go.com", "disney.com", "dadt.com", "disneyplus.com"], "excluded": false },
{ "type": 55, "domains": ["pokemon-gl.com", "pokemon.com"], "excluded": false },
{ "type": 56, "domains": ["myuv.com", "uvvu.com"], "excluded": false },
{ "type": 58, "domains": ["mdsol.com", "imedidata.com"], "excluded": false },
{ "type": 57, "domains": ["bank-yahav.co.il", "bankhapoalim.co.il"], "excluded": false },
{ "type": 59, "domains": ["sears.com", "shld.net"], "excluded": false },
{ "type": 60, "domains": ["xiami.com", "alipay.com"], "excluded": false },
{ "type": 61, "domains": ["belkin.com", "seedonk.com"], "excluded": false },
{ "type": 62, "domains": ["turbotax.com", "intuit.com"], "excluded": false },
{ "type": 63, "domains": ["shopify.com", "myshopify.com"], "excluded": false },
{ "type": 64, "domains": ["ebay.com", "ebay.at", "ebay.be", "ebay.ca", "ebay.ch", "ebay.cn", "ebay.co.jp", "ebay.co.th", "ebay.co.uk", "ebay.com.au", "ebay.com.hk", "ebay.com.my", "ebay.com.sg", "ebay.com.tw", "ebay.de", "ebay.es", "ebay.fr", "ebay.ie", "ebay.in", "ebay.it", "ebay.nl", "ebay.ph", "ebay.pl"], "excluded": false },
{ "type": 65, "domains": ["techdata.com", "techdata.ch"], "excluded": false },
{ "type": 66, "domains": ["schwab.com", "schwabplan.com"], "excluded": false },
{ "type": 68, "domains": ["tesla.com", "teslamotors.com"], "excluded": false },
{ "type": 69, "domains": ["morganstanley.com", "morganstanleyclientserv.com", "stockplanconnect.com", "ms.com"], "excluded": false },
{ "type": 70, "domains": ["taxact.com", "taxactonline.com"], "excluded": false },
{ "type": 71, "domains": ["mediawiki.org", "wikibooks.org", "wikidata.org", "wikimedia.org", "wikinews.org", "wikipedia.org", "wikiquote.org", "wikisource.org", "wikiversity.org", "wikivoyage.org", "wiktionary.org"], "excluded": false },
{ "type": 72, "domains": ["airbnb.at", "airbnb.be", "airbnb.ca", "airbnb.ch", "airbnb.cl", "airbnb.co.cr", "airbnb.co.id", "airbnb.co.in", "airbnb.co.kr", "airbnb.co.nz", "airbnb.co.uk", "airbnb.co.ve", "airbnb.com", "airbnb.com.ar", "airbnb.com.au", "airbnb.com.bo", "airbnb.com.br", "airbnb.com.bz", "airbnb.com.co", "airbnb.com.ec", "airbnb.com.gt", "airbnb.com.hk", "airbnb.com.hn", "airbnb.com.mt", "airbnb.com.my", "airbnb.com.ni", "airbnb.com.pa", "airbnb.com.pe", "airbnb.com.py", "airbnb.com.sg", "airbnb.com.sv", "airbnb.com.tr", "airbnb.com.tw", "airbnb.cz", "airbnb.de", "airbnb.dk", "airbnb.es", "airbnb.fi", "airbnb.fr", "airbnb.gr", "airbnb.gy", "airbnb.hu", "airbnb.ie", "airbnb.is", "airbnb.it", "airbnb.jp", "airbnb.mx", "airbnb.nl", "airbnb.no", "airbnb.pl", "airbnb.pt", "airbnb.ru", "airbnb.se"], "excluded": false },
{ "type": 73, "domains": ["eventbrite.at", "eventbrite.be", "eventbrite.ca", "eventbrite.ch", "eventbrite.cl", "eventbrite.co", "eventbrite.co.nz", "eventbrite.co.uk", "eventbrite.com", "eventbrite.com.ar", "eventbrite.com.au", "eventbrite.com.br", "eventbrite.com.mx", "eventbrite.com.pe", "eventbrite.de", "eventbrite.dk", "eventbrite.es", "eventbrite.fi", "eventbrite.fr", "eventbrite.hk", "eventbrite.ie", "eventbrite.it", "eventbrite.nl", "eventbrite.pt", "eventbrite.se", "eventbrite.sg"], "excluded": false },
{ "type": 74, "domains": ["stackexchange.com", "superuser.com", "stackoverflow.com", "serverfault.com", "mathoverflow.net", "askubuntu.com", "stackapps.com"], "excluded": false },
{ "type": 75, "domains": ["docusign.com", "docusign.net"], "excluded": false },
{ "type": 76, "domains": ["envato.com", "themeforest.net", "codecanyon.net", "videohive.net", "audiojungle.net", "graphicriver.net", "photodune.net", "3docean.net"], "excluded": false },
{ "type": 77, "domains": ["x10hosting.com", "x10premium.com"], "excluded": false },
{ "type": 78, "domains": ["dnsomatic.com", "opendns.com", "umbrella.com"], "excluded": false },
{ "type": 79, "domains": ["cagreatamerica.com", "canadaswonderland.com", "carowinds.com", "cedarfair.com", "cedarpoint.com", "dorneypark.com", "kingsdominion.com", "knotts.com", "miadventure.com", "schlitterbahn.com", "valleyfair.com", "visitkingsisland.com", "worldsoffun.com"], "excluded": false },
{ "type": 80, "domains": ["ubnt.com", "ui.com"], "excluded": false },
{ "type": 81, "domains": ["discordapp.com", "discord.com"], "excluded": false },
{ "type": 82, "domains": ["netcup.de", "netcup.eu", "customercontrolpanel.de"], "excluded": false },
{ "type": 83, "domains": ["yandex.com", "ya.ru", "yandex.az", "yandex.by", "yandex.co.il", "yandex.com.am", "yandex.com.ge", "yandex.com.tr", "yandex.ee", "yandex.fi", "yandex.fr", "yandex.kg", "yandex.kz", "yandex.lt", "yandex.lv", "yandex.md", "yandex.pl", "yandex.ru", "yandex.tj", "yandex.tm", "yandex.ua", "yandex.uz"], "excluded": false },
{ "type": 84, "domains": ["sonyentertainmentnetwork.com", "sony.com"], "excluded": false },
{ "type": 85, "domains": ["proton.me", "protonmail.com", "protonvpn.com"], "excluded": false },
{ "type": 86, "domains": ["ubisoft.com", "ubi.com"], "excluded": false },
{ "type": 87, "domains": ["transferwise.com", "wise.com"], "excluded": false },
{ "type": 88, "domains": ["takeaway.com", "just-eat.dk", "just-eat.no", "just-eat.fr", "just-eat.ch", "lieferando.de", "lieferando.at", "thuisbezorgd.nl", "pyszne.pl"], "excluded": false },
{ "type": 89, "domains": ["atlassian.com", "bitbucket.org", "trello.com", "statuspage.io", "atlassian.net", "jira.com"], "excluded": false },
{ "type": 90, "domains": ["pinterest.com", "pinterest.com.au", "pinterest.cl", "pinterest.de", "pinterest.dk", "pinterest.es", "pinterest.fr", "pinterest.co.uk", "pinterest.jp", "pinterest.co.kr", "pinterest.nz", "pinterest.pt", "pinterest.se"], "excluded": false },
{ "type": 91, "domains": ["twitter.com", "x.com"], "excluded": false }
]
@@ -0,0 +1,15 @@
{
"source": "https://github.com/bitwarden/server",
"ref": "main",
"generatedAt": "2026-05-05T00:00:00.000Z",
"rulesCount": 91,
"domainsCount": 436,
"sourceFiles": [
"src/Core/Enums/GlobalEquivalentDomainsType.cs",
"src/Core/Utilities/StaticStore.cs"
],
"sourceUrls": [
"https://raw.githubusercontent.com/bitwarden/server/main/src/Core/Enums/GlobalEquivalentDomainsType.cs",
"https://raw.githubusercontent.com/bitwarden/server/main/src/Core/Utilities/StaticStore.cs"
]
}
+8
View File
@@ -0,0 +1,8 @@
[
{
"type": -10001,
"domains": ["nodewarden.example", "nw.example"],
"excluded": false,
"source": "nodewarden"
}
]
+28
View File
@@ -55,6 +55,34 @@ export interface User {
updatedAt: string;
}
export interface UserDomainSettings {
userId: string;
equivalentDomains: string[][];
customEquivalentDomains: CustomEquivalentDomain[];
excludedGlobalEquivalentDomains: number[];
updatedAt: string | null;
}
export interface CustomEquivalentDomain {
id: string;
domains: string[];
excluded: boolean;
}
export interface GlobalEquivalentDomain {
type: number;
domains: string[];
excluded: boolean;
[key: string]: unknown;
}
export interface DomainRulesResponse {
equivalentDomains: string[][];
customEquivalentDomains: CustomEquivalentDomain[];
globalEquivalentDomains: GlobalEquivalentDomain[];
object: 'domains';
}
export interface Invite {
code: string;
createdBy: string;