@plitzi/sdk-server 0.38.3 → 0.38.5

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.
Files changed (125) hide show
  1. package/CHANGELOG.md +530 -0
  2. package/README.md +142 -24
  3. package/dist/actions.d.ts +8 -5
  4. package/dist/actions.js +3 -1
  5. package/dist/adapters/jsonAdapters.d.ts +1 -1
  6. package/dist/adapters/jsonAdapters.js +16 -3
  7. package/dist/core/configSeam.js +20 -4
  8. package/dist/core/createServer.js +3 -2
  9. package/dist/core/handlers/socialRouteHandlers.js +6 -1
  10. package/dist/core/http/stages/devReload.d.ts +11 -0
  11. package/dist/core/http/stages/devReload.js +17 -0
  12. package/dist/core/http/stages/images.js +1 -1
  13. package/dist/core/server/baseServer.d.ts +2 -0
  14. package/dist/core/server/baseServer.js +3 -0
  15. package/dist/core/server/pageServer.js +18 -0
  16. package/dist/core/services/registry.js +2 -0
  17. package/dist/core/services/rsc.js +1 -1
  18. package/dist/core/services/spaceDocument.d.ts +2 -0
  19. package/dist/core/services/spaceDocument.js +11 -0
  20. package/dist/functions-api.d.ts +6 -1
  21. package/dist/functionsRunner.d.ts +5 -0
  22. package/dist/functionsRunner.js +2 -1
  23. package/dist/helpers/hydrationPayload.d.ts +25 -4
  24. package/dist/helpers/hydrationPayload.js +36 -30
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.js +2 -2
  27. package/dist/modules/actions/index.d.ts +11 -2
  28. package/dist/modules/actions/index.js +29 -7
  29. package/dist/modules/actions/runtime/fileKv.d.ts +28 -0
  30. package/dist/modules/actions/runtime/fileKv.js +95 -0
  31. package/dist/modules/actions/runtime/fileKv.test.d.ts +1 -0
  32. package/dist/modules/actions/runtime/memoryKv.d.ts +23 -0
  33. package/dist/modules/actions/runtime/memoryKv.js +39 -24
  34. package/dist/modules/actions/runtime/namespaceKv.d.ts +2 -0
  35. package/dist/modules/actions/runtime/namespaceKv.js +5 -3
  36. package/dist/modules/actions/runtime/renderResolver.d.ts +3 -2
  37. package/dist/modules/actions/runtime/renderResolver.js +13 -9
  38. package/dist/modules/actions/runtime/renderShare.d.ts +7 -24
  39. package/dist/modules/actions/runtime/renderShare.js +47 -13
  40. package/dist/modules/actions/runtime/runAction.js +15 -1
  41. package/dist/modules/actions/runtime/scope.js +16 -1
  42. package/dist/modules/actions/runtime/signing.js +4 -1
  43. package/dist/modules/actions/runtime/taskContext.d.ts +7 -1
  44. package/dist/modules/actions/runtime/taskContext.js +6 -2
  45. package/dist/modules/actions/tasks/registry.d.ts +3 -1
  46. package/dist/modules/actions/tasks/registry.js +15 -1
  47. package/dist/modules/actions/types.d.ts +15 -2
  48. package/dist/modules/functions/config.d.ts +6 -0
  49. package/dist/modules/functions/context.d.ts +4 -2
  50. package/dist/modules/functions/context.js +43 -21
  51. package/dist/modules/functions/load.d.ts +8 -1
  52. package/dist/modules/functions/load.js +8 -4
  53. package/dist/modules/functions/manifest.d.ts +4 -1
  54. package/dist/modules/functions/manifest.js +14 -2
  55. package/dist/modules/functions/native.d.ts +2 -1
  56. package/dist/modules/functions/native.js +2 -2
  57. package/dist/modules/functions/plugins.test.d.ts +1 -0
  58. package/dist/modules/functions/routes.d.ts +8 -3
  59. package/dist/modules/functions/routes.js +41 -21
  60. package/dist/modules/functions/scope.d.ts +30 -0
  61. package/dist/modules/functions/scope.js +28 -0
  62. package/dist/modules/functions/space.d.ts +10 -5
  63. package/dist/modules/functions/space.js +37 -15
  64. package/dist/modules/images/imageProxy.js +19 -1
  65. package/dist/modules/rsc/connectorRscData.d.ts +3 -1
  66. package/dist/modules/rsc/connectorRscData.js +9 -5
  67. package/dist/modules/rsc/handler.d.ts +3 -16
  68. package/dist/modules/rsc/handler.js +10 -4
  69. package/dist/modules/rsc/publicFileResolver.d.ts +15 -9
  70. package/dist/modules/rsc/publicFileResolver.js +61 -23
  71. package/dist/modules/rsc/resolveRscData.d.ts +3 -1
  72. package/dist/modules/rsc/resolveRscData.js +8 -5
  73. package/dist/modules/runtime/bundle.d.ts +5 -0
  74. package/dist/modules/runtime/bundle.js +20 -5
  75. package/dist/modules/sqlite/kv.d.ts +34 -0
  76. package/dist/modules/sqlite/kv.js +99 -0
  77. package/dist/modules/sqlite/kv.test.d.ts +1 -0
  78. package/dist/modules/ssr/prepareRender.js +45 -6
  79. package/dist/modules/ssr/spaceDocument.d.ts +25 -0
  80. package/dist/modules/ssr/spaceDocument.js +72 -0
  81. package/dist/modules/ssr/spaceDocument.test.d.ts +1 -0
  82. package/dist/modules/ssr/testing/offlineData.d.ts +5 -0
  83. package/dist/modules/ssr/views/template.ejs +122 -20
  84. package/dist/plugins/compile.js +6 -21
  85. package/dist/plugins/compile.test.d.ts +1 -0
  86. package/dist/plugins/copy.d.ts +5 -0
  87. package/dist/plugins/copy.js +16 -1
  88. package/dist/plugins/copy.test.d.ts +1 -0
  89. package/dist/plugins/manager.d.ts +13 -0
  90. package/dist/plugins/manager.js +41 -12
  91. package/dist/plugins/watch.d.ts +12 -0
  92. package/dist/plugins/watch.js +79 -0
  93. package/dist/runtime.d.ts +1 -1
  94. package/dist/runtime.js +2 -2
  95. package/dist/sqlite.d.ts +17 -0
  96. package/dist/sqlite.js +2 -0
  97. package/package.json +8 -3
  98. package/skills/plitzi-authoring/CHEATSHEET.md +9 -5
  99. package/skills/plitzi-authoring/SKILL.md +24 -20
  100. package/skills/plitzi-authoring/recipes/accordion.ts +58 -0
  101. package/skills/plitzi-authoring/recipes/link-to-a-section.ts +1 -1
  102. package/skills/plitzi-authoring/recipes/plugin.ts +1 -1
  103. package/skills/plitzi-authoring/recipes/server-data.ts +65 -0
  104. package/skills/plitzi-authoring/recipes/show-data.ts +3 -2
  105. package/skills/plitzi-authoring/recipes/style-a-page.ts +1 -1
  106. package/skills/plitzi-authoring/recipes/typed-data.ts +1 -1
  107. package/skills/plitzi-authoring/reference/accessibility.md +6 -2
  108. package/skills/plitzi-authoring/reference/authoring-errors.md +16 -5
  109. package/skills/plitzi-authoring/reference/colours-and-motion.md +8 -2
  110. package/skills/plitzi-authoring/reference/data-and-visibility.md +38 -26
  111. package/skills/plitzi-authoring/reference/drawing.md +24 -0
  112. package/skills/plitzi-authoring/reference/efficiency.md +27 -1
  113. package/skills/plitzi-authoring/reference/elements-and-styles.md +5 -9
  114. package/skills/plitzi-authoring/reference/flows.md +3 -0
  115. package/skills/plitzi-authoring/reference/layouts.md +8 -23
  116. package/skills/plitzi-authoring/reference/lists.md +1 -1
  117. package/skills/plitzi-authoring/reference/performance.md +2 -2
  118. package/skills/plitzi-authoring/reference/plugins.md +51 -38
  119. package/skills/plitzi-authoring/reference/review-checklist.md +16 -2
  120. package/skills/plitzi-authoring/reference/snippets-and-export.md +2 -2
  121. package/skills/plitzi-authoring/reference/structure.md +32 -1
  122. package/skills/plitzi-authoring/reference/templates.md +6 -3
  123. package/skills/plitzi-authoring/reference/testing.md +10 -3
  124. package/skills/plitzi-authoring/reference/typed-sources.md +3 -1
  125. package/skills/plitzi-authoring/reference/validation.md +1 -1
package/README.md CHANGED
@@ -4,8 +4,9 @@ The page server for Plitzi spaces: server-side rendering, React Server Component
4
4
  HTTP kernel they run on. Ships as an HTTP/2 server by default, with support for HTTP/1.1 and HTTP/3.
5
5
 
6
6
  This package serves **pages**. The AI surface — the MCP server, its tool engine and the draft-preview
7
- endpoint — lives in [`@plitzi/sdk-mcp`](../mcp/README.md), which builds on this one. A deployment that only
8
- renders pages never installs it, and never loads it.
7
+ endpoint — lives in [`@plitzi/sdk-mcp`](../mcp/README.md), which depends on this one's narrow entries (the kernel,
8
+ OAuth, and the renderer for draft preview alone), never the other way round. A deployment that only renders pages
9
+ never installs it, and never loads it.
9
10
 
10
11
  ## Installation
11
12
 
@@ -158,16 +159,21 @@ serve it — worth running over anything that arrives as a file.
158
159
  A space on Plitzi comes out whole, as a project wired to this server: `npx @plitzi/cli create my-site --from <space>`
159
160
  writes its pages as authoring code, its actions as `defineAction` code, its functions, the source of its plugins and
160
161
  runtime, and its files into `public/` — a draft, or any published snapshot (`--environment`, `--revision`) — and
161
- `plitzi pull` keeps it in step. See `docs/en/projects-from-spaces.md` in the workspace.
162
+ `plitzi pull` keeps it in step, and `plitzi push` puts what changed in it back on the space. See
163
+ `docs/en/projects-from-spaces.md` in the workspace.
162
164
 
163
165
  ## Configuration
164
166
 
167
+ `createServer` takes a `ServerConfig` (exported, as a type, from `@plitzi/sdk-server`) — what a project types its own
168
+ options with: a `plitzi create` project keeps them in `src/config/serverOptions.ts`, typed from it less what its `main.ts`
169
+ wires itself.
170
+
165
171
  | Option | Type | Default | Description |
166
172
  |---|---|---|---|
167
173
  | `httpVersion` | `1 \| 2 \| 3` | `2` with `tls`, else `1` | HTTP protocol version. Falls back to the nearest available lower version. |
168
174
  | `tls` | `{ key, cert, minVersion? }` | — | TLS key and certificate. Required for versions 2 and 3; optional for version 1. |
169
175
  | `devMode` | `boolean` | `false` | Enables development mode: appends `?dev` to esm.sh CDN URLs for React, and activates per-request timing metrics (see [Dev metrics](#dev-metrics)). Off, the process must run with `NODE_ENV=production` or React renders with its development build; the server says so once, at `error`. |
170
- | `devReload` | `boolean` | `false` | Every page this server renders listens for `server.reloadPages()` and loads again — for a project that re-authors its space on save (what `plitzi create` writes into `main.ts`). Not turned on by `devMode`: each open page holds a connection for it. |
176
+ | `devReload` | `boolean` | `false` | Every page this server renders listens for `server.reloadPages()` and loads again — for a project that re-authors its space on save (what `plitzi create` writes into `main.ts`) — and loads again too when it reconnects to a server that restarted (a new process), so a change to the server's code reaches the open page. With `devMode` too, a plugin whose source changes is built again and swapped where it is drawn on every open page — the rest of the page, its state included, stays as it was; a plugin whose declaration changed loads the page again. Not turned on by `devMode`: each open page holds a connection for it. |
171
177
  | `assetVersion` | `string` | — | Cache-buster appended as `?v=<assetVersion>` to all default SDK asset URLs. Compute from file mtime or package version at startup. |
172
178
  | `cacheTtlMs` | `number` | `300000` | TTL in milliseconds for the SSR render cache. Set to `0` to disable. |
173
179
  | `flags` | `Record<string, boolean> \| ({ spaceId, environment }) => Record<string, boolean>` | — | This deployment's say over the spaces' feature flags: above what a space declares, below the SDK's `flags` prop and a tester's dev tools, and only for flags the space declares. A function answers per space. Server actions and RSC resolve with it too. See `docs/en/feature-flags.md` in the workspace. |
@@ -176,11 +182,12 @@ runtime, and its files into `public/` — a draft, or any published snapshot (`-
176
182
  | `signIn` | `SSRSignInConfig` | — | Sign visitors in by redirect through an OAuth 2.1 authorization server: `GET /auth/sign-in?return=/x` registers this host as a client, sends the browser out with PKCE (state in a `__Host-` cookie), and `/auth/sign-in/callback` redeems the code server to server and hands the token to `exchangeCredential`, which makes the session here. Needs the `exchangeCredential` adapter. |
177
183
  | `templateFn` | `SSRTemplateFn` | built-in EJS template | Custom render function. Receives all template params and returns an HTML string. |
178
184
  | `plugins` | `Record<string, PluginSource>` | — | Named plugin definitions. Compiled or copied on first use and cached for `pluginsTtlMs`. |
179
- | `pluginsCacheDir` | `string` | `.sdk-plugins` | Directory where compiled plugin files are stored. |
185
+ | `pluginsCacheDir` | `string` | `tmp/.sdk-plugins` | Directory where compiled plugin files are stored — under `tmp/`, with everything else the server writes for itself. |
180
186
  | `pluginsTtlMs` | `number` | `604800000` | TTL in milliseconds for compiled plugins (default: 1 week). |
181
187
  | `autoLoadSchemaPlugins` | `boolean` | `true` | Auto-download and cache plugins declared in the schema's `offlineData.plugins` list. A type the deployment registers itself (`plugins`, named in `pluginNames`) is never looked for there: its own build is the one rendered. Set to `false` to manage plugin loading manually. |
182
188
  | `allowPrivatePluginHosts` | `boolean` | `false` | Read a schema plugin from a private address (`localhost`, this server's own network). A plugin's address is typed by whoever edits a space, so leave it off anywhere but a development machine whose bucket is local. |
183
- | `publicDir` | `string` | — | Absolute path to a directory served at the root URL level (e.g. `robots.txt`, `favicon.png`). Files are checked before `static` prefix routes. |
189
+ | `publicDir` | `string` | — | Absolute path to a directory served at the root URL level (e.g. `robots.txt`, `favicon.png`). Files are checked before `static` prefix routes. **Everything in it is public:** served to anyone who asks, with no check. |
190
+ | `dataDir` | `string` | — | The project's own data, never served: a provider resolved on the server (`runtime: 'server'`) whose `query` is `/data/<file>` reads `<dataDir>/<file>`. See [Project data](#project-data). |
184
191
  | `static` | `Record<string, string>` | — | URL prefix → filesystem path mappings for static file serving. |
185
192
  | `images` | `{ domains, cacheDir? }` | — | Pictures from other sites resized at `/_plitzi/img`, for an `image` to offer as a `srcset` (see [Images](#images)). |
186
193
  | `ssrOnly` | `boolean` | `false` | Omit client-side JS from the rendered page. Useful for verifying SSR HTML without hydration. |
@@ -195,6 +202,20 @@ runtime, and its files into `public/` — a draft, or any published snapshot (`-
195
202
  | `logger` | `ServerLogger` | the console | Where it says it: one structured `ServerLogEvent` stream. `consoleLogger` prints each as one line. |
196
203
  | `onListenError` | `(error, { port, host, label }) => void` | exits non-zero | What to do when the server cannot take its port. By default it prints what went wrong and what to do about it, then exits — a process whose server never bound is not running. Supply this to keep it alive and decide yourself. |
197
204
  | `workers` | `boolean \| number \| 'auto'` | `'auto'` under `NODE_ENV=production`, else `false` | How many processes serve the port: one per core, a number (lowered to the cores there are), or one. `SDK_SERVER_WORKERS` sets it when the config does not. See [Using every core](#using-every-core). |
205
+ | `action` | `SSRActionConfig` | — | The space's server actions: the write endpoint (`path`, default `/_action`), `lookups` (`ActionLookupsConfig`: `getAction`, `getFunctions`, `getPluginFunctions`, `getData`…), `kv`, `jobs`, `limits`, `email`, `signingSecret`, `onRun` / `onReject`. Absent, the server serves reads only. See `docs/en/server-actions.md` § 13 in the workspace. |
206
+ | `functions` | `{ native?, plugins?, runner?, limits? }` | — | The server's code (`FunctionsConfig` in `@plitzi/sdk-server/functions`, checked as the server starts): its own tasks and routes (`native`, from `loadFunctions`), the server halves of the plugins it ships (`plugins`, by plugin type — see [A plugin's server half](#a-plugins-server-half)), the `runner` the spaces' functions run on, and the per-invocation `limits`. See `docs/en/functions.md` § 8 in the workspace. |
207
+ | `connectors` | `ConnectorLookupsConfig` | — | `getConnector` and `getCredential` for the space's connectors, read by the RSC read path and the `/_action` write endpoint alike. See `docs/en/connectors.md` in the workspace. |
208
+ | `auth` | `Auth` | — | A `createAuth(...)` result: fills the three auth adapters in and brings the cookie naming. See [User authentication](#user-authentication). |
209
+ | `sessionRenewal` | `{ url } \| false` | on with `auth` | Where a page request whose access cookie died, its refresh cookie alive, renews before it is rendered. |
210
+ | `authCookie` | `SSRAuthCookie` | — | Name, domain, `sameSite`, `secure`, `refreshPath` (`/auth`) and `hintSuffix` (`_hint`) of the session cookies. `auth` brings its own; set here, a piece wins over it. |
211
+ | `exchangePath` | `string \| false` | `'/auth/exchange'` | Where a browser-obtained credential is handed over; served only with the `exchangeCredential` adapter. |
212
+ | `debugMode` | `boolean` | `devMode`, or the space's `devTools` | Authorizes debugging on the pages this server renders. Set, it decides for every space: `false` is a refusal no space turns around. |
213
+ | `frameOptions` | `'DENY' \| 'SAMEORIGIN' \| string[] \| false` | `'DENY'` | Who may embed the pages in a frame (CSP `frame-ancestors`, and `X-Frame-Options`); `false` omits both headers. A space adapter's `frameAncestors` derives it per space — see [Space adapters](#space-adapters-a-space-per-domain). |
214
+ | `fonts` | `{ baseUrl?, dir? }` | `baseUrl: '/fonts'` | Where the font files a space uploaded are addressed, and with `dir` served from this server under `/fonts/*`. See `docs/en/fonts.md` in the workspace. |
215
+ | `environment` | `'production' \| 'staging' \| 'development' \| 'local'` | `'production'` | The Plitzi tier whose endpoints the rendered pages are handed. |
216
+ | `services` | `{ ssr?, rsc? }` | `ssr` on, `rsc` with `getRscData` | Which request-handling services are mounted. |
217
+ | `preview` / `draftStore` | `SSRPreviewConfig` / `DraftStore` | off / in memory | The draft-preview endpoint (`enabled`, `path` `/__preview`, `secret`, `ttlMs`) and the store behind its tokens — share one across replicas. The rest of draft preview is `@plitzi/sdk-mcp`'s: see its [Draft preview](../mcp/README.md#draft-preview). |
218
+ | `port` / `host` | `number` / `string` | — | Not read: `server.listen(port, host?)` takes where it binds (`host` defaults to `0.0.0.0`). |
198
219
 
199
220
  ### HTTP version behaviour
200
221
 
@@ -296,13 +317,17 @@ const server = createServer({
296
317
  server.listen(3001);
297
318
  ```
298
319
 
320
+ Beside the page adapters it offers `getSchema` and `getStyle`, read from the same file, and — when `offlineData` is a
321
+ path — `saveSchema` and `saveStyle`, each writing its document back into the file. So an MCP server
322
+ ([`@plitzi/sdk-mcp`](../mcp/README.md#adapters)) can run over it, with a `getGrant` of yours beside it.
323
+
299
324
  ### `JsonAdaptersConfig`
300
325
 
301
326
  | Option | Type | Description |
302
327
  |---|---|---|
303
328
  | `offlineData` | `string` | Path to a single JSON file used for every request. |
304
329
  | `offlineData` | `(spaceId, environment, revision?) => string` | Function returning the path for the requested space. |
305
- | `offlineData` | `OfflineDataRaw` | The space itself, for a consumer that already holds it. Read-only: `saveOfflineData` is offered only for a path. |
330
+ | `offlineData` | `OfflineDataRaw` | The space itself, for a consumer that already holds it. Read-only: `saveSchema` and `saveStyle` are offered only for a path. |
306
331
  | `deployment` | `string` | Path to a JSON file containing an `SSRSpaceDeployment` object. |
307
332
  | `deployment` | `SSRSpaceDeployment` | Inline deployment object used for every request. |
308
333
  | `deployment` | `Record<hostname, SSRSpaceDeployment>` | Per-hostname map. Use `'*'` as a catch-all. |
@@ -397,11 +422,13 @@ createServer({ images: { domains: ['images.example.com', '*.cdn.example.com'] },
397
422
 
398
423
  An `image` whose `src` is another site's then offers the browser a `srcset` of widths this server makes (320 to 1920
399
424
  px) at `/_plitzi/img?url=…&w=…`, in AVIF or WebP when the browser takes them. Everything is kept on disk (`cacheDir`,
400
- default `.plitzi/images`): each original is downloaded once, and each size is made once from it. A week on, the kept
425
+ default `tmp/images`): each original is downloaded once, and each size is made once from it. A week on, the kept
401
426
  files keep answering while the original is asked for with its `ETag` / `Last-Modified` — unchanged, nothing is resized
402
427
  again; changed, its sizes are made anew. A picture that could not be fetched is not asked for again for five minutes.
403
428
  Only the listed hosts are fetched, every redirect is held to the same
404
- list and to the rule every outbound request follows (no private network), and SVG is refused. Resizing needs `sharp`
429
+ list and to the rule every outbound request follows (no private network). An SVG is never resized nor served from
430
+ this origin: the proxy answers it with a redirect to its own address (`307`), so the picture still shows. An `image`
431
+ with `resize: false` keeps its `src` as written. Resizing needs `sharp`
405
432
  (an optional peer): without it pictures are passed through and kept, and the server says so once.
406
433
 
407
434
  ### Public directory
@@ -417,8 +444,30 @@ createServer({
417
444
 
418
445
  The lookup order for a request is: `publicDir` → `static` prefix routes → SSR renderer.
419
446
 
447
+ **Everything in `publicDir` is on the internet.** A file there is served to anyone who asks for it, as it is — no
448
+ session, no permission, no check. Never put a secret, a key, an `.env`, a private document, a database dump or data only
449
+ some visitors may read in it: that is served by a server action whose `access` checks who is asking.
450
+
420
451
  `/.well-known/` paths follow the same lookup order: served from `publicDir` if a matching file exists, otherwise `404 Not Found`. They are never handled by the SSR renderer.
421
452
 
453
+ ### Project data
454
+
455
+ A project with no backend keeps its data as JSON files, and `dataDir` is where they stay off the internet: nothing
456
+ serves the folder. A provider resolved on the server whose `query` is `/data/<file>` reads `<dataDir>/<file>` before the
457
+ page is answered — in the shape a browser request publishes (`{ status, data }`), so a binding reads `<source>.data.…`
458
+ in either runtime — and the page arrives with it. A browser asking for the same path gets a page, never the file.
459
+
460
+ ```ts
461
+ createServer({ dataDir: path.join(PROJECT_ROOT, 'src/data'), adapters: { ... } });
462
+ ```
463
+
464
+ Without a `dataDir`, `/data/<file>` is answered from `action.lookups.getData(spaceId, at)` — the space's own data, as
465
+ its files' text by path, as of the version being rendered: what a cloud deployment keeps for a space (`plitzi push`).
466
+
467
+ `/data/…` is the data folder's first; a server provider reading another path is answered from `publicDir`, as the
468
+ browser would be. What a provider reads goes into the page it renders, whole: data a page must not carry — a price
469
+ list's costs, another visitor's rows — is a server action's to read, answering only what is shown.
470
+
422
471
  ## Compression
423
472
 
424
473
  Responses are compressed based on the `Accept-Encoding` request header. By default the server offers Brotli, then
@@ -510,15 +559,19 @@ All fields are optional and combined with AND logic. Calling `invalidate()` with
510
559
 
511
560
  Plugins are React component bundles that extend the Plitzi schema renderer. They are defined globally at server config level, compiled or copied on first use, and cached on disk for one week. The `getSpaceDeployment` adapter controls which plugins each space gets access to via `pluginNames`.
512
561
 
562
+ A compiled plugin is one module and its stylesheet, built as `plitzi pack plugin` builds it: React and the SDK kept out, and everything else inside — images and fonts as data URIs, and a file imported whole the way Vite imports it (`worker.js?raw` for its text, `engine.wasm?inline` for a data URI).
563
+
564
+ Whatever stylesheet the server writes for a plugin — compiled, copied or downloaded — goes into the `plitzi-sdk-plugin` cascade layer (`inPluginLayer` from `@plitzi/sdk-shared/style`), below the space's own: a space's classes and `customCss` win over what the plugin shipped, whatever the specificity. A `css` given as a web path (`/builder-assets/…`) is linked as it is.
565
+
513
566
  ```ts
514
- import type { SSRSpaceDeployment } from '@plitzi/sdk-server';
567
+ import type { SSRSpaceDeployment } from '@plitzi/sdk-shared';
515
568
 
516
569
  const server = createServer({
517
570
  plugins: {
518
571
  // From a source file — compiled to ESM with esbuild
519
572
  'my-chart': {
520
573
  js: '/abs/path/to/MyChart.tsx',
521
- css: '/abs/path/to/MyChart.css', // filesystem path — copied to .sdk-plugins
574
+ css: '/abs/path/to/MyChart.css', // filesystem path — copied to tmp/.sdk-plugins
522
575
  version: '1.2.0'
523
576
  },
524
577
  // Pre-compiled local file — copied as-is (version defaults to '1.0.0')
@@ -637,6 +690,10 @@ Compiled and copied plugin files are served under `/sdk-plugins/{name}/`:
637
690
 
638
691
  Plugin responses are compressed with Brotli or gzip like all other responses.
639
692
 
693
+ A page imports only the plugins it draws: its own elements, its layouts', and those of the components and references it
694
+ holds (`pageElementTypes`). The rest reach the page as their declaration (`PluginEntry.deferred`) and are imported when
695
+ a page that draws them is opened, client-side navigation included.
696
+
640
697
  ### TTL and invalidation
641
698
 
642
699
  Plugins are compiled once and cached for `pluginsTtlMs` (default: 1 week). The TTL is tracked via a `meta.json` file written alongside each plugin's compiled output. On the next request after expiry the plugin is automatically recompiled.
@@ -665,13 +722,37 @@ server.plugins.register('my-chart', {
665
722
 
666
723
  `register` clears any in-memory cache for that plugin name, so the next request triggers a fresh compile/copy. Previously compiled disk files are reused if they are within their TTL; call `server.plugins.invalidate(name)` beforehand to force a full rebuild.
667
724
 
725
+ ### A plugin's server half
726
+
727
+ A plugin can bring server code — its `functions/`, written as a space's functions are — whose routes answer under
728
+ `/fn/plugins/<type>/` and whose tasks are steps named `<type>.<action>`, run with a plugin's narrower `ctx` (its own
729
+ corner of `kv`, none of the space's credentials or channels). This server's own plugins hand theirs over by type:
730
+
731
+ ```ts
732
+ import { createServer, loadFunctions } from '@plitzi/sdk-server';
733
+
734
+ const [board] = await loadFunctions(new URL('./plugins/Board/functions/', import.meta.url));
735
+
736
+ const server = createServer({ adapters, functions: { native, plugins: { board } } });
737
+
738
+ // While it runs: a server half loaded again — or, with none, taken away — without a restart.
739
+ server.functions.setPlugin('board', board);
740
+ ```
741
+
742
+ `loadFunctionsSource(files)` loads one from its files rather than a folder: what a packed plugin carries as
743
+ `functions.source.json`. The plugins a space uploaded reach a run through `action.lookups.getPluginFunctions`. The
744
+ whole of it — `ctx`, routes, how a packed plugin carries it — is in
745
+ [functions § a plugin's server half](../../docs/en/functions.md#10-a-plugins-server-half).
746
+
668
747
  ## React Server Components (RSC)
669
748
 
670
749
  The SSR server includes a lightweight RSC endpoint that delivers server-side data to schema elements with `runtime: 'server'`. This is not the React RSC wire protocol — it uses a simple JSON transport that any element can consume via `useRscData()`.
671
750
 
672
751
  ### Schema setup
673
752
 
674
- Enable RSC at the top level of your schema:
753
+ Server data is on unless the schema turns it off with `"rsc": { "enabled": false }` — the page render and the
754
+ default `getRscData` (`connectorRscData`) read a missing `rsc` alike. `authorSpace` writes it out for a space with a
755
+ server provider:
675
756
 
676
757
  ```json
677
758
  {
@@ -710,7 +791,7 @@ Implement `getRscData` in your adapters to serve data from the `/_rsc` endpoint.
710
791
  When `ids` is provided the client is performing a **partial refresh** — only return data for those element IDs. When `ids` is absent, return data for all elements (full fetch):
711
792
 
712
793
  ```ts
713
- import type { SSRAdapters, SSRRscContext, SSRRscData } from '@plitzi/sdk-server';
794
+ import type { SSRAdapters, SSRRscContext, SSRRscData } from '@plitzi/sdk-shared';
714
795
 
715
796
  const getRscData = async ({ user, ids, loadOfflineData }: SSRRscContext): Promise<SSRRscData> => {
716
797
  // Only serve data when the schema has RSC enabled. `loadOfflineData` joins the read the page render already
@@ -773,6 +854,10 @@ type SSRRscData = {
773
854
 
774
855
  Return `{}` (empty object) when there is no server data for the current request. Each key must match the `id` of a schema element with `runtime: 'server'`.
775
856
 
857
+ **One request per question, the newest winning.** The client (`refreshRsc`) joins a refresh already in flight for the same URL, and aborts one a newer refresh would overwrite — a whole payload asked again, an element asked again — so answers never land out of order. `cancelRsc(store, ids)` (an `apiContainer`'s `cancelQuery`) aborts on purpose. The server hears it: `SSRRscContext.signal` aborts when the browser hangs up, and the default `getRscData` stops every element still resolving — an action's render run included, unless another visitor's identical render is still waiting on the same run. A custom adapter does the same with the signal it is handed.
858
+
859
+ What a refresh asks for besides the location (`/_rsc?location=…&country=fr`) is `req.ctx.rscParams`; the action resolver hands it to the action over the `input` the element was saved with.
860
+
776
861
  ### `/_rsc` endpoint
777
862
 
778
863
  The server automatically registers `GET /_rsc` when `adapters.getRscData` is provided. The endpoint:
@@ -780,7 +865,7 @@ The server automatically registers `GET /_rsc` when `adapters.getRscData` is pro
780
865
  1. Reads `spaceId`, `environment`, and `revision` from the resolved `spaceDeployment` context.
781
866
  2. Reads the authenticated user from `ctx.user`.
782
867
  3. Reads optional `?ids=elem1,elem2` for partial refresh.
783
- 4. Calls `adapters.getRscData({ req, spaceId, environment, revision, user, ids, loadOfflineData })`.
868
+ 4. Calls `adapters.getRscData({ req, spaceId, environment, revision, user, ids, loadOfflineData, signal })`.
784
869
  5. Returns a JSON payload:
785
870
 
786
871
  ```json
@@ -815,6 +900,7 @@ createServer({
815
900
  | `enabled` | `boolean` | `true` (when adapter provided) | Activate or deactivate the RSC endpoint. |
816
901
  | `path` | `string` | `'/_rsc'` | URL path for the RSC endpoint. |
817
902
  | `cacheTtlMs` | `number` | `30000` | TTL in milliseconds for the RSC response cache. Set to `0` to disable RSC caching. Ignored for the `main` environment. |
903
+ | `elementTimeoutMs` | `number` | `5000` | How long one server element may take before the page is answered without it — and what it was resolving is cancelled. The page's ceiling, winning over the producer's own (`action.limits.timeoutMs`). A section cut by it says to raise this. |
818
904
 
819
905
  ### Consuming RSC data in plugins
820
906
 
@@ -1109,7 +1195,7 @@ Credential comparison uses `crypto.timingSafeEqual` to prevent timing attacks. I
1109
1195
  Register request-scoped middleware to run before the SSR renderer. Middlewares execute in the order they are declared and can short-circuit by not calling `next()`.
1110
1196
 
1111
1197
  ```ts
1112
- import type { SSRMiddleware } from '@plitzi/sdk-server';
1198
+ import type { SSRMiddleware } from '@plitzi/sdk-shared';
1113
1199
 
1114
1200
  const corsMiddleware: SSRMiddleware = (req, res, next) => {
1115
1201
  res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
@@ -1243,7 +1329,9 @@ ends any other event stream, lets what is being answered finish, and cuts whatev
1243
1329
  (`SHUTDOWN_GRACE_MS`). A page left open on a board, or an agent's app listening, no longer holds a Ctrl+C or a
1244
1330
  SIGTERM until something kills the process.
1245
1331
 
1246
- `@plitzi/sdk-server/mysql` has the same pair over a `mysql2` pool (`createMysqlJobQueue`, `createMysqlKv`). Anything
1332
+ `@plitzi/sdk-server/mysql` has the same pair over a `mysql2` pool (`createMysqlJobQueue`, `createMysqlKv`). With no
1333
+ database at all, `kv` survives a restart in a file: `createFileKv` (`@plitzi/sdk-server/actions`, one process, JSON) or
1334
+ `createSqliteKv` (`@plitzi/sdk-server/sqlite`, any number of processes on the file). Anything
1247
1335
  else is the `ActionJobQueue` contract written against your store — see
1248
1336
  [server actions § self-hosted](https://github.com/plitzi/plitzi-workspace/blob/main/docs/en/server-actions.md#13-for-a-self-hosted-deployment).
1249
1337
 
@@ -1299,7 +1387,7 @@ URLs become `/sdk-assets/plitzi-sdk.js?v=<assetVersion>`. This is separate from
1299
1387
  By default the server uses its built-in EJS template. You can replace it with any function that receives the template params and returns an HTML string:
1300
1388
 
1301
1389
  ```ts
1302
- import type { SSRTemplateFn } from '@plitzi/sdk-server';
1390
+ import type { SSRTemplateFn } from '@plitzi/sdk-shared';
1303
1391
 
1304
1392
  const templateFn: SSRTemplateFn = ({ html, offlineData, jsPath, cssPath, plugins, react, reactDom, reactDomClient, reactJsx }) => `
1305
1393
  <!doctype html>
@@ -1322,6 +1410,16 @@ createServer({ templateFn, adapters: { ... } });
1322
1410
 
1323
1411
  The function is called once per render (cache misses only). The built-in `template.ejs` is used as fallback when `templateFn` is not set.
1324
1412
 
1413
+ What the built-in template does that a custom one has to decide for itself:
1414
+
1415
+ - **The space travels beside the page.** The built-in template leaves it out of the payload, names it with
1416
+ `spaceDocumentPath` (`<link id="plitzi-space" rel="preload" as="fetch">`) and its bootstrap fetches it before it
1417
+ hydrates. A custom template is handed the space inline in `offlineData`, as before.
1418
+ - **Only the plugins the page draws are imported up front.** An entry with `deferred` is handed to `render()` as
1419
+ `{ load: () => import(js), css, declaration }`: the SDK registers it at once and loads its code and stylesheet the
1420
+ first time one of its elements is drawn. A template that imports every entry statically still works; it only loads
1421
+ more than the page draws.
1422
+
1325
1423
  **Streaming compatibility**: when `streaming: true` the server calls `templateFn` with a sentinel placeholder (`<!--SSR_CONTENT-->`) in place of the React HTML, splits the output at that marker, and streams head and tail separately. Existing templates that interpolate `html` as-is are compatible without any changes.
1326
1424
 
1327
1425
  ## Template props
@@ -1346,9 +1444,11 @@ return {
1346
1444
  | `title` | `string` | Page `<title>`. Defaults to `'Plitzi App'`. |
1347
1445
  | `jsPath` | `string` | URL for the SDK JS module. Defaults to `/sdk-assets/plitzi-sdk.js`. |
1348
1446
  | `cssPath` | `string` | URL for the SDK stylesheet. Defaults to `/sdk-assets/plitzi-sdk.css`. |
1447
+ | `iconsCssPath` | `string` | URL for Font Awesome's stylesheet, linked after `cssPath`; its fonts are files beside it. Defaults to `/sdk-assets/plitzi-sdk-icons.css`. |
1448
+ | `spaceDocumentPath` | `string` | Where the page fetches the space from (`/_plitzi/space/<hash>.json`), preloaded in the head and read by the bootstrap before it hydrates. Set by the built-in template's render; absent for a draft preview and for a custom `templateFn`, which receive the space inline in `offlineData`. |
1349
1449
  | `builderJsPath` | `string` | URL for the builder JS module. Omitted by default. |
1350
1450
  | `builderCssPath` | `string` | URL for the builder stylesheet. Omitted by default. |
1351
- | `plugins` | `PluginEntry[]` | Plugin entries to inject. Normally set automatically via `pluginNames`. |
1451
+ | `plugins` | `PluginEntry[]` | Plugin entries to inject. Normally set automatically via `pluginNames`. An entry with `deferred` is one the page draws none of: its declaration, for the browser to load its code when a page that draws it is opened. |
1352
1452
  | `react` | `string` | React ESM URL. Defaults to `esm.sh/react@19`. |
1353
1453
  | `reactDom` | `string` | ReactDOM ESM URL. |
1354
1454
  | `reactDomClient` | `string` | ReactDOM client ESM URL. |
@@ -1438,6 +1538,27 @@ package, not a snippet.
1438
1538
 
1439
1539
  ## Exported types
1440
1540
 
1541
+ What this package's own code defines is exported from it:
1542
+
1543
+ ```ts
1544
+ import type {
1545
+ ServerConfig,
1546
+ SSRContext,
1547
+ PipelineExtensions,
1548
+ JsonAdaptersConfig,
1549
+ AuthAdapters,
1550
+ AuthAdaptersConfig,
1551
+ CloudAdaptersConfig,
1552
+ SpaceAdaptersConfig
1553
+ } from '@plitzi/sdk-server';
1554
+ ```
1555
+
1556
+ `ServerConfig` is what `createServer` takes — `SSRServerConfig` with the page adapters required, plus `auth` and
1557
+ `sessionRenewal` — and what a project types its own options with. `SSRContext` here is the request context a pipeline
1558
+ stage sees.
1559
+
1560
+ The shapes the server shares with the SDK and `@plitzi/sdk-mcp` are `@plitzi/sdk-shared`'s:
1561
+
1441
1562
  ```ts
1442
1563
  import type {
1443
1564
  SSRAdapters,
@@ -1446,13 +1567,13 @@ import type {
1446
1567
  SSRResponseHelpers,
1447
1568
  SSRMiddleware,
1448
1569
  SSRMiddlewareNext,
1449
- SSRContext,
1450
1570
  SSRSpaceDeployment,
1451
1571
  SSRTemplateProps,
1452
1572
  SSRTemplateFn,
1453
1573
  SSRCredential,
1454
1574
  SSRUser,
1455
1575
  SSRHeaders,
1576
+ SSRRscContext,
1456
1577
  SSRRscData,
1457
1578
  SSRRscConfig,
1458
1579
  SSRServer,
@@ -1463,9 +1584,6 @@ import type {
1463
1584
  PluginEntry,
1464
1585
  PluginRegistry,
1465
1586
  CacheFilter,
1466
- CacheManager,
1467
- JsonAdaptersConfig,
1468
- AuthAdapters,
1469
- AuthAdaptersConfig
1470
- } from '@plitzi/sdk-server';
1587
+ CacheManager
1588
+ } from '@plitzi/sdk-shared';
1471
1589
  ```
package/dist/actions.d.ts CHANGED
@@ -41,18 +41,21 @@ export { checkAction } from './modules/actions/runtime/check';
41
41
  export type { ActionCheckDeps } from './modules/actions/runtime/check';
42
42
  export { ActionRefusal, ActionRunError } from './modules/actions/runtime/errors';
43
43
  export { DEFAULT_LIMITS } from './modules/actions/runtime/limits';
44
+ export { MIN_SIGNING_SECRET_LENGTH } from './modules/actions/runtime/signing';
44
45
  export { createTaskRegistry, taskName } from './modules/actions/tasks/registry';
45
46
  export { describeCatalog, describeTask } from './modules/actions/taskCatalog';
46
47
  /**
47
48
  * The `kv` seam: an ADAPTER a deployment fills, and the logic that sits on top of it.
48
49
  *
49
- * There is deliberately nothing here that talks to a store — no Redis, no database, no client of any kind. A
50
- * deployment passes five string operations over whatever it already runs, and `createKvStore` supplies everything
51
- * that decides how a counter behaves, so the rule a rate limit depends on is written once rather than once per
52
- * deployment. The in-process Map is the same shape, which is why the default and a cluster's store behave
53
- * identically instead of nearly so.
50
+ * There is deliberately no client of any store here: a deployment passes six string operations over whatever it
51
+ * already runs (`createRedisKv` wraps a client it opened), and `createKvStore` supplies everything that decides how a
52
+ * counter behaves, so the rule a rate limit depends on is written once rather than once per deployment. The
53
+ * in-process Map is the same shape, which is why the default and a cluster's store behave identically instead of
54
+ * nearly so — and `createFileKv` is that Map kept in a JSON file, for one process that must survive a restart.
54
55
  */
55
56
  export { createMemoryKv } from './modules/actions/runtime/memoryKv';
57
+ export { createFileKv } from './modules/actions/runtime/fileKv';
58
+ export type { FileKv, FileKvOptions } from './modules/actions/runtime/fileKv';
56
59
  export { createKvStore } from './modules/actions/runtime/kvStore';
57
60
  export { createRedisKv } from './modules/actions/runtime/redisKv';
58
61
  export type { RedisKvClient, RedisKvOptions } from './modules/actions/runtime/redisKv';
package/dist/actions.js CHANGED
@@ -9,11 +9,13 @@ import { createKvStore } from "./modules/actions/runtime/kvStore.js";
9
9
  import { DEFAULT_LIMITS } from "./modules/actions/runtime/limits.js";
10
10
  import { createMemoryKv } from "./modules/actions/runtime/memoryKv.js";
11
11
  import { spaceKvPatterns } from "./modules/actions/runtime/namespaceKv.js";
12
+ import { MIN_SIGNING_SECRET_LENGTH } from "./modules/actions/runtime/signing.js";
12
13
  import { createTaskRegistry, taskName } from "./modules/actions/tasks/registry.js";
13
14
  import { checkAction } from "./modules/actions/runtime/check.js";
14
15
  import { createRejectLogger, createRunLogger } from "./modules/actions/runtime/runLogger.js";
15
16
  import { describeCatalog, describeTask } from "./modules/actions/taskCatalog.js";
16
17
  import { createActionsModule } from "./modules/actions/index.js";
18
+ import { createFileKv } from "./modules/actions/runtime/fileKv.js";
17
19
  import { createRedisKv } from "./modules/actions/runtime/redisKv.js";
18
20
  import { cronFiresBetween, cronMatches, cronNextFire, parseCron } from "@plitzi/sdk-shared/actions";
19
- export { ActionRefusal, ActionRunError, DEFAULT_LIMITS, DEFAULT_MAX_ATTEMPTS, MAX_LIST_BYTES, MAX_LIST_ENTRIES, checkAction, createActionJobs, createActionsModule, createJobWorker, createKvStore, createMemoryJobQueue, createMemoryKv, createRedisKv, createRejectLogger, createRunLogger, createScheduler, createTaskRegistry, cronFiresBetween, cronMatches, cronNextFire, describeCatalog, describeTask, parseCron, scheduleFor, scheduleJobId, schedulesFor, spaceKvPatterns, taskName };
21
+ export { ActionRefusal, ActionRunError, DEFAULT_LIMITS, DEFAULT_MAX_ATTEMPTS, MAX_LIST_BYTES, MAX_LIST_ENTRIES, MIN_SIGNING_SECRET_LENGTH, checkAction, createActionJobs, createActionsModule, createFileKv, createJobWorker, createKvStore, createMemoryJobQueue, createMemoryKv, createRedisKv, createRejectLogger, createRunLogger, createScheduler, createTaskRegistry, cronFiresBetween, cronMatches, cronNextFire, describeCatalog, describeTask, parseCron, scheduleFor, scheduleJobId, schedulesFor, spaceKvPatterns, taskName };
@@ -7,7 +7,7 @@ export type JsonAdaptersConfig = {
7
7
  /**
8
8
  * The space: a path to a `{ schema, style }` JSON, a function returning one per request, or the data itself for a
9
9
  * consumer that already holds it (composed at startup, fetched once, built in a test). Only a path can be written
10
- * back to, so `saveOfflineData` is offered only when one was given.
10
+ * back to, so `saveSchema` and `saveStyle` are offered only when one was given.
11
11
  */
12
12
  offlineData: OfflineDataRaw | string | ((spaceId: number, environment: string, revision?: number) => string);
13
13
  deployment?: string | SSRSpaceDeployment | Record<string, SSRSpaceDeployment>;
@@ -38,13 +38,21 @@ var createJsonAdapters = (config) => {
38
38
  return Promise.resolve(void 0);
39
39
  }
40
40
  };
41
- const saveOfflineData = (spaceId, environment, data) => {
41
+ const getSchema = async (spaceId, environment) => (await getOfflineData(spaceId, environment))?.schema;
42
+ const getStyle = async (spaceId, environment) => (await getOfflineData(spaceId, environment))?.style;
43
+ const writeBack = (spaceId, environment, change) => {
42
44
  const filePath = pathFor(spaceId, environment);
43
45
  if (!filePath) return Promise.resolve();
44
- writeFileSync(filePath, JSON.stringify(data, null, 2));
46
+ const current = json.read(filePath);
47
+ writeFileSync(filePath, JSON.stringify({
48
+ ...current,
49
+ ...change
50
+ }, null, 2));
45
51
  json.forget(filePath);
46
52
  return Promise.resolve();
47
53
  };
54
+ const saveSchema = (spaceId, environment, schema) => writeBack(spaceId, environment, { schema });
55
+ const saveStyle = (spaceId, environment, style) => writeBack(spaceId, environment, { style });
48
56
  const getSpaceDeployment = (req) => {
49
57
  const { deployment } = config;
50
58
  if (!deployment) return Promise.resolve({
@@ -72,7 +80,12 @@ var createJsonAdapters = (config) => {
72
80
  return {
73
81
  getOfflineData,
74
82
  getSpaceDeployment,
75
- ...typeof config.offlineData !== "object" ? { saveOfflineData } : {}
83
+ getSchema,
84
+ getStyle,
85
+ ...typeof config.offlineData !== "object" ? {
86
+ saveSchema,
87
+ saveStyle
88
+ } : {}
76
89
  };
77
90
  };
78
91
  //#endregion
@@ -22,6 +22,10 @@ var isManifest = checkedOnce(isConnectorManifestDraft);
22
22
  var isDocument = checkedOnce(isActionDocument);
23
23
  /** What `getFunctions` answers: built by `@plitzi/sdk-server/functions` from a space's saved bundle, a live object. */
24
24
  var isSpaceFunctions = (value) => isRecord(value) && isRecord(value.bundle) && typeof value.bundle.id === "string" && typeof value.bundle.load === "function" && isRecord(value.manifest);
25
+ /** What `getPluginFunctions` answers: a `SpaceFunctions` per plugin type. */
26
+ var isPluginFunctions = (value) => isRecord(value) && Object.values(value).every(isSpaceFunctions);
27
+ /** What `getData` answers: the space's data files' text, by path. */
28
+ var isProjectData = (value) => isRecord(value) && Object.values(value).every((text) => typeof text === "string");
25
29
  /** Why a document is not one this server runs: the first problem its validator found, by where it is. */
26
30
  var firstProblem = (errors) => {
27
31
  const first = errors.at(0);
@@ -63,7 +67,7 @@ var connectorLookupsOf = (config) => {
63
67
  * not one is refused; listed, it is left out and said, so one broken action does not take its space's schedule down.
64
68
  */
65
69
  var actionLookupsOf = (config) => {
66
- const { getConnector, getFunctions, listActions } = config;
70
+ const { getConnector, getData, getFunctions, getPluginFunctions, listActions } = config;
67
71
  return {
68
72
  getAction: async (spaceId, actionId, at) => {
69
73
  const found = await config.getAction(spaceId, actionId, at);
@@ -89,14 +93,26 @@ var actionLookupsOf = (config) => {
89
93
  if (!isSpaceFunctions(found)) throw new Error(`The functions of space ${String(spaceId)} are not what @plitzi/sdk-server/functions builds for one`);
90
94
  return found;
91
95
  } } : {},
96
+ ...getData ? { getData: async (spaceId, at) => {
97
+ const found = await getData(spaceId, at);
98
+ if (found === void 0 || found === null) return;
99
+ if (!isProjectData(found)) throw new Error(`The data of space ${String(spaceId)} is not its files' text by path`);
100
+ return found;
101
+ } } : {},
102
+ ...getPluginFunctions ? { getPluginFunctions: async (spaceId, at) => {
103
+ const found = await getPluginFunctions(spaceId, at);
104
+ if (found === void 0 || found === null) return;
105
+ if (!isPluginFunctions(found)) throw new Error(`The plugin functions of space ${String(spaceId)} are not, by plugin type, what @plitzi/sdk-server/functions builds for one`);
106
+ return found;
107
+ } } : {},
92
108
  ...config.getFlags ? { getFlags: config.getFlags } : {}
93
109
  };
94
110
  };
95
111
  var isFunction = (value) => typeof value === "function";
96
112
  /** A driver as `createDbDrivers` builds one: the engine a credential names, and how to query it. */
97
113
  var isDbDriver = (value) => isRecord(value) && typeof value.engine === "string" && isFunction(value.query);
98
- /** The functions config as `@plitzi/sdk-server/functions` builds it: native definitions, a runner, ceilings. */
99
- var isFunctionsConfig = (value) => isRecord(value) && (value.native === void 0 || Array.isArray(value.native) && value.native.every(isRecord)) && (value.runner === void 0 || isRecord(value.runner) && isFunction(value.runner.describe) && isFunction(value.runner.invoke)) && (value.limits === void 0 || isRecord(value.limits));
114
+ /** The functions config as `@plitzi/sdk-server/functions` builds it: native and plugin definitions, a runner, ceilings. */
115
+ var isFunctionsConfig = (value) => isRecord(value) && (value.native === void 0 || Array.isArray(value.native) && value.native.every(isRecord)) && (value.plugins === void 0 || isRecord(value.plugins) && Object.values(value.plugins).every(isRecord)) && (value.runner === void 0 || isRecord(value.runner) && isFunction(value.runner.describe) && isFunction(value.runner.invoke)) && (value.limits === void 0 || isRecord(value.limits));
100
116
  /** The deployment's database drivers, each held to what a `db` task calls — or why one is not, as the server starts. */
101
117
  var dbDriversOf = (drivers) => {
102
118
  if (drivers === void 0) return;
@@ -107,7 +123,7 @@ var dbDriversOf = (drivers) => {
107
123
  /** The deployment's functions config, held to what runs a space's code — or why it is not, as the server starts. */
108
124
  var functionsConfigOf = (config) => {
109
125
  if (config === void 0) return;
110
- if (!isFunctionsConfig(config)) throw new Error("`functions` is not what @plitzi/sdk-server/functions configures: `native` definitions, a `runner` with `describe` and `invoke`, `limits`.");
126
+ if (!isFunctionsConfig(config)) throw new Error("`functions` is not what @plitzi/sdk-server/functions configures: `native` definitions, `plugins` by type, a `runner` with `describe` and `invoke`, `limits`.");
111
127
  return config;
112
128
  };
113
129
  //#endregion
@@ -14,7 +14,7 @@ import { createPageServer } from "./server/pageServer.js";
14
14
  * caches or plugin manager this one does. */
15
15
  /**
16
16
  * Fills in `getRscData` from whatever can produce a server element's data — `connectors`, `action.lookups`, the
17
- * files of `publicDir` — when the deployment did not write one.
17
+ * files of `dataDir` and `publicDir` — when the deployment did not write one.
18
18
  *
19
19
  * The lookups are already here and the assembly is entirely this package's — a resolver over `resolveRscData`.
20
20
  * Leaving it out meant every deployment passed the same lookups twice: once as config, for the write endpoint,
@@ -25,7 +25,7 @@ import { createPageServer } from "./server/pageServer.js";
25
25
  * with no configuration missing anywhere.
26
26
  */
27
27
  var withConnectorRsc = (config) => {
28
- if (config.adapters.getRscData || !config.connectors && !config.action?.lookups && !config.publicDir) return config;
28
+ if (config.adapters.getRscData || !config.connectors && !config.action?.lookups && !config.publicDir && !config.dataDir) return config;
29
29
  const resolved = {
30
30
  ...config,
31
31
  adapters: { ...config.adapters }
@@ -39,6 +39,7 @@ var withConnectorRsc = (config) => {
39
39
  connectors: config.connectors ? connectorLookupsOf(config.connectors) : void 0,
40
40
  actions,
41
41
  publicDir: config.publicDir,
42
+ dataDir: config.dataDir,
42
43
  elementTimeoutMs: config.rsc?.elementTimeoutMs
43
44
  });
44
45
  return resolved;
@@ -60,11 +60,16 @@ var createSocialAuthRouteHandlers = ({ social, cookies, issueSession, onError })
60
60
  * uses: a relative path or an allowed origin is honoured, anything else silently becomes the default. A
61
61
  * missing or unresolved parameter lands on the default too, which is what makes it safe for a template to
62
62
  * pass a token it could not fill in.
63
+ *
64
+ * `?fallback=` is where to go instead when `redirect` is refused, vetted by the same judgement — a way BACK
65
+ * from the screen, whose default ("you are signed in") is the wrong place for somebody who never signed in.
63
66
  */
64
67
  method: "GET",
65
68
  path: "/continue",
66
69
  handle: (req, res) => {
67
- res.redirect(social.sanitizeRedirect(req.query?.redirect));
70
+ const redirect = req.query?.redirect;
71
+ const vetted = social.sanitizeRedirect(redirect);
72
+ res.redirect(vetted === redirect ? vetted : social.sanitizeRedirect(req.query?.fallback));
68
73
  return Promise.resolve();
69
74
  }
70
75
  },
@@ -1,4 +1,5 @@
1
1
  import { Stage } from '../types';
2
+ import { PluginEntry } from '@plitzi/sdk-shared';
2
3
  /** Where an open page listens for "the space changed: load again", in development. */
3
4
  export declare const DEV_RELOAD_PATH = "/__plitzi/reload";
4
5
  /**
@@ -8,11 +9,21 @@ export declare const DEV_RELOAD_PATH = "/__plitzi/reload";
8
9
  *
9
10
  * Only with `devReload` on: every open page keeps a connection for it, which a server that never calls `reloadPages`
10
11
  * — a published one, or a platform's — would pay for and get nothing from. The template listens only then too.
12
+ *
13
+ * Each connection is greeted with this process's `boot`, and a page that reconnects to a different one loads again: the
14
+ * server restarted — `start:dev` saw its code change (`main.ts`, its options, its actions, `src/functions/`) — and
15
+ * what is on screen was built by the one before. Without it the page kept the old plugin until somebody reloaded it,
16
+ * which reads as "my change did nothing".
11
17
  */
12
18
  export declare const createDevReload: () => {
13
19
  stage: Stage;
14
20
  /** Every page open on this server loads again. */
15
21
  reload: () => void;
22
+ /**
23
+ * A plugin built again: every open page imports its new bundle and swaps it where it is drawn (`keyName` is what
24
+ * the page registered it as), and points its stylesheet at the new one — without loading again.
25
+ */
26
+ plugin: ({ keyName, js, css }: PluginEntry) => void;
16
27
  /** The streams ended — the server is going away, and each page reconnects to whichever comes up next. */
17
28
  close: () => void;
18
29
  };