Poll workspace revision through the authorized state RPC

This commit is contained in:
pavlov346346-source 2026-09-18 19:17:03 +03:00
parent 71bc56b0cd
commit 7807bf4f8f
9 changed files with 31 additions and 21 deletions

View File

@ -11,7 +11,7 @@
"serverReady": true, "serverReady": true,
"workspaceAutoDiscovery": true, "workspaceAutoDiscovery": true,
"invitesTemporarilyDisabled": false, "invitesTemporarilyDisabled": false,
"pwaCache": "v104-20260918-russia-proxy", "pwaCache": "v105-20260918-russia-proxy-rpc",
"fullOfferDescriptions": true, "fullOfferDescriptions": true,
"dynamicOfferRows": true, "dynamicOfferRows": true,
"pdfOfferDescriptionFix": true, "pdfOfferDescriptionFix": true,

View File

@ -25,6 +25,8 @@ 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 chat refresh every 10 seconds. Unchanged bases are not downloaded again. Requests
from the previous account are discarded. Typing/online indicators require a from the previous account are discarded. Typing/online indicators require a
future WebSocket-capable proxy and are not advertised by the HTTP mode. future WebSocket-capable proxy and are not advertised by the HTTP mode.
Revision reads project `revision` from the existing `sun_fetch_app_state` RPC;
direct table access remains revoked. No database grants or schema changes are required.
This covers application data and media. Optional map/geocoding providers remain 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 external and do not gate login or order loading. If the login page itself cannot

View File

@ -3363,7 +3363,9 @@ window.SUN_LEGACY_CATALOG_V175=[];
try{ try{
// Poll only the revision; download/merge the full base only after a change. // 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. // 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(); // Direct table reads are intentionally revoked in production. PostgREST
// projects just revision from the existing, tenant-checked RPC result.
const {data,error}=await client.rpc('sun_fetch_app_state',{p_workspace:startedWorkspace}).select('revision').maybeSingle();
if(error)throw error; if(error)throw error;
const baseline=await getBaseline(); const baseline=await getBaseline();
if(current()&&data&&Number(data.revision)!==Number(baseline?.revision))await syncNow({quiet:true}); if(current()&&data&&Number(data.revision)!==Number(baseline?.revision))await syncNow({quiet:true});

View File

@ -1,7 +1,7 @@
(()=>{ (()=>{
'use strict'; 'use strict';
const VERSION='17.7.3'; const VERSION='17.7.3';
const RELEASE='20260918-russia-proxy'; const RELEASE='20260918-russia-proxy-rpc';
const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}}; 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(){ function installAuthBoot(){

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
const CACHE='sun-catering-pwa-v104-20260918-russia-proxy'; const CACHE='sun-catering-pwa-v105-20260918-russia-proxy-rpc';
const VERSION='20260918-russia-proxy'; const VERSION='20260918-russia-proxy-rpc';
const CORE=[ const CORE=[
'./vendor/supabase-2.112.4.min.js', './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}`, `./core/help-center.js?v=${VERSION}`,`./core/help-center.css?v=${VERSION}`,`./help/knowledge-v1.json?v=${VERSION}`,

View File

@ -24,17 +24,17 @@ test('revision polling reads only changed bases and ignores the previous account
const result=await page.evaluate(async()=>{ const result=await page.evaluate(async()=>{
let revision=1,readCount=0,resolveRevision;const calls=[]; let revision=1,readCount=0,resolveRevision;const calls=[];
const payload={format:'sun-cloud-v2',version:2,storage:{sunOrders:{t:'j',v:[]}}}; 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 query={select(fields){calls.push(fields);return this},async maybeSingle(){readCount++;return revision===99?new Promise(r=>resolveRevision=r):{data:{revision}}},then(resolve){calls.push('full-state');return Promise.resolve({data:[{revision,payload}]}).then(resolve)}};
const c={from(table){calls.push(table);return query},async rpc(name){calls.push(name);return {data:[{revision,payload}]}}}; const c={from(){throw new Error('Direct table access denied')},rpc(name,args){calls.push(name);if(name==='sun_fetch_app_state'){calls.push(args.p_workspace);return query}return Promise.resolve({data:[{revision,payload}]})}};
SunCloudV2.testInit(c);await SunCloudV2.testBaseline({revision:1,payload}); SunCloudV2.testInit(c);await SunCloudV2.testBaseline({revision:1,payload});
await SunCloudV2.testPoll();const unchanged=calls.splice(0); await SunCloudV2.testPoll();const unchanged=calls.splice(0);
revision=2;await SunCloudV2.testPoll();const changed=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; revision=99;const pending=SunCloudV2.testPoll();await SunCloudV2.testPoll();SunCloudV2.testLeave();resolveRevision({data:{revision:99}});await pending;
return {unchanged,changed,late:calls,readCount}; return {unchanged,changed,late:calls,readCount};
}); });
expect(result.unchanged).toEqual(['sun_app_state','revision',['workspace_id','company']]); expect(result.unchanged).toEqual(['sun_fetch_app_state','company','revision']);
expect(result.changed).toContain('sun_fetch_app_state'); expect(result.changed).toContain('full-state');
expect(result.late).not.toContain('sun_fetch_app_state');expect(result.readCount).toBe(3); expect(result.late).not.toContain('full-state');expect(result.readCount).toBe(3);
}); });
test('Safari without streaming uploads can send login and binary bodies through fallback',async({page})=>{ test('Safari without streaming uploads can send login and binary bodies through fallback',async({page})=>{
@ -223,6 +223,7 @@ test('a failed company load replaces the stale login form with an actionable ret
}); });
for(const primaryFails of [false,true])test('real SDK loads orders with foreign services blocked, primary failure='+primaryFails,async({page})=>{ for(const primaryFails of [false,true])test('real SDK loads orders with foreign services blocked, primary failure='+primaryFails,async({page})=>{
await page.clock.install();
const userId='11111111-1111-4111-8111-111111111111',workspaceId='22222222-2222-4222-8222-222222222222',expires=Math.floor(Date.now()/1000)+3600; 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 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 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('.');
@ -230,10 +231,12 @@ for(const primaryFails of [false,true])test('real SDK loads orders with foreign
page.on('console',m=>{if(m.type()==='warning')warnings.push(m.text())});page.on('websocket',ws=>sockets.push(ws.url())); 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()}); await page.route('https://**',r=>{foreign.push(r.request().url());return r.abort()});
const handle=async route=>{ const handle=async route=>{
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 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(),select:url.searchParams.get('select')});
const headers={'access-control-allow-origin':'*'}; const headers={'access-control-allow-origin':'*'};
if(request.method()==='OPTIONS')return route.fulfill({status:204,headers}); if(request.method()==='OPTIONS')return route.fulfill({status:204,headers});
if(primaryFails&&url.pathname==='/api/index.php')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});
if(path==='/rest/v1/sun_app_state')return route.fulfill({status:403,contentType:'application/json',body:'{"message":"direct table access revoked"}',headers});
if(path.endsWith('/sun_fetch_app_state')&&url.searchParams.get('select')==='revision')return route.fulfill({status:200,contentType:'application/json',body:'{"revision":1}',headers});
let body=null; let body=null;
if(path==='/auth/v1/token')body={access_token:token,refresh_token:'test-refresh',token_type:'bearer',expires_in:3600,expires_at:expires,user}; 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==='/auth/v1/user')body=user;
@ -249,6 +252,9 @@ for(const primaryFails of [false,true])test('real SDK loads orders with foreign
await expect(page.locator('#sunCloudAuthGateV3')).toHaveCount(0,{timeout:20000});await expect(page.locator('body > header')).toBeVisible(); 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(await page.evaluate(()=>window.SunCloudV2.getWorkspace()?.id)).toBe(workspaceId);
await expect.poll(()=>page.evaluate(()=>JSON.parse(localStorage.getItem('sunOrders')||'[]').map(o=>o.id))).toContain('proxy-order'); await expect.poll(()=>page.evaluate(()=>JSON.parse(localStorage.getItem('sunOrders')||'[]').map(o=>o.id))).toContain('proxy-order');
await page.clock.fastForward(21000);
await expect.poll(()=>seen.filter(s=>s.path.endsWith('/sun_fetch_app_state')&&s.select==='revision').length).toBeGreaterThan(0);
expect(seen.some(s=>s.path==='/rest/v1/sun_app_state')).toBe(false);
expect(seen.some(s=>s.path==='/auth/v1/token'&&s.host!=='api.caterium.ru')).toBe(true); 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(seen.some(s=>s.host==='api.caterium.ru')).toBe(primaryFails);
expect(foreign).toEqual([]);expect(sockets).toEqual([]); expect(foreign).toEqual([]);expect(sockets).toEqual([]);

View File

@ -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((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(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([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4');
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(sw.includes('v105-20260918-russia-proxy-rpc')&&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(index.includes('20260918-russia-proxy-rpc')&&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('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("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'); 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(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.version===`v${pkg.version}`,'release manifest version matches package.json');
check(releaseManifest.channel==='production','release manifest channel is production'); check(releaseManifest.channel==='production','release manifest channel is production');
check(String(releaseManifest.pwaCache||'').includes('v104-20260918-russia-proxy'),'release manifest points to current PWA cache'); check(String(releaseManifest.pwaCache||'').includes('v105-20260918-russia-proxy-rpc'),'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(['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('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'); 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(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(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(pkg.version==='17.7.3','package version is v17.7.3');
check(index.includes('20260918-russia-proxy'),'index cache bust is v17.7.3'); check(index.includes('20260918-russia-proxy-rpc'),'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(sw.includes('v105-20260918-russia-proxy-rpc')&&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(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(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'); check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3');

View File

@ -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'); 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)); 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(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed');
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(!sw.includes('20260918-russia-proxy-rpc')||!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(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-russia-proxy-rpc')||!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('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("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'); 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');