@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,206 @@
1
+ import { ActionRunError } from "../runtime/errors.js";
2
+ import { findTriggerNode, triggerParams } from "../runtime/triggers.js";
3
+ import { precheckRun } from "../runtime/precheck.js";
4
+ import onAbort from "../../../helpers/onAbort.js";
5
+ import { reportReject } from "../runtime/report.js";
6
+ import { verifySignature } from "./verifySignature.js";
7
+ import { triggerHasStaleVerify, triggerVerify } from "@plitzi/sdk-shared/actions";
8
+ //#region src/modules/actions/transport/webhookHandler.ts
9
+ var DEFAULT_PER_MINUTE = 60;
10
+ var send = (res, status, payload) => {
11
+ res.setStatus(status);
12
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
13
+ res.setHeader("Cache-Control", "no-store");
14
+ res.send(JSON.stringify(payload));
15
+ };
16
+ /**
17
+ * The signing secret, from the credential the verification NAMES.
18
+ *
19
+ * Named outright rather than templated. This runs before anything else does — before the body is parsed, before a
20
+ * run exists — so there is no flow scope for a token to resolve against, and one that rendered to nothing would
21
+ * leave the endpoint verifying every request against an empty secret.
22
+ */
23
+ var resolveSecret = async (verification, spaceId, getCredential) => {
24
+ const secret = (await getCredential?.(spaceId, verification.credential))?.[verification.secretField ?? "secret"];
25
+ return typeof secret === "string" ? secret : "";
26
+ };
27
+ /**
28
+ * Handles an inbound webhook.
29
+ *
30
+ * The public face of an action, and the one an attacker can reach without a session — so everything that costs
31
+ * anything happens after the signature is checked, in this order: the action must exist and declare the trigger,
32
+ * the rate limit must allow it, the signature must verify, and only then does a run start.
33
+ *
34
+ * The answer is deliberately thin. A sender needs to know it was accepted; it has no business learning whether an
35
+ * action exists, why a run failed, or what the flow did — and a 404 that distinguishes "no such action" from
36
+ * "wrong signature" is an oracle for both.
37
+ */
38
+ var handleActionWebhook = async (deps) => {
39
+ const { req, res, config, module, signal, actionId, callerId, lineage } = deps;
40
+ const { environment = "main", spaceId, revision = 0 } = req.ctx.spaceDeployment ?? {};
41
+ if (typeof spaceId !== "number") {
42
+ send(res, 404, { error: "Not found" });
43
+ return;
44
+ }
45
+ /**
46
+ * Every way this ends without a run, told to whoever configured the endpoint.
47
+ *
48
+ * A rejected webhook is the single most common way an integration is broken, and until this existed it was
49
+ * indistinguishable from the sender never firing: the reason went to the process log, where the person setting
50
+ * up the integration cannot see it. The SENDER still learns nothing it should not — that answer is unchanged.
51
+ */
52
+ const reject = (reason, detail) => reportReject(config, {
53
+ actionId,
54
+ spaceId,
55
+ environment,
56
+ trigger: "webhook",
57
+ reason,
58
+ callerId,
59
+ ...detail === void 0 ? {} : { detail }
60
+ });
61
+ const entry = await config.action?.lookups?.getAction(spaceId, actionId);
62
+ const trigger = entry ? findTriggerNode(entry.document.nodes, "webhook") : void 0;
63
+ if (!entry || !trigger) {
64
+ await reject("not_found", entry ? "The action declares no webhook trigger" : "No action with this identifier");
65
+ send(res, 404, { error: "Not found" });
66
+ return;
67
+ }
68
+ const stepParams = triggerParams(trigger);
69
+ const verify = triggerVerify(stepParams);
70
+ /**
71
+ * A verification nothing reads is not an unsigned webhook — it is a broken one, and it is refused.
72
+ *
73
+ * The check used to be a JSON blob and is now fields on the step. A document stored before that carries the
74
+ * blob and no signing credential, and reading it as "unsigned" would turn a protected endpoint into a public
75
+ * one without a single line of it changing. Fail closed, and say which document to fix.
76
+ */
77
+ if (triggerHasStaleVerify(stepParams)) {
78
+ console.error(`[Actions] webhook "${actionId}" in space ${spaceId} carries a signature check in a format nothing reads any more, so it is refused. Name the signing credential on the trigger step.`);
79
+ await reject("unverifiable", "The signature check is in a format nothing reads any more; name the signing credential on the trigger step");
80
+ send(res, 503, { error: "Unavailable" });
81
+ return;
82
+ }
83
+ const perMinute = config.action?.rateLimit?.webhookPerMinute ?? DEFAULT_PER_MINUTE;
84
+ const window = Math.floor(Date.now() / 6e4);
85
+ try {
86
+ if (await module.kv(spaceId).increment(`hook:${actionId}:${callerId}:${window}`, 1, 120) > perMinute) {
87
+ res.setHeader("Retry-After", "60");
88
+ await reject("rate_limited", `More than ${perMinute} deliveries in one minute from this caller`);
89
+ send(res, 429, { error: "Too many requests" });
90
+ return;
91
+ }
92
+ } catch {
93
+ await reject("unverifiable", "The rate-limit store could not answer, so the endpoint failed closed");
94
+ send(res, 503, { error: "Unavailable" });
95
+ return;
96
+ }
97
+ const rawBody = req.body ?? "";
98
+ if (verify) {
99
+ const secret = await resolveSecret(verify, spaceId, config.action?.lookups?.getCredential);
100
+ const check = verifySignature(verify, secret, req.headers, rawBody);
101
+ if (!check.ok) {
102
+ console.warn(`[Actions] webhook "${actionId}" rejected: ${check.reason}`);
103
+ await reject("invalid_signature", check.reason);
104
+ send(res, 401, { error: "Invalid signature" });
105
+ return;
106
+ }
107
+ }
108
+ let payload = {};
109
+ try {
110
+ const parsed = rawBody ? JSON.parse(rawBody) : {};
111
+ payload = parsed !== null && typeof parsed === "object" ? parsed : {};
112
+ } catch {
113
+ await reject("malformed_body", "The body is not JSON");
114
+ send(res, 400, { error: "Body is not JSON" });
115
+ return;
116
+ }
117
+ const input = {
118
+ ...payload,
119
+ payload
120
+ };
121
+ const limits = module.limitsFor(entry.document);
122
+ const begin = {
123
+ spaceId,
124
+ actionId: entry.id,
125
+ callerId,
126
+ input,
127
+ idempotencyKey: deliveryId(req),
128
+ sharedKey: true,
129
+ ttlMs: limits.timeoutMs
130
+ };
131
+ const replayed = await module.guards.replay(begin);
132
+ if (replayed) {
133
+ send(res, 202, {
134
+ accepted: true,
135
+ runId: replayed.runId,
136
+ status: replayed.status,
137
+ replayed: true
138
+ });
139
+ return;
140
+ }
141
+ let run;
142
+ try {
143
+ precheckRun(entry, {
144
+ trigger: "webhook",
145
+ input,
146
+ lineage
147
+ });
148
+ run = await module.guards.begin(begin);
149
+ } catch (error) {
150
+ const reason = error instanceof ActionRunError ? error.reason : "failed";
151
+ await reject(reason, error instanceof Error ? error.message : void 0);
152
+ send(res, reason === "duplicate" ? 202 : 400, { accepted: reason === "duplicate" });
153
+ return;
154
+ }
155
+ await config.adapters.meter?.({
156
+ kind: "server_action",
157
+ cached: false,
158
+ req,
159
+ spaceId,
160
+ environment,
161
+ revision
162
+ });
163
+ const abortRun = () => run.controller.abort();
164
+ const releaseAbort = onAbort(signal, abortRun);
165
+ let outcome;
166
+ try {
167
+ const result = await module.runAction({
168
+ entry,
169
+ input,
170
+ spaceId,
171
+ environment,
172
+ trigger: "webhook",
173
+ callerId,
174
+ runId: run.runId,
175
+ lineage,
176
+ signal: run.controller.signal
177
+ });
178
+ outcome = result;
179
+ send(res, 200, {
180
+ accepted: true,
181
+ runId: result.runId,
182
+ status: result.status
183
+ });
184
+ } catch (error) {
185
+ console.error("[Actions] webhook run failed:", error);
186
+ send(res, 500, { accepted: false });
187
+ } finally {
188
+ releaseAbort();
189
+ await module.guards.end(run, outcome);
190
+ }
191
+ };
192
+ /** The delivery id a provider sends, under the header names the common ones use. */
193
+ var deliveryId = (req) => {
194
+ for (const name of [
195
+ "x-plitzi-delivery",
196
+ "x-github-delivery",
197
+ "x-request-id",
198
+ "idempotency-key"
199
+ ]) {
200
+ const value = req.headers[name];
201
+ const single = Array.isArray(value) ? value[0] : value;
202
+ if (single) return single;
203
+ }
204
+ };
205
+ //#endregion
206
+ export { handleActionWebhook };
@@ -0,0 +1,234 @@
1
+ import { ActionEntry, ActionLimits, ActionRejectRecord, ActionRunRecord, ActionRunStatus, ActionTriggerType, ConnectorManifest, Environment, InteractionCallbackParam, InteractionNode, InteractionNodeStatus, SpaceRevision, SSRUser } from '@plitzi/sdk-shared';
2
+ /** Resolved secret material, as the connector engine already models it. */
3
+ export type ActionCredential = Record<string, string>;
4
+ /**
5
+ * A connector with its secret already resolved.
6
+ *
7
+ * Handed over as one thing because the engine needs both, and a task holding only the manifest would have to go
8
+ * looking for the credential itself — which is exactly the lookup that must not be open to a task. Authorizing
9
+ * the connector authorizes the credential it names: that is what the `/_action` write endpoint has always done.
10
+ */
11
+ export type ResolvedConnector = {
12
+ manifest: ConnectorManifest;
13
+ credential?: ActionCredential;
14
+ };
15
+ /**
16
+ * A connection to a database that is NOT this deployment's.
17
+ *
18
+ * Registered by the deployment, one per engine it supports, and reached only through a credential the space
19
+ * declared — which is the whole security position of `db.query`: a flow queries a database its owner configured,
20
+ * never the one holding other tenants' spaces.
21
+ *
22
+ * `params` are BOUND, never interpolated. The task refuses a statement containing a template for exactly that
23
+ * reason, and a driver that pastes them into the SQL itself would undo the rule from the other end.
24
+ */
25
+ export type ActionDbDriver = {
26
+ /** Engine name a credential names, e.g. `mysql`, `postgres`. */
27
+ engine: string;
28
+ query: (dsn: string, sql: string, params: unknown[], signal: AbortSignal) => Promise<unknown[]>;
29
+ };
30
+ /**
31
+ * Where a deployment keeps the `kv` tasks' data — Redis, Memcached, a table, whatever it already runs.
32
+ *
33
+ * Deliberately the DUMBEST possible surface: five operations over strings, with no rule to obey. Everything that
34
+ * decides how a counter behaves — the key prefixing, the JSON round trip, and the one rule a rate limit lives or
35
+ * dies by — is `createKvStore`'s, above this. An adapter that had to remember "extend the TTL only when the
36
+ * counter did not exist" would be an adapter each deployment gets to write that rule wrongly in.
37
+ *
38
+ * Making it a seam at all is what keeps rate limiting and idempotency across replicas a deployment's decision
39
+ * rather than a silent no-op: the in-process default counts only its own replica, and honestly says so.
40
+ */
41
+ export type ActionKvAdapter = {
42
+ /** The stored string, or undefined when the key is absent OR has expired. */
43
+ get: (key: string) => Promise<string | undefined>;
44
+ set: (key: string, value: string, ttlSeconds?: number) => Promise<void>;
45
+ delete: (key: string) => Promise<void>;
46
+ /** Adds to a counter and answers the NEW total. Must be atomic — this is what get-then-set cannot be. */
47
+ increment: (key: string, amount: number) => Promise<number>;
48
+ /** Sets a lifetime on a key that already exists. When to call it is decided above, never here. */
49
+ expire: (key: string, ttlSeconds: number) => Promise<void>;
50
+ };
51
+ /**
52
+ * The `kv` tasks' own view: values rather than strings, and a TTL that means what a caller expects.
53
+ *
54
+ * Built by `createKvStore` over an {@link ActionKvAdapter}, and then narrowed to one space by `namespaceKv`. A
55
+ * task never sees an adapter.
56
+ */
57
+ export type ActionKvStore = {
58
+ get: (key: string) => Promise<unknown>;
59
+ set: (key: string, value: unknown, ttlSeconds?: number) => Promise<void>;
60
+ delete: (key: string) => Promise<void>;
61
+ increment: (key: string, amount: number, ttlSeconds?: number) => Promise<number>;
62
+ };
63
+ /** Re-exported so the module's files import one place. One type for actions and connectors: it is one concept. */
64
+ export type { SpaceRevision } from '@plitzi/sdk-shared';
65
+ /**
66
+ * How the server reaches a space's actions. Structurally identical in spirit to `ConnectorLookups`: the module
67
+ * never learns where a deployment stores anything.
68
+ */
69
+ export type ActionLookups = {
70
+ getAction: (spaceId: number, actionId: string, at?: SpaceRevision) => Promise<ActionEntry | undefined>;
71
+ /** Only the builder's catalog, the MCP and the scheduler need the list; a page call never asks for it. */
72
+ listActions?: (spaceId: number, at?: SpaceRevision) => Promise<ActionEntry[]>;
73
+ getCredential?: (spaceId: number, identifier: string) => Promise<ActionCredential | undefined>;
74
+ getConnector?: (spaceId: number, connectorId: string, at?: SpaceRevision) => Promise<ConnectorManifest | undefined>;
75
+ };
76
+ /**
77
+ * The only door a task has to the outside world.
78
+ *
79
+ * `credential` and `connector` answer solely for identifiers the running document declared, so a task cannot reach
80
+ * a secret the action did not ask for — the check lives here rather than in each task, where it would be optional.
81
+ * `fetch` is the run's own: abort-wired and counted against the request budget.
82
+ */
83
+ export type ActionTaskContext = {
84
+ runId: string;
85
+ spaceId: number;
86
+ environment: Environment;
87
+ trigger: ActionTriggerType;
88
+ user?: SSRUser;
89
+ /**
90
+ * Who is asking, as the transport identified them: `user:<id>` for a session, `ip:<address>` for everyone else,
91
+ * and `render` or `schedule` for the runs no visitor started.
92
+ *
93
+ * A task that must happen once per PERSON has nothing else to key on — a `public` action has no session, and an
94
+ * identifier the caller sent is not an identity. Coarse by construction, since a shared address is one caller,
95
+ * and personal data when it is one: a task that stores it is storing that.
96
+ */
97
+ callerId: string;
98
+ signal: AbortSignal;
99
+ /**
100
+ * The flow scope as the current node sees it: `input`, every previous node's result by id, `user`, and the
101
+ * credentials the document declared. Read-only, and present because a `rawParams` task has to render templates
102
+ * against something.
103
+ */
104
+ scope: Readonly<Record<string, unknown>>;
105
+ credential: (identifier: string) => Promise<ActionCredential | undefined>;
106
+ connector: (connectorId: string) => Promise<ResolvedConnector | undefined>;
107
+ fetch: typeof fetch;
108
+ /** Key/value storage, namespaced to this space by the runner — a key one space writes is a key only it reads. */
109
+ kv: ActionKvStore;
110
+ /** The database engines this deployment registered, for the `db.query` task. */
111
+ dbDrivers: ActionDbDriver[];
112
+ /** Pushes a `data` frame to a streaming caller. A no-op when nobody negotiated a stream. */
113
+ emit: (chunk: unknown) => void;
114
+ };
115
+ /**
116
+ * One step a server action can take.
117
+ *
118
+ * `params` is the same `InteractionCallbackParam` map the builder's WorkflowNode already renders, which is what
119
+ * lets a deployment's own task appear in its editor without a single change there.
120
+ */
121
+ export type ActionTask<T extends Record<string, unknown> = Record<string, unknown>> = {
122
+ /** Addressed in a node as `<namespace>.<action>`. */
123
+ namespace: string;
124
+ action: string;
125
+ title: string;
126
+ description?: string;
127
+ params: Record<keyof T, InteractionCallbackParam<T>>;
128
+ /**
129
+ * Receive params BEFORE twig resolution. Only for a task whose whole job is to render a template — resolving
130
+ * first would consume the very tokens it exists to interpret. The client engine special-cases `twigTemplate` by
131
+ * name for this; a flag is the same rule without the name check.
132
+ */
133
+ rawParams?: boolean;
134
+ run: (params: T, ctx: ActionTaskContext) => unknown;
135
+ };
136
+ /** A task with its addressable name resolved, as the registry stores and lists it. */
137
+ export type RegisteredTask = ActionTask<Record<string, unknown>> & {
138
+ name: string;
139
+ };
140
+ export type ActionTaskRegistry = {
141
+ get: (name: string) => RegisteredTask | undefined;
142
+ list: () => RegisteredTask[];
143
+ };
144
+ /** What a deployment hands to `createServer` under `actions`. Absent → the module is never constructed. */
145
+ export type ActionsConfig = {
146
+ lookups: ActionLookups;
147
+ /** Deployment-owned tasks, validated at boot against the built-ins. */
148
+ tasks?: ActionTask<never>[];
149
+ /** Ceilings a per-action document may tighten but never exceed. */
150
+ limits?: ActionLimits;
151
+ /** How many runs may be in flight at once. Counted per space and for the process as a whole. */
152
+ concurrency?: {
153
+ perSpace?: number;
154
+ perProcess?: number;
155
+ renderPerProcess?: number;
156
+ };
157
+ /** Where the `kv` tasks keep things. Omitted → an in-process Map, which is per-replica by definition. */
158
+ kv?: ActionKvAdapter;
159
+ /** Inbound webhooks are public, so they are counted per caller per minute. Default 60. */
160
+ rateLimit?: {
161
+ webhookPerMinute?: number;
162
+ };
163
+ /** Database engines this deployment lets a flow reach. Empty → the `db.query` task is not offered at all. */
164
+ dbDrivers?: ActionDbDriver[];
165
+ /**
166
+ * Called once per run that started, for a deployment that keeps a record.
167
+ *
168
+ * Best-effort by contract: it is awaited but never allowed to fail a run — a logging outage must not take an
169
+ * action down, which is the same rule metering follows.
170
+ */
171
+ onRun?: (record: ActionRunRecord) => void | Promise<void>;
172
+ /**
173
+ * Called once per request that was REFUSED before it became a run.
174
+ *
175
+ * Its own hook rather than a status on `onRun`, because the two answer different questions and a deployment
176
+ * wants them in different places: runs are history, refusals are a fault report. Without it a webhook rejected
177
+ * for a bad signature is invisible to everyone but whoever reads the process log — which is nobody, on the
178
+ * afternoon the integration is being set up.
179
+ *
180
+ * Everything is reported, including the refusals that are somebody else retrying politely. Which of them are
181
+ * worth keeping is a policy the deployment owns; the mechanism does not decide it by staying quiet.
182
+ */
183
+ onReject?: (record: ActionRejectRecord) => void | Promise<void>;
184
+ /**
185
+ * How long a COMPLETED run's answer may be replayed to a caller that asks for it again by the same key.
186
+ *
187
+ * Off by default, and it only ever applies to an explicit key — an `idempotencyKey` on the call, or the
188
+ * delivery id a webhook sender stamps. A derived key is a hash of the input, and two identical calls a minute
189
+ * apart are usually two things somebody wants to happen twice.
190
+ *
191
+ * Needs a shared {@link ActionKvAdapter} to mean anything across replicas, exactly as single-flight does.
192
+ */
193
+ idempotency?: {
194
+ replayTtlMs?: number;
195
+ };
196
+ fetchImpl?: typeof fetch;
197
+ };
198
+ /** Re-exported so the module's own files import one place, and a deployment writing an `onRun` or an `onReject`
199
+ * sees the same shapes the module emits. */
200
+ export type { ActionRejectRecord, ActionRunRecord } from '@plitzi/sdk-shared';
201
+ export type ResolvedActionLimits = Required<ActionLimits>;
202
+ export type ActionRunRequest = {
203
+ entry: ActionEntry;
204
+ input: Record<string, unknown>;
205
+ spaceId: number;
206
+ environment: Environment;
207
+ trigger: ActionTriggerType;
208
+ user?: SSRUser;
209
+ /** Who is asking. Stated by whatever started the run — see {@link ActionTaskContext.callerId}. */
210
+ callerId: string;
211
+ runId: string;
212
+ /** Chain of run ids that caused this one; a run naming its own action is refused before its first node. */
213
+ lineage?: string[];
214
+ /**
215
+ * The published version this run belongs to, when a page started it.
216
+ *
217
+ * The action was read as of this revision, so the connectors it calls are read as of it too — a flow and the
218
+ * manifests it goes through are one published thing, and mixing versions inside one run is the discrepancy in
219
+ * miniature.
220
+ */
221
+ at?: SpaceRevision;
222
+ emit?: (chunk: unknown) => void;
223
+ /** Reports each step as it settles, for a caller watching the run happen. Absent for a plain request/response. */
224
+ onNode?: (id: string, status: InteractionNodeStatus) => void;
225
+ signal?: AbortSignal;
226
+ };
227
+ export type ActionRunResult = {
228
+ runId: string;
229
+ status: ActionRunStatus;
230
+ /** Projected down to the document's declared `output`. Never the raw flow scope. */
231
+ output: Record<string, unknown>;
232
+ /** The same shape the dev-tools Interactions panel renders. Redacted before it leaves the process. */
233
+ trace: InteractionNode[];
234
+ };
@@ -1,6 +1,7 @@
1
1
  import { getByPath } from "./getByPath.js";
2
- import { BODYLESS_METHODS, DEFAULT_READ_ENDPOINT, EMPTY_RESPONSE_METHODS } from "@plitzi/sdk-shared/connectors";
2
+ import { assertOutboundAllowed } from "../../helpers/outboundGuard.js";
3
3
  import { processTwig } from "@plitzi/sdk-shared/helpers/twigWrapper";
4
+ import { BODYLESS_METHODS, DEFAULT_READ_ENDPOINT, EMPTY_RESPONSE_METHODS } from "@plitzi/sdk-shared/connectors";
4
5
  //#region src/modules/connectors/engine.ts
5
6
  var DEFAULT_LIMIT = 10;
6
7
  /** Only scalars can become a URL fragment, a header or a record key. Anything else is treated as absent. */
@@ -149,6 +150,7 @@ var fetchConnectorRecords = async ({ manifest, endpoint = DEFAULT_READ_ENDPOINT,
149
150
  ...renderEntries(read.query ?? {}, variables),
150
151
  ...renderFilters(resolved, connection.operators, variables)
151
152
  }).forEach(([key, value]) => url.searchParams.set(key, value));
153
+ await assertOutboundAllowed(url);
152
154
  const method = read.method ?? "GET";
153
155
  const headers = {
154
156
  ...applyAuth(connection, variables, url),
@@ -200,6 +202,7 @@ var writeConnectorRecord = async ({ manifest, credential = {}, action, resource,
200
202
  };
201
203
  const url = new URL(render(operation.path, variables), connection.baseUrl);
202
204
  Object.entries(renderEntries(operation.query ?? {}, variables)).forEach(([key, value]) => url.searchParams.set(key, value));
205
+ await assertOutboundAllowed(url);
203
206
  const headers = {
204
207
  ...applyAuth(connection, variables, url),
205
208
  ...renderEntries(operation.headers ?? {}, variables)
@@ -1,8 +1,16 @@
1
1
  import { ConnectorCredential, ConnectorManifest } from './types';
2
2
  import { RscElementResolver } from '../rsc/resolveRscData';
3
+ import { SpaceRevision } from '@plitzi/sdk-shared';
3
4
  export type ConnectorLookups = {
4
- /** Reads a manifest by id. Server-side state: manifests name endpoints and must never reach the browser. */
5
- getConnector: (spaceId: number, connectorId: string) => Promise<ConnectorManifest | undefined>;
5
+ /**
6
+ * Reads a manifest by id, as of a published revision. Server-side state: manifests name endpoints and must never
7
+ * reach the browser.
8
+ *
9
+ * `at` absent means the live document, the one the builder edits. A page passes the revision it was published
10
+ * at, so it reads through the manifest it shipped with rather than whatever it says after somebody points that
11
+ * connector at a different API.
12
+ */
13
+ getConnector: (spaceId: number, connectorId: string, at?: SpaceRevision) => Promise<ConnectorManifest | undefined>;
6
14
  /** Resolves the secret referenced by `manifest.credential`. */
7
15
  getCredential?: (spaceId: number, identifier: string) => Promise<ConnectorCredential | undefined>;
8
16
  fetchImpl?: typeof fetch;
@@ -28,11 +28,16 @@ var toPage = (queryParams, attributes) => {
28
28
  * An element without a connector resolves to nothing rather than failing — an unconfigured provider on a page is a
29
29
  * draft, not an error.
30
30
  */
31
- var createConnectorResolver = ({ getConnector, getCredential, fetchImpl }) => async ({ element, flat, routeParams, queryParams, spaceId }) => {
31
+ var createConnectorResolver = ({ getConnector, getCredential, fetchImpl }) => async ({ element, flat, routeParams, queryParams, spaceId, environment, req }) => {
32
+ const deployment = req.ctx.spaceDeployment;
33
+ const at = {
34
+ environment: deployment?.environment ?? environment,
35
+ revision: deployment?.revision ?? 0
36
+ };
32
37
  const attributes = element.attributes;
33
38
  const { connector: connectorId, endpoint, resource, limit, singleRecord = false, filters } = attributes;
34
39
  if (!connectorId) return;
35
- const manifest = await getConnector(spaceId, connectorId);
40
+ const manifest = await getConnector(spaceId, connectorId, at);
36
41
  if (!manifest) throw new Error(`Connector "${connectorId}" is not configured for space ${spaceId}`);
37
42
  const credential = manifest.credential && getCredential ? await getCredential(spaceId, manifest.credential) : void 0;
38
43
  const pageSize = toLimit(limit, singleRecord);
@@ -62,7 +67,7 @@ var createConnectorResolver = ({ getConnector, getCredential, fetchImpl }) => as
62
67
  errorMessage: ""
63
68
  };
64
69
  if (manifest.projection === "full") return slice;
65
- return projectSlice(slice, collectBoundPaths(flat, element.id, getSourceName(element.definition.type, element)));
70
+ return projectSlice(slice, collectBoundPaths(flat, element.id, getSourceName(element.definition.type, element.id)));
66
71
  };
67
72
  //#endregion
68
73
  export { createConnectorResolver };
@@ -0,0 +1,5 @@
1
+ import { Tables } from './config';
2
+ import { Queryable } from './query';
3
+ import { AccountAdapters } from '../../core/auth/api';
4
+ import { IdentityAdapters } from '../../core/auth/identity';
5
+ export declare const createAccountStore: (db: Queryable, t: Tables) => IdentityAdapters & AccountAdapters;