@plitzi/sdk-server 0.33.1 → 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 +53 -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,233 @@
1
+ import { ActionRunError } from "../runtime/errors.js";
2
+ import { triggerParams } from "../runtime/triggers.js";
3
+ import { precheckRun } from "../runtime/precheck.js";
4
+ import onAbort from "../../../helpers/onAbort.js";
5
+ import { openStream, wantsStream } from "./stream.js";
6
+ import { reportReject } from "../runtime/report.js";
7
+ import { triggerAccess } from "@plitzi/sdk-shared/actions";
8
+ //#region src/modules/actions/transport/callHandler.ts
9
+ /**
10
+ * How a refusal reaches the caller.
11
+ *
12
+ * Every reason maps to a status the client can act on without parsing prose — a 409 means "your other run is still
13
+ * going", a 429 means "come back", a 508 means "you built a loop". `recursion` is the one worth the unusual code:
14
+ * 508 Loop Detected says exactly what happened, and anything else would read as a server fault.
15
+ */
16
+ var STATUS_BY_REASON = {
17
+ not_found: 404,
18
+ disabled: 409,
19
+ unauthenticated: 401,
20
+ forbidden: 403,
21
+ invalid_input: 422,
22
+ duplicate: 409,
23
+ over_capacity: 429,
24
+ recursion: 508,
25
+ timeout: 504,
26
+ aborted: 499,
27
+ failed: 500
28
+ };
29
+ var parseBody = (body) => {
30
+ if (!body) return;
31
+ try {
32
+ const parsed = JSON.parse(body);
33
+ return parsed !== null && typeof parsed === "object" ? parsed : void 0;
34
+ } catch {
35
+ return;
36
+ }
37
+ };
38
+ var send = (res, status, payload, runId) => {
39
+ res.setStatus(status);
40
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
41
+ res.setHeader("Cache-Control", "no-store");
42
+ if (runId) res.setHeader("X-Plitzi-Run-Id", runId);
43
+ res.send(JSON.stringify(payload));
44
+ };
45
+ var fail = (res, reason, error, runId) => send(res, STATUS_BY_REASON[reason], {
46
+ error,
47
+ reason
48
+ }, runId);
49
+ /**
50
+ * Whether the caller may be told what the flow DID, as opposed to what it answered.
51
+ *
52
+ * The dev-tools Actions tab reads the trace, so this is the rule that decides what it can show — and it is the
53
+ * action's own access rule, not the server's mode. An action open to everybody has already decided that starting
54
+ * it is anybody's business, so its steps are too; one that asks for a session owes its steps to a session and to
55
+ * nobody else. `devMode` on its own is not an answer to that question: it says this deployment is being built,
56
+ * not that whoever just posted to the endpoint is the person building it.
57
+ *
58
+ * It is deliberately a second lock on a door `precheckRun` already shut — an anonymous call to a `session` action
59
+ * never becomes a run. That is the point: the trace is the one part of the answer that carries the author's own
60
+ * data, and the day a deployment mounts a trigger of its own is the day it stops being covered by the first lock.
61
+ */
62
+ var mayReadTrace = (trigger, user) => triggerAccess(triggerParams(trigger))?.mode === "public" || user !== void 0;
63
+ /**
64
+ * Handles an action-addressed call.
65
+ *
66
+ * The browser names an action and hands it inputs. Everything that decides whether the run may happen — the
67
+ * document, the access rule, the input contract, the lineage — is the runner's, so a deployment mounting its own
68
+ * trigger cannot end up with a weaker set of checks than this endpoint applies.
69
+ */
70
+ var handleActionCall = async (deps) => {
71
+ const { req, res, raw, config, module, signal, callerId, lineage } = deps;
72
+ const { environment = "main", spaceId, revision = 0, authoring } = req.ctx.spaceDeployment ?? {};
73
+ if (typeof spaceId !== "number") {
74
+ fail(res, "not_found", "Invalid space deployment");
75
+ return;
76
+ }
77
+ const body = parseBody(req.body);
78
+ if (!body?.actionId) {
79
+ fail(res, "invalid_input", "Expected { actionId, input }");
80
+ return;
81
+ }
82
+ /**
83
+ * A call that never became a run, told to whoever keeps the deployment's records.
84
+ *
85
+ * The caller already learns the reason from the status and the payload — that is what makes a refusal
86
+ * actionable for them. This is the other half: an author watching their own space see that calls are arriving
87
+ * and being turned away, which is otherwise a thing only the browser's network tab knows.
88
+ */
89
+ const reject = (reason, detail) => reportReject(config, {
90
+ actionId: body.actionId,
91
+ spaceId,
92
+ environment,
93
+ trigger: "call",
94
+ reason,
95
+ callerId,
96
+ ...detail === void 0 ? {} : { detail }
97
+ });
98
+ const entry = await config.action?.lookups?.getAction(spaceId, body.actionId, {
99
+ environment,
100
+ revision
101
+ });
102
+ if (!entry) {
103
+ await reject("not_found", "No action with this identifier at the revision being served");
104
+ fail(res, "not_found", "Unknown action");
105
+ return;
106
+ }
107
+ const limits = module.limitsFor(entry.document);
108
+ const input = body.input ?? {};
109
+ const begin = {
110
+ spaceId,
111
+ actionId: entry.id,
112
+ callerId,
113
+ input,
114
+ ...body.idempotencyKey ? { idempotencyKey: body.idempotencyKey } : {},
115
+ ttlMs: limits.timeoutMs
116
+ };
117
+ /**
118
+ * The answer this key already produced, for a caller repeating itself after the first run finished.
119
+ *
120
+ * Only ever a key the caller NAMED: they told us which request this is, so answering the retry with what it
121
+ * answered the first time is what they asked for. Not metered and not counted — nothing ran.
122
+ */
123
+ const replayed = await module.guards.replay(begin);
124
+ if (replayed) {
125
+ res.setHeader("X-Plitzi-Replayed", "true");
126
+ send(res, 200, {
127
+ ...replayed,
128
+ replayed: true
129
+ }, replayed.runId);
130
+ return;
131
+ }
132
+ let run;
133
+ let entryPoint;
134
+ try {
135
+ entryPoint = precheckRun(entry, {
136
+ trigger: "call",
137
+ input,
138
+ user: req.ctx.user,
139
+ lineage
140
+ }).trigger;
141
+ run = await module.guards.begin(begin);
142
+ } catch (error) {
143
+ const reason = error instanceof ActionRunError ? error.reason : "failed";
144
+ await reject(reason, error instanceof Error ? error.message : void 0);
145
+ fail(res, reason, error instanceof Error ? error.message : "Action refused");
146
+ return;
147
+ }
148
+ await config.adapters.meter?.({
149
+ kind: "server_action",
150
+ cached: false,
151
+ req,
152
+ spaceId,
153
+ environment,
154
+ revision
155
+ });
156
+ const abortRun = () => run.controller.abort();
157
+ const releaseAbort = onAbort(signal, abortRun);
158
+ const stream = wantsStream(req.headers.accept) ? openStream(raw, abortRun, run.runId) : void 0;
159
+ let outcome;
160
+ try {
161
+ const result = await module.runAction({
162
+ entry,
163
+ input,
164
+ spaceId,
165
+ environment,
166
+ trigger: "call",
167
+ user: req.ctx.user,
168
+ callerId,
169
+ runId: run.runId,
170
+ lineage,
171
+ at: {
172
+ environment,
173
+ revision
174
+ },
175
+ signal: run.controller.signal,
176
+ ...stream ? {
177
+ emit: (chunk) => stream.send({
178
+ event: "data",
179
+ data: { chunk }
180
+ }),
181
+ onNode: (id, status) => stream.send({
182
+ event: "node",
183
+ data: {
184
+ id,
185
+ status
186
+ }
187
+ })
188
+ } : {}
189
+ });
190
+ outcome = result;
191
+ if (stream) {
192
+ stream.send({
193
+ event: "done",
194
+ data: {
195
+ runId: result.runId,
196
+ status: result.status,
197
+ output: result.output
198
+ }
199
+ });
200
+ stream.close();
201
+ return;
202
+ }
203
+ const payload = {
204
+ runId: result.runId,
205
+ status: result.status,
206
+ output: result.output
207
+ };
208
+ if ((authoring === true || config.devMode === true) && mayReadTrace(entryPoint, req.ctx.user)) payload.trace = result.trace;
209
+ send(res, 200, payload, result.runId);
210
+ } catch (error) {
211
+ const reason = error instanceof ActionRunError ? error.reason : "failed";
212
+ const message = error instanceof ActionRunError ? error.message : "Action failed";
213
+ if (!(error instanceof ActionRunError)) console.error("[Actions] run failed:", error);
214
+ if (stream) {
215
+ stream.send({
216
+ event: "error",
217
+ data: {
218
+ runId: run.runId,
219
+ error: message,
220
+ reason
221
+ }
222
+ });
223
+ stream.close();
224
+ return;
225
+ }
226
+ fail(res, reason, message, run.runId);
227
+ } finally {
228
+ releaseAbort();
229
+ await module.guards.end(run, outcome);
230
+ }
231
+ };
232
+ //#endregion
233
+ export { handleActionCall };
@@ -0,0 +1,23 @@
1
+ import { ActionsModule } from '../index';
2
+ import { SSRResponseHelpers } from '@plitzi/sdk-shared';
3
+ export type ActionCancelDeps = {
4
+ res: SSRResponseHelpers;
5
+ module: ActionsModule;
6
+ runId: string;
7
+ callerId: string;
8
+ };
9
+ /**
10
+ * Stops a run in flight.
11
+ *
12
+ * The socket closing already aborts a run, so this exists for the cases it cannot cover: a mobile client whose
13
+ * peer is gone without the server seeing it for minutes, and a run started in one tab that the visitor wants to
14
+ * stop from another. Ownership is checked in the guards — a run id travels to the browser, and holding one must
15
+ * not let anybody stop somebody else's run.
16
+ *
17
+ * A run that is not there, or is not the caller's, answers 404 either way: distinguishing them would turn this
18
+ * into an oracle for which run ids are live.
19
+ *
20
+ * Awaited because the run is usually somewhere else: behind a load balancer the tab asking to stop a checkout
21
+ * almost never reaches the replica running it, so the cancel is left in the shared store for the one that is.
22
+ */
23
+ export declare const handleActionCancel: ({ res, module, runId, callerId }: ActionCancelDeps) => Promise<void>;
@@ -0,0 +1,23 @@
1
+ //#region src/modules/actions/transport/cancelHandler.ts
2
+ /**
3
+ * Stops a run in flight.
4
+ *
5
+ * The socket closing already aborts a run, so this exists for the cases it cannot cover: a mobile client whose
6
+ * peer is gone without the server seeing it for minutes, and a run started in one tab that the visitor wants to
7
+ * stop from another. Ownership is checked in the guards — a run id travels to the browser, and holding one must
8
+ * not let anybody stop somebody else's run.
9
+ *
10
+ * A run that is not there, or is not the caller's, answers 404 either way: distinguishing them would turn this
11
+ * into an oracle for which run ids are live.
12
+ *
13
+ * Awaited because the run is usually somewhere else: behind a load balancer the tab asking to stop a checkout
14
+ * almost never reaches the replica running it, so the cancel is left in the shared store for the one that is.
15
+ */
16
+ var handleActionCancel = async ({ res, module, runId, callerId }) => {
17
+ const cancelled = await module.guards.cancel(runId, callerId);
18
+ res.setStatus(cancelled ? 204 : 404);
19
+ res.setHeader("Cache-Control", "no-store");
20
+ res.send("");
21
+ };
22
+ //#endregion
23
+ export { handleActionCancel };
@@ -0,0 +1,18 @@
1
+ import { ActionsModule } from '../index';
2
+ import { SSRResponseHelpers } from '@plitzi/sdk-shared';
3
+ export type ActionCatalogDeps = {
4
+ res: SSRResponseHelpers;
5
+ module: ActionsModule;
6
+ };
7
+ /**
8
+ * Serves the tasks this server can run.
9
+ *
10
+ * The editor asks the SERVER what steps exist instead of holding a hardcoded list, and that is the whole reason a
11
+ * self-hoster's own task appears in their builder with no fork: registering it is publishing it. It also means a
12
+ * deployment with no mail transport never offers `email.send` — the catalog is what that deployment actually has,
13
+ * not what the SDK could imagine.
14
+ *
15
+ * Session-gated by the pipeline: the catalog names a deployment's server-side capabilities, which is not something
16
+ * to hand an anonymous visitor.
17
+ */
18
+ export declare const handleActionCatalog: ({ res, module }: ActionCatalogDeps) => void;
@@ -0,0 +1,21 @@
1
+ import { describeCatalog } from "../taskCatalog.js";
2
+ //#region src/modules/actions/transport/catalogHandler.ts
3
+ /**
4
+ * Serves the tasks this server can run.
5
+ *
6
+ * The editor asks the SERVER what steps exist instead of holding a hardcoded list, and that is the whole reason a
7
+ * self-hoster's own task appears in their builder with no fork: registering it is publishing it. It also means a
8
+ * deployment with no mail transport never offers `email.send` — the catalog is what that deployment actually has,
9
+ * not what the SDK could imagine.
10
+ *
11
+ * Session-gated by the pipeline: the catalog names a deployment's server-side capabilities, which is not something
12
+ * to hand an anonymous visitor.
13
+ */
14
+ var handleActionCatalog = ({ res, module }) => {
15
+ res.setStatus(200);
16
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
17
+ res.setHeader("Cache-Control", "no-store");
18
+ res.send(JSON.stringify({ tasks: describeCatalog(module.registry) }));
19
+ };
20
+ //#endregion
21
+ export { handleActionCatalog };
@@ -0,0 +1,23 @@
1
+ import { RawResponse } from '../../../helpers/buildResponseHelpers';
2
+ import { ActionStreamFrame } from '@plitzi/sdk-shared';
3
+ export type ActionStream = {
4
+ /** Sends one frame. Silently drops once the socket is gone — a stream writing into a closed peer is not an error
5
+ * worth failing a run over. */
6
+ send: (frame: ActionStreamFrame) => void;
7
+ /** Ends the stream and stops the heartbeat. */
8
+ close: () => void;
9
+ /** Whether the peer is still there. */
10
+ isOpen: () => boolean;
11
+ };
12
+ /**
13
+ * Opens a Server-Sent Events response.
14
+ *
15
+ * Written straight to the RAW response, never through the response helpers: those compress the body, set a
16
+ * Content-Length and end it in one go, which is precisely what a stream cannot do. `no-transform` says the same
17
+ * thing to every proxy in between, and `X-Accel-Buffering: no` says it to nginx, which otherwise holds frames
18
+ * until its buffer fills and turns a live stream into one late burst.
19
+ */
20
+ export declare const openStream: (raw: RawResponse, onAbort: () => void, runId: string) => ActionStream;
21
+ /** Whether this caller asked for a stream. The negotiation is the client's, so a page that cannot read one never
22
+ * gets one by accident. */
23
+ export declare const wantsStream: (accept: string | string[] | undefined) => boolean;
@@ -0,0 +1,100 @@
1
+ //#region src/modules/actions/transport/stream.ts
2
+ /** How often a comment frame goes out. Short enough that a dead peer is noticed in seconds, long enough that an
3
+ * idle stream costs nothing worth counting. */
4
+ var HEARTBEAT_MS = 15e3;
5
+ /**
6
+ * How long a hand-rolled `EventSource` waits before reconnecting, if one ever gets here.
7
+ *
8
+ * A day, deliberately. `EventSource` reconnects whenever a stream ends — INCLUDING on normal completion — so a
9
+ * client using it would start a fresh run every time one finished, forever. Our own client uses `fetch` and a
10
+ * reader for exactly this reason; this value is what stops somebody else's from becoming a loop.
11
+ */
12
+ var RETRY_MS = 864e5;
13
+ /**
14
+ * How much unread body may pile up before the run is stopped.
15
+ *
16
+ * A `write` that answers `false` has been buffered rather than sent, which is normal for a moment and a leak
17
+ * forever: a peer that stopped reading — a phone that slept, a proxy that wedged — otherwise has the whole run's
18
+ * output accumulated for it in memory. The socket says nothing, so the only signal is this one.
19
+ */
20
+ var MAX_BUFFERED_BYTES = 1e6;
21
+ var encode = (frame) => `event: ${frame.event}\ndata: ${JSON.stringify(frame.data)}\n\n`;
22
+ /** What the host has written but not yet handed to the socket. Node's own response reports it; the minimum raw
23
+ * response this server accepts does not have to, and a host that cannot say simply never trips the cap. */
24
+ var bufferedBytes = (raw) => {
25
+ const { writableLength } = raw;
26
+ return typeof writableLength === "number" ? writableLength : 0;
27
+ };
28
+ /**
29
+ * Opens a Server-Sent Events response.
30
+ *
31
+ * Written straight to the RAW response, never through the response helpers: those compress the body, set a
32
+ * Content-Length and end it in one go, which is precisely what a stream cannot do. `no-transform` says the same
33
+ * thing to every proxy in between, and `X-Accel-Buffering: no` says it to nginx, which otherwise holds frames
34
+ * until its buffer fills and turns a live stream into one late burst.
35
+ */
36
+ var openStream = (raw, onAbort, runId) => {
37
+ let open = true;
38
+ raw.writeHead(200, {
39
+ "Content-Type": "text/event-stream; charset=utf-8",
40
+ "Cache-Control": "no-cache, no-transform",
41
+ Connection: "keep-alive",
42
+ "X-Accel-Buffering": "no",
43
+ /**
44
+ * The run's id, on the response head rather than in a frame.
45
+ *
46
+ * A streaming step returns as soon as the stream OPENS — that is what makes it a stream — so anything it
47
+ * learns from a frame arrives after the flow has already carried on. The head is the one place a caller can
48
+ * read the id in time to cancel the run it just started.
49
+ */
50
+ "X-Plitzi-Run-Id": runId
51
+ });
52
+ raw.write(`retry: ${RETRY_MS}\n\n`);
53
+ const heartbeat = setInterval(() => {
54
+ if (!open) return;
55
+ try {
56
+ raw.write(": ping\n\n");
57
+ } catch {
58
+ open = false;
59
+ clearInterval(heartbeat);
60
+ onAbort();
61
+ }
62
+ }, HEARTBEAT_MS);
63
+ const close = () => {
64
+ if (!open) return;
65
+ open = false;
66
+ clearInterval(heartbeat);
67
+ try {
68
+ raw.end();
69
+ } catch {}
70
+ };
71
+ return {
72
+ send: (frame) => {
73
+ if (!open) return;
74
+ try {
75
+ raw.write(encode(frame));
76
+ if (bufferedBytes(raw) > MAX_BUFFERED_BYTES) {
77
+ open = false;
78
+ clearInterval(heartbeat);
79
+ onAbort();
80
+ try {
81
+ raw.end();
82
+ } catch {}
83
+ }
84
+ } catch {
85
+ open = false;
86
+ clearInterval(heartbeat);
87
+ onAbort();
88
+ }
89
+ },
90
+ close,
91
+ isOpen: () => open
92
+ };
93
+ };
94
+ /** Whether this caller asked for a stream. The negotiation is the client's, so a page that cannot read one never
95
+ * gets one by accident. */
96
+ var wantsStream = (accept) => {
97
+ return (Array.isArray(accept) ? accept.join(",") : accept ?? "").includes("text/event-stream");
98
+ };
99
+ //#endregion
100
+ export { openStream, wantsStream };
@@ -0,0 +1,16 @@
1
+ import { ActionWebhookVerification } from '@plitzi/sdk-shared';
2
+ export type SignatureCheck = {
3
+ ok: true;
4
+ } | {
5
+ ok: false;
6
+ reason: string;
7
+ };
8
+ /**
9
+ * Checks an inbound webhook signature against the RAW body.
10
+ *
11
+ * Raw, before parsing, because a signature covers bytes: `JSON.parse` then `JSON.stringify` reorders keys and
12
+ * drops whitespace, and the digest of the result is not the digest the sender computed. That is the single most
13
+ * common way a webhook verification is written wrong, and it fails closed — which is why it is worth naming here
14
+ * rather than trusting every caller to remember.
15
+ */
16
+ export declare const verifySignature: (verification: ActionWebhookVerification, secret: string, headers: Record<string, string | string[] | undefined>, rawBody: string) => SignatureCheck;
@@ -0,0 +1,72 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ //#region src/modules/actions/transport/verifySignature.ts
3
+ /**
4
+ * Digests as senders actually write them.
5
+ *
6
+ * A provider sends the same bytes in one of a few dressings — bare hex, base64, `sha256=<hex>` (GitHub), or a
7
+ * comma-separated list of `k=v` pairs where one value is the signature (Stripe). Accepting all of them is what
8
+ * lets one declarative verification cover real webhooks instead of only the tidy ones.
9
+ */
10
+ var candidates = (header) => {
11
+ const parts = [header.trim()];
12
+ for (const segment of header.split(",")) {
13
+ const [, value] = segment.split("=", 2);
14
+ if (value) parts.push(value.trim());
15
+ }
16
+ return parts;
17
+ };
18
+ /** Constant time, and only for equal-length values: a length comparison leaks nothing an attacker cannot measure
19
+ * from the encoding anyway, while `timingSafeEqual` throws outright on a mismatch. */
20
+ var matches = (expected, received) => {
21
+ const a = Buffer.from(expected);
22
+ const b = Buffer.from(received);
23
+ return a.length === b.length && timingSafeEqual(a, b);
24
+ };
25
+ /**
26
+ * Checks an inbound webhook signature against the RAW body.
27
+ *
28
+ * Raw, before parsing, because a signature covers bytes: `JSON.parse` then `JSON.stringify` reorders keys and
29
+ * drops whitespace, and the digest of the result is not the digest the sender computed. That is the single most
30
+ * common way a webhook verification is written wrong, and it fails closed — which is why it is worth naming here
31
+ * rather than trusting every caller to remember.
32
+ */
33
+ var verifySignature = (verification, secret, headers, rawBody) => {
34
+ const raw = headers[verification.header.toLowerCase()];
35
+ const signature = Array.isArray(raw) ? raw[0] : raw;
36
+ if (!signature) return {
37
+ ok: false,
38
+ reason: `Missing "${verification.header}" signature`
39
+ };
40
+ if (!secret) return {
41
+ ok: false,
42
+ reason: "Signing secret is not available"
43
+ };
44
+ let signed = rawBody;
45
+ if (verification.timestampHeader) {
46
+ const rawStamp = headers[verification.timestampHeader.toLowerCase()];
47
+ const stamp = Array.isArray(rawStamp) ? rawStamp[0] : rawStamp;
48
+ if (!stamp) return {
49
+ ok: false,
50
+ reason: `Missing "${verification.timestampHeader}" timestamp`
51
+ };
52
+ const sentAt = Number.parseInt(stamp, 10);
53
+ if (!Number.isFinite(sentAt)) return {
54
+ ok: false,
55
+ reason: "Timestamp is not a number"
56
+ };
57
+ const tolerance = verification.toleranceSeconds;
58
+ if (tolerance !== void 0 && Math.abs(Date.now() / 1e3 - sentAt) > tolerance) return {
59
+ ok: false,
60
+ reason: "Signature is outside its tolerance window"
61
+ };
62
+ signed = `${stamp}.${rawBody}`;
63
+ }
64
+ const hex = createHmac(verification.algorithm, secret).update(signed).digest("hex");
65
+ const base64 = createHmac(verification.algorithm, secret).update(signed).digest("base64");
66
+ return candidates(signature).some((value) => matches(hex, value) || matches(base64, value)) ? { ok: true } : {
67
+ ok: false,
68
+ reason: "Signature does not match"
69
+ };
70
+ };
71
+ //#endregion
72
+ export { verifySignature };
@@ -0,0 +1,25 @@
1
+ import { ActionsModule } from '../index';
2
+ import { SSRPageServerConfig, SSRRequest, SSRResponseHelpers } from '@plitzi/sdk-shared';
3
+ export type ActionWebhookDeps = {
4
+ req: SSRRequest;
5
+ res: SSRResponseHelpers;
6
+ config: SSRPageServerConfig;
7
+ module: ActionsModule;
8
+ signal: AbortSignal;
9
+ actionId: string;
10
+ /** The caller's address: a webhook has no session, so this is all there is to rate limit by. */
11
+ callerId: string;
12
+ lineage: string[];
13
+ };
14
+ /**
15
+ * Handles an inbound webhook.
16
+ *
17
+ * The public face of an action, and the one an attacker can reach without a session — so everything that costs
18
+ * anything happens after the signature is checked, in this order: the action must exist and declare the trigger,
19
+ * the rate limit must allow it, the signature must verify, and only then does a run start.
20
+ *
21
+ * The answer is deliberately thin. A sender needs to know it was accepted; it has no business learning whether an
22
+ * action exists, why a run failed, or what the flow did — and a 404 that distinguishes "no such action" from
23
+ * "wrong signature" is an oracle for both.
24
+ */
25
+ export declare const handleActionWebhook: (deps: ActionWebhookDeps) => Promise<void>;