feedkeeper 0.27.2 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,7 +30,7 @@
30
30
  <meta name="twitter:image" content="/logo.svg" />
31
31
  <!-- Inline so the splash paints before any script or stylesheet loads -->
32
32
  <style>html{background:#f8fafc}html[data-theme=dark]{background:#0b0f17}@media (prefers-color-scheme:dark){html:not([data-theme=light]){background:#0b0f17}}#splash{z-index:100;background:inherit;justify-content:center;align-items:center;transition:opacity .3s;display:flex;position:fixed;inset:0}#splash img{width:62px;height:62px;animation:.4s both splash-in}#splash.splash-hidden{opacity:0;pointer-events:none}@keyframes splash-in{0%{opacity:0;transform:scale(.92)}}@media (prefers-reduced-motion:reduce){#splash,#splash img{transition:none;animation:none}}</style>
33
- <script type="module" crossorigin src="/assets/index-Ci8_fWis.js"></script>
33
+ <script type="module" crossorigin src="/assets/index-CTTNHCfi.js"></script>
34
34
  <link rel="stylesheet" crossorigin href="/assets/index-BprbGvrl.css">
35
35
  </head>
36
36
  <body>
package/web/dist/sw.js CHANGED
@@ -1,8 +1,27 @@
1
1
  // FeedKeeper service worker. Built into dist/sw.js, where the placeholder below is
2
2
  // replaced with a hash of the build's file names so every release gets its own app cache.
3
- const BUILD_ID = "e34333ade39b";
3
+ const BUILD_ID = "0038c2a9c847";
4
4
  const STATIC_CACHE = `fk-static-${BUILD_ID}`;
5
5
  const API_CACHE = "fk-api";
6
+ let privateGeneration = 0;
7
+ let authenticationChanges = 0;
8
+
9
+ async function invalidatePrivateCache() {
10
+ privateGeneration++;
11
+ await caches.delete(API_CACHE);
12
+ }
13
+
14
+ async function authenticate(request) {
15
+ authenticationChanges++;
16
+ try {
17
+ await invalidatePrivateCache();
18
+ return await fetch(request);
19
+ } finally {
20
+ // Responses that started before the cookie changed must not refill the new session's cache.
21
+ await invalidatePrivateCache();
22
+ authenticationChanges--;
23
+ }
24
+ }
6
25
  const SHELL = "/index.html";
7
26
  const PRECACHE = ["/theme-init.js", "/manifest.webmanifest", "/logo.svg", "/favicon-32.png?v=2", "/app-icon-180.png?v=2", "/app-icon-192.png?v=2", "/app-icon-512.png?v=2"];
8
27
  // The server answers with `Vary: Origin`, but entries cached without that header must still match.
@@ -30,6 +49,9 @@ self.addEventListener("activate", (event) => {
30
49
  (async () => {
31
50
  const names = await caches.keys();
32
51
  await Promise.all(names.filter((name) => name.startsWith("fk-static-") && name !== STATIC_CACHE).map((name) => caches.delete(name)));
52
+ // Older releases stored personal responses without checking account ownership.
53
+ await invalidatePrivateCache();
54
+ await caches.delete("fk-offline");
33
55
  await self.clients.claim();
34
56
  })(),
35
57
  );
@@ -42,9 +64,16 @@ self.addEventListener("message", (event) => {
42
64
 
43
65
  self.addEventListener("fetch", (event) => {
44
66
  const { request } = event;
45
- if (request.method !== "GET") return;
46
67
  const url = new URL(request.url);
47
68
  if (url.origin !== self.location.origin) return;
69
+ if (request.method !== "GET") {
70
+ if (url.pathname.startsWith("/api/auth/") || url.pathname.startsWith("/auth/") || url.pathname.startsWith("/api/onboarding/")) {
71
+ event.respondWith(authenticate(request));
72
+ }
73
+ return;
74
+ }
75
+ // While a login changes the cookie, personal requests must bypass all offline responses.
76
+ if (authenticationChanges && url.pathname.startsWith("/api/")) return;
48
77
  // Server-rendered OAuth pages and discovery documents must never be answered with the app shell.
49
78
  if (url.pathname.startsWith("/oauth/") || url.pathname.startsWith("/.well-known/")) return;
50
79
 
@@ -106,11 +135,12 @@ async function navigate(request) {
106
135
  }
107
136
 
108
137
  async function cacheFirst(cacheName, request, limit = 0) {
138
+ const generation = privateGeneration;
109
139
  const cache = await caches.open(cacheName);
110
140
  const cached = await cache.match(request, MATCH);
111
- if (cached) return cached;
141
+ if (cached && (cacheName !== API_CACHE || generation === privateGeneration)) return cached;
112
142
  const response = await fetch(request);
113
- if (response.ok) {
143
+ if (response.ok && (cacheName !== API_CACHE || generation === privateGeneration)) {
114
144
  await cache.put(request, response.clone());
115
145
  if (limit) await trim(cache, (key) => /\/api\/(archive\/images\/|items\/\d+\/image)/.test(key.url), limit);
116
146
  }
@@ -118,12 +148,13 @@ async function cacheFirst(cacheName, request, limit = 0) {
118
148
  }
119
149
 
120
150
  async function mutableImage(request) {
151
+ const generation = privateGeneration;
121
152
  const cache = await caches.open(API_CACHE);
122
153
  const cached = await cache.match(request, MATCH);
123
154
  const date = cached && Date.parse(cached.headers.get("date") ?? "");
124
155
  const maxAge = Number(cached?.headers.get("cache-control")?.match(/max-age=(\d+)/)?.[1] ?? 0);
125
156
  // Old immutable proxy responses must also expire at the new six-hour boundary.
126
- if (cached && !cached.headers.get("cache-control")?.includes("no-store") && Date.now() - date < Math.min(maxAge, 21600) * 1000) return cached;
157
+ if (cached && generation === privateGeneration && !cached.headers.get("cache-control")?.includes("no-store") && Date.now() - date < Math.min(maxAge, 21600) * 1000) return cached;
127
158
  try {
128
159
  const headers = new Headers(request.headers);
129
160
  if (cached?.headers.get("etag")) headers.set("If-None-Match", cached.headers.get("etag"));
@@ -134,40 +165,58 @@ async function mutableImage(request) {
134
165
  refreshed.set("date", response.headers.get("date") ?? new Date().toUTCString());
135
166
  response = new Response(await cached.arrayBuffer(), { status: 200, headers: refreshed });
136
167
  }
137
- if (response.ok) {
168
+ if (response.ok && generation === privateGeneration) {
138
169
  if (response.headers.get("cache-control")?.includes("no-store")) await cache.delete(request);
139
170
  else { await cache.put(request, response.clone()); await trim(cache, key => /\/api\/(archive\/images\/|items\/\d+\/image)/.test(key.url), MAX_CACHED_IMAGES); }
140
171
  }
141
172
  return response; // An online denial must never fall back to the previous account's bytes.
142
- } catch { return cached ?? Response.error(); }
173
+ } catch { return generation === privateGeneration ? cached ?? Response.error() : Response.error(); }
143
174
  }
144
175
 
145
176
  async function networkFirst(cacheName, request, limit) {
146
- const cache = await caches.open(cacheName);
177
+ let generation = privateGeneration;
178
+ let cache = await caches.open(cacheName);
147
179
  try {
148
180
  const response = await withTimeout(fetch(request), NETWORK_TIMEOUT_MS);
149
- if (response.ok) {
181
+ if (generation !== privateGeneration) throw new Error("session_changed");
182
+ if (new URL(request.url).pathname === "/api/auth/me" && generation === privateGeneration) {
183
+ if (response.ok) {
184
+ const user = await response.clone().json();
185
+ const previous = await cache.match(request, MATCH);
186
+ const previousUser = previous ? await previous.json() : null;
187
+ if (!Number.isSafeInteger(user.id) || user.id <= 0) return Response.error();
188
+ if (previousUser?.id !== user.id) {
189
+ await invalidatePrivateCache();
190
+ generation = privateGeneration;
191
+ cache = await caches.open(cacheName);
192
+ }
193
+ } else if (response.status === 401) {
194
+ await invalidatePrivateCache();
195
+ }
196
+ }
197
+ if (response.ok && generation === privateGeneration && !authenticationChanges) {
150
198
  await cache.put(request, response.clone());
151
199
  if (limit) await trim(cache, (key) => new URL(key.url).pathname === new URL(request.url).pathname, limit);
152
200
  }
153
201
  return response;
154
202
  } catch (error) {
155
- const cached = await cache.match(request, MATCH);
156
- if (cached) return cached;
203
+ const cached = generation === privateGeneration && !authenticationChanges ? await cache.match(request, MATCH) : null;
204
+ if (cached && generation === privateGeneration) return cached;
157
205
  throw error;
158
206
  }
159
207
  }
160
208
 
161
209
  async function staleWhileRevalidate(cacheName, request) {
210
+ const generation = privateGeneration;
162
211
  const cache = await caches.open(cacheName);
163
212
  const cached = await cache.match(request, MATCH);
164
213
  const refresh = fetch(request)
165
214
  .then((response) => {
166
- if (response.ok) cache.put(request, response.clone());
215
+ if (response.ok && (cacheName !== API_CACHE || generation === privateGeneration)) return cache.put(request, response.clone()).then(() => response);
167
216
  return response;
168
217
  })
169
218
  .catch(() => undefined);
170
- return cached ?? (await refresh) ?? Response.error();
219
+ return (cacheName !== API_CACHE || generation === privateGeneration ? cached : null) ?? (await refresh) ?? Response.error();
171
220
  }
172
221
 
173
222
  /** Drop the oldest entries so a long-lived install does not grow without bound. */