shugoi 0.3.0 → 0.4.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 57dac16265f7ee95d5a4b898026d14fb318c695681eef25607ba427fbc260b59
4
- data.tar.gz: ec35d03b156df7fa25750d794e338978c47dc8288bded4aec4542a732eb70c58
3
+ metadata.gz: 1f63abeeff1710b4f01a153682e1e2f4f5dce888b9d983a32029edf88e72340a
4
+ data.tar.gz: 74343d50af7a65a38d71ad7aed4467c03add9eb30b7716267bcec6691484beec
5
5
  SHA512:
6
- metadata.gz: 995c4835ad283fb5b7e614ab897c896d7dd29a0bb1ebbdb7a3bb7d3374a51921419e0f1160f59482b5e1e108f606a32a6d7b6f9adfded0a23b9531e6c4d3f1cd
7
- data.tar.gz: 2af87cfffe903de0dffc18ba56887347e18af11c31d49fa1e3973b35d56e2c8d345b9d67b47592554798e7aa7afe72084d46abecefa70232ae43910b06300c75
6
+ metadata.gz: 93796f86c5b561da96e506970bc5b9c820467b886fb336fbf88d57fb1153d9e48ef962d5836671e7038eecabd4bf529b7d186d15547e9d0e98d2e01025d74e34
7
+ data.tar.gz: 2d0802d5ae1c2f16319312a1c052b2ee036e225e75a7bc3661204f22d84ecccf1c778de548b3df2e91c5c8e832b056c489e0ea49497bca7f07008b56abbfa6ad
@@ -54,6 +54,19 @@ module Shugoi
54
54
  { "valid" => false, "reason" => "network" }
55
55
  end
56
56
 
57
+ # POST /rate-limit-check — vérifie le quota edge par IP (parité core.ts).
58
+ def check_rate_limit(site_key, ip, user_agent = "")
59
+ payload = {
60
+ siteKey: site_key,
61
+ scope: "edge_ip",
62
+ ip: ip,
63
+ metadata: { ip: ip, userAgent: user_agent || "", middleware: true }
64
+ }
65
+ post_json("/rate-limit-check", payload)
66
+ rescue StandardError
67
+ { "allowed" => true }
68
+ end
69
+
57
70
  private
58
71
 
59
72
  def get(path, params)
data/lib/shugoi/core.rb CHANGED
@@ -15,10 +15,11 @@ module Shugoi
15
15
  CHALLENGE_MAX_BLOCK_MS = 15 * 60 * 1000
16
16
  VALIDATION_WARN_INTERVAL = 3_600_000
17
17
 
18
- def initialize(config, pow, api_client)
18
+ def initialize(config, pow, api_client, config_cache = nil)
19
19
  @config = config
20
20
  @pow = pow
21
21
  @api_client = api_client
22
+ @config_cache = config_cache
22
23
  @validation_valid = false
23
24
  @validation_failed = false
24
25
  @validation_warned_at = 0
@@ -66,12 +67,41 @@ module Shugoi
66
67
 
67
68
  loc = Locales.resolve_locale(@config.locale, ctx[:accept_language])
68
69
  msgs = Locales.messages(loc)
69
- return Decision.new(status: 429, content_type: "text/html", body: shield_page(msgs[:rate_limit_title], msgs[:rate_limit_body], msgs[:rate_limit_badge], ctx[:host].to_s, 60, loc), headers: {})
70
+ return Decision.new(status: 429, content_type: "text/html", body: shield_page(msgs[:rate_limit_title], msgs[:rate_limit_body].call("1 min"), msgs[:rate_limit_badge], ctx[:host].to_s, 60, loc), headers: {})
71
+ end
72
+ end
73
+
74
+ # Flags de détection (whitelist + skipPaths), parité fetchConfigForSiteKey.
75
+ flags = @config_cache ? @config_cache.fetch(@config.site_key)[:flags] : {}
76
+ headless_enabled = flags["enableHeadlessCheck"] != false
77
+
78
+ # Rate limit check — activé uniquement si le flag est explicitement vrai.
79
+ if flags["enableRateLimit"] == true
80
+ rl = @api_client.check_rate_limit(@config.site_key, ctx[:ip].to_s, ctx[:ua].to_s)
81
+ if rl && rl["allowed"] == false
82
+ reset_at = rl["resetAt"].to_i
83
+ reset_at = (reset_at / 1000.0).ceil if reset_at > 1_000_000_000_000
84
+ remain = [0, reset_at - Utils.now_sec].max
85
+ mins = remain / 60
86
+ secs = remain % 60
87
+ time_str = if mins.positive?
88
+ "#{mins} min#{mins > 1 ? 's' : ''}#{secs.positive? ? " #{secs} s" : ''}"
89
+ else
90
+ "#{secs} seconde#{secs > 1 ? 's' : ''}"
91
+ end
92
+ loc = @config.locale || Locales.resolve_locale(nil, ctx[:accept_language])
93
+ msgs = Locales.messages(loc)
94
+ body = if @config.block_page.respond_to?(:call)
95
+ @config.block_page.call(reason: "rate_limit", title: msgs[:rate_limit_title], message: msgs[:rate_limit_body].call(time_str), badge: msgs[:rate_limit_badge], host: ctx[:host].to_s, remainingSeconds: remain, locale: loc)
96
+ else
97
+ shield_page(msgs[:rate_limit_title], msgs[:rate_limit_body].call(time_str), msgs[:rate_limit_badge], ctx[:host].to_s, remain, loc)
98
+ end
99
+ return Decision.new(status: 429, content_type: "text/html", body: body, headers: {})
70
100
  end
71
101
  end
72
102
 
73
103
  # Blocage headless (UA). Actif par défaut, y compris sans configuration chargée.
74
- if !ua.empty? && !is_trusted_bot?(ua, ctx[:ip].to_s) && @config.is_headless?(ua)
104
+ if headless_enabled && !ua.empty? && !is_trusted_bot?(ua, ctx[:ip].to_s) && @config.is_headless?(ua)
75
105
  @api_client.post_event(@config.site_key, "headless", "")
76
106
  return Decision.new(status: @config.block_status, content_type: "text/plain", body: BLOCK_PAGE, headers: {})
77
107
  end
@@ -93,7 +123,7 @@ module Shugoi
93
123
  loc = Locales.resolve_locale(@config.locale, ctx[:accept_language])
94
124
  if !allow_challenge?(ctx[:ip].to_s)
95
125
  msgs = Locales.messages(loc)
96
- return Decision.new(status: 429, content_type: "text/html", body: shield_page(msgs[:rate_limit_title], msgs[:rate_limit_body], msgs[:rate_limit_badge], ctx[:host].to_s, 60, loc), headers: {})
126
+ return Decision.new(status: 429, content_type: "text/html", body: shield_page(msgs[:rate_limit_title], msgs[:rate_limit_body].call("1 min"), msgs[:rate_limit_badge], ctx[:host].to_s, 60, loc), headers: {})
97
127
  end
98
128
  js = <<~JS
99
129
  (function(){
@@ -5,6 +5,7 @@ module Shugoi
5
5
  module Locales
6
6
  FR = {
7
7
  rate_limit_title: "Trop de requêtes",
8
+ rate_limit_body: ->(t) { "Vous avez effectué trop de requêtes en peu de temps. Il reste #{t} avant de pouvoir réessayer." },
8
9
  rate_limit_badge: "Rate Limit",
9
10
  blocked_title: "Accès bloqué",
10
11
  blocked_badge: "Blocage",
@@ -16,6 +17,7 @@ module Shugoi
16
17
 
17
18
  EN = {
18
19
  rate_limit_title: "Too Many Requests",
20
+ rate_limit_body: ->(t) { "You have made too many requests in a short time. #{t} remaining before you can try again." },
19
21
  rate_limit_badge: "Rate Limit",
20
22
  blocked_title: "Access Blocked",
21
23
  blocked_badge: "Blocked",
data/lib/shugoi/notice.rb CHANGED
@@ -6,71 +6,83 @@ module Shugoi
6
6
  # dans le HTML APRÈS le split-render (elle ne disparaît pas quand le render remplace
7
7
  # le document). L'ack est décidé côté SERVEUR via /notice (machineId), pas de cookie.
8
8
  class Notice
9
- # Script renforcé (parité avec guard.src.js du site Node, audit §8.5.3) :
10
- # - overlay bloquant z-index max
11
- # - verrouillage scroll/wheel/touchmove tant que la notice est affichée
12
- # - MutationObserver anti-bypass COMPLET : ré-affiche la notice si elle est retirée
13
- # du DOM, et RESTAURE intégralement son style ET son innerHTML s'ils sont modifiés
14
- # (devtools / manipulation JS du style, des attributs, du texte, du contenu).
15
- # L'overlay est reconstruit à partir du template si un élément clé manque.
16
- # - obligation d'accepter (bouton OK) pour déverrouiller — l'ack est serveur (mid).
17
- # Les placeholders `mid` et `sk` sont remplacés à l'injection.
18
- SCRIPT = <<~'JS'
19
- <script>
20
- (function(){
21
- var mid=window.__sg_mid||'';
22
- var sk=window.__sg_siteKey||'';
23
- if(!mid||!sk||window.__sg_noticeEnabled===false)return;
24
- var base=window.__sg_baseUrl||'';
25
- var origin=base.replace(/\/api\/v1\/?$/,'');
26
- var _OVERLAY_CSS='position:fixed!important;inset:0!important;z-index:2147483647!important;background:rgba(0,0,0,.6)!important;display:flex!important;align-items:center!important;justify-content:center!important;padding:1.2rem!important';
27
- var _CARD_CSS='background:#fff!important;border:4px solid #000!important;border-radius:28px 6px 32px 10px!important;box-shadow:14px 14px 0 #000!important;padding:0!important;max-width:720px!important;width:100%!important;text-align:center!important;font-family:Arial,sans-serif!important;display:flex!important;overflow:hidden!important';
28
- var _CARD_HTML='<div style="flex:0 0 320px;display:flex;align-items:center;justify-content:center;padding:1.5rem 1rem 1.5rem 3rem;overflow:hidden"><img src="'+origin+'/favicon.png" alt="" style="width:100%;height:auto;max-width:220px;pointer-events:none"></div><div style="flex:1;padding:1.6rem 1.8rem;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center"><img src="'+origin+'/brand-block.png" alt="Shugoi" style="display:block;margin:0 0 .3rem;pointer-events:none;max-width:100%;height:auto;max-height:40px"><div style="border:2px solid #000;display:inline-block;border-radius:8px 2px 12px 4px;padding:.2rem .6rem;font-size:.5rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:#E87090;margin-bottom:.6rem">Protection anti-abus</div><p style="font-size:.8rem;color:#555;line-height:1.7;margin:0 .4rem .6rem;max-width:280px">Ce site utilise Shugoi pour se protéger contre les abus et la fraude. Des caractéristiques techniques de votre navigateur sont analysées pour détecter les scripts automatisés, Tor, les VPN et les environnements virtuels. Aucune donnée personnelle n\\'est collectée.</p><button id="__sg_ok" style="background:#E87090;color:#fff;border:3px solid #000;border-radius:12px 3px 14px 5px;padding:.35rem 1.4rem;font-size:.8rem;font-weight:700;cursor:pointer">OK</button><div style="margin-top:.5rem;font-size:.5rem;color:#ccc"><a href="'+origin+'/legal/shugoi-notice" target="_blank" style="color:#E87090;text-decoration:underline">En savoir plus · shugoi.com</a></div></div>';
29
- function ack(){try{fetch(base+'/notice',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({machineId:mid,siteKey:sk}),keepalive:true,signal:AbortSignal.timeout(4000)}).catch(function(){})}catch(e){}}
30
- function buildOverlay(){
31
- var o=document.createElement('div');o.id='__sg_o';o.style.cssText=_OVERLAY_CSS;
32
- var c=document.createElement('div');c.id='__sg_cd';c.style.cssText=_CARD_CSS;
33
- c.innerHTML=_CARD_HTML;
34
- o.appendChild(c);return o;
35
- }
36
- function okHandler(keepMo){ack();var el=document.getElementById('__sg_o');if(el&&el.parentNode)el.parentNode.removeChild(el);document.body.style.overflow='';document.documentElement.style.overflow='';if(keepMo===true){try{mo.disconnect()}catch(e){}}}
37
- function rebindOk(){var b=document.getElementById('__sg_ok');if(b)b.onclick=function(){okHandler(true)};}
38
- function enforce(){
9
+ # Script renforcé PARITÉ EXACTE avec NOTICE_SCRIPT de render.ts (module Node) :
10
+ # - overlay bloquant z-index max (position:fixed;inset:0;pointer-events:auto)
11
+ # - MutationObserver anti-bypass ciblé + garde _applying et mo.takeRecords()
12
+ # (anti-boucle infinie : nos propres ré-applications ne re-déclenchent pas le MO)
13
+ # - PAS de setInterval, PAS de piège clavier (version légère du module Node)
14
+ # - seul OK ferme (ack serveur via POST /notice, lié au machineId)
15
+ # Les placeholders `mid` et `sk` (et `base`) sont remplacés à l'injection.
16
+ SCRIPT = <<~'JS'
17
+ <script>
18
+ (function(){
19
+ var mid=window.__sg_mid||'';
20
+ var sk=window.__sg_siteKey||'';
21
+ if(!mid||!sk||window.__sg_noticeEnabled===false)return;
22
+ var base=window.__sg_baseUrl||'';
23
+ var origin=base.replace(/\/api\/v1\/?$/,'');
24
+ var _OVERLAY_CSS='position:fixed!important;inset:0!important;z-index:2147483647!important;background:rgba(0,0,0,.6)!important;display:flex!important;align-items:center!important;justify-content:center!important;padding:1.2rem!important;pointer-events:auto!important';
25
+ var _CARD_CSS='background:#fff!important;border:4px solid #000!important;border-radius:28px 6px 32px 10px!important;box-shadow:14px 14px 0 #000!important;padding:0!important;max-width:720px!important;width:100%!important;text-align:center!important;font-family:Arial,sans-serif!important;display:flex!important;overflow:hidden!important';
26
+ var _CARD_HTML='<div style="flex:0 0 320px;display:flex;align-items:center;justify-content:center;padding:1.5rem 1rem 1.5rem 3rem;overflow:hidden"><img src="'+origin+'/favicon.png" alt="" style="width:100%;height:auto;max-width:220px;pointer-events:none"></div><div style="flex:1;padding:1.6rem 1.8rem;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center"><img src="'+origin+'/brand-block.png" alt="Shugoi" style="display:block;margin:0 0 .3rem;pointer-events:none;max-width:100%;height:auto;max-height:40px"><div style="border:2px solid #000;display:inline-block;border-radius:8px 2px 12px 4px;padding:.2rem .6rem;font-size:.5rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:#E87090;margin-bottom:.6rem">Protection anti-abus</div><p style="font-size:.8rem;color:#555;line-height:1.7;margin:0 .4rem .6rem;max-width:280px">Ce site utilise Shugoi pour se prot\\u00e9ger contre les abus et la fraude. Des caract\\u00e9ristiques techniques de votre navigateur sont analys\\u00e9es pour d\\u00e9tecter les scripts automatis\\u00e9s, Tor, les VPN et les environnements virtuels. Aucune donn\\u00e9e personnelle n\\'est collect\\u00e9e.</p><div style="margin-top:.5rem"><button id="__sg_ok" style="background:#E87090;color:#fff;border:3px solid #000;border-radius:12px 3px 14px 5px;padding:.4rem 2rem;font-size:.8rem;font-weight:700;cursor:pointer">OK</button></div><div style="margin-top:.5rem;font-size:.5rem;color:#ccc"><a href="'+origin+'/legal/shugoi-notice" target="_blank" style="color:#E87090;text-decoration:underline">En savoir plus \\u00b7 shugoi.com</a></div></div>';
27
+ var _closed=false;
28
+ var mo=null;
29
+ var _applying=false;
30
+ function ack(){try{fetch(base+'/notice',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({machineId:mid,siteKey:sk}),keepalive:true,signal:AbortSignal.timeout(4000)}).catch(function(){})}catch(e){}}
31
+ function buildOverlay(){
32
+ var o=document.createElement('div');o.id='__sg_o';o.style.cssText=_OVERLAY_CSS;
33
+ var c=document.createElement('div');c.id='__sg_cd';c.style.cssText=_CARD_CSS;
34
+ c.innerHTML=_CARD_HTML;
35
+ o.appendChild(c);return o;
36
+ }
37
+ function close(){_closed=true;try{if(mo)mo.disconnect()}catch(e){}var el=document.getElementById('__sg_o');if(el&&el.parentNode)el.parentNode.removeChild(el);document.body.style.overflow='';document.documentElement.style.overflow='';}
38
+ function okHandler(){ack();close();}
39
+ function rebind(){var b=document.getElementById('__sg_ok');if(b)b.onclick=okHandler;}
40
+ // Anti-bypass 100% MUTATION OBSERVER (aucun setInterval).
41
+ // CRITIQUE anti-freeze : le flag _applying + mo.takeRecords() cassent la boucle MO —
42
+ // nos propres modifications (style/innerHTML re-appliqués) ne re-déclenchent PAS le MO
43
+ // (le navigateur normalise cssText/innerHTML, donc la comparaison échoue toujours et
44
+ // on ré-appliquerait à l'infini). takeRecords() vide la file des mutations que NOS
45
+ // changements ont générée → une seule passe par altération réelle, jamais de gel.
46
+ function enforce(){
47
+ if(_closed||_applying)return;
48
+ _applying=true;
49
+ try{
39
50
  var o=document.getElementById('__sg_o');
40
- if(!o){
41
- document.documentElement.appendChild(buildOverlay());
42
- document.body.style.overflow='hidden';document.documentElement.style.overflow='hidden';
43
- rebindOk();return;
44
- }
51
+ if(!o){o=buildOverlay();document.documentElement.appendChild(o);}
45
52
  if(o.style.cssText!==_OVERLAY_CSS)o.style.cssText=_OVERLAY_CSS;
46
53
  var c=document.getElementById('__sg_cd');
47
- if(!c){
48
- // card supprimée → reconstruit l'overlay entier
49
- var fresh=buildOverlay();o.innerHTML='';o.appendChild(fresh.childNodes[0]);
50
- document.body.style.overflow='hidden';document.documentElement.style.overflow='hidden';rebindOk();return;
54
+ if(!c){o.innerHTML='';o.appendChild(buildOverlay().firstChild);}
55
+ else{
56
+ if(c.style.cssText!==_CARD_CSS)c.style.cssText=_CARD_CSS;
57
+ if(c.innerHTML!==_CARD_HTML)c.innerHTML=_CARD_HTML;
51
58
  }
52
- if(c.style.cssText!==_CARD_CSS)c.style.cssText=_CARD_CSS;
53
- if(c.innerHTML!==_CARD_HTML){c.innerHTML=_CARD_HTML;rebindOk();}
59
+ rebind();
54
60
  if(document.body.style.overflow!=='hidden')document.body.style.overflow='hidden';
55
61
  if(document.documentElement.style.overflow!=='hidden')document.documentElement.style.overflow='hidden';
62
+ try{
63
+ if(o&&!o.__sgObserved){o.__sgObserved=true;mo&&mo.observe(o,{childList:true,subtree:true,attributes:true,characterData:true,attributeFilter:['style','class','id']});}
64
+ }catch(e){}
65
+ }finally{
66
+ _applying=false;
67
+ try{if(mo)mo.takeRecords();}catch(e){}
56
68
  }
57
- var mo=null;
58
- function show(){
59
- document.documentElement.appendChild(buildOverlay());
60
- var sp=window.scrollY||window.pageYOffset||0;
61
- document.body.style.overflow='hidden';document.documentElement.style.overflow='hidden';
62
- window.addEventListener('scroll',function(){window.scrollTo(0,sp)}, {passive:false});
63
- window.addEventListener('touchmove',function(e){e.preventDefault()},{passive:false});
64
- window.addEventListener('wheel',function(e){e.preventDefault()},{passive:false});
65
- rebindOk();
69
+ }
70
+ function show(){
71
+ _closed=false;
72
+ var o=buildOverlay();document.documentElement.appendChild(o);
73
+ document.body.style.overflow='hidden';document.documentElement.style.overflow='hidden';
74
+ rebind();
75
+ try{
66
76
  mo=new MutationObserver(function(){enforce();});
67
- mo.observe(document.documentElement,{childList:true,subtree:true,attributes:true,characterData:true,attributeFilter:['style','class','id']});
68
- }
69
- function init(){if(document.body)show();else if(document.addEventListener)document.addEventListener('DOMContentLoaded',show);else setTimeout(init,50)}
70
- fetch(base+'/notice?machineId='+encodeURIComponent(mid)+'&siteKey='+encodeURIComponent(sk),{signal:AbortSignal.timeout(4000)}).then(function(r){return r.json()}).then(function(d){if(!d.acknowledged)init()}).catch(function(){init()});
71
- })();
72
- </script>
73
- JS
77
+ mo.observe(document.documentElement,{childList:true});
78
+ try{o.__sgObserved=true;mo.observe(o,{childList:true,subtree:true,attributes:true,characterData:true,attributeFilter:['style','class','id']});}catch(e){}
79
+ }catch(e){}
80
+ }
81
+ function init(){if(document.body)show();else if(document.addEventListener)document.addEventListener('DOMContentLoaded',show);else setTimeout(init,50)}
82
+ fetch(base+'/notice?machineId='+encodeURIComponent(mid)+'&siteKey='+encodeURIComponent(sk),{signal:AbortSignal.timeout(4000)}).then(function(r){return r.json()}).then(function(d){if(!d.acknowledged)init()}).catch(function(){init()});
83
+ })();
84
+ </script>
85
+ JS
74
86
 
75
87
  # Injecte la notice dans le HTML rendu (avant </body>).
76
88
  # @param html [String] HTML rendu
data/lib/shugoi/pow.rb CHANGED
@@ -94,7 +94,7 @@ module Shugoi
94
94
  end
95
95
 
96
96
  def production?
97
- ENV["NODE_ENV"] == "production" || ENV["RACK_ENV"] == "production"
97
+ ENV["NODE_ENV"] == "production" || ENV["RACK_ENV"] == "production" || ENV["RAILS_ENV"] == "production"
98
98
  end
99
99
  end
100
100
  end
@@ -15,6 +15,7 @@ module Shugoi
15
15
  def initialize(app, options = {})
16
16
  @app = app
17
17
  @config = Config.new(options)
18
+ @disk_path_option = options[:disk_path]
18
19
  @api_client = ApiClient.new(@config.base_url, debug: @config.debug)
19
20
  # Config (whitelist + flags + skipPaths) et render : via internalUrl pour éviter
20
21
  # le deadlock (appels serveur→serveur qui repasseraient par le middleware public).
@@ -22,11 +23,13 @@ module Shugoi
22
23
  @guard_cache = GuardCache.new(@api_client)
23
24
  @config_cache = ConfigCache.new(@api_internal)
24
25
  @token_signer = TokenSigner.new(@config.signing_secret)
25
- @html_store = HtmlStore.new(disk_path: options[:disk_path])
26
+ # multi_process → stockage disque du HTML (nécessaire en cluster PM2, parité
27
+ # enableDiskStore(true) du module Node).
28
+ @html_store = HtmlStore.new(disk_path: disk_path)
26
29
  @pow = Pow.new(@config.signing_secret, @config.pow_difficulty, @config.pow_ttl_ms)
27
30
  @skeleton = SkeletonGenerator.new(@config, @guard_cache, @config_cache, @token_signer)
28
31
  @render = RenderHandler.new(@config, @token_signer, @html_store, @config_cache, @pow)
29
- @core = Core.new(@config, @pow, @api_client)
32
+ @core = Core.new(@config, @pow, @api_client, @config_cache)
30
33
  @csp = Csp.build(site_key: @config.site_key, api_origin: Csp.origin_of(@config.base_url), extra_directives: @config.extra_directives, split_render: @config.split_render)
31
34
  end
32
35
 
@@ -46,6 +49,16 @@ module Shugoi
46
49
  return method_not_allowed
47
50
  end
48
51
 
52
+ # SkipPaths (SSR direct) : on laisse l'app servir la page sans challenge ni skeleton.
53
+ # Parité middleware.ts : le check est AVANT core.evaluate (un skipPath hors allowlist
54
+ # ne doit PAS passer par le challenge PoW ni la protection des assets).
55
+ if @config.auto_inject && @config.site_key
56
+ cfg = @config_cache.fetch(@config.site_key)
57
+ if cfg[:skip_paths].include?(path)
58
+ return @app.call(env)
59
+ end
60
+ end
61
+
49
62
  ctx = build_ctx(env, query)
50
63
  decision = @core.evaluate(ctx)
51
64
 
@@ -57,14 +70,6 @@ module Shugoi
57
70
  return [decision.status, h, [decision.body]]
58
71
  end
59
72
 
60
- # SkipPaths (SSR direct) : on laisse l'app servir la page sans challenge ni skeleton.
61
- if @config.auto_inject && @config.site_key
62
- cfg = @config_cache.fetch(@config.site_key)
63
- if cfg[:skip_paths].include?(path)
64
- return @app.call(env)
65
- end
66
- end
67
-
68
73
  status, resp_headers, body = @app.call(env)
69
74
 
70
75
  # Rack 3 exige des noms de headers en minuscules → on normalise.
@@ -186,6 +191,23 @@ module Shugoi
186
191
  def parse_query(qs)
187
192
  ::Rack::Utils.parse_nested_query(qs)
188
193
  end
194
+
195
+ # Chemin du stockage disque (parité enableDiskStore du module Node).
196
+ # multi_process → répertoire tmp partagé (cluster PM2) ; sinon l'option disk_path
197
+ # explicite éventuelle.
198
+ def disk_path
199
+ return @disk_path_option if @disk_path_option
200
+ return nil unless @config.multi_process
201
+
202
+ dir = File.join(Dir.tmpdir, "shugoi-render-#{Process.uid}")
203
+ begin
204
+ Dir.mkdir(dir, 0o700) unless Dir.exist?(dir)
205
+ File.chmod(0o700, dir)
206
+ rescue StandardError
207
+ return nil
208
+ end
209
+ dir
210
+ end
189
211
  end
190
212
  end
191
213
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shugoi
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shugoi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugoi