Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
# Ignore rack cache
/tmp/rack-cache-*


# Ignore frontend build output and tooling caches
/frontend/dist/
/frontend/node_modules/
Expand All @@ -54,3 +53,4 @@

.yardoc
frontend/.astro
.pnpm-store
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ gem 'parallel'
gem 'rack-cache'
gem 'rack-timeout'
gem 'roda'
gem 'ssrf_filter'
gem 'zeitwerk'
Comment thread
gildesmarais marked this conversation as resolved.

gem 'puma', require: false
Expand Down
41 changes: 35 additions & 6 deletions app/web/feeds/responder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,9 @@ class << self
# @param identifier [String]
# @return [String] serialized feed body.
def call(request:, target_kind:, identifier:)
feed_request = Request.call(request:, target_kind:, identifier:)
resolved_source = SourceResolver.call(feed_request)
result = Service.call(resolved_source)
normalized_identifier = feed_request.feed_name || identifier
feed_request, resolved_source, result = resolve_request(request:, target_kind:, identifier:)
body = write_response(response: request.response, representation: feed_request.representation, result:)

emit_result(target_kind:, identifier: normalized_identifier, resolved_source:, result:)
emit_response_result(target_kind:, identifier:, feed_request:, resolved_source:, result:)
body
rescue StandardError => error
emit_failure(target_kind:, identifier:, error:)
Expand All @@ -27,6 +23,39 @@ def call(request:, target_kind:, identifier:)

private

# @param request [Rack::Request]
# @param target_kind [Symbol]
# @param identifier [String]
# @return [Array<(Html2rss::Web::Feeds::Contracts::Request, Html2rss::Web::Feeds::Contracts::ResolvedSource, Html2rss::Web::Feeds::Contracts::RenderResult)>]
def resolve_request(request:, target_kind:, identifier:)
feed_request = Request.call(request:, target_kind:, identifier:)
resolved_source = SourceResolver.call(feed_request)
result = Service.call(resolved_source)
[feed_request, resolved_source, result]
end

# @param feed_request [Html2rss::Web::Feeds::Contracts::Request]
# @param identifier [String]
# @return [String]
def normalized_identifier(feed_request, identifier)
feed_request.feed_name || identifier
end

# @param target_kind [Symbol]
# @param identifier [String]
# @param feed_request [Html2rss::Web::Feeds::Contracts::Request]
# @param resolved_source [Html2rss::Web::Feeds::Contracts::ResolvedSource]
# @param result [Html2rss::Web::Feeds::Contracts::RenderResult]
# @return [void]
def emit_response_result(target_kind:, identifier:, feed_request:, resolved_source:, result:)
emit_result(
target_kind:,
identifier: normalized_identifier(feed_request, identifier),
resolved_source:,
result:
)
end

# @param response [Rack::Response]
# @param representation [Symbol]
# @param result [Html2rss::Web::Feeds::Contracts::RenderResult]
Expand Down
15 changes: 9 additions & 6 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,23 @@ Running the app directly on the host is not supported.
| ------------------------------ | ---------------------------------------------------------- |
| `make setup` | Install Ruby and Node dependencies. |
| `make dev` | Run Ruby (port 4000) and frontend (port 4001) dev servers. |
| `make ready` | Full pre-flight check (Lint + Test + OpenAPI + Zeitwerk). |
| `make ready` | Pre-commit gate: `make quick-check` + `bundle exec rspec`. |
| `make test` | Run Ruby and frontend test suites. |
| `make lint` | Run all linters. |
| `make yard-verify-public-docs` | Enforce typed YARD docs for public methods in `app/`. |
| `make openapi` | Regenerate `public/openapi.yaml` from request specs. |
| `make openapi-verify` | Verify generated OpenAPI and frontend client artifacts are current. |
| `make openapi-lint` | Lint OpenAPI with Redocly + Spectral. |

### Frontend npm Scripts
### Frontend pnpm Scripts

| Command | Purpose |
| ----------------------- | -------------------------------------------- |
| `npm run dev` | Vite dev server with hot reload (port 4001). |
| `npm run build` | Build static assets into `frontend/dist/`. |
| `npm run test:run` | Unit tests (Vitest). |
| `npm run test:contract` | Contract tests with MSW. |
| `pnpm run dev` | Vite dev server with hot reload (port 4001). |
| `pnpm run build` | Build static assets into `frontend/dist/`. |
| `pnpm run lint` | Run ESLint across the frontend workspace. |
| `pnpm run test:run` | Unit tests (Vitest). |
| `pnpm run test:contract`| Contract tests with MSW. |

---

Expand Down
16 changes: 11 additions & 5 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,21 @@ flowchart TD

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

- **API v1**: Authenticated via `app/web/security/auth.rb`.
- **Public Feeds**: Validated via HMAC tokens in `app/web/security/feed_token.rb`.
- **Static feed pages (`/<feed_name>`)**: Routed by `app/web/routes/feed_pages.rb` and resolved as `target_kind: :static`.
- Source: static config in `config/feeds.yml` (via `LocalConfig.find`).
- Auth boundary: no feed token required on this route.
- Failure mode: unknown feed names fail at static config lookup.
- **Token-backed feed reads (`/api/v1/feeds/:token`)**: Routed by `app/web/routes/api_v1/feed_routes.rb` and resolved as `target_kind: :token`.
- Token scope: `FeedAccess.authorize_feed_token!` validates signature/expiry and re-checks account URL access.
- Constraint: disabled when AutoSource is off (`ForbiddenError` from `SourceResolver.ensure_auto_source_enabled!`).
- **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.

### 2. Resolution

The `Html2rss::Web::Feeds::SourceResolver` determines where the feed configuration comes from:
The `Html2rss::Web::Feeds::SourceResolver` determines where feed configuration comes from based on route target:

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

### 3. Fetching & Rendering

Expand Down
6 changes: 6 additions & 0 deletions public/feed-reader-link.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
document.addEventListener('DOMContentLoaded', function () {
var readerLink = document.querySelector('[data-feed-reader-link]');
if (!readerLink) return;

readerLink.setAttribute('href', 'feed:' + window.location.href);
Comment thread
gildesmarais marked this conversation as resolved.
Outdated
});
Loading
Loading