@plitzi/sdk-server 0.37.2 → 0.37.4

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 (359) hide show
  1. package/CHANGELOG.md +1170 -0
  2. package/README.md +100 -7
  3. package/dist/actions.d.ts +6 -2
  4. package/dist/actions.js +5 -2
  5. package/dist/adapters/cloudAdapters.js +4 -3
  6. package/dist/adapters/jsonAdapters.js +28 -6
  7. package/dist/adapters/jsonAdapters.test.d.ts +1 -0
  8. package/dist/auth.d.ts +7 -4
  9. package/dist/auth.js +8 -6
  10. package/dist/core/auth/api.d.ts +29 -1
  11. package/dist/core/auth/api.js +47 -13
  12. package/dist/core/auth/createAuth.d.ts +4 -0
  13. package/dist/core/auth/createAuth.js +3 -3
  14. package/dist/core/auth/credentials.d.ts +3 -0
  15. package/dist/core/auth/credentials.js +1 -1
  16. package/dist/core/auth/csrf.d.ts +25 -1
  17. package/dist/core/auth/csrf.js +29 -4
  18. package/dist/core/auth/renewal.d.ts +26 -0
  19. package/dist/core/auth/renewal.js +24 -0
  20. package/dist/core/auth/routes.js +8 -6
  21. package/dist/core/auth/session.d.ts +45 -0
  22. package/dist/core/auth/session.js +47 -1
  23. package/dist/core/auth/sessionActivity.d.ts +11 -0
  24. package/dist/core/auth/sessionActivity.js +14 -0
  25. package/dist/core/auth/throttle.d.ts +15 -0
  26. package/dist/core/auth/throttle.js +29 -1
  27. package/dist/core/auth/totp.d.ts +9 -1
  28. package/dist/core/auth/totp.js +11 -6
  29. package/dist/core/createServer.actions.test.d.ts +1 -0
  30. package/dist/core/createServer.d.ts +14 -1
  31. package/dist/core/createServer.functions.test.d.ts +1 -0
  32. package/dist/core/createServer.js +22 -12
  33. package/dist/core/fontStore.js +1 -1
  34. package/dist/core/handlers/authRouteHandlers.d.ts +1 -1
  35. package/dist/core/handlers/authRouteHandlers.js +57 -21
  36. package/dist/core/handlers/csrfMiddleware.d.ts +23 -0
  37. package/dist/core/handlers/csrfMiddleware.js +31 -7
  38. package/dist/core/handlers/csrfMiddleware.test.d.ts +1 -0
  39. package/dist/core/handlers/oauthRouteHandlers.js +5 -3
  40. package/dist/core/handlers/socialRouteHandlers.d.ts +1 -1
  41. package/dist/core/handlers/socialRouteHandlers.js +2 -1
  42. package/dist/core/http/dispatcher.d.ts +9 -1
  43. package/dist/core/http/dispatcher.js +38 -11
  44. package/dist/core/http/navigation.d.ts +5 -2
  45. package/dist/core/http/navigation.js +7 -4
  46. package/dist/core/http/socketResponse.d.ts +18 -0
  47. package/dist/core/http/socketResponse.js +59 -0
  48. package/dist/core/http/sse.d.ts +26 -0
  49. package/dist/core/http/sse.js +78 -0
  50. package/dist/core/http/stages/authRoutes.js +3 -1
  51. package/dist/core/http/stages/exchangeRoute.test.d.ts +1 -0
  52. package/dist/core/http/stages/middlewares.d.ts +1 -1
  53. package/dist/core/http/stages/middlewares.js +16 -10
  54. package/dist/core/http/stages/middlewares.test.d.ts +1 -0
  55. package/dist/core/http/stages/oauth.js +2 -2
  56. package/dist/core/http/stages/sessionRenewal.d.ts +16 -0
  57. package/dist/core/http/stages/sessionRenewal.js +81 -0
  58. package/dist/core/http/stages/sessionRenewal.test.d.ts +1 -0
  59. package/dist/core/http/stages/signInRoute.d.ts +2 -0
  60. package/dist/core/http/stages/signInRoute.js +139 -0
  61. package/dist/core/http/stages/signInRoute.test.d.ts +1 -0
  62. package/dist/core/http/types.d.ts +9 -0
  63. package/dist/core/http/webExchange.d.ts +18 -0
  64. package/dist/core/http/webExchange.js +53 -0
  65. package/dist/core/oauth/authorize.js +37 -13
  66. package/dist/core/oauth/consentPage.js +11 -0
  67. package/dist/core/oauth/issueContext.d.ts +14 -0
  68. package/dist/core/oauth/issueContext.js +32 -0
  69. package/dist/core/oauth/records.d.ts +7 -0
  70. package/dist/core/oauth/register.d.ts +2 -0
  71. package/dist/core/oauth/register.js +32 -11
  72. package/dist/core/oauth/register.test.d.ts +1 -0
  73. package/dist/core/oauth/token.js +21 -6
  74. package/dist/core/oauth/token.test.d.ts +1 -0
  75. package/dist/core/requestParser.d.ts +4 -0
  76. package/dist/core/requestParser.js +14 -7
  77. package/dist/core/sdkAssets.js +1 -1
  78. package/dist/core/server/baseServer.d.ts +10 -0
  79. package/dist/core/server/baseServer.js +42 -11
  80. package/dist/core/server/closeOnSignals.js +4 -3
  81. package/dist/core/server/drain.d.ts +18 -0
  82. package/dist/core/server/drain.js +62 -0
  83. package/dist/core/server/drain.test.d.ts +1 -0
  84. package/dist/core/server/fleet/channel.d.ts +103 -0
  85. package/dist/core/server/fleet/channel.js +191 -0
  86. package/dist/core/server/fleet/channel.test.d.ts +1 -0
  87. package/dist/core/server/fleet/fixtures/fleetServer.d.ts +1 -0
  88. package/dist/core/server/fleet/fleet.integration.test.d.ts +1 -0
  89. package/dist/core/server/fleet/link.d.ts +23 -0
  90. package/dist/core/server/fleet/link.js +70 -0
  91. package/dist/core/server/fleet/relay.d.ts +11 -0
  92. package/dist/core/server/fleet/relay.js +104 -0
  93. package/dist/core/server/fleet/relay.test.d.ts +1 -0
  94. package/dist/core/server/fleet/role.d.ts +46 -0
  95. package/dist/core/server/fleet/role.js +59 -0
  96. package/dist/core/server/fleet/role.test.d.ts +1 -0
  97. package/dist/core/server/fleet/stores.d.ts +7 -0
  98. package/dist/core/server/fleet/stores.js +19 -0
  99. package/dist/core/server/fleet/supervisor.d.ts +26 -0
  100. package/dist/core/server/fleet/supervisor.js +108 -0
  101. package/dist/core/server/fleet/supervisor.test.d.ts +1 -0
  102. package/dist/core/server/pageServer.js +34 -5
  103. package/dist/core/services/action.js +10 -2
  104. package/dist/core/services/functionRoutes.d.ts +9 -0
  105. package/dist/core/services/functionRoutes.js +47 -0
  106. package/dist/core/services/realtime.d.ts +8 -0
  107. package/dist/core/services/realtime.js +57 -0
  108. package/dist/core/services/registry.js +6 -2
  109. package/dist/core/staticFiles.js +12 -4
  110. package/dist/core/staticFiles.test.d.ts +1 -0
  111. package/dist/core/transports.d.ts +13 -3
  112. package/dist/core/transports.js +14 -5
  113. package/dist/functions-api.d.ts +281 -0
  114. package/dist/functions.d.ts +19 -0
  115. package/dist/functions.js +5 -0
  116. package/dist/functionsRunner.d.ts +15 -0
  117. package/dist/functionsRunner.js +6 -0
  118. package/dist/handlers.d.ts +2 -2
  119. package/dist/handlers.js +2 -2
  120. package/dist/helpers/atomicFile.d.ts +10 -0
  121. package/dist/helpers/atomicFile.js +33 -0
  122. package/dist/helpers/atomicFile.test.d.ts +1 -0
  123. package/dist/helpers/buildResponseHelpers.js +35 -11
  124. package/dist/helpers/buildServerInfo.js +4 -2
  125. package/dist/helpers/buildServerInfo.test.d.ts +1 -0
  126. package/dist/helpers/cache/CompressedFileCache.d.ts +18 -0
  127. package/dist/helpers/cache/CompressedFileCache.js +44 -0
  128. package/dist/helpers/cache/CompressedFileCache.test.d.ts +1 -0
  129. package/dist/helpers/cache/cacheManager.d.ts +1 -1
  130. package/dist/helpers/cache/cacheManager.js +2 -1
  131. package/dist/helpers/cache/cacheManager.test.d.ts +1 -0
  132. package/dist/helpers/cache/index.d.ts +1 -1
  133. package/dist/helpers/cache/keys.d.ts +15 -1
  134. package/dist/helpers/cache/keys.js +47 -5
  135. package/dist/helpers/cache/serverCaches.d.ts +7 -1
  136. package/dist/helpers/compress.d.ts +3 -1
  137. package/dist/helpers/compress.js +11 -3
  138. package/dist/helpers/hydrationPayload.d.ts +11 -0
  139. package/dist/helpers/hydrationPayload.js +51 -0
  140. package/dist/helpers/hydrationPayload.test.d.ts +1 -0
  141. package/dist/helpers/metrics.d.ts +1 -1
  142. package/dist/helpers/metrics.js +3 -2
  143. package/dist/helpers/outboundGuard.d.ts +14 -6
  144. package/dist/helpers/outboundGuard.js +132 -8
  145. package/dist/helpers/paintedCookie.d.ts +8 -0
  146. package/dist/helpers/paintedCookie.js +13 -0
  147. package/dist/helpers/reportReactBuild.d.ts +9 -0
  148. package/dist/helpers/reportReactBuild.js +18 -0
  149. package/dist/helpers/reportReactBuild.test.d.ts +1 -0
  150. package/dist/helpers/serverLog.d.ts +25 -1
  151. package/dist/helpers/serverLog.js +68 -3
  152. package/dist/helpers/serverLog.test.d.ts +1 -0
  153. package/dist/helpers/wsFrame.d.ts +3 -0
  154. package/dist/helpers/wsFrame.js +5 -0
  155. package/dist/index.d.ts +6 -1
  156. package/dist/index.js +5 -2
  157. package/dist/kernel.d.ts +2 -1
  158. package/dist/kernel.js +3 -2
  159. package/dist/modules/actions/connectorWrite.js +2 -1
  160. package/dist/modules/actions/index.d.ts +29 -10
  161. package/dist/modules/actions/index.js +43 -6
  162. package/dist/modules/actions/jobs/failureStreak.d.ts +17 -0
  163. package/dist/modules/actions/jobs/failureStreak.js +47 -0
  164. package/dist/modules/actions/jobs/failureStreak.test.d.ts +1 -0
  165. package/dist/modules/actions/jobs/index.d.ts +2 -1
  166. package/dist/modules/actions/jobs/index.js +5 -3
  167. package/dist/modules/actions/jobs/memoryQueue.d.ts +3 -0
  168. package/dist/modules/actions/jobs/memoryQueue.js +17 -1
  169. package/dist/modules/actions/jobs/scheduler.d.ts +2 -0
  170. package/dist/modules/actions/jobs/scheduler.js +12 -7
  171. package/dist/modules/actions/jobs/worker.d.ts +3 -1
  172. package/dist/modules/actions/jobs/worker.js +9 -4
  173. package/dist/modules/actions/moduleFor.js +5 -2
  174. package/dist/modules/actions/runtime/errors.d.ts +12 -0
  175. package/dist/modules/actions/runtime/errors.js +16 -1
  176. package/dist/modules/actions/runtime/kvChange.d.ts +27 -0
  177. package/dist/modules/actions/runtime/kvChange.js +23 -0
  178. package/dist/modules/actions/runtime/kvList.d.ts +51 -0
  179. package/dist/modules/actions/runtime/kvList.js +52 -0
  180. package/dist/modules/actions/runtime/kvStore.d.ts +8 -0
  181. package/dist/modules/actions/runtime/kvStore.js +46 -20
  182. package/dist/modules/actions/runtime/memoryKv.d.ts +3 -0
  183. package/dist/modules/actions/runtime/memoryKv.js +19 -1
  184. package/dist/modules/actions/runtime/namespaceKv.d.ts +4 -6
  185. package/dist/modules/actions/runtime/namespaceKv.js +15 -3
  186. package/dist/modules/actions/runtime/namespaceKv.test.d.ts +1 -0
  187. package/dist/modules/actions/runtime/precheck.js +4 -8
  188. package/dist/modules/actions/runtime/rateLimit.d.ts +19 -0
  189. package/dist/modules/actions/runtime/rateLimit.js +19 -0
  190. package/dist/modules/actions/runtime/redisKv.d.ts +27 -0
  191. package/dist/modules/actions/runtime/redisKv.integration.test.d.ts +1 -0
  192. package/dist/modules/actions/runtime/redisKv.js +46 -0
  193. package/dist/modules/actions/runtime/report.js +2 -1
  194. package/dist/modules/actions/runtime/runAction.d.ts +10 -2
  195. package/dist/modules/actions/runtime/runAction.js +52 -116
  196. package/dist/modules/actions/runtime/runLogger.d.ts +5 -0
  197. package/dist/modules/actions/runtime/runLogger.js +8 -2
  198. package/dist/modules/actions/runtime/signing.d.ts +20 -0
  199. package/dist/modules/actions/runtime/signing.js +25 -0
  200. package/dist/modules/actions/runtime/signing.test.d.ts +1 -0
  201. package/dist/modules/actions/runtime/taskContext.d.ts +33 -0
  202. package/dist/modules/actions/runtime/taskContext.js +124 -0
  203. package/dist/modules/actions/tasks/builtins.js +5 -1
  204. package/dist/modules/actions/tasks/flow.js +81 -10
  205. package/dist/modules/actions/tasks/helpers.js +2 -2
  206. package/dist/modules/actions/tasks/http.js +2 -3
  207. package/dist/modules/actions/tasks/kv.js +39 -0
  208. package/dist/modules/actions/tasks/list.d.ts +2 -0
  209. package/dist/modules/actions/tasks/list.js +145 -0
  210. package/dist/modules/actions/tasks/realtime.d.ts +2 -0
  211. package/dist/modules/actions/tasks/realtime.js +104 -0
  212. package/dist/modules/actions/tasks/registry.d.ts +10 -1
  213. package/dist/modules/actions/tasks/registry.js +17 -6
  214. package/dist/modules/actions/transport/callHandler.js +5 -2
  215. package/dist/modules/actions/transport/catalogHandler.d.ts +5 -2
  216. package/dist/modules/actions/transport/catalogHandler.js +3 -2
  217. package/dist/modules/actions/transport/stream.d.ts +4 -5
  218. package/dist/modules/actions/transport/stream.js +12 -76
  219. package/dist/modules/actions/transport/webhookHandler.js +4 -3
  220. package/dist/modules/actions/types.d.ts +86 -3
  221. package/dist/modules/connectors/engine.js +3 -5
  222. package/dist/modules/functions/build.d.ts +22 -0
  223. package/dist/modules/functions/build.js +122 -0
  224. package/dist/modules/functions/build.test.d.ts +1 -0
  225. package/dist/modules/functions/capabilities.d.ts +22 -0
  226. package/dist/modules/functions/capabilities.js +228 -0
  227. package/dist/modules/functions/config.d.ts +34 -0
  228. package/dist/modules/functions/config.js +24 -0
  229. package/dist/modules/functions/context.d.ts +17 -0
  230. package/dist/modules/functions/context.js +88 -0
  231. package/dist/modules/functions/contract.d.ts +121 -0
  232. package/dist/modules/functions/contract.js +9 -0
  233. package/dist/modules/functions/driver.d.ts +36 -0
  234. package/dist/modules/functions/driver.js +150 -0
  235. package/dist/modules/functions/load.d.ts +15 -0
  236. package/dist/modules/functions/load.js +45 -0
  237. package/dist/modules/functions/load.test.d.ts +1 -0
  238. package/dist/modules/functions/local.d.ts +29 -0
  239. package/dist/modules/functions/local.js +61 -0
  240. package/dist/modules/functions/local.test.d.ts +1 -0
  241. package/dist/modules/functions/manifest.d.ts +24 -0
  242. package/dist/modules/functions/manifest.js +163 -0
  243. package/dist/modules/functions/manifest.test.d.ts +1 -0
  244. package/dist/modules/functions/native.d.ts +8 -0
  245. package/dist/modules/functions/native.js +21 -0
  246. package/dist/modules/functions/native.test.d.ts +1 -0
  247. package/dist/modules/functions/protocol.d.ts +216 -0
  248. package/dist/modules/functions/protocol.js +18 -0
  249. package/dist/modules/functions/routes.d.ts +36 -0
  250. package/dist/modules/functions/routes.js +181 -0
  251. package/dist/modules/functions/routes.test.d.ts +1 -0
  252. package/dist/modules/functions/runner/messages.d.ts +5 -0
  253. package/dist/modules/functions/runner/messages.js +188 -0
  254. package/dist/modules/functions/runner/remote.d.ts +19 -0
  255. package/dist/modules/functions/runner/remote.js +116 -0
  256. package/dist/modules/functions/runner/remote.test.d.ts +1 -0
  257. package/dist/modules/functions/runner/service.d.ts +32 -0
  258. package/dist/modules/functions/runner/service.js +209 -0
  259. package/dist/modules/functions/sandbox/guest.d.ts +45 -0
  260. package/dist/modules/functions/sandbox/guest.js +660 -0
  261. package/dist/modules/functions/sandbox/isolate.d.ts +37 -0
  262. package/dist/modules/functions/sandbox/isolate.js +347 -0
  263. package/dist/modules/functions/sandbox/isolate.test.d.ts +1 -0
  264. package/dist/modules/functions/sandbox/prelude.d.ts +2 -0
  265. package/dist/modules/functions/sandbox/prelude.js +49 -0
  266. package/dist/modules/functions/space.d.ts +40 -0
  267. package/dist/modules/functions/space.js +162 -0
  268. package/dist/modules/functions/testing/tasksOf.d.ts +4 -0
  269. package/dist/modules/functions/tryEntry.d.ts +9 -0
  270. package/dist/modules/functions/tryEntry.js +49 -0
  271. package/dist/modules/mongo/kv.js +31 -0
  272. package/dist/modules/mysql/accounts.js +19 -9
  273. package/dist/modules/mysql/index.d.ts +2 -1
  274. package/dist/modules/mysql/jobs/index.d.ts +1 -1
  275. package/dist/modules/mysql/jobs/kv.js +30 -0
  276. package/dist/modules/mysql/jobs/schema.d.ts +9 -0
  277. package/dist/modules/mysql/jobs/schema.js +21 -4
  278. package/dist/modules/mysql/schema.js +25 -0
  279. package/dist/modules/mysql/sessions.d.ts +1 -11
  280. package/dist/modules/mysql/sessions.js +6 -1
  281. package/dist/modules/realtime/declarations.d.ts +11 -0
  282. package/dist/modules/realtime/declarations.js +26 -0
  283. package/dist/modules/realtime/grants.d.ts +23 -0
  284. package/dist/modules/realtime/grants.js +61 -0
  285. package/dist/modules/realtime/handlers.d.ts +74 -0
  286. package/dist/modules/realtime/handlers.js +224 -0
  287. package/dist/modules/realtime/hub.d.ts +62 -0
  288. package/dist/modules/realtime/hub.js +141 -0
  289. package/dist/modules/realtime/index.d.ts +51 -0
  290. package/dist/modules/realtime/index.js +62 -0
  291. package/dist/modules/realtime/memoryPubSub.d.ts +10 -0
  292. package/dist/modules/realtime/memoryPubSub.js +43 -0
  293. package/dist/modules/realtime/realtime.test.d.ts +1 -0
  294. package/dist/modules/realtime/redisPubSub.d.ts +24 -0
  295. package/dist/modules/realtime/redisPubSub.js +36 -0
  296. package/dist/modules/realtime/redisPubSub.test.d.ts +1 -0
  297. package/dist/modules/realtime/socket.d.ts +25 -0
  298. package/dist/modules/realtime/socket.js +122 -0
  299. package/dist/modules/realtime/socket.test.d.ts +1 -0
  300. package/dist/modules/rsc/handler.d.ts +16 -0
  301. package/dist/modules/rsc/handler.js +17 -10
  302. package/dist/modules/rsc/handler.test.d.ts +1 -0
  303. package/dist/modules/rsc/resolveRscData.js +2 -1
  304. package/dist/modules/runtime/bundle.d.ts +24 -0
  305. package/dist/modules/runtime/bundle.js +102 -0
  306. package/dist/modules/runtime/contract.d.ts +37 -0
  307. package/dist/modules/runtime/contract.js +21 -0
  308. package/dist/modules/runtime/entry.test.d.ts +1 -0
  309. package/dist/modules/runtime/host.d.ts +38 -0
  310. package/dist/modules/runtime/host.js +68 -0
  311. package/dist/modules/runtime/inside.d.ts +15 -0
  312. package/dist/modules/runtime/inside.js +47 -0
  313. package/dist/modules/runtime/inside.test.d.ts +1 -0
  314. package/dist/modules/runtime/runner.d.ts +11 -0
  315. package/dist/modules/runtime/runner.js +38 -0
  316. package/dist/modules/runtime/runtime.test.d.ts +1 -0
  317. package/dist/modules/runtime/stages.d.ts +50 -0
  318. package/dist/modules/runtime/stages.js +117 -0
  319. package/dist/modules/ssr/Component.d.ts +3 -1
  320. package/dist/modules/ssr/Component.js +2 -1
  321. package/dist/modules/ssr/handler.js +10 -2
  322. package/dist/modules/ssr/loadPluginComponents.js +2 -1
  323. package/dist/modules/ssr/prepareRender.js +39 -10
  324. package/dist/modules/ssr/preview.d.ts +3 -0
  325. package/dist/modules/ssr/preview.js +17 -7
  326. package/dist/modules/ssr/redirect.test.d.ts +1 -0
  327. package/dist/modules/ssr/registerExternalPlugins.d.ts +6 -1
  328. package/dist/modules/ssr/registerExternalPlugins.js +19 -16
  329. package/dist/modules/ssr/registerExternalPlugins.test.d.ts +1 -0
  330. package/dist/modules/ssr/reportMissingPlugins.d.ts +9 -0
  331. package/dist/modules/ssr/reportMissingPlugins.js +24 -0
  332. package/dist/modules/ssr/reportMissingPlugins.test.d.ts +1 -0
  333. package/dist/modules/ssr/streamBody.d.ts +2 -2
  334. package/dist/modules/ssr/streamBody.js +4 -1
  335. package/dist/modules/ssr/template.js +1 -1
  336. package/dist/modules/ssr/views/template.ejs +8 -5
  337. package/dist/mysql.d.ts +1 -1
  338. package/dist/mysql.js +2 -2
  339. package/dist/oauth.js +2 -2
  340. package/dist/plugins/compile.js +9 -4
  341. package/dist/plugins/copy.js +3 -3
  342. package/dist/plugins/manager.d.ts +35 -3
  343. package/dist/plugins/manager.js +109 -38
  344. package/dist/runtime.d.ts +15 -0
  345. package/dist/runtime.js +7 -0
  346. package/package.json +39 -8
  347. package/skills/plitzi-authoring/SKILL.md +98 -14
  348. package/skills/plitzi-authoring/reference/accessibility.md +111 -0
  349. package/skills/plitzi-authoring/reference/authoring-errors.md +70 -0
  350. package/skills/plitzi-authoring/reference/data-and-visibility.md +84 -18
  351. package/skills/plitzi-authoring/reference/elements-and-styles.md +61 -7
  352. package/skills/plitzi-authoring/reference/flows.md +108 -17
  353. package/skills/plitzi-authoring/reference/lists.md +74 -0
  354. package/skills/plitzi-authoring/reference/performance.md +67 -0
  355. package/skills/plitzi-authoring/reference/plugins.md +167 -0
  356. package/skills/plitzi-authoring/reference/review-checklist.md +19 -2
  357. package/skills/plitzi-authoring/reference/templates.md +67 -27
  358. package/skills/plitzi-authoring/reference/testing.md +72 -5
  359. package/skills/plitzi-authoring/reference/validation.md +73 -0
package/README.md CHANGED
@@ -106,9 +106,12 @@ server.listen(3001, '127.0.0.1');
106
106
 
107
107
  ```bash
108
108
  yarn add @plitzi/sdk-server react react-dom
109
- yarn tsx server.ts # http://127.0.0.1:3001/
109
+ node server.ts # http://127.0.0.1:3001/ — Node 22.18+ runs TypeScript itself
110
110
  ```
111
111
 
112
+ No transpiler in front of it: Node strips the types. A loader such as `tsx` costs a page server more memory than the
113
+ server itself — on a 128 MB host, the difference between starting and being killed on boot.
114
+
112
115
  Both pages render server-side, with the CSS the space declared. `authorSpace` refuses to hand back a space that
113
116
  would not render — a CSS property the style editor could not read back, a class nothing declares, a binding
114
117
  pointing at an element that is not there — so a mistake is an error at the line that made it rather than a blank
@@ -158,26 +161,32 @@ serve it — worth running over anything that arrives as a file.
158
161
  |---|---|---|---|
159
162
  | `httpVersion` | `1 \| 2 \| 3` | `2` with `tls`, else `1` | HTTP protocol version. Falls back to the nearest available lower version. |
160
163
  | `tls` | `{ key, cert, minVersion? }` | — | TLS key and certificate. Required for versions 2 and 3; optional for version 1. |
161
- | `devMode` | `boolean` | `NODE_ENV !== 'production'` | Enables development mode: appends `?dev` to esm.sh CDN URLs for React, and activates per-request timing metrics (see [Dev metrics](#dev-metrics)). |
164
+ | `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`. |
162
165
  | `assetVersion` | `string` | — | Cache-buster appended as `?v=<assetVersion>` to all default SDK asset URLs. Compute from file mtime or package version at startup. |
163
166
  | `cacheTtlMs` | `number` | `300000` | TTL in milliseconds for the SSR render cache. Set to `0` to disable. |
164
167
  | `loginPath` | `string \| false` | `'/auth/login'` | Path for the built-in login endpoint. Set to `false` to disable it entirely. |
165
168
  | `logoutPath` | `string \| false` | `'/auth/logout'` | Path for the built-in logout endpoint. Set to `false` to disable it entirely. |
169
+ | `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. |
166
170
  | `templateFn` | `SSRTemplateFn` | built-in EJS template | Custom render function. Receives all template params and returns an HTML string. |
167
171
  | `plugins` | `Record<string, PluginSource>` | — | Named plugin definitions. Compiled or copied on first use and cached for `pluginsTtlMs`. |
168
172
  | `pluginsCacheDir` | `string` | `.sdk-plugins` | Directory where compiled plugin files are stored. |
169
173
  | `pluginsTtlMs` | `number` | `604800000` | TTL in milliseconds for compiled plugins (default: 1 week). |
170
174
  | `autoLoadSchemaPlugins` | `boolean` | `true` | Auto-download and cache plugins declared in the schema's `offlineData.plugins` list. Set to `false` to manage plugin loading manually. |
175
+ | `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. |
171
176
  | `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. |
172
177
  | `static` | `Record<string, string>` | — | URL prefix → filesystem path mappings for static file serving. |
173
178
  | `ssrOnly` | `boolean` | `false` | Omit client-side JS from the rendered page. Useful for verifying SSR HTML without hydration. |
174
179
  | `streaming` | `boolean` | `false` | Stream HTML to the browser incrementally to reduce TTFB. See [Streaming](#streaming). |
175
180
  | `middlewares` | `SSRMiddleware[]` | — | Array of custom middleware functions executed before the SSR renderer on every request (see [Custom middlewares](#custom-middlewares)). |
176
181
  | `rsc` | `SSRRscConfig` | — | React Server Components endpoint configuration (see [RSC](#react-server-components-rsc)). |
182
+ | `realtime` | `{ pubsub?, path?, transport?, allowedOrigins? } \| false` | in memory, at `/_realtime`, `sse` | Realtime channels the space declares. `transport` is what pages connect with: `sse` (a stream, publishes over `POST`) or `websocket` (one socket both ways, HTTP/1.1 only; pages fall back to the stream where a socket cannot open). `allowedOrigins` lists the other origins whose pages may open a socket. `pubsub` is how messages reach other processes — `createMemoryPubSub()` (the default, across workers) or `createRedisPubSub({ publisher, subscriber })` for several replicas. `false` serves no endpoint. See [`docs/en/realtime.md`](../../docs/en/realtime.md). |
177
183
  | `compression` | `SSRCompressionConfig \| false` | Brotli, then gzip | Response compression (see [Compression](#compression)). `false` never compresses. |
178
184
  | `health` | `SSRHealthConfig` | identity payload | The `/health` endpoint. `check` adds live state per probe and turns it into a readiness probe (see [Health](#health)). |
179
185
  | `adapters` | `SSRAdapters` | — | Required. Adapter callbacks for data fetching. |
186
+ | `logLevel` | `'silent' \| 'error' \| 'warn' \| 'info' \| 'debug'` | `info` with `devMode`, else `error` | How much the server says, for the whole process (see [Logging](#logging)). |
187
+ | `logger` | `ServerLogger` | the console | Where it says it: one structured `ServerLogEvent` stream. `consoleLogger` prints each as one line. |
180
188
  | `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. |
189
+ | `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). |
181
190
 
182
191
  ### HTTP version behaviour
183
192
 
@@ -409,7 +418,8 @@ createServer({ compression: false, adapters });
409
418
  |---|---|---|---|
410
419
  | `encodings` | `('br' \| 'gzip')[]` | `['br', 'gzip']` | What this server offers, most preferred first; the first one the client accepts wins. `[]` disables compression. |
411
420
  | `threshold` | `number` | `1024` | Responses smaller than this many bytes go out uncompressed. |
412
- | `brotliQuality` | `number` | `4` | Brotli quality, 0–11. Past 4 the CPU cost outgrows the bytes saved on HTML. |
421
+ | `brotliQuality` | `number` | `2` | Brotli quality, 0–11, for a body compressed on every request (a page rendered for that request alone). Measured on a quarter core, 4 cost a tenth of the pages a second to save half a kilobyte each. |
422
+ | `keptBrotliQuality` | `number` | `6` | Brotli quality for a body compressed once and kept: a cached page, a static file such as the SDK bundle. Paid once; the bundle comes out 10% smaller than at 4, for the same memory. 9 needs ~40 MB more to compress the bundle, which a 128 MB server does not have. |
413
423
  | `gzipLevel` | `number` | `6` | Gzip level, 0–9. |
414
424
 
415
425
  A response that sets `Cache-Control: no-transform` is never compressed, whatever the settings say — that header is
@@ -546,7 +556,7 @@ const getSpaceDeployment = async (req): Promise<SSRSpaceDeployment> => {
546
556
  };
547
557
  ```
548
558
 
549
- Plugins listed in `pluginSources` are registered into the plugin manager on-the-fly using `ensure()`, which only triggers a rebuild if the plugin is new or its `version` has changed. Both `pluginNames` and `pluginSources` entries are resolved in parallel before the HTML is rendered.
559
+ Plugins listed in `pluginSources` — and the external plugins a space's schema lists — are registered into the plugin manager on-the-fly using `ensure()`, under a key that carries the source's identity as well as its name and version (`name@1.0.0+<digest>` of its `js`, `css`, `action` and `props`). A plugin published again under the same version, or another space's plugin with the same name and version, is therefore a source of its own rather than whichever this process met first, and is built only once. Both `pluginNames` and `pluginSources` entries are resolved in parallel before the HTML is rendered.
550
560
 
551
561
  ### Plugin sources
552
562
 
@@ -966,8 +976,8 @@ and every refusal names a machine-readable `reason`, so a client can tell "renew
966
976
  | `identity` | Your own hosts and origins — the floor for domain binding and framing |
967
977
  | `tokens.lifetimes` | How long each credential lives |
968
978
 
969
- Working examples are in [`examples/02-with-users`](../../examples/02-with-users): `01-sessions` over a store you
970
- write, `02-mysql` over one you do not.
979
+ Working examples are in [`examples/self-hosting`](../../examples/self-hosting): `03-sessions` over a store you
980
+ write, `04-mysql` over one you do not.
971
981
 
972
982
  ### Cross-site request forgery
973
983
 
@@ -1134,6 +1144,54 @@ A stage receives the `SSRContext` — the request, the config, and the render si
1134
1144
  **is** the decision to mount them: there is no config flag mirroring it, and a server that never passes them
1135
1145
  never loads them.
1136
1146
 
1147
+ ## Using every core
1148
+
1149
+ Node runs JavaScript on one thread, so one process renders on one core however many the machine has. With
1150
+ `workers`, the server runs one process per core and the connections are spread between them: more requests a second,
1151
+ and less time waiting behind another page's render.
1152
+
1153
+ ```ts
1154
+ createServer({ adapters, workers: 'auto' }); // one per core — the default under NODE_ENV=production
1155
+ createServer({ adapters, workers: 4 }); // at most four, and never more than the cores there are
1156
+ createServer({ adapters, workers: false }); // one process, exactly as before
1157
+ ```
1158
+
1159
+ `SDK_SERVER_WORKERS=auto|false|<n>` sets it from the environment. The cores are the ones the process may use: a
1160
+ container's CPU quota counts, so a pod limited to one CPU runs one process whatever the node has.
1161
+
1162
+ **One process is the server as it always was.** Workers off, one asked for, or one core to run on: nothing is forked,
1163
+ nothing is wrapped, and every in-memory default is this process's own.
1164
+
1165
+ **Several are still one server.** The process you started serves nothing itself; it starts the workers, restarts them
1166
+ and keeps what they must share:
1167
+
1168
+ - The in-memory defaults — an action's `kv`, the job queue, draft previews, the sign-in rate limit — are kept once, by
1169
+ that process, and every worker reaches the same copy. A counter counts every request once; a draft written through
1170
+ one worker is read through another. A store you supply (Redis, a table) is used as it is.
1171
+ - The scheduler and the job consumers run in one worker only: a schedule fires once, and `jobs.workers` is the whole
1172
+ server's concurrency.
1173
+ - `server.cache.invalidate()`, `server.plugins.register()` and `server.plugins.invalidate()` reach every worker,
1174
+ whichever process calls them — a publish webhook lands on one of them.
1175
+ - Plugins are built once, before the workers start.
1176
+
1177
+ Per worker: the rendered-page cache (each renders a page once), the plugin components, and the action caps
1178
+ (`action.concurrency`). A plugin registered with a `component` stays in the process that registered it — register
1179
+ those where the server is created.
1180
+
1181
+ **When a worker dies** — a crash, an uncaught error, the OOM killer — it is replaced, and its replacement takes over the
1182
+ jobs it carried (a claimed job goes back to the queue when its lease lapses). Past one death per worker a minute the
1183
+ replacements wait, doubling from half a second up to thirty, so a request that crashes every worker it reaches cannot
1184
+ turn the server into a fork loop. A replacement that fails to start is retried the same way while the rest keep
1185
+ serving. Two exceptions: a worker that dies before any has served stops the server with a non-zero exit (a port in use
1186
+ would fail the same way every time), and a server that is killed outright takes its workers with it.
1187
+
1188
+ `closeOnSignals(server)` stops them all on SIGTERM, each finishing what it is serving. The workers of one machine are
1189
+ one replica; several machines are several replicas, and what must hold across those still needs a shared store — see
1190
+ below.
1191
+
1192
+ A process that starts servers of its own beside this one (an Express app on another port) should say `workers:
1193
+ false`: each worker runs the whole entry file again.
1194
+
1137
1195
  ## Scheduled jobs across replicas
1138
1196
 
1139
1197
  Server actions with a `schedule` trigger, and jobs queued for later, run with no configuration — over an in-process
@@ -1156,6 +1214,12 @@ server.listen(3000);
1156
1214
  closeOnSignals(server, { afterClose: () => mongoClient.close() });
1157
1215
  ```
1158
1216
 
1217
+ `close()` does not wait for what never ends on its own: it stops taking connections, tells every realtime socket and
1218
+ stream to go (a WebSocket is closed with 1001, "going away", which the pages reconnect from once the server is back),
1219
+ ends any other event stream, lets what is being answered finish, and cuts whatever is still open after ten seconds
1220
+ (`SHUTDOWN_GRACE_MS`). A page left open on a board, or an agent's app listening, no longer holds a Ctrl+C or a
1221
+ SIGTERM until something kills the process.
1222
+
1159
1223
  `@plitzi/sdk-server/mysql` has the same pair over a `mysql2` pool (`createMysqlJobQueue`, `createMysqlKv`). Anything
1160
1224
  else is the `ActionJobQueue` contract written against your store — see
1161
1225
  [server actions § self-hosted](https://github.com/plitzi/plitzi-workspace/blob/main/docs/en/server-actions.md#13-for-a-self-hosted-deployment).
@@ -1291,12 +1355,41 @@ How it works:
1291
1355
 
1292
1356
  **Compression**: streaming responses use chunked transfer encoding and skip Brotli/gzip compression. A `Content-Length` header cannot be set before the body is complete, so compression is intentionally bypassed for streaming responses.
1293
1357
 
1358
+ ## Logging
1359
+
1360
+ One stream and one threshold, the usual hierarchy: `silent` < `error` < `warn` < `info` < `debug`. In production the
1361
+ server says only what went wrong — a failed request, a failed run, a server that could not start — and everything
1362
+ below it is one `logLevel` away:
1363
+
1364
+ | Level | What it adds |
1365
+ |---|---|
1366
+ | `error` | Requests answered 5xx or that threw, runs that did not complete, anything the server could not do. |
1367
+ | `warn` | Refused actions (a bad webhook signature, a missing session), plugin manifests it could not fetch, HTTP/3 falling back. |
1368
+ | `info` | Every request line, every run, plugin builds, `listening on`. |
1369
+ | `debug` | The per-render phase breakdown of [Dev metrics](#dev-metrics). |
1370
+
1371
+ ```ts
1372
+ createServer({ adapters, logLevel: process.env.LOG_LEVEL === 'info' ? 'info' : 'error', logger: consoleLogger });
1373
+ ```
1374
+
1375
+ A request below the threshold is not even turned into an event, so a quiet server pays nothing per request. Everything goes to the `logger` —
1376
+ requests, MCP tool calls, runs, and the server's own messages as `kind: 'message'` events — and `logLevelOf(event)`
1377
+ tells a sink how severe each one is. The run and refusal loggers a deployment wires itself (`createRunLogger`,
1378
+ `createRejectLogger`) are held to the same threshold, and so is `serverLog`, the server's own voice, which a
1379
+ deployment's adapters can speak through too:
1380
+
1381
+ ```ts
1382
+ import { serverLog } from '@plitzi/sdk-server';
1383
+
1384
+ serverLog.warn('Adapters', 'space 12 has no deployment, serving main');
1385
+ ```
1386
+
1294
1387
  ## Dev metrics
1295
1388
 
1296
1389
  When `devMode: true`, per-phase timing is instrumented on every render and reported in two ways:
1297
1390
 
1298
1391
  - A `Server-Timing` header is set on the response, visible in the browser's DevTools under **Network → Timing**.
1299
- - A one-line summary is logged to stdout:
1392
+ - A one-line summary is logged at `debug` (see [Logging](#logging)):
1300
1393
 
1301
1394
  ```
1302
1395
  [SSR] GET / — schema=1ms rsc=0ms extPlugins=0ms plugins=0ms template=2ms react=16ms | total=19ms
package/dist/actions.d.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  * from somewhere else — a queue consumer, or the API role answering a builder's test run. Same runner, same
16
16
  * checks: a trigger that skipped them would be a weaker path to the same work.
17
17
  */
18
- export { createActionsModule } from './modules/actions';
18
+ export { createActionsModule, spaceKvPatterns } from './modules/actions';
19
19
  /**
20
20
  * Scheduled runs: a durable queue, a leaderless producer and the workers that drain it.
21
21
  *
@@ -39,7 +39,7 @@ export { cronFiresBetween, cronMatches, cronNextFire, parseCron } from '@plitzi/
39
39
  */
40
40
  export { checkAction } from './modules/actions/runtime/check';
41
41
  export type { ActionCheckDeps } from './modules/actions/runtime/check';
42
- export { ActionRunError } from './modules/actions/runtime/errors';
42
+ export { ActionRefusal, ActionRunError } from './modules/actions/runtime/errors';
43
43
  export { DEFAULT_LIMITS } from './modules/actions/runtime/limits';
44
44
  export { createTaskRegistry, taskName } from './modules/actions/tasks/registry';
45
45
  export { describeCatalog, describeTask } from './modules/actions/taskCatalog';
@@ -54,6 +54,10 @@ export { describeCatalog, describeTask } from './modules/actions/taskCatalog';
54
54
  */
55
55
  export { createMemoryKv } from './modules/actions/runtime/memoryKv';
56
56
  export { createKvStore } from './modules/actions/runtime/kvStore';
57
+ export { createRedisKv } from './modules/actions/runtime/redisKv';
58
+ export type { RedisKvClient, RedisKvOptions } from './modules/actions/runtime/redisKv';
59
+ export { MAX_LIST_BYTES, MAX_LIST_ENTRIES } from './modules/actions/runtime/kvList';
60
+ export type { KvListEntry, KvListPut, KvListPutOptions, KvListRange } from './modules/actions/runtime/kvList';
57
61
  /**
58
62
  * `onRun` and `onReject` for a deployment that wants to SEE its flows without building somewhere to keep them: a
59
63
  * run — and a request that never became one — becomes one more event on the log stream the server already
package/dist/actions.js CHANGED
@@ -1,16 +1,19 @@
1
1
  import { createMemoryJobQueue } from "./modules/actions/jobs/memoryQueue.js";
2
2
  import { DEFAULT_MAX_ATTEMPTS, scheduleFor, scheduleJobId, schedulesFor } from "./modules/actions/jobs/schedules.js";
3
3
  import { createScheduler } from "./modules/actions/jobs/scheduler.js";
4
- import { ActionRunError } from "./modules/actions/runtime/errors.js";
4
+ import { ActionRefusal, ActionRunError } from "./modules/actions/runtime/errors.js";
5
5
  import { createJobWorker } from "./modules/actions/jobs/worker.js";
6
6
  import { createActionJobs } from "./modules/actions/jobs/index.js";
7
+ import { MAX_LIST_BYTES, MAX_LIST_ENTRIES } from "./modules/actions/runtime/kvList.js";
7
8
  import { createKvStore } from "./modules/actions/runtime/kvStore.js";
8
9
  import { DEFAULT_LIMITS } from "./modules/actions/runtime/limits.js";
9
10
  import { createMemoryKv } from "./modules/actions/runtime/memoryKv.js";
11
+ import { spaceKvPatterns } from "./modules/actions/runtime/namespaceKv.js";
10
12
  import { createTaskRegistry, taskName } from "./modules/actions/tasks/registry.js";
11
13
  import { checkAction } from "./modules/actions/runtime/check.js";
12
14
  import { createRejectLogger, createRunLogger } from "./modules/actions/runtime/runLogger.js";
13
15
  import { describeCatalog, describeTask } from "./modules/actions/taskCatalog.js";
14
16
  import { createActionsModule } from "./modules/actions/index.js";
17
+ import { createRedisKv } from "./modules/actions/runtime/redisKv.js";
15
18
  import { cronFiresBetween, cronMatches, cronNextFire, parseCron } from "@plitzi/sdk-shared/actions";
16
- export { ActionRunError, DEFAULT_LIMITS, DEFAULT_MAX_ATTEMPTS, checkAction, createActionJobs, createActionsModule, createJobWorker, createKvStore, createMemoryJobQueue, createMemoryKv, createRejectLogger, createRunLogger, createScheduler, createTaskRegistry, cronFiresBetween, cronMatches, cronNextFire, describeCatalog, describeTask, parseCron, scheduleFor, scheduleJobId, schedulesFor, taskName };
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 };
@@ -1,3 +1,4 @@
1
+ import { serverLog } from "../helpers/serverLog.js";
1
2
  //#region src/adapters/cloudAdapters.ts
2
3
  /**
3
4
  * A space fetched from Plitzi, for a server that is not Plitzi's.
@@ -150,17 +151,17 @@ var createCloudAdapters = (config) => {
150
151
  })
151
152
  });
152
153
  if (!response.ok) {
153
- console.error(`[CloudAdapters] ${what} answered ${response.status}`);
154
+ serverLog.error("CloudAdapters", `${what} answered ${response.status}`);
154
155
  return;
155
156
  }
156
157
  const payload = await response.json();
157
158
  if (payload.errors?.length) {
158
- console.error(`[CloudAdapters] ${what}: ${payload.errors.map((error) => error.message).join("; ")}`);
159
+ serverLog.error("CloudAdapters", `${what}: ${payload.errors.map((error) => error.message).join("; ")}`);
159
160
  return;
160
161
  }
161
162
  return payload.data;
162
163
  } catch (error) {
163
- console.error(`[CloudAdapters] ${what} could not be reached:`, error.message);
164
+ serverLog.error("CloudAdapters", `${what} could not be reached`, error);
164
165
  return;
165
166
  }
166
167
  };
@@ -1,8 +1,29 @@
1
- import { readFileSync, writeFileSync } from "node:fs";
1
+ import { serverLog } from "../helpers/serverLog.js";
2
+ import { readFileSync, statSync, writeFileSync } from "node:fs";
2
3
  //#region src/adapters/jsonAdapters.ts
3
4
  var isDeploymentObject = (v) => typeof v === "object" && ("spaceId" in v || "environment" in v || "error" in v);
4
- var readJson = (filePath) => JSON.parse(readFileSync(filePath, "utf-8"));
5
+ var createJsonReader = () => {
6
+ const held = /* @__PURE__ */ new Map();
7
+ return {
8
+ read: (filePath) => {
9
+ const { mtimeMs, size } = statSync(filePath);
10
+ const hit = held.get(filePath);
11
+ if (hit?.mtimeMs === mtimeMs && hit.size === size) return hit.value;
12
+ const value = JSON.parse(readFileSync(filePath, "utf-8"));
13
+ held.set(filePath, {
14
+ mtimeMs,
15
+ size,
16
+ value
17
+ });
18
+ return value;
19
+ },
20
+ forget: (filePath) => {
21
+ held.delete(filePath);
22
+ }
23
+ };
24
+ };
5
25
  var createJsonAdapters = (config) => {
26
+ const json = createJsonReader();
6
27
  const pathFor = (spaceId, environment, revision) => {
7
28
  if (typeof config.offlineData === "function") return config.offlineData(spaceId, environment, revision);
8
29
  return typeof config.offlineData === "string" ? config.offlineData : void 0;
@@ -11,9 +32,9 @@ var createJsonAdapters = (config) => {
11
32
  try {
12
33
  const filePath = pathFor(spaceId, environment, revision);
13
34
  if (!filePath) return Promise.resolve(config.offlineData);
14
- return Promise.resolve(readJson(filePath));
35
+ return Promise.resolve(json.read(filePath));
15
36
  } catch (err) {
16
- console.error("[JsonAdapters] Failed to read offlineData:", err.message);
37
+ serverLog.error("JsonAdapters", "Failed to read offlineData", err);
17
38
  return Promise.resolve(void 0);
18
39
  }
19
40
  };
@@ -21,6 +42,7 @@ var createJsonAdapters = (config) => {
21
42
  const filePath = pathFor(spaceId, environment);
22
43
  if (!filePath) return Promise.resolve();
23
44
  writeFileSync(filePath, JSON.stringify(data, null, 2));
45
+ json.forget(filePath);
24
46
  return Promise.resolve();
25
47
  };
26
48
  const getSpaceDeployment = (req) => {
@@ -31,9 +53,9 @@ var createJsonAdapters = (config) => {
31
53
  revision: 0
32
54
  });
33
55
  if (typeof deployment === "string") try {
34
- return Promise.resolve(readJson(deployment));
56
+ return Promise.resolve(json.read(deployment));
35
57
  } catch (err) {
36
- console.error("[JsonAdapters] Failed to read deployment file:", err.message);
58
+ serverLog.error("JsonAdapters", "Failed to read deployment file", err);
37
59
  return Promise.resolve({ error: {
38
60
  code: 500,
39
61
  message: "Deployment config unreadable"
@@ -0,0 +1 @@
1
+ export {};
package/dist/auth.d.ts CHANGED
@@ -23,6 +23,7 @@
23
23
  export { createAuth } from './core/auth/createAuth';
24
24
  export { createAuthApi } from './core/auth/api';
25
25
  export { createCsrf, csrfFailureMessage } from './core/auth/csrf';
26
+ export { SESSION_ACTIVITY_RESOLUTION_SECONDS, activityDue } from './core/auth/sessionActivity';
26
27
  export { createAuthorizer, checkPermission, checkSpaceAccess, requirementFor } from './core/auth/authorize';
27
28
  export { createCarriers, presentedOrigin } from './core/auth/credentials';
28
29
  export { ANY_DOMAIN, corsOrigins, domainAllowed, frameAncestors, hostnameOf, normalizeDomain } from './core/auth/domains';
@@ -30,21 +31,23 @@ export { createIdentity } from './core/auth/identity';
30
31
  export { generateToken, hashPassword, verifyPassword } from './core/auth/passwords';
31
32
  export { BUILT_IN_PROVIDERS, OAuthFailure, createSocialAuth, requestProfileJson } from './core/auth/oauth';
32
33
  export { createRedirectPolicy } from './core/auth/redirects';
34
+ export { isDocumentNavigation, renewForNavigation } from './core/auth/renewal';
35
+ export type { RenewalOptions } from './core/auth/renewal';
33
36
  export type { RedirectPolicyConfig } from './core/auth/redirects';
34
37
  export { applySessionOutcome, authPolicyRules, authRoutes } from './core/auth/routes';
35
- export { appendCookies, clearFlowCookie, clearSessionCookies, createSessionCookies, isLocalHost, readFlowCookie, readRefreshToken, readSessionToken, sessionCookieParams, sessionHintValue, writeFlowCookie, writeSessionCookies } from './core/auth/session';
38
+ export { appendCookies, clearFlowCookie, clearSessionCookies, createSessionCookies, isLocalHost, parseSessionHint, readFlowCookie, readRefreshToken, readSessionHint, readSessionToken, sessionCookieParams, sessionHintValue, sessionReturnTarget, writeFlowCookie, writeSessionCookies } from './core/auth/session';
36
39
  export { createMemoryRateLimit } from './core/auth/throttle';
37
40
  export { createSpaceTokenApi } from './core/auth/spaceTokens';
38
41
  export { SCOPES, authFailureMessage, createTokens, userIdOf } from './core/auth/tokens';
39
- export { generateRecoveryCodes, generateTotpSecret, normalizeRecoveryCode, randomCode, totpCode, totpUri, verifyTotp } from './core/auth/totp';
42
+ export { generateRecoveryCodes, generateTotpSecret, normalizeRecoveryCode, randomCode, totpCode, totpStep, totpUri, verifyTotp } from './core/auth/totp';
40
43
  export type { Auth, AuthConfig } from './core/auth/createAuth';
41
- export type { AccountAccess, AccountAdapters, AccountQuery, AccountRecord, AccountStatus, AuthApi, AuthApiConfig, AuthOutcome, ExchangeResult, MfaRecord, PasswordPolicy, SecurityEvent, SessionClient, SessionContext, SessionSummary, ThrottleAttempt, ThrottledAction } from './core/auth/api';
44
+ export type { AccountAccess, AccountAdapters, AccountQuery, AccountRecord, AccountStatus, AuthApi, AuthApiConfig, AuthOutcome, ExchangeResult, MfaRecord, PasswordPolicy, SecurityEvent, SessionApp, SessionClient, SessionContext, SessionSummary, ThrottleAttempt, ThrottledAction } from './core/auth/api';
42
45
  export type { AuthPolicy, AuthorizeResult, Authorizer, MembershipFacts, PathMatcher, PermissionCheck, Requirement, SpaceAccessCheck } from './core/auth/authorize';
43
46
  export type { CredentialCarrier } from './core/auth/credentials';
44
47
  export type { Csrf, CsrfCarrier, CsrfConfig, CsrfFailure, CsrfResult, CsrfSubject } from './core/auth/csrf';
45
48
  export type { Actor, ActorResult, Grant, GrantOptions, GrantResult, Identity, IdentityAdapters, IdentityConfig, SpaceMembership, StoredSpaceToken } from './core/auth/identity';
46
49
  export type { CompletedFlow, OAuthFailureReason, OAuthProfile, OAuthProvider, OAuthProviderConfig, SocialAuth, SocialAuthAdapters, SocialAuthConfig, StartedFlow } from './core/auth/oauth';
47
50
  export type { AuthRequest, AuthRoute } from './core/auth/routes';
48
- export type { CookieCarrier, CookieSink, SessionCookieParams, SessionCookies } from './core/auth/session';
51
+ export type { CookieCarrier, CookieSink, SessionCookieParams, SessionCookies, SessionHint } from './core/auth/session';
49
52
  export type { SpaceTokenAdapters, SpaceTokenApi, SpaceTokenContext, SpaceTokenOutcome, SpaceTokenRecord, SpaceTokenSummary } from './core/auth/spaceTokens';
50
53
  export type { AuthFailure, MfaChallengePayload, RefreshTokenPayload, SpaceScope, SpaceTokenOptions, SpaceTokenPayload, TokenConfig, TokenScope, Tokens, UserTokenOptions, UserTokenPayload, VerifyResult, WidgetTokenPayload } from './core/auth/tokens';
package/dist/auth.js CHANGED
@@ -1,19 +1,21 @@
1
- import { appendCookies, clearFlowCookie, clearSessionCookies, createSessionCookies, isLocalHost, readFlowCookie, readRefreshToken, readSessionToken, sessionCookieParams, sessionHintValue, writeFlowCookie, writeSessionCookies } from "./core/auth/session.js";
1
+ import { appendCookies, clearFlowCookie, clearSessionCookies, createSessionCookies, isLocalHost, parseSessionHint, readFlowCookie, readRefreshToken, readSessionHint, readSessionToken, sessionCookieParams, sessionHintValue, sessionReturnTarget, writeFlowCookie, writeSessionCookies } from "./core/auth/session.js";
2
+ import { createCsrf, csrfFailureMessage } from "./core/auth/csrf.js";
3
+ import { applySessionOutcome, authPolicyRules, authRoutes } from "./core/auth/routes.js";
4
+ import { isDocumentNavigation, renewForNavigation } from "./core/auth/renewal.js";
5
+ import { createCarriers, presentedOrigin } from "./core/auth/credentials.js";
2
6
  import { ANY_DOMAIN, corsOrigins, domainAllowed, frameAncestors, hostnameOf, normalizeDomain } from "./core/auth/domains.js";
3
7
  import { checkPermission, checkSpaceAccess, createAuthorizer, requirementFor } from "./core/auth/authorize.js";
4
8
  import { generateToken, hashPassword, verifyPassword } from "./core/auth/passwords.js";
5
9
  import { createMemoryRateLimit } from "./core/auth/throttle.js";
6
10
  import { SCOPES, authFailureMessage, createTokens, userIdOf } from "./core/auth/tokens.js";
7
- import { generateRecoveryCodes, generateTotpSecret, normalizeRecoveryCode, randomCode, totpCode, totpUri, verifyTotp } from "./core/auth/totp.js";
11
+ import { generateRecoveryCodes, generateTotpSecret, normalizeRecoveryCode, randomCode, totpCode, totpStep, totpUri, verifyTotp } from "./core/auth/totp.js";
8
12
  import { createAuthApi } from "./core/auth/api.js";
9
- import { createCarriers, presentedOrigin } from "./core/auth/credentials.js";
10
- import { createCsrf, csrfFailureMessage } from "./core/auth/csrf.js";
11
13
  import { createIdentity } from "./core/auth/identity.js";
12
- import { applySessionOutcome, authPolicyRules, authRoutes } from "./core/auth/routes.js";
13
14
  import { createAuth } from "./core/auth/createAuth.js";
15
+ import { SESSION_ACTIVITY_RESOLUTION_SECONDS, activityDue } from "./core/auth/sessionActivity.js";
14
16
  import { OAuthFailure } from "./core/auth/oauth/types.js";
15
17
  import { requestProfileJson } from "./core/auth/oauth/client.js";
16
18
  import { createRedirectPolicy } from "./core/auth/redirects.js";
17
19
  import { BUILT_IN_PROVIDERS, createSocialAuth } from "./core/auth/oauth/index.js";
18
20
  import { createSpaceTokenApi } from "./core/auth/spaceTokens.js";
19
- export { ANY_DOMAIN, BUILT_IN_PROVIDERS, OAuthFailure, SCOPES, appendCookies, applySessionOutcome, authFailureMessage, authPolicyRules, authRoutes, checkPermission, checkSpaceAccess, clearFlowCookie, clearSessionCookies, corsOrigins, createAuth, createAuthApi, createAuthorizer, createCarriers, createCsrf, createIdentity, createMemoryRateLimit, createRedirectPolicy, createSessionCookies, createSocialAuth, createSpaceTokenApi, createTokens, csrfFailureMessage, domainAllowed, frameAncestors, generateRecoveryCodes, generateToken, generateTotpSecret, hashPassword, hostnameOf, isLocalHost, normalizeDomain, normalizeRecoveryCode, presentedOrigin, randomCode, readFlowCookie, readRefreshToken, readSessionToken, requestProfileJson, requirementFor, sessionCookieParams, sessionHintValue, totpCode, totpUri, userIdOf, verifyPassword, verifyTotp, writeFlowCookie, writeSessionCookies };
21
+ export { ANY_DOMAIN, BUILT_IN_PROVIDERS, OAuthFailure, SCOPES, SESSION_ACTIVITY_RESOLUTION_SECONDS, activityDue, appendCookies, applySessionOutcome, authFailureMessage, authPolicyRules, authRoutes, checkPermission, checkSpaceAccess, clearFlowCookie, clearSessionCookies, corsOrigins, createAuth, createAuthApi, createAuthorizer, createCarriers, createCsrf, createIdentity, createMemoryRateLimit, createRedirectPolicy, createSessionCookies, createSocialAuth, createSpaceTokenApi, createTokens, csrfFailureMessage, domainAllowed, frameAncestors, generateRecoveryCodes, generateToken, generateTotpSecret, hashPassword, hostnameOf, isDocumentNavigation, isLocalHost, normalizeDomain, normalizeRecoveryCode, parseSessionHint, presentedOrigin, randomCode, readFlowCookie, readRefreshToken, readSessionHint, readSessionToken, renewForNavigation, requestProfileJson, requirementFor, sessionCookieParams, sessionHintValue, sessionReturnTarget, totpCode, totpStep, totpUri, userIdOf, verifyPassword, verifyTotp, writeFlowCookie, writeSessionCookies };
@@ -36,6 +36,16 @@ export interface AccountAccess {
36
36
  export interface SessionClient {
37
37
  userAgent?: string;
38
38
  ip?: string;
39
+ /**
40
+ * The application holding it, when it is not a browser: a native client names itself when it registers
41
+ * (`Plitzi CLI on carlos-mbp`, software `plitzi-cli`). A user agent says which browser; this says which app.
42
+ */
43
+ app?: SessionApp;
44
+ }
45
+ export interface SessionApp {
46
+ name: string;
47
+ /** A stable name for the software, the same on every machine — what a list picks an icon by. */
48
+ softwareId?: string;
39
49
  }
40
50
  /**
41
51
  * What an account is allowed to be. `inactive` is the account's own doing (deactivated, never confirmed);
@@ -47,7 +57,10 @@ export interface SessionSummary {
47
57
  id: number;
48
58
  userAgent?: string;
49
59
  ip?: string;
60
+ app?: SessionApp;
50
61
  createdAt: number;
62
+ /** When it was last used, where the store keeps it; a session never seen since it was made has none. */
63
+ lastActiveAt?: number;
51
64
  expiresAt: number;
52
65
  /** The session asking. A device list without it invites someone to revoke the one they are using. */
53
66
  current: boolean;
@@ -58,6 +71,12 @@ export interface MfaRecord {
58
71
  /** Unix seconds the enrolment was proven with a real code. Absent means started and never finished. */
59
72
  confirmedAt?: number;
60
73
  recoveryCodes?: string[];
74
+ /**
75
+ * The time step of the last code that was accepted. A code of that step or an earlier one is refused: a code is valid
76
+ * for its whole window, and one watched over a shoulder or lifted from a phishing page would otherwise sign a second
77
+ * person in within that window (RFC 6238 §5.2).
78
+ */
79
+ lastUsedStep?: number;
61
80
  }
62
81
  /** Something worth writing down. Fed to an audit log, a webhook, a SIEM — whatever the deployment has. */
63
82
  export interface SecurityEvent {
@@ -138,6 +157,15 @@ export interface AccountAdapters {
138
157
  * session, which deleting the account's sessions does.
139
158
  */
140
159
  deleteAccount?: (userId: number) => Promise<void>;
160
+ /**
161
+ * What would be lost if this account were deleted now, in sentences its owner can act on — "“Studio” has 3 spaces;
162
+ * delete them or make somebody else an owner". Any at all and the deletion is refused, with the list.
163
+ *
164
+ * The account is what holds things a deployment cannot simply drop: a workspace nobody else owns, the spaces in it,
165
+ * a plan still being paid for. Deleting it anyway leaves them orphaned — unreachable by anyone, and possibly still
166
+ * billed. Optional, for a deployment whose accounts own nothing.
167
+ */
168
+ deletionBlockers?: (userId: number) => Promise<string[]>;
141
169
  /** Page through accounts, for an administrator. `total` is the count before the page was taken. */
142
170
  listAccounts?: (query: AccountQuery) => Promise<{
143
171
  accounts: AccountRecord[];
@@ -298,7 +326,7 @@ export interface AuthApiConfig {
298
326
  allowed: boolean;
299
327
  retryAfter?: number;
300
328
  }>;
301
- /** Where a failed delivery is reported. Defaults to `console.error`; it is never thrown — see `deliver`. */
329
+ /** Where a failed delivery is reported. Defaults to the server log at `error`; it is never thrown — see `deliver`. */
302
330
  onMailError?: (error: unknown, message: {
303
331
  to: string;
304
332
  template: string;
@@ -1,14 +1,26 @@
1
+ import { serverLog } from "../../helpers/serverLog.js";
2
+ import { forwardedIp } from "../requestParser.js";
1
3
  import { checkPermission } from "./authorize.js";
2
4
  import { generateToken, hashPassword, verifyPassword } from "./passwords.js";
3
- import { createMemoryRateLimit } from "./throttle.js";
5
+ import { createMemoryRateLimit, fleetRateLimit } from "./throttle.js";
4
6
  import { authFailureMessage } from "./tokens.js";
5
- import { generateRecoveryCodes, generateTotpSecret, normalizeRecoveryCode, randomCode, totpUri, verifyTotp } from "./totp.js";
7
+ import { generateRecoveryCodes, generateTotpSecret, normalizeRecoveryCode, randomCode, totpStep, totpUri } from "./totp.js";
6
8
  import { createHash } from "node:crypto";
7
9
  //#region src/core/auth/api.ts
8
- /** The user agent, off whatever carried the request. Nothing else here reads headers, so it is done once. */
10
+ /**
11
+ * The user agent and the address, off whatever carried the request — what lets somebody tell their devices apart.
12
+ * Nothing else here reads headers, so it is done once. The address is the host's when it resolved one, the proxies'
13
+ * otherwise; it names a session in a list and decides nothing.
14
+ */
9
15
  var clientOf = (carrier) => {
10
- const userAgent = carrier?.headers["user-agent"];
11
- return typeof userAgent === "string" && userAgent ? { userAgent } : void 0;
16
+ if (!carrier) return;
17
+ const userAgent = carrier.headers["user-agent"];
18
+ const ip = carrier.ip || forwardedIp(carrier.headers);
19
+ const client = {
20
+ ...typeof userAgent === "string" && userAgent ? { userAgent } : {},
21
+ ...ip ? { ip } : {}
22
+ };
23
+ return client.userAgent || client.ip ? client : void 0;
12
24
  };
13
25
  var refuse = (status, error, reason) => ({
14
26
  ok: false,
@@ -88,7 +100,7 @@ var profileOf = (account) => ({
88
100
  * form without either owning it.
89
101
  */
90
102
  var createAuthApi = ({ tokens, identity, adapters, config = {} }) => {
91
- const { verifyPassword: verifyPassword$1 = verifyPassword, hashPassword: hashPassword$1 = hashPassword, generateToken: generateToken$1 = generateToken, verifyOnSignup = false, adminPermission = "userManage", impersonationPermission, password: policy = {}, rateLimit = createMemoryRateLimit(), onMailError, onEvent, mfaIssuer, csrf } = config;
103
+ const { verifyPassword: verifyPassword$1 = verifyPassword, hashPassword: hashPassword$1 = hashPassword, generateToken: generateToken$1 = generateToken, verifyOnSignup = false, adminPermission = "userManage", impersonationPermission, password: policy = {}, rateLimit = fleetRateLimit() ?? createMemoryRateLimit(), onMailError, onEvent, mfaIssuer, csrf } = config;
92
104
  /** Never awaited, never able to throw: a logging outage must not become an authentication outage. */
93
105
  const record = (event) => {
94
106
  if (!onEvent) return;
@@ -98,7 +110,7 @@ var createAuthApi = ({ tokens, identity, adapters, config = {} }) => {
98
110
  at: Math.floor(Date.now() / 1e3)
99
111
  });
100
112
  } catch (error) {
101
- console.error("[auth] security event handler threw:", error);
113
+ serverLog.error("auth", "security event handler threw", error);
102
114
  }
103
115
  };
104
116
  /**
@@ -132,7 +144,7 @@ var createAuthApi = ({ tokens, identity, adapters, config = {} }) => {
132
144
  try {
133
145
  await adapters.sendMail?.(message);
134
146
  } catch (error) {
135
- (onMailError ?? ((cause) => console.error("[auth] could not send mail:", cause)))(error, {
147
+ (onMailError ?? ((cause) => serverLog.error("auth", "could not send mail", cause)))(error, {
136
148
  to: message.to,
137
149
  template: message.template
138
150
  });
@@ -386,6 +398,11 @@ var createAuthApi = ({ tokens, identity, adapters, config = {} }) => {
386
398
  return refuse(401, authFailureMessage[reason], reason);
387
399
  }
388
400
  if ((capabilities.mfa ? await adapters.loadMfa?.(account.id) : void 0)?.confirmedAt) {
401
+ throttleSucceeded({
402
+ action: "login",
403
+ key: username,
404
+ carrier
405
+ });
389
406
  record({
390
407
  type: "login.mfa-required",
391
408
  userId: account.id,
@@ -444,17 +461,23 @@ var createAuthApi = ({ tokens, identity, adapters, config = {} }) => {
444
461
  const stored = mfa.recoveryCodes ?? [];
445
462
  const supplied = digestRecoveryCode(code);
446
463
  const usedRecovery = stored.includes(supplied);
447
- if (!usedRecovery && !verifyTotp(mfa.secret, code)) {
464
+ const step = usedRecovery ? void 0 : totpStep(mfa.secret, code);
465
+ const replayed = step !== void 0 && mfa.lastUsedStep !== void 0 && step <= mfa.lastUsedStep;
466
+ if (!usedRecovery && (step === void 0 || replayed)) {
448
467
  record({
449
468
  type: "mfa.failed",
450
469
  userId,
451
- carrier
470
+ carrier,
471
+ ...replayed ? { detail: { replayed: true } } : {}
452
472
  });
453
473
  return refuse(401, "Invalid code");
454
474
  }
455
- if (usedRecovery) await adapters.saveMfa?.(userId, {
475
+ await adapters.saveMfa?.(userId, usedRecovery ? {
456
476
  ...mfa,
457
477
  recoveryCodes: stored.filter((entry) => entry !== supplied)
478
+ } : {
479
+ ...mfa,
480
+ lastUsedStep: step
458
481
  });
459
482
  const session = await issue(userId, { client: clientOf(carrier) });
460
483
  throttleSucceeded({
@@ -631,7 +654,8 @@ var createAuthApi = ({ tokens, identity, adapters, config = {} }) => {
631
654
  const mfa = await adapters.loadMfa?.(actor.id);
632
655
  if (!mfa) return refuse(400, "Start the enrolment first");
633
656
  if (mfa.confirmedAt) return refuse(409, "A second factor is already set up");
634
- if (!verifyTotp(mfa.secret, code)) {
657
+ const step = totpStep(mfa.secret, code);
658
+ if (step === void 0) {
635
659
  record({
636
660
  type: "mfa.failed",
637
661
  userId: actor.id
@@ -642,7 +666,8 @@ var createAuthApi = ({ tokens, identity, adapters, config = {} }) => {
642
666
  await adapters.saveMfa?.(actor.id, {
643
667
  ...mfa,
644
668
  confirmedAt: Math.floor(Date.now() / 1e3),
645
- recoveryCodes: plain.map(digestRecoveryCode)
669
+ recoveryCodes: plain.map(digestRecoveryCode),
670
+ lastUsedStep: step
646
671
  });
647
672
  record({
648
673
  type: "mfa.enabled",
@@ -1070,6 +1095,15 @@ var createAuthApi = ({ tokens, identity, adapters, config = {} }) => {
1070
1095
  if (account?.passwordHash) {
1071
1096
  if (!password || !await verifyPassword$1(password, account.passwordHash)) return refuse(401, "Invalid credentials");
1072
1097
  }
1098
+ const blockers = await adapters.deletionBlockers?.(actor.id) ?? [];
1099
+ if (blockers.length > 0) return {
1100
+ ok: false,
1101
+ status: 409,
1102
+ body: {
1103
+ error: "This account still owns things that would be lost with it",
1104
+ blockers
1105
+ }
1106
+ };
1073
1107
  await adapters.deleteAccount(actor.id);
1074
1108
  record({
1075
1109
  type: "account.deleted",
@@ -177,6 +177,9 @@ export declare const createAuth: (config: AuthConfig) => {
177
177
  hostname: string;
178
178
  }, res: CookieSink) => void;
179
179
  readFlow: (req: import('./session').CookieCarrier) => string | undefined;
180
+ returnTarget: (req: {
181
+ hostname: string;
182
+ }, target: unknown) => string;
180
183
  resolveSessionToken: (req: import('./session').CookieCarrier) => string | undefined;
181
184
  resolveRefreshToken: (req: import('./session').CookieCarrier, body?: unknown) => string | undefined;
182
185
  };
@@ -202,6 +205,7 @@ export declare const createAuth: (config: AuthConfig) => {
202
205
  }, res: CookieSink) => void;
203
206
  read: (carrier: import('./csrf').CsrfCarrier) => string | undefined;
204
207
  required: (carrier: import('./csrf').CsrfCarrier, subject?: import('./csrf').CsrfSubject) => boolean;
208
+ crossSite: (carrier: import('./csrf').CsrfCarrier, alsoAllowed?: readonly string[]) => boolean;
205
209
  verify: (carrier: import('./csrf').CsrfCarrier, sessionToken?: string) => import('./csrf').CsrfResult;
206
210
  } | undefined;
207
211
  /** The `/auth` flows as descriptors, ready for whatever serves HTTP here. */