Skip to content

4.x#33

Merged
andrewshell merged 90 commits into
mainfrom
4.x
Jun 14, 2026
Merged

4.x#33
andrewshell merged 90 commits into
mainfrom
4.x

Conversation

@andrewshell

Copy link
Copy Markdown
Collaborator

No description provided.

Andrew Shell and others added 30 commits May 24, 2026 13:19
Move all application code into apps/server/ as the @rsscloud/server
package (v4.0.0). Root becomes a workspace orchestrator with shared
devDependencies. Updates Dockerfile, docker-compose, release-please,
and CI configs for the new monorepo structure.

BREAKING CHANGE: project restructured as pnpm monorepo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Set up a new TypeScript library package at packages/core/ that will
eventually receive logic migrated from apps/server. Configures dual
ESM/CJS output via tsup, unit tests via vitest with 100% coverage
thresholds, and registers the package with release-please at 0.0.0.
Also updates pnpm-workspace.yaml to include packages/*, fans out
lint/build/typecheck across the workspace, and ignores dist/coverage
in git/prettier/docker contexts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Existing apps/server code diverged from its eslint.config.js rules
(space-before-function-paren: never, brace-style: 1tbs switch-case
indentation). Running eslint --fix reformats source to match. No
behavior changes; purely whitespace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Routes build, lint, typecheck, and test through `turbo run` at the
workspace root. Establishes `^build` dependencies for typecheck and
test so consumers of `@rsscloud/core` will wait on its `dist/` once
they exist. Caches build outputs and lint/typecheck inputs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mocha test suite requires the running app plus mock-server
containers, so it cannot be invoked on the host outside Docker. The
generic `test` script name collided with `turbo run test` (which
should run unit tests across the workspace). Renaming to `e2e-test`
makes the intent explicit and lets turbo's `test` task cleanly map
to vitest in `@rsscloud/core`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matrix job runs lint, typecheck, and unit tests across Node 22, 24,
and 26. Integration job runs the docker-compose e2e suite once on
Node 22, gated on the matrix passing. Swap CircleCI badge for the
CI workflow badge in the README.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitHub Actions now runs the test suite via .github/workflows/ci.yml.
The CircleCI project should be deactivated in the CircleCI UI after
merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The monorepo conversion (6940429) changed the server's CWD from the
repo root to apps/server/, but the /docs and /LICENSE.md handlers
still read README.md and LICENSE.md as CWD-relative paths — so both
routes returned HTTP 500 because the files only existed at the
repo root.

- Move the server-specific README.md into apps/server/, where the
  /docs handler now finds it via its existing CWD-relative read. The
  content (install, API docs, upgrade notes) was always about the
  server, not the monorepo as a whole.
- Add a new minimal monorepo-level README.md at the repo root that
  describes the workspace and links to each package.
- Fix the /LICENSE.md handler to read LICENSE.md via a __dirname-
  anchored path; LICENSE.md stays at the repo root since it covers
  the whole project.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mocha suite was never unit tests of server internals — every
file talks to the server over HTTP via APP_URL and spins up its own
mock servers on separate ports. Living inside apps/server forced
the server's devDependencies to carry mocha, chai*, supertest, and
the like, and bundled test code into anything that built the server.

Move the entire suite into a new apps/e2e workspace package:

- apps/e2e/package.json (private) owns mocha, chai*, supertest and
  the mock-server runtime deps (express, body-parser, xml2js,
  xmlbuilder, dayjs).
- All test files git-mv'd to apps/e2e/test/ so history follows.
- The 5 server helper modules the tests previously imported from
  ../services (dayjs-wrapper, init-subscription, parse-rpc-request,
  rpc-return-fault, rpc-return-success) plus the 3 config keys
  pulled from ../config are duplicated into apps/e2e/test/helpers/.
  The tests now have no cross-package require()s.
- apps/server/package.json drops the e2e-test script, the mocha/
  chai/supertest devDeps, the unused 'https' stub, and 'test/' from
  the lint glob.
- Root 'pnpm test' delegates to 'pnpm --filter @rsscloud/e2e run
  test:e2e' — the e2e package owns its docker-compose invocation.

BREAKING CHANGE: apps/server no longer exposes the mocha test
suite or its devDependencies. Anything that ran 'pnpm --filter
@rsscloud/server run test' (none externally) must switch to
'pnpm --filter @rsscloud/e2e run e2e-test' inside the docker
container, or 'pnpm test' at the root.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before: a single root Dockerfile built one image used as both the
production server runtime AND the e2e test runner. The image
shipped every workspace devDep (mocha, chai, supertest, dockerize)
into the production server.

Split into two Dockerfiles, each consumed by the apps/e2e
docker-compose.yml:

- apps/server/Dockerfile: lean production image. Installs only
  @rsscloud/server's runtime deps via 'pnpm install --filter
  @rsscloud/server --prod --ignore-scripts'. No dockerize, no
  mocha, no test fixtures.
- apps/e2e/Dockerfile: test-runner image. Installs dockerize,
  mocha, chai*, etc. via 'pnpm install --filter @rsscloud/e2e
  --ignore-scripts'.
- apps/e2e/docker-compose.yml: orchestrates both services using
  'context: ../..' so pnpm sees the workspace lockfile.
- Root Dockerfile and docker-compose.yml deleted.

Also colocate LICENSE.md inside apps/server so the new lean image
can serve /LICENSE.md without reaching across the workspace; the
handler reverts to the CWD-relative read that /docs already uses
post-monorepo-split.

--ignore-scripts is required because the root 'prepare' script
runs husky, which isn't present in production installs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the e2e split, docker-compose.yml lives at
apps/e2e/docker-compose.yml. Update the integration job to use it
explicitly and adjust the xunit artifact path accordingly
(apps/e2e/xunit/test-results.xml).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the e2e split, xunit output lives at apps/e2e/xunit/ rather
than the repo root. The non-recursive 'xunit' pattern no longer
matched the new location. '**/xunit' catches it anywhere in the
build context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- CLAUDE.md: update structure tree to show apps/e2e, packages/core,
  and the per-app Dockerfiles. Add test:unit/build/typecheck to the
  command list. Rewrite the Testing section to point at apps/e2e/
  and note the duplicated helpers pattern.
- .dockerignore: drop the .circleci entry — directory was removed
  in the CircleCI → GitHub Actions migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removed directory tree, command list, API endpoints, env var
listing, per-directory descriptions, and conventional-commits
boilerplate. All of these are trivially discoverable from
package.json, controllers/, config.js, or the commitlint config.

Kept: project identity, data-storage architecture (incl. the
2.x → 2.4.0 → 3.0 migration constraint), the deliberate e2e
helper-duplication pattern (easy to misread as a bug), and
release-please specifics.

124 lines → 25 lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v4 of these actions uses Node.js 20, which GitHub deprecated and
will force-upgrade in June 2026. v5 ships with the Node 24 runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bump vitest 2.1.8 → 3.2.4 (and @vitest/coverage-v8 in step) so it
pulls in vite 8.x and esbuild 0.27.x, closing the path-traversal
and dev-server request advisories.

Add pnpm overrides for qs (>=6.15.2) and vite (>=6.4.2) so the
patched versions stick across the workspace, including the e2e
suite's body-parser transitive.

The remaining low-severity diff advisory (via mocha) would require
forcing mocha onto an incompatible diff major; deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Map out the type-only contracts for the reusable rssCloud engine ahead of
implementation: protocol-neutral request/response DTOs, the async Store port,
the rssCloud-first Subscription/Resource model, the ProtocolPlugin seam
(verify/deliver) for pluggable transports including future WebSub, a typed
observability EventBus, plus config/errors/stats and the RssCloudCore facade.

No runtime behavior is added and the server is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the concrete logic fulfilling the @rsscloud/core contracts: the
protocol-neutral engine (createRssCloudCore) with ping change-detection
and fan-out, subscribe/unsubscribe, generateStats, and removeExpired; the
rssCloud REST protocol plugin (http-post/https-post) with challenge
verification and form-encoded delivery; an xml2js feed parser; a
Map-backed in-memory Store; plus the config resolver, event bus, and
RssCloudError class.

Built test-first with 100% coverage (93 tests). xml2js is core's first
runtime dependency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add XML-RPC support to @rsscloud/core, additive to the core package:

- xml-rpc-codec: network-free wire codec (parseMethodCall, serializeSuccess,
  serializeFault, buildNotifyCall) ported from the legacy server services
  using xml2js, decoding dateTime.iso8601 with native Date.
- xml-rpc-plugin: createXmlRpcProtocolPlugin owning ['xml-rpc']; deliver POSTs
  a text/xml methodCall with an AbortController timeout; verify is a plain test
  notify with no challenge handshake (diffDomain ignored), per legacy behavior.
- xml-rpc-dispatcher: createXmlRpcDispatcher, raw-XML-in/raw-XML-out, never
  throws. Maps hello/pleaseNotify/ping to core, preserving legacy param
  semantics (arity, protocol validation, callback URL glue, https inference,
  ::ffff strip, IPv6 bracketing) and Dave's ping-always-succeeds quirk.

100% line/branch/function/statement coverage on all three new modules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move flat src/ files into engine/, feed/, store/, and protocols/
folders, co-locating each interface contract with its implementation.
Pure file moves plus relative-import rewiring; no behavior change and
the public index barrel is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add createRestDispatcher mirroring the XML-RPC dispatcher: maps a parsed
REST body into SubscribeRequest/PingRequest (parseUrlList, missing-param
errors, glueUrlParts, protocol validation), drives core.subscribe/ping,
and renders xml/json/406 responses relaying core's messages.

REST ping reports failures as success:false (unlike XML-RPC); negotiation
happens at render time so a 406 still performs the use case. 100% covered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The REST and XML-RPC front doors emitted freshly-written engine strings
that diverged from the legacy server's wire contract (which the e2e suite
asserts verbatim). Most critically, an all-resources-failed pleaseNotify
over XML-RPC returned a boolean-false success response instead of a fault.

Port the legacy app-messages catalog into a shared
protocols/app-messages.ts. The engine now speaks codes only (a new
SubscribeResult.errorCode plus RssCloudError codes) and the dispatchers
own the wire wording — necessary because one engine condition
(resource-read-failed) renders as two different strings depending on the
front door ("the ping was cancelled..." vs "the subscription was
cancelled...").

- XML-RPC pleaseNotify failure now returns a fault, not boolean false
- subscribe success / read-failure / verify-failure / no-resources all
  match the legacy strings across REST and XML-RPC
- centralize missingParams / invalidProtocol / RPC arity strings

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add createFileStore(), a file-backed Store alongside createInMemoryStore,
to back apps/server's persistence on @rsscloud/core.

- Debounced write-behind queue: puts/removes update memory and resolve
  immediately, (re)arming a debounceMs timer capped by maxWaitMs; a single
  in-flight write, with mid-write mutations coalesced into one follow-up pass.
- Round-trip-faithful legacy subscriptions.json mapping in both directions
  (keyed by feed URL, flat feed fields <-> nested resource.feed, ISO-Z dates,
  epoch <-> null, notifyProcedure false <-> absent, synthesized whenCreated).
- Loads on init, awaitable flush()/close(); best-effort on write failure.

100% covered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce @rsscloud/express with per-endpoint factories — pleaseNotify,
ping, and rpc2 — each a drop-in Express handler stack built from a
@rsscloud/core engine. Handlers parse their own body, resolve the client
address from X-Forwarded-For/socket, negotiate the Accept response format,
and delegate to core's REST and XML-RPC dispatchers; they hold no protocol
logic of their own. 100% covered with vitest + supertest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
typescript-eslint 8.59 infers tsconfigRootDir when it is unset and errors
when multiple candidate roots are present. Adding the @rsscloud/express
package introduced a second package config, so pin the root explicitly to
silence the parser error in editors that lint from the repo root.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…em in image

Add the two workspace packages as dependencies of apps/server and rework the
Dockerfile into a multi-stage build that compiles core -> express to their CJS
dist before the slim --prod runtime copies the built dist in, so require()
resolves at runtime.

No runtime change yet: app.js still uses the legacy json-store and services.
This is slice 1a (plumbing) of migrating the protocol endpoints onto the
@rsscloud/express middleware; the full Docker e2e suite stays green (134
passing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lice 1b)

Add a composition root (apps/server/core.js) constructing @rsscloud/core with
the REST + XML-RPC delivery plugins and a Store adapter over the legacy
synchronous json-store, plus a core.events -> websocket bridge for /viewLog.

The adapter (services/core-store-adapter.js) maps the legacy on-disk shape to
and from core's model (mirroring file-store.ts), so core and the not-yet-
migrated legacy services + /test/* share one in-memory store with no changes to
either. createFileStore stays unused until the endgame swap.

core is wired but not yet on any request path; slice 2 (/ping) is its first
live exercise. Adapter + bridge unit-tested with node:test; full Docker e2e
remains green (134 passing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the hand-rolled controllers/ping.js with the @rsscloud/express
ping({ core }) handler stack, mounted POST-only in app.js ahead of the
controllers router. This puts @rsscloud/core on a live request path for the
first time. POST-binding matches how the package is designed and tested (it
returns a method-agnostic handler stack and delegates method-binding to the
consumer), so GET /ping still falls through to a 404 like the legacy router.

Also fixes a packaging bug this surfaced: @rsscloud/express declared express
only as a peerDependency, so its CJS build's require('express') failed to
resolve in the --prod Docker runtime (the workspace package is symlinked to
source, where peers are not installed) and the server crashed on boot. Add
express to @rsscloud/express dependencies, keeping the peer range as the
published contract, mirroring how core resolves xml2js.

Gate: full Docker e2e green (134 passing); all unit suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
andrewshell and others added 28 commits June 13, 2026 10:02
The "Feeds changed in last 7 days" label and the feedsChangedLast7Days
wire field were baked-in literals while the window itself is the
configurable feedsChangedWindowDays — change the config and the label
kept claiming 7. Carry core's feedsChangedLastWindow + windowDays through
toLegacyStats (and the no-file fallback) and let the template interpolate
the count, so the stats page reflects the actual window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The service was () => core.removeExpired() with no transform or error
handling, and its 152-line test re-verified core's maintenance behaviour
(expiry, retention window, orphan pruning, the error limit, the
MaintenanceResult shape) — all already covered by core's
engine/maintenance.test.ts. A shallow module whose deletion test passes
cleanly: the one-liner inlines at its three call sites (the startup +
scheduled cleanup in app.js, the /test/removeExpired endpoint) and the
behaviour stays owned and tested once, in core.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The stats-label fix removed the last legacy field (feedsChangedLast7Days),
so "legacy" no longer describes the projection — it maps core's Stats onto
the current wire shape the /stats view and /stats.json expose. Rename the
function and clear the adjacent stale "legacy" wording.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All seven test-API routes repeated the same try/catch: render
{ success: true, ...fields } on success, a 500 { success: false, error }
on throw. Lift that contract into one wrap(handler) — each route returns
its payload fields (or nothing) and owns only its own logic. Net -26
lines; behaviour-preserving (verified the success, merged-field, and
error envelopes against an in-process mount). Controllers stay covered by
the e2e suite per the recorded no-HTTP-unit-tests decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The architecture-cleanup section was entirely completed; per the TODO's
own "open work only" rule, that history belongs in git. Fold a brief
"done, see git" mention into the preamble and keep the one forward-looking
conclusion (client is the only warranted apps/server extraction) on the
client section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the client sketch with the agreed three-workspace plan: a generic
@rsscloud/xml-rpc codec both core and client build on, a published
@rsscloud/client (factory API, full subscriber+publisher), and a private
apps/client harness. Codec-first slicing keeps every step green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New published package mirroring the @rsscloud/express setup (tsup esm+cjs+dts,
vitest at 100% coverage, tsconfig/eslint). Placeholder index for now; the
generic XML-RPC codec lands in the next slices. Registered with release-please
(config + manifest) so it tracks as its own component.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port the generic XML-RPC value decoder + parseMethodCall (all value types:
i4/int/double, string, boolean, dateTime.iso8601, base64, struct, array, and
the untyped/unknown fallbacks) with its full spec. This is the decode half of
the shared codec core and client will build on; core is re-pointed at it in a
later slice. 100% coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A tagged XmlRpcValue model with constructor helpers (str/i4/int/bool/array/
struct) feeds buildMethodCall, buildMethodResponse, and buildFault. Explicit
typing is deliberate — i4-vs-number and array-vs-scalar can't be inferred, and
the rssCloud shapes depend on it (port is i4, urlList is array). Tests
round-trip through parseMethodCall where possible. Only the value types core and
client actually emit are built. 100% coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The generic decoder + response builders moved to @rsscloud/xml-rpc; core
now imports parseMethodCall and renders responses via buildMethodResponse/
buildFault (wrapped as the dispatcher's serializeSuccess/serializeFault).
The one rssCloud-specific shape — the notify methodCall's untyped string
param — stays core-local (inlined into the xml-rpc plugin) to preserve its
exact wire bytes. core's generic codec tests now live in the shared package;
output is byte-identical and coverage stays 100%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New published package for the subscriber/publisher end, mirroring the sibling
package setup (tsup esm+cjs+dts, vitest at 100% coverage). Depends on
@rsscloud/xml-rpc for its wire codec. Placeholder index for now; builders and
the send/receive layers land in the next slices. Registered with release-please.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
buildPleaseNotifyCall (the six wire params: notifyProcedure, port, path,
protocol, urlList, domain) and buildPingCall, both over @rsscloud/xml-rpc's
typed buildMethodCall. Tests round-trip through parseMethodCall. 100% coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A factory bound to one hub with an injectable fetch: pleaseNotify registers a
callback (xml-rpc over /RPC2, http-post/https-post over the REST /pleaseNotify)
and ping signals a change (/ping by default, /RPC2 with transport: 'xml-rpc').
Mirrors the reference test client's front-door selection. Fake-fetch tests cover
both protocols, both operations, and the construction branches. 100% coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…derer

parseXmlRpcNotify / parseHttpPostNotify extract the changed resource URL from
each notification shape, and buildNotifyResponse mints the boolean-true ack a
subscriber returns to an XML-RPC notify. renderCloudFeed emits an RSS 2.0
document carrying the <cloud> element a publisher advertises. Completes the
subscriber+publisher surface. 100% coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the inline XML-RPC builders, the RSS-with-<cloud> generator, the notify
ack, and the raw fetch calls with @rsscloud/client (createRssCloudClient +
renderCloudFeed + buildNotifyResponse). client.js is now just the dev-harness UI
shell. Verified the render routes (home, feed, challenge echo, /RPC2 ack) against
a live instance. Relocation to apps/client follows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move client.js out of apps/server into a new private @rsscloud/client-app
workspace (the manual counterpart to apps/e2e), and drop the now-unused
client script, body-parser, and @rsscloud/client dependency from apps/server.
The root `client` script now targets the new app. Verified the harness runs
from its new home (UI, <cloud> feed, /RPC2 ack); full workspace build,
typecheck, lint, and unit tests all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The glossary was hub-centric; the client extraction added a whole
subscriber/publisher layer. Add terms for Hub, Client, Subscriber,
Publisher, Notification, Cloud element, and the shared XML-RPC codec
(each with its Avoid list), frame the new packages in the intro, and
extend the example dialogue with a client/hub mirror exchange.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lead with the https-post/http-post path (createRssCloudClient, pleaseNotify, the
verify-challenge + parseHttpPostNotify callback, ping, renderCloudFeed) and keep
XML-RPC as a clearly-separated secondary section. Fix the Node engine note (22+).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
callback.domain is now optional and drives the hub's verification flow on both
transports: given, the hub uses that host (the diffDomain flow, with a challenge
for http-post/https-post); omitted, it falls back to the caller's connection
address. The REST pleaseNotify previously dropped domain entirely while still
requiring it in the type — so an explicit https-post callback host was silently
ignored. Send it in the form when present, pass '' over xml-rpc when absent
(ADR-0001), and correct the README, which wrongly claimed domain was xml-rpc-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A subscriber must host its own notify endpoint, so the client is app-shaped, not
a clean published library. Move the wire logic (createRssCloudClient, the
pleaseNotify/ping builders, renderCloudFeed, the notify ack) into apps/client/lib
as plain CommonJS, ported with node:test coverage (matching apps/server), and
delete the packages/client workspace. apps/client now depends on @rsscloud/xml-rpc
directly; the shared codec stays (core builds its /RPC2 dispatcher on it). Drops
the unused inbound notify parsers — the harness only logs. Untracked from
release-please; CONTEXT/TODO/xml-rpc docs updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The server Dockerfile predated the @rsscloud/xml-rpc extraction and never
built it, so @rsscloud/core's dts build could not resolve the module
(TS2307) during the CI Docker build. Copy the package, build the express
dependency graph topologically (xml-rpc -> core -> express), and ship its
dist in the runtime stage.

Also tidy dependencies surfaced while auditing the workspace:
- e2e: drop unused chai-json and supertest
- server: move xml2js to devDependencies (only the OPML test uses it)
- root: declare @eslint/js, which eslint.config.js requires

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The package overview only mentioned apps/server and packages/core. Add the
units introduced on this branch — @rsscloud/express, @rsscloud/xml-rpc, and
the private apps/client dev harness — and restore apps/client's own README
(dropped in the package->app fold-in) so each entry links to live docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The workflow triggered on both push to 4.x and pull_request to main, so an
open PR from 4.x ran each job twice (a push run and a pull_request run).
Drop feature branches from the push trigger: PRs are covered by the
pull_request event (which tests the merge result), and push CI stays on
main for the post-merge run, release flow, and status badge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@andrewshell andrewshell merged commit 70c1aef into main Jun 14, 2026
4 checks passed
@andrewshell andrewshell deleted the 4.x branch June 14, 2026 03:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant