@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.
Files changed (127) hide show
  1. package/CHANGELOG.md +825 -0
  2. package/README.md +24 -2
  3. package/dist/actions.d.ts +1 -1
  4. package/dist/adapters/cloudAdapters.d.ts +5 -3
  5. package/dist/adapters/cloudAdapters.js +123 -63
  6. package/dist/adapters/space/types.d.ts +3 -0
  7. package/dist/core/configSeam.d.ts +14 -0
  8. package/dist/core/configSeam.js +114 -0
  9. package/dist/core/configSeam.test.d.ts +1 -0
  10. package/dist/core/createServer.js +11 -5
  11. package/dist/core/createServer.publicData.test.d.ts +1 -0
  12. package/dist/core/freePort.d.ts +10 -0
  13. package/dist/core/freePort.js +36 -0
  14. package/dist/core/freePort.test.d.ts +1 -0
  15. package/dist/core/http/stages/images.d.ts +6 -0
  16. package/dist/core/http/stages/images.js +47 -0
  17. package/dist/core/http/stages/images.test.d.ts +1 -0
  18. package/dist/core/server/fleet/channel.js +1 -1
  19. package/dist/core/server/fleet/relay.js +1 -1
  20. package/dist/core/services/action.js +5 -4
  21. package/dist/core/services/functionRoutes.d.ts +2 -2
  22. package/dist/core/services/functionRoutes.js +2 -2
  23. package/dist/core/services/registry.js +2 -0
  24. package/dist/functions-api.d.ts +23 -2
  25. package/dist/functions.d.ts +1 -1
  26. package/dist/functions.js +2 -2
  27. package/dist/helpers/cache/keys.d.ts +23 -10
  28. package/dist/helpers/cache/keys.js +16 -2
  29. package/dist/helpers/flagOverrides.d.ts +27 -0
  30. package/dist/helpers/flagOverrides.js +25 -0
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +2 -1
  33. package/dist/modules/actions/index.d.ts +1 -1
  34. package/dist/modules/actions/index.js +3 -2
  35. package/dist/modules/actions/moduleFor.js +5 -3
  36. package/dist/modules/actions/runtime/flags.d.ts +16 -0
  37. package/dist/modules/actions/runtime/flags.js +49 -0
  38. package/dist/modules/actions/runtime/flags.test.d.ts +1 -0
  39. package/dist/modules/actions/runtime/kvList.js +1 -1
  40. package/dist/modules/actions/runtime/renderResolver.js +30 -4
  41. package/dist/modules/actions/runtime/runAction.js +2 -0
  42. package/dist/modules/actions/taskCatalog.d.ts +3 -1
  43. package/dist/modules/actions/taskCatalog.js +1 -0
  44. package/dist/modules/actions/tasks/registry.js +4 -2
  45. package/dist/modules/actions/transport/callHandler.js +8 -0
  46. package/dist/modules/actions/types.d.ts +20 -1
  47. package/dist/modules/connectors/engine.js +1 -1
  48. package/dist/modules/functions/capabilities.js +1 -1
  49. package/dist/modules/functions/config.d.ts +19 -3
  50. package/dist/modules/functions/config.js +30 -14
  51. package/dist/modules/functions/contract.d.ts +10 -3
  52. package/dist/modules/functions/driver.js +4 -2
  53. package/dist/modules/functions/load.js +2 -2
  54. package/dist/modules/functions/manifest.d.ts +5 -3
  55. package/dist/modules/functions/manifest.js +37 -6
  56. package/dist/modules/functions/routes.d.ts +1 -1
  57. package/dist/modules/functions/routes.js +3 -3
  58. package/dist/modules/functions/runner/messages.js +1 -1
  59. package/dist/modules/functions/space.d.ts +7 -5
  60. package/dist/modules/functions/space.js +17 -10
  61. package/dist/modules/images/allowlist.d.ts +5 -0
  62. package/dist/modules/images/allowlist.js +18 -0
  63. package/dist/modules/images/imageProxy.d.ts +36 -0
  64. package/dist/modules/images/imageProxy.js +273 -0
  65. package/dist/modules/images/imageProxy.test.d.ts +1 -0
  66. package/dist/modules/images/transform.d.ts +10 -0
  67. package/dist/modules/images/transform.js +28 -0
  68. package/dist/modules/images/transform.test.d.ts +1 -0
  69. package/dist/modules/rsc/connectorRscData.d.ts +12 -7
  70. package/dist/modules/rsc/connectorRscData.js +12 -8
  71. package/dist/modules/rsc/handler.js +18 -4
  72. package/dist/modules/rsc/publicFileResolver.d.ts +13 -0
  73. package/dist/modules/rsc/publicFileResolver.js +39 -0
  74. package/dist/modules/rsc/publicFileResolver.test.d.ts +1 -0
  75. package/dist/modules/rsc/resolveRscData.d.ts +4 -2
  76. package/dist/modules/rsc/resolveRscData.js +10 -2
  77. package/dist/modules/runtime/bundle.js +2 -2
  78. package/dist/modules/runtime/contract.d.ts +1 -1
  79. package/dist/modules/runtime/stages.d.ts +1 -1
  80. package/dist/modules/runtime/stages.js +1 -1
  81. package/dist/modules/ssr/Component.d.ts +5 -1
  82. package/dist/modules/ssr/Component.js +3 -1
  83. package/dist/modules/ssr/prepareRender.js +38 -10
  84. package/dist/modules/ssr/registerExternalPlugins.d.ts +5 -1
  85. package/dist/modules/ssr/registerExternalPlugins.js +6 -2
  86. package/package.json +9 -4
  87. package/skills/plitzi-authoring/CHEATSHEET.md +135 -0
  88. package/skills/plitzi-authoring/SKILL.md +77 -74
  89. package/skills/plitzi-authoring/recipes/carousel.ts +114 -0
  90. package/skills/plitzi-authoring/recipes/detail-page.ts +70 -0
  91. package/skills/plitzi-authoring/recipes/embed-and-svg.ts +48 -0
  92. package/skills/plitzi-authoring/recipes/every-few-seconds.ts +119 -0
  93. package/skills/plitzi-authoring/recipes/feature-flag.ts +24 -0
  94. package/skills/plitzi-authoring/recipes/filter-a-list.ts +63 -0
  95. package/skills/plitzi-authoring/recipes/forms-and-modals.ts +64 -0
  96. package/skills/plitzi-authoring/recipes/from-tailwind.ts +52 -0
  97. package/skills/plitzi-authoring/recipes/link-to-a-section.ts +35 -0
  98. package/skills/plitzi-authoring/recipes/link-with-data.ts +40 -0
  99. package/skills/plitzi-authoring/recipes/marquee.ts +57 -0
  100. package/skills/plitzi-authoring/recipes/plugin.ts +53 -0
  101. package/skills/plitzi-authoring/recipes/scroll-a-row.ts +79 -0
  102. package/skills/plitzi-authoring/recipes/show-data.ts +73 -0
  103. package/skills/plitzi-authoring/recipes/style-a-page.ts +45 -0
  104. package/skills/plitzi-authoring/recipes/typed-data.ts +64 -0
  105. package/skills/plitzi-authoring/recipes/usable-without-sight.ts +39 -0
  106. package/skills/plitzi-authoring/reference/accessibility.md +2 -2
  107. package/skills/plitzi-authoring/reference/authoring-errors.md +195 -53
  108. package/skills/plitzi-authoring/reference/colours-and-motion.md +63 -0
  109. package/skills/plitzi-authoring/reference/components.md +83 -0
  110. package/skills/plitzi-authoring/reference/data-and-visibility.md +29 -29
  111. package/skills/plitzi-authoring/reference/efficiency.md +53 -0
  112. package/skills/plitzi-authoring/reference/elements-and-styles.md +39 -24
  113. package/skills/plitzi-authoring/reference/feature-flags.md +78 -0
  114. package/skills/plitzi-authoring/reference/flows.md +26 -37
  115. package/skills/plitzi-authoring/reference/kept-state.md +28 -0
  116. package/skills/plitzi-authoring/reference/layouts.md +20 -3
  117. package/skills/plitzi-authoring/reference/lists.md +45 -16
  118. package/skills/plitzi-authoring/reference/plugins.md +27 -0
  119. package/skills/plitzi-authoring/reference/realtime.md +35 -0
  120. package/skills/plitzi-authoring/reference/review-checklist.md +8 -3
  121. package/skills/plitzi-authoring/reference/{templates-and-export.md → snippets-and-export.md} +15 -8
  122. package/skills/plitzi-authoring/reference/structure.md +29 -20
  123. package/skills/plitzi-authoring/reference/tailwind.md +19 -0
  124. package/skills/plitzi-authoring/reference/templates.md +6 -5
  125. package/skills/plitzi-authoring/reference/testing.md +11 -0
  126. package/skills/plitzi-authoring/reference/typed-sources.md +20 -0
  127. 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
  };
@@ -16,6 +16,7 @@ var describeTask = (task) => ({
16
16
  action: task.action,
17
17
  title: task.title,
18
18
  description: task.description,
19
+ origin: task.origin,
19
20
  params: jsonSafe(task.params)
20
21
  });
21
22
  /** The whole catalog, in the order the registry holds it. */
@@ -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
- /** The most one invocation may spend on this deployment. A space's plan may ask for less, never more. */
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
- /** What one invocation of a space may spend: its own limits, never above the deployment's. */
34
- export declare const functionLimitsFor: (ceilings?: Partial<FunctionLimits>, space?: Partial<FunctionLimits>) => FunctionLimits;
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
- cpuMs: 100,
4
- wallMs: 1e4,
5
+ ...DEFAULT_FUNCTION_TIME_LIMITS,
5
6
  memoryMb: 64,
6
7
  outputBytes: 1e6,
7
8
  calls: 100
8
9
  };
9
- /** What one invocation of a space may spend: its own limits, never above the deployment's. */
10
- var functionLimitsFor = (ceilings = {}, space = {}) => {
11
- const ceiling = {
12
- ...DEFAULT_FUNCTION_LIMITS,
13
- ...ceilings
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(space.cpuMs ?? ceiling.cpuMs, ceiling.cpuMs),
17
- wallMs: Math.min(space.wallMs ?? ceiling.wallMs, ceiling.wallMs),
18
- memoryMb: Math.min(space.memoryMb ?? ceiling.memoryMb, ceiling.memoryMb),
19
- outputBytes: Math.min(space.outputBytes ?? ceiling.outputBytes, ceiling.outputBytes),
20
- calls: Math.min(space.calls ?? ceiling.calls, ceiling.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 `/api/`: a web-standard handler, the shape every edge runtime runs. The request
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 `/api/board-assets/…`. */
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 { fileURLToPath } from "node:url";
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 `/api/`. Anything else — a query, a
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>) => ManifestReading;
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 `/api/`. Anything else — a query, a
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 `/api`), for this visit — or none. */
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 `/api`), for this visit — or none. */
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, limits: FunctionLimits, { admit, onUsage }?: Pick<FunctionsConfig, "admit" | "onUsage">) => ActionTask<Record<string, unknown>>[];
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>) => Promise<PreparedFunctions>;
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, limits, { admit, onUsage } = {}) => functions.manifest.tasks.map((task) => {
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 limits = functionLimitsFor(config.limits, functions.limits);
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 registry = composeRegistry(base, spaceTasks(functions, runner, limits, config));
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 };