@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 +108 -11
- package/dist/recipes.js.map +1 -1
- package/guides/api-routes.md +4 -2
- package/guides/deployment.md +69 -0
- package/guides/go.md +3 -2
- package/guides/laravel.md +26 -13
- package/guides/sections.md +342 -40
- package/guides/your-own-backend.md +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
1807
|
-
|
|
1808
|
-
|
|
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
|
|
1813
|
-
|
|
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
|
|
1819
|
-
|
|
1820
|
-
|
|
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
|
|
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
|