shabbat-gate 0.4.0 → 0.4.2

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/CHANGELOG.md CHANGED
@@ -3,7 +3,56 @@
3
3
  All notable changes to this package are documented here. Format loosely follows
4
4
  [Keep a Changelog](https://keepachangelog.com/).
5
5
 
6
- ## [0.4.0] - Unreleased
6
+ ## [0.4.2] - Unreleased
7
+
8
+ ### Fixed
9
+
10
+ - **A window-list refresh during Shabbat or a holiday dropped the closure in progress, so the
11
+ site reopened mid-Shabbat.** Found 2026-09-15 on a consumer site. `fetchWindows` asked Hebcal
12
+ for a range starting *today*; on a closed day the candle lighting was yesterday, so the
13
+ response held only the havdalah, `pairWindows` (which opens a window only on candles) ignored
14
+ it, and the list had no window covering `now`. The cache is fresh for 24 hours, so a list
15
+ fetched on Friday went stale on Saturday and the first visitor after that refreshed it into
16
+ an "open" answer for the rest of Shabbat. Same for Yom Kippur and every Yom Tov day after the
17
+ first. Verified against live Hebcal: a range starting 2026-09-19 does not block Saturday noon,
18
+ one starting 2026-09-21 does not block Yom Kippur noon.
19
+
20
+ The range now starts `HEBCAL_LOOKBACK_DAYS` (4) days back, which covers the longest closure
21
+ (a two-day Rosh Hashana running into Shabbat). Past windows in the list are harmless. Applies
22
+ to the visitor-location windows too. Regression test: a fetch on Saturday noon, with Hebcal's
23
+ start-date filtering simulated, must still report blocked.
24
+
25
+ ## [0.4.1] - 2026-08-29
26
+
27
+ ### Fixed
28
+
29
+ - **`adsbot-google` in the crawler allowlist was cloaking under Google Ads policy - caused a
30
+ real Ads + Merchant Center account suspension.** Found 2026-08-29 on a consumer site: during
31
+ a gate closure window, AdsBot-Google (the crawler Google Ads uses to verify what a real ad
32
+ landing page shows) saw the full real site, while a human clicking the same ad saw the holding
33
+ page. Google's suspension notice for "עקיפת מערכות: הסוואה" (circumventing systems: cloaking)
34
+ described exactly this - showing Google's verification bot different content than what users
35
+ see - almost verbatim.
36
+
37
+ Removing the explicit `adsbot-google` term from `BOT_PATTERN`'s alternation alone would **not**
38
+ have fixed this: the pattern's generic `bot` alternative already matches the substring "Bot" in
39
+ "AdsBot-Google", so it stayed allowed regardless. `isBot()` now checks a dedicated
40
+ `AD_VERIFICATION_BOT_PATTERN` first and returns `false` before the broad pattern ever runs, so
41
+ these crawlers are gated exactly like a human visitor. Every other crawler in `BOT_PATTERN`
42
+ (Googlebot, Bingbot, GPTBot, ClaudeBot, etc.) is unaffected - organic/AI indexing during a
43
+ closure window is a different, broadly-tolerated risk category from a dedicated ad-verification
44
+ bot, which ad-network policy specifically requires to see what humans see.
45
+
46
+ Two more crawlers in the same risk category are excluded alongside AdsBot-Google, for the same
47
+ reason and hitting the same "bot" substring problem: **`Storebot-Google`** (Google
48
+ Merchant Center/Shopping's product landing-page verifier - directly relevant here, since
49
+ Merchant Center was the linked account also suspended) and **`adidxbot`** (Microsoft
50
+ Advertising/Bing Ads' equivalent landing-page verification crawler).
51
+
52
+ Regression tests assert `isBot()` returns `false` for AdsBot-Google, AdsBot-Google-Mobile,
53
+ Storebot-Google, and adidxbot user agents.
54
+
55
+ ## [0.4.0] - 2026-07-28
7
56
 
8
57
  ### Fixed
9
58
 
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Broad, case-insensitive allowlist for search engine and AI crawlers.
3
3
  * Matches let a request through unconditionally, before any gate logic runs.
4
- * Erring toward "let more things through" is the safe direction here, since
5
- * the whole point of this list is protecting SEO/crawlability.
4
+ * Erring toward "let more things through" is the safe direction for organic/AI
5
+ * crawlers, since the whole point of this list is protecting SEO/crawlability.
6
6
  */
7
7
  export declare const BOT_PATTERN: RegExp;
8
8
  export declare function isBot(userAgent: string): boolean;
@@ -1,10 +1,37 @@
1
1
  /**
2
2
  * Broad, case-insensitive allowlist for search engine and AI crawlers.
3
3
  * Matches let a request through unconditionally, before any gate logic runs.
4
- * Erring toward "let more things through" is the safe direction here, since
5
- * the whole point of this list is protecting SEO/crawlability.
4
+ * Erring toward "let more things through" is the safe direction for organic/AI
5
+ * crawlers, since the whole point of this list is protecting SEO/crawlability.
6
6
  */
7
- export const BOT_PATTERN = /bot|crawl|spider|slurp|googlebot|google-inspectiontool|adsbot-google|bingbot|duckduckbot|baiduspider|yandex|facebookexternalhit|twitterbot|linkedinbot|whatsapp|telegrambot|discordbot|applebot|gptbot|chatgpt-user|oai-searchbot|ccbot|claudebot|claude-web|anthropic-ai|perplexitybot|google-extended|bytespider|semrushbot|ahrefsbot|mj12bot|petalbot/i;
7
+ export const BOT_PATTERN = /bot|crawl|spider|slurp|googlebot|google-inspectiontool|bingbot|duckduckbot|baiduspider|yandex|facebookexternalhit|twitterbot|linkedinbot|whatsapp|telegrambot|discordbot|applebot|gptbot|chatgpt-user|oai-searchbot|ccbot|claudebot|claude-web|anthropic-ai|perplexitybot|google-extended|bytespider|semrushbot|ahrefsbot|mj12bot|petalbot/i;
8
+ /**
9
+ * Dedicated ad/shopping-network verification crawlers are deliberately NOT
10
+ * allowed through the gate, even though their user agents also match the
11
+ * generic `bot` term above (e.g. "AdsBot-Google" and "Storebot-Google" both
12
+ * contain the substring "bot", "adidxbot" ends in it) - excluding them from
13
+ * BOT_PATTERN's alternation alone would do nothing, since that bare substring
14
+ * still matches.
15
+ *
16
+ * Unlike organic/AI indexing crawlers, these bots exist specifically to
17
+ * verify that a live ad or shopping listing's destination page matches what a
18
+ * human clicking it sees - Google Ads, Google Merchant Center/Shopping, and
19
+ * Microsoft Advertising all require exactly that. Letting one bypass the gate
20
+ * means it sees the real site during a closure window while a human visitor
21
+ * sees the holding page - a textbook cloaking pattern, and the exact
22
+ * violation Google's policy team cited when it suspended a consumer's Ads +
23
+ * Merchant Center accounts over this (2026-08-29).
24
+ *
25
+ * - `adsbot-google` - AdsBot-Google / AdsBot-Google-Mobile, verifies Google
26
+ * Ads landing pages.
27
+ * - `storebot-google` - verifies Google Shopping / Merchant Center product
28
+ * landing pages against the submitted feed.
29
+ * - `adidxbot` - Microsoft Advertising's (Bing Ads) equivalent landing-page
30
+ * verification crawler.
31
+ */
32
+ const AD_VERIFICATION_BOT_PATTERN = /adsbot-google|storebot-google|adidxbot/i;
8
33
  export function isBot(userAgent) {
34
+ if (AD_VERIFICATION_BOT_PATTERN.test(userAgent))
35
+ return false;
9
36
  return BOT_PATTERN.test(userAgent);
10
37
  }
package/dist/hebcal.d.ts CHANGED
@@ -50,6 +50,10 @@ export declare function pairWindows(items: HebcalItem[], defaults?: {
50
50
  label: string;
51
51
  closingLabel: string;
52
52
  }): Window[];
53
+ /** How far back the Hebcal range starts. The longest closure is a two-day Rosh
54
+ * Hashana running into Shabbat, whose first candles are lit three days before
55
+ * it ends; four days covers it with a margin for the UTC date boundary. */
56
+ export declare const HEBCAL_LOOKBACK_DAYS = 4;
53
57
  export interface FetchWindowsOptions {
54
58
  /** `true` (default) = Israel single-day Yom Tov reckoning (`i=on`). `false` =
55
59
  * diaspora two-day Yom Tov reckoning (`i=off`), correct for a visitor
@@ -92,7 +96,8 @@ export declare class HebcalTimeoutError extends Error {
92
96
  constructor(timeoutMs: number);
93
97
  }
94
98
  /**
95
- * Fetches and merges Shabbat + major-holiday windows for the next ~45 days from
99
+ * Fetches and merges Shabbat + major-holiday windows from a few days back (so a
100
+ * closure already in progress is included) to ~45 days ahead, from
96
101
  * Hebcal's free public JSON API. Defaults to Israel single-day Yom Tov mode at
97
102
  * Jerusalem's timezone; pass `options` to compute windows for a visitor's own
98
103
  * location/reckoning instead (see {@link FetchWindowsOptions}).
package/dist/hebcal.js CHANGED
@@ -60,6 +60,10 @@ export function pairWindows(items, defaults) {
60
60
  function toISODate(date) {
61
61
  return date.toISOString().slice(0, 10);
62
62
  }
63
+ /** How far back the Hebcal range starts. The longest closure is a two-day Rosh
64
+ * Hashana running into Shabbat, whose first candles are lit three days before
65
+ * it ends; four days covers it with a margin for the UTC date boundary. */
66
+ export const HEBCAL_LOOKBACK_DAYS = 4;
63
67
  /** Conservative default: Hebcal normally answers in well under a second, and
64
68
  * the answer only has to be waited for on a cold cache anyway. */
65
69
  export const DEFAULT_HEBCAL_TIMEOUT_MS = 3000;
@@ -73,7 +77,8 @@ export class HebcalTimeoutError extends Error {
73
77
  }
74
78
  }
75
79
  /**
76
- * Fetches and merges Shabbat + major-holiday windows for the next ~45 days from
80
+ * Fetches and merges Shabbat + major-holiday windows from a few days back (so a
81
+ * closure already in progress is included) to ~45 days ahead, from
77
82
  * Hebcal's free public JSON API. Defaults to Israel single-day Yom Tov mode at
78
83
  * Jerusalem's timezone; pass `options` to compute windows for a visitor's own
79
84
  * location/reckoning instead (see {@link FetchWindowsOptions}).
@@ -93,8 +98,12 @@ export class HebcalTimeoutError extends Error {
93
98
  export async function fetchWindows(latitude, longitude, options = {}) {
94
99
  const israelMode = options.israelMode ?? true;
95
100
  const tzid = options.tzid ?? 'Asia/Jerusalem';
96
- const start = new Date();
97
- const end = new Date(start.getTime() + 45 * 24 * 60 * 60 * 1000);
101
+ // Start a few days back, not today: on a closed day the candle lighting that
102
+ // opened the closure was yesterday, and without it pairWindows drops the
103
+ // whole window - a refresh during Shabbat would reopen the site (2026-09-15).
104
+ const now = Date.now();
105
+ const start = new Date(now - HEBCAL_LOOKBACK_DAYS * 24 * 60 * 60 * 1000);
106
+ const end = new Date(now + 45 * 24 * 60 * 60 * 1000);
98
107
  const startParam = toISODate(start);
99
108
  const endParam = toISODate(end);
100
109
  // A specific city (geonameid) yields Hebcal's official times for that city;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shabbat-gate",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Cloudflare Pages/Workers middleware that closes a site to human visitors during Shabbat and major Jewish holidays (Israel-observance rules), while always letting search engines and AI crawlers through.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",