@rsc-kit/mcp 0.27.0 → 0.28.0

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/dist/recipes.js CHANGED
@@ -1730,6 +1730,39 @@ validates at runtime), move info/servers/security to the option, move a
1730
1730
  route's summary/tags/responses to its openapi export.
1731
1731
 
1732
1732
  Full guide: read_guide({ slug: 'openapi' }).`,
1733
+ },
1734
+ {
1735
+ topic: 'secrets',
1736
+ summary: 'The secrets rsc-kit reads - RSC_HOST_CALL_SECRET, RSC_SIGNING_SECRET, RSC_ACTION_ENCRYPTION_KEY - what each protects, how to generate them, where they go in production, and what rotating one does',
1737
+ body: `Three, each its own key for its own job. Never reuse one as another.
1738
+
1739
+ RSC_HOST_CALL_SECRET renderer <-> Go/Laravel backend calls. Required with a
1740
+ backend; read by the renderer AND the backend.
1741
+ RSC_SIGNING_SECRET signs refreshOn names a page hands the tab. Required in
1742
+ production when any page/section uses refreshOn (the
1743
+ server refuses to serve without it). Renderer only.
1744
+ Never falls back to the host-call secret.
1745
+ RSC_ACTION_ENCRYPTION_KEY optional: pins the key server actions encrypt with, so
1746
+ actions survive a deploy. Set at BUILD time too.
1747
+
1748
+ GENERATE - once each, a different value each:
1749
+ openssl rand -base64 32
1750
+ node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
1751
+ php -r "echo base64_encode(random_bytes(32)), PHP_EOL;"
1752
+ Development: create-rsc-kit / rsc-kit init (with a backend) and
1753
+ php artisan rsc:install write the first two into .env already.
1754
+
1755
+ PRODUCTION - in the platform's environment settings, never in git:
1756
+ - the SAME value on every instance (a tab served by one renderer is checked by
1757
+ another; a call may land on any backend replica);
1758
+ - read at runtime, so one build deploys anywhere (except the action key).
1759
+
1760
+ ROTATING is a deploy:
1761
+ - host-call: change renderer and backend together; between them every rpc() is refused.
1762
+ - signing: open pages stop refreshing until reloaded; nothing else breaks.
1763
+ - action key: actions from pages loaded before the change fail.
1764
+
1765
+ Docs: /hosts/deployment#secrets.`,
1733
1766
  },
1734
1767
  {
1735
1768
  topic: 'env',
@@ -1792,36 +1825,100 @@ SAY IT CHANGED - wherever the change happens, not only in an action:
1792
1825
  Go: reg.Changed(ctx, "team:"+teamID+":repos") // the webhook handler, a job
1793
1826
  Laravel: Rsc::changed("team:$teamId:repos"); // a controller, a job, a listener
1794
1827
  JS only: import { changed } from '@rsc-kit/core/changed'; await changed(\`team:\${team}:repos\`)
1828
+ ANY DATABASE - rsc-kit ships no driver; hand it yours. Postgres:
1829
+ postgresVersions(sql) (postgres.js/Bun.sql, or Drizzle's db.$client) - one
1830
+ statement per change, instant via LISTEN. MySQL/MariaDB: sqlVersions({
1831
+ dialect: 'mysql', query: async (t, p) => (await pool.query(t, p))[0] })
1832
+ (mysql2). SQLite: sqlVersions({ dialect: 'sqlite', query: async (t, p) =>
1833
+ db.query(t).all(...p) }). libSQL/Turso: sqlVersions({ dialect: 'sqlite',
1834
+ query: async (sql, args) => (await client.execute({ sql, args })).rows }).
1835
+ Always pass dialect: without it a change is a read + write per name.
1836
+ Redis/ORM query builders: createVersions({ read, bump, listen?, notify? }).
1837
+ Instant across instances: postgres.js LISTEN, Redis pub/sub; others within ~1s.
1795
1838
  NO BACKEND (JS only) - changed() works as-is on one server. With more than
1796
1839
  one instance, or a worker process that finishes jobs, keep versions in a
1797
1840
  table they all share: installVersionSource(postgresVersions(sql)) in
1798
1841
  instrumentation.ts register() AND in the worker (postgres.js LISTENs, so
1799
- cross-instance changes are instant; Bun.sql polls once a second; any SQL
1842
+ cross-instance changes are instant; so does Bun.sql (Bun 1.4+); any SQL
1800
1843
  driver/ORM: sqlVersions({ query }) - Prisma: query: (t, p) =>
1801
1844
  prisma.$queryRawUnsafe(t, ...p); anything else (Drizzle query builder, Redis,
1802
1845
  KV): createVersions({ read(names), bump(names), listen?, notify? }) and
1803
1846
  rsc-kit does the waiting/waking). An app-installed store always wins over a
1804
- backend's. In-memory versions fail SILENTLY with 2+ instances.
1847
+ backend's. In-memory versions fail with 2+ instances; in production rsc-kit logs a
1848
+ warning once when it falls back to them - installVersionSource(memoryVersions())
1849
+ says "one instance on purpose" and silences it.
1805
1850
  Table: rsc_versions(name TEXT PRIMARY KEY, version BIGINT NOT NULL) - same as
1806
- Go's SQLVersions. ANY process can bump without rsc-kit: upsert version + 1
1807
- (Postgres: ON CONFLICT (name) DO UPDATE SET version = rsc_versions.version + 1;
1808
- then NOTIFY rsc_versions).
1851
+ Go's SQLVersions. ANY process can bump without rsc-kit, with one upsert that
1852
+ moves version to GREATEST(version + 1, now in ms) - Postgres:
1853
+ INSERT ... VALUES (name, (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint)
1854
+ ON CONFLICT (name) DO UPDATE SET version = GREATEST(rsc_versions.version + 1,
1855
+ EXCLUDED.version); then NOTIFY rsc_versions. MySQL: GREATEST(version + 1,
1856
+ VALUES(version)) with FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000); SQLite: MAX(...)
1857
+ with CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER).
1858
+ CLEANUP - a version is the time a name last changed, so deleting old rows is
1859
+ always safe (a tab holding one refreshes once; it never comes back at a value
1860
+ a tab holds): versions.prune() on sqlVersions/postgresVersions (30 days), or
1861
+ DELETE FROM rsc_versions WHERE version < <ms 30 days ago> from a cron.
1862
+ memoryVersions forgets after 30 days; the server forgets names no tab
1863
+ watches. Go: store.Prune(ctx, 0) on SQLVersions/MemoryVersions. Laravel:
1864
+ cache keys expire after RSC_VERSIONS_KEEP_DAYS (30); with
1865
+ RSC_VERSIONS=database schedule php artisan rsc:prune-versions daily. A custom store's bump should use nextVersion(current), not + 1.
1866
+ TABLE - the app owns rsc_versions (rsc-kit never creates/alters it at
1867
+ runtime): name TEXT PRIMARY KEY (MySQL: VARCHAR(255)), version BIGINT NOT
1868
+ NULL. Extra columns fine if they have defaults. Drizzle: a pgTable in your
1869
+ schema + createVersions with the query builder (not raw SQL).
1870
+ LISTEN THROUGH AN ORM - pass the driver's listen/notify to createVersions:
1871
+ Drizzle on Bun.sql: listen: wake => client.listen('rsc_versions', wake),
1872
+ notify: () => client.notify('rsc_versions', '') with the SQL client you gave
1873
+ drizzle(). node-postgres: a dedicated pg.Client, LISTEN rsc_versions, and
1874
+ on('notification', wake). A listening store reads the table only as a safety
1875
+ net (~every 5s per server), not every second. NOTIFY inside a transaction is
1876
+ delivered on commit, so it keeps the after-commit rule for you.
1877
+ AFTER COMMIT - call changed() AFTER the transaction resolves, never inside
1878
+ it: a tab refreshes at once and would read the old data, then sit at the new
1879
+ version. Laravel: DB::afterCommit(fn () => Rsc::changed(...)).
1880
+ SAME NAME - many sections/pages may refresh on one name; one changed() moves
1881
+ it once and every tab refreshes what it shows. Section names are per page.
1882
+ STARTUP - assertSigningSecret() from @rsc-kit/core/changed in
1883
+ instrumentation.ts register() makes a deploy without the secret fail before
1884
+ taking traffic (Kubernetes readiness); otherwise it is refused on the first
1885
+ request.
1809
1886
  SECRET - set RSC_SIGNING_SECRET, the same on every instance, with or without
1810
1887
  a backend (NOT the host-call secret). In production an app using refreshOn
1811
1888
  refuses to serve without it. Names are signed per request, never at build.
1812
- REQUEST - a refreshOn function runs per request: cookies()/headers() work,
1813
- e.g. a name for the signed-in user.
1889
+ REQUEST - a refreshOn function gets params and searchParams ALREADY AWAITED
1890
+ (({ params }) => [\`team:\${params.team}\`] is right), and runs per request:
1891
+ cookies()/headers() work, e.g. a name for the signed-in user.
1892
+ ISOLATION - a name is a signal, not data or a permission: a refresh renders
1893
+ for that tab's visitor through their own session and guards, so nobody sees
1894
+ another's data through a name. A tab can only listen for names its page was
1895
+ rendered with (signed). Name the data narrowly with ids: conversation:{id},
1896
+ inbox:{userId}. No emails/secrets in names. Never shared: true on per-visitor
1897
+ sections.
1814
1898
  SHARED - section(name, C, { refreshOn, shared: true }) for a section that
1815
1899
  renders the same for everyone allowed to see the page: tabs refreshing
1816
1900
  because of the same change share ONE render (guards still run per tab).
1817
1901
  Never on per-visitor content - rpc() runs with the first visitor's session.
1818
- LARAVEL AT SCALE - RSC_VERSIONS=database (publish rsc-migrations) and
1819
- installVersionSource(postgresVersions(sql)) in the renderer: watching costs
1820
- PHP zero requests; Postgres NOTIFY makes it instant.
1902
+ LARAVEL - keep the default (versions in the cache; the renderer asks Laravel
1903
+ ~every 2s, ONE request per renderer process for all its tabs - negligible).
1904
+ Laravel stays the only thing talking to its database. RSC_VERSIONS=database
1905
+ (publish rsc-migrations) is for pruning or for writers outside Laravel; Laravel
1906
+ still answers. INSTANT: Laravel broadcasting (any Pusher-protocol server:
1907
+ Reverb, Pusher, Soketi) - RSC_BROADCAST=true in Laravel, RSC_BROADCAST_URL +
1908
+ RSC_BROADCAST_KEY for the renderer. Rsc::changed() announces a change (no
1909
+ names - public channel), the renderer asks Laravel at once, else every 30s.
1910
+ Never have the renderer read Laravel's database.
1911
+ WAKE ANYTHING - wakeOn(listen) from @rsc-kit/core/changed: listenToBroadcast({
1912
+ url, key }), or (wake) => subscriber.subscribe('rsc:changed', wake) for Redis.
1913
+ A wake carries nothing; the renderer then asks where versions live. Versions
1914
+ are still needed: they say WHICH names moved, per visitor, and let a
1915
+ reconnecting tab catch up.
1821
1916
  DEBUG - dev console lists what each region watches; a region missing there
1822
1917
  rendered no names (server log says why). Hidden tabs stop watching and catch
1823
1918
  up when shown.
1824
- COST - per visible tab one SSE connection (keepalive every 15s, ~40 bytes
1919
+ COST - per visible tab one SSE connection (keepalive every 8s - set
1920
+ RSC_STREAM_KEEPALIVE_MS lower if a server/proxy drops idle connections sooner;
1921
+ ~40 bytes
1825
1922
  per change); per change one refresh of just the sections showing the name;
1826
1923
  per server one versions read covering all its tabs.
1827
1924
  HOW - a name has a version, a number that moves when it is said to have