atagen

malice

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