@plitzi/sdk-server 0.33.2 → 0.34.1
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.
- package/CHANGELOG.md +54 -0
- package/README.md +283 -8
- package/dist/actions.d.ts +62 -0
- package/dist/actions.js +12 -0
- package/dist/adapters/cloudAdapters.d.ts +72 -0
- package/dist/adapters/cloudAdapters.js +303 -0
- package/dist/adapters/space/index.d.ts +93 -0
- package/dist/adapters/space/index.js +122 -0
- package/dist/adapters/space/resolvers.d.ts +69 -0
- package/dist/adapters/space/resolvers.js +79 -0
- package/dist/adapters/space/space.test.d.ts +1 -0
- package/dist/adapters/space/types.d.ts +64 -0
- package/dist/adapters/space/types.js +41 -0
- package/dist/auth.d.ts +50 -0
- package/dist/auth.js +19 -0
- package/dist/core/auth/api.d.ts +400 -11
- package/dist/core/auth/api.js +1044 -50
- package/dist/core/auth/createAuth.d.ts +135 -30
- package/dist/core/auth/createAuth.js +96 -24
- package/dist/core/auth/credentials.d.ts +9 -3
- package/dist/core/auth/credentials.js +10 -4
- package/dist/core/auth/csrf.d.ts +89 -0
- package/dist/core/auth/csrf.js +234 -0
- package/dist/core/auth/csrf.test.d.ts +1 -0
- package/dist/core/auth/identity.d.ts +6 -0
- package/dist/core/auth/identity.js +37 -8
- package/dist/core/auth/lifecycle.test.d.ts +1 -0
- package/dist/core/auth/oauth/index.d.ts +8 -0
- package/dist/core/auth/oauth/index.js +13 -15
- package/dist/core/auth/passwords.d.ts +15 -0
- package/dist/core/auth/passwords.js +32 -0
- package/dist/core/auth/redirects.d.ts +18 -0
- package/dist/core/auth/redirects.js +31 -0
- package/dist/core/auth/redirects.test.d.ts +1 -0
- package/dist/core/auth/routes.d.ts +4 -2
- package/dist/core/auth/routes.js +234 -9
- package/dist/core/auth/session.d.ts +11 -0
- package/dist/core/auth/session.js +1 -1
- package/dist/core/auth/spaceTokens.d.ts +38 -2
- package/dist/core/auth/spaceTokens.js +54 -2
- package/dist/core/auth/throttle.d.ts +5 -0
- package/dist/core/auth/throttle.js +81 -0
- package/dist/core/auth/throttle.test.d.ts +1 -0
- package/dist/core/auth/tokens.d.ts +83 -12
- package/dist/core/auth/tokens.js +51 -9
- package/dist/core/auth/totp.d.ts +48 -0
- package/dist/core/auth/totp.js +118 -0
- package/dist/core/auth/totp.test.d.ts +1 -0
- package/dist/core/createServer.d.ts +0 -6
- package/dist/core/createServer.js +35 -3
- package/dist/core/fontStore.d.ts +33 -0
- package/dist/core/fontStore.js +53 -0
- package/dist/core/fontStore.test.d.ts +1 -0
- package/dist/core/handlers/authMiddleware.js +1 -1
- package/dist/core/handlers/authRouteHandlers.d.ts +9 -1
- package/dist/core/handlers/authRouteHandlers.js +4 -3
- package/dist/core/handlers/csrfMiddleware.d.ts +20 -0
- package/dist/core/handlers/csrfMiddleware.js +32 -0
- package/dist/core/handlers/oauthRouteHandlers.d.ts +46 -0
- package/dist/core/handlers/oauthRouteHandlers.js +159 -0
- package/dist/core/handlers/socialRouteHandlers.d.ts +40 -0
- package/dist/core/handlers/socialRouteHandlers.js +129 -0
- package/dist/core/handlers/socialRouteHandlers.test.d.ts +1 -0
- package/dist/core/handlers/types.d.ts +22 -5
- package/dist/core/health.d.ts +11 -1
- package/dist/core/health.js +27 -3
- package/dist/core/http/dispatcher.d.ts +1 -1
- package/dist/core/http/dispatcher.js +20 -1
- package/dist/core/http/stages/authApi.js +1 -1
- package/dist/core/http/stages/fontAssets.d.ts +9 -0
- package/dist/core/http/stages/fontAssets.js +27 -0
- package/dist/core/http/stages/oauth.d.ts +19 -0
- package/dist/core/http/stages/oauth.js +118 -0
- package/dist/core/http/types.d.ts +12 -0
- package/dist/core/oauth/authorize.d.ts +22 -0
- package/dist/core/oauth/authorize.js +254 -0
- package/dist/core/oauth/authorize.test.d.ts +1 -0
- package/dist/core/oauth/challenge.d.ts +11 -0
- package/dist/core/oauth/challenge.js +33 -0
- package/dist/core/oauth/consentPage.d.ts +12 -0
- package/dist/core/oauth/consentPage.js +131 -0
- package/dist/core/oauth/metadata.d.ts +41 -0
- package/dist/core/oauth/metadata.js +71 -0
- package/dist/core/oauth/params.d.ts +5 -0
- package/dist/core/oauth/params.js +5 -0
- package/dist/core/oauth/pkce.d.ts +6 -0
- package/dist/core/oauth/pkce.js +15 -0
- package/dist/core/oauth/records.d.ts +72 -0
- package/dist/core/oauth/records.js +49 -0
- package/dist/core/oauth/register.d.ts +5 -0
- package/dist/core/oauth/register.js +55 -0
- package/dist/core/oauth/respond.d.ts +21 -0
- package/dist/core/oauth/respond.js +59 -0
- package/dist/core/oauth/token.d.ts +14 -0
- package/dist/core/oauth/token.js +130 -0
- package/dist/core/previewToken.d.ts +20 -0
- package/dist/core/previewToken.js +29 -1
- package/dist/core/server/pageServer.js +4 -1
- package/dist/core/services/action.d.ts +6 -4
- package/dist/core/services/action.js +103 -10
- package/dist/core/services/registry.js +2 -0
- package/dist/core/services/resolve.d.ts +8 -0
- package/dist/core/services/resolve.js +12 -1
- package/dist/core/staticFiles.js +1 -2
- package/dist/handlers.d.ts +7 -1
- package/dist/handlers.js +4 -1
- package/dist/helpers/buildResponseHelpers.d.ts +8 -0
- package/dist/helpers/buildResponseHelpers.js +9 -1
- package/dist/helpers/buildServerInfo.d.ts +8 -3
- package/dist/helpers/buildServerInfo.js +18 -14
- package/dist/helpers/cache/keys.d.ts +13 -0
- package/dist/helpers/cache/keys.js +12 -1
- package/dist/helpers/onAbort.d.ts +12 -0
- package/dist/helpers/onAbort.js +22 -0
- package/dist/helpers/onAbort.test.d.ts +1 -0
- package/dist/helpers/outboundGuard.d.ts +27 -0
- package/dist/helpers/outboundGuard.js +49 -0
- package/dist/helpers/outboundGuard.test.d.ts +1 -0
- package/dist/helpers/serverLog.d.ts +6 -3
- package/dist/helpers/serverLog.js +20 -3
- package/dist/index.d.ts +6 -0
- package/dist/index.js +9 -4
- package/dist/kernel.d.ts +5 -22
- package/dist/kernel.js +1 -14
- package/dist/modules/actions/{handler.js → connectorWrite.js} +5 -2
- package/dist/modules/actions/connectorWrite.test.d.ts +1 -0
- package/dist/modules/actions/index.d.ts +40 -0
- package/dist/modules/actions/index.js +46 -0
- package/dist/modules/actions/moduleFor.d.ts +3 -0
- package/dist/modules/actions/moduleFor.js +35 -0
- package/dist/modules/actions/moduleFor.test.d.ts +1 -0
- package/dist/modules/actions/runtime/check.d.ts +24 -0
- package/dist/modules/actions/runtime/check.js +111 -0
- package/dist/modules/actions/runtime/check.test.d.ts +1 -0
- package/dist/modules/actions/runtime/errors.d.ts +11 -0
- package/dist/modules/actions/runtime/errors.js +17 -0
- package/dist/modules/actions/runtime/guards.d.ts +126 -0
- package/dist/modules/actions/runtime/guards.js +246 -0
- package/dist/modules/actions/runtime/guards.test.d.ts +1 -0
- package/dist/modules/actions/runtime/kvStore.d.ts +27 -0
- package/dist/modules/actions/runtime/kvStore.js +41 -0
- package/dist/modules/actions/runtime/kvStore.test.d.ts +1 -0
- package/dist/modules/actions/runtime/limits.d.ts +11 -0
- package/dist/modules/actions/runtime/limits.js +35 -0
- package/dist/modules/actions/runtime/memoryKv.d.ts +2 -0
- package/dist/modules/actions/runtime/memoryKv.js +77 -0
- package/dist/modules/actions/runtime/namespaceKv.d.ts +9 -0
- package/dist/modules/actions/runtime/namespaceKv.js +19 -0
- package/dist/modules/actions/runtime/precheck.d.ts +27 -0
- package/dist/modules/actions/runtime/precheck.js +45 -0
- package/dist/modules/actions/runtime/renderResolver.d.ts +15 -0
- package/dist/modules/actions/runtime/renderResolver.js +130 -0
- package/dist/modules/actions/runtime/renderResolver.test.d.ts +1 -0
- package/dist/modules/actions/runtime/renderShare.d.ts +29 -0
- package/dist/modules/actions/runtime/renderShare.js +36 -0
- package/dist/modules/actions/runtime/renderShare.test.d.ts +1 -0
- package/dist/modules/actions/runtime/report.d.ts +14 -0
- package/dist/modules/actions/runtime/report.js +22 -0
- package/dist/modules/actions/runtime/runAction.d.ts +12 -0
- package/dist/modules/actions/runtime/runAction.js +405 -0
- package/dist/modules/actions/runtime/runAction.test.d.ts +1 -0
- package/dist/modules/actions/runtime/runLogger.d.ts +31 -0
- package/dist/modules/actions/runtime/runLogger.js +66 -0
- package/dist/modules/actions/runtime/runLogger.test.d.ts +1 -0
- package/dist/modules/actions/runtime/schedule.d.ts +35 -0
- package/dist/modules/actions/runtime/schedule.js +76 -0
- package/dist/modules/actions/runtime/schedule.test.d.ts +1 -0
- package/dist/modules/actions/runtime/scope.d.ts +40 -0
- package/dist/modules/actions/runtime/scope.js +103 -0
- package/dist/modules/actions/runtime/triggers.d.ts +12 -0
- package/dist/modules/actions/runtime/triggers.js +14 -0
- package/dist/modules/actions/taskCatalog.d.ts +26 -0
- package/dist/modules/actions/taskCatalog.js +24 -0
- package/dist/modules/actions/taskCatalog.test.d.ts +1 -0
- package/dist/modules/actions/tasks/auth.d.ts +2 -0
- package/dist/modules/actions/tasks/auth.js +40 -0
- package/dist/modules/actions/tasks/builtins.d.ts +8 -0
- package/dist/modules/actions/tasks/builtins.js +24 -0
- package/dist/modules/actions/tasks/connector.d.ts +2 -0
- package/dist/modules/actions/tasks/connector.js +141 -0
- package/dist/modules/actions/tasks/db.d.ts +2 -0
- package/dist/modules/actions/tasks/db.js +59 -0
- package/dist/modules/actions/tasks/db.test.d.ts +1 -0
- package/dist/modules/actions/tasks/flow.d.ts +3 -0
- package/dist/modules/actions/tasks/flow.js +109 -0
- package/dist/modules/actions/tasks/helpers.d.ts +16 -0
- package/dist/modules/actions/tasks/helpers.js +30 -0
- package/dist/modules/actions/tasks/http.d.ts +2 -0
- package/dist/modules/actions/tasks/http.js +111 -0
- package/dist/modules/actions/tasks/http.test.d.ts +1 -0
- package/dist/modules/actions/tasks/kv.d.ts +2 -0
- package/dist/modules/actions/tasks/kv.js +102 -0
- package/dist/modules/actions/tasks/kv.test.d.ts +1 -0
- package/dist/modules/actions/tasks/registry.d.ts +10 -0
- package/dist/modules/actions/tasks/registry.js +36 -0
- package/dist/modules/actions/tasks/transform.d.ts +2 -0
- package/dist/modules/actions/tasks/transform.js +34 -0
- package/dist/modules/actions/transport/callHandler.d.ts +24 -0
- package/dist/modules/actions/transport/callHandler.js +233 -0
- package/dist/modules/actions/transport/callHandler.test.d.ts +1 -0
- package/dist/modules/actions/transport/cancelHandler.d.ts +23 -0
- package/dist/modules/actions/transport/cancelHandler.js +23 -0
- package/dist/modules/actions/transport/catalogHandler.d.ts +18 -0
- package/dist/modules/actions/transport/catalogHandler.js +21 -0
- package/dist/modules/actions/transport/stream.d.ts +23 -0
- package/dist/modules/actions/transport/stream.js +100 -0
- package/dist/modules/actions/transport/verifySignature.d.ts +16 -0
- package/dist/modules/actions/transport/verifySignature.js +72 -0
- package/dist/modules/actions/transport/webhookHandler.d.ts +25 -0
- package/dist/modules/actions/transport/webhookHandler.js +206 -0
- package/dist/modules/actions/transport/webhookHandler.test.d.ts +1 -0
- package/dist/modules/actions/types.d.ts +234 -0
- package/dist/modules/connectors/engine.js +4 -1
- package/dist/modules/connectors/resolver.d.ts +10 -2
- package/dist/modules/connectors/resolver.js +8 -3
- package/dist/modules/mysql/accounts.d.ts +5 -0
- package/dist/modules/mysql/accounts.js +365 -0
- package/dist/modules/mysql/accounts.test.d.ts +1 -0
- package/dist/modules/mysql/admin.d.ts +58 -0
- package/dist/modules/mysql/admin.js +133 -0
- package/dist/modules/mysql/config.d.ts +82 -0
- package/dist/modules/mysql/config.js +30 -0
- package/dist/modules/mysql/identities.d.ts +35 -0
- package/dist/modules/mysql/identities.js +43 -0
- package/dist/modules/mysql/index.d.ts +67 -0
- package/dist/modules/mysql/index.js +62 -0
- package/dist/modules/mysql/pool.d.ts +3 -0
- package/dist/modules/mysql/pool.js +88 -0
- package/dist/modules/mysql/query.d.ts +12 -0
- package/dist/modules/mysql/query.js +21 -0
- package/dist/modules/mysql/schema.d.ts +43 -0
- package/dist/modules/mysql/schema.js +340 -0
- package/dist/modules/mysql/schema.test.d.ts +1 -0
- package/dist/modules/mysql/sessions.d.ts +33 -0
- package/dist/modules/mysql/sessions.js +46 -0
- package/dist/modules/mysql/spaceTokens.d.ts +11 -0
- package/dist/modules/mysql/spaceTokens.js +59 -0
- package/dist/modules/mysql/store.integration.test.d.ts +1 -0
- package/dist/modules/rsc/connectorRscData.d.ts +25 -0
- package/dist/modules/rsc/connectorRscData.js +51 -0
- package/dist/modules/rsc/connectorRscData.test.d.ts +1 -0
- package/dist/modules/rsc/handler.js +16 -3
- package/dist/modules/rsc/matchRscPage.d.ts +14 -0
- package/dist/modules/rsc/matchRscPage.js +26 -0
- package/dist/modules/rsc/resolveRscData.d.ts +14 -1
- package/dist/modules/rsc/resolveRscData.js +23 -34
- package/dist/modules/ssr/Component.d.ts +6 -2
- package/dist/modules/ssr/Component.js +3 -1
- package/dist/modules/ssr/draftSession.test.d.ts +1 -0
- package/dist/modules/ssr/handler.js +15 -2
- package/dist/modules/ssr/loadPluginComponents.js +22 -5
- package/dist/modules/ssr/prepareRender.js +87 -8
- package/dist/modules/ssr/prepareRender.test.d.ts +1 -0
- package/dist/modules/ssr/preview.d.ts +21 -8
- package/dist/modules/ssr/preview.js +38 -14
- package/dist/modules/ssr/registerExternalPlugins.js +2 -2
- package/dist/modules/ssr/resolvePageSeo.d.ts +18 -0
- package/dist/modules/ssr/resolvePageSeo.js +26 -0
- package/dist/modules/ssr/resolvePageSeo.test.d.ts +1 -0
- package/dist/modules/ssr/template.test.d.ts +1 -0
- package/dist/modules/ssr/views/template.ejs +71 -5
- package/dist/mysql.d.ts +26 -0
- package/dist/mysql.js +4 -0
- package/dist/oauth.d.ts +24 -0
- package/dist/oauth.js +10 -0
- package/dist/plugins/compile.d.ts +1 -0
- package/dist/plugins/compile.js +37 -3
- package/dist/plugins/manager.d.ts +40 -0
- package/dist/plugins/manager.js +113 -13
- package/dist/plugins/manager.test.d.ts +1 -0
- package/package.json +43 -14
- package/skills/plitzi-authoring/SKILL.md +259 -0
- /package/dist/{modules/actions/handler.test.d.ts → adapters/cloudAdapters.test.d.ts} +0 -0
- /package/dist/modules/actions/{handler.d.ts → connectorWrite.d.ts} +0 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { ActionRunError } from "./errors.js";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
//#region src/modules/actions/runtime/guards.ts
|
|
4
|
+
var DEFAULTS = {
|
|
5
|
+
perSpace: 10,
|
|
6
|
+
perProcess: 100,
|
|
7
|
+
renderPerProcess: 1e3
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* How long a cancellation waits to be read, when the replica that owns the run is not the one that took the
|
|
11
|
+
* request.
|
|
12
|
+
*
|
|
13
|
+
* Longer than any run's own timeout would be pointless — the run it addresses is gone — and shorter risks a flag
|
|
14
|
+
* expiring before a slow step finishes. A streaming run's ceiling is two minutes, so this covers it with room.
|
|
15
|
+
*/
|
|
16
|
+
var CANCEL_FLAG_TTL_SECONDS = 180;
|
|
17
|
+
/** Kept away from the `kv` tasks' own prefix: a flow writing `run:…` must not be able to release a run. */
|
|
18
|
+
var keyPrefix = "action:run:";
|
|
19
|
+
/**
|
|
20
|
+
* Where a cancellation is left for whichever replica is actually running the flow.
|
|
21
|
+
*
|
|
22
|
+
* Exported because the RUNNER is what reads it, at every step boundary, and the two must not each spell the key
|
|
23
|
+
* their own way — a cancel written under one name and read under another is a stop button that does nothing.
|
|
24
|
+
*/
|
|
25
|
+
var runCancelKey = (runId) => `${keyPrefix}cancel:${runId}`;
|
|
26
|
+
/** Who started a run, so a cancel arriving at another replica can still check ownership. */
|
|
27
|
+
var runOwnerKey = (runId) => `${keyPrefix}owner:${runId}`;
|
|
28
|
+
/** The answer a finished run left behind, for a caller that asks again with the same key. */
|
|
29
|
+
var runResultKey = (runKey) => `${keyPrefix}done:${runKey}`;
|
|
30
|
+
/** Stable regardless of key order, so the same call from the same caller derives the same key every time. */
|
|
31
|
+
var canonical = (value) => {
|
|
32
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
33
|
+
if (value !== null && typeof value === "object") return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(",")}}`;
|
|
34
|
+
if (value === void 0) return "null";
|
|
35
|
+
return JSON.stringify(value);
|
|
36
|
+
};
|
|
37
|
+
var deriveRunKey = ({ spaceId, actionId, callerId, input, idempotencyKey, sharedKey }) => {
|
|
38
|
+
if (idempotencyKey) return sharedKey ? `${spaceId}:${actionId}:${idempotencyKey}` : `${spaceId}:${actionId}:${callerId}:${idempotencyKey}`;
|
|
39
|
+
return `${spaceId}:${actionId}:${createHash("sha256").update(canonical({
|
|
40
|
+
callerId,
|
|
41
|
+
input
|
|
42
|
+
})).digest("hex").slice(0, 32)}`;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Single-flight, concurrency caps and cancellation, in one place.
|
|
46
|
+
*
|
|
47
|
+
* The client engine already refuses re-entry — `InteractionsManager` drops a trigger that fires while its own flow
|
|
48
|
+
* is still running — and this is the same rule keyed by what identifies a RUN rather than a DOM event. Without it
|
|
49
|
+
* a client that retries, reconnects or simply double-clicks turns one intent into many runs, and a streaming
|
|
50
|
+
* caller can turn it into an unbounded loop.
|
|
51
|
+
*
|
|
52
|
+
* **What is shared and what is not**, when a store is handed over:
|
|
53
|
+
*
|
|
54
|
+
* - **Single-flight is**, because per replica it is not a guarantee at all: the same double-click behind a load
|
|
55
|
+
* balancer lands on two of them and both run. The key is taken with `increment`, which is the one atomic
|
|
56
|
+
* test-and-set every adapter already has to provide — the holder is whoever's increment answered `1` — and it
|
|
57
|
+
* carries the run's own timeout as its lifetime, so a replica that dies holding one frees it by expiring.
|
|
58
|
+
* - **The caps are not.** Counting them across replicas needs a decrementing counter, and a run that dies without
|
|
59
|
+
* decrementing throttles a healthy space until somebody notices. A ceiling that is per replica is a number a
|
|
60
|
+
* deployment can multiply and reason about; one that leaks is not. So `perSpace` is per replica, deliberately,
|
|
61
|
+
* and a cluster's real ceiling is that times the number of them.
|
|
62
|
+
* - **Cancellation is**, in one direction. Only the process running a flow holds the `AbortController` that stops
|
|
63
|
+
* it, so a `DELETE` that lands anywhere else leaves a FLAG in the store and the run reads it at its next step
|
|
64
|
+
* boundary. Behind a load balancer that is the normal case rather than the exotic one: the tab that wants to
|
|
65
|
+
* stop a checkout almost never reaches the replica running it.
|
|
66
|
+
* - **A finished answer is**, when the caller named the key and the deployment set a replay window.
|
|
67
|
+
*/
|
|
68
|
+
var createRunGuards = (config = {}, store, idempotency = {}) => {
|
|
69
|
+
const perSpace = config.perSpace ?? DEFAULTS.perSpace;
|
|
70
|
+
const perProcess = config.perProcess ?? DEFAULTS.perProcess;
|
|
71
|
+
const renderPerProcess = config.renderPerProcess ?? DEFAULTS.renderPerProcess;
|
|
72
|
+
const replayTtlMs = idempotency.replayTtlMs ?? 0;
|
|
73
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
74
|
+
const byId = /* @__PURE__ */ new Map();
|
|
75
|
+
/** The no-store half of replay: one process remembering its own answers, which is what one process can honestly
|
|
76
|
+
* offer. With a store it is never read — a cluster's replays have to be everybody's. */
|
|
77
|
+
const localResults = /* @__PURE__ */ new Map();
|
|
78
|
+
const sweep = () => {
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
byKey.forEach((run) => {
|
|
81
|
+
if (run.expiresAt <= now) {
|
|
82
|
+
byKey.delete(run.runKey);
|
|
83
|
+
byId.delete(run.runId);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
localResults.forEach((entry, key) => {
|
|
87
|
+
if (entry.expiresAt <= now) localResults.delete(key);
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
const count = (matches) => {
|
|
91
|
+
let total = 0;
|
|
92
|
+
byKey.forEach((run) => {
|
|
93
|
+
if (matches(run)) total += 1;
|
|
94
|
+
});
|
|
95
|
+
return total;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Takes the key, here and — when there is one — everywhere.
|
|
99
|
+
*
|
|
100
|
+
* `increment` is the primitive because it is the only atomic one the adapter contract has, and it says exactly
|
|
101
|
+
* what is needed: the caller whose increment answers `1` created the key and holds it. The lifetime is set by
|
|
102
|
+
* that same caller, which is the rule `createKvStore` already follows for a rate-limit window; the gap between
|
|
103
|
+
* the two calls is a key that outlives its holder if the process dies inside it, and it is the same gap that
|
|
104
|
+
* idiom has always had.
|
|
105
|
+
*/
|
|
106
|
+
const ttlSeconds = (ttlMs) => Math.max(1, Math.ceil(ttlMs / 1e3));
|
|
107
|
+
const takeShared = async (runKey, ttlMs) => {
|
|
108
|
+
if (!store) return true;
|
|
109
|
+
if (await store.increment(`${keyPrefix}${runKey}`, 1) !== 1) return false;
|
|
110
|
+
await store.expire(`${keyPrefix}${runKey}`, ttlSeconds(ttlMs));
|
|
111
|
+
return true;
|
|
112
|
+
};
|
|
113
|
+
const begin = async (params) => {
|
|
114
|
+
sweep();
|
|
115
|
+
const runKey = deriveRunKey(params);
|
|
116
|
+
const existing = byKey.get(runKey);
|
|
117
|
+
if (existing) throw new ActionRunError("duplicate", `This action is already running as ${existing.runId}`);
|
|
118
|
+
/**
|
|
119
|
+
* Two budgets, because they are two different risks.
|
|
120
|
+
*
|
|
121
|
+
* A call is counted against its space AND the process: one space's callers must not starve another's, and
|
|
122
|
+
* neither may take the box down. A render is counted against the PROCESS alone — it arrives because people
|
|
123
|
+
* are reading the page, so a per-space ceiling would refuse the visitor who made the space worth having.
|
|
124
|
+
*/
|
|
125
|
+
const kind = params.kind ?? "call";
|
|
126
|
+
if (kind === "render" ? count((run) => run.kind === "render") >= renderPerProcess : count((run) => run.kind === "call") >= perProcess || count((run) => run.kind === "call" && run.spaceId === params.spaceId) >= perSpace) throw new ActionRunError("over_capacity", "Too many actions are running right now");
|
|
127
|
+
const run = {
|
|
128
|
+
runId: randomUUID(),
|
|
129
|
+
runKey,
|
|
130
|
+
kind,
|
|
131
|
+
spaceId: params.spaceId,
|
|
132
|
+
actionId: params.actionId,
|
|
133
|
+
callerId: params.callerId,
|
|
134
|
+
controller: new AbortController(),
|
|
135
|
+
expiresAt: Date.now() + params.ttlMs,
|
|
136
|
+
explicitKey: Boolean(params.idempotencyKey)
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* The local slot is taken BEFORE anything is awaited, and that ordering is the guarantee.
|
|
140
|
+
*
|
|
141
|
+
* Everything above this line runs in one synchronous stretch, so two callers in the same process cannot both
|
|
142
|
+
* pass it — which is what made this work when `begin` was synchronous throughout. Reach for the store first
|
|
143
|
+
* and both of them would sail through the local check while the other was suspended on it.
|
|
144
|
+
*/
|
|
145
|
+
byKey.set(runKey, run);
|
|
146
|
+
byId.set(run.runId, run);
|
|
147
|
+
try {
|
|
148
|
+
if (!await takeShared(runKey, params.ttlMs)) throw new ActionRunError("duplicate", "This action is already running");
|
|
149
|
+
} catch (error) {
|
|
150
|
+
byKey.delete(runKey);
|
|
151
|
+
byId.delete(run.runId);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
await store?.set(runOwnerKey(run.runId), params.callerId, ttlSeconds(params.ttlMs)).catch(() => void 0);
|
|
155
|
+
return run;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Remembers what a run answered, for a caller that asks again by the same key.
|
|
159
|
+
*
|
|
160
|
+
* Only a COMPLETED run, and only a key the caller named. A failure is not replayed because a retry after one is
|
|
161
|
+
* the reasonable thing to do — pinning a transient outage for the whole window would turn a blip into an hour
|
|
162
|
+
* of it — and a derived key would make two deliberate identical calls into one.
|
|
163
|
+
*/
|
|
164
|
+
const remember = async (run, outcome) => {
|
|
165
|
+
if (replayTtlMs <= 0 || !run.explicitKey || outcome.status !== "completed") return;
|
|
166
|
+
const answer = {
|
|
167
|
+
runId: outcome.runId,
|
|
168
|
+
status: outcome.status,
|
|
169
|
+
output: outcome.output
|
|
170
|
+
};
|
|
171
|
+
if (!store) {
|
|
172
|
+
localResults.set(run.runKey, {
|
|
173
|
+
expiresAt: Date.now() + replayTtlMs,
|
|
174
|
+
result: answer
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
await store.set(runResultKey(run.runKey), JSON.stringify(answer), ttlSeconds(replayTtlMs)).catch(() => void 0);
|
|
179
|
+
};
|
|
180
|
+
const end = async (run, outcome) => {
|
|
181
|
+
byKey.delete(run.runKey);
|
|
182
|
+
byId.delete(run.runId);
|
|
183
|
+
if (outcome) await remember(run, outcome);
|
|
184
|
+
await store?.delete(`${keyPrefix}${run.runKey}`).catch(() => void 0);
|
|
185
|
+
await store?.delete(runOwnerKey(run.runId)).catch(() => void 0);
|
|
186
|
+
await store?.delete(runCancelKey(run.runId)).catch(() => void 0);
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* The answer this key already produced, if it is still inside the window.
|
|
190
|
+
*
|
|
191
|
+
* Asked BEFORE a slot is taken, so a retry of something that already happened costs nothing and — this is the
|
|
192
|
+
* point — does not happen twice. Single-flight covers the retry that arrives while the first run is going; this
|
|
193
|
+
* covers the one that arrives a minute after it finished, which is the shape every webhook provider retries in.
|
|
194
|
+
*/
|
|
195
|
+
const replay = async (params) => {
|
|
196
|
+
if (replayTtlMs <= 0 || !params.idempotencyKey) return;
|
|
197
|
+
const runKey = deriveRunKey(params);
|
|
198
|
+
if (!store) {
|
|
199
|
+
sweep();
|
|
200
|
+
return localResults.get(runKey)?.result;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const stored = await store.get(runResultKey(runKey));
|
|
204
|
+
return stored ? JSON.parse(stored) : void 0;
|
|
205
|
+
} catch {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* Only the caller that started a run may stop it.
|
|
211
|
+
*
|
|
212
|
+
* A run id is a bearer string that travels to the browser, and without this check anyone holding one could stop
|
|
213
|
+
* another visitor's checkout — a denial of service addressed at a single person.
|
|
214
|
+
*
|
|
215
|
+
* Local first, because a cancel that reaches the right replica should stop the flow at its next boundary rather
|
|
216
|
+
* than a store round trip later. When it does not — the usual case behind a load balancer — the request leaves
|
|
217
|
+
* the flag the running replica polls, and ownership is checked against what `begin` published.
|
|
218
|
+
*/
|
|
219
|
+
const cancel = async (runId, callerId) => {
|
|
220
|
+
const run = byId.get(runId);
|
|
221
|
+
if (run) {
|
|
222
|
+
if (run.callerId !== callerId) return false;
|
|
223
|
+
run.controller.abort();
|
|
224
|
+
await store?.set(runCancelKey(runId), "1", ttlSeconds(run.expiresAt - Date.now())).catch(() => void 0);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
if (!store) return false;
|
|
228
|
+
try {
|
|
229
|
+
if (await store.get(runOwnerKey(runId)) !== callerId) return false;
|
|
230
|
+
await store.set(runCancelKey(runId), "1", CANCEL_FLAG_TTL_SECONDS);
|
|
231
|
+
return true;
|
|
232
|
+
} catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
begin,
|
|
238
|
+
end,
|
|
239
|
+
get: (runId) => byId.get(runId),
|
|
240
|
+
replay,
|
|
241
|
+
cancel,
|
|
242
|
+
active: () => byKey.size
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
//#endregion
|
|
246
|
+
export { createRunGuards, deriveRunKey, runCancelKey };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ActionKvAdapter, ActionKvStore } from '../types';
|
|
2
|
+
export type KvStoreConfig = {
|
|
3
|
+
/**
|
|
4
|
+
* Prefixed onto every key before it reaches the adapter. Defaults to `kv:`.
|
|
5
|
+
*
|
|
6
|
+
* The store a deployment hands over usually holds other things — a page cache, a session index — and a flow
|
|
7
|
+
* writing `visits:home` must not be able to land on one of them.
|
|
8
|
+
*/
|
|
9
|
+
prefix?: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The `kv` tasks, over whatever a deployment gave them to write into.
|
|
13
|
+
*
|
|
14
|
+
* The adapter is transport: strings in, strings out. Everything a counter's BEHAVIOUR depends on is here, in one
|
|
15
|
+
* place, for every deployment at once — which is the point of the split. Three rules:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Values round-trip through JSON**, so a flow reads back what it wrote instead of its own `toString`. A value
|
|
18
|
+
* written by something else is answered as the string it is rather than failing.
|
|
19
|
+
* 2. **A counter's TTL is set once, by whoever created it, and never extended.** A window refreshed on every hit
|
|
20
|
+
* never closes while traffic keeps arriving — which is a rate limit that stops limiting exactly when it is
|
|
21
|
+
* being leant on. The counter is the one that created it precisely when the increment returns its own amount,
|
|
22
|
+
* so this needs no conditional-expire support from the adapter and works the same on Redis, Memcached or a
|
|
23
|
+
* table.
|
|
24
|
+
* 3. **Nothing is caught.** This is not a cache: a miss means the rate limit did not count and the idempotency key
|
|
25
|
+
* was not seen, so an adapter that cannot answer fails the run rather than being read as "no value".
|
|
26
|
+
*/
|
|
27
|
+
export declare const createKvStore: (adapter: ActionKvAdapter, { prefix }?: KvStoreConfig) => ActionKvStore;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/modules/actions/runtime/kvStore.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `kv` tasks, over whatever a deployment gave them to write into.
|
|
4
|
+
*
|
|
5
|
+
* The adapter is transport: strings in, strings out. Everything a counter's BEHAVIOUR depends on is here, in one
|
|
6
|
+
* place, for every deployment at once — which is the point of the split. Three rules:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Values round-trip through JSON**, so a flow reads back what it wrote instead of its own `toString`. A value
|
|
9
|
+
* written by something else is answered as the string it is rather than failing.
|
|
10
|
+
* 2. **A counter's TTL is set once, by whoever created it, and never extended.** A window refreshed on every hit
|
|
11
|
+
* never closes while traffic keeps arriving — which is a rate limit that stops limiting exactly when it is
|
|
12
|
+
* being leant on. The counter is the one that created it precisely when the increment returns its own amount,
|
|
13
|
+
* so this needs no conditional-expire support from the adapter and works the same on Redis, Memcached or a
|
|
14
|
+
* table.
|
|
15
|
+
* 3. **Nothing is caught.** This is not a cache: a miss means the rate limit did not count and the idempotency key
|
|
16
|
+
* was not seen, so an adapter that cannot answer fails the run rather than being read as "no value".
|
|
17
|
+
*/
|
|
18
|
+
var createKvStore = (adapter, { prefix = "kv:" } = {}) => {
|
|
19
|
+
const prefixed = (key) => `${prefix}${key}`;
|
|
20
|
+
return {
|
|
21
|
+
get: async (key) => {
|
|
22
|
+
const raw = await adapter.get(prefixed(key));
|
|
23
|
+
if (raw === void 0) return;
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(raw);
|
|
26
|
+
} catch {
|
|
27
|
+
return raw;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
set: (key, value, ttlSeconds) => adapter.set(prefixed(key), JSON.stringify(value), ttlSeconds),
|
|
31
|
+
delete: (key) => adapter.delete(prefixed(key)),
|
|
32
|
+
increment: async (key, amount, ttlSeconds) => {
|
|
33
|
+
const full = prefixed(key);
|
|
34
|
+
const value = await adapter.increment(full, amount);
|
|
35
|
+
if (ttlSeconds !== void 0 && value === amount) await adapter.expire(full, ttlSeconds);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
export { createKvStore };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ResolvedActionLimits } from '../types';
|
|
2
|
+
import { ActionLimits } from '@plitzi/sdk-shared';
|
|
3
|
+
export declare const DEFAULT_LIMITS: ResolvedActionLimits;
|
|
4
|
+
/**
|
|
5
|
+
* Merges a document's limits over the deployment's.
|
|
6
|
+
*
|
|
7
|
+
* A per-action value may only ever TIGHTEN: the deployment is the party paying for the connection, the slot and
|
|
8
|
+
* the outbound traffic, and an authored document is customer input. Letting it widen would make every ceiling
|
|
9
|
+
* advisory — which is the same as not having one.
|
|
10
|
+
*/
|
|
11
|
+
export declare const resolveLimits: (deployment?: ActionLimits, document?: ActionLimits) => ResolvedActionLimits;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/modules/actions/runtime/limits.ts
|
|
2
|
+
var DEFAULT_LIMITS = {
|
|
3
|
+
timeoutMs: 1e4,
|
|
4
|
+
streamTimeoutMs: 12e4,
|
|
5
|
+
maxNodes: 50,
|
|
6
|
+
maxRequests: 20,
|
|
7
|
+
/** Generous for an API answer and nowhere near what it takes to hurt a process holding several runs at once. */
|
|
8
|
+
maxResponseBytes: 5e6
|
|
9
|
+
};
|
|
10
|
+
var tightest = (deployment, document) => document === void 0 || document <= 0 ? deployment : Math.min(deployment, document);
|
|
11
|
+
/**
|
|
12
|
+
* Merges a document's limits over the deployment's.
|
|
13
|
+
*
|
|
14
|
+
* A per-action value may only ever TIGHTEN: the deployment is the party paying for the connection, the slot and
|
|
15
|
+
* the outbound traffic, and an authored document is customer input. Letting it widen would make every ceiling
|
|
16
|
+
* advisory — which is the same as not having one.
|
|
17
|
+
*/
|
|
18
|
+
var resolveLimits = (deployment = {}, document = {}) => {
|
|
19
|
+
const base = {
|
|
20
|
+
timeoutMs: deployment.timeoutMs ?? DEFAULT_LIMITS.timeoutMs,
|
|
21
|
+
streamTimeoutMs: deployment.streamTimeoutMs ?? DEFAULT_LIMITS.streamTimeoutMs,
|
|
22
|
+
maxNodes: deployment.maxNodes ?? DEFAULT_LIMITS.maxNodes,
|
|
23
|
+
maxRequests: deployment.maxRequests ?? DEFAULT_LIMITS.maxRequests,
|
|
24
|
+
maxResponseBytes: deployment.maxResponseBytes ?? DEFAULT_LIMITS.maxResponseBytes
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
timeoutMs: tightest(base.timeoutMs, document.timeoutMs),
|
|
28
|
+
streamTimeoutMs: tightest(base.streamTimeoutMs, document.streamTimeoutMs),
|
|
29
|
+
maxNodes: tightest(base.maxNodes, document.maxNodes),
|
|
30
|
+
maxRequests: tightest(base.maxRequests, document.maxRequests),
|
|
31
|
+
maxResponseBytes: tightest(base.maxResponseBytes, document.maxResponseBytes)
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
export { DEFAULT_LIMITS, resolveLimits };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
//#region src/modules/actions/runtime/memoryKv.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `kv` tasks' fallback store: in this process, and nowhere else.
|
|
4
|
+
*
|
|
5
|
+
* Real enough for a single replica and for local work, and deliberately not pretending otherwise — a deployment
|
|
6
|
+
* running several replicas passes an adapter over something shared, because a counter that only counts its own
|
|
7
|
+
* replica is a rate limit that multiplies by the number of them.
|
|
8
|
+
*
|
|
9
|
+
* An adapter like any other: it stores strings and obeys no rules of its own. What a counter DOES lives in
|
|
10
|
+
* `createKvStore`, so the in-process store and a deployment's own behave identically rather than nearly so.
|
|
11
|
+
*/
|
|
12
|
+
/** How many writes go by before the map is swept. Cheap enough to be unnoticeable, often enough to bound it. */
|
|
13
|
+
var SWEEP_EVERY = 256;
|
|
14
|
+
var createMemoryKv = () => {
|
|
15
|
+
const entries = /* @__PURE__ */ new Map();
|
|
16
|
+
let writes = 0;
|
|
17
|
+
/**
|
|
18
|
+
* Expiry only ever happened on READ, which is fine for a counter that is read as often as it is written and
|
|
19
|
+
* unbounded for everything else: a rate-limit window keyed by caller, an idempotency key per delivery, anything
|
|
20
|
+
* whose key is used once and never asked for again. Those entries expire and stay, and the map is the process's
|
|
21
|
+
* memory. Redis sweeps its own; this is the same courtesy, amortised over the writes that cause the growth.
|
|
22
|
+
*/
|
|
23
|
+
const sweep = () => {
|
|
24
|
+
writes += 1;
|
|
25
|
+
if (writes % SWEEP_EVERY !== 0) return;
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
entries.forEach((entry, key) => {
|
|
28
|
+
if (entry.expiresAt !== void 0 && entry.expiresAt <= now) entries.delete(key);
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
const read = (key) => {
|
|
32
|
+
const entry = entries.get(key);
|
|
33
|
+
if (!entry) return;
|
|
34
|
+
if (entry.expiresAt !== void 0 && entry.expiresAt <= Date.now()) {
|
|
35
|
+
entries.delete(key);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
return entry;
|
|
39
|
+
};
|
|
40
|
+
const expiry = (ttlSeconds) => ttlSeconds === void 0 ? void 0 : Date.now() + ttlSeconds * 1e3;
|
|
41
|
+
return {
|
|
42
|
+
get: (key) => Promise.resolve(read(key)?.value),
|
|
43
|
+
set: (key, value, ttlSeconds) => {
|
|
44
|
+
entries.set(key, {
|
|
45
|
+
value,
|
|
46
|
+
expiresAt: expiry(ttlSeconds)
|
|
47
|
+
});
|
|
48
|
+
sweep();
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
},
|
|
51
|
+
delete: (key) => {
|
|
52
|
+
entries.delete(key);
|
|
53
|
+
return Promise.resolve();
|
|
54
|
+
},
|
|
55
|
+
increment: (key, amount) => {
|
|
56
|
+
const current = read(key);
|
|
57
|
+
const base = Number(current?.value ?? 0);
|
|
58
|
+
const next = (Number.isFinite(base) ? base : 0) + amount;
|
|
59
|
+
entries.set(key, {
|
|
60
|
+
value: String(next),
|
|
61
|
+
expiresAt: current?.expiresAt
|
|
62
|
+
});
|
|
63
|
+
sweep();
|
|
64
|
+
return Promise.resolve(next);
|
|
65
|
+
},
|
|
66
|
+
expire: (key, ttlSeconds) => {
|
|
67
|
+
const current = read(key);
|
|
68
|
+
if (current) entries.set(key, {
|
|
69
|
+
...current,
|
|
70
|
+
expiresAt: expiry(ttlSeconds)
|
|
71
|
+
});
|
|
72
|
+
return Promise.resolve();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
export { createMemoryKv };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ActionKvStore } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Prefixes every key with the space that wrote it.
|
|
4
|
+
*
|
|
5
|
+
* A shared store with unprefixed keys is one space reading — or overwriting — another's counters, which is the
|
|
6
|
+
* cross-tenant leak this whole design exists to avoid. Done at the runner rather than in each task so a
|
|
7
|
+
* deployment's own tasks inherit it too.
|
|
8
|
+
*/
|
|
9
|
+
export declare const namespaceKv: (store: ActionKvStore, spaceId: number) => ActionKvStore;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/modules/actions/runtime/namespaceKv.ts
|
|
2
|
+
/**
|
|
3
|
+
* Prefixes every key with the space that wrote it.
|
|
4
|
+
*
|
|
5
|
+
* A shared store with unprefixed keys is one space reading — or overwriting — another's counters, which is the
|
|
6
|
+
* cross-tenant leak this whole design exists to avoid. Done at the runner rather than in each task so a
|
|
7
|
+
* deployment's own tasks inherit it too.
|
|
8
|
+
*/
|
|
9
|
+
var namespaceKv = (store, spaceId) => {
|
|
10
|
+
const scoped = (key) => `action:${spaceId}:${key}`;
|
|
11
|
+
return {
|
|
12
|
+
get: (key) => store.get(scoped(key)),
|
|
13
|
+
set: (key, value, ttlSeconds) => store.set(scoped(key), value, ttlSeconds),
|
|
14
|
+
delete: (key) => store.delete(scoped(key)),
|
|
15
|
+
increment: (key, amount, ttlSeconds) => store.increment(scoped(key), amount, ttlSeconds)
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
export { namespaceKv };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ActionEntry, ActionTriggerType, ElementInteraction, SSRUser } from '@plitzi/sdk-shared';
|
|
2
|
+
export type PrecheckParams = {
|
|
3
|
+
trigger: ActionTriggerType;
|
|
4
|
+
input: Record<string, unknown>;
|
|
5
|
+
user?: SSRUser;
|
|
6
|
+
/** Chain of run ids that caused this one. A run naming its own action is a loop. */
|
|
7
|
+
lineage?: string[];
|
|
8
|
+
};
|
|
9
|
+
/** The entry point that was cleared, and the input that survived its contract. */
|
|
10
|
+
export type PrecheckResult = {
|
|
11
|
+
trigger: ElementInteraction;
|
|
12
|
+
values: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Everything that decides whether a run may happen at all, before it costs anything.
|
|
16
|
+
*
|
|
17
|
+
* One implementation, two callers, on purpose. The RUNNER calls it because a deployment mounting its own trigger
|
|
18
|
+
* goes straight there, and a check that lived only in the endpoint would be a check a custom trigger silently
|
|
19
|
+
* skips. The ENDPOINT calls it first because these are pure and cheap, and a refusal that has already taken a
|
|
20
|
+
* concurrency slot and a metering event teaches callers to retry harder for something that was never going to run.
|
|
21
|
+
*
|
|
22
|
+
* Everything it reads about the caller — may they, and with what — is on the TRIGGER STEP, so an action reachable
|
|
23
|
+
* both by a signed webhook and by a page call answers each on its own terms instead of on the loosest of the two.
|
|
24
|
+
*
|
|
25
|
+
* Returns the entry point and the coerced input, so neither caller has to find or validate either twice.
|
|
26
|
+
*/
|
|
27
|
+
export declare const precheckRun: (entry: ActionEntry, params: PrecheckParams) => PrecheckResult;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ActionRunError } from "./errors.js";
|
|
2
|
+
import { applyFields } from "./scope.js";
|
|
3
|
+
import { findTriggerNode, triggerParams } from "./triggers.js";
|
|
4
|
+
import { triggerAccess, triggerInput } from "@plitzi/sdk-shared/actions";
|
|
5
|
+
//#region src/modules/actions/runtime/precheck.ts
|
|
6
|
+
var authorize = (access, kind, user) => {
|
|
7
|
+
if (kind === "schedule") return;
|
|
8
|
+
if (!access) throw new ActionRunError("forbidden", "This trigger declares no access rule");
|
|
9
|
+
if (access.mode === "public") return;
|
|
10
|
+
if (!user) throw new ActionRunError("unauthenticated", "This action requires a signed-in visitor");
|
|
11
|
+
if (access.mode === "role") {
|
|
12
|
+
const held = new Set(user.permissions);
|
|
13
|
+
if (access.permissions.some((permission) => !held.has(permission))) throw new ActionRunError("forbidden", "This action requires permissions the caller does not hold");
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Everything that decides whether a run may happen at all, before it costs anything.
|
|
18
|
+
*
|
|
19
|
+
* One implementation, two callers, on purpose. The RUNNER calls it because a deployment mounting its own trigger
|
|
20
|
+
* goes straight there, and a check that lived only in the endpoint would be a check a custom trigger silently
|
|
21
|
+
* skips. The ENDPOINT calls it first because these are pure and cheap, and a refusal that has already taken a
|
|
22
|
+
* concurrency slot and a metering event teaches callers to retry harder for something that was never going to run.
|
|
23
|
+
*
|
|
24
|
+
* Everything it reads about the caller — may they, and with what — is on the TRIGGER STEP, so an action reachable
|
|
25
|
+
* both by a signed webhook and by a page call answers each on its own terms instead of on the loosest of the two.
|
|
26
|
+
*
|
|
27
|
+
* Returns the entry point and the coerced input, so neither caller has to find or validate either twice.
|
|
28
|
+
*/
|
|
29
|
+
var precheckRun = (entry, params) => {
|
|
30
|
+
const { document } = entry;
|
|
31
|
+
const trigger = findTriggerNode(document.nodes, params.trigger);
|
|
32
|
+
if (!trigger) throw new ActionRunError("forbidden", `This action cannot be started by a "${params.trigger}" trigger`);
|
|
33
|
+
if (!trigger.enabled) throw new ActionRunError("disabled", `This action's "${params.trigger}" trigger is switched off`);
|
|
34
|
+
if (params.lineage?.includes(entry.id)) throw new ActionRunError("recursion", "This action already appears in the run lineage");
|
|
35
|
+
const stepParams = triggerParams(trigger);
|
|
36
|
+
authorize(triggerAccess(stepParams), params.trigger, params.user);
|
|
37
|
+
const { values, missing, invalid } = applyFields(triggerInput(stepParams), params.input);
|
|
38
|
+
if (missing.length > 0 || invalid.length > 0) throw new ActionRunError("invalid_input", `Invalid input: ${[...missing, ...invalid].join(", ")}`);
|
|
39
|
+
return {
|
|
40
|
+
trigger,
|
|
41
|
+
values
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
export { precheckRun };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { RscElementResolver } from '../../rsc/resolveRscData';
|
|
2
|
+
import { ActionsModule } from '../index';
|
|
3
|
+
import { ActionLookups } from '../types';
|
|
4
|
+
/**
|
|
5
|
+
* Resolves a `runtime: 'server'` element that names an ACTION rather than a connector.
|
|
6
|
+
*
|
|
7
|
+
* This is the read a manifest cannot express: two calls that have to be joined, a field computed from both, a
|
|
8
|
+
* shape that depends on who is looking. The element names an action exactly as it would name a connector, and the
|
|
9
|
+
* page still learns nothing about what happens on the other side.
|
|
10
|
+
*
|
|
11
|
+
* Its input is the page's own context — route params, then query params — plus whatever the element declares, so
|
|
12
|
+
* an action feeding `/blog/:slug` reads `{{ input.slug }}` and needs nothing else. The action's own input contract
|
|
13
|
+
* still drops everything it did not declare.
|
|
14
|
+
*/
|
|
15
|
+
export declare const createActionResolver: (lookups: ActionLookups, module: ActionsModule) => RscElementResolver;
|