@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.
@@ -0,0 +1,252 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ declare const claimsCrawler: (ua: string | null) => boolean;
4
+ declare const isPreviewBot: (ua: string | null) => boolean;
5
+ type CrawlerVerdict = 'verified' | 'spoofed' | 'unknown' | 'none';
6
+ interface CrawlerMatcherOptions {
7
+ sources: string[];
8
+ ttlMs?: number;
9
+ staleAfterMs?: number;
10
+ timeoutMs?: number;
11
+ fetch?: typeof fetch;
12
+ now?: () => number;
13
+ /** Called (at most once per hour) when the last good list is older than `staleAfterMs`. */
14
+ onStale?: (ageMs: number) => void;
15
+ }
16
+ declare function createCrawlerMatcher(opts: CrawlerMatcherOptions): {
17
+ /** `verified`: IP is in a published range. `spoofed`: UA claims a crawler, lists loaded, IP not in them. `unknown`: UA claims a crawler, no list ever loaded. `none`: not a crawler claim and not in ranges. */
18
+ check(ip: string | null, ua: string | null): Promise<CrawlerVerdict>;
19
+ state: () => {
20
+ loadedAt: number;
21
+ attemptedAt: number;
22
+ ranges: number;
23
+ };
24
+ };
25
+ type CrawlerMatcher = ReturnType<typeof createCrawlerMatcher>;
26
+
27
+ /**
28
+ * Human-check gate: configuration types and defaults.
29
+ *
30
+ * Off by default. The gate is a request, not a wall: the enforcement is the
31
+ * server-verified Turnstile cookie, the visible text only asks cooperating
32
+ * agents to stop. See `@dev/planned/bot-check-gate/PLAN.md`.
33
+ */
34
+ type HumanCheckLocale = 'en' | 'ru';
35
+ interface HumanCheckMessages {
36
+ title: string;
37
+ /** Stop text for agents. Rendered server-side, before any script. */
38
+ stop: string;
39
+ /** Text for people. */
40
+ human: string;
41
+ checking: string;
42
+ verifying: string;
43
+ done: string;
44
+ failed: string;
45
+ blocked: string;
46
+ misconfigured: string;
47
+ noscript: string;
48
+ }
49
+ type ScopeRule = 'site' | {
50
+ include: string[];
51
+ exclude?: string[];
52
+ };
53
+ type HumanCheckScope = {
54
+ kind: 'site';
55
+ } | {
56
+ kind: 'routes';
57
+ include: string[];
58
+ exclude?: string[];
59
+ }
60
+ /** Per host. A host that is not listed is NOT gated (e.g. the marketing host). */
61
+ | {
62
+ kind: 'hosts';
63
+ hosts: Record<string, ScopeRule>;
64
+ };
65
+ interface HumanCheckConfig {
66
+ /** Master switch. Default false. */
67
+ enabled?: boolean;
68
+ /**
69
+ * `production` (default): the gate never runs when `isDev` from
70
+ * `@djangocfg/api` is true. `all`: it runs in development too.
71
+ * Note `NODE_ENV=production` is also what staging builds use; limit staging
72
+ * with `scope: { kind: 'hosts' }`.
73
+ */
74
+ enabledIn?: 'production' | 'all';
75
+ scope?: HumanCheckScope;
76
+ /** Path of the gate route, served by the proxy itself. Default `/__human-check`. */
77
+ gatePath?: string;
78
+ message?: {
79
+ mode?: 'agent-visible' | 'plain';
80
+ /** Per-locale overrides. */
81
+ overrides?: Partial<Record<HumanCheckLocale, Partial<HumanCheckMessages>>>;
82
+ defaultLocale?: HumanCheckLocale;
83
+ };
84
+ cookie?: {
85
+ name?: string;
86
+ ttlSeconds?: number;
87
+ /** Env var holding the signing secret. Comma-separated: first signs, all verify (rotation). */
88
+ secretEnv?: string;
89
+ };
90
+ turnstile?: {
91
+ siteKeyEnv?: string;
92
+ secretKeyEnv?: string;
93
+ /** Informational: the mode is a property of the widget in Cloudflare. */
94
+ mode?: 'non-interactive' | 'managed';
95
+ action?: string;
96
+ /** Hostnames Turnstile may report. Default: the request hostname. */
97
+ hostnames?: string[];
98
+ };
99
+ bypass?: {
100
+ crawlers?: boolean;
101
+ previewBots?: boolean;
102
+ /** Extra always-open paths (same pattern syntax as scope). */
103
+ paths?: string[];
104
+ /** Crawler IP lists (JSON with `prefixes`). Default: Google and Bing lists. */
105
+ crawlerSources?: string[];
106
+ /**
107
+ * When no crawler list has ever loaded: `allow-claimed` (default) lets a
108
+ * UA that claims to be a crawler through, so an outage cannot de-index the
109
+ * site; `gate` gates it.
110
+ */
111
+ onListUnavailable?: 'allow-claimed' | 'gate';
112
+ /** Trust `cf-connecting-ip` (only when the origin accepts Cloudflare traffic only). */
113
+ trustCloudflareIp?: boolean;
114
+ };
115
+ /** `private` (default): in-scope responses get `Cache-Control: private, no-store`. `leave`: do not touch. */
116
+ cache?: 'private' | 'leave';
117
+ }
118
+ interface ResolvedHumanCheckConfig {
119
+ enabled: boolean;
120
+ enabledIn: 'production' | 'all';
121
+ scope: HumanCheckScope;
122
+ gatePath: string;
123
+ message: {
124
+ mode: 'agent-visible' | 'plain';
125
+ overrides: Partial<Record<HumanCheckLocale, Partial<HumanCheckMessages>>>;
126
+ defaultLocale: HumanCheckLocale;
127
+ };
128
+ cookie: {
129
+ name: string;
130
+ ttlSeconds: number;
131
+ secretEnv: string;
132
+ };
133
+ turnstile: {
134
+ siteKeyEnv: string;
135
+ secretKeyEnv: string;
136
+ mode: 'non-interactive' | 'managed';
137
+ action: string;
138
+ hostnames?: string[];
139
+ };
140
+ bypass: {
141
+ crawlers: boolean;
142
+ previewBots: boolean;
143
+ paths: string[];
144
+ crawlerSources: string[];
145
+ onListUnavailable: 'allow-claimed' | 'gate';
146
+ trustCloudflareIp: boolean;
147
+ };
148
+ cache: 'private' | 'leave';
149
+ }
150
+ declare const DEFAULT_CRAWLER_SOURCES: string[];
151
+ /** Paths that are never gated: crawl infrastructure, assets, OG images, health, webhooks. */
152
+ declare const ALWAYS_OPEN_PATHS: string[];
153
+ declare function resolveConfig(c?: HumanCheckConfig): ResolvedHumanCheckConfig;
154
+
155
+ /**
156
+ * Human-check gate for Next.js (proxy / middleware).
157
+ *
158
+ * ```ts
159
+ * // proxy.ts
160
+ * import { createHumanCheck, withHumanCheck } from '@djangocfg/nextjs/human-check';
161
+ *
162
+ * const humanCheck = createHumanCheck({ enabled: true, scope: { kind: 'hosts', hosts: { 'app.example.com': 'site' } } });
163
+ * export default withHumanCheck(humanCheck, intlProxy);
164
+ * ```
165
+ *
166
+ * The proxy serves the gate route itself (GET page, POST verify), so no app
167
+ * route is needed (an `app/__human-check` folder would be a private folder
168
+ * and never routable). Order: disabled -> dev skip -> always-open paths ->
169
+ * gate route -> scope -> crawler/preview bypass -> valid cookie -> 302 to gate.
170
+ */
171
+
172
+ interface HumanCheckDeps {
173
+ /** Test seam. Default: `isDev` from `@djangocfg/api`. */
174
+ isDev?: boolean;
175
+ fetch?: typeof fetch;
176
+ now?: () => number;
177
+ env?: Record<string, string | undefined>;
178
+ crawlers?: CrawlerMatcher;
179
+ log?: (msg: string) => void;
180
+ }
181
+ type HumanCheckDecision = 'disabled' | 'dev' | 'open' | 'out-of-scope' | 'crawler' | 'preview' | 'cleared' | 'gate';
182
+ declare function clientIp(h: Headers, trustCloudflare: boolean): string | null;
183
+ declare function createHumanCheck(userConfig?: HumanCheckConfig, deps?: HumanCheckDeps): {
184
+ proxy: (req: NextRequest) => Promise<NextResponse | undefined>;
185
+ decide: (req: NextRequest) => Promise<HumanCheckDecision>;
186
+ config: ResolvedHumanCheckConfig;
187
+ };
188
+ type HumanCheck = ReturnType<typeof createHumanCheck>;
189
+ /** Compose with another proxy (e.g. the i18n one): the gate runs first. */
190
+ declare function withHumanCheck<A extends unknown[], R>(humanCheck: HumanCheck, next: (req: NextRequest, ...args: A) => R | Promise<R>): (req: NextRequest, ...args: A) => Promise<NextResponse | R>;
191
+
192
+ /**
193
+ * Wording is a neutral notice about the page's own state, not an override of
194
+ * the user's task: agents may treat instruction-like page text as prompt
195
+ * injection. To be tuned by the live test (PLAN task A1).
196
+ */
197
+ declare const MESSAGES: Record<HumanCheckLocale, HumanCheckMessages>;
198
+
199
+ interface PageOptions {
200
+ locale: HumanCheckLocale;
201
+ messages: HumanCheckMessages;
202
+ mode: 'agent-visible' | 'plain';
203
+ /** Null when the gate is misconfigured: the page then shows the error and no widget. */
204
+ siteKey: string | null;
205
+ action: string;
206
+ /** Minimum time the message stays on screen after success, so a late snapshot still reads it. */
207
+ dwellMs?: number;
208
+ }
209
+ /**
210
+ * Server-rendered gate page. The stop text and the h1 are in the first HTML,
211
+ * before any script, and the same text is repeated in <noscript>. No hidden
212
+ * text, no timers that move the page before verification succeeded.
213
+ */
214
+ declare function renderHumanCheckHtml(o: PageOptions): string;
215
+
216
+ /**
217
+ * Path pattern: exact (`/pricing`), prefix (`/app/*` matches `/app` and everything under it)
218
+ * or bare-star suffix (`/sitemap*` matches `/sitemap.xml`, `/sitemap-1.xml`).
219
+ */
220
+ declare function matchPath(pattern: string, pathname: string): boolean;
221
+ /** Host pattern: exact or `*.example.com`. Case-insensitive, port ignored. */
222
+ declare function matchHost(pattern: string, hostname: string): boolean;
223
+ /** Is this host+path inside the gated scope? A host that is not listed in a `hosts` scope is not gated. */
224
+ declare function inScope(scope: HumanCheckScope, hostname: string, pathname: string): boolean;
225
+ /** `next` must be a same-origin relative path. Anything else becomes `/`. */
226
+ declare function safeNext(raw: string | null | undefined, gatePath: string): string;
227
+
228
+ declare function signClearance(secret: string, ua: string | null, ttlSeconds: number, now?: number): Promise<string>;
229
+ declare function verifyClearance(secrets: string[], value: string | undefined, ua: string | null, now?: number): Promise<boolean>;
230
+
231
+ type VerifyResult = {
232
+ ok: true;
233
+ } | {
234
+ ok: false;
235
+ reason: string;
236
+ retry: boolean;
237
+ };
238
+ /**
239
+ * Server-side Turnstile check. `success` alone is not enough: the token must
240
+ * have been issued for this host and this action, or a token minted on any
241
+ * other site that shares the key would clear the gate.
242
+ */
243
+ declare function verifyTurnstile(args: {
244
+ secret: string;
245
+ token: string;
246
+ remoteIp?: string | null;
247
+ expectedHostnames: string[];
248
+ expectedAction: string;
249
+ fetch?: typeof fetch;
250
+ }): Promise<VerifyResult>;
251
+
252
+ export { ALWAYS_OPEN_PATHS, type CrawlerMatcher, type CrawlerVerdict, DEFAULT_CRAWLER_SOURCES, type HumanCheck, type HumanCheckConfig, type HumanCheckDecision, type HumanCheckDeps, type HumanCheckLocale, type HumanCheckMessages, type HumanCheckScope, MESSAGES, type ScopeRule, claimsCrawler, clientIp, createCrawlerMatcher, createHumanCheck, inScope, isPreviewBot, matchHost, matchPath, renderHumanCheckHtml, resolveConfig, safeNext, signClearance, verifyClearance, verifyTurnstile, withHumanCheck };