@plitzi/sdk-server 0.33.2 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +283 -8
  3. package/dist/actions.d.ts +62 -0
  4. package/dist/actions.js +12 -0
  5. package/dist/adapters/cloudAdapters.d.ts +72 -0
  6. package/dist/adapters/cloudAdapters.js +303 -0
  7. package/dist/adapters/space/index.d.ts +93 -0
  8. package/dist/adapters/space/index.js +122 -0
  9. package/dist/adapters/space/resolvers.d.ts +69 -0
  10. package/dist/adapters/space/resolvers.js +79 -0
  11. package/dist/adapters/space/space.test.d.ts +1 -0
  12. package/dist/adapters/space/types.d.ts +64 -0
  13. package/dist/adapters/space/types.js +41 -0
  14. package/dist/auth.d.ts +50 -0
  15. package/dist/auth.js +19 -0
  16. package/dist/core/auth/api.d.ts +400 -11
  17. package/dist/core/auth/api.js +1044 -50
  18. package/dist/core/auth/createAuth.d.ts +135 -30
  19. package/dist/core/auth/createAuth.js +96 -24
  20. package/dist/core/auth/credentials.d.ts +9 -3
  21. package/dist/core/auth/credentials.js +10 -4
  22. package/dist/core/auth/csrf.d.ts +89 -0
  23. package/dist/core/auth/csrf.js +234 -0
  24. package/dist/core/auth/csrf.test.d.ts +1 -0
  25. package/dist/core/auth/identity.d.ts +6 -0
  26. package/dist/core/auth/identity.js +37 -8
  27. package/dist/core/auth/lifecycle.test.d.ts +1 -0
  28. package/dist/core/auth/oauth/index.d.ts +8 -0
  29. package/dist/core/auth/oauth/index.js +13 -15
  30. package/dist/core/auth/passwords.d.ts +15 -0
  31. package/dist/core/auth/passwords.js +32 -0
  32. package/dist/core/auth/redirects.d.ts +18 -0
  33. package/dist/core/auth/redirects.js +31 -0
  34. package/dist/core/auth/redirects.test.d.ts +1 -0
  35. package/dist/core/auth/routes.d.ts +4 -2
  36. package/dist/core/auth/routes.js +234 -9
  37. package/dist/core/auth/session.d.ts +11 -0
  38. package/dist/core/auth/session.js +1 -1
  39. package/dist/core/auth/spaceTokens.d.ts +38 -2
  40. package/dist/core/auth/spaceTokens.js +54 -2
  41. package/dist/core/auth/throttle.d.ts +5 -0
  42. package/dist/core/auth/throttle.js +81 -0
  43. package/dist/core/auth/throttle.test.d.ts +1 -0
  44. package/dist/core/auth/tokens.d.ts +83 -12
  45. package/dist/core/auth/tokens.js +51 -9
  46. package/dist/core/auth/totp.d.ts +48 -0
  47. package/dist/core/auth/totp.js +118 -0
  48. package/dist/core/auth/totp.test.d.ts +1 -0
  49. package/dist/core/createServer.d.ts +0 -6
  50. package/dist/core/createServer.js +35 -3
  51. package/dist/core/fontStore.d.ts +33 -0
  52. package/dist/core/fontStore.js +53 -0
  53. package/dist/core/fontStore.test.d.ts +1 -0
  54. package/dist/core/handlers/authMiddleware.js +1 -1
  55. package/dist/core/handlers/authRouteHandlers.d.ts +9 -1
  56. package/dist/core/handlers/authRouteHandlers.js +4 -3
  57. package/dist/core/handlers/csrfMiddleware.d.ts +20 -0
  58. package/dist/core/handlers/csrfMiddleware.js +32 -0
  59. package/dist/core/handlers/oauthRouteHandlers.d.ts +46 -0
  60. package/dist/core/handlers/oauthRouteHandlers.js +159 -0
  61. package/dist/core/handlers/socialRouteHandlers.d.ts +40 -0
  62. package/dist/core/handlers/socialRouteHandlers.js +129 -0
  63. package/dist/core/handlers/socialRouteHandlers.test.d.ts +1 -0
  64. package/dist/core/handlers/types.d.ts +22 -5
  65. package/dist/core/health.d.ts +11 -1
  66. package/dist/core/health.js +27 -3
  67. package/dist/core/http/dispatcher.d.ts +1 -1
  68. package/dist/core/http/dispatcher.js +20 -1
  69. package/dist/core/http/stages/authApi.js +1 -1
  70. package/dist/core/http/stages/fontAssets.d.ts +9 -0
  71. package/dist/core/http/stages/fontAssets.js +27 -0
  72. package/dist/core/http/stages/oauth.d.ts +19 -0
  73. package/dist/core/http/stages/oauth.js +118 -0
  74. package/dist/core/http/types.d.ts +12 -0
  75. package/dist/core/oauth/authorize.d.ts +22 -0
  76. package/dist/core/oauth/authorize.js +254 -0
  77. package/dist/core/oauth/authorize.test.d.ts +1 -0
  78. package/dist/core/oauth/challenge.d.ts +11 -0
  79. package/dist/core/oauth/challenge.js +33 -0
  80. package/dist/core/oauth/consentPage.d.ts +12 -0
  81. package/dist/core/oauth/consentPage.js +131 -0
  82. package/dist/core/oauth/metadata.d.ts +41 -0
  83. package/dist/core/oauth/metadata.js +71 -0
  84. package/dist/core/oauth/params.d.ts +5 -0
  85. package/dist/core/oauth/params.js +5 -0
  86. package/dist/core/oauth/pkce.d.ts +6 -0
  87. package/dist/core/oauth/pkce.js +15 -0
  88. package/dist/core/oauth/records.d.ts +72 -0
  89. package/dist/core/oauth/records.js +49 -0
  90. package/dist/core/oauth/register.d.ts +5 -0
  91. package/dist/core/oauth/register.js +55 -0
  92. package/dist/core/oauth/respond.d.ts +21 -0
  93. package/dist/core/oauth/respond.js +59 -0
  94. package/dist/core/oauth/token.d.ts +14 -0
  95. package/dist/core/oauth/token.js +130 -0
  96. package/dist/core/previewToken.d.ts +20 -0
  97. package/dist/core/previewToken.js +29 -1
  98. package/dist/core/server/pageServer.js +4 -1
  99. package/dist/core/services/action.d.ts +6 -4
  100. package/dist/core/services/action.js +103 -10
  101. package/dist/core/services/registry.js +2 -0
  102. package/dist/core/services/resolve.d.ts +8 -0
  103. package/dist/core/services/resolve.js +12 -1
  104. package/dist/core/staticFiles.js +1 -2
  105. package/dist/handlers.d.ts +7 -1
  106. package/dist/handlers.js +4 -1
  107. package/dist/helpers/buildResponseHelpers.d.ts +8 -0
  108. package/dist/helpers/buildResponseHelpers.js +9 -1
  109. package/dist/helpers/buildServerInfo.d.ts +8 -3
  110. package/dist/helpers/buildServerInfo.js +18 -14
  111. package/dist/helpers/cache/keys.d.ts +13 -0
  112. package/dist/helpers/cache/keys.js +12 -1
  113. package/dist/helpers/onAbort.d.ts +12 -0
  114. package/dist/helpers/onAbort.js +22 -0
  115. package/dist/helpers/onAbort.test.d.ts +1 -0
  116. package/dist/helpers/outboundGuard.d.ts +27 -0
  117. package/dist/helpers/outboundGuard.js +49 -0
  118. package/dist/helpers/outboundGuard.test.d.ts +1 -0
  119. package/dist/helpers/serverLog.d.ts +6 -3
  120. package/dist/helpers/serverLog.js +20 -3
  121. package/dist/index.d.ts +6 -0
  122. package/dist/index.js +9 -4
  123. package/dist/kernel.d.ts +5 -22
  124. package/dist/kernel.js +1 -14
  125. package/dist/modules/actions/{handler.js → connectorWrite.js} +5 -2
  126. package/dist/modules/actions/connectorWrite.test.d.ts +1 -0
  127. package/dist/modules/actions/index.d.ts +40 -0
  128. package/dist/modules/actions/index.js +46 -0
  129. package/dist/modules/actions/moduleFor.d.ts +3 -0
  130. package/dist/modules/actions/moduleFor.js +35 -0
  131. package/dist/modules/actions/moduleFor.test.d.ts +1 -0
  132. package/dist/modules/actions/runtime/check.d.ts +24 -0
  133. package/dist/modules/actions/runtime/check.js +111 -0
  134. package/dist/modules/actions/runtime/check.test.d.ts +1 -0
  135. package/dist/modules/actions/runtime/errors.d.ts +11 -0
  136. package/dist/modules/actions/runtime/errors.js +17 -0
  137. package/dist/modules/actions/runtime/guards.d.ts +126 -0
  138. package/dist/modules/actions/runtime/guards.js +246 -0
  139. package/dist/modules/actions/runtime/guards.test.d.ts +1 -0
  140. package/dist/modules/actions/runtime/kvStore.d.ts +27 -0
  141. package/dist/modules/actions/runtime/kvStore.js +41 -0
  142. package/dist/modules/actions/runtime/kvStore.test.d.ts +1 -0
  143. package/dist/modules/actions/runtime/limits.d.ts +11 -0
  144. package/dist/modules/actions/runtime/limits.js +35 -0
  145. package/dist/modules/actions/runtime/memoryKv.d.ts +2 -0
  146. package/dist/modules/actions/runtime/memoryKv.js +77 -0
  147. package/dist/modules/actions/runtime/namespaceKv.d.ts +9 -0
  148. package/dist/modules/actions/runtime/namespaceKv.js +19 -0
  149. package/dist/modules/actions/runtime/precheck.d.ts +27 -0
  150. package/dist/modules/actions/runtime/precheck.js +45 -0
  151. package/dist/modules/actions/runtime/renderResolver.d.ts +15 -0
  152. package/dist/modules/actions/runtime/renderResolver.js +130 -0
  153. package/dist/modules/actions/runtime/renderResolver.test.d.ts +1 -0
  154. package/dist/modules/actions/runtime/renderShare.d.ts +29 -0
  155. package/dist/modules/actions/runtime/renderShare.js +36 -0
  156. package/dist/modules/actions/runtime/renderShare.test.d.ts +1 -0
  157. package/dist/modules/actions/runtime/report.d.ts +14 -0
  158. package/dist/modules/actions/runtime/report.js +22 -0
  159. package/dist/modules/actions/runtime/runAction.d.ts +12 -0
  160. package/dist/modules/actions/runtime/runAction.js +405 -0
  161. package/dist/modules/actions/runtime/runAction.test.d.ts +1 -0
  162. package/dist/modules/actions/runtime/runLogger.d.ts +31 -0
  163. package/dist/modules/actions/runtime/runLogger.js +66 -0
  164. package/dist/modules/actions/runtime/runLogger.test.d.ts +1 -0
  165. package/dist/modules/actions/runtime/schedule.d.ts +35 -0
  166. package/dist/modules/actions/runtime/schedule.js +76 -0
  167. package/dist/modules/actions/runtime/schedule.test.d.ts +1 -0
  168. package/dist/modules/actions/runtime/scope.d.ts +40 -0
  169. package/dist/modules/actions/runtime/scope.js +103 -0
  170. package/dist/modules/actions/runtime/triggers.d.ts +12 -0
  171. package/dist/modules/actions/runtime/triggers.js +14 -0
  172. package/dist/modules/actions/taskCatalog.d.ts +26 -0
  173. package/dist/modules/actions/taskCatalog.js +24 -0
  174. package/dist/modules/actions/taskCatalog.test.d.ts +1 -0
  175. package/dist/modules/actions/tasks/auth.d.ts +2 -0
  176. package/dist/modules/actions/tasks/auth.js +40 -0
  177. package/dist/modules/actions/tasks/builtins.d.ts +8 -0
  178. package/dist/modules/actions/tasks/builtins.js +24 -0
  179. package/dist/modules/actions/tasks/connector.d.ts +2 -0
  180. package/dist/modules/actions/tasks/connector.js +141 -0
  181. package/dist/modules/actions/tasks/db.d.ts +2 -0
  182. package/dist/modules/actions/tasks/db.js +59 -0
  183. package/dist/modules/actions/tasks/db.test.d.ts +1 -0
  184. package/dist/modules/actions/tasks/flow.d.ts +3 -0
  185. package/dist/modules/actions/tasks/flow.js +109 -0
  186. package/dist/modules/actions/tasks/helpers.d.ts +16 -0
  187. package/dist/modules/actions/tasks/helpers.js +30 -0
  188. package/dist/modules/actions/tasks/http.d.ts +2 -0
  189. package/dist/modules/actions/tasks/http.js +111 -0
  190. package/dist/modules/actions/tasks/http.test.d.ts +1 -0
  191. package/dist/modules/actions/tasks/kv.d.ts +2 -0
  192. package/dist/modules/actions/tasks/kv.js +102 -0
  193. package/dist/modules/actions/tasks/kv.test.d.ts +1 -0
  194. package/dist/modules/actions/tasks/registry.d.ts +10 -0
  195. package/dist/modules/actions/tasks/registry.js +36 -0
  196. package/dist/modules/actions/tasks/transform.d.ts +2 -0
  197. package/dist/modules/actions/tasks/transform.js +34 -0
  198. package/dist/modules/actions/transport/callHandler.d.ts +24 -0
  199. package/dist/modules/actions/transport/callHandler.js +233 -0
  200. package/dist/modules/actions/transport/callHandler.test.d.ts +1 -0
  201. package/dist/modules/actions/transport/cancelHandler.d.ts +23 -0
  202. package/dist/modules/actions/transport/cancelHandler.js +23 -0
  203. package/dist/modules/actions/transport/catalogHandler.d.ts +18 -0
  204. package/dist/modules/actions/transport/catalogHandler.js +21 -0
  205. package/dist/modules/actions/transport/stream.d.ts +23 -0
  206. package/dist/modules/actions/transport/stream.js +100 -0
  207. package/dist/modules/actions/transport/verifySignature.d.ts +16 -0
  208. package/dist/modules/actions/transport/verifySignature.js +72 -0
  209. package/dist/modules/actions/transport/webhookHandler.d.ts +25 -0
  210. package/dist/modules/actions/transport/webhookHandler.js +206 -0
  211. package/dist/modules/actions/transport/webhookHandler.test.d.ts +1 -0
  212. package/dist/modules/actions/types.d.ts +234 -0
  213. package/dist/modules/connectors/engine.js +4 -1
  214. package/dist/modules/connectors/resolver.d.ts +10 -2
  215. package/dist/modules/connectors/resolver.js +8 -3
  216. package/dist/modules/mysql/accounts.d.ts +5 -0
  217. package/dist/modules/mysql/accounts.js +365 -0
  218. package/dist/modules/mysql/accounts.test.d.ts +1 -0
  219. package/dist/modules/mysql/admin.d.ts +58 -0
  220. package/dist/modules/mysql/admin.js +133 -0
  221. package/dist/modules/mysql/config.d.ts +82 -0
  222. package/dist/modules/mysql/config.js +30 -0
  223. package/dist/modules/mysql/identities.d.ts +35 -0
  224. package/dist/modules/mysql/identities.js +43 -0
  225. package/dist/modules/mysql/index.d.ts +67 -0
  226. package/dist/modules/mysql/index.js +62 -0
  227. package/dist/modules/mysql/pool.d.ts +3 -0
  228. package/dist/modules/mysql/pool.js +88 -0
  229. package/dist/modules/mysql/query.d.ts +12 -0
  230. package/dist/modules/mysql/query.js +21 -0
  231. package/dist/modules/mysql/schema.d.ts +43 -0
  232. package/dist/modules/mysql/schema.js +340 -0
  233. package/dist/modules/mysql/schema.test.d.ts +1 -0
  234. package/dist/modules/mysql/sessions.d.ts +33 -0
  235. package/dist/modules/mysql/sessions.js +46 -0
  236. package/dist/modules/mysql/spaceTokens.d.ts +11 -0
  237. package/dist/modules/mysql/spaceTokens.js +59 -0
  238. package/dist/modules/mysql/store.integration.test.d.ts +1 -0
  239. package/dist/modules/rsc/connectorRscData.d.ts +25 -0
  240. package/dist/modules/rsc/connectorRscData.js +51 -0
  241. package/dist/modules/rsc/connectorRscData.test.d.ts +1 -0
  242. package/dist/modules/rsc/handler.js +16 -3
  243. package/dist/modules/rsc/matchRscPage.d.ts +14 -0
  244. package/dist/modules/rsc/matchRscPage.js +26 -0
  245. package/dist/modules/rsc/resolveRscData.d.ts +14 -1
  246. package/dist/modules/rsc/resolveRscData.js +23 -34
  247. package/dist/modules/ssr/Component.d.ts +6 -2
  248. package/dist/modules/ssr/Component.js +3 -1
  249. package/dist/modules/ssr/draftSession.test.d.ts +1 -0
  250. package/dist/modules/ssr/handler.js +15 -2
  251. package/dist/modules/ssr/loadPluginComponents.js +22 -5
  252. package/dist/modules/ssr/prepareRender.js +87 -8
  253. package/dist/modules/ssr/prepareRender.test.d.ts +1 -0
  254. package/dist/modules/ssr/preview.d.ts +21 -8
  255. package/dist/modules/ssr/preview.js +38 -14
  256. package/dist/modules/ssr/registerExternalPlugins.js +2 -2
  257. package/dist/modules/ssr/resolvePageSeo.d.ts +18 -0
  258. package/dist/modules/ssr/resolvePageSeo.js +26 -0
  259. package/dist/modules/ssr/resolvePageSeo.test.d.ts +1 -0
  260. package/dist/modules/ssr/template.test.d.ts +1 -0
  261. package/dist/modules/ssr/views/template.ejs +71 -5
  262. package/dist/mysql.d.ts +26 -0
  263. package/dist/mysql.js +4 -0
  264. package/dist/oauth.d.ts +24 -0
  265. package/dist/oauth.js +10 -0
  266. package/dist/plugins/compile.d.ts +1 -0
  267. package/dist/plugins/compile.js +37 -3
  268. package/dist/plugins/manager.d.ts +40 -0
  269. package/dist/plugins/manager.js +113 -13
  270. package/dist/plugins/manager.test.d.ts +1 -0
  271. package/package.json +43 -14
  272. package/skills/plitzi-authoring/SKILL.md +252 -0
  273. /package/dist/{modules/actions/handler.test.d.ts → adapters/cloudAdapters.test.d.ts} +0 -0
  274. /package/dist/modules/actions/{handler.d.ts → connectorWrite.d.ts} +0 -0
@@ -0,0 +1,130 @@
1
+ import { ActionRunError } from "./errors.js";
2
+ import { findTriggerNode, triggerParams } from "./triggers.js";
3
+ import onAbort from "../../../helpers/onAbort.js";
4
+ import { createRenderShare } from "./renderShare.js";
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ import { triggerCacheMs } from "@plitzi/sdk-shared/actions";
7
+ //#region src/modules/actions/runtime/renderResolver.ts
8
+ /**
9
+ * What makes two renders the same question.
10
+ *
11
+ * Everything that can change the answer, and nothing else: the space and the version being served, the action,
12
+ * the visitor, and the input the flow will actually see. The USER is in there because an action may read
13
+ * `{{ user.* }}` — sharing across visitors would hand one person another's page — while anonymous visitors, who
14
+ * are most of them, share the one key that matters.
15
+ */
16
+ var shareKey = (parts) => createHash("sha256").update(JSON.stringify(parts)).digest("hex").slice(0, 32);
17
+ /**
18
+ * Resolves a `runtime: 'server'` element that names an ACTION rather than a connector.
19
+ *
20
+ * This is the read a manifest cannot express: two calls that have to be joined, a field computed from both, a
21
+ * shape that depends on who is looking. The element names an action exactly as it would name a connector, and the
22
+ * page still learns nothing about what happens on the other side.
23
+ *
24
+ * Its input is the page's own context — route params, then query params — plus whatever the element declares, so
25
+ * an action feeding `/blog/:slug` reads `{{ input.slug }}` and needs nothing else. The action's own input contract
26
+ * still drops everything it did not declare.
27
+ */
28
+ var createActionResolver = (lookups, module) => {
29
+ const share = createRenderShare();
30
+ return async ({ element, routeParams, queryParams, spaceId, environment, user, req, signal }) => {
31
+ /**
32
+ * Which version of the space is being rendered — environment AND revision from the same record.
33
+ *
34
+ * Reading one from the deployment and the other from the resolve context would let them disagree, and the
35
+ * combination "this environment, that revision" names a snapshot nobody published.
36
+ */
37
+ const deployment = req.ctx.spaceDeployment;
38
+ const at = {
39
+ environment: deployment?.environment ?? environment,
40
+ revision: deployment?.revision ?? 0
41
+ };
42
+ const { action: actionId, input = {} } = element.attributes;
43
+ if (!actionId) return;
44
+ const entry = await lookups.getAction(spaceId, actionId, at);
45
+ if (!entry) throw new Error(`Action "${actionId}" is not configured for space ${spaceId}`);
46
+ const values = {
47
+ ...routeParams,
48
+ ...queryParams,
49
+ ...input
50
+ };
51
+ const trigger = findTriggerNode(entry.document.nodes, "render");
52
+ const ttlMs = trigger ? triggerCacheMs(triggerParams(trigger)) : 0;
53
+ const key = shareKey([
54
+ spaceId,
55
+ at.environment,
56
+ at.revision,
57
+ entry.id,
58
+ user?.id ?? null,
59
+ values
60
+ ]);
61
+ const callerId = user ? `user:${user.id}` : "render";
62
+ const startRun = async () => {
63
+ /**
64
+ * A key of its own per render, so two visitors are never each other's duplicate.
65
+ *
66
+ * Single-flight exists for the caller who submits twice — a double-click, a retry — and keys on the caller
67
+ * and the input. A render has neither of those to go on: every anonymous visitor of one URL is `render`
68
+ * with the same input, so the derived key made concurrent page loads collide and one of them had its
69
+ * section refused as a duplicate. The busier the page, the more often.
70
+ */
71
+ const run = await module.guards.begin({
72
+ spaceId,
73
+ actionId: entry.id,
74
+ callerId,
75
+ input,
76
+ idempotencyKey: `render:${randomUUID()}`,
77
+ kind: "render",
78
+ ttlMs: module.limitsFor(entry.document).timeoutMs
79
+ });
80
+ /**
81
+ * The render giving up ends the run, not just the wait for it.
82
+ *
83
+ * `resolveRscData` stops waiting when an element's budget is gone, and before this the run carried on to
84
+ * its own timeout — holding a slot and an outbound connection for a page that had already been answered.
85
+ */
86
+ const releaseRenderStop = onAbort(signal, () => run.controller.abort());
87
+ try {
88
+ const result = await module.runAction({
89
+ entry,
90
+ input: values,
91
+ spaceId,
92
+ environment,
93
+ trigger: "render",
94
+ user,
95
+ callerId,
96
+ runId: run.runId,
97
+ at,
98
+ signal: run.controller.signal
99
+ });
100
+ /**
101
+ * A run that did not COMPLETE resolved nothing, and must not look like one that resolved to nothing.
102
+ *
103
+ * A step that throws — an outbound call with no internet behind it, a `flow.fail` guard, a timeout — ends
104
+ * the run with `status: 'failed'` and an empty output, which published as a slice is indistinguishable
105
+ * from a provider that legitimately returned no records. The element then renders its empty state instead
106
+ * of its error one, and the bindings meant for exactly this (`hasError`, `errorMessage`) never fire: the
107
+ * page says "nothing here" when the truth is "this could not be fetched".
108
+ */
109
+ if (result.status !== "completed") throw new Error(`Action "${actionId}" ended as ${result.status}`);
110
+ return result.output;
111
+ } catch (error) {
112
+ if (error instanceof ActionRunError) throw new Error(`Action "${actionId}" refused this render: ${error.reason}`, { cause: error });
113
+ throw error;
114
+ } finally {
115
+ releaseRenderStop();
116
+ await module.guards.end(run);
117
+ }
118
+ };
119
+ /**
120
+ * Everyone asking the same question at once gets one run, and its answer.
121
+ *
122
+ * Not an optimisation — it is what makes a page survive being read. Without it, a thousand visitors of one URL
123
+ * are a thousand identical flows and a thousand identical outbound requests, arriving at whatever the action
124
+ * reads all in the same instant.
125
+ */
126
+ return share.run(key, ttlMs, startRun);
127
+ };
128
+ };
129
+ //#endregion
130
+ export { createActionResolver };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * One answer for the visitors asking the same question at the same moment — and, when the author says so, for the
3
+ * ones asking it a moment later.
4
+ *
5
+ * A `render` is a READ repeated once per visitor. A thousand people opening one page is a thousand runs of the
6
+ * same flow and a thousand outbound requests to whatever it reads, all in flight together, all computing the same
7
+ * thing. Refusing them is the wrong answer — that is what a per-space cap did, and it broke the page that was
8
+ * doing well. Sharing is the right one.
9
+ *
10
+ * Two behaviours, and only one of them is a cache:
11
+ *
12
+ * - **In flight**: a render that arrives while an identical one is running joins it. Always on, and it cannot
13
+ * serve anything stale — the answer is being computed right now, for this request as much as for the first.
14
+ * - **Reuse**: the answer is kept for as long as the trigger's `cacheSeconds` says. Off unless authored, because
15
+ * only the author knows whether their page may repeat itself.
16
+ *
17
+ * A failure is never kept. The joiners of a failed run fail with it — they would have failed too — but the next
18
+ * request tries again rather than being told for a minute about a request that was already over.
19
+ *
20
+ * In-process, and that is honest here in a way it is not for a lock: two replicas each keeping their own copy is
21
+ * two computations instead of one, while two replicas each keeping their own LOCK is no lock at all.
22
+ */
23
+ export type RenderShare = {
24
+ /** Runs `produce`, or joins whatever is already producing the same key. `ttlMs` of 0 keeps nothing afterwards. */
25
+ run: (key: string, ttlMs: number, produce: () => Promise<unknown>) => Promise<unknown>;
26
+ /** Entries currently held, for a deployment that wants to see it. */
27
+ size: () => number;
28
+ };
29
+ export declare const createRenderShare: (now?: () => number) => RenderShare;
@@ -0,0 +1,36 @@
1
+ //#region src/modules/actions/runtime/renderShare.ts
2
+ /** Keeps the map from growing without bound on a space with many keys: expired entries are dropped on write. */
3
+ var sweep = (entries, now) => {
4
+ entries.forEach((entry, key) => {
5
+ if (!entry.inFlight && (entry.expiresAt === void 0 || entry.expiresAt <= now)) entries.delete(key);
6
+ });
7
+ };
8
+ var createRenderShare = (now = Date.now) => {
9
+ const entries = /* @__PURE__ */ new Map();
10
+ const run = async (key, ttlMs, produce) => {
11
+ const existing = entries.get(key);
12
+ if (existing?.inFlight) return existing.inFlight;
13
+ if (existing && existing.expiresAt !== void 0 && existing.expiresAt > now()) return existing.value;
14
+ const inFlight = produce();
15
+ entries.set(key, { inFlight });
16
+ try {
17
+ const value = await inFlight;
18
+ sweep(entries, now());
19
+ if (ttlMs > 0) entries.set(key, {
20
+ value,
21
+ expiresAt: now() + ttlMs
22
+ });
23
+ else entries.delete(key);
24
+ return value;
25
+ } catch (error) {
26
+ entries.delete(key);
27
+ throw error;
28
+ }
29
+ };
30
+ return {
31
+ run,
32
+ size: () => entries.size
33
+ };
34
+ };
35
+ //#endregion
36
+ export { createRenderShare };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import { ActionRejectRecord, SSRPageServerConfig } from '@plitzi/sdk-shared';
2
+ /**
3
+ * Reports a request that never became a run.
4
+ *
5
+ * Its own function rather than a call at each refusal so the two transports cannot end up reporting different
6
+ * things — and so the rule that a report may not fail the answer is written once. A deployment's sink throwing
7
+ * must not turn a clean 401 into a 500: the caller was refused either way, and the refusal is the part that has
8
+ * to reach them.
9
+ *
10
+ * Nothing is filtered here. A duplicate delivery is a well-behaved sender retrying and an invalid signature is a
11
+ * broken integration, but which of those is worth a row in somebody's activity feed is a policy, and policies
12
+ * belong to the deployment.
13
+ */
14
+ export declare const reportReject: (config: SSRPageServerConfig, record: ActionRejectRecord) => Promise<void>;
@@ -0,0 +1,22 @@
1
+ //#region src/modules/actions/runtime/report.ts
2
+ /**
3
+ * Reports a request that never became a run.
4
+ *
5
+ * Its own function rather than a call at each refusal so the two transports cannot end up reporting different
6
+ * things — and so the rule that a report may not fail the answer is written once. A deployment's sink throwing
7
+ * must not turn a clean 401 into a 500: the caller was refused either way, and the refusal is the part that has
8
+ * to reach them.
9
+ *
10
+ * Nothing is filtered here. A duplicate delivery is a well-behaved sender retrying and an invalid signature is a
11
+ * broken integration, but which of those is worth a row in somebody's activity feed is a policy, and policies
12
+ * belong to the deployment.
13
+ */
14
+ var reportReject = async (config, record) => {
15
+ try {
16
+ await config.action?.onReject?.(record);
17
+ } catch (error) {
18
+ console.error("[Actions] reject record failed:", error);
19
+ }
20
+ };
21
+ //#endregion
22
+ export { reportReject };
@@ -0,0 +1,12 @@
1
+ import { ActionRunRequest, ActionRunResult, ActionsConfig, ActionTaskRegistry } from '../types';
2
+ export type ActionRunner = {
3
+ runAction: (request: ActionRunRequest) => Promise<ActionRunResult>;
4
+ };
5
+ /**
6
+ * Builds the runner every trigger goes through.
7
+ *
8
+ * Every check that decides whether a run may happen at all — the trigger and whether it is on, access, lineage,
9
+ * input contract — lives HERE rather than in a transport, because a deployment mounting its own trigger calls this
10
+ * directly. A check in the endpoint would be a check a custom trigger silently skips.
11
+ */
12
+ export declare const createActionRunner: (config: ActionsConfig, registry: ActionTaskRegistry, baseFetch?: typeof fetch) => ActionRunner;
@@ -0,0 +1,405 @@
1
+ import { ActionRunError } from "./errors.js";
2
+ import { runCancelKey } from "./guards.js";
3
+ import { createKvStore } from "./kvStore.js";
4
+ import { resolveLimits } from "./limits.js";
5
+ import { createMemoryKv } from "./memoryKv.js";
6
+ import { namespaceKv } from "./namespaceKv.js";
7
+ import { createRedactor, projectUser } from "./scope.js";
8
+ import { precheckRun } from "./precheck.js";
9
+ import onAbort from "../../../helpers/onAbort.js";
10
+ import { evaluateRuleGroup } from "@plitzi/sdk-shared/helpers/ruleEvaluator";
11
+ import { hasValidToken, processTwig } from "@plitzi/sdk-shared/helpers/twigWrapper";
12
+ //#region src/modules/actions/runtime/runAction.ts
13
+ var MAX_TWIG_RESOLUTION_PASSES = 5;
14
+ /** How often a run asks the shared store whether it has been cancelled. Once a second is far finer than the
15
+ * boundaries a flow actually has, and it keeps a long run to sixty reads a minute. */
16
+ var CANCEL_POLL_MS = 1e3;
17
+ /**
18
+ * The run's deadline, as a promise that loses patience.
19
+ *
20
+ * Aborting the controller is the polite half: a task that watches its signal stops, and the loop refuses to start
21
+ * the next step. It is not enough on its own, because a step that ignores the signal — a driver that does not
22
+ * take one, a provider client that swallows it, a `while (true)` inside a task — leaves the run awaiting something
23
+ * that never returns. That is the stuck run this exists to end: nothing else in the process can tell the
24
+ * difference between a flow that is working hard and one that will never finish.
25
+ *
26
+ * Racing the flow against this means the RUN always ends, even when a step does not. What the step does after
27
+ * that is beyond anybody's reach in a single-threaded runtime — but it no longer holds a caller, a slot, or a
28
+ * connection while it does it.
29
+ */
30
+ var createDeadline = (timeoutMs, controller) => {
31
+ let timer;
32
+ return {
33
+ promise: new Promise((_resolve, reject) => {
34
+ timer = setTimeout(() => {
35
+ controller.abort();
36
+ reject(new ActionRunError("timeout", `Action exceeded its ${timeoutMs}ms budget`));
37
+ }, timeoutMs);
38
+ }),
39
+ clear: () => clearTimeout(timer)
40
+ };
41
+ };
42
+ /**
43
+ * Whether somebody has asked this run to stop, from wherever the request landed.
44
+ *
45
+ * The socket closing aborts a run in the process that is running it, and a `DELETE` that happens to reach that
46
+ * same process does too. Behind a load balancer it usually does not: a cancel arrives at a replica that has never
47
+ * heard of the run, and the flow it was meant to stop carries on charging cards. So the cancel is a FLAG in the
48
+ * store the deployment already shares, and this is the run reading it at every step boundary.
49
+ *
50
+ * Without a shared store it is a no-op rather than a lie — one process's local abort is the whole mechanism there,
51
+ * and it already works.
52
+ */
53
+ var createCancelWatch = (store, runId) => {
54
+ if (!store) return () => Promise.resolve(false);
55
+ let checkedAt = 0;
56
+ return async () => {
57
+ const now = Date.now();
58
+ if (now - checkedAt < CANCEL_POLL_MS) return false;
59
+ checkedAt = now;
60
+ try {
61
+ return await store.get(runCancelKey(runId)) !== void 0;
62
+ } catch {
63
+ return false;
64
+ }
65
+ };
66
+ };
67
+ /**
68
+ * Resolves twig in a node's params against the flow scope.
69
+ *
70
+ * Multi-pass with a ceiling, exactly as the client engine does: a value can resolve to another template, and
71
+ * without the ceiling a self-referencing pair spins forever. Unresolved tokens are left as-is and show up in the
72
+ * trace, which is how an author sees that a step referenced something the flow never produced.
73
+ */
74
+ var resolveParams = (params, scope, raw) => {
75
+ if (raw) return params;
76
+ return Object.entries(params).reduce((acum, [key, param]) => {
77
+ let value = param;
78
+ let passes = MAX_TWIG_RESOLUTION_PASSES;
79
+ while (typeof value === "string" && hasValidToken(value) && passes > 0) {
80
+ value = processTwig(value, scope, false, true);
81
+ passes--;
82
+ }
83
+ acum[key] = value;
84
+ return acum;
85
+ }, {});
86
+ };
87
+ /**
88
+ * Holds an answer to what the run is allowed to carry.
89
+ *
90
+ * Two checks rather than one, because a body arrives in two ways. `Content-Length` is refused before a byte is
91
+ * read, which is the cheap half; a chunked answer that declares nothing is counted AS it streams and errored the
92
+ * moment it goes over — so the ceiling holds for a backend that lies about its size or never states one.
93
+ *
94
+ * The cap is on one response and not on the run: it exists so that a single answer cannot be unbounded, which is
95
+ * a different failure from a flow that makes many small calls (that is `maxRequests`).
96
+ */
97
+ var capped = (response, maxBytes) => {
98
+ const declared = Number(response.headers.get("content-length") ?? "");
99
+ if (Number.isFinite(declared) && declared > maxBytes) throw new ActionRunError("over_capacity", `Response is larger than the ${maxBytes} byte budget`);
100
+ if (!response.body) return response;
101
+ let seen = 0;
102
+ const counted = response.body.pipeThrough(new TransformStream({ transform: (chunk, controller) => {
103
+ seen += chunk.byteLength;
104
+ if (seen > maxBytes) {
105
+ controller.error(new ActionRunError("over_capacity", `Response exceeded the ${maxBytes} byte budget`));
106
+ return;
107
+ }
108
+ controller.enqueue(chunk);
109
+ } }));
110
+ return new Response(counted, {
111
+ status: response.status,
112
+ statusText: response.statusText,
113
+ headers: response.headers
114
+ });
115
+ };
116
+ /**
117
+ * Counts outbound calls, refuses past the budget, caps what one answer may carry back, and stamps the run's
118
+ * lineage on every one of them.
119
+ *
120
+ * The budget stops a loop from turning one run into a hundred requests. The lineage header is what makes the
121
+ * OTHER loop detectable: an action whose HTTP step reaches its own space's webhook arrives carrying the chain
122
+ * that led there, and the run it would start refuses itself. It names the space's own actions to a backend that
123
+ * space configured, which is the cost of catching a cycle nothing else can see.
124
+ *
125
+ * Every ceiling lives HERE rather than in each task, because this is the only door a task has to the outside
126
+ * world — a task that had to remember to count its own bytes is a task that will forget.
127
+ */
128
+ var createRunFetch = (base, signal, limits, lineage) => {
129
+ let issued = 0;
130
+ return async (input, init) => {
131
+ issued += 1;
132
+ if (issued > limits.maxRequests) throw new ActionRunError("over_capacity", `Action exceeded its ${limits.maxRequests} outbound request budget`);
133
+ const headers = new Headers(init?.headers);
134
+ headers.set("X-Plitzi-Action-Lineage", lineage.join(","));
135
+ return capped(await base(input, {
136
+ ...init,
137
+ headers,
138
+ signal: init?.signal ?? signal
139
+ }), limits.maxResponseBytes);
140
+ };
141
+ };
142
+ /**
143
+ * Fills in what the node left out, from the task's own catalog entry.
144
+ *
145
+ * A task declares its params with a `defaultValue`, and a document written before a param existed simply has no
146
+ * value for it. Without this the task receives `undefined` for something its signature says is a string, which is
147
+ * how tasks end up full of defensive conversions that hide the real gap.
148
+ */
149
+ /**
150
+ * The pseudo-step a failure is recorded as.
151
+ *
152
+ * A run that died has no node to blame — a ceiling it hit, a throw from outside any step — and the trace is a list
153
+ * of steps. Written out in full rather than cast into shape: a field added to a step becomes a compile error here
154
+ * instead of an `undefined` whoever reads the trace has to work out.
155
+ */
156
+ var errorNode = (action) => ({
157
+ id: "error",
158
+ title: "Error",
159
+ type: "task",
160
+ action,
161
+ params: {},
162
+ preview: {},
163
+ elementId: null,
164
+ beforeNode: "",
165
+ afterNode: "",
166
+ flowId: "",
167
+ enabled: true
168
+ });
169
+ var withDefaults = (task, params) => Object.entries(task.params).reduce((acum, [key, param]) => {
170
+ if (acum[key] === void 0 || acum[key] === "") acum[key] = param.defaultValue ?? "";
171
+ return acum;
172
+ }, { ...params });
173
+ var runNode = async (node, scope, registry, buildContext) => {
174
+ if (!node.action || !node.enabled) return {
175
+ status: "disabled",
176
+ result: {}
177
+ };
178
+ if (node.when && !evaluateRuleGroup(node.when, {
179
+ ...scope,
180
+ [node.id]: node.params
181
+ })) return {
182
+ status: "skipped",
183
+ result: {}
184
+ };
185
+ if (node.type !== "task") throw new ActionRunError("failed", `Step "${node.action}" is a ${node.type}, which cannot run on the server`);
186
+ const task = registry.get(node.action);
187
+ if (!task) throw new ActionRunError("failed", `No server task is registered as "${node.action}"`);
188
+ const params = withDefaults(task, resolveParams(node.params, scope, task.rawParams === true));
189
+ return {
190
+ status: "success",
191
+ result: await task.run(params, buildContext(scope))
192
+ };
193
+ };
194
+ /**
195
+ * Builds the runner every trigger goes through.
196
+ *
197
+ * Every check that decides whether a run may happen at all — the trigger and whether it is on, access, lineage,
198
+ * input contract — lives HERE rather than in a transport, because a deployment mounting its own trigger calls this
199
+ * directly. A check in the endpoint would be a check a custom trigger silently skips.
200
+ */
201
+ var createActionRunner = (config, registry, baseFetch = fetch) => {
202
+ const kv = createKvStore(config.kv ?? createMemoryKv());
203
+ /** Never allowed to fail a run: a logging outage must not take an action down, the same rule metering follows. */
204
+ const record = async (entry) => {
205
+ try {
206
+ await config.onRun?.(entry);
207
+ } catch (error) {
208
+ console.error("[Actions] run record failed:", error);
209
+ }
210
+ };
211
+ const runAction = async (request) => {
212
+ const { entry, runId } = request;
213
+ const { document } = entry;
214
+ const { trigger: triggerNode, values } = precheckRun(entry, {
215
+ trigger: request.trigger,
216
+ input: request.input,
217
+ user: request.user,
218
+ lineage: request.lineage
219
+ });
220
+ const limits = resolveLimits(config.limits, document.limits);
221
+ const redactor = createRedactor();
222
+ const redact = redactor.redact;
223
+ const controller = new AbortController();
224
+ const deadline = createDeadline(request.emit ? limits.streamTimeoutMs : limits.timeoutMs, controller);
225
+ const releaseOuter = onAbort(request.signal, () => controller.abort());
226
+ const scopedKv = namespaceKv(kv, request.spaceId);
227
+ const runFetch = createRunFetch(baseFetch, controller.signal, limits, [...request.lineage ?? [], entry.id]);
228
+ const buildContext = (scope) => ({
229
+ runId,
230
+ spaceId: request.spaceId,
231
+ environment: request.environment,
232
+ trigger: request.trigger,
233
+ user: request.user,
234
+ callerId: request.callerId,
235
+ signal: controller.signal,
236
+ scope,
237
+ /**
238
+ * The secret a STEP asked for, resolved inside that step and never in the flow scope.
239
+ *
240
+ * There is no allow-list to check it against, deliberately: an action is authored by someone who may edit
241
+ * every action in the space, so a list they can edit is not a boundary — it only ever told the redactor what
242
+ * to look for, and the redactor now learns from what was actually resolved. What IS a boundary is that a
243
+ * credential reaches only the params of the step that named it, which is `renderTaskParams`' whole job.
244
+ */
245
+ credential: async (identifier) => {
246
+ const credential = await config.lookups.getCredential?.(request.spaceId, identifier);
247
+ if (credential) redactor.add(credential);
248
+ return credential;
249
+ },
250
+ connector: async (connectorId) => {
251
+ const manifest = await config.lookups.getConnector?.(request.spaceId, connectorId, request.at);
252
+ if (!manifest) return;
253
+ const credential = manifest.credential ? await config.lookups.getCredential?.(request.spaceId, manifest.credential) : void 0;
254
+ if (credential) redactor.add(credential);
255
+ return {
256
+ manifest,
257
+ credential
258
+ };
259
+ },
260
+ fetch: runFetch,
261
+ kv: scopedKv,
262
+ dbDrivers: config.dbDrivers ?? [],
263
+ emit: (chunk) => request.emit?.(redact(chunk))
264
+ });
265
+ const trace = [];
266
+ const startedAt = Date.now();
267
+ let failure;
268
+ /**
269
+ * What every step can see.
270
+ *
271
+ * The run itself carries only the basics — which space, which environment, who asked, what came in — and
272
+ * everything else a flow can reach is whatever a TASK chose to return into it. Credentials are deliberately
273
+ * NOT here: an ambient `{{credential.*}}` would be interpolable by any node, including `flow.output`, which
274
+ * is a secret handed to the browser through a step nobody would think to audit. A task that needs one asks
275
+ * for it by identifier and resolves it inside its own execution.
276
+ */
277
+ const scope = {
278
+ input: values,
279
+ user: projectUser(request.user),
280
+ spaceId: request.spaceId,
281
+ environment: request.environment,
282
+ trigger: request.trigger,
283
+ runId
284
+ };
285
+ let status = "completed";
286
+ let returned;
287
+ const isCancelled = createCancelWatch(config.kv, runId);
288
+ /**
289
+ * The flow itself, as something that can be raced.
290
+ *
291
+ * Separated from the `try` around it for one reason: what awaits it must be able to stop awaiting. A run that
292
+ * hits its deadline answers, releases its slot and closes its connection while whatever step ignored the abort
293
+ * carries on in the background — which is the difference between a stuck STEP and a stuck server.
294
+ */
295
+ const runFlow = async () => {
296
+ let current = triggerNode;
297
+ let executed = 0;
298
+ let next = document.nodes[current.afterNode];
299
+ while (next) {
300
+ if (controller.signal.aborted) {
301
+ status = "aborted";
302
+ break;
303
+ }
304
+ if (await isCancelled()) {
305
+ controller.abort();
306
+ status = "aborted";
307
+ break;
308
+ }
309
+ executed += 1;
310
+ if (executed > limits.maxNodes) throw new ActionRunError("over_capacity", `Action exceeded its ${limits.maxNodes} step budget`);
311
+ const startTime = Date.now();
312
+ const outcome = await runNode(next, scope, registry, buildContext);
313
+ trace.push({
314
+ node: next,
315
+ status: outcome.status,
316
+ result: redact(outcome.result),
317
+ postCallbacks: [],
318
+ startTime,
319
+ endTime: Date.now()
320
+ });
321
+ request.onNode?.(next.id, outcome.status);
322
+ scope[next.id] = outcome.result;
323
+ if (next.action === "flow.output" && outcome.status === "success") returned = outcome.result;
324
+ current = next;
325
+ next = document.nodes[current.afterNode];
326
+ }
327
+ };
328
+ /** Set when the run ends in a way the CALLER has to hear as a status code rather than as a result. */
329
+ let fatal;
330
+ try {
331
+ await Promise.race([runFlow(), deadline.promise]);
332
+ } catch (error) {
333
+ status = controller.signal.aborted ? "aborted" : "failed";
334
+ if (error instanceof ActionRunError) {
335
+ /**
336
+ * Recorded before it is rethrown.
337
+ *
338
+ * A run refused before it began is not a run and leaves nothing; a run that STARTED and then hit a
339
+ * ceiling — its deadline, its step budget, its request budget — is exactly the one somebody needs to find
340
+ * afterwards. Rethrowing straight from here used to skip the record, so the runs hardest to explain were
341
+ * the ones that left no trace at all.
342
+ */
343
+ fatal = error;
344
+ failure = error.message;
345
+ trace.push({
346
+ node: errorNode(error.reason),
347
+ status: "failed",
348
+ result: redact({
349
+ error: error.message,
350
+ reason: error.reason
351
+ }),
352
+ postCallbacks: [],
353
+ startTime: Date.now(),
354
+ endTime: Date.now()
355
+ });
356
+ } else {
357
+ failure = error instanceof Error ? error.message : String(error);
358
+ trace.push({
359
+ node: errorNode("error"),
360
+ status: "failed",
361
+ result: redact({ error: error instanceof Error ? error.message : String(error) }),
362
+ postCallbacks: [],
363
+ startTime: Date.now(),
364
+ endTime: Date.now()
365
+ });
366
+ }
367
+ } finally {
368
+ deadline.clear();
369
+ releaseOuter();
370
+ }
371
+ const output = returned ?? {};
372
+ await record({
373
+ runId,
374
+ actionId: entry.id,
375
+ spaceId: request.spaceId,
376
+ environment: request.environment,
377
+ trigger: request.trigger,
378
+ status,
379
+ durationMs: Date.now() - startedAt,
380
+ ...request.user ? { userId: request.user.id } : {},
381
+ nodes: trace.map((step) => ({
382
+ id: step.node.id,
383
+ action: step.node.action,
384
+ status: step.status
385
+ })),
386
+ ...failure === void 0 ? {} : { error: redact(failure) }
387
+ });
388
+ if (fatal) throw fatal;
389
+ /**
390
+ * The trace is COPIED out.
391
+ *
392
+ * A run that hit its deadline is no longer awaited, and whatever step ignored the abort may still push into
393
+ * this array while the answer is being serialized. The caller gets what was true when it asked.
394
+ */
395
+ return {
396
+ runId,
397
+ status,
398
+ output: redact(output),
399
+ trace: [...trace]
400
+ };
401
+ };
402
+ return { runAction };
403
+ };
404
+ //#endregion
405
+ export { createActionRunner };
@@ -0,0 +1 @@
1
+ export {};