@djangocfg/nextjs 2.1.595 → 2.1.596
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/human-check/index.d.mts +252 -0
- package/dist/human-check/index.mjs +542 -0
- package/dist/human-check/index.mjs.map +1 -0
- package/package.json +19 -9
- package/src/human-check/cookie.test.ts +29 -0
- package/src/human-check/cookie.ts +87 -0
- package/src/human-check/crawlers.test.ts +65 -0
- package/src/human-check/crawlers.ts +165 -0
- package/src/human-check/index.ts +14 -0
- package/src/human-check/messages.ts +49 -0
- package/src/human-check/page.test.ts +47 -0
- package/src/human-check/page.ts +69 -0
- package/src/human-check/proxy.test.ts +170 -0
- package/src/human-check/proxy.ts +215 -0
- package/src/human-check/scope.test.ts +42 -0
- package/src/human-check/scope.ts +57 -0
- package/src/human-check/turnstile.ts +45 -0
- package/src/human-check/types.ts +167 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-check gate: configuration types and defaults.
|
|
3
|
+
*
|
|
4
|
+
* Off by default. The gate is a request, not a wall: the enforcement is the
|
|
5
|
+
* server-verified Turnstile cookie, the visible text only asks cooperating
|
|
6
|
+
* agents to stop. See `@dev/planned/bot-check-gate/PLAN.md`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type HumanCheckLocale = 'en' | 'ru';
|
|
10
|
+
|
|
11
|
+
export interface HumanCheckMessages {
|
|
12
|
+
title: string;
|
|
13
|
+
/** Stop text for agents. Rendered server-side, before any script. */
|
|
14
|
+
stop: string;
|
|
15
|
+
/** Text for people. */
|
|
16
|
+
human: string;
|
|
17
|
+
checking: string;
|
|
18
|
+
verifying: string;
|
|
19
|
+
done: string;
|
|
20
|
+
failed: string;
|
|
21
|
+
blocked: string;
|
|
22
|
+
misconfigured: string;
|
|
23
|
+
noscript: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ScopeRule = 'site' | { include: string[]; exclude?: string[] };
|
|
27
|
+
|
|
28
|
+
export type HumanCheckScope =
|
|
29
|
+
| { kind: 'site' }
|
|
30
|
+
| { kind: 'routes'; include: string[]; exclude?: string[] }
|
|
31
|
+
/** Per host. A host that is not listed is NOT gated (e.g. the marketing host). */
|
|
32
|
+
| { kind: 'hosts'; hosts: Record<string, ScopeRule> };
|
|
33
|
+
|
|
34
|
+
export interface HumanCheckConfig {
|
|
35
|
+
/** Master switch. Default false. */
|
|
36
|
+
enabled?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* `production` (default): the gate never runs when `isDev` from
|
|
39
|
+
* `@djangocfg/api` is true. `all`: it runs in development too.
|
|
40
|
+
* Note `NODE_ENV=production` is also what staging builds use; limit staging
|
|
41
|
+
* with `scope: { kind: 'hosts' }`.
|
|
42
|
+
*/
|
|
43
|
+
enabledIn?: 'production' | 'all';
|
|
44
|
+
scope?: HumanCheckScope;
|
|
45
|
+
/** Path of the gate route, served by the proxy itself. Default `/__human-check`. */
|
|
46
|
+
gatePath?: string;
|
|
47
|
+
message?: {
|
|
48
|
+
mode?: 'agent-visible' | 'plain';
|
|
49
|
+
/** Per-locale overrides. */
|
|
50
|
+
overrides?: Partial<Record<HumanCheckLocale, Partial<HumanCheckMessages>>>;
|
|
51
|
+
defaultLocale?: HumanCheckLocale;
|
|
52
|
+
};
|
|
53
|
+
cookie?: {
|
|
54
|
+
name?: string;
|
|
55
|
+
ttlSeconds?: number;
|
|
56
|
+
/** Env var holding the signing secret. Comma-separated: first signs, all verify (rotation). */
|
|
57
|
+
secretEnv?: string;
|
|
58
|
+
};
|
|
59
|
+
turnstile?: {
|
|
60
|
+
siteKeyEnv?: string;
|
|
61
|
+
secretKeyEnv?: string;
|
|
62
|
+
/** Informational: the mode is a property of the widget in Cloudflare. */
|
|
63
|
+
mode?: 'non-interactive' | 'managed';
|
|
64
|
+
action?: string;
|
|
65
|
+
/** Hostnames Turnstile may report. Default: the request hostname. */
|
|
66
|
+
hostnames?: string[];
|
|
67
|
+
};
|
|
68
|
+
bypass?: {
|
|
69
|
+
crawlers?: boolean;
|
|
70
|
+
previewBots?: boolean;
|
|
71
|
+
/** Extra always-open paths (same pattern syntax as scope). */
|
|
72
|
+
paths?: string[];
|
|
73
|
+
/** Crawler IP lists (JSON with `prefixes`). Default: Google and Bing lists. */
|
|
74
|
+
crawlerSources?: string[];
|
|
75
|
+
/**
|
|
76
|
+
* When no crawler list has ever loaded: `allow-claimed` (default) lets a
|
|
77
|
+
* UA that claims to be a crawler through, so an outage cannot de-index the
|
|
78
|
+
* site; `gate` gates it.
|
|
79
|
+
*/
|
|
80
|
+
onListUnavailable?: 'allow-claimed' | 'gate';
|
|
81
|
+
/** Trust `cf-connecting-ip` (only when the origin accepts Cloudflare traffic only). */
|
|
82
|
+
trustCloudflareIp?: boolean;
|
|
83
|
+
};
|
|
84
|
+
/** `private` (default): in-scope responses get `Cache-Control: private, no-store`. `leave`: do not touch. */
|
|
85
|
+
cache?: 'private' | 'leave';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ResolvedHumanCheckConfig {
|
|
89
|
+
enabled: boolean;
|
|
90
|
+
enabledIn: 'production' | 'all';
|
|
91
|
+
scope: HumanCheckScope;
|
|
92
|
+
gatePath: string;
|
|
93
|
+
message: { mode: 'agent-visible' | 'plain'; overrides: Partial<Record<HumanCheckLocale, Partial<HumanCheckMessages>>>; defaultLocale: HumanCheckLocale };
|
|
94
|
+
cookie: { name: string; ttlSeconds: number; secretEnv: string };
|
|
95
|
+
turnstile: { siteKeyEnv: string; secretKeyEnv: string; mode: 'non-interactive' | 'managed'; action: string; hostnames?: string[] };
|
|
96
|
+
bypass: {
|
|
97
|
+
crawlers: boolean;
|
|
98
|
+
previewBots: boolean;
|
|
99
|
+
paths: string[];
|
|
100
|
+
crawlerSources: string[];
|
|
101
|
+
onListUnavailable: 'allow-claimed' | 'gate';
|
|
102
|
+
trustCloudflareIp: boolean;
|
|
103
|
+
};
|
|
104
|
+
cache: 'private' | 'leave';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const DEFAULT_CRAWLER_SOURCES = [
|
|
108
|
+
'https://developers.google.com/static/search/apis/ipranges/googlebot.json',
|
|
109
|
+
'https://developers.google.com/static/search/apis/ipranges/special-crawlers.json',
|
|
110
|
+
'https://developers.google.com/static/search/apis/ipranges/user-triggered-fetchers.json',
|
|
111
|
+
'https://developers.google.com/static/search/apis/ipranges/user-triggered-fetchers-google.json',
|
|
112
|
+
'https://www.bing.com/toolbox/bingbot.json',
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/** Paths that are never gated: crawl infrastructure, assets, OG images, health, webhooks. */
|
|
116
|
+
export const ALWAYS_OPEN_PATHS = [
|
|
117
|
+
'/robots.txt',
|
|
118
|
+
'/sitemap.xml',
|
|
119
|
+
'/sitemap*',
|
|
120
|
+
'/favicon.ico',
|
|
121
|
+
'/.well-known/*',
|
|
122
|
+
'/_next/*',
|
|
123
|
+
'/api/og/*',
|
|
124
|
+
'/health',
|
|
125
|
+
'/healthz',
|
|
126
|
+
'/api/health',
|
|
127
|
+
'/api/webhooks/*',
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
export function resolveConfig(c: HumanCheckConfig = {}): ResolvedHumanCheckConfig {
|
|
131
|
+
const gatePath = c.gatePath ?? '/__human-check';
|
|
132
|
+
if (!gatePath.startsWith('/') || gatePath.startsWith('//')) {
|
|
133
|
+
throw new Error(`human-check: gatePath must be an absolute path, got "${gatePath}"`);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
enabled: c.enabled ?? false,
|
|
137
|
+
enabledIn: c.enabledIn ?? 'production',
|
|
138
|
+
scope: c.scope ?? { kind: 'site' },
|
|
139
|
+
gatePath,
|
|
140
|
+
message: {
|
|
141
|
+
mode: c.message?.mode ?? 'agent-visible',
|
|
142
|
+
overrides: c.message?.overrides ?? {},
|
|
143
|
+
defaultLocale: c.message?.defaultLocale ?? 'en',
|
|
144
|
+
},
|
|
145
|
+
cookie: {
|
|
146
|
+
name: c.cookie?.name ?? '__hc',
|
|
147
|
+
ttlSeconds: c.cookie?.ttlSeconds ?? 86400,
|
|
148
|
+
secretEnv: c.cookie?.secretEnv ?? 'HUMAN_CHECK_SECRET',
|
|
149
|
+
},
|
|
150
|
+
turnstile: {
|
|
151
|
+
siteKeyEnv: c.turnstile?.siteKeyEnv ?? 'NEXT_PUBLIC_TURNSTILE_SITE_KEY',
|
|
152
|
+
secretKeyEnv: c.turnstile?.secretKeyEnv ?? 'TURNSTILE_SECRET_KEY',
|
|
153
|
+
mode: c.turnstile?.mode ?? 'non-interactive',
|
|
154
|
+
action: c.turnstile?.action ?? 'human-check',
|
|
155
|
+
hostnames: c.turnstile?.hostnames,
|
|
156
|
+
},
|
|
157
|
+
bypass: {
|
|
158
|
+
crawlers: c.bypass?.crawlers ?? true,
|
|
159
|
+
previewBots: c.bypass?.previewBots ?? true,
|
|
160
|
+
paths: c.bypass?.paths ?? [],
|
|
161
|
+
crawlerSources: c.bypass?.crawlerSources ?? DEFAULT_CRAWLER_SOURCES,
|
|
162
|
+
onListUnavailable: c.bypass?.onListUnavailable ?? 'allow-claimed',
|
|
163
|
+
trustCloudflareIp: c.bypass?.trustCloudflareIp ?? false,
|
|
164
|
+
},
|
|
165
|
+
cache: c.cache ?? 'private',
|
|
166
|
+
};
|
|
167
|
+
}
|