@plitzi/sdk-server 0.37.8 → 0.37.10
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 +825 -0
- package/README.md +24 -2
- package/dist/actions.d.ts +1 -1
- package/dist/adapters/cloudAdapters.d.ts +5 -3
- package/dist/adapters/cloudAdapters.js +123 -63
- package/dist/adapters/space/types.d.ts +3 -0
- package/dist/core/configSeam.d.ts +14 -0
- package/dist/core/configSeam.js +114 -0
- package/dist/core/configSeam.test.d.ts +1 -0
- package/dist/core/createServer.js +11 -5
- package/dist/core/createServer.publicData.test.d.ts +1 -0
- package/dist/core/freePort.d.ts +10 -0
- package/dist/core/freePort.js +36 -0
- package/dist/core/freePort.test.d.ts +1 -0
- package/dist/core/http/stages/images.d.ts +6 -0
- package/dist/core/http/stages/images.js +47 -0
- package/dist/core/http/stages/images.test.d.ts +1 -0
- package/dist/core/server/fleet/channel.js +1 -1
- package/dist/core/server/fleet/relay.js +1 -1
- package/dist/core/services/action.js +5 -4
- package/dist/core/services/functionRoutes.d.ts +2 -2
- package/dist/core/services/functionRoutes.js +2 -2
- package/dist/core/services/registry.js +2 -0
- package/dist/functions-api.d.ts +23 -2
- package/dist/functions.d.ts +1 -1
- package/dist/functions.js +2 -2
- package/dist/helpers/cache/keys.d.ts +23 -10
- package/dist/helpers/cache/keys.js +16 -2
- package/dist/helpers/flagOverrides.d.ts +27 -0
- package/dist/helpers/flagOverrides.js +25 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/modules/actions/index.d.ts +1 -1
- package/dist/modules/actions/index.js +3 -2
- package/dist/modules/actions/moduleFor.js +5 -3
- package/dist/modules/actions/runtime/flags.d.ts +16 -0
- package/dist/modules/actions/runtime/flags.js +49 -0
- package/dist/modules/actions/runtime/flags.test.d.ts +1 -0
- package/dist/modules/actions/runtime/kvList.js +1 -1
- package/dist/modules/actions/runtime/renderResolver.js +30 -4
- package/dist/modules/actions/runtime/runAction.js +2 -0
- package/dist/modules/actions/taskCatalog.d.ts +3 -1
- package/dist/modules/actions/taskCatalog.js +1 -0
- package/dist/modules/actions/tasks/registry.js +4 -2
- package/dist/modules/actions/transport/callHandler.js +8 -0
- package/dist/modules/actions/types.d.ts +20 -1
- package/dist/modules/connectors/engine.js +1 -1
- package/dist/modules/functions/capabilities.js +1 -1
- package/dist/modules/functions/config.d.ts +19 -3
- package/dist/modules/functions/config.js +30 -14
- package/dist/modules/functions/contract.d.ts +10 -3
- package/dist/modules/functions/driver.js +4 -2
- package/dist/modules/functions/load.js +2 -2
- package/dist/modules/functions/manifest.d.ts +5 -3
- package/dist/modules/functions/manifest.js +37 -6
- package/dist/modules/functions/routes.d.ts +1 -1
- package/dist/modules/functions/routes.js +3 -3
- package/dist/modules/functions/runner/messages.js +1 -1
- package/dist/modules/functions/space.d.ts +7 -5
- package/dist/modules/functions/space.js +17 -10
- package/dist/modules/images/allowlist.d.ts +5 -0
- package/dist/modules/images/allowlist.js +18 -0
- package/dist/modules/images/imageProxy.d.ts +36 -0
- package/dist/modules/images/imageProxy.js +273 -0
- package/dist/modules/images/imageProxy.test.d.ts +1 -0
- package/dist/modules/images/transform.d.ts +10 -0
- package/dist/modules/images/transform.js +28 -0
- package/dist/modules/images/transform.test.d.ts +1 -0
- package/dist/modules/rsc/connectorRscData.d.ts +12 -7
- package/dist/modules/rsc/connectorRscData.js +12 -8
- package/dist/modules/rsc/handler.js +18 -4
- package/dist/modules/rsc/publicFileResolver.d.ts +13 -0
- package/dist/modules/rsc/publicFileResolver.js +39 -0
- package/dist/modules/rsc/publicFileResolver.test.d.ts +1 -0
- package/dist/modules/rsc/resolveRscData.d.ts +4 -2
- package/dist/modules/rsc/resolveRscData.js +10 -2
- package/dist/modules/runtime/bundle.js +2 -2
- package/dist/modules/runtime/contract.d.ts +1 -1
- package/dist/modules/runtime/stages.d.ts +1 -1
- package/dist/modules/runtime/stages.js +1 -1
- package/dist/modules/ssr/Component.d.ts +5 -1
- package/dist/modules/ssr/Component.js +3 -1
- package/dist/modules/ssr/prepareRender.js +38 -10
- package/dist/modules/ssr/registerExternalPlugins.d.ts +5 -1
- package/dist/modules/ssr/registerExternalPlugins.js +6 -2
- package/package.json +9 -4
- package/skills/plitzi-authoring/CHEATSHEET.md +135 -0
- package/skills/plitzi-authoring/SKILL.md +77 -74
- package/skills/plitzi-authoring/recipes/carousel.ts +114 -0
- package/skills/plitzi-authoring/recipes/detail-page.ts +70 -0
- package/skills/plitzi-authoring/recipes/embed-and-svg.ts +48 -0
- package/skills/plitzi-authoring/recipes/every-few-seconds.ts +119 -0
- package/skills/plitzi-authoring/recipes/feature-flag.ts +24 -0
- package/skills/plitzi-authoring/recipes/filter-a-list.ts +63 -0
- package/skills/plitzi-authoring/recipes/forms-and-modals.ts +64 -0
- package/skills/plitzi-authoring/recipes/from-tailwind.ts +52 -0
- package/skills/plitzi-authoring/recipes/link-to-a-section.ts +35 -0
- package/skills/plitzi-authoring/recipes/link-with-data.ts +40 -0
- package/skills/plitzi-authoring/recipes/marquee.ts +57 -0
- package/skills/plitzi-authoring/recipes/plugin.ts +53 -0
- package/skills/plitzi-authoring/recipes/scroll-a-row.ts +79 -0
- package/skills/plitzi-authoring/recipes/show-data.ts +73 -0
- package/skills/plitzi-authoring/recipes/style-a-page.ts +45 -0
- package/skills/plitzi-authoring/recipes/typed-data.ts +64 -0
- package/skills/plitzi-authoring/recipes/usable-without-sight.ts +39 -0
- package/skills/plitzi-authoring/reference/accessibility.md +2 -2
- package/skills/plitzi-authoring/reference/authoring-errors.md +195 -53
- package/skills/plitzi-authoring/reference/colours-and-motion.md +63 -0
- package/skills/plitzi-authoring/reference/components.md +83 -0
- package/skills/plitzi-authoring/reference/data-and-visibility.md +29 -29
- package/skills/plitzi-authoring/reference/efficiency.md +53 -0
- package/skills/plitzi-authoring/reference/elements-and-styles.md +39 -24
- package/skills/plitzi-authoring/reference/feature-flags.md +78 -0
- package/skills/plitzi-authoring/reference/flows.md +26 -37
- package/skills/plitzi-authoring/reference/kept-state.md +28 -0
- package/skills/plitzi-authoring/reference/layouts.md +20 -3
- package/skills/plitzi-authoring/reference/lists.md +45 -16
- package/skills/plitzi-authoring/reference/plugins.md +27 -0
- package/skills/plitzi-authoring/reference/realtime.md +35 -0
- package/skills/plitzi-authoring/reference/review-checklist.md +8 -3
- package/skills/plitzi-authoring/reference/{templates-and-export.md → snippets-and-export.md} +15 -8
- package/skills/plitzi-authoring/reference/structure.md +29 -20
- package/skills/plitzi-authoring/reference/tailwind.md +19 -0
- package/skills/plitzi-authoring/reference/templates.md +6 -5
- package/skills/plitzi-authoring/reference/testing.md +11 -0
- package/skills/plitzi-authoring/reference/typed-sources.md +20 -0
- package/skills/plitzi-authoring/reference/validation.md +6 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionTaskRegistry, RegisteredTask } from './types';
|
|
1
|
+
import { ActionTaskOrigin, ActionTaskRegistry, RegisteredTask } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* A task as an editor renders it.
|
|
4
4
|
*
|
|
@@ -18,6 +18,8 @@ export type ActionTaskDescriptor = {
|
|
|
18
18
|
action: string;
|
|
19
19
|
title: string;
|
|
20
20
|
description?: string;
|
|
21
|
+
/** The deployment's step, or one of the space's own functions. */
|
|
22
|
+
origin: ActionTaskOrigin;
|
|
21
23
|
/** Serializable params only — see above. */
|
|
22
24
|
params: Record<string, unknown>;
|
|
23
25
|
};
|
|
@@ -26,7 +26,8 @@ var createTaskRegistry = (functions = [], { db = false } = {}) => {
|
|
|
26
26
|
const tasks = /* @__PURE__ */ new Map();
|
|
27
27
|
(db ? [...builtinTasks, ...dbTasks] : builtinTasks).forEach((task) => tasks.set(taskName(task), {
|
|
28
28
|
...task,
|
|
29
|
-
name: taskName(task)
|
|
29
|
+
name: taskName(task),
|
|
30
|
+
origin: "deployment"
|
|
30
31
|
}));
|
|
31
32
|
nativeTasks(functions).forEach((task) => {
|
|
32
33
|
const problem = taskNameProblem(task);
|
|
@@ -35,7 +36,8 @@ var createTaskRegistry = (functions = [], { db = false } = {}) => {
|
|
|
35
36
|
if (tasks.has(name)) throw new Error(`[Actions] Task "${name}" is registered twice`);
|
|
36
37
|
tasks.set(name, {
|
|
37
38
|
...task,
|
|
38
|
-
name
|
|
39
|
+
name,
|
|
40
|
+
origin: "deployment"
|
|
39
41
|
});
|
|
40
42
|
});
|
|
41
43
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { serverLog } from "../../../helpers/serverLog.js";
|
|
2
2
|
import { triggerParams } from "../runtime/triggers.js";
|
|
3
3
|
import { ActionRunError } from "../runtime/errors.js";
|
|
4
|
+
import { forcedFlagsFor } from "../../../helpers/flagOverrides.js";
|
|
4
5
|
import { precheckRun } from "../runtime/precheck.js";
|
|
5
6
|
import onAbort from "../../../helpers/onAbort.js";
|
|
6
7
|
import { openStream, wantsStream } from "./stream.js";
|
|
@@ -176,6 +177,12 @@ var handleActionCall = async (deps) => {
|
|
|
176
177
|
const readsRun = mayReadTrace(entryPoint, req.ctx.user);
|
|
177
178
|
const fullTrace = readsRun && (authoring === true || config.devMode === true);
|
|
178
179
|
const showsOutline = async () => readsRun && (fullTrace || await resolveDebugAuthorization(config, async () => (await config.adapters.getOfflineData(spaceId, environment, revision))?.schema.settings));
|
|
180
|
+
/**
|
|
181
|
+
* The flags a tester forced on the page this call came from, so the flow sees the feature the page was drawn with.
|
|
182
|
+
* Only for a page allowed to debug — the cookie is the visitor's own, and anywhere else it would let any of them
|
|
183
|
+
* switch a feature on for the server too. The authorization is asked only when there is a cookie to honour.
|
|
184
|
+
*/
|
|
185
|
+
const forcedFlags = await forcedFlagsFor(req, () => resolveDebugAuthorization(config, async () => (await config.adapters.getOfflineData(spaceId, environment, revision))?.schema.settings));
|
|
179
186
|
let outcome;
|
|
180
187
|
try {
|
|
181
188
|
const result = await module.runAction({
|
|
@@ -192,6 +199,7 @@ var handleActionCall = async (deps) => {
|
|
|
192
199
|
environment,
|
|
193
200
|
revision
|
|
194
201
|
},
|
|
202
|
+
...forcedFlags ? { forcedFlags } : {},
|
|
195
203
|
signal: run.controller.signal,
|
|
196
204
|
...stream ? {
|
|
197
205
|
emit: (chunk) => stream.send({
|
|
@@ -4,7 +4,7 @@ import { SpaceFunctions } from '../functions/protocol';
|
|
|
4
4
|
import { KvChangeLifetime } from './runtime/kvChange';
|
|
5
5
|
import { KvListEntry, KvListPut, KvListPutOptions, KvListRange } from './runtime/kvList';
|
|
6
6
|
import { SpaceSigning } from './runtime/signing';
|
|
7
|
-
import { ActionEmailConfig, ActionJobQueue, ActionEntry, ActionLimits, ActionRejectRecord, ActionRunRecord, ActionRunStatus, ActionRunStep, ActionTriggerType, ConnectorManifest, Environment, InteractionCallbackParam, InteractionNode, InteractionNodeStatus, SpaceRevision, SSRUser } from '@plitzi/sdk-shared';
|
|
7
|
+
import { ActionEmailConfig, ActionJobQueue, ActionEntry, ActionLimits, ActionRejectRecord, ActionRunRecord, ActionRunStatus, ActionRunStep, ActionTriggerType, ConnectorManifest, Environment, InteractionCallbackParam, InteractionNode, InteractionNodeStatus, Schema, SpaceRevision, SSRServerConfig, SSRUser } from '@plitzi/sdk-shared';
|
|
8
8
|
/** Resolved secret material, as the connector engine already models it. */
|
|
9
9
|
export type ActionCredential = Record<string, string>;
|
|
10
10
|
/**
|
|
@@ -120,6 +120,11 @@ export type ActionLookups = {
|
|
|
120
120
|
* asked when a run names a task the deployment does not have, or a request a path under `/api/`.
|
|
121
121
|
*/
|
|
122
122
|
getFunctions?: (spaceId: number, at?: SpaceRevision) => Promise<SpaceFunctions | undefined>;
|
|
123
|
+
/**
|
|
124
|
+
* The feature flags the space declares as of that revision, for a flow that reads `{{ flags.<name> }}`. Only asked
|
|
125
|
+
* for a run whose document names `flags` at all. Absent, such a flow sees no flags — and the server says so.
|
|
126
|
+
*/
|
|
127
|
+
getFlags?: (spaceId: number, at?: SpaceRevision) => Promise<Schema['flags']>;
|
|
123
128
|
};
|
|
124
129
|
/**
|
|
125
130
|
* The only door a task has to the outside world.
|
|
@@ -219,8 +224,14 @@ export type ActionTask<T extends Record<string, unknown> = Record<string, unknow
|
|
|
219
224
|
run: (params: T, ctx: ActionTaskContext) => unknown;
|
|
220
225
|
};
|
|
221
226
|
/** A task with its addressable name resolved, as the registry stores and lists it. */
|
|
227
|
+
/**
|
|
228
|
+
* Whose task it is: the deployment's — shipped with the server, or its own native functions — or the space's, from the
|
|
229
|
+
* functions it saved. The run treats both alike; an editor lists them apart, as the platform's steps and the space's own.
|
|
230
|
+
*/
|
|
231
|
+
export type ActionTaskOrigin = 'deployment' | 'space';
|
|
222
232
|
export type RegisteredTask = ActionTask<Record<string, unknown>> & {
|
|
223
233
|
name: string;
|
|
234
|
+
origin: ActionTaskOrigin;
|
|
224
235
|
};
|
|
225
236
|
export type ActionTaskRegistry = {
|
|
226
237
|
get: (name: string) => RegisteredTask | undefined;
|
|
@@ -229,6 +240,8 @@ export type ActionTaskRegistry = {
|
|
|
229
240
|
/** What a deployment hands to `createServer` under `actions`. Absent → the module is never constructed. */
|
|
230
241
|
export type ActionsConfig = {
|
|
231
242
|
lookups: ActionLookups;
|
|
243
|
+
/** The flags this deployment decides — the server's own `flags`, which a flow sees the way a page does. */
|
|
244
|
+
flags?: SSRServerConfig['flags'];
|
|
232
245
|
/** Code of the server's own, and the sandbox for the spaces' — see `FunctionsConfig`. */
|
|
233
246
|
functions?: FunctionsConfig;
|
|
234
247
|
/** Ceilings a per-action document may tighten but never exceed. */
|
|
@@ -380,6 +393,12 @@ export type ActionRunRequest = {
|
|
|
380
393
|
* miniature.
|
|
381
394
|
*/
|
|
382
395
|
at?: SpaceRevision;
|
|
396
|
+
/**
|
|
397
|
+
* The flags a tester forced, for a run started from a page allowed to debug — the same `qa` layer the page was
|
|
398
|
+
* drawn with, so a forced feature is not on in the page and off in the flow behind it. Never what the SDK's own
|
|
399
|
+
* `flags` say: those are the browser's claim, and a flow does not take a claim as a fact.
|
|
400
|
+
*/
|
|
401
|
+
forcedFlags?: Record<string, boolean>;
|
|
383
402
|
emit?: (chunk: unknown) => void;
|
|
384
403
|
/** Reports each step as it settles, for a caller watching the run happen. Absent for a plain request/response. */
|
|
385
404
|
onNode?: (id: string, status: InteractionNodeStatus) => void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fetchOutbound } from "../../helpers/outboundGuard.js";
|
|
2
2
|
import { getByPath } from "./getByPath.js";
|
|
3
|
-
import { processTwig } from "@plitzi/sdk-shared/helpers/twigWrapper";
|
|
4
3
|
import { BODYLESS_METHODS, DEFAULT_READ_ENDPOINT, EMPTY_RESPONSE_METHODS } from "@plitzi/sdk-shared/connectors";
|
|
4
|
+
import { processTwig } from "@plitzi/sdk-shared/helpers/twigWrapper";
|
|
5
5
|
//#region src/modules/connectors/engine.ts
|
|
6
6
|
var DEFAULT_LIMIT = 10;
|
|
7
7
|
/** Only scalars can become a URL fragment, a header or a record key. Anything else is treated as absent. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
1
2
|
//#region src/modules/functions/capabilities.ts
|
|
2
3
|
var TEXTUAL = /^(text\/|application\/([\w.+-]*\+)?(json|xml|javascript|x-www-form-urlencoded))/i;
|
|
3
4
|
/** A body as the wire carries it: as text when its type says it is text, base64 otherwise. */
|
|
@@ -42,7 +43,6 @@ var numberArg = (value, what) => {
|
|
|
42
43
|
return value;
|
|
43
44
|
};
|
|
44
45
|
var optionalNumber = (value, what) => value === void 0 || value === null ? void 0 : numberArg(value, what);
|
|
45
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46
46
|
var headerPairsOf = (value) => Array.isArray(value) ? value.flatMap((pair) => Array.isArray(pair) && typeof pair[0] === "string" && typeof pair[1] === "string" ? [[pair[0], pair[1]]] : []) : [];
|
|
47
47
|
var wireBodyFrom = (value) => {
|
|
48
48
|
const body = isRecord(value) ? value : void 0;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FunctionsDefinition } from './contract';
|
|
2
2
|
import { FunctionLimits, FunctionRunner, FunctionUsage } from './protocol';
|
|
3
|
+
import { FunctionTimeLimits } from '@plitzi/sdk-shared';
|
|
3
4
|
/** One invocation of a space's task, as metering sees it. */
|
|
4
5
|
export type FunctionUsageRecord = {
|
|
5
6
|
spaceId: number;
|
|
@@ -19,7 +20,11 @@ export type FunctionsConfig = {
|
|
|
19
20
|
* functions here — their tasks are not offered and none of their code runs.
|
|
20
21
|
*/
|
|
21
22
|
runner?: FunctionRunner;
|
|
22
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* The most one invocation may spend on this deployment ({@link DEFAULT_FUNCTION_CEILINGS} for each unset): what a
|
|
25
|
+
* task may ask for in CPU and time, and what every invocation gets of the rest. A space's plan may allow less, never
|
|
26
|
+
* more.
|
|
27
|
+
*/
|
|
23
28
|
limits?: Partial<FunctionLimits>;
|
|
24
29
|
/**
|
|
25
30
|
* Asked before each invocation: why this space may not run code right now — its account spent its CPU for the minute,
|
|
@@ -29,6 +34,17 @@ export type FunctionsConfig = {
|
|
|
29
34
|
/** Told what each invocation spent, however it ended: what `admit` budgets by, and what a deployment meters. */
|
|
30
35
|
onUsage?: (record: FunctionUsageRecord) => void;
|
|
31
36
|
};
|
|
37
|
+
/** What an invocation gets unless its task asks for more CPU or time. */
|
|
32
38
|
export declare const DEFAULT_FUNCTION_LIMITS: FunctionLimits;
|
|
33
|
-
/**
|
|
34
|
-
|
|
39
|
+
/**
|
|
40
|
+
* The most a deployment lets one invocation have, unless it says otherwise: two seconds of CPU and half a minute of
|
|
41
|
+
* time for a task that asks; the rest as every invocation gets it.
|
|
42
|
+
*/
|
|
43
|
+
export declare const DEFAULT_FUNCTION_CEILINGS: FunctionLimits;
|
|
44
|
+
/** A deployment's ceilings, each unset one the default's. */
|
|
45
|
+
export declare const functionCeilings: (ceilings?: Partial<FunctionLimits>) => FunctionLimits;
|
|
46
|
+
/**
|
|
47
|
+
* What one invocation of a space may spend: the CPU and time its task asked for — the default's when it asked for none
|
|
48
|
+
* — and the rest as the deployment gives it; never above the space's plan, nor above the deployment's ceilings.
|
|
49
|
+
*/
|
|
50
|
+
export declare const functionLimitsFor: (ceilings?: Partial<FunctionLimits>, space?: Partial<FunctionLimits>, asked?: FunctionTimeLimits) => FunctionLimits;
|
|
@@ -1,24 +1,40 @@
|
|
|
1
|
+
import { DEFAULT_FUNCTION_TIME_LIMITS } from "@plitzi/sdk-shared/actions";
|
|
1
2
|
//#region src/modules/functions/config.ts
|
|
3
|
+
/** What an invocation gets unless its task asks for more CPU or time. */
|
|
2
4
|
var DEFAULT_FUNCTION_LIMITS = {
|
|
3
|
-
|
|
4
|
-
wallMs: 1e4,
|
|
5
|
+
...DEFAULT_FUNCTION_TIME_LIMITS,
|
|
5
6
|
memoryMb: 64,
|
|
6
7
|
outputBytes: 1e6,
|
|
7
8
|
calls: 100
|
|
8
9
|
};
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The most a deployment lets one invocation have, unless it says otherwise: two seconds of CPU and half a minute of
|
|
12
|
+
* time for a task that asks; the rest as every invocation gets it.
|
|
13
|
+
*/
|
|
14
|
+
var DEFAULT_FUNCTION_CEILINGS = {
|
|
15
|
+
...DEFAULT_FUNCTION_LIMITS,
|
|
16
|
+
cpuMs: 2e3,
|
|
17
|
+
wallMs: 3e4
|
|
18
|
+
};
|
|
19
|
+
/** A deployment's ceilings, each unset one the default's. */
|
|
20
|
+
var functionCeilings = (ceilings = {}) => ({
|
|
21
|
+
...DEFAULT_FUNCTION_CEILINGS,
|
|
22
|
+
...ceilings
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* What one invocation of a space may spend: the CPU and time its task asked for — the default's when it asked for none
|
|
26
|
+
* — and the rest as the deployment gives it; never above the space's plan, nor above the deployment's ceilings.
|
|
27
|
+
*/
|
|
28
|
+
var functionLimitsFor = (ceilings = {}, space = {}, asked = {}) => {
|
|
29
|
+
const ceiling = functionCeilings(ceilings);
|
|
30
|
+
const capOf = (key) => Math.min(space[key] ?? ceiling[key], ceiling[key]);
|
|
15
31
|
return {
|
|
16
|
-
cpuMs: Math.min(
|
|
17
|
-
wallMs: Math.min(
|
|
18
|
-
memoryMb:
|
|
19
|
-
outputBytes:
|
|
20
|
-
calls:
|
|
32
|
+
cpuMs: Math.min(asked.cpuMs ?? DEFAULT_FUNCTION_LIMITS.cpuMs, capOf("cpuMs")),
|
|
33
|
+
wallMs: Math.min(asked.wallMs ?? DEFAULT_FUNCTION_LIMITS.wallMs, capOf("wallMs")),
|
|
34
|
+
memoryMb: capOf("memoryMb"),
|
|
35
|
+
outputBytes: capOf("outputBytes"),
|
|
36
|
+
calls: capOf("calls")
|
|
21
37
|
};
|
|
22
38
|
};
|
|
23
39
|
//#endregion
|
|
24
|
-
export { DEFAULT_FUNCTION_LIMITS, functionLimitsFor };
|
|
40
|
+
export { DEFAULT_FUNCTION_CEILINGS, DEFAULT_FUNCTION_LIMITS, functionCeilings, functionLimitsFor };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RateCount, RateLimit } from '../actions/runtime/rateLimit';
|
|
2
2
|
import { ActionKvStore } from '../actions/types';
|
|
3
|
-
import { Environment, InteractionCallbackParam } from '@plitzi/sdk-shared';
|
|
3
|
+
import { Environment, FunctionTimeLimits, InteractionCallbackParam } from '@plitzi/sdk-shared';
|
|
4
4
|
/**
|
|
5
5
|
* Who started the run, as a space's own code may know them: everything a function decides by — who, their roles and
|
|
6
6
|
* permissions — and never their session. A token inside the sandbox is a token a function could send anywhere.
|
|
@@ -85,6 +85,11 @@ export type FunctionTask<T extends Record<string, unknown> = Record<string, unkn
|
|
|
85
85
|
title: string;
|
|
86
86
|
description?: string;
|
|
87
87
|
params: Record<keyof T, InteractionCallbackParam<T>>;
|
|
88
|
+
/**
|
|
89
|
+
* More CPU or time than an invocation gets by default — `{ cpuMs: 1000 }` for a task that reshapes a big feed — up to
|
|
90
|
+
* what the server allows. Over the functions' own `limits`, for this task.
|
|
91
|
+
*/
|
|
92
|
+
limits?: FunctionTimeLimits;
|
|
88
93
|
run: (params: T, ctx: FunctionContext) => unknown;
|
|
89
94
|
};
|
|
90
95
|
/** What a route's handler gets besides the request: the context, and the `:params` its key named, by name. */
|
|
@@ -92,7 +97,7 @@ export type FunctionRouteContext = FunctionContext & {
|
|
|
92
97
|
params: Record<string, string>;
|
|
93
98
|
};
|
|
94
99
|
/**
|
|
95
|
-
* An HTTP endpoint of the space, under `/
|
|
100
|
+
* An HTTP endpoint of the space, under `/fn/`: a web-standard handler, the shape every edge runtime runs. The request
|
|
96
101
|
* carries no credentials — `ctx.user` says who is asking — and the response sets no cookies: the host's are the
|
|
97
102
|
* platform's.
|
|
98
103
|
*/
|
|
@@ -104,8 +109,10 @@ export type FunctionsDefinition = {
|
|
|
104
109
|
hosts?: string[];
|
|
105
110
|
};
|
|
106
111
|
tasks?: FunctionTask<never>[];
|
|
107
|
-
/** `'GET /board-assets/:board/:asset'` → a handler, served at `/
|
|
112
|
+
/** `'GET /board-assets/:board/:asset'` → a handler, served at `/fn/board-assets/…`. */
|
|
108
113
|
routes?: Record<string, FunctionRoute>;
|
|
114
|
+
/** More CPU or time than an invocation gets by default, for every task and route — a task may ask for its own. */
|
|
115
|
+
limits?: FunctionTimeLimits;
|
|
109
116
|
};
|
|
110
117
|
/**
|
|
111
118
|
* What a function throws to refuse, with a reason written for whoever asked — a wrong password, a board that is
|
|
@@ -14,9 +14,11 @@ var describeFunctions = (definition) => {
|
|
|
14
14
|
action: task.action,
|
|
15
15
|
title: task.title,
|
|
16
16
|
description: task.description,
|
|
17
|
-
params: task.params
|
|
17
|
+
params: task.params,
|
|
18
|
+
limits: task.limits
|
|
18
19
|
} : {}),
|
|
19
|
-
routes: isRecord(found.routes) ? Object.keys(found.routes) : []
|
|
20
|
+
routes: isRecord(found.routes) ? Object.keys(found.routes) : [],
|
|
21
|
+
limits: found.limits
|
|
20
22
|
};
|
|
21
23
|
};
|
|
22
24
|
/**
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { buildFunctions } from "./build.js";
|
|
2
2
|
import { readFunctionsSource } from "@plitzi/sdk-shared/actions";
|
|
3
|
-
import {
|
|
3
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
4
4
|
import { readFile, readdir } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
//#region src/modules/functions/load.ts
|
|
6
7
|
var nodeReader = {
|
|
7
8
|
list: async (dir) => (await readdir(dir, { withFileTypes: true })).map((entry) => ({
|
|
@@ -10,7 +11,6 @@ var nodeReader = {
|
|
|
10
11
|
})),
|
|
11
12
|
read: (file) => readFile(file, "utf8")
|
|
12
13
|
};
|
|
13
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
14
|
/** Whether a module's default export has a definition's shape: what a server can register without surprises. */
|
|
15
15
|
var isFunctionsDefinition = (value) => {
|
|
16
16
|
if (!isRecord(value)) return false;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FunctionLimits } from './protocol';
|
|
1
2
|
import { FunctionsManifest } from '@plitzi/sdk-shared';
|
|
2
3
|
export declare const ROUTE_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
3
4
|
export type RouteMethod = (typeof ROUTE_METHODS)[number];
|
|
@@ -6,7 +7,7 @@ export type RouteKey = {
|
|
|
6
7
|
segments: string[];
|
|
7
8
|
};
|
|
8
9
|
/**
|
|
9
|
-
* A route key, `'<METHOD> /<path>'`: literal segments and `:params`, served under `/
|
|
10
|
+
* A route key, `'<METHOD> /<path>'`: literal segments and `:params`, served under `/fn/`. Anything else — a query, a
|
|
10
11
|
* wildcard, `..` — is not a key, so what a space may answer is only ever a path it spelled out.
|
|
11
12
|
*/
|
|
12
13
|
export declare const parseRouteKey: (key: string) => RouteKey | undefined;
|
|
@@ -19,6 +20,7 @@ export type ManifestReading = {
|
|
|
19
20
|
* afterwards, because what is stored is only ever what passed it.
|
|
20
21
|
*
|
|
21
22
|
* `reserved` is every namespace this deployment's own tasks use: a space's task may take none of them, so a step that
|
|
22
|
-
* names a platform task always runs the platform's.
|
|
23
|
+
* names a platform task always runs the platform's. `ceilings` is the most this deployment gives one invocation: what a
|
|
24
|
+
* task may ask for.
|
|
23
25
|
*/
|
|
24
|
-
export declare const readManifest: (value: unknown, reserved: ReadonlySet<string
|
|
26
|
+
export declare const readManifest: (value: unknown, reserved: ReadonlySet<string>, ceilings: FunctionLimits) => ManifestReading;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { taskName, taskNameProblem } from "../actions/tasks/registry.js";
|
|
2
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
2
3
|
//#region src/modules/functions/manifest.ts
|
|
3
4
|
var ROUTE_METHODS = [
|
|
4
5
|
"GET",
|
|
@@ -10,7 +11,7 @@ var ROUTE_METHODS = [
|
|
|
10
11
|
var SEGMENT = /^(:[a-zA-Z][a-zA-Z0-9]*|[A-Za-z0-9._~-]+)$/;
|
|
11
12
|
var isRouteMethod = (method) => ROUTE_METHODS.some((known) => known === method);
|
|
12
13
|
/**
|
|
13
|
-
* A route key, `'<METHOD> /<path>'`: literal segments and `:params`, served under `/
|
|
14
|
+
* A route key, `'<METHOD> /<path>'`: literal segments and `:params`, served under `/fn/`. Anything else — a query, a
|
|
14
15
|
* wildcard, `..` — is not a key, so what a space may answer is only ever a path it spelled out.
|
|
15
16
|
*/
|
|
16
17
|
var parseRouteKey = (key) => {
|
|
@@ -24,7 +25,6 @@ var parseRouteKey = (key) => {
|
|
|
24
25
|
};
|
|
25
26
|
};
|
|
26
27
|
var HOST = /^(\*\.)?([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
27
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
28
|
var stringOf = (value) => typeof value === "string" ? value : void 0;
|
|
29
29
|
var optionsOf = (value) => Array.isArray(value) ? value.flatMap((option) => {
|
|
30
30
|
const label = isRecord(option) ? stringOf(option.label) : void 0;
|
|
@@ -83,14 +83,41 @@ var paramOf = (value) => {
|
|
|
83
83
|
default: return `has a type the builder cannot draw ("${String(value.type)}"): text, textarea, codemirror-text, codemirror-json, boolean, elements or select`;
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
|
+
/** What each asked limit is, in words a problem is written in. */
|
|
87
|
+
var LIMIT_WORDS = {
|
|
88
|
+
cpuMs: "of CPU",
|
|
89
|
+
wallMs: "to finish"
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* What `where` asks for beyond the default, read: whole milliseconds above nothing — and never more than `ceilings`, which
|
|
93
|
+
* is said rather than quietly cut down, so the author knows what the code will get.
|
|
94
|
+
*/
|
|
95
|
+
var limitsOf = (value, where, ceilings, problems) => {
|
|
96
|
+
if (value === void 0) return;
|
|
97
|
+
if (!isRecord(value)) {
|
|
98
|
+
problems.push(`${where}: limits is not an object like { cpuMs: 1000, wallMs: 20000 }`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const limits = {};
|
|
102
|
+
["cpuMs", "wallMs"].forEach((key) => {
|
|
103
|
+
const asked = value[key];
|
|
104
|
+
if (asked === void 0) return;
|
|
105
|
+
if (typeof asked !== "number" || !Number.isInteger(asked) || asked <= 0) problems.push(`${where}: limits.${key} is not a whole number of milliseconds above 0`);
|
|
106
|
+
else if (asked > ceilings[key]) problems.push(`${where}: asks for ${String(asked)} ms ${LIMIT_WORDS[key]}, and this server allows at most ${String(ceilings[key])} ms`);
|
|
107
|
+
else limits[key] = asked;
|
|
108
|
+
});
|
|
109
|
+
Object.keys(value).filter((key) => key !== "cpuMs" && key !== "wallMs").forEach((key) => problems.push(`${where}: limits.${key} is not a limit a function asks for (cpuMs, wallMs)`));
|
|
110
|
+
return Object.keys(limits).length ? limits : void 0;
|
|
111
|
+
};
|
|
86
112
|
/**
|
|
87
113
|
* Reads what a bundle said it declares, and every rule it must meet — the one validator, at save and nowhere else
|
|
88
114
|
* afterwards, because what is stored is only ever what passed it.
|
|
89
115
|
*
|
|
90
116
|
* `reserved` is every namespace this deployment's own tasks use: a space's task may take none of them, so a step that
|
|
91
|
-
* names a platform task always runs the platform's.
|
|
117
|
+
* names a platform task always runs the platform's. `ceilings` is the most this deployment gives one invocation: what a
|
|
118
|
+
* task may ask for.
|
|
92
119
|
*/
|
|
93
|
-
var readManifest = (value, reserved) => {
|
|
120
|
+
var readManifest = (value, reserved, ceilings) => {
|
|
94
121
|
const problems = [];
|
|
95
122
|
const raw = isRecord(value) ? value : {};
|
|
96
123
|
const hosts = Array.isArray(raw.hosts) ? raw.hosts.filter((host) => typeof host === "string") : [];
|
|
@@ -130,12 +157,14 @@ var readManifest = (value, reserved) => {
|
|
|
130
157
|
else params[key] = read;
|
|
131
158
|
});
|
|
132
159
|
const description = stringOf(task.description);
|
|
160
|
+
const limits = limitsOf(task.limits, where, ceilings, problems);
|
|
133
161
|
tasks.push({
|
|
134
162
|
namespace,
|
|
135
163
|
action,
|
|
136
164
|
title,
|
|
137
165
|
...description ? { description } : {},
|
|
138
|
-
params
|
|
166
|
+
params,
|
|
167
|
+
...limits ? { limits } : {}
|
|
139
168
|
});
|
|
140
169
|
});
|
|
141
170
|
const routes = Array.isArray(raw.routes) ? raw.routes.filter((route) => typeof route === "string") : [];
|
|
@@ -150,11 +179,13 @@ var readManifest = (value, reserved) => {
|
|
|
150
179
|
if (routed.has(shape)) problems.push(`Route "${route}" answers the same requests as another one`);
|
|
151
180
|
routed.add(shape);
|
|
152
181
|
});
|
|
182
|
+
const limits = limitsOf(raw.limits, "The functions", ceilings, problems);
|
|
153
183
|
return {
|
|
154
184
|
manifest: {
|
|
155
185
|
hosts,
|
|
156
186
|
tasks,
|
|
157
|
-
routes
|
|
187
|
+
routes,
|
|
188
|
+
...limits ? { limits } : {}
|
|
158
189
|
},
|
|
159
190
|
problems
|
|
160
191
|
};
|
|
@@ -31,6 +31,6 @@ export type RouteSources = {
|
|
|
31
31
|
* each run with the same context a task of theirs gets, `trigger: 'route'`.
|
|
32
32
|
*/
|
|
33
33
|
export declare const createRoutes: ({ config, getFunctions, taskContext }: RouteSources) => {
|
|
34
|
-
/** The route that answers `method path` (the path after `/
|
|
34
|
+
/** The route that answers `method path` (the path after `/fn`), for this visit — or none. */
|
|
35
35
|
routeFor: (visit: RouteVisit, method: string, path: string) => Promise<RouteHandler | undefined>;
|
|
36
36
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { serverLog } from "../../helpers/serverLog.js";
|
|
2
2
|
import { ActionRefusal } from "../actions/runtime/errors.js";
|
|
3
3
|
import { functionContextFor } from "./context.js";
|
|
4
|
+
import { functionLimitsFor } from "./config.js";
|
|
4
5
|
import { parseRouteKey } from "./manifest.js";
|
|
5
6
|
import { answerCall, readCall, responseOf, wireRequestOf, wireResponseFrom } from "./capabilities.js";
|
|
6
|
-
import { functionLimitsFor } from "./config.js";
|
|
7
7
|
import { FunctionFailure } from "./protocol.js";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
//#region src/modules/functions/routes.ts
|
|
@@ -151,7 +151,7 @@ var createRoutes = ({ config, getFunctions, taskContext }) => {
|
|
|
151
151
|
...fnCtx.user ? { user: fnCtx.user } : {}
|
|
152
152
|
}
|
|
153
153
|
},
|
|
154
|
-
limits: functionLimitsFor(config.limits, functions.limits),
|
|
154
|
+
limits: functionLimitsFor(config.limits, functions.limits, functions.manifest.limits),
|
|
155
155
|
answer: (call) => answerCall(fnCtx, readCall(call)),
|
|
156
156
|
signal: visit.signal,
|
|
157
157
|
onUsage: (usage) => {
|
|
@@ -174,7 +174,7 @@ var createRoutes = ({ config, getFunctions, taskContext }) => {
|
|
|
174
174
|
};
|
|
175
175
|
};
|
|
176
176
|
return {
|
|
177
|
-
/** The route that answers `method path` (the path after `/
|
|
177
|
+
/** The route that answers `method path` (the path after `/fn`), for this visit — or none. */
|
|
178
178
|
routeFor: async (visit, method, path) => nativeRoute(visit, method, path) ?? await spaceRoute(visit, method, path) };
|
|
179
179
|
};
|
|
180
180
|
//#endregion
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
1
2
|
//#region src/modules/functions/runner/messages.ts
|
|
2
3
|
var ENVIRONMENTS = {
|
|
3
4
|
production: true,
|
|
@@ -6,7 +7,6 @@ var ENVIRONMENTS = {
|
|
|
6
7
|
main: true
|
|
7
8
|
};
|
|
8
9
|
var isEnvironment = (value) => typeof value === "string" && Object.hasOwn(ENVIRONMENTS, value);
|
|
9
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
10
|
var parse = (data) => {
|
|
11
11
|
try {
|
|
12
12
|
const value = JSON.parse(data);
|
|
@@ -2,12 +2,13 @@ import { FunctionsSource } from './build';
|
|
|
2
2
|
import { FunctionsConfig } from './config';
|
|
3
3
|
import { FunctionsBundle, FunctionLimits, FunctionRunner, SpaceFunctions } from './protocol';
|
|
4
4
|
import { ActionTask, ActionTaskRegistry } from '../actions/types';
|
|
5
|
-
import { FunctionsManifest, FunctionsProblem } from '@plitzi/sdk-shared';
|
|
5
|
+
import { FunctionsManifest, FunctionsProblem, FunctionTimeLimits } from '@plitzi/sdk-shared';
|
|
6
6
|
/**
|
|
7
7
|
* A space's tasks as the registry holds them — each run by the runner, answering the code's calls with the run's own
|
|
8
|
-
* {@link FunctionContext}: the same one native functions get, built by the same function.
|
|
8
|
+
* {@link FunctionContext}: the same one native functions get, built by the same function. Each is given what
|
|
9
|
+
* `limitsFor` answers for what it asked: its own limits, over its functions'.
|
|
9
10
|
*/
|
|
10
|
-
export declare const spaceTasks: (functions: SpaceFunctions, runner: FunctionRunner,
|
|
11
|
+
export declare const spaceTasks: (functions: SpaceFunctions, runner: FunctionRunner, limitsFor: (asked: FunctionTimeLimits) => FunctionLimits, { admit, onUsage }?: Pick<FunctionsConfig, "admit" | "onUsage">) => ActionTask<Record<string, unknown>>[];
|
|
11
12
|
export type SpaceRegistries = {
|
|
12
13
|
/** The tasks one run of that space can use: the deployment's, and its own functions' when it has any. */
|
|
13
14
|
registryFor: (functions: SpaceFunctions | undefined) => ActionTaskRegistry;
|
|
@@ -35,6 +36,7 @@ export type PreparedFunctions = {
|
|
|
35
36
|
};
|
|
36
37
|
/**
|
|
37
38
|
* A space's source made ready to save: built, read by the runner, and checked — the only way a space's functions are
|
|
38
|
-
* stored, so what is stored always passed every rule. `reserved` is every namespace the deployment's own tasks use
|
|
39
|
+
* stored, so what is stored always passed every rule. `reserved` is every namespace the deployment's own tasks use;
|
|
40
|
+
* `ceilings`, the most it gives one invocation — what a task may ask for.
|
|
39
41
|
*/
|
|
40
|
-
export declare const prepareFunctions: (source: FunctionsSource, runner: FunctionRunner, reserved: ReadonlySet<string
|
|
42
|
+
export declare const prepareFunctions: (source: FunctionsSource, runner: FunctionRunner, reserved: ReadonlySet<string>, ceilings: FunctionLimits) => Promise<PreparedFunctions>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ActionRefusal } from "../actions/runtime/errors.js";
|
|
2
2
|
import { functionContextFor } from "./context.js";
|
|
3
3
|
import { taskName } from "../actions/tasks/registry.js";
|
|
4
|
+
import { functionLimitsFor } from "./config.js";
|
|
4
5
|
import { readManifest } from "./manifest.js";
|
|
5
6
|
import { answerCall, readCall } from "./capabilities.js";
|
|
6
|
-
import { functionLimitsFor } from "./config.js";
|
|
7
7
|
import { FunctionFailure } from "./protocol.js";
|
|
8
8
|
import { FunctionsBuildError, buildFunctions } from "./build.js";
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
@@ -18,10 +18,15 @@ var invocationContextOf = ({ spaceId, environment, runId, trigger, callerId, use
|
|
|
18
18
|
});
|
|
19
19
|
/**
|
|
20
20
|
* A space's tasks as the registry holds them — each run by the runner, answering the code's calls with the run's own
|
|
21
|
-
* {@link FunctionContext}: the same one native functions get, built by the same function.
|
|
21
|
+
* {@link FunctionContext}: the same one native functions get, built by the same function. Each is given what
|
|
22
|
+
* `limitsFor` answers for what it asked: its own limits, over its functions'.
|
|
22
23
|
*/
|
|
23
|
-
var spaceTasks = (functions, runner,
|
|
24
|
+
var spaceTasks = (functions, runner, limitsFor, { admit, onUsage } = {}) => functions.manifest.tasks.map((task) => {
|
|
24
25
|
const name = taskName(task);
|
|
26
|
+
const limits = limitsFor({
|
|
27
|
+
...functions.manifest.limits,
|
|
28
|
+
...task.limits
|
|
29
|
+
});
|
|
25
30
|
return {
|
|
26
31
|
namespace: task.namespace,
|
|
27
32
|
action: task.action,
|
|
@@ -73,7 +78,8 @@ var composeRegistry = (base, tasks) => {
|
|
|
73
78
|
const name = taskName(task);
|
|
74
79
|
if (!base.get(name)) own.set(name, {
|
|
75
80
|
...task,
|
|
76
|
-
name
|
|
81
|
+
name,
|
|
82
|
+
origin: "space"
|
|
77
83
|
});
|
|
78
84
|
});
|
|
79
85
|
return {
|
|
@@ -91,11 +97,11 @@ var createSpaceRegistries = (base, config = {}) => {
|
|
|
91
97
|
return { registryFor: (functions) => {
|
|
92
98
|
const runner = functions?.runner ?? config.runner;
|
|
93
99
|
if (!functions || !runner) return base;
|
|
94
|
-
const
|
|
95
|
-
const key = `${functions.bundle.id}:${JSON.stringify(limits)}`;
|
|
100
|
+
const key = `${functions.bundle.id}:${JSON.stringify(functions.limits ?? {})}`;
|
|
96
101
|
const found = cache.get(key);
|
|
97
102
|
if (found) return found;
|
|
98
|
-
const
|
|
103
|
+
const limitsFor = (asked) => functionLimitsFor(config.limits, functions.limits, asked);
|
|
104
|
+
const registry = composeRegistry(base, spaceTasks(functions, runner, limitsFor, config));
|
|
99
105
|
cache.set(key, registry);
|
|
100
106
|
if (cache.size > REGISTRY_CACHE) {
|
|
101
107
|
const oldest = cache.keys().next().value;
|
|
@@ -114,9 +120,10 @@ var functionsInHand = ({ bundle, manifest }) => ({
|
|
|
114
120
|
});
|
|
115
121
|
/**
|
|
116
122
|
* A space's source made ready to save: built, read by the runner, and checked — the only way a space's functions are
|
|
117
|
-
* stored, so what is stored always passed every rule. `reserved` is every namespace the deployment's own tasks use
|
|
123
|
+
* stored, so what is stored always passed every rule. `reserved` is every namespace the deployment's own tasks use;
|
|
124
|
+
* `ceilings`, the most it gives one invocation — what a task may ask for.
|
|
118
125
|
*/
|
|
119
|
-
var prepareFunctions = async (source, runner, reserved) => {
|
|
126
|
+
var prepareFunctions = async (source, runner, reserved, ceilings) => {
|
|
120
127
|
let code;
|
|
121
128
|
try {
|
|
122
129
|
({code} = await buildFunctions(source));
|
|
@@ -143,7 +150,7 @@ var prepareFunctions = async (source, runner, reserved) => {
|
|
|
143
150
|
}]
|
|
144
151
|
};
|
|
145
152
|
}
|
|
146
|
-
const { manifest, problems } = readManifest(declared, reserved);
|
|
153
|
+
const { manifest, problems } = readManifest(declared, reserved, ceilings);
|
|
147
154
|
return problems.length ? {
|
|
148
155
|
ok: false,
|
|
149
156
|
problems: problems.map((message) => ({
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a host is one a picture may come from: named exactly (`images.example.com`), or under a wildcard
|
|
3
|
+
* (`*.example.com` — its subdomains, not `example.com` itself, which is named on its own when it is meant).
|
|
4
|
+
*/
|
|
5
|
+
export declare const isAllowedImageHost: (domains: readonly string[], hostname: string) => boolean;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/modules/images/allowlist.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether a host is one a picture may come from: named exactly (`images.example.com`), or under a wildcard
|
|
4
|
+
* (`*.example.com` — its subdomains, not `example.com` itself, which is named on its own when it is meant).
|
|
5
|
+
*/
|
|
6
|
+
var isAllowedImageHost = (domains, hostname) => {
|
|
7
|
+
const host = hostname.toLowerCase().replace(/\.$/, "");
|
|
8
|
+
return domains.some((entry) => {
|
|
9
|
+
const domain = entry.toLowerCase().trim();
|
|
10
|
+
if (domain.startsWith("*.")) {
|
|
11
|
+
const parent = domain.slice(2);
|
|
12
|
+
return host.endsWith(`.${parent}`);
|
|
13
|
+
}
|
|
14
|
+
return host === domain;
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { isAllowedImageHost };
|