atagen

malice

public
Star0
Watch0
7cca1d5628d18a9b56ed09393a47b08e1349fdef
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/sw.ts

973f8fbdaf80atagen2026-09-28 09:10 UTC1
import { statusForKind } from './lib/wire-status';
973f8fbdaf80atagen2026-09-28 09:10 UTC2
import { decodeAttachmentIv, importAttachmentKey, RangeDecryptor } from './lib/media-decrypt';
973f8fbdaf80atagen2026-09-28 09:10 UTC3
import { attachmentContentType } from './lib/media-sniff';
973f8fbdaf80atagen2026-09-28 09:10 UTC4
973f8fbdaf80atagen2026-09-28 09:10 UTC5
// Prepended by vite.config.ts's precacheManifestPlugin: every emitted chunk,
973f8fbdaf80atagen2026-09-28 09:10 UTC6
// asset, and the wasm, as root-relative URLs.
973f8fbdaf80atagen2026-09-28 09:10 UTC7
const sw = self as unknown as ServiceWorkerGlobalScope & { __MALICE_PRECACHE__: string[] };
973f8fbdaf80atagen2026-09-28 09:10 UTC8
973f8fbdaf80atagen2026-09-28 09:10 UTC9
const CACHE_NAME = 'malice-shell-v1';
cf23dbf4e77catagen2026-10-23 06:36 UTC10
const AVATAR_CACHE = 'malice-avatars-v1';
973f8fbdaf80atagen2026-09-28 09:10 UTC11
973f8fbdaf80atagen2026-09-28 09:10 UTC12
sw.addEventListener('install', (event) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC13
  event.waitUntil(
973f8fbdaf80atagen2026-09-28 09:10 UTC14
    (async () => {
973f8fbdaf80atagen2026-09-28 09:10 UTC15
      const cache = await caches.open(CACHE_NAME);
973f8fbdaf80atagen2026-09-28 09:10 UTC16
      await cache.addAll(['/', '/index.html', ...sw.__MALICE_PRECACHE__]);
973f8fbdaf80atagen2026-09-28 09:10 UTC17
    })()
973f8fbdaf80atagen2026-09-28 09:10 UTC18
  );
973f8fbdaf80atagen2026-09-28 09:10 UTC19
  void sw.skipWaiting();
973f8fbdaf80atagen2026-09-28 09:10 UTC20
});
973f8fbdaf80atagen2026-09-28 09:10 UTC21
973f8fbdaf80atagen2026-09-28 09:10 UTC22
sw.addEventListener('activate', (event) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC23
  event.waitUntil(
973f8fbdaf80atagen2026-09-28 09:10 UTC24
    (async () => {
973f8fbdaf80atagen2026-09-28 09:10 UTC25
      const keys = await caches.keys();
cf23dbf4e77catagen2026-10-23 06:36 UTC26
      await Promise.all(keys.filter((key) => key !== CACHE_NAME && key !== AVATAR_CACHE).map((key) => caches.delete(key)));
973f8fbdaf80atagen2026-09-28 09:10 UTC27
      await sw.clients.claim();
973f8fbdaf80atagen2026-09-28 09:10 UTC28
    })()
973f8fbdaf80atagen2026-09-28 09:10 UTC29
  );
973f8fbdaf80atagen2026-09-28 09:10 UTC30
});
973f8fbdaf80atagen2026-09-28 09:10 UTC31
973f8fbdaf80atagen2026-09-28 09:10 UTC32
interface MediaReply {
dffd63b58af4atagen2026-09-28 09:10 UTC33
  bytes?: Uint8Array<ArrayBuffer>;
973f8fbdaf80atagen2026-09-28 09:10 UTC34
  content_type?: string | null;
973f8fbdaf80atagen2026-09-28 09:10 UTC35
  error?: { kind: string; message: string };
973f8fbdaf80atagen2026-09-28 09:10 UTC36
}
973f8fbdaf80atagen2026-09-28 09:10 UTC37
973f8fbdaf80atagen2026-09-28 09:10 UTC38
interface AttachmentEncryptionWire {
973f8fbdaf80atagen2026-09-28 09:10 UTC39
  key: string;
973f8fbdaf80atagen2026-09-28 09:10 UTC40
  iv: string;
973f8fbdaf80atagen2026-09-28 09:10 UTC41
  sha256: string;
973f8fbdaf80atagen2026-09-28 09:10 UTC42
}
973f8fbdaf80atagen2026-09-28 09:10 UTC43
973f8fbdaf80atagen2026-09-28 09:10 UTC44
interface MediaFileWire {
973f8fbdaf80atagen2026-09-28 09:10 UTC45
  url: string;
973f8fbdaf80atagen2026-09-28 09:10 UTC46
  mimetype: string | null;
973f8fbdaf80atagen2026-09-28 09:10 UTC47
  declared_size: number | null;
973f8fbdaf80atagen2026-09-28 09:10 UTC48
  encryption: AttachmentEncryptionWire | null;
973f8fbdaf80atagen2026-09-28 09:10 UTC49
}
973f8fbdaf80atagen2026-09-28 09:10 UTC50
973f8fbdaf80atagen2026-09-28 09:10 UTC51
type MediaLocationWire = { File: MediaFileWire } | { ServerThumbnail: { url: string; fallback: MediaFileWire } } | 'Local';
973f8fbdaf80atagen2026-09-28 09:10 UTC52
973f8fbdaf80atagen2026-09-28 09:10 UTC53
interface LocateReply {
973f8fbdaf80atagen2026-09-28 09:10 UTC54
  location?: MediaLocationWire;
973f8fbdaf80atagen2026-09-28 09:10 UTC55
  token?: string | null;
973f8fbdaf80atagen2026-09-28 09:10 UTC56
  error?: { kind: string; message: string };
973f8fbdaf80atagen2026-09-28 09:10 UTC57
}
973f8fbdaf80atagen2026-09-28 09:10 UTC58
973f8fbdaf80atagen2026-09-28 09:10 UTC59
const MEDIA_TIMEOUT_MS = 30000;
973f8fbdaf80atagen2026-09-28 09:10 UTC60
const mediaChannel = new BroadcastChannel('malice-media');
973f8fbdaf80atagen2026-09-28 09:10 UTC61
const pendingMedia = new Map<string, (reply: MediaReply & LocateReply) => void>();
973f8fbdaf80atagen2026-09-28 09:10 UTC62
let nextMediaRequestId = 0;
973f8fbdaf80atagen2026-09-28 09:10 UTC63
973f8fbdaf80atagen2026-09-28 09:10 UTC64
mediaChannel.onmessage = (ev: MessageEvent) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC65
  const msg = ev.data as MediaReply & LocateReply & { id: string };
973f8fbdaf80atagen2026-09-28 09:10 UTC66
  const resolve = pendingMedia.get(msg.id);
973f8fbdaf80atagen2026-09-28 09:10 UTC67
  if (!resolve) return;
973f8fbdaf80atagen2026-09-28 09:10 UTC68
  pendingMedia.delete(msg.id);
973f8fbdaf80atagen2026-09-28 09:10 UTC69
  resolve(msg);
973f8fbdaf80atagen2026-09-28 09:10 UTC70
};
973f8fbdaf80atagen2026-09-28 09:10 UTC71
973f8fbdaf80atagen2026-09-28 09:10 UTC72
function requestMedia(kind: 'avatar' | 'media', fields: Record<string, string>): Promise<MediaReply | null> {
973f8fbdaf80atagen2026-09-28 09:10 UTC73
  const id = `${Date.now()}-${nextMediaRequestId++}`;
973f8fbdaf80atagen2026-09-28 09:10 UTC74
  return new Promise((resolve) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC75
    const timer = setTimeout(() => {
973f8fbdaf80atagen2026-09-28 09:10 UTC76
      pendingMedia.delete(id);
973f8fbdaf80atagen2026-09-28 09:10 UTC77
      resolve(null);
973f8fbdaf80atagen2026-09-28 09:10 UTC78
    }, MEDIA_TIMEOUT_MS);
973f8fbdaf80atagen2026-09-28 09:10 UTC79
    pendingMedia.set(id, (reply) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC80
      clearTimeout(timer);
973f8fbdaf80atagen2026-09-28 09:10 UTC81
      resolve(reply);
973f8fbdaf80atagen2026-09-28 09:10 UTC82
    });
973f8fbdaf80atagen2026-09-28 09:10 UTC83
    mediaChannel.postMessage({ id, kind, ...fields });
973f8fbdaf80atagen2026-09-28 09:10 UTC84
  });
973f8fbdaf80atagen2026-09-28 09:10 UTC85
}
973f8fbdaf80atagen2026-09-28 09:10 UTC86
973f8fbdaf80atagen2026-09-28 09:10 UTC87
interface LocateEntry {
973f8fbdaf80atagen2026-09-28 09:10 UTC88
  location: MediaLocationWire;
973f8fbdaf80atagen2026-09-28 09:10 UTC89
  token: string | null;
973f8fbdaf80atagen2026-09-28 09:10 UTC90
  expiresAt: number;
973f8fbdaf80atagen2026-09-28 09:10 UTC91
}
973f8fbdaf80atagen2026-09-28 09:10 UTC92
973f8fbdaf80atagen2026-09-28 09:10 UTC93
const LOCATE_TTL_MS = 5 * 60 * 1000;
973f8fbdaf80atagen2026-09-28 09:10 UTC94
const locateCache = new Map<string, LocateEntry>();
973f8fbdaf80atagen2026-09-28 09:10 UTC95
const sniffCache = new Map<string, { contentType: string | null; expiresAt: number }>();
973f8fbdaf80atagen2026-09-28 09:10 UTC96
973f8fbdaf80atagen2026-09-28 09:10 UTC97
function requestLocate(mediaId: string, size: string): Promise<{ location: MediaLocationWire; token: string | null } | null> {
973f8fbdaf80atagen2026-09-28 09:10 UTC98
  const id = `${Date.now()}-${nextMediaRequestId++}`;
973f8fbdaf80atagen2026-09-28 09:10 UTC99
  return new Promise((resolve) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC100
    const timer = setTimeout(() => {
973f8fbdaf80atagen2026-09-28 09:10 UTC101
      pendingMedia.delete(id);
973f8fbdaf80atagen2026-09-28 09:10 UTC102
      resolve(null);
973f8fbdaf80atagen2026-09-28 09:10 UTC103
    }, MEDIA_TIMEOUT_MS);
973f8fbdaf80atagen2026-09-28 09:10 UTC104
    pendingMedia.set(id, (reply) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC105
      clearTimeout(timer);
973f8fbdaf80atagen2026-09-28 09:10 UTC106
      resolve(reply.error || !reply.location ? null : { location: reply.location, token: reply.token ?? null });
973f8fbdaf80atagen2026-09-28 09:10 UTC107
    });
973f8fbdaf80atagen2026-09-28 09:10 UTC108
    mediaChannel.postMessage({ id, kind: 'locate', media_id: mediaId, size });
973f8fbdaf80atagen2026-09-28 09:10 UTC109
  });
973f8fbdaf80atagen2026-09-28 09:10 UTC110
}
973f8fbdaf80atagen2026-09-28 09:10 UTC111
973f8fbdaf80atagen2026-09-28 09:10 UTC112
/** In-memory only: the access token never touches Cache Storage or IndexedDB, and expires with the entry. */
973f8fbdaf80atagen2026-09-28 09:10 UTC113
async function locate(mediaId: string, size: string, force: boolean): Promise<LocateEntry | null> {
973f8fbdaf80atagen2026-09-28 09:10 UTC114
  if (!force) {
973f8fbdaf80atagen2026-09-28 09:10 UTC115
    const cached = locateCache.get(mediaId);
973f8fbdaf80atagen2026-09-28 09:10 UTC116
    if (cached && cached.expiresAt > Date.now()) return cached;
973f8fbdaf80atagen2026-09-28 09:10 UTC117
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC118
  const reply = await requestLocate(mediaId, size);
973f8fbdaf80atagen2026-09-28 09:10 UTC119
  if (!reply) return null;
973f8fbdaf80atagen2026-09-28 09:10 UTC120
  const entry: LocateEntry = { ...reply, expiresAt: Date.now() + LOCATE_TTL_MS };
973f8fbdaf80atagen2026-09-28 09:10 UTC121
  locateCache.set(mediaId, entry);
973f8fbdaf80atagen2026-09-28 09:10 UTC122
  return entry;
973f8fbdaf80atagen2026-09-28 09:10 UTC123
}
973f8fbdaf80atagen2026-09-28 09:10 UTC124
973f8fbdaf80atagen2026-09-28 09:10 UTC125
interface RangeSlice {
dffd63b58af4atagen2026-09-28 09:10 UTC126
  body: Uint8Array<ArrayBuffer>;
973f8fbdaf80atagen2026-09-28 09:10 UTC127
  status: number;
973f8fbdaf80atagen2026-09-28 09:10 UTC128
  contentRange?: string;
973f8fbdaf80atagen2026-09-28 09:10 UTC129
}
973f8fbdaf80atagen2026-09-28 09:10 UTC130
973f8fbdaf80atagen2026-09-28 09:10 UTC131
/** The engine hands back the whole attachment at once, so seeking in <video>/<audio> needs this SW to slice it. */
dffd63b58af4atagen2026-09-28 09:10 UTC132
function sliceForRange(bytes: Uint8Array<ArrayBuffer>, rangeHeader: string | null): RangeSlice {
973f8fbdaf80atagen2026-09-28 09:10 UTC133
  const match = rangeHeader ? /^bytes=(\d*)-(\d*)$/.exec(rangeHeader) : null;
973f8fbdaf80atagen2026-09-28 09:10 UTC134
  if (!match) return { body: bytes, status: 200 };
973f8fbdaf80atagen2026-09-28 09:10 UTC135
  const total = bytes.byteLength;
973f8fbdaf80atagen2026-09-28 09:10 UTC136
  let start = match[1] ? parseInt(match[1], 10) : undefined;
973f8fbdaf80atagen2026-09-28 09:10 UTC137
  let end = match[2] ? parseInt(match[2], 10) : undefined;
973f8fbdaf80atagen2026-09-28 09:10 UTC138
  if (start === undefined && end !== undefined) {
973f8fbdaf80atagen2026-09-28 09:10 UTC139
    start = Math.max(total - end, 0);
973f8fbdaf80atagen2026-09-28 09:10 UTC140
    end = total - 1;
973f8fbdaf80atagen2026-09-28 09:10 UTC141
  } else if (end === undefined) {
973f8fbdaf80atagen2026-09-28 09:10 UTC142
    end = total - 1;
973f8fbdaf80atagen2026-09-28 09:10 UTC143
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC144
  if (start === undefined || start > end || start >= total) return { body: bytes, status: 200 };
973f8fbdaf80atagen2026-09-28 09:10 UTC145
  end = Math.min(end, total - 1);
973f8fbdaf80atagen2026-09-28 09:10 UTC146
  return { body: bytes.subarray(start, end + 1), status: 206, contentRange: `bytes ${start}-${end}/${total}` };
973f8fbdaf80atagen2026-09-28 09:10 UTC147
}
973f8fbdaf80atagen2026-09-28 09:10 UTC148
973f8fbdaf80atagen2026-09-28 09:10 UTC149
function mediaResponse(reply: MediaReply | null, rangeHeader: string | null, attachment: boolean): Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC150
  if (!reply) return new Response(null, { status: 504 });
973f8fbdaf80atagen2026-09-28 09:10 UTC151
  if (reply.error) return new Response(reply.error.message, { status: statusForKind(reply.error.kind) });
973f8fbdaf80atagen2026-09-28 09:10 UTC152
  const bytes = reply.bytes ?? new Uint8Array();
973f8fbdaf80atagen2026-09-28 09:10 UTC153
  const { body, status, contentRange } = sliceForRange(bytes, rangeHeader);
973f8fbdaf80atagen2026-09-28 09:10 UTC154
  const headers = new Headers({
973f8fbdaf80atagen2026-09-28 09:10 UTC155
    'Content-Type': reply.content_type ?? 'application/octet-stream',
973f8fbdaf80atagen2026-09-28 09:10 UTC156
    'Cache-Control': 'private, max-age=3600',
973f8fbdaf80atagen2026-09-28 09:10 UTC157
    'X-Content-Type-Options': 'nosniff'
973f8fbdaf80atagen2026-09-28 09:10 UTC158
  });
973f8fbdaf80atagen2026-09-28 09:10 UTC159
  if (attachment) {
973f8fbdaf80atagen2026-09-28 09:10 UTC160
    headers.set('Content-Disposition', reply.content_type ? 'inline' : 'attachment');
973f8fbdaf80atagen2026-09-28 09:10 UTC161
    headers.set('Content-Security-Policy', 'sandbox');
973f8fbdaf80atagen2026-09-28 09:10 UTC162
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC163
  if (contentRange) {
973f8fbdaf80atagen2026-09-28 09:10 UTC164
    headers.set('Content-Range', contentRange);
973f8fbdaf80atagen2026-09-28 09:10 UTC165
    headers.set('Accept-Ranges', 'bytes');
973f8fbdaf80atagen2026-09-28 09:10 UTC166
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC167
  return new Response(body, { status, headers });
973f8fbdaf80atagen2026-09-28 09:10 UTC168
}
973f8fbdaf80atagen2026-09-28 09:10 UTC169
973f8fbdaf80atagen2026-09-28 09:10 UTC170
interface ByteRange {
973f8fbdaf80atagen2026-09-28 09:10 UTC171
  start: number;
973f8fbdaf80atagen2026-09-28 09:10 UTC172
  end: number;
973f8fbdaf80atagen2026-09-28 09:10 UTC173
}
973f8fbdaf80atagen2026-09-28 09:10 UTC174
973f8fbdaf80atagen2026-09-28 09:10 UTC175
/** Unlike sliceForRange, the total comes from the server's declared_size, not a buffer already in hand. */
973f8fbdaf80atagen2026-09-28 09:10 UTC176
function parseByteRange(rangeHeader: string | null, total: number): ByteRange | null {
973f8fbdaf80atagen2026-09-28 09:10 UTC177
  const match = rangeHeader ? /^bytes=(\d*)-(\d*)$/.exec(rangeHeader) : null;
973f8fbdaf80atagen2026-09-28 09:10 UTC178
  if (!match) return null;
973f8fbdaf80atagen2026-09-28 09:10 UTC179
  let start = match[1] ? parseInt(match[1], 10) : undefined;
973f8fbdaf80atagen2026-09-28 09:10 UTC180
  let end = match[2] ? parseInt(match[2], 10) : undefined;
973f8fbdaf80atagen2026-09-28 09:10 UTC181
  if (start === undefined && end !== undefined) {
973f8fbdaf80atagen2026-09-28 09:10 UTC182
    start = Math.max(total - end, 0);
973f8fbdaf80atagen2026-09-28 09:10 UTC183
    end = total - 1;
973f8fbdaf80atagen2026-09-28 09:10 UTC184
  } else if (end === undefined) {
973f8fbdaf80atagen2026-09-28 09:10 UTC185
    start = start ?? 0;
973f8fbdaf80atagen2026-09-28 09:10 UTC186
    end = total - 1;
973f8fbdaf80atagen2026-09-28 09:10 UTC187
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC188
  if (start === undefined || start > end || start >= total) return null;
973f8fbdaf80atagen2026-09-28 09:10 UTC189
  return { start, end: Math.min(end, total - 1) };
973f8fbdaf80atagen2026-09-28 09:10 UTC190
}
973f8fbdaf80atagen2026-09-28 09:10 UTC191
973f8fbdaf80atagen2026-09-28 09:10 UTC192
function attachmentHeaders(contentType: string | null): Headers {
973f8fbdaf80atagen2026-09-28 09:10 UTC193
  return new Headers({
973f8fbdaf80atagen2026-09-28 09:10 UTC194
    'Content-Type': contentType ?? 'application/octet-stream',
973f8fbdaf80atagen2026-09-28 09:10 UTC195
    'Cache-Control': 'private, max-age=3600',
973f8fbdaf80atagen2026-09-28 09:10 UTC196
    'X-Content-Type-Options': 'nosniff',
973f8fbdaf80atagen2026-09-28 09:10 UTC197
    'Content-Disposition': contentType ? 'inline' : 'attachment',
973f8fbdaf80atagen2026-09-28 09:10 UTC198
    'Content-Security-Policy': 'sandbox'
973f8fbdaf80atagen2026-09-28 09:10 UTC199
  });
973f8fbdaf80atagen2026-09-28 09:10 UTC200
}
973f8fbdaf80atagen2026-09-28 09:10 UTC201
973f8fbdaf80atagen2026-09-28 09:10 UTC202
/**
973f8fbdaf80atagen2026-09-28 09:10 UTC203
 * The Authorization header carries the account's access token, so it's only worth the risk on the
973f8fbdaf80atagen2026-09-28 09:10 UTC204
 * exact authenticated media endpoints, and only when the URL being fetched is the one `locate` named
973f8fbdaf80atagen2026-09-28 09:10 UTC205
 * (never a redirect target, should a future caller pass one through).
973f8fbdaf80atagen2026-09-28 09:10 UTC206
 */
973f8fbdaf80atagen2026-09-28 09:10 UTC207
function shouldAuthorize(fetchUrl: string, locatedUrl: string): boolean {
973f8fbdaf80atagen2026-09-28 09:10 UTC208
  try {
973f8fbdaf80atagen2026-09-28 09:10 UTC209
    const target = new URL(fetchUrl);
973f8fbdaf80atagen2026-09-28 09:10 UTC210
    const located = new URL(locatedUrl);
973f8fbdaf80atagen2026-09-28 09:10 UTC211
    if (target.origin !== located.origin) return false;
973f8fbdaf80atagen2026-09-28 09:10 UTC212
    return target.pathname.startsWith('/_matrix/client/v1/media/') || target.pathname.startsWith('/_matrix/media/');
973f8fbdaf80atagen2026-09-28 09:10 UTC213
  } catch {
973f8fbdaf80atagen2026-09-28 09:10 UTC214
    return false;
973f8fbdaf80atagen2026-09-28 09:10 UTC215
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC216
}
973f8fbdaf80atagen2026-09-28 09:10 UTC217
dffd63b58af4atagen2026-09-28 09:10 UTC218
function concatChunks(parts: Uint8Array[]): Uint8Array<ArrayBuffer> {
973f8fbdaf80atagen2026-09-28 09:10 UTC219
  const total = parts.reduce((sum, part) => sum + part.byteLength, 0);
973f8fbdaf80atagen2026-09-28 09:10 UTC220
  const out = new Uint8Array(total);
973f8fbdaf80atagen2026-09-28 09:10 UTC221
  let offset = 0;
973f8fbdaf80atagen2026-09-28 09:10 UTC222
  for (const part of parts) {
973f8fbdaf80atagen2026-09-28 09:10 UTC223
    out.set(part, offset);
973f8fbdaf80atagen2026-09-28 09:10 UTC224
    offset += part.byteLength;
973f8fbdaf80atagen2026-09-28 09:10 UTC225
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC226
  return out;
973f8fbdaf80atagen2026-09-28 09:10 UTC227
}
973f8fbdaf80atagen2026-09-28 09:10 UTC228
dffd63b58af4atagen2026-09-28 09:10 UTC229
async function readUpTo(
dffd63b58af4atagen2026-09-28 09:10 UTC230
  reader: ReadableStreamDefaultReader<Uint8Array<ArrayBuffer>>,
dffd63b58af4atagen2026-09-28 09:10 UTC231
  limit: number
dffd63b58af4atagen2026-09-28 09:10 UTC232
): Promise<Uint8Array<ArrayBuffer>> {
973f8fbdaf80atagen2026-09-28 09:10 UTC233
  const chunks: Uint8Array[] = [];
973f8fbdaf80atagen2026-09-28 09:10 UTC234
  let total = 0;
973f8fbdaf80atagen2026-09-28 09:10 UTC235
  while (total < limit) {
973f8fbdaf80atagen2026-09-28 09:10 UTC236
    const { value, done } = await reader.read();
973f8fbdaf80atagen2026-09-28 09:10 UTC237
    if (done) break;
973f8fbdaf80atagen2026-09-28 09:10 UTC238
    chunks.push(value);
973f8fbdaf80atagen2026-09-28 09:10 UTC239
    total += value.byteLength;
973f8fbdaf80atagen2026-09-28 09:10 UTC240
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC241
  reader.cancel().catch(() => undefined);
973f8fbdaf80atagen2026-09-28 09:10 UTC242
  return concatChunks(chunks).subarray(0, Math.min(limit, total));
973f8fbdaf80atagen2026-09-28 09:10 UTC243
}
973f8fbdaf80atagen2026-09-28 09:10 UTC244
973f8fbdaf80atagen2026-09-28 09:10 UTC245
/**
973f8fbdaf80atagen2026-09-28 09:10 UTC246
 * Ciphertext in, plaintext out: skips the bytes the upstream sent but the caller didn't ask for
973f8fbdaf80atagen2026-09-28 09:10 UTC247
 * (only nonzero when the upstream ignored the Range request), decrypts, drops the sub-16-byte
973f8fbdaf80atagen2026-09-28 09:10 UTC248
 * alignment prefix from the very first output, and stops once `limit` bytes have been produced.
973f8fbdaf80atagen2026-09-28 09:10 UTC249
 */
973f8fbdaf80atagen2026-09-28 09:10 UTC250
async function* decryptedChunks(
dffd63b58af4atagen2026-09-28 09:10 UTC251
  reader: ReadableStreamDefaultReader<Uint8Array<ArrayBuffer>>,
973f8fbdaf80atagen2026-09-28 09:10 UTC252
  decryptor: RangeDecryptor | null,
973f8fbdaf80atagen2026-09-28 09:10 UTC253
  skipBytes: number,
973f8fbdaf80atagen2026-09-28 09:10 UTC254
  dropPrefix: number,
973f8fbdaf80atagen2026-09-28 09:10 UTC255
  limit: number
973f8fbdaf80atagen2026-09-28 09:10 UTC256
): AsyncGenerator<Uint8Array> {
973f8fbdaf80atagen2026-09-28 09:10 UTC257
  let toSkip = skipBytes;
973f8fbdaf80atagen2026-09-28 09:10 UTC258
  let toDrop = dropPrefix;
973f8fbdaf80atagen2026-09-28 09:10 UTC259
  let remaining = limit;
973f8fbdaf80atagen2026-09-28 09:10 UTC260
  while (remaining > 0) {
973f8fbdaf80atagen2026-09-28 09:10 UTC261
    const { value, done } = await reader.read();
973f8fbdaf80atagen2026-09-28 09:10 UTC262
    if (done) break;
973f8fbdaf80atagen2026-09-28 09:10 UTC263
    let chunk = value;
973f8fbdaf80atagen2026-09-28 09:10 UTC264
    if (toSkip > 0) {
973f8fbdaf80atagen2026-09-28 09:10 UTC265
      if (chunk.byteLength <= toSkip) {
973f8fbdaf80atagen2026-09-28 09:10 UTC266
        toSkip -= chunk.byteLength;
973f8fbdaf80atagen2026-09-28 09:10 UTC267
        continue;
973f8fbdaf80atagen2026-09-28 09:10 UTC268
      }
973f8fbdaf80atagen2026-09-28 09:10 UTC269
      chunk = chunk.subarray(toSkip);
973f8fbdaf80atagen2026-09-28 09:10 UTC270
      toSkip = 0;
973f8fbdaf80atagen2026-09-28 09:10 UTC271
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC272
    const plain = decryptor ? await decryptor.push(chunk) : chunk;
973f8fbdaf80atagen2026-09-28 09:10 UTC273
    if (plain.byteLength === 0) continue;
973f8fbdaf80atagen2026-09-28 09:10 UTC274
    const emitted = emit(plain);
973f8fbdaf80atagen2026-09-28 09:10 UTC275
    if (emitted) yield emitted;
973f8fbdaf80atagen2026-09-28 09:10 UTC276
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC277
  if (remaining > 0 && decryptor) {
973f8fbdaf80atagen2026-09-28 09:10 UTC278
    const emitted = emit(await decryptor.finish());
973f8fbdaf80atagen2026-09-28 09:10 UTC279
    if (emitted) yield emitted;
973f8fbdaf80atagen2026-09-28 09:10 UTC280
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC281
  reader.cancel().catch(() => undefined);
973f8fbdaf80atagen2026-09-28 09:10 UTC282
973f8fbdaf80atagen2026-09-28 09:10 UTC283
  function emit(chunk: Uint8Array): Uint8Array | null {
973f8fbdaf80atagen2026-09-28 09:10 UTC284
    let out = chunk;
973f8fbdaf80atagen2026-09-28 09:10 UTC285
    if (toDrop > 0) {
973f8fbdaf80atagen2026-09-28 09:10 UTC286
      if (out.byteLength <= toDrop) {
973f8fbdaf80atagen2026-09-28 09:10 UTC287
        toDrop -= out.byteLength;
973f8fbdaf80atagen2026-09-28 09:10 UTC288
        return null;
973f8fbdaf80atagen2026-09-28 09:10 UTC289
      }
973f8fbdaf80atagen2026-09-28 09:10 UTC290
      out = out.subarray(toDrop);
973f8fbdaf80atagen2026-09-28 09:10 UTC291
      toDrop = 0;
973f8fbdaf80atagen2026-09-28 09:10 UTC292
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC293
    if (out.byteLength > remaining) out = out.subarray(0, remaining);
973f8fbdaf80atagen2026-09-28 09:10 UTC294
    remaining -= out.byteLength;
973f8fbdaf80atagen2026-09-28 09:10 UTC295
    return out.byteLength > 0 ? out : null;
973f8fbdaf80atagen2026-09-28 09:10 UTC296
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC297
}
973f8fbdaf80atagen2026-09-28 09:10 UTC298
973f8fbdaf80atagen2026-09-28 09:10 UTC299
async function headSniff(mediaId: string, file: MediaFileWire, token: string | null, retried: boolean): Promise<string | null> {
973f8fbdaf80atagen2026-09-28 09:10 UTC300
  const cached = sniffCache.get(mediaId);
973f8fbdaf80atagen2026-09-28 09:10 UTC301
  if (cached && cached.expiresAt > Date.now()) return cached.contentType;
973f8fbdaf80atagen2026-09-28 09:10 UTC302
  const headers = new Headers({ Range: 'bytes=0-79' });
973f8fbdaf80atagen2026-09-28 09:10 UTC303
  if (shouldAuthorize(file.url, file.url) && token) headers.set('Authorization', `Bearer ${token}`);
973f8fbdaf80atagen2026-09-28 09:10 UTC304
  let upstream: Response;
973f8fbdaf80atagen2026-09-28 09:10 UTC305
  try {
973f8fbdaf80atagen2026-09-28 09:10 UTC306
    upstream = await fetch(file.url, { headers });
973f8fbdaf80atagen2026-09-28 09:10 UTC307
  } catch {
973f8fbdaf80atagen2026-09-28 09:10 UTC308
    return null;
973f8fbdaf80atagen2026-09-28 09:10 UTC309
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC310
  if (upstream.status === 401 && !retried) {
973f8fbdaf80atagen2026-09-28 09:10 UTC311
    const fresh = await locate(mediaId, 'Full', true);
973f8fbdaf80atagen2026-09-28 09:10 UTC312
    if (fresh && typeof fresh.location === 'object' && 'File' in fresh.location) {
973f8fbdaf80atagen2026-09-28 09:10 UTC313
      return headSniff(mediaId, fresh.location.File, fresh.token, true);
973f8fbdaf80atagen2026-09-28 09:10 UTC314
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC315
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC316
  if (!upstream.body || (!upstream.ok && upstream.status !== 206)) return null;
973f8fbdaf80atagen2026-09-28 09:10 UTC317
  const head = await readUpTo(upstream.body.getReader(), 80);
973f8fbdaf80atagen2026-09-28 09:10 UTC318
  let contentType: string | null;
973f8fbdaf80atagen2026-09-28 09:10 UTC319
  if (file.encryption) {
973f8fbdaf80atagen2026-09-28 09:10 UTC320
    const key = await importAttachmentKey(file.encryption.key);
973f8fbdaf80atagen2026-09-28 09:10 UTC321
    const iv = decodeAttachmentIv(file.encryption.iv);
973f8fbdaf80atagen2026-09-28 09:10 UTC322
    const decryptor = new RangeDecryptor(key, iv, 0);
973f8fbdaf80atagen2026-09-28 09:10 UTC323
    contentType = attachmentContentType(concatChunks([await decryptor.push(head), await decryptor.finish()]).subarray(0, 64));
973f8fbdaf80atagen2026-09-28 09:10 UTC324
  } else {
973f8fbdaf80atagen2026-09-28 09:10 UTC325
    contentType = attachmentContentType(head.subarray(0, 64));
973f8fbdaf80atagen2026-09-28 09:10 UTC326
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC327
  sniffCache.set(mediaId, { contentType, expiresAt: Date.now() + LOCATE_TTL_MS });
973f8fbdaf80atagen2026-09-28 09:10 UTC328
  return contentType;
973f8fbdaf80atagen2026-09-28 09:10 UTC329
}
973f8fbdaf80atagen2026-09-28 09:10 UTC330
973f8fbdaf80atagen2026-09-28 09:10 UTC331
function wholeFileFallback(mediaId: string, rangeHeader: string | null): Promise<Response> {
973f8fbdaf80atagen2026-09-28 09:10 UTC332
  return requestMedia('media', { media_id: mediaId, size: 'Full' }).then((reply) => mediaResponse(reply, rangeHeader, true));
973f8fbdaf80atagen2026-09-28 09:10 UTC333
}
973f8fbdaf80atagen2026-09-28 09:10 UTC334
973f8fbdaf80atagen2026-09-28 09:10 UTC335
async function streamFullMedia(mediaId: string, rangeHeader: string | null, entry?: LocateEntry, retried = false): Promise<Response> {
973f8fbdaf80atagen2026-09-28 09:10 UTC336
  const located = entry ?? (await locate(mediaId, 'Full', false));
973f8fbdaf80atagen2026-09-28 09:10 UTC337
  if (!located) return wholeFileFallback(mediaId, rangeHeader);
973f8fbdaf80atagen2026-09-28 09:10 UTC338
  if (typeof located.location !== 'object' || !('File' in located.location)) return wholeFileFallback(mediaId, rangeHeader);
973f8fbdaf80atagen2026-09-28 09:10 UTC339
  const file = located.location.File;
973f8fbdaf80atagen2026-09-28 09:10 UTC340
  const total = file.declared_size;
973f8fbdaf80atagen2026-09-28 09:10 UTC341
  if (total == null) return wholeFileFallback(mediaId, rangeHeader);
973f8fbdaf80atagen2026-09-28 09:10 UTC342
973f8fbdaf80atagen2026-09-28 09:10 UTC343
  const requested = parseByteRange(rangeHeader, total);
973f8fbdaf80atagen2026-09-28 09:10 UTC344
  const start = requested?.start ?? 0;
973f8fbdaf80atagen2026-09-28 09:10 UTC345
  const end = requested?.end ?? total - 1;
973f8fbdaf80atagen2026-09-28 09:10 UTC346
  const alignedStart = start - (start % 16);
973f8fbdaf80atagen2026-09-28 09:10 UTC347
973f8fbdaf80atagen2026-09-28 09:10 UTC348
  const headers = new Headers({ Range: `bytes=${alignedStart}-${end}` });
973f8fbdaf80atagen2026-09-28 09:10 UTC349
  if (shouldAuthorize(file.url, file.url) && located.token) headers.set('Authorization', `Bearer ${located.token}`);
973f8fbdaf80atagen2026-09-28 09:10 UTC350
973f8fbdaf80atagen2026-09-28 09:10 UTC351
  let upstream: Response;
973f8fbdaf80atagen2026-09-28 09:10 UTC352
  try {
973f8fbdaf80atagen2026-09-28 09:10 UTC353
    upstream = await fetch(file.url, { headers });
973f8fbdaf80atagen2026-09-28 09:10 UTC354
  } catch {
973f8fbdaf80atagen2026-09-28 09:10 UTC355
    return new Response('media upstream unreachable', { status: 502 });
973f8fbdaf80atagen2026-09-28 09:10 UTC356
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC357
  if (upstream.status === 401 && !retried) {
973f8fbdaf80atagen2026-09-28 09:10 UTC358
    const fresh = await locate(mediaId, 'Full', true);
973f8fbdaf80atagen2026-09-28 09:10 UTC359
    if (fresh) return streamFullMedia(mediaId, rangeHeader, fresh, true);
973f8fbdaf80atagen2026-09-28 09:10 UTC360
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC361
  if (!upstream.body || (!upstream.ok && upstream.status !== 206)) {
973f8fbdaf80atagen2026-09-28 09:10 UTC362
    return new Response('media upstream error', { status: 502 });
973f8fbdaf80atagen2026-09-28 09:10 UTC363
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC364
973f8fbdaf80atagen2026-09-28 09:10 UTC365
  const decryptor = file.encryption
973f8fbdaf80atagen2026-09-28 09:10 UTC366
    ? new RangeDecryptor(await importAttachmentKey(file.encryption.key), decodeAttachmentIv(file.encryption.iv), alignedStart / 16)
973f8fbdaf80atagen2026-09-28 09:10 UTC367
    : null;
973f8fbdaf80atagen2026-09-28 09:10 UTC368
  const skipBytes = upstream.status === 206 ? 0 : alignedStart;
973f8fbdaf80atagen2026-09-28 09:10 UTC369
  const dropPrefix = start - alignedStart;
973f8fbdaf80atagen2026-09-28 09:10 UTC370
  const limit = end - start + 1;
973f8fbdaf80atagen2026-09-28 09:10 UTC371
973f8fbdaf80atagen2026-09-28 09:10 UTC372
  const reader = upstream.body.getReader();
973f8fbdaf80atagen2026-09-28 09:10 UTC373
  const chunks = decryptedChunks(reader, decryptor, skipBytes, dropPrefix, limit);
973f8fbdaf80atagen2026-09-28 09:10 UTC374
973f8fbdaf80atagen2026-09-28 09:10 UTC375
  const prefetched: Uint8Array[] = [];
973f8fbdaf80atagen2026-09-28 09:10 UTC376
  let prefetchedLen = 0;
973f8fbdaf80atagen2026-09-28 09:10 UTC377
  let chunksDone = false;
973f8fbdaf80atagen2026-09-28 09:10 UTC378
  let contentType: string | null;
973f8fbdaf80atagen2026-09-28 09:10 UTC379
973f8fbdaf80atagen2026-09-28 09:10 UTC380
  if (alignedStart === 0) {
973f8fbdaf80atagen2026-09-28 09:10 UTC381
    while (prefetchedLen < 64) {
973f8fbdaf80atagen2026-09-28 09:10 UTC382
      const step = await chunks.next();
973f8fbdaf80atagen2026-09-28 09:10 UTC383
      if (step.done) {
973f8fbdaf80atagen2026-09-28 09:10 UTC384
        chunksDone = true;
973f8fbdaf80atagen2026-09-28 09:10 UTC385
        break;
973f8fbdaf80atagen2026-09-28 09:10 UTC386
      }
973f8fbdaf80atagen2026-09-28 09:10 UTC387
      prefetched.push(step.value);
973f8fbdaf80atagen2026-09-28 09:10 UTC388
      prefetchedLen += step.value.byteLength;
973f8fbdaf80atagen2026-09-28 09:10 UTC389
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC390
    contentType = attachmentContentType(concatChunks(prefetched).subarray(0, Math.min(64, prefetchedLen)));
973f8fbdaf80atagen2026-09-28 09:10 UTC391
    sniffCache.set(mediaId, { contentType, expiresAt: Date.now() + LOCATE_TTL_MS });
973f8fbdaf80atagen2026-09-28 09:10 UTC392
  } else {
973f8fbdaf80atagen2026-09-28 09:10 UTC393
    contentType = await headSniff(mediaId, file, located.token, retried);
973f8fbdaf80atagen2026-09-28 09:10 UTC394
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC395
973f8fbdaf80atagen2026-09-28 09:10 UTC396
  const body = new ReadableStream<Uint8Array>({
973f8fbdaf80atagen2026-09-28 09:10 UTC397
    async pull(controller) {
973f8fbdaf80atagen2026-09-28 09:10 UTC398
      const next = prefetched.shift();
973f8fbdaf80atagen2026-09-28 09:10 UTC399
      if (next) {
973f8fbdaf80atagen2026-09-28 09:10 UTC400
        controller.enqueue(next);
973f8fbdaf80atagen2026-09-28 09:10 UTC401
        return;
973f8fbdaf80atagen2026-09-28 09:10 UTC402
      }
973f8fbdaf80atagen2026-09-28 09:10 UTC403
      if (chunksDone) {
973f8fbdaf80atagen2026-09-28 09:10 UTC404
        controller.close();
973f8fbdaf80atagen2026-09-28 09:10 UTC405
        return;
973f8fbdaf80atagen2026-09-28 09:10 UTC406
      }
973f8fbdaf80atagen2026-09-28 09:10 UTC407
      const step = await chunks.next();
973f8fbdaf80atagen2026-09-28 09:10 UTC408
      if (step.done) controller.close();
973f8fbdaf80atagen2026-09-28 09:10 UTC409
      else controller.enqueue(step.value);
973f8fbdaf80atagen2026-09-28 09:10 UTC410
    },
973f8fbdaf80atagen2026-09-28 09:10 UTC411
    cancel() {
973f8fbdaf80atagen2026-09-28 09:10 UTC412
      reader.cancel().catch(() => undefined);
973f8fbdaf80atagen2026-09-28 09:10 UTC413
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC414
  });
973f8fbdaf80atagen2026-09-28 09:10 UTC415
973f8fbdaf80atagen2026-09-28 09:10 UTC416
  const responseHeaders = attachmentHeaders(contentType);
973f8fbdaf80atagen2026-09-28 09:10 UTC417
  responseHeaders.set('Accept-Ranges', 'bytes');
973f8fbdaf80atagen2026-09-28 09:10 UTC418
  if (requested) {
973f8fbdaf80atagen2026-09-28 09:10 UTC419
    responseHeaders.set('Content-Range', `bytes ${start}-${end}/${total}`);
973f8fbdaf80atagen2026-09-28 09:10 UTC420
    return new Response(body, { status: 206, headers: responseHeaders });
973f8fbdaf80atagen2026-09-28 09:10 UTC421
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC422
  return new Response(body, { status: 200, headers: responseHeaders });
973f8fbdaf80atagen2026-09-28 09:10 UTC423
}
973f8fbdaf80atagen2026-09-28 09:10 UTC424
973f8fbdaf80atagen2026-09-28 09:10 UTC425
async function cacheFirst(request: Request): Promise<Response> {
973f8fbdaf80atagen2026-09-28 09:10 UTC426
  const cached = await caches.match(request);
973f8fbdaf80atagen2026-09-28 09:10 UTC427
  if (cached) return cached;
973f8fbdaf80atagen2026-09-28 09:10 UTC428
  const response = await fetch(request);
973f8fbdaf80atagen2026-09-28 09:10 UTC429
  if (response.ok) {
973f8fbdaf80atagen2026-09-28 09:10 UTC430
    const cache = await caches.open(CACHE_NAME);
973f8fbdaf80atagen2026-09-28 09:10 UTC431
    void cache.put(request, response.clone());
973f8fbdaf80atagen2026-09-28 09:10 UTC432
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC433
  return response;
973f8fbdaf80atagen2026-09-28 09:10 UTC434
}
973f8fbdaf80atagen2026-09-28 09:10 UTC435
cf23dbf4e77catagen2026-10-23 06:36 UTC436
/** Keyed by mxc alone, so the cached bytes outlive the access token and survive restarts. */
cf23dbf4e77catagen2026-10-23 06:36 UTC437
async function cachedAvatar(request: Request, mxc: string): Promise<Response> {
cf23dbf4e77catagen2026-10-23 06:36 UTC438
  const cache = await caches.open(AVATAR_CACHE);
cf23dbf4e77catagen2026-10-23 06:36 UTC439
  const cached = await cache.match(request);
cf23dbf4e77catagen2026-10-23 06:36 UTC440
  if (cached) return cached;
cf23dbf4e77catagen2026-10-23 06:36 UTC441
  const response = mediaResponse(await requestMedia('avatar', { mxc }), null, false);
cf23dbf4e77catagen2026-10-23 06:36 UTC442
  if (response.ok) void cache.put(request, response.clone());
cf23dbf4e77catagen2026-10-23 06:36 UTC443
  return response;
cf23dbf4e77catagen2026-10-23 06:36 UTC444
}
cf23dbf4e77catagen2026-10-23 06:36 UTC445
973f8fbdaf80atagen2026-09-28 09:10 UTC446
async function networkFirstNavigation(request: Request): Promise<Response> {
973f8fbdaf80atagen2026-09-28 09:10 UTC447
  try {
973f8fbdaf80atagen2026-09-28 09:10 UTC448
    return await fetch(request);
973f8fbdaf80atagen2026-09-28 09:10 UTC449
  } catch {
973f8fbdaf80atagen2026-09-28 09:10 UTC450
    const cached = await caches.match('/index.html');
973f8fbdaf80atagen2026-09-28 09:10 UTC451
    if (cached) return cached;
973f8fbdaf80atagen2026-09-28 09:10 UTC452
    throw new Error('offline and no cached shell');
973f8fbdaf80atagen2026-09-28 09:10 UTC453
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC454
}
973f8fbdaf80atagen2026-09-28 09:10 UTC455
973f8fbdaf80atagen2026-09-28 09:10 UTC456
sw.addEventListener('fetch', (event) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC457
  const url = new URL(event.request.url);
973f8fbdaf80atagen2026-09-28 09:10 UTC458
  const range = event.request.headers.get('Range');
973f8fbdaf80atagen2026-09-28 09:10 UTC459
973f8fbdaf80atagen2026-09-28 09:10 UTC460
  if (url.pathname === '/api/media') {
973f8fbdaf80atagen2026-09-28 09:10 UTC461
    const mxc = url.searchParams.get('mxc');
973f8fbdaf80atagen2026-09-28 09:10 UTC462
    if (mxc) {
cf23dbf4e77catagen2026-10-23 06:36 UTC463
      event.respondWith(cachedAvatar(event.request, mxc));
973f8fbdaf80atagen2026-09-28 09:10 UTC464
      return;
973f8fbdaf80atagen2026-09-28 09:10 UTC465
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC466
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC467
  if (url.pathname === '/api/message-media') {
973f8fbdaf80atagen2026-09-28 09:10 UTC468
    const mediaId = url.searchParams.get('media_id');
973f8fbdaf80atagen2026-09-28 09:10 UTC469
    const size = url.searchParams.get('size');
973f8fbdaf80atagen2026-09-28 09:10 UTC470
    if (mediaId && size === 'Full') {
973f8fbdaf80atagen2026-09-28 09:10 UTC471
      event.respondWith(streamFullMedia(mediaId, range));
973f8fbdaf80atagen2026-09-28 09:10 UTC472
      return;
973f8fbdaf80atagen2026-09-28 09:10 UTC473
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC474
    if (mediaId && size) {
973f8fbdaf80atagen2026-09-28 09:10 UTC475
      event.respondWith(
973f8fbdaf80atagen2026-09-28 09:10 UTC476
        requestMedia('media', { media_id: mediaId, size }).then((reply) => mediaResponse(reply, range, true))
973f8fbdaf80atagen2026-09-28 09:10 UTC477
      );
973f8fbdaf80atagen2026-09-28 09:10 UTC478
      return;
973f8fbdaf80atagen2026-09-28 09:10 UTC479
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC480
  }
544a170ead2catagen2026-10-20 11:47 UTC481
  if (url.pathname.startsWith('/assets/') || url.pathname.startsWith('/twemoji/')) {
973f8fbdaf80atagen2026-09-28 09:10 UTC482
    event.respondWith(cacheFirst(event.request));
973f8fbdaf80atagen2026-09-28 09:10 UTC483
    return;
973f8fbdaf80atagen2026-09-28 09:10 UTC484
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC485
  if (event.request.mode === 'navigate') {
973f8fbdaf80atagen2026-09-28 09:10 UTC486
    event.respondWith(networkFirstNavigation(event.request));
973f8fbdaf80atagen2026-09-28 09:10 UTC487
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC488
});