API / Security: - Add api/_helpers.php: shared send_security_headers(), verify_origin(), get_hmac_secret(), check_rate_limit(), read_json_locked(), write_json_locked() - shorten.php: remove Access-Control-Allow-Origin:*, restrict to same-origin, rate-limit 20 req/h per IP, atomic JSON read+lock, HMAC secret from file - verify.php: rate-limit GET (30/min) and POST (10/h) per IP, atomic lock, prevent overwriting existing proofs, origin check on POST - node.php: fix rate limit from 1000 to 60 req/min, add security headers, origin check - check-short.php: add security headers, re-derive signature server-side - s.php: use file-based HMAC secret via get_hmac_secret(), hash_equals() for timing-safe comparison Service Worker: - sw.js: navigation requests (mode=navigate) never served from cache; network-first with offline fallback to prevent stale invoice state Documentation (honest claims): - README: tagline "No backend" -> "No tracking"; new Architecture table listing exactly what server sees for each feature; Security Model section - index.html: meta description and footer updated from "No Backend" to "Minimal Backend" - i18n.js footer: already updated in previous commit
73 lines
1.8 KiB
JavaScript
73 lines
1.8 KiB
JavaScript
var CACHE_NAME = 'xmrpay-v3';
|
|
var ASSETS = [
|
|
'/',
|
|
'/index.html',
|
|
'/app.js',
|
|
'/i18n.js',
|
|
'/style.css',
|
|
'/lib/qrcode.min.js',
|
|
'/fonts/inter-400.woff2',
|
|
'/fonts/jetbrains-400.woff2'
|
|
// xmr-crypto.bundle.js and jspdf.min.js are lazy-loaded and runtime-cached
|
|
];
|
|
|
|
self.addEventListener('install', function (e) {
|
|
e.waitUntil(
|
|
caches.open(CACHE_NAME).then(function (cache) {
|
|
return cache.addAll(ASSETS);
|
|
})
|
|
);
|
|
self.skipWaiting();
|
|
});
|
|
|
|
self.addEventListener('activate', function (e) {
|
|
e.waitUntil(
|
|
caches.keys().then(function (names) {
|
|
return Promise.all(
|
|
names.filter(function (n) { return n !== CACHE_NAME; })
|
|
.map(function (n) { return caches.delete(n); })
|
|
);
|
|
})
|
|
);
|
|
self.clients.claim();
|
|
});
|
|
|
|
self.addEventListener('fetch', function (e) {
|
|
var url = new URL(e.request.url);
|
|
|
|
// API calls — network only, don't cache
|
|
if (url.pathname.startsWith('/api/')) {
|
|
e.respondWith(fetch(e.request));
|
|
return;
|
|
}
|
|
|
|
// Navigation (HTML) — network first, fall back to cached index.html for offline
|
|
// Invoice data is in the URL hash, so caching the document would cause stale state
|
|
if (e.request.mode === 'navigate') {
|
|
e.respondWith(
|
|
fetch(e.request).catch(function () {
|
|
return caches.match('/index.html');
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
// App assets — cache first, fallback to network
|
|
e.respondWith(
|
|
caches.match(e.request).then(function (cached) {
|
|
var networkFetch = fetch(e.request).then(function (response) {
|
|
if (response.ok) {
|
|
var clone = response.clone();
|
|
caches.open(CACHE_NAME).then(function (cache) {
|
|
cache.put(e.request, clone);
|
|
});
|
|
}
|
|
return response;
|
|
}).catch(function () {
|
|
return cached;
|
|
});
|
|
return cached || networkFetch;
|
|
})
|
|
);
|
|
});
|