@@ -16,7 +16,7 @@ const uuidKey = (u) => [...u].map((b) => b.toString(16).padStart(2, '0')).join('
1616// fallback: requests are tried direct first; on a network failure
1717// (typically CORS) the same request is retried through proxyUrl.
1818// Covers the subset PebbleKit JS apps use.
19- export function makeXhrOverFetch ( fetchFn , proxyUrl ) {
19+ export function makeXhrOverFetch ( fetchFn , proxyUrl , log = ( ) => { } ) {
2020 const proxied = ( url ) => proxyUrl ? proxyUrl + encodeURIComponent ( url ) : null ;
2121
2222 async function robustFetch ( url , opts ) {
@@ -25,6 +25,7 @@ export function makeXhrOverFetch(fetchFn, proxyUrl) {
2525 } catch ( e ) {
2626 const p = / ^ h t t p s ? : / i. test ( url ) && proxied ( url ) ;
2727 if ( ! p ) throw e ;
28+ log ( `direct fetch blocked (${ String ( url ) . slice ( 0 , 80 ) } ), retrying via proxy` ) ;
2829 return fetchFn ( p , opts ) ;
2930 }
3031 }
@@ -81,6 +82,7 @@ export function makeXhrOverFetch(fetchFn, proxyUrl) {
8182 if ( timer ) clearTimeout ( timer ) ;
8283 if ( this . _aborted && ! this . _timedOut ) return ;
8384 this . readyState = 4 ;
85+ log ( `XHR ${ this . _timedOut ? 'timeout' : 'failed' } : ${ String ( this . _url ) . slice ( 0 , 80 ) } (${ e . message } )` ) ;
8486 if ( this . _timedOut && this . ontimeout ) this . ontimeout ( e ) ;
8587 else if ( this . onerror ) this . onerror ( e ) ;
8688 } ) ;
@@ -248,7 +250,7 @@ export function makeMemoryStorage() {
248250
249251// Browser sandbox: hidden same-origin iframe. The app JS gets the page's
250252// real fetch/XHR (patched with the proxy fallback), geolocation, etc.
251- export function makeIframeSandbox ( proxyUrl ) {
253+ export function makeIframeSandbox ( proxyUrl , log = ( ) => { } ) {
252254 return ( globals ) => {
253255 const frame = document . createElement ( 'iframe' ) ;
254256 frame . style . display = 'none' ;
@@ -260,15 +262,39 @@ export function makeIframeSandbox(proxyUrl) {
260262 for ( const [ k , v ] of Object . entries ( globals ) ) {
261263 Object . defineProperty ( w , k , { value : v , configurable : true , writable : true } ) ;
262264 }
263- const XHR = makeXhrOverFetch ( w . fetch . bind ( w ) , proxyUrl ) ;
265+ const XHR = makeXhrOverFetch ( w . fetch . bind ( w ) , proxyUrl , log ) ;
264266 w . XMLHttpRequest = XHR ;
265267 const nativeFetch = w . fetch . bind ( w ) ;
266268 w . fetch = ( url , opts ) => nativeFetch ( url , opts ) . catch ( ( e ) => {
267269 if ( proxyUrl && typeof url === 'string' && / ^ h t t p s ? : / i. test ( url ) ) {
270+ log ( `direct fetch blocked (${ url . slice ( 0 , 80 ) } ), retrying via proxy` ) ;
268271 return nativeFetch ( proxyUrl + encodeURIComponent ( url ) , opts ) ;
269272 }
270273 throw e ;
271274 } ) ;
275+ // Log geolocation outcomes — a silent OS-level denial otherwise
276+ // looks identical to an app that never asked.
277+ try {
278+ const geo = w . navigator . geolocation ;
279+ const origGet = geo . getCurrentPosition . bind ( geo ) ;
280+ geo . getCurrentPosition = ( ok , err , opts ) => {
281+ log ( 'app requested geolocation…' ) ;
282+ origGet (
283+ ( pos ) => { log ( `geolocation ok (±${ Math . round ( pos . coords . accuracy ) } m)` ) ; ok ( pos ) ; } ,
284+ ( e ) => { log ( `geolocation DENIED/failed: ${ e . message } (code ${ e . code } )` ) ; if ( err ) err ( e ) ; } ,
285+ opts ,
286+ ) ;
287+ } ;
288+ const origWatch = geo . watchPosition . bind ( geo ) ;
289+ geo . watchPosition = ( ok , err , opts ) => {
290+ log ( 'app watching geolocation…' ) ;
291+ return origWatch (
292+ ( pos ) => { log ( 'geolocation update' ) ; ok ( pos ) ; } ,
293+ ( e ) => { log ( `geolocation DENIED/failed: ${ e . message } (code ${ e . code } )` ) ; if ( err ) err ( e ) ; } ,
294+ opts ,
295+ ) ;
296+ } ;
297+ } catch ( e ) { /* geolocation unavailable in this context */ }
272298 return {
273299 run : ( code ) => w . eval ( code ) ,
274300 dispose : ( ) => frame . remove ( ) ,
0 commit comments