forked from akai/readest
f6dfd09d82
Replaces the URL-only placeholder extension with a full MV3 page-clipper
that builds a self-contained EPUB on the user's machine and uploads it
to the inbox.
- Captures the rendered DOM in a content script, then runs Readability,
asset bundling, and EPUB build through the shared
`convertToEpub({kind: 'page'})` pipeline inside a Chrome offscreen
document (the SW lacks DOMParser).
- Uploads the resulting EPUB directly from the offscreen page to the
new `POST /api/send/inbox/file` endpoint — keeps the bytes in one
realm because `runtime.sendMessage` JSON-serialises ArrayBuffer to
`{}` between extension contexts.
- Adds a long-lived Port + ping handshake between SW, offscreen, and
the on-demand capture content script so neither idle-eviction nor
load-order races can hang the popup.
- Localised popup, badge feedback, key-as-content i18n (`_('English source')`)
with an extract script that seeds locale stubs from i18n-langs.json
and writes a static-imports map for the runtime. All 33 locales
fully translated.
- Server: `pages/api/send/inbox/file.ts` accepts a raw EPUB body
(Content-Type: application/epub+zip), enforces the inbox pending cap,
stores to the existing send-inbox R2 bucket as `kind='file'`.
`assetBundler` now sets `credentials: 'include'` in the non-Tauri
branch so the extension SW carries paywalled-CDN cookies.
- 47 vitest cases for the extension shell (upload, badge, auth, lazy,
popup state machine, auth-bridge token sync) + 8 cases for the new
server endpoint. CI's `test_web_app` invokes both via the extended
`test:pr:web` plus a `build-browser-ext` step that catches webpack
alias / Tauri-stub regressions.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
57 lines
1.8 KiB
TypeScript
57 lines
1.8 KiB
TypeScript
/**
|
|
* Runs on `web.readest.com` (and the dev host). Reads Readest's Supabase
|
|
* session token out of the page's localStorage and copies it into
|
|
* `chrome.storage.local`, so the extension popup can authenticate to
|
|
* `/api/send/inbox` without prompting the user for credentials.
|
|
*
|
|
* No user data leaves the user's browser — the token is only ever stored in
|
|
* the extension's own storage area, scoped to the extension.
|
|
*/
|
|
|
|
interface SupabaseAuthValue {
|
|
access_token?: string;
|
|
}
|
|
|
|
function findAccessToken(): string | null {
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const key = localStorage.key(i);
|
|
if (!key || !/^sb-.*-auth-token$/.test(key)) continue;
|
|
const raw = localStorage.getItem(key);
|
|
if (!raw) continue;
|
|
try {
|
|
const value = JSON.parse(raw) as SupabaseAuthValue;
|
|
if (value && typeof value.access_token === 'string' && value.access_token) {
|
|
return value.access_token;
|
|
}
|
|
} catch {
|
|
// Malformed entries are ignored — Supabase occasionally writes partials.
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function syncToken(): void {
|
|
const token = findAccessToken();
|
|
if (token) {
|
|
chrome.storage.local.set({
|
|
readestAccessToken: token,
|
|
readestTokenAt: Date.now(),
|
|
});
|
|
} else {
|
|
// Token went away (sign-out) — clear so the extension stops showing a
|
|
// stale "signed in" state.
|
|
chrome.storage.local.remove(['readestAccessToken', 'readestTokenAt']);
|
|
}
|
|
}
|
|
|
|
syncToken();
|
|
|
|
// Refresh on storage changes too — Supabase rotates the access token a few
|
|
// times an hour. Polling localStorage isn't free, but a `storage` event
|
|
// listener fires only when the value actually changes.
|
|
window.addEventListener('storage', (event) => {
|
|
if (event.key && /^sb-.*-auth-token$/.test(event.key)) {
|
|
syncToken();
|
|
}
|
|
});
|