From 71bc56b0cd6e1f8195b3843e9e71d1bb2ec303d4 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Fri, 18 Sep 2026 19:07:47 +0300 Subject: [PATCH] Route cloud access through Caterium and vendor login SDK --- .gitattributes | 2 + .github/workflows/qa.yml | 1 + docs/release-manifest.json | 2 +- ops/timeweb/README.md | 30 +++++ ops/timeweb/api-proxy.php | 36 ++--- public/api/index.php | 185 ++++++++++++++++++++++++++ public/app-runtime.js | 94 +++++++------ public/core/cloud-transport.js | 32 +++-- public/core/performance.js | 2 +- public/index.html | 6 +- public/service-worker.js | 5 +- public/vendor/README.md | 12 ++ public/vendor/supabase-2.112.4.min.js | Bin 0 -> 212426 bytes public/vendor/supabase-LICENSE.txt | 21 +++ tests/backend-cutover.mjs | 3 + tests/login-recovery.spec.mjs | 80 ++++++++--- tests/release-check.mjs | 10 +- tests/static-security.mjs | 4 +- 18 files changed, 426 insertions(+), 99 deletions(-) create mode 100644 .gitattributes create mode 100644 public/api/index.php create mode 100644 public/vendor/README.md create mode 100644 public/vendor/supabase-2.112.4.min.js create mode 100644 public/vendor/supabase-LICENSE.txt diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c0d9f9a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Preserve the published SDK bytes, including whitespace inside template strings. +public/vendor/supabase-2.112.4.min.js -text -diff diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index a7260d4..cb93866 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -16,5 +16,6 @@ jobs: - run: npm ci - run: npm audit --audit-level=high - run: npm run check:deploy + - run: php -l public/api/index.php && php -l ops/timeweb/api-proxy.php - run: npx playwright install --with-deps chromium webkit - run: npm run test:e2e diff --git a/docs/release-manifest.json b/docs/release-manifest.json index 2f838b5..78319c6 100644 --- a/docs/release-manifest.json +++ b/docs/release-manifest.json @@ -11,7 +11,7 @@ "serverReady": true, "workspaceAutoDiscovery": true, "invitesTemporarilyDisabled": false, - "pwaCache": "v103-20260918-help-center", + "pwaCache": "v104-20260918-russia-proxy", "fullOfferDescriptions": true, "dynamicOfferRows": true, "pdfOfferDescriptionFix": true, diff --git a/ops/timeweb/README.md b/ops/timeweb/README.md index dd7dfac..b76b8f7 100644 --- a/ops/timeweb/README.md +++ b/ops/timeweb/README.md @@ -1,5 +1,35 @@ # Timeweb fallback auto-deploy +## Backend access without external browser dependencies + +The browser loads the pinned Supabase SDK from `public/vendor/`, then sends +Auth, REST, Storage and Functions traffic to the same-origin PHP entry point +`https://app.caterium.ru/api/index.php?__caterium_path=...`. +The query route is deliberate: nginx can serve static extensions before Apache, +so rewriting a storage URL ending in `.jpg` is insufficient on this hosting. + +`public/api/index.php` must be identical to `ops/timeweb/api-proxy.php`. +The app copy is deployed with the release; the dedicated API host copy is +installed separately at `/home/c/ci503744/public_html/api-proxy/index.php`. +Only `app.caterium.ru` and `api.caterium.ru` are accepted, with a fixed Supabase +upstream and the existing allowlist of backend paths. Browser authorization is +forwarded unchanged; RLS remains enforced. Responses are private/no-store, and +the service worker never caches `/api/`. + +Safe reads and password login may retry on `api.caterium.ru`. Direct Supabase +fallback is disabled. Writes and refresh-token exchanges are not replayed; +uncertain saves retain the existing read/revision recovery path. The Supabase +client still uses the project URL internally, preserving existing auth sessions. + +Updates use HTTP: a revision-only request every 20 seconds while visible, and +chat refresh every 10 seconds. Unchanged bases are not downloaded again. Requests +from the previous account are discarded. Typing/online indicators require a +future WebSocket-capable proxy and are not advertised by the HTTP mode. + +This covers application data and media. Optional map/geocoding providers remain +external and do not gate login or order loading. If the login page itself cannot +open, diagnose DNS/TLS/operator connectivity separately. + Use this only for the Year/shared-hosting fallback where system `crontab` is unavailable and scheduling is configured in the Timeweb panel. ## Production target: app.caterium.ru diff --git a/ops/timeweb/api-proxy.php b/ops/timeweb/api-proxy.php index 8190d05..11fb0e1 100644 --- a/ops/timeweb/api-proxy.php +++ b/ops/timeweb/api-proxy.php @@ -1,23 +1,26 @@ Supabase managed backend reverse proxy. + * Caterium same-origin /api and api.caterium.ru -> Supabase HTTP proxy. * Forwards only REST/Auth/Storage/Edge Functions HTTP traffic. - * Realtime/WebSocket is intentionally NOT proxied here (see ops/timeweb/README.md). + * Browser updates use authenticated HTTP polling; this is not a WebSocket proxy. * Upstream host is a fixed constant - never derived from request input (no open-proxy risk). */ const UPSTREAM = 'https://usfjwhztqoopzzfmfbis.supabase.co'; -const PUBLIC_BASE = 'https://api.caterium.ru'; -const SERVE_HOST = 'api.caterium.ru'; const ALLOWED_PREFIXES = ['/rest/v1/', '/auth/v1/', '/storage/v1/', '/functions/v1/']; -// Defense in depth: this script lives inside a document root shared with -// caterium.ru (2-site plan limit). The root .htaccess only rewrites into -// here for Host: api.caterium.ru, but a bare direct hit on this file's own -// path (under any host) must still refuse to act as a generic proxy. +// The same source is deployed to the dedicated API host and app /api/index.php. +// Never accept another Host or derive the upstream host from browser input. $requestHost = strtolower(explode(':', $_SERVER['HTTP_HOST'] ?? '')[0]); -if ($requestHost !== SERVE_HOST) { +$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; +$query = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_QUERY); +if ($requestHost === 'app.caterium.ru' && $path === '/api/index.php' && is_string($_GET['__caterium_path'] ?? null)) { + $path = $_GET['__caterium_path']; + $query = implode('&', array_filter(explode('&', $query ?? ''), static function ($part) { + return urldecode(explode('=', $part, 2)[0]) !== '__caterium_path'; + })); +} elseif ($requestHost !== 'api.caterium.ru') { http_response_code(404); exit; } @@ -34,7 +37,7 @@ const FORWARD_REQUEST_HEADERS = [ ]; const STRIP_RESPONSE_HEADERS = [ - 'transfer-encoding', 'connection', 'content-encoding', 'content-length', + 'transfer-encoding', 'connection', 'content-encoding', 'content-length', 'cache-control', 'expires', 'pragma', 'set-cookie', ]; function send_cors_headers(): void @@ -72,15 +75,14 @@ function request_headers(): array } send_cors_headers(); +header('Cache-Control: private, no-store'); +header('X-Content-Type-Options: nosniff'); if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') { http_response_code(204); exit; } -$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; -$query = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_QUERY); - $allowed = false; foreach (ALLOWED_PREFIXES as $prefix) { if (strpos($path, $prefix) === 0) { @@ -89,7 +91,7 @@ foreach (ALLOWED_PREFIXES as $prefix) { } } -if (!$allowed) { +if (!$allowed || strpos(rawurldecode($path), '..') !== false || preg_match('/[\\\\?#\x00-\x20]/', $path)) { http_response_code(404); header('Content-Type: application/json'); echo json_encode(['error' => 'not_found', 'message' => 'Path not proxied.']); @@ -125,6 +127,7 @@ curl_setopt_array($ch, [ CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_NOBODY => $method === 'HEAD', ]); if ($body !== null && $body !== '') { @@ -165,6 +168,9 @@ foreach (preg_split('/\r\n/', $rawHeaders) as $line) { if (in_array($headerName, STRIP_RESPONSE_HEADERS, true) || strpos($headerName, 'access-control-') === 0) { continue; } + // Backend HTTP endpoints do not require external redirects. Keep any + // upstream redirect on the dedicated API host, including signed downloads. + if ($headerName === 'location') $line = str_replace(UPSTREAM, 'https://api.caterium.ru', $line); header($line, false); } @@ -173,7 +179,7 @@ foreach (preg_split('/\r\n/', $rawHeaders) as $line) { // request (an , a download link, ...) is proxied too, not sent to // *.supabase.co directly. Only touch text/JSON bodies - never binary payloads. if (stripos($responseContentType, 'application/json') !== false || stripos($responseContentType, 'text/') !== false) { - $respBody = str_replace(UPSTREAM, PUBLIC_BASE, $respBody); + $respBody = str_replace(UPSTREAM, 'https://api.caterium.ru', $respBody); } echo $respBody; diff --git a/public/api/index.php b/public/api/index.php new file mode 100644 index 0000000..11fb0e1 --- /dev/null +++ b/public/api/index.php @@ -0,0 +1,185 @@ + Supabase HTTP proxy. + * Forwards only REST/Auth/Storage/Edge Functions HTTP traffic. + * Browser updates use authenticated HTTP polling; this is not a WebSocket proxy. + * Upstream host is a fixed constant - never derived from request input (no open-proxy risk). + */ + +const UPSTREAM = 'https://usfjwhztqoopzzfmfbis.supabase.co'; + +const ALLOWED_PREFIXES = ['/rest/v1/', '/auth/v1/', '/storage/v1/', '/functions/v1/']; + +// The same source is deployed to the dedicated API host and app /api/index.php. +// Never accept another Host or derive the upstream host from browser input. +$requestHost = strtolower(explode(':', $_SERVER['HTTP_HOST'] ?? '')[0]); +$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; +$query = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_QUERY); +if ($requestHost === 'app.caterium.ru' && $path === '/api/index.php' && is_string($_GET['__caterium_path'] ?? null)) { + $path = $_GET['__caterium_path']; + $query = implode('&', array_filter(explode('&', $query ?? ''), static function ($part) { + return urldecode(explode('=', $part, 2)[0]) !== '__caterium_path'; + })); +} elseif ($requestHost !== 'api.caterium.ru') { + http_response_code(404); + exit; +} + +const ALLOWED_ORIGINS = [ + 'https://app.caterium.ru', + 'https://caterium.ru', + 'https://www.caterium.ru', +]; + +const FORWARD_REQUEST_HEADERS = [ + 'authorization', 'apikey', 'content-type', 'prefer', 'range', + 'x-client-info', 'x-supabase-api-version', 'accept-profile', 'content-profile', 'x-upsert', 'cache-control', +]; + +const STRIP_RESPONSE_HEADERS = [ + 'transfer-encoding', 'connection', 'content-encoding', 'content-length', 'cache-control', 'expires', 'pragma', 'set-cookie', +]; + +function send_cors_headers(): void +{ + $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; + if (in_array($origin, ALLOWED_ORIGINS, true)) { + header('Access-Control-Allow-Origin: ' . $origin); + header('Vary: Origin'); + } + header('Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS'); + header('Access-Control-Allow-Headers: ' . implode(', ', FORWARD_REQUEST_HEADERS)); + header('Access-Control-Max-Age: 86400'); +} + +function request_headers(): array +{ + if (function_exists('getallheaders')) { + $raw = getallheaders(); + if (is_array($raw)) { + return $raw; + } + } + // Fallback for environments without getallheaders(). + $out = []; + foreach ($_SERVER as $key => $value) { + if (strpos($key, 'HTTP_') === 0) { + $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5))))); + $out[$name] = $value; + } + } + if (isset($_SERVER['CONTENT_TYPE'])) { + $out['Content-Type'] = $_SERVER['CONTENT_TYPE']; + } + return $out; +} + +send_cors_headers(); +header('Cache-Control: private, no-store'); +header('X-Content-Type-Options: nosniff'); + +if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') { + http_response_code(204); + exit; +} + +$allowed = false; +foreach (ALLOWED_PREFIXES as $prefix) { + if (strpos($path, $prefix) === 0) { + $allowed = true; + break; + } +} + +if (!$allowed || strpos(rawurldecode($path), '..') !== false || preg_match('/[\\\\?#\x00-\x20]/', $path)) { + http_response_code(404); + header('Content-Type: application/json'); + echo json_encode(['error' => 'not_found', 'message' => 'Path not proxied.']); + exit; +} + +$upstreamUrl = UPSTREAM . $path . ($query !== null && $query !== '' ? '?' . $query : ''); + +$incoming = request_headers(); +$incomingLower = []; +foreach ($incoming as $name => $value) { + $incomingLower[strtolower($name)] = $value; +} + +$forwardHeaders = []; +foreach (FORWARD_REQUEST_HEADERS as $name) { + if (isset($incomingLower[$name]) && $incomingLower[$name] !== '') { + $forwardHeaders[] = $name . ': ' . $incomingLower[$name]; + } +} + +$method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; +$body = ($method === 'GET' || $method === 'HEAD') ? null : file_get_contents('php://input'); + +$ch = curl_init($upstreamUrl); +curl_setopt_array($ch, [ + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_HTTPHEADER => $forwardHeaders, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_NOBODY => $method === 'HEAD', +]); + +if ($body !== null && $body !== '') { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); +} + +$response = curl_exec($ch); + +if ($response === false) { + http_response_code(502); + header('Content-Type: application/json'); + echo json_encode(['error' => 'upstream_unreachable']); + curl_close($ch); + exit; +} + +$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); +$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$rawHeaders = substr($response, 0, $headerSize); +$respBody = substr($response, $headerSize); +curl_close($ch); + +http_response_code($statusCode); + +$responseContentType = ''; +foreach (preg_split('/\r\n/', $rawHeaders) as $line) { + if ($line === '' || stripos($line, 'HTTP/') === 0) { + continue; + } + $colon = strpos($line, ':'); + if ($colon === false) { + continue; + } + $headerName = strtolower(trim(substr($line, 0, $colon))); + if ($headerName === 'content-type') { + $responseContentType = trim(substr($line, $colon + 1)); + } + if (in_array($headerName, STRIP_RESPONSE_HEADERS, true) || strpos($headerName, 'access-control-') === 0) { + continue; + } + // Backend HTTP endpoints do not require external redirects. Keep any + // upstream redirect on the dedicated API host, including signed downloads. + if ($headerName === 'location') $line = str_replace(UPSTREAM, 'https://api.caterium.ru', $line); + header($line, false); +} + +// Supabase returns absolute upstream URLs inside some JSON bodies (e.g. Storage +// createSignedUrl). Rewrite those to our public host so the browser's follow-up +// request (an , a download link, ...) is proxied too, not sent to +// *.supabase.co directly. Only touch text/JSON bodies - never binary payloads. +if (stripos($responseContentType, 'application/json') !== false || stripos($responseContentType, 'text/') !== false) { + $respBody = str_replace(UPSTREAM, 'https://api.caterium.ru', $respBody); +} + +echo $respBody; diff --git a/public/app-runtime.js b/public/app-runtime.js index 807a7a3..853be01 100644 --- a/public/app-runtime.js +++ b/public/app-runtime.js @@ -2158,7 +2158,7 @@ window.SUN_LEGACY_CATALOG_V175=[]; const DB_NAME = 'SunCloudV2'; const DB_VERSION = 1; const BUCKET = 'sun-media'; - const SUPABASE_JS = 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2.112.4'; + const SUPABASE_JS = '/vendor/supabase-2.112.4.min.js'; const MEDIA_PREFIX = '__sun_media__:'; const SYNC_DEBOUNCE = 1800; @@ -2175,7 +2175,7 @@ window.SUN_LEGACY_CATALOG_V175=[]; const DEFAULT_SUPABASE_KEY = 'sb_publishable_CAxfhMKrduJjuk_5ybCQLg_TqSGWGoy'; const SUPABASE_API_PROXY = 'https://api.caterium.ru'; const PROXY_FETCH_TIMEOUT_MS = 12000; - const supabaseProxyFetch=window.CateriumCloudTransport.create({upstream:DEFAULT_SUPABASE_URL,proxy:SUPABASE_API_PROXY,timeout:PROXY_FETCH_TIMEOUT_MS}); + const supabaseProxyFetch=window.CateriumCloudTransport.create({upstream:DEFAULT_SUPABASE_URL,proxy:location.origin+'/api/index.php',fallbackProxy:SUPABASE_API_PROXY,timeout:PROXY_FETCH_TIMEOUT_MS}); let config = loadConfig(); let client = null; @@ -2189,7 +2189,8 @@ window.SUN_LEGACY_CATALOG_V175=[]; let membershipsLoaded = false; let membershipError = ''; let membershipLoad = null; - let realtimeChannel = null; + let remotePollTimer = null; + let remotePollBusy = false; let syncTimer = null; let networkRetryTimer = null; let isSyncing = false; @@ -3353,14 +3354,27 @@ window.SUN_LEGACY_CATALOG_V175=[]; catch(error){handleError(error,'Не удалось создать приглашение.');} } - function unsubscribeRealtime(){if(realtimeChannel&&client){try{client.removeChannel(realtimeChannel);}catch(_){}}realtimeChannel=null;} + function unsubscribeRealtime(){clearInterval(remotePollTimer);remotePollTimer=null;} + async function pollRemoteChanges(){ + if(remotePollBusy||document.hidden||!navigator.onLine||isSyncing||supportMode||!workspaceMigrated()||!session?.user||!workspace?.id)return; + const startedUser=session.user.id,startedWorkspace=workspace.id,startedClient=client; + const current=()=>!signOutInProgress&&client===startedClient&&session?.user?.id===startedUser&&workspace?.id===startedWorkspace; + remotePollBusy=true; + try{ + // Poll only the revision; download/merge the full base only after a change. + // The same RLS and authorization apply as to the previous socket channel. + const {data,error}=await client.from('sun_app_state').select('revision').eq('workspace_id',startedWorkspace).maybeSingle(); + if(error)throw error; + const baseline=await getBaseline(); + if(current()&&data&&Number(data.revision)!==Number(baseline?.revision))await syncNow({quiet:true}); + }catch(_){/* A later poll retries; normal saves retain their visible error/retry UI. */} + finally{remotePollBusy=false;} + } function subscribeRealtime(){ unsubscribeRealtime();if(!client||!session||!workspace?.id)return; - realtimeChannel=client.channel(`sun-state-${workspace.id}-${clientId()}`).on('postgres_changes',{event:'INSERT',schema:'public',table:'sun_sync_events',filter:`workspace_id=eq.${workspace.id}`},async payload=>{ - if(payload?.new?.client_id===clientId())return; - try{const row=await fetchRemoteRow();if(row)await handleRealtimeRow(row);}catch(error){handleError(error,'Не удалось получить изменения с другого устройства.');} - }).subscribe(status=>{if(status==='CHANNEL_ERROR')setStatus('error','Realtime не подключился. Обычная синхронизация продолжит работать.');}); + remotePollTimer=setInterval(pollRemoteChanges,20000); } + document.addEventListener('visibilitychange',()=>{if(remotePollTimer&&!document.hidden)pollRemoteChanges();}); function handleError(error, message) { if(signOutInProgress)return; @@ -5513,15 +5527,12 @@ window.SUN_LEGACY_CATALOG_V175=[]; let messages = []; let activeThread = null; let workspaceChannel = null; - let threadChannel = null; let workspaceChannelId = ''; let threadChannelId = ''; let online = new Set(); let pendingFiles = []; let loadingThreads = false; let loadingMessages = false; - let typingStopTimer = null; - let lastTypingSent = 0; let typingUsers = new Map(); let sidebarMode = 'general'; let chatDock = 'global'; @@ -5643,7 +5654,7 @@ window.SUN_LEGACY_CATALOG_V175=[]; function activeSubtitle(t){ if(!t)return ''; if(t.kind==='direct'){const m=memberById(t.other_user_id);return online.has(String(t.other_user_id))?'онлайн':esc(ROLE_LABELS[m?.role]||'сотрудник');} - if(t.kind==='company'){const count=members.length,on=members.filter(m=>online.has(String(m.user_id))).length;return `${count} сотрудников · ${on} онлайн`;} + if(t.kind==='company')return `${members.length} сотрудников`; return 'Обсуждение заказа'; } @@ -5676,7 +5687,7 @@ window.SUN_LEGACY_CATALOG_V175=[]; async function hydrateAttachments(){ const c=client();if(!c)return; const links=qa('[data-chat-attachment]',$('sunChatBodyV29')); - for(const a of links){const path=a.dataset.chatAttachment;if(!path)continue;try{const r=await c.storage.from('sun-chat').createSignedUrl(path,3600);if(r.error)throw r.error;const url=r.data?.signedUrl;if(!url)continue;a.href=url;const type=a.dataset.chatType||'';if(type.startsWith('image/')){const p=a.querySelector('.sun-chat-file-preview');if(p)p.innerHTML=`Вложение`;}}catch(e){a.onclick=ev=>{ev.preventDefault();toast('Не удалось открыть вложение.','error')}}} + for(const a of links){const path=a.dataset.chatAttachment;if(!path)continue;try{const r=await c.storage.from('sun-chat').createSignedUrl(path,3600);if(r.error)throw r.error;const url=window.CateriumCloudTransport.mediaUrl(r.data?.signedUrl);if(!url)continue;a.href=url;const type=a.dataset.chatType||'';if(type.startsWith('image/')){const p=a.querySelector('.sun-chat-file-preview');if(p)p.innerHTML=`Вложение`;}}catch(e){a.onclick=ev=>{ev.preventDefault();toast('Не удалось открыть вложение.','error')}}} } function renderTyping(){const root=$('sunChatTypingV29');if(!root)return;const names=[...typingUsers.values()].filter(x=>x&&x.user_id!==me()&&Date.now()-x.at<3500).map(x=>x.name||'Сотрудник');root.textContent=names.length?`${names.slice(0,2).join(', ')} ${names.length>1?'печатают':'печатает'}…`:'';} @@ -5685,10 +5696,14 @@ window.SUN_LEGACY_CATALOG_V175=[]; async function loadThreads({quiet=false}={}){ if(!available()||loadingThreads)return; + const startedWorkspace=workspace().id,startedUser=me(); + const current=()=>available()&&workspace().id===startedWorkspace&&me()===startedUser; loadingThreads=true; try{ const ws=workspace();await rpc('sun_chat_get_company_thread_v29',{p_workspace:ws.id}); + if(!current())return; const [t,m]=await Promise.all([rpc('sun_chat_list_threads_v29',{p_workspace:ws.id}),rpc('sun_chat_list_members_v29',{p_workspace:ws.id})]); + if(!current())return; threads=Array.isArray(t)?t:[];members=Array.isArray(m)?m:[]; if(activeThread){const fresh=threadById(activeThread.thread_id);if(fresh)activeThread={...activeThread,...fresh};} renderSidebar();renderHead();updateUnreadBadges(); @@ -5697,11 +5712,16 @@ window.SUN_LEGACY_CATALOG_V175=[]; async function loadMessages({mark=true,quiet=false}={}){ if(!activeThread||!available()||loadingMessages)return; - loadingMessages=true;renderMessages(); + const startedThread=activeThread.thread_id,startedWorkspace=workspace().id,startedUser=me(); + const current=()=>available()&&workspace().id===startedWorkspace&&me()===startedUser&&activeThread?.thread_id===startedThread; + let changed=!quiet; + loadingMessages=true;if(!quiet)renderMessages(); try{ - const data=await rpc('sun_chat_list_messages_v29',{p_thread:activeThread.thread_id,p_limit:120,p_before:null});messages=Array.isArray(data)?data:[]; + const data=await rpc('sun_chat_list_messages_v29',{p_thread:startedThread,p_limit:120,p_before:null}); + if(!current())return; + const next=Array.isArray(data)?data:[];changed=changed||JSON.stringify(next)!==JSON.stringify(messages);messages=next; if(mark){const t=threadById(activeThread.thread_id);if(Number(t?.unread_count||0)>0){await rpc('sun_chat_mark_read_v29',{p_thread:activeThread.thread_id});await loadThreads({quiet:true});}} - }catch(e){if(!quiet)toast(e.message||String(e),'error',6500)}finally{loadingMessages=false;renderMessages();} + }catch(e){if(!quiet&¤t())toast(e.message||String(e),'error',6500)}finally{loadingMessages=false;if(current()&&changed)renderMessages();} } async function openThread(t){ @@ -5776,34 +5796,26 @@ window.SUN_LEGACY_CATALOG_V175=[]; } async function ensureWorkspaceChannel(){ - if(!available())return unsubscribeWorkspace();const c=client(),ws=workspace(),ss=session();if(!c||!ws||!ss)return; - if(workspaceChannel&&workspaceChannelId===ws.id)return; - unsubscribeWorkspace();workspaceChannelId=ws.id; - try{if(c.realtime?.setAuth)await c.realtime.setAuth(ss.access_token);}catch(_){} - const ch=c.channel(`sun-chat-workspace:${ws.id}`,{config:{private:true,presence:{key:me()},broadcast:{ack:true}}});workspaceChannel=ch; - ch.on('broadcast',{event:'chat_changed'},async()=>{await loadThreads({quiet:true});if(activeThread){const fresh=threadById(activeThread.thread_id);if(fresh&&Number(fresh.unread_count||0)>0)await loadMessages({mark:true,quiet:true});else renderMessages();}}) - .on('presence',{event:'sync'},()=>{online=new Set(Object.keys(ch.presenceState()||{}));renderSidebar();renderHead();}) - .on('presence',{event:'join'},()=>{online=new Set(Object.keys(ch.presenceState()||{}));renderSidebar();renderHead();}) - .on('presence',{event:'leave'},()=>{online=new Set(Object.keys(ch.presenceState()||{}));renderSidebar();renderHead();}) - .subscribe(async(status)=>{if(status==='SUBSCRIBED'){try{await ch.track({user_id:me(),name:meName(),at:new Date().toISOString()})}catch(_){}await loadThreads({quiet:true});}}); + if(!available())return unsubscribeWorkspace(); + const identity=workspace().id+':'+me(); + if(workspaceChannel&&workspaceChannelId===identity)return; + if(workspaceChannelId&&workspaceChannelId!==identity){activeThread=null;threads=[];members=[];messages=[];pendingFiles=[];renderMessages();renderPending();} + unsubscribeWorkspace();workspaceChannelId=identity; + workspaceChannel=setInterval(async()=>{ + if(document.hidden||!navigator.onLine||!available()||workspaceChannelId!==identity)return; + await loadThreads({quiet:true}); + if(workspaceChannelId!==identity)return; + const main=$('sunChatMainV29'); + if(activeThread&&main?.getClientRects().length)await loadMessages({mark:true,quiet:true}); + },10000); } - function unsubscribeWorkspace(){if(workspaceChannel){try{client()?.removeChannel?.(workspaceChannel)}catch(_){}workspaceChannel=null;}workspaceChannelId='';online=new Set();unsubscribeThread();} + function unsubscribeWorkspace(){clearInterval(workspaceChannel);workspaceChannel=null;workspaceChannelId='';online=new Set();unsubscribeThread();} async function subscribeThread(id){ - if(!available()||!id)return;const c=client(),ss=session();if(threadChannel&&threadChannelId===id)return;unsubscribeThread();threadChannelId=id; - try{if(c.realtime?.setAuth)await c.realtime.setAuth(ss.access_token);}catch(_){} - const ch=c.channel(`sun-chat-thread:${id}`,{config:{private:true,broadcast:{ack:false,self:false}}});threadChannel=ch; - ch.on('broadcast',{event:'message'},()=>loadMessages({mark:true,quiet:true})) - .on('broadcast',{event:'read'},()=>loadMessages({mark:false,quiet:true})) - .on('broadcast',{event:'typing'},payload=>{const p=payload?.payload||{};if(!p.user_id||String(p.user_id)===String(me()))return;if(p.typing)typingUsers.set(String(p.user_id),{user_id:p.user_id,name:p.name||'Сотрудник',at:Date.now()});else typingUsers.delete(String(p.user_id));renderTyping();setTimeout(()=>renderTyping(),3600)}) - .subscribe(); - } - function unsubscribeThread(){if(threadChannel){try{client()?.removeChannel?.(threadChannel)}catch(_){}threadChannel=null;}threadChannelId='';typingUsers.clear();renderTyping();} - async function sendTyping(flag){ - if(!threadChannel||!activeThread)return;const now=Date.now();if(flag&&now-lastTypingSent<700){clearTimeout(typingStopTimer);typingStopTimer=setTimeout(()=>sendTyping(false),1800);return}lastTypingSent=now; - try{await threadChannel.send({type:'broadcast',event:'typing',payload:{user_id:me(),name:meName(),typing:Boolean(flag)}})}catch(_){} - clearTimeout(typingStopTimer);if(flag)typingStopTimer=setTimeout(()=>sendTyping(false),1800); + if(!available()||!id)return;unsubscribeThread();threadChannelId=id; } + function unsubscribeThread(){threadChannelId='';typingUsers.clear();renderTyping();} + function sendTyping(){/* Typing/presence require a WebSocket-capable proxy. */} function updateOrderChatBadge(){ const b=$('sunOrderChatTabV31');if(!b)return;const id=currentOrderId(),t=id?orderThreadByOrder(id):null,n=Number(t?.unread_count||0);let badge=b.querySelector('.sun-chat-mini-badge');if(n>0){if(!badge){badge=document.createElement('span');badge.className='sun-chat-mini-badge';b.appendChild(badge);}badge.textContent=n>99?'99+':String(n);}else badge?.remove(); @@ -5851,7 +5863,7 @@ window.SUN_LEGACY_CATALOG_V175=[]; window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(safeStateChanged,60)); window.addEventListener('sun:cloud-state-applied',()=>setTimeout(safeStateChanged,60)); window.addEventListener('online',()=>{if(available())ensureWorkspaceChannel()}); - window.addEventListener('beforeunload',()=>{try{workspaceChannel?.untrack?.()}catch(_){}}); + window.addEventListener('beforeunload',unsubscribeWorkspace); let tries=0;bootRetryTimer=setInterval(()=>{tries++;safeStateChanged();if(available()||tries>=24){clearInterval(bootRetryTimer);bootRetryTimer=null;}},750);setTimeout(safeStateChanged,250); } diff --git a/public/core/cloud-transport.js b/public/core/cloud-transport.js index 50c7183..b5c7c19 100644 --- a/public/core/cloud-transport.js +++ b/public/core/cloud-transport.js @@ -3,9 +3,21 @@ const READ_RPCS=new Set(['sun_my_workspaces','sun_fetch_app_state','sun_is_platform_admin','caterium_trial_demo_status']); const isTransient=error=>/TimeoutError|AbortError|CATERIUM_TIMEOUT|Failed to fetch|fetch failed|NetworkError|Load failed|network request failed|превышено время ожидания/i.test(String(error?.message||error||'')); const errorMessage=error=>isTransient(error)?'Сервер временно не отвечает. Изменения остаются на этом устройстве. Проверьте соединение и повторите загрузку.':String(error?.message||error||'Неизвестная ошибка'); - function create({upstream,proxy,timeout=12000,fallbackTimeout=15000,writeTimeout=35000,cooldown=60000}){ + function routeUrl(base,url){ + // Explicit PHP entry point also works for storage paths ending in .jpg: + // Timeweb serves static extensions before Apache rewrite rules. + return base.endsWith('.php')?base+'?__caterium_path='+encodeURIComponent(url.pathname)+(url.search?'&'+url.search.slice(1):''):base+url.pathname+url.search; + } + function mediaUrl(value){ + if(!value)return value; + const url=new URL(value,location.href); + if(['https://usfjwhztqoopzzfmfbis.supabase.co','https://api.caterium.ru'].includes(url.origin)&&url.pathname.startsWith('/storage/v1/'))return routeUrl(location.origin+'/api/index.php',url); + return value; + } + function create({upstream,proxy,fallbackProxy=proxy,timeout=12000,fallbackTimeout=15000,writeTimeout=35000,cooldown=60000}){ const origin=new URL(upstream).origin; - let directUntil=0; + proxy=proxy.replace(/\/$/,'');fallbackProxy=fallbackProxy.replace(/\/$/,''); + let fallbackUntil=0; return async function(input,init={}){ const original=new Request(input,init),url=new URL(original.url),isBackend=url.origin===origin; // A Request used as RequestInit exposes its ReadableStream body. Safari @@ -14,7 +26,7 @@ const requestInit={method:original.method,headers:original.headers,body,credentials:original.credentials,mode:original.mode,cache:original.cache,redirect:original.redirect,referrer:original.referrer,referrerPolicy:original.referrerPolicy,integrity:original.integrity,keepalive:original.keepalive}; const read=original.method==='GET'||original.method==='HEAD'||(original.method==='POST'&&url.pathname.startsWith('/rest/v1/rpc/')&&READ_RPCS.has(url.pathname.slice('/rest/v1/rpc/'.length))); const passwordLogin=original.method==='POST'&&url.pathname==='/auth/v1/token'&&url.searchParams.get('grant_type')==='password'; - const safeFallback=isBackend&&(read||passwordLogin); + const safeFallback=isBackend&&proxy!==fallbackProxy&&(read||passwordLogin); const expectJson=original.method!=='HEAD'&&(passwordLogin||url.pathname.startsWith('/rest/v1/rpc/')||(read&&(url.pathname.startsWith('/rest/v1/')||url.pathname.startsWith('/auth/v1/')))); async function attempt(target,limit){ const controller=new AbortController(),abort=()=>controller.abort(original.signal.reason); @@ -31,18 +43,18 @@ return response; }finally{clearTimeout(timer);original.signal.removeEventListener('abort',abort)} } - const directFirst=isBackend&&Date.now(){ 'use strict'; const VERSION='17.7.3'; - const RELEASE='20260918-help-center'; + const RELEASE='20260918-russia-proxy'; const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}}; function installAuthBoot(){ diff --git a/public/index.html b/public/index.html index 5852d56..457b0f2 100644 --- a/public/index.html +++ b/public/index.html @@ -1,4 +1,4 @@ - - + diff --git a/public/service-worker.js b/public/service-worker.js index dbc92e6..9f4f081 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,6 +1,7 @@ -const CACHE='sun-catering-pwa-v103-20260918-help-center'; -const VERSION='20260918-help-center'; +const CACHE='sun-catering-pwa-v104-20260918-russia-proxy'; +const VERSION='20260918-russia-proxy'; const CORE=[ + './vendor/supabase-2.112.4.min.js', `./core/help-center.js?v=${VERSION}`,`./core/help-center.css?v=${VERSION}`,`./help/knowledge-v1.json?v=${VERSION}`, './','./index.html',`./core/mobile-order.js?v=${VERSION}`,`./core/proposal-layout.js?v=${VERSION}`,'./fonts/Manrope.ttf','./fonts/PlayfairDisplay.ttf','./fonts/PlayfairDisplay-Italic.ttf',`./core/trial-demo.js?v=${VERSION}`,`./core/cloud-transport.js?v=${VERSION}`,`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`, `./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/login-signature-v1776.css?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`, diff --git a/public/vendor/README.md b/public/vendor/README.md new file mode 100644 index 0000000..6e8d148 --- /dev/null +++ b/public/vendor/README.md @@ -0,0 +1,12 @@ +# Supabase browser SDK + +`supabase-2.112.4.min.js` is the unmodified `dist/umd/supabase.js` from +the official npm package `@supabase/supabase-js@2.112.4`. + +Source: https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.112.4.tgz + +SHA-256: `f8ce7fab799af1916019cbd0b485b39bb80dbdbc6dc062909a751c9e5198e04c` + +MIT license: `supabase-LICENSE.txt`, from the upstream Supabase repository. +Hosting the SDK here keeps login independent of external CDNs. Keep this +version aligned with the runtime loader, PWA assets and browser tests. diff --git a/public/vendor/supabase-2.112.4.min.js b/public/vendor/supabase-2.112.4.min.js new file mode 100644 index 0000000000000000000000000000000000000000..3783bf2a2da54cfe6613c8d09b2455cbbac5d6a9 GIT binary patch literal 212426 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 literal 0 HcmV?d00001 diff --git a/public/vendor/supabase-LICENSE.txt b/public/vendor/supabase-LICENSE.txt new file mode 100644 index 0000000..ddeba6a --- /dev/null +++ b/public/vendor/supabase-LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Supabase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/backend-cutover.mjs b/tests/backend-cutover.mjs index f374b21..9c76d8a 100644 --- a/tests/backend-cutover.mjs +++ b/tests/backend-cutover.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import vm from 'node:vm'; import assert from 'node:assert/strict'; +import {createHash} from 'node:crypto'; const source=fs.readFileSync('public/app-runtime.js','utf8'); const code=source.slice(source.indexOf(' function loadConfig() {'),source.indexOf(' function saveConfig() {')); const url='https://usfjwhztqoopzzfmfbis.supabase.co',key='sb_publishable_CAxfhMKrduJjuk_5ybCQLg_TqSGWGoy'; @@ -8,4 +9,6 @@ const run=config=>{const storage=new Map([['sunCloudV2Config',JSON.stringify(con for(const retired of ['https://cksuehzcimitsxmeloes.supabase.co','https://api.caterium.ru']){const {result,storage}=run({url:retired,key:'old-key',workspaceId:'old-company',autoSync:false});assert.equal(result.url,url);assert.equal(result.key,key);assert.equal(result.workspaceId,'');assert.equal(result.legacyLocalWorkspaceId,'old-company');assert.equal(result.autoSync,false);assert.equal(storage.get('sunOrders'),'[{"id":"old-order"}]');assert.ok(storage.has('cateriumRetiredBackend20260917'));} const {result}=run({url,key,workspaceId:'new-company',localWorkspaceId:'new-company',tenantStorageReady:true});assert.equal(result.workspaceId,'new-company');assert.equal(result.localWorkspaceId,'new-company'); assert.equal(run({}).result.url,url); +assert.equal(fs.readFileSync('public/api/index.php','utf8').replaceAll('\r\n','\n'),fs.readFileSync('ops/timeweb/api-proxy.php','utf8').replaceAll('\r\n','\n'),'both deployed proxy entry points use the same reviewed source'); +assert.equal(createHash('sha256').update(fs.readFileSync('public/vendor/supabase-2.112.4.min.js','utf8').replaceAll('\r\n','\n')).digest('hex'),'f8ce7fab799af1916019cbd0b485b39bb80dbdbc6dc062909a751c9e5198e04c','vendored SDK matches the published package'); console.log('PASS backend cutover: retired config upgraded, old local data preserved separately, fresh config stable'); diff --git a/tests/login-recovery.spec.mjs b/tests/login-recovery.spec.mjs index 4af126b..1e7da9f 100644 --- a/tests/login-recovery.spec.mjs +++ b/tests/login-recovery.spec.mjs @@ -1,6 +1,42 @@ import fs from 'node:fs'; import {test,expect} from '@playwright/test'; +test('same-origin proxy preserves encoded storage paths, tokens, filters and never falls back to Supabase',async({page})=>{ + await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:''})); + await page.goto('/index.html');await page.addScriptTag({url:'/core/cloud-transport.js'}); + const result=await page.evaluate(async()=>{ + const calls=[],upstream='https://usfjwhztqoopzzfmfbis.supabase.co',proxy=location.origin+'/api/index.php'; + const send=CateriumCloudTransport.create({upstream,proxy,fallbackProxy:'https://api.caterium.ru'}); + window.fetch=async request=>{calls.push(request.url);return new Response('unavailable',{status:503})}; + await send(upstream+'/rest/v1/sun_app_state?select=revision&workspace_id=eq.company'); + const path='/storage/v1/object/sign/sun-chat/company/photo%20one.jpg',query='?token=a%2Bb%2Fc%3D&download=photo.jpg'; + const media=CateriumCloudTransport.mediaUrl(upstream+path+query),url=new URL(media); + return {calls,media,pathname:url.pathname,path:url.searchParams.get('__caterium_path'),token:url.searchParams.get('token'),download:url.searchParams.get('download'),external:CateriumCloudTransport.mediaUrl('https://example.invalid/image.jpg')}; + }); + expect(result.calls).toHaveLength(2);expect(result.calls.every(u=>!u.includes('.supabase.co'))).toBe(true); + expect(new URL(result.calls[0]).searchParams.get('workspace_id')).toBe('eq.company'); + expect(result.pathname).toBe('/api/index.php');expect(result.path).toBe('/storage/v1/object/sign/sun-chat/company/photo%20one.jpg');expect(result.token).toBe('a+b/c=');expect(result.download).toBe('photo.jpg'); + expect(result.external).toBe('https://example.invalid/image.jpg'); +}); + +test('revision polling reads only changed bases and ignores the previous account response',async({page})=>{ + await syncHarness(page); + const result=await page.evaluate(async()=>{ + let revision=1,readCount=0,resolveRevision;const calls=[]; + const payload={format:'sun-cloud-v2',version:2,storage:{sunOrders:{t:'j',v:[]}}}; + const query={select(fields){calls.push(fields);return this},eq(key,value){calls.push([key,value]);return this},async maybeSingle(){readCount++;return revision===99?new Promise(r=>resolveRevision=r):{data:{revision}}}}; + const c={from(table){calls.push(table);return query},async rpc(name){calls.push(name);return {data:[{revision,payload}]}}}; + SunCloudV2.testInit(c);await SunCloudV2.testBaseline({revision:1,payload}); + await SunCloudV2.testPoll();const unchanged=calls.splice(0); + revision=2;await SunCloudV2.testPoll();const changed=calls.splice(0); + revision=99;const pending=SunCloudV2.testPoll();await SunCloudV2.testPoll();SunCloudV2.testLeave();resolveRevision({data:{revision:99}});await pending; + return {unchanged,changed,late:calls,readCount}; + }); + expect(result.unchanged).toEqual(['sun_app_state','revision',['workspace_id','company']]); + expect(result.changed).toContain('sun_fetch_app_state'); + expect(result.late).not.toContain('sun_fetch_app_state');expect(result.readCount).toBe(3); +}); + test('Safari without streaming uploads can send login and binary bodies through fallback',async({page})=>{ await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:''})); await page.goto('/index.html');await page.addScriptTag({url:'/core/cloud-transport.js'}); @@ -9,7 +45,7 @@ test('Safari without streaming uploads can send login and binary bodies through window.Request=new Proxy(NativeRequest,{construct(Target,args){if(args[1]?.body instanceof ReadableStream)throw new TypeError('ReadableStream uploading is not supported');return new Target(...args)}}); const calls=[]; window.fetch=async request=>{calls.push({url:request.url,body:[...new Uint8Array(await request.arrayBuffer())],type:request.headers.get('content-type')});return request.url.includes('proxy.example')?new Response('',{status:503}):new Response('{}',{headers:{'content-type':'application/json'}})}; - const send=CateriumCloudTransport.create({upstream:'https://backend.example',proxy:'https://proxy.example',cooldown:0}); + const send=CateriumCloudTransport.create({upstream:'https://backend.example',proxy:'https://proxy.example',fallbackProxy:'https://secondary.example',cooldown:0}); const body=JSON.stringify({email:'test@example.invalid',password:'test-password'}); await send('https://backend.example/auth/v1/token?grant_type=password',{method:'POST',headers:{'content-type':'application/json'},body}); await send('https://backend.example/storage/v1/object/test/image',{method:'POST',headers:{'content-type':'image/png'},body:new Uint8Array([0,255,137,80]).buffer}); @@ -69,20 +105,20 @@ test('slow connections use a healthy route, allow longer saves and preserve call const upstream='https://backend.example.invalid',proxy='https://proxy.example.invalid',calls=[]; const ok=()=>new Response('{}',{headers:{'content-type':'application/json'}}); window.fetch=(request,{signal})=>{calls.push(request.url);return request.url.startsWith(proxy)?new Promise((_,reject)=>signal.addEventListener('abort',()=>reject(signal.reason),{once:true})):Promise.resolve(ok())}; - const send=CateriumCloudTransport.create({upstream,proxy,timeout:10,fallbackTimeout:100,writeTimeout:100}); + const send=CateriumCloudTransport.create({upstream,proxy,fallbackProxy:'https://secondary.example.invalid',timeout:10,fallbackTimeout:100,writeTimeout:100}); await send(upstream+'/rest/v1/rpc/sun_my_workspaces',{method:'POST',body:'{}'}); await send(upstream+'/rest/v1/rpc/sun_fetch_app_state',{method:'POST',body:'{}'}); const routes=calls.splice(0); window.fetch=(request,{signal})=>{calls.push(request.url);return new Promise((resolve,reject)=>{const timer=setTimeout(()=>resolve(ok()),35);signal.addEventListener('abort',()=>{clearTimeout(timer);reject(signal.reason)},{once:true})})}; - const slowSave=CateriumCloudTransport.create({upstream,proxy,timeout:10,writeTimeout:100}); + const slowSave=CateriumCloudTransport.create({upstream,proxy,fallbackProxy:'https://secondary.example.invalid',timeout:10,writeTimeout:100}); const saved=(await slowSave(upstream+'/rest/v1/rpc/sun_save_app_state_v17',{method:'POST',body:'{}'})).status;const saveCalls=calls.splice(0); - const timeoutSend=CateriumCloudTransport.create({upstream,proxy,writeTimeout:5});let timeoutName=''; + const timeoutSend=CateriumCloudTransport.create({upstream,proxy,fallbackProxy:'https://secondary.example.invalid',writeTimeout:5});let timeoutName=''; try{await timeoutSend(upstream+'/rest/v1/rpc/sun_save_app_state_v17',{method:'POST',body:'{}'})}catch(e){timeoutName=e.name}const timeoutCalls=calls.splice(0); const controller=new AbortController();controller.abort(new DOMException('Account changed','AbortError'));let cancel=''; try{await send(upstream+'/rest/v1/rpc/sun_my_workspaces',{method:'POST',body:'{}',signal:controller.signal})}catch(e){cancel=e.message} return {routes,saved,saveCalls,timeoutName,timeoutCalls,cancel,cancelCalls:calls}; }); - expect(result.routes.map(u=>new URL(u).host)).toEqual(['proxy.example.invalid','backend.example.invalid','backend.example.invalid']); + expect(result.routes.map(u=>new URL(u).host)).toEqual(['proxy.example.invalid','secondary.example.invalid','secondary.example.invalid']); expect(result.saved).toBe(200);expect(result.saveCalls).toHaveLength(1);expect(result.timeoutName).toBe('TimeoutError');expect(result.timeoutCalls).toHaveLength(1); expect(result.cancel).toBe('Account changed');expect(result.cancelCalls).toHaveLength(0); }); @@ -91,7 +127,7 @@ async function syncHarness(page){ await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:''}));await page.goto('/index.html'); await page.addScriptTag({url:'/core/sun-safe.js'});await page.addScriptTag({url:'/core/cloud-transport.js'}); const runtime=fs.readFileSync('public/app-runtime.js','utf8');let source=runtime.slice(runtime.indexOf('/* ===== MODULE: cloud-sync-v2.js'),runtime.indexOf('/* ===== MODULE: admin-rbac-v3.js')); - source=source.replace('async function boot(){','async function boot(){return;').replace('window.SunCloudV2={',`window.SunCloudV2={testUploadDataUrl:uploadDataUrl,testInit:c=>{client=c;session={user:{id:'test-user'}};workspace={id:'company',role:'admin'};config.migrated.company=true;config.tenantStorageReady=true;config.localWorkspaceId='company';config.workspaceId='company'},testBaseline:setBaseline,testLeave:()=>{session=null;workspace=null},`); + source=source.replace('async function boot(){','async function boot(){return;').replace('window.SunCloudV2={',`window.SunCloudV2={testPoll:pollRemoteChanges,testUploadDataUrl:uploadDataUrl,testInit:c=>{client=c;session={user:{id:'test-user'}};workspace={id:'company',role:'admin'};config.migrated.company=true;config.tenantStorageReady=true;config.localWorkspaceId='company';config.workspaceId='company'},testBaseline:setBaseline,testLeave:()=>{session=null;workspace=null},`); await page.addScriptTag({content:'var orders=[],boxes=[];'+source}); } @@ -139,7 +175,7 @@ test('cloud reads and password login recover from empty proxy responses without const result=await page.evaluate(async()=>{ const calls=[],upstream='https://backend.example.invalid',proxy='https://proxy.example.invalid';let scenario='read'; window.fetch=async request=>{calls.push({url:request.url,body:await request.text(),auth:request.headers.get('authorization')});if(scenario==='denied')return new Response('{"error":"denied"}',{status:401});if(scenario==='write')return new Response('',{status:503});return request.url.startsWith(proxy)?new Response('',{headers:{'content-type':'text/html'}}):new Response('[{"id":"company"}]',{headers:{'content-type':'application/json'}})}; - const send=window.CateriumCloudTransport.create({upstream,proxy,cooldown:0}); + const send=window.CateriumCloudTransport.create({upstream,proxy,fallbackProxy:'https://secondary.example.invalid',cooldown:0}); const read=await (await send(upstream+'/rest/v1/rpc/sun_my_workspaces',{method:'POST',headers:{Authorization:'Bearer test-token'},body:'{}'})).json(); const readCalls=calls.splice(0); await send(upstream+'/auth/v1/token?grant_type=password',{method:'POST',body:'{"email":"test@example.invalid","password":"test"}'});const authCalls=calls.splice(0); @@ -148,7 +184,7 @@ test('cloud reads and password login recover from empty proxy responses without scenario='denied';const denied=await send(upstream+'/auth/v1/token?grant_type=password',{method:'POST',body:'{}'});const deniedCalls=calls.splice(0); return {read,readCalls,authCalls,write:write.status,writeCalls,writeError,emptyWriteCalls,denied:denied.status,deniedCalls}; }); - expect(result.read).toEqual([{id:'company'}]);expect(result.readCalls.map(c=>c.url)).toEqual(['https://proxy.example.invalid/rest/v1/rpc/sun_my_workspaces','https://backend.example.invalid/rest/v1/rpc/sun_my_workspaces']); + expect(result.read).toEqual([{id:'company'}]);expect(result.readCalls.map(c=>c.url)).toEqual(['https://proxy.example.invalid/rest/v1/rpc/sun_my_workspaces','https://secondary.example.invalid/rest/v1/rpc/sun_my_workspaces']); expect(result.readCalls.every(c=>c.auth==='Bearer test-token'&&c.body==='{}')).toBe(true); expect(result.authCalls).toHaveLength(2);expect(result.authCalls[0].body).toBe(result.authCalls[1].body); expect(result.write).toBe(503);expect(result.writeCalls).toHaveLength(1);expect(result.writeError).toContain('пустой');expect(result.emptyWriteCalls).toHaveLength(1); @@ -186,29 +222,35 @@ test('a failed company load replaces the stale login form with an actionable ret await expect(page.getByRole('button',{name:'Повторить загрузку',exact:true})).toBeEnabled();await expect(page.locator('body > header')).toBeHidden(); }); -test('real SDK login opens the ordinary app when the proxy returns empty successful responses',async({page})=>{ +for(const primaryFails of [false,true])test('real SDK loads orders with foreign services blocked, primary failure='+primaryFails,async({page})=>{ const userId='11111111-1111-4111-8111-111111111111',workspaceId='22222222-2222-4222-8222-222222222222',expires=Math.floor(Date.now()/1000)+3600; const user={id:userId,aud:'authenticated',role:'authenticated',email:'test@example.invalid',email_confirmed_at:new Date().toISOString(),app_metadata:{provider:'email'},user_metadata:{}}; const token=[{alg:'HS256',typ:'JWT'},{sub:userId,role:'authenticated',aud:'authenticated',exp:expires,iat:expires-3600,aal:'aal1'},'test'].map(x=>typeof x==='string'?x:Buffer.from(JSON.stringify(x)).toString('base64url')).join('.'); - const seen=[],warnings=[];page.on('console',m=>{if(m.type()==='warning')warnings.push(m.text())}); + const seen=[],foreign=[],sockets=[],warnings=[]; + page.on('console',m=>{if(m.type()==='warning')warnings.push(m.text())});page.on('websocket',ws=>sockets.push(ws.url())); + await page.route('https://**',r=>{foreign.push(r.request().url());return r.abort()}); const handle=async route=>{ - const request=route.request(),url=new URL(request.url());seen.push(url.host+url.pathname); + const request=route.request(),url=new URL(request.url()),path=url.searchParams.get('__caterium_path')||url.pathname;seen.push({host:url.host,path,method:request.method()}); const headers={'access-control-allow-origin':'*'}; if(request.method()==='OPTIONS')return route.fulfill({status:204,headers}); - if(url.host==='api.caterium.ru')return route.fulfill({status:200,contentType:'text/html',body:'',headers}); + if(primaryFails&&url.pathname==='/api/index.php')return route.fulfill({status:200,contentType:'text/html',body:'',headers}); let body=null; - if(url.pathname==='/auth/v1/token')body={access_token:token,refresh_token:'test-refresh',token_type:'bearer',expires_in:3600,expires_at:expires,user}; - else if(url.pathname==='/auth/v1/user')body=user; - else if(url.pathname.endsWith('/sun_my_workspaces'))body=[{id:workspaceId,name:'Test workspace',role:'admin',is_active:true,permissions:{}}]; - else if(url.pathname.endsWith('/sun_is_platform_admin'))body=true; + if(path==='/auth/v1/token')body={access_token:token,refresh_token:'test-refresh',token_type:'bearer',expires_in:3600,expires_at:expires,user}; + else if(path==='/auth/v1/user')body=user; + else if(path.endsWith('/sun_my_workspaces'))body=[{id:workspaceId,name:'Test workspace',role:'admin',is_active:true,permissions:{}}]; + else if(path.endsWith('/sun_is_platform_admin'))body=false; + else if(path.endsWith('/sun_fetch_app_state'))body=[{revision:1,payload:{format:'sun-cloud-v2',version:2,storage:{sunOrders:{t:'j',v:[{id:'proxy-order',event:'Заказ без VPN',contact:'Тестовый клиент',phone:'79990000000',address:'Тестовый адрес',total:2400,lines:[]}]},sunBoxes:{t:'j',v:[]}}}}]; + else if(path.endsWith('/sun_app_state'))body={revision:1}; return route.fulfill({status:200,contentType:'application/json',body:JSON.stringify(body),headers}); }; - await page.route('**://api.caterium.ru/**',handle);await page.route('**://*.supabase.co/**',handle); + await page.route('**://api.caterium.ru/**',handle);await page.route('**/api/index.php?**',handle); await page.goto('/index.html',{waitUntil:'domcontentloaded'});await page.waitForFunction(()=>window.CateriumAuthSecurityV1774&&window.SunCloudV2?.getClient()); await page.locator('#sunGateEmailV3').fill(user.email);await page.locator('#sunGatePasswordV3').fill('test-password');await page.locator('#sunGateSubmitV3').click(); await expect(page.locator('#sunCloudAuthGateV3')).toHaveCount(0,{timeout:20000});await expect(page.locator('body > header')).toBeVisible(); expect(await page.evaluate(()=>window.SunCloudV2.getWorkspace()?.id)).toBe(workspaceId); - expect(seen.some(s=>s==='api.caterium.ru/rest/v1/rpc/sun_my_workspaces')).toBe(true); - expect(seen.some(s=>s.endsWith('.supabase.co/rest/v1/rpc/sun_my_workspaces'))).toBe(true); + await expect.poll(()=>page.evaluate(()=>JSON.parse(localStorage.getItem('sunOrders')||'[]').map(o=>o.id))).toContain('proxy-order'); + expect(seen.some(s=>s.path==='/auth/v1/token'&&s.host!=='api.caterium.ru')).toBe(true); + expect(seen.some(s=>s.host==='api.caterium.ru')).toBe(primaryFails); + expect(foreign).toEqual([]);expect(sockets).toEqual([]); expect(warnings.some(s=>s.includes('Multiple GoTrueClient'))).toBe(false); }); diff --git a/tests/release-check.mjs b/tests/release-check.mjs index ffa8595..c642811 100644 --- a/tests/release-check.mjs +++ b/tests/release-check.mjs @@ -14,8 +14,8 @@ check(!index.includes('offer-gallery-data.js'),'blocking Base64 gallery absent') check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer'); check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox'); check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4'); -check(sw.includes('v103-20260918-help-center')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3'); -check(index.includes('20260918-help-center')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3'); +check(sw.includes('v104-20260918-russia-proxy')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3'); +check(index.includes('20260918-russia-proxy')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3'); check(performance.includes('SunAttachmentGuard')&&performance.includes('TARGET=2*1024*1024'),'chat photo auto-compression is versioned'); check(performance.includes("rpc('sun_dev_dashboard')")&&performance.includes('server_size')&&performance.includes('storage_size'),'Developer Console server/storage counters are versioned'); check(performance.includes('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded'); @@ -39,7 +39,7 @@ check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30 check(lock.version===pkg.version&&lock.packages?.['']?.version===pkg.version,'package.json and package-lock.json versions match'); check(releaseManifest.version===`v${pkg.version}`,'release manifest version matches package.json'); check(releaseManifest.channel==='production','release manifest channel is production'); -check(String(releaseManifest.pwaCache||'').includes('v103-20260918-help-center'),'release manifest points to current PWA cache'); +check(String(releaseManifest.pwaCache||'').includes('v104-20260918-russia-proxy'),'release manifest points to current PWA cache'); check(['17.6.2','17.6.3','17.6.4','17.6.5','17.6.6','17.6.7','17.6.8','17.6.9','17.7.0','17.7.1','17.7.2','17.7.3'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.7.3'); check(runtime.includes('CLOUD_RPC_TIMEOUT_MS=45000')&&runtime.includes('CLOUD_CONFLICT_MAX_RETRIES=4')&&runtime.includes('retryCount'),'cloud sync has timeout and capped exponential conflict retries'); check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes('ERROR_DEDUPE_MS=5*60*1000')&&runtime.includes('mirrorBusy=false')&&runtime.includes('backupBusy=false'),'stability logger uses current version, dedupe and single-flight guards'); @@ -65,8 +65,8 @@ check(offerWorkspace.includes('PDF и предпросмотр')&&offerWorkspace check(offerWorkspace.includes('SunClassicOfferPDFV1767')&&offerWorkspace.includes('finalGallery=galleryFor'),'custom gallery is injected into PDF renderer'); check(releaseManifest.offerWorkspaceTabs===true&&releaseManifest.offerTemplatesSeparateTab===true&&releaseManifest.offerTwoCustomGalleryPhotos===true,'release manifest records offer workspace changes'); check(pkg.version==='17.7.3','package version is v17.7.3'); -check(index.includes('20260918-help-center'),'index cache bust is v17.7.3'); -check(sw.includes('v103-20260918-help-center')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules'); +check(index.includes('20260918-russia-proxy'),'index cache bust is v17.7.3'); +check(sw.includes('v104-20260918-russia-proxy')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules'); check(fs.existsSync(path.join(root,'public/core/data-layer-v1773.js'))&&fs.existsSync(path.join(root,'public/core/server-automation-v1770.js')),'data layer and server automation modules exist'); check(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active'); check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3'); diff --git a/tests/static-security.mjs b/tests/static-security.mjs index cbb436c..653a48c 100644 --- a/tests/static-security.mjs +++ b/tests/static-security.mjs @@ -28,8 +28,8 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos'); const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x)); if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed'); -if(!sw.includes('20260918-help-center')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh'); -if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-help-center')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current'); +if(!sw.includes('20260918-russia-proxy')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh'); +if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-russia-proxy')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current'); if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present'); if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present'); if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed');