feedkeeper 0.0.0-stage → 0.23.1
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.
- package/LICENSE +21 -0
- package/README.md +22 -2
- package/package.json +57 -4
- package/server/dist/api/archive.js +19 -0
- package/server/dist/api/auth.js +167 -0
- package/server/dist/api/feeds.js +207 -0
- package/server/dist/api/filters.js +32 -0
- package/server/dist/api/folders.js +53 -0
- package/server/dist/api/index.js +37 -0
- package/server/dist/api/items.js +129 -0
- package/server/dist/api/oauth.js +14 -0
- package/server/dist/api/onboarding.js +30 -0
- package/server/dist/api/push.js +51 -0
- package/server/dist/api/system.js +42 -0
- package/server/dist/api/tokens.js +28 -0
- package/server/dist/api/v1/index.js +178 -0
- package/server/dist/api/v1/resources.js +653 -0
- package/server/dist/api/v1/serialize.js +48 -0
- package/server/dist/auth/avatar.js +13 -0
- package/server/dist/auth/capabilities.js +53 -0
- package/server/dist/auth/middleware.js +77 -0
- package/server/dist/auth/pairing.js +36 -0
- package/server/dist/auth/password.js +21 -0
- package/server/dist/auth/security.js +126 -0
- package/server/dist/auth/securityRoutes.js +249 -0
- package/server/dist/auth/tokens.js +64 -0
- package/server/dist/auth/users.js +54 -0
- package/server/dist/backup-cli.js +101 -0
- package/server/dist/cli.js +135 -0
- package/server/dist/clientAddress.js +26 -0
- package/server/dist/config.js +51 -0
- package/server/dist/db/index.js +40 -0
- package/server/dist/db/migrations/0001_init.sql +70 -0
- package/server/dist/db/migrations/0002_feed_health.sql +5 -0
- package/server/dist/db/migrations/0003_retention.sql +13 -0
- package/server/dist/db/migrations/0004_bookmarks_and_filters.sql +20 -0
- package/server/dist/db/migrations/0005_folders.sql +15 -0
- package/server/dist/db/migrations/0006_feed_position.sql +5 -0
- package/server/dist/db/migrations/0007_item_image_url.sql +2 -0
- package/server/dist/db/migrations/0008_item_content_html.sql +2 -0
- package/server/dist/db/migrations/0009_item_full_content_html.sql +1 -0
- package/server/dist/db/migrations/0010_token_scope.sql +3 -0
- package/server/dist/db/migrations/0011_user_avatars.sql +8 -0
- package/server/dist/db/migrations/0012_full_text_settings.sql +5 -0
- package/server/dist/db/migrations/0013_feed_icons.sql +4 -0
- package/server/dist/db/migrations/0014_bookmark_archive.sql +16 -0
- package/server/dist/db/migrations/0015_push.sql +13 -0
- package/server/dist/db/migrations/0016_feed_badge.sql +2 -0
- package/server/dist/db/migrations/0017_devices.sql +13 -0
- package/server/dist/db/migrations/0018_change_log.sql +166 -0
- package/server/dist/db/migrations/0019_native_extensions.sql +111 -0
- package/server/dist/db/migrations/0020_fts.sql +32 -0
- package/server/dist/db/migrations/0021_note_revisions.sql +33 -0
- package/server/dist/db/migrations/0022_high_resolution_icons.sql +2 -0
- package/server/dist/db/migrations/0023_server_editions.sql +81 -0
- package/server/dist/db/migrations/0024_inbox.sql +5 -0
- package/server/dist/db/migrations/0025_edition_overview.sql +2 -0
- package/server/dist/db/migrations/0026_article_content.sql +43 -0
- package/server/dist/db/migrations/0027_oauth.sql +38 -0
- package/server/dist/db/migrations/0028_user_intro.sql +2 -0
- package/server/dist/db/migrations/0029_account_security.sql +35 -0
- package/server/dist/db/migrations/0030_feed_poll_jobs.sql +15 -0
- package/server/dist/db/migrations/0031_push_outbox.sql +19 -0
- package/server/dist/db/migrations/0032_normalize_published_at.sql +12 -0
- package/server/dist/feedkeeper.js +38 -0
- package/server/dist/feeds/archive.js +334 -0
- package/server/dist/feeds/articleContent.js +26 -0
- package/server/dist/feeds/cleanup.js +179 -0
- package/server/dist/feeds/dates.js +21 -0
- package/server/dist/feeds/discovery.js +153 -0
- package/server/dist/feeds/extractor.js +208 -0
- package/server/dist/feeds/feedIcon.js +171 -0
- package/server/dist/feeds/fetcher.js +217 -0
- package/server/dist/feeds/fullText.js +91 -0
- package/server/dist/feeds/fullTextQueue.js +159 -0
- package/server/dist/feeds/icon.js +97 -0
- package/server/dist/feeds/imageCache.js +266 -0
- package/server/dist/feeds/imageCacheSettings.js +19 -0
- package/server/dist/feeds/imageTypes.js +16 -0
- package/server/dist/feeds/inbox.js +128 -0
- package/server/dist/feeds/opml.js +190 -0
- package/server/dist/feeds/pollQueue.js +301 -0
- package/server/dist/feeds/pollSettings.js +23 -0
- package/server/dist/feeds/poller.js +223 -0
- package/server/dist/feeds/repository.js +687 -0
- package/server/dist/feeds/requestGate.js +99 -0
- package/server/dist/feeds/service.js +162 -0
- package/server/dist/feeds/ssrfGuard.js +98 -0
- package/server/dist/feeds/text.js +35 -0
- package/server/dist/mcp/digest.js +73 -0
- package/server/dist/mcp/http.js +46 -0
- package/server/dist/mcp/items.js +73 -0
- package/server/dist/mcp/promptsAndResources.js +43 -0
- package/server/dist/mcp/server.js +547 -0
- package/server/dist/native/editionSelection.js +127 -0
- package/server/dist/native/editionText.js +15 -0
- package/server/dist/native/editions.js +218 -0
- package/server/dist/native/preferences.js +66 -0
- package/server/dist/native/requests.js +35 -0
- package/server/dist/oauth/constants.js +6 -0
- package/server/dist/oauth/host.js +8 -0
- package/server/dist/oauth/pages.js +148 -0
- package/server/dist/oauth/routes.js +370 -0
- package/server/dist/oauth/store.js +104 -0
- package/server/dist/push.js +147 -0
- package/server/dist/pushOutbox.js +164 -0
- package/server/dist/security-recover-cli.js +29 -0
- package/server/dist/server.js +108 -0
- package/server/dist/setup-cli.js +97 -0
- package/server/dist/sync/changeLog.js +170 -0
- package/server/dist/sync/mutations.js +144 -0
- package/server/dist/trustProxy.js +14 -0
- package/server/dist/version.js +6 -0
- package/server/package.json +6 -0
- package/web/dist/app-icon-180.png +0 -0
- package/web/dist/app-icon-192.png +0 -0
- package/web/dist/app-icon-512.png +0 -0
- package/web/dist/assets/index-B4e2oqbo.css +2 -0
- package/web/dist/assets/index-xx6TyOwB.js +32 -0
- package/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/web/dist/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/web/dist/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/web/dist/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/web/dist/assets/manrope-latin-600-normal-4f0koTD-.woff2 +0 -0
- package/web/dist/assets/manrope-latin-600-normal-BqgrALkZ.woff +0 -0
- package/web/dist/assets/manrope-latin-700-normal-BZp_XxE4.woff2 +0 -0
- package/web/dist/assets/manrope-latin-700-normal-DGRFkw-m.woff +0 -0
- package/web/dist/favicon-32.png +0 -0
- package/web/dist/index.html +40 -0
- package/web/dist/logo.svg +50 -0
- package/web/dist/manifest.webmanifest +64 -0
- package/web/dist/robots.txt +3 -0
- package/web/dist/sw.js +198 -0
- package/web/dist/theme-init.js +10 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
|
+
<meta name="color-scheme" content="light dark" />
|
|
7
|
+
<meta name="apple-mobile-web-app-title" content="FeedKeeper" />
|
|
8
|
+
<meta name="application-name" content="FeedKeeper" />
|
|
9
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
10
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
11
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
|
12
|
+
<link rel="manifest" href="/manifest.webmanifest" />
|
|
13
|
+
<script src="/theme-init.js"></script>
|
|
14
|
+
<title>FeedKeeper</title>
|
|
15
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
16
|
+
<meta name="theme-color" content="#0f172a" />
|
|
17
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png?v=2" />
|
|
18
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/app-icon-180.png?v=2" />
|
|
19
|
+
<meta name="description" content="Self-hosted RSS/Atom feed manager and remote MCP server for AI assistants." />
|
|
20
|
+
<!-- Open Graph / Facebook -->
|
|
21
|
+
<meta property="og:type" content="website" />
|
|
22
|
+
<meta property="og:site_name" content="FeedKeeper" />
|
|
23
|
+
<meta property="og:title" content="FeedKeeper – RSS Reader & MCP Server" />
|
|
24
|
+
<meta property="og:description" content="Self-hosted RSS/Atom feed manager and remote MCP server for AI assistants." />
|
|
25
|
+
<meta property="og:image" content="/logo.svg" />
|
|
26
|
+
<!-- Twitter -->
|
|
27
|
+
<meta name="twitter:card" content="summary" />
|
|
28
|
+
<meta name="twitter:title" content="FeedKeeper – RSS Reader & MCP Server" />
|
|
29
|
+
<meta name="twitter:description" content="Self-hosted RSS/Atom feed manager and remote MCP server for AI assistants." />
|
|
30
|
+
<meta name="twitter:image" content="/logo.svg" />
|
|
31
|
+
<!-- Inline so the splash paints before any script or stylesheet loads -->
|
|
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-xx6TyOwB.js"></script>
|
|
34
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B4e2oqbo.css">
|
|
35
|
+
</head>
|
|
36
|
+
<body>
|
|
37
|
+
<div id="splash" aria-hidden="true"><img src="/logo.svg" alt="" /></div>
|
|
38
|
+
<div id="root"></div>
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
3
|
+
<svg width="686" height="685" viewBox="0 0 686 685" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
4
|
+
<g id="Logo" transform="matrix(1,0,0,1,-522.256,-225.729)">
|
|
5
|
+
<g transform="matrix(1,0,0,1,-861.996,-1144.89)">
|
|
6
|
+
<path d="M1817.97,1751.27C1820.2,1745.13 1823.24,1728.46 1823.45,1726.79C1824.89,1735.29 1826.18,1744.98 1826.18,1754.13C1826.18,1875.81 1726.39,1974.6 1604.72,1974.6C1483.04,1974.6 1384.25,1875.81 1384.25,1754.13C1384.25,1726.37 1389.4,1699.79 1398.79,1675.31C1412.13,1784.24 1505.08,1868.73 1617.6,1868.73C1702.42,1868.73 1793.28,1823.63 1817.97,1751.27Z" style="fill:url(#_Linear1);"/>
|
|
7
|
+
</g>
|
|
8
|
+
<g transform="matrix(1,0,0,1,-861.996,-1144.89)">
|
|
9
|
+
<path d="M1796.27,1649.4C1795.92,1648.73 1794.98,1648.14 1794.62,1647.47C1855.11,1686.24 1898.03,1758.06 1898.03,1833.88C1898.03,1955.62 1798.06,2054.64 1676.32,2054.64C1591.66,2054.64 1518,2006.6 1481.03,1936.53C1516.35,1960.56 1560.11,1973.52 1606.01,1973.52C1727.75,1973.52 1823.24,1876.01 1825.54,1754.28C1825.71,1745.17 1824.94,1735.93 1823.45,1726.72C1823.72,1723.75 1823.86,1720.74 1823.86,1717.69C1823.86,1691.2 1813.35,1667.13 1796.27,1649.4Z" style="fill:url(#_Linear2);"/>
|
|
10
|
+
</g>
|
|
11
|
+
<g transform="matrix(1,0,0,1,-861.996,-1144.89)">
|
|
12
|
+
<path d="M1724.94,1619.07C1722.85,1619.07 1718.26,1619.17 1718.26,1619.17C1737.83,1613.48 1762.12,1609.66 1783.52,1609.66C1905.26,1609.66 2004.09,1708.49 2004.09,1830.23C2004.09,1951.96 1905.26,2050.79 1783.52,2050.79C1767.87,2050.79 1751.09,2048.86 1736.35,2045.75C1828.11,2019.04 1896.75,1934.57 1896.75,1834.23C1896.75,1760.29 1858.54,1695.18 1801.58,1655.38C1783.44,1633.22 1755.83,1619.07 1724.94,1619.07Z" style="fill:url(#_Linear3);"/>
|
|
13
|
+
</g>
|
|
14
|
+
<g transform="matrix(1,0,0,1,-861.996,-1144.89)">
|
|
15
|
+
<path d="M1645.64,1658.76C1645.08,1659.19 1644.53,1659.62 1643.99,1660.06C1677.52,1580.84 1757.4,1524.68 1848.14,1524.68C1970.2,1524.68 2069.29,1623.78 2069.29,1745.83C2069.29,1816.22 2036.34,1878.97 1985.03,1919.48C1996.79,1892.47 2003.31,1862.68 2003.31,1831.37C2003.31,1709.32 1904.21,1610.22 1782.16,1610.22C1761.61,1610.22 1741.5,1613.4 1722.36,1619.11C1690.97,1619.91 1663.2,1635.32 1645.64,1658.76Z" style="fill:url(#_Linear4);"/>
|
|
16
|
+
</g>
|
|
17
|
+
<g transform="matrix(1,0,0,1,-861.996,-1144.89)">
|
|
18
|
+
<path d="M1628.34,1738.99C1628.31,1740.02 1628.29,1741.05 1628.27,1742.09C1612.4,1711.63 1602.76,1677.35 1602.76,1640.66C1602.76,1518.86 1701.64,1419.97 1823.45,1419.97C1945.25,1419.97 2044.14,1518.86 2044.14,1640.66C2044.14,1641.81 2044.13,1642.95 2044.11,1644.11C2007.3,1573.45 1933.38,1525.36 1848.26,1525.36C1757.57,1525.36 1679.97,1579.8 1646.23,1657.98C1633.55,1674.56 1626.02,1695.26 1626.02,1717.69C1626.02,1725.01 1626.82,1732.13 1628.34,1738.99Z" style="fill:url(#_Linear5);"/>
|
|
19
|
+
</g>
|
|
20
|
+
<g transform="matrix(1,0,0,1,-861.996,-1144.89)">
|
|
21
|
+
<path d="M1679.46,1805.28L1679.89,1805.66C1582.08,1783.16 1508.55,1695.63 1508.55,1591.02C1508.55,1469.38 1607.31,1370.62 1728.95,1370.62C1784.81,1370.62 1835.9,1391.92 1874.75,1426.21C1858.81,1422.55 1842.07,1420.5 1825.04,1420.5C1703.4,1420.5 1604.74,1518.89 1604.74,1640.53C1604.74,1676.07 1613.31,1709.61 1628.41,1739.31C1634.86,1768.04 1653.91,1792.06 1679.46,1805.28Z" style="fill:url(#_Linear6);"/>
|
|
22
|
+
</g>
|
|
23
|
+
</g>
|
|
24
|
+
<defs>
|
|
25
|
+
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(224.412,-186.191,186.191,224.412,1598.13,1920.71)">
|
|
26
|
+
<stop offset="0" style="stop-color:#cbd5e1;stop-opacity:1"/>
|
|
27
|
+
<stop offset="1" style="stop-color:#94a3b8;stop-opacity:1"/>
|
|
28
|
+
</linearGradient>
|
|
29
|
+
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.87487,-316.253,316.253,2.87487,1793.34,1964.97)">
|
|
30
|
+
<stop offset="0" style="stop-color:#94a3b8;stop-opacity:1"/>
|
|
31
|
+
<stop offset="1" style="stop-color:#475569;stop-opacity:1"/>
|
|
32
|
+
</linearGradient>
|
|
33
|
+
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-233.77,-220.784,220.784,-233.77,1957.45,1839.71)">
|
|
34
|
+
<stop offset="0" style="stop-color:#334155;stop-opacity:1"/>
|
|
35
|
+
<stop offset="1" style="stop-color:#0f172a;stop-opacity:1"/>
|
|
36
|
+
</linearGradient>
|
|
37
|
+
<linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-317.872,10.9515,-10.9515,-317.872,1972.41,1637.06)">
|
|
38
|
+
<stop offset="0" style="stop-color:#fed7aa;stop-opacity:1"/>
|
|
39
|
+
<stop offset="1" style="stop-color:#fb923c;stop-opacity:1"/>
|
|
40
|
+
</linearGradient>
|
|
41
|
+
<linearGradient id="_Linear5" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-174.067,249.927,-249.927,-174.067,1800.69,1481.59)">
|
|
42
|
+
<stop offset="0" style="stop-color:#fb923c;stop-opacity:1"/>
|
|
43
|
+
<stop offset="1" style="stop-color:#ea580c;stop-opacity:1"/>
|
|
44
|
+
</linearGradient>
|
|
45
|
+
<linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(74.0899,314.976,-314.976,74.0899,1599.97,1488.94)">
|
|
46
|
+
<stop offset="0" style="stop-color:#c2410c;stop-opacity:1"/>
|
|
47
|
+
<stop offset="1" style="stop-color:#7c2d12;stop-opacity:1"/>
|
|
48
|
+
</linearGradient>
|
|
49
|
+
</defs>
|
|
50
|
+
</svg>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "FeedKeeper",
|
|
3
|
+
"short_name": "FeedKeeper",
|
|
4
|
+
"description": "Self-hosted RSS/Atom reader and remote MCP server for AI assistants.",
|
|
5
|
+
"start_url": "/",
|
|
6
|
+
"scope": "/",
|
|
7
|
+
"display": "standalone",
|
|
8
|
+
"background_color": "#0b0f17",
|
|
9
|
+
"theme_color": "#0f172a",
|
|
10
|
+
"icons": [
|
|
11
|
+
{
|
|
12
|
+
"src": "/app-icon-180.png?v=2",
|
|
13
|
+
"sizes": "180x180",
|
|
14
|
+
"type": "image/png"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"src": "/app-icon-192.png?v=2",
|
|
18
|
+
"sizes": "192x192",
|
|
19
|
+
"type": "image/png",
|
|
20
|
+
"purpose": "any"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"src": "/app-icon-512.png?v=2",
|
|
24
|
+
"sizes": "512x512",
|
|
25
|
+
"type": "image/png",
|
|
26
|
+
"purpose": "any"
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"shortcuts": [
|
|
30
|
+
{
|
|
31
|
+
"name": "Saved articles",
|
|
32
|
+
"short_name": "Saved",
|
|
33
|
+
"url": "/items?saved=1",
|
|
34
|
+
"icons": [
|
|
35
|
+
{
|
|
36
|
+
"src": "/app-icon-192.png?v=2",
|
|
37
|
+
"sizes": "192x192",
|
|
38
|
+
"type": "image/png"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "Add feed",
|
|
44
|
+
"short_name": "Add feed",
|
|
45
|
+
"url": "/feeds?add=1",
|
|
46
|
+
"icons": [
|
|
47
|
+
{
|
|
48
|
+
"src": "/app-icon-192.png?v=2",
|
|
49
|
+
"sizes": "192x192",
|
|
50
|
+
"type": "image/png"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"share_target": {
|
|
56
|
+
"action": "/feeds",
|
|
57
|
+
"method": "GET",
|
|
58
|
+
"params": {
|
|
59
|
+
"title": "title",
|
|
60
|
+
"text": "text",
|
|
61
|
+
"url": "url"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
package/web/dist/sw.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// FeedKeeper service worker. Built into dist/sw.js, where the placeholder below is
|
|
2
|
+
// replaced with a hash of the build's file names so every release gets its own app cache.
|
|
3
|
+
const BUILD_ID = "93f2f6a49c67";
|
|
4
|
+
const STATIC_CACHE = `fk-static-${BUILD_ID}`;
|
|
5
|
+
const API_CACHE = "fk-api";
|
|
6
|
+
const SHELL = "/index.html";
|
|
7
|
+
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
|
+
// The server answers with `Vary: Origin`, but entries cached without that header must still match.
|
|
9
|
+
const MATCH = { ignoreVary: true };
|
|
10
|
+
const NETWORK_TIMEOUT_MS = 4000;
|
|
11
|
+
const MAX_CACHED_ITEM_LISTS = 30;
|
|
12
|
+
const MAX_CACHED_IMAGES = 3000;
|
|
13
|
+
|
|
14
|
+
// Read-only API responses worth showing when the network is gone. Everything else
|
|
15
|
+
// (tokens, users, settings, mutations) always goes to the server.
|
|
16
|
+
const READABLE_API = [/^\/api\/onboarding\/status$/, /^\/api\/config$/, /^\/api\/auth\/me$/, /^\/api\/feeds$/, /^\/api\/folders$/, /^\/api\/items$/, /^\/api\/filters\/muted$/];
|
|
17
|
+
const FEED_ICON = /^\/api\/feeds\/\d+\/icon$/;
|
|
18
|
+
const IMMUTABLE_API = [/^\/api\/archive\/images\/\d+$/, /^\/api\/items\/\d+\/image$/, /^\/api\/auth\/me\/avatar$/];
|
|
19
|
+
// Files next to index.html (theme script, manifest, icons) are the only other thing served from the cache. Any other
|
|
20
|
+
// path answers per request and per signed-in user, so a stored copy could show one account another's data.
|
|
21
|
+
const STATIC_FILE = /^\/[^/]+\.(?:js|css|svg|png|ico|webmanifest|txt|woff2?)$/;
|
|
22
|
+
|
|
23
|
+
self.addEventListener("install", (event) => {
|
|
24
|
+
event.waitUntil(precache().catch(() => undefined).then(() => self.skipWaiting()));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
self.addEventListener("activate", (event) => {
|
|
28
|
+
event.waitUntil(
|
|
29
|
+
(async () => {
|
|
30
|
+
const names = await caches.keys();
|
|
31
|
+
await Promise.all(names.filter((name) => name.startsWith("fk-static-") && name !== STATIC_CACHE).map((name) => caches.delete(name)));
|
|
32
|
+
await self.clients.claim();
|
|
33
|
+
})(),
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
self.addEventListener("message", (event) => {
|
|
38
|
+
if (event.data?.type !== "cache-urls" || !Array.isArray(event.data.urls)) return;
|
|
39
|
+
event.waitUntil(cacheUrls(event.data.urls));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
self.addEventListener("fetch", (event) => {
|
|
43
|
+
const { request } = event;
|
|
44
|
+
if (request.method !== "GET") return;
|
|
45
|
+
const url = new URL(request.url);
|
|
46
|
+
if (url.origin !== self.location.origin) return;
|
|
47
|
+
// Server-rendered OAuth pages and discovery documents must never be answered with the app shell.
|
|
48
|
+
if (url.pathname.startsWith("/oauth/") || url.pathname.startsWith("/.well-known/")) return;
|
|
49
|
+
|
|
50
|
+
if (request.mode === "navigate") {
|
|
51
|
+
event.respondWith(navigate(request));
|
|
52
|
+
} else if (url.pathname.startsWith("/assets/")) {
|
|
53
|
+
event.respondWith(cacheFirst(STATIC_CACHE, request));
|
|
54
|
+
} else if (url.pathname.startsWith("/api/")) {
|
|
55
|
+
// Lists for the offline copy are stored by the app itself.
|
|
56
|
+
if (url.searchParams.has("offline")) return;
|
|
57
|
+
if (FEED_ICON.test(url.pathname)) {
|
|
58
|
+
event.respondWith(staleWhileRevalidate(API_CACHE, request));
|
|
59
|
+
} else if (IMMUTABLE_API.some((pattern) => pattern.test(url.pathname))) {
|
|
60
|
+
event.respondWith(cacheFirst(API_CACHE, request, MAX_CACHED_IMAGES));
|
|
61
|
+
} else if (READABLE_API.some((pattern) => pattern.test(url.pathname))) {
|
|
62
|
+
event.respondWith(networkFirst(API_CACHE, request, url.pathname === "/api/items" ? MAX_CACHED_ITEM_LISTS : 0));
|
|
63
|
+
}
|
|
64
|
+
} else if (!url.pathname.startsWith("/mcp") && STATIC_FILE.test(url.pathname)) {
|
|
65
|
+
event.respondWith(staleWhileRevalidate(STATIC_CACHE, request));
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
async function precache() {
|
|
70
|
+
const cache = await caches.open(STATIC_CACHE);
|
|
71
|
+
const response = await fetch(SHELL, { cache: "reload" });
|
|
72
|
+
if (!response.ok) return;
|
|
73
|
+
const html = await response.clone().text();
|
|
74
|
+
await cache.put(SHELL, response);
|
|
75
|
+
const assets = [...html.matchAll(/(?:src|href)="(\/assets\/[^"]+)"/g)].map((match) => match[1]);
|
|
76
|
+
await Promise.all([...PRECACHE, ...assets].map((asset) => cache.add(asset).catch(() => undefined)));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function cacheUrls(urls) {
|
|
80
|
+
const cache = await caches.open(STATIC_CACHE);
|
|
81
|
+
await Promise.all(
|
|
82
|
+
urls
|
|
83
|
+
.filter((raw) => typeof raw === "string" && raw.startsWith("/assets/"))
|
|
84
|
+
.map(async (path) => {
|
|
85
|
+
if (!(await cache.match(path, MATCH))) await cache.add(path).catch(() => undefined);
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function withTimeout(promise, ms) {
|
|
91
|
+
return Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), ms))]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function navigate(request) {
|
|
95
|
+
const cache = await caches.open(STATIC_CACHE);
|
|
96
|
+
try {
|
|
97
|
+
const response = await withTimeout(fetch(request), NETWORK_TIMEOUT_MS);
|
|
98
|
+
if (response.ok && response.headers.get("content-type")?.includes("text/html")) cache.put(SHELL, response.clone());
|
|
99
|
+
return response;
|
|
100
|
+
} catch {
|
|
101
|
+
return (await cache.match(SHELL, MATCH)) ?? Response.error();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function cacheFirst(cacheName, request, limit = 0) {
|
|
106
|
+
const cache = await caches.open(cacheName);
|
|
107
|
+
const cached = await cache.match(request, MATCH);
|
|
108
|
+
if (cached) return cached;
|
|
109
|
+
const response = await fetch(request);
|
|
110
|
+
if (response.ok) {
|
|
111
|
+
await cache.put(request, response.clone());
|
|
112
|
+
if (limit) await trim(cache, (key) => /\/api\/(archive\/images\/|items\/\d+\/image)/.test(key.url), limit);
|
|
113
|
+
}
|
|
114
|
+
return response;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function networkFirst(cacheName, request, limit) {
|
|
118
|
+
const cache = await caches.open(cacheName);
|
|
119
|
+
try {
|
|
120
|
+
const response = await withTimeout(fetch(request), NETWORK_TIMEOUT_MS);
|
|
121
|
+
if (response.ok) {
|
|
122
|
+
await cache.put(request, response.clone());
|
|
123
|
+
if (limit) await trim(cache, (key) => new URL(key.url).pathname === new URL(request.url).pathname, limit);
|
|
124
|
+
}
|
|
125
|
+
return response;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
const cached = await cache.match(request, MATCH);
|
|
128
|
+
if (cached) return cached;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function staleWhileRevalidate(cacheName, request) {
|
|
134
|
+
const cache = await caches.open(cacheName);
|
|
135
|
+
const cached = await cache.match(request, MATCH);
|
|
136
|
+
const refresh = fetch(request)
|
|
137
|
+
.then((response) => {
|
|
138
|
+
if (response.ok) cache.put(request, response.clone());
|
|
139
|
+
return response;
|
|
140
|
+
})
|
|
141
|
+
.catch(() => undefined);
|
|
142
|
+
return cached ?? (await refresh) ?? Response.error();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Drop the oldest entries so a long-lived install does not grow without bound. */
|
|
146
|
+
async function trim(cache, matches, limit) {
|
|
147
|
+
const keys = (await cache.keys()).filter(matches);
|
|
148
|
+
await Promise.all(keys.slice(0, Math.max(0, keys.length - limit)).map((key) => cache.delete(key)));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Web Push: show what the server announces and open the app where it leads.
|
|
152
|
+
self.addEventListener("push", (event) => {
|
|
153
|
+
let data = {};
|
|
154
|
+
try {
|
|
155
|
+
data = event.data ? event.data.json() : {};
|
|
156
|
+
} catch {
|
|
157
|
+
data = { body: event.data ? event.data.text() : "" };
|
|
158
|
+
}
|
|
159
|
+
event.waitUntil(
|
|
160
|
+
(async () => {
|
|
161
|
+
if (typeof data.unread === "number" && self.navigator.setAppBadge) {
|
|
162
|
+
await (data.unread > 0 ? self.navigator.setAppBadge(data.unread) : self.navigator.clearAppBadge()).catch(() => undefined);
|
|
163
|
+
}
|
|
164
|
+
await self.registration.showNotification(data.title || "FeedKeeper", {
|
|
165
|
+
body: data.body || "",
|
|
166
|
+
icon: "/app-icon-192.png",
|
|
167
|
+
tag: data.tag,
|
|
168
|
+
// Without this a second message for the same feed would replace the first silently.
|
|
169
|
+
renotify: Boolean(data.tag),
|
|
170
|
+
data: { url: data.url },
|
|
171
|
+
});
|
|
172
|
+
})(),
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
self.addEventListener("notificationclick", (event) => {
|
|
177
|
+
event.notification.close();
|
|
178
|
+
// Only addresses inside the app are followed.
|
|
179
|
+
let target = new URL("/items", self.location.origin);
|
|
180
|
+
try {
|
|
181
|
+
const requested = new URL(event.notification.data?.url || "/items", self.location.origin);
|
|
182
|
+
if (requested.origin === self.location.origin) target = requested;
|
|
183
|
+
} catch {
|
|
184
|
+
// keep the default
|
|
185
|
+
}
|
|
186
|
+
event.waitUntil(
|
|
187
|
+
(async () => {
|
|
188
|
+
const windows = await self.clients.matchAll({ type: "window", includeUncontrolled: true });
|
|
189
|
+
const open = windows.find((client) => new URL(client.url).origin === self.location.origin);
|
|
190
|
+
if (open) {
|
|
191
|
+
await open.focus().catch(() => undefined);
|
|
192
|
+
if ("navigate" in open) await open.navigate(target.href).catch(() => undefined);
|
|
193
|
+
} else {
|
|
194
|
+
await self.clients.openWindow(target.href);
|
|
195
|
+
}
|
|
196
|
+
})(),
|
|
197
|
+
);
|
|
198
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
try {
|
|
3
|
+
var saved = localStorage.getItem("fk_theme");
|
|
4
|
+
if (saved === "light" || saved === "dark") {
|
|
5
|
+
document.documentElement.setAttribute("data-theme", saved);
|
|
6
|
+
}
|
|
7
|
+
} catch {
|
|
8
|
+
// Storage may be unavailable in private browsing.
|
|
9
|
+
}
|
|
10
|
+
})();
|