Skip to content

Commit ab2a113

Browse files
committed
fix(web): harden feed reader fallback and rss rendering
1 parent 6dfa1a9 commit ab2a113

11 files changed

Lines changed: 627 additions & 110 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
# Ignore rack cache
3636
/tmp/rack-cache-*
3737

38-
3938
# Ignore frontend build output and tooling caches
4039
/frontend/dist/
4140
/frontend/node_modules/
@@ -54,3 +53,4 @@
5453

5554
.yardoc
5655
frontend/.astro
56+
.pnpm-store

Gemfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ gem 'parallel'
1616
gem 'rack-cache'
1717
gem 'rack-timeout'
1818
gem 'roda'
19-
gem 'ssrf_filter'
2019
gem 'zeitwerk'
2120

2221
gem 'puma', require: false

app/web/feeds/responder.rb

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,9 @@ class << self
1212
# @param identifier [String]
1313
# @return [String] serialized feed body.
1414
def call(request:, target_kind:, identifier:)
15-
feed_request = Request.call(request:, target_kind:, identifier:)
16-
resolved_source = SourceResolver.call(feed_request)
17-
result = Service.call(resolved_source)
18-
normalized_identifier = feed_request.feed_name || identifier
15+
feed_request, resolved_source, result = resolve_request(request:, target_kind:, identifier:)
1916
body = write_response(response: request.response, representation: feed_request.representation, result:)
20-
21-
emit_result(target_kind:, identifier: normalized_identifier, resolved_source:, result:)
17+
emit_response_result(target_kind:, identifier:, feed_request:, resolved_source:, result:)
2218
body
2319
rescue StandardError => error
2420
emit_failure(target_kind:, identifier:, error:)
@@ -27,6 +23,39 @@ def call(request:, target_kind:, identifier:)
2723

2824
private
2925

26+
# @param request [Rack::Request]
27+
# @param target_kind [Symbol]
28+
# @param identifier [String]
29+
# @return [Array<(Html2rss::Web::Feeds::Contracts::Request, Html2rss::Web::Feeds::Contracts::ResolvedSource, Html2rss::Web::Feeds::Contracts::RenderResult)>]
30+
def resolve_request(request:, target_kind:, identifier:)
31+
feed_request = Request.call(request:, target_kind:, identifier:)
32+
resolved_source = SourceResolver.call(feed_request)
33+
result = Service.call(resolved_source)
34+
[feed_request, resolved_source, result]
35+
end
36+
37+
# @param feed_request [Html2rss::Web::Feeds::Contracts::Request]
38+
# @param identifier [String]
39+
# @return [String]
40+
def normalized_identifier(feed_request, identifier)
41+
feed_request.feed_name || identifier
42+
end
43+
44+
# @param target_kind [Symbol]
45+
# @param identifier [String]
46+
# @param feed_request [Html2rss::Web::Feeds::Contracts::Request]
47+
# @param resolved_source [Html2rss::Web::Feeds::Contracts::ResolvedSource]
48+
# @param result [Html2rss::Web::Feeds::Contracts::RenderResult]
49+
# @return [void]
50+
def emit_response_result(target_kind:, identifier:, feed_request:, resolved_source:, result:)
51+
emit_result(
52+
target_kind:,
53+
identifier: normalized_identifier(feed_request, identifier),
54+
resolved_source:,
55+
result:
56+
)
57+
end
58+
3059
# @param response [Rack::Response]
3160
# @param representation [Symbol]
3261
# @param result [Html2rss::Web::Feeds::Contracts::RenderResult]

docs/README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,23 @@ Running the app directly on the host is not supported.
4141
| ------------------------------ | ---------------------------------------------------------- |
4242
| `make setup` | Install Ruby and Node dependencies. |
4343
| `make dev` | Run Ruby (port 4000) and frontend (port 4001) dev servers. |
44-
| `make ready` | Full pre-flight check (Lint + Test + OpenAPI + Zeitwerk). |
44+
| `make ready` | Pre-commit gate: `make quick-check` + `bundle exec rspec`. |
4545
| `make test` | Run Ruby and frontend test suites. |
4646
| `make lint` | Run all linters. |
4747
| `make yard-verify-public-docs` | Enforce typed YARD docs for public methods in `app/`. |
4848
| `make openapi` | Regenerate `public/openapi.yaml` from request specs. |
49+
| `make openapi-verify` | Verify generated OpenAPI and frontend client artifacts are current. |
50+
| `make openapi-lint` | Lint OpenAPI with Redocly + Spectral. |
4951

50-
### Frontend npm Scripts
52+
### Frontend pnpm Scripts
5153

5254
| Command | Purpose |
5355
| ----------------------- | -------------------------------------------- |
54-
| `npm run dev` | Vite dev server with hot reload (port 4001). |
55-
| `npm run build` | Build static assets into `frontend/dist/`. |
56-
| `npm run test:run` | Unit tests (Vitest). |
57-
| `npm run test:contract` | Contract tests with MSW. |
56+
| `pnpm run dev` | Vite dev server with hot reload (port 4001). |
57+
| `pnpm run build` | Build static assets into `frontend/dist/`. |
58+
| `pnpm run lint` | Run ESLint across the frontend workspace. |
59+
| `pnpm run test:run` | Unit tests (Vitest). |
60+
| `pnpm run test:contract`| Contract tests with MSW. |
5861

5962
---
6063

docs/architecture.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,21 @@ flowchart TD
2121

2222
Requests enter via `app.rb` and are dispatched to `app/web/routes/`.
2323

24-
- **API v1**: Authenticated via `app/web/security/auth.rb`.
25-
- **Public Feeds**: Validated via HMAC tokens in `app/web/security/feed_token.rb`.
24+
- **Static feed pages (`/<feed_name>`)**: Routed by `app/web/routes/feed_pages.rb` and resolved as `target_kind: :static`.
25+
- Source: static config in `config/feeds.yml` (via `LocalConfig.find`).
26+
- Auth boundary: no feed token required on this route.
27+
- Failure mode: unknown feed names fail at static config lookup.
28+
- **Token-backed feed reads (`/api/v1/feeds/:token`)**: Routed by `app/web/routes/api_v1/feed_routes.rb` and resolved as `target_kind: :token`.
29+
- Token scope: `FeedAccess.authorize_feed_token!` validates signature/expiry and re-checks account URL access.
30+
- Constraint: disabled when AutoSource is off (`ForbiddenError` from `SourceResolver.ensure_auto_source_enabled!`).
31+
- **Feed creation (`POST /api/v1/feeds`)**: Authenticated via bearer token in `app/web/security/auth.rb`; this endpoint mints feed tokens for subsequent token-backed reads.
2632

2733
### 2. Resolution
2834

29-
The `Html2rss::Web::Feeds::SourceResolver` determines where the feed configuration comes from:
35+
The `Html2rss::Web::Feeds::SourceResolver` determines where feed configuration comes from based on route target:
3036

31-
- **Static**: Pre-defined in `config/feeds.yml`.
32-
- **Dynamic**: Generated on-the-fly via the `/api/v1/feeds` endpoint (AutoSource).
37+
- **Static (`target_kind: :static`)**: Pre-defined in `config/feeds.yml`.
38+
- **Token (`target_kind: :token`)**: Generated from validated feed token payload + AutoSource globals.
3339

3440
### 3. Fetching & Rendering
3541

public/feed-reader-link.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
document.addEventListener('DOMContentLoaded', function () {
2+
var readerLink = document.querySelector('[data-feed-reader-link]');
3+
if (!readerLink) return;
4+
5+
readerLink.setAttribute('href', 'feed:' + window.location.href);
6+
});

0 commit comments

Comments
 (0)