@plitzi/sdk-server 0.38.4 → 0.38.6

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 (118) hide show
  1. package/CHANGELOG.md +350 -0
  2. package/README.md +126 -17
  3. package/dist/actions.d.ts +8 -5
  4. package/dist/actions.js +3 -1
  5. package/dist/adapters/jsonAdapters.d.ts +1 -1
  6. package/dist/adapters/jsonAdapters.js +16 -3
  7. package/dist/core/configSeam.js +20 -4
  8. package/dist/core/createServer.js +3 -2
  9. package/dist/core/handlers/socialRouteHandlers.js +6 -1
  10. package/dist/core/http/stages/devReload.d.ts +11 -0
  11. package/dist/core/http/stages/devReload.js +17 -0
  12. package/dist/core/server/baseServer.d.ts +2 -0
  13. package/dist/core/server/baseServer.js +3 -0
  14. package/dist/core/server/pageServer.js +18 -0
  15. package/dist/core/services/registry.js +2 -0
  16. package/dist/core/services/rsc.js +1 -1
  17. package/dist/core/services/spaceDocument.d.ts +2 -0
  18. package/dist/core/services/spaceDocument.js +11 -0
  19. package/dist/functions-api.d.ts +6 -1
  20. package/dist/functionsRunner.d.ts +5 -0
  21. package/dist/functionsRunner.js +2 -1
  22. package/dist/helpers/hydrationPayload.d.ts +25 -4
  23. package/dist/helpers/hydrationPayload.js +36 -30
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.js +2 -2
  26. package/dist/modules/actions/index.d.ts +11 -2
  27. package/dist/modules/actions/index.js +29 -7
  28. package/dist/modules/actions/runtime/fileKv.d.ts +28 -0
  29. package/dist/modules/actions/runtime/fileKv.js +95 -0
  30. package/dist/modules/actions/runtime/fileKv.test.d.ts +1 -0
  31. package/dist/modules/actions/runtime/memoryKv.d.ts +23 -0
  32. package/dist/modules/actions/runtime/memoryKv.js +39 -24
  33. package/dist/modules/actions/runtime/namespaceKv.d.ts +2 -0
  34. package/dist/modules/actions/runtime/namespaceKv.js +5 -3
  35. package/dist/modules/actions/runtime/renderResolver.d.ts +3 -2
  36. package/dist/modules/actions/runtime/renderResolver.js +13 -9
  37. package/dist/modules/actions/runtime/renderShare.d.ts +7 -24
  38. package/dist/modules/actions/runtime/renderShare.js +47 -13
  39. package/dist/modules/actions/runtime/runAction.js +15 -1
  40. package/dist/modules/actions/runtime/scope.js +16 -1
  41. package/dist/modules/actions/runtime/signing.js +4 -1
  42. package/dist/modules/actions/runtime/taskContext.d.ts +7 -1
  43. package/dist/modules/actions/runtime/taskContext.js +6 -2
  44. package/dist/modules/actions/tasks/registry.d.ts +3 -1
  45. package/dist/modules/actions/tasks/registry.js +15 -1
  46. package/dist/modules/actions/types.d.ts +15 -2
  47. package/dist/modules/functions/config.d.ts +6 -0
  48. package/dist/modules/functions/context.d.ts +4 -2
  49. package/dist/modules/functions/context.js +43 -21
  50. package/dist/modules/functions/load.d.ts +8 -1
  51. package/dist/modules/functions/load.js +8 -4
  52. package/dist/modules/functions/manifest.d.ts +4 -1
  53. package/dist/modules/functions/manifest.js +14 -2
  54. package/dist/modules/functions/native.d.ts +2 -1
  55. package/dist/modules/functions/native.js +2 -2
  56. package/dist/modules/functions/plugins.test.d.ts +1 -0
  57. package/dist/modules/functions/routes.d.ts +8 -3
  58. package/dist/modules/functions/routes.js +41 -21
  59. package/dist/modules/functions/scope.d.ts +30 -0
  60. package/dist/modules/functions/scope.js +28 -0
  61. package/dist/modules/functions/space.d.ts +10 -5
  62. package/dist/modules/functions/space.js +37 -15
  63. package/dist/modules/rsc/connectorRscData.d.ts +3 -1
  64. package/dist/modules/rsc/connectorRscData.js +8 -4
  65. package/dist/modules/rsc/handler.d.ts +3 -16
  66. package/dist/modules/rsc/handler.js +10 -4
  67. package/dist/modules/rsc/publicFileResolver.d.ts +15 -12
  68. package/dist/modules/rsc/publicFileResolver.js +56 -24
  69. package/dist/modules/rsc/resolveRscData.d.ts +3 -1
  70. package/dist/modules/rsc/resolveRscData.js +8 -5
  71. package/dist/modules/runtime/bundle.d.ts +5 -0
  72. package/dist/modules/runtime/bundle.js +20 -5
  73. package/dist/modules/sqlite/kv.d.ts +34 -0
  74. package/dist/modules/sqlite/kv.js +99 -0
  75. package/dist/modules/sqlite/kv.test.d.ts +1 -0
  76. package/dist/modules/ssr/Component.d.ts +2 -1
  77. package/dist/modules/ssr/Component.js +2 -1
  78. package/dist/modules/ssr/prepareRender.js +48 -6
  79. package/dist/modules/ssr/spaceDocument.d.ts +25 -0
  80. package/dist/modules/ssr/spaceDocument.js +72 -0
  81. package/dist/modules/ssr/spaceDocument.test.d.ts +1 -0
  82. package/dist/modules/ssr/testing/offlineData.d.ts +5 -0
  83. package/dist/modules/ssr/views/template.ejs +122 -20
  84. package/dist/plugins/compile.js +6 -21
  85. package/dist/plugins/compile.test.d.ts +1 -0
  86. package/dist/plugins/copy.d.ts +5 -0
  87. package/dist/plugins/copy.js +16 -1
  88. package/dist/plugins/copy.test.d.ts +1 -0
  89. package/dist/plugins/manager.d.ts +13 -0
  90. package/dist/plugins/manager.js +40 -11
  91. package/dist/plugins/watch.d.ts +12 -0
  92. package/dist/plugins/watch.js +79 -0
  93. package/dist/runtime.d.ts +1 -1
  94. package/dist/runtime.js +2 -2
  95. package/dist/sqlite.d.ts +17 -0
  96. package/dist/sqlite.js +2 -0
  97. package/package.json +8 -3
  98. package/skills/plitzi-authoring/CHEATSHEET.md +4 -2
  99. package/skills/plitzi-authoring/SKILL.md +20 -19
  100. package/skills/plitzi-authoring/recipes/plugin.ts +1 -1
  101. package/skills/plitzi-authoring/recipes/server-data.ts +3 -3
  102. package/skills/plitzi-authoring/recipes/show-data.ts +3 -2
  103. package/skills/plitzi-authoring/recipes/typed-data.ts +1 -1
  104. package/skills/plitzi-authoring/reference/authoring-errors.md +8 -2
  105. package/skills/plitzi-authoring/reference/colours-and-motion.md +3 -1
  106. package/skills/plitzi-authoring/reference/data-and-visibility.md +24 -22
  107. package/skills/plitzi-authoring/reference/drawing.md +24 -0
  108. package/skills/plitzi-authoring/reference/efficiency.md +19 -2
  109. package/skills/plitzi-authoring/reference/flows.md +3 -0
  110. package/skills/plitzi-authoring/reference/layouts.md +8 -23
  111. package/skills/plitzi-authoring/reference/performance.md +2 -2
  112. package/skills/plitzi-authoring/reference/plugins.md +51 -38
  113. package/skills/plitzi-authoring/reference/review-checklist.md +2 -0
  114. package/skills/plitzi-authoring/reference/structure.md +32 -1
  115. package/skills/plitzi-authoring/reference/templates.md +5 -2
  116. package/skills/plitzi-authoring/reference/testing.md +9 -2
  117. package/skills/plitzi-authoring/reference/typed-sources.md +3 -1
  118. package/skills/plitzi-authoring/reference/validation.md +1 -1
@@ -1,5 +1,7 @@
1
+ import { prefixKv } from "../actions/runtime/namespaceKv.js";
1
2
  import { fetchOutbound } from "../../helpers/outboundGuard.js";
2
3
  import { countRate } from "../actions/runtime/rateLimit.js";
4
+ import { pluginKvPrefix, pluginSigned } from "./scope.js";
3
5
  import { processTwigParam } from "@plitzi/sdk-shared/helpers/twigWrapper";
4
6
  //#region src/modules/functions/context.ts
5
7
  /**
@@ -26,7 +28,8 @@ var userOf = ({ id, username, email, verified, permissions, roles }) => ({
26
28
  * the same run budget `http.request` goes through), and — naming a credential — with its values written in where the
27
29
  * request says `{{credential.<key>}}`. The value is resolved here and never handed to the code.
28
30
  */
29
- var functionFetch = (ctx, hosts) => async (input, init = {}) => {
31
+ var functionFetch = (ctx, hosts, scope) => async (input, init = {}) => {
32
+ if (init.credential && scope) throw new Error(`A plugin's functions name no credential of the space's ("${init.credential}")`);
30
33
  const credential = init.credential ? await ctx.credential(init.credential) : void 0;
31
34
  if (init.credential && !credential) throw new Error(`Credential "${init.credential}" is not available for this space`);
32
35
  const render = (value) => credential ? String(processTwigParam(value, { credential })) : value;
@@ -63,26 +66,45 @@ var unavailable = (what) => () => Promise.reject(/* @__PURE__ */ new Error(`This
63
66
  var unsigned = () => Promise.reject(/* @__PURE__ */ new Error("This server signs nothing: its actions were given no signingSecret"));
64
67
  /**
65
68
  * What a space's code is handed, built from the run's own context: the one place a {@link FunctionContext} is made.
66
- * A self-hosted server hands it to the code directly; the platform answers the sandbox's calls with it.
69
+ * A self-hosted server hands it to the code directly; the platform answers the sandbox's calls with it. With a `scope`,
70
+ * it is a plugin's code, and gets the plugin's corner of the space (`./scope`).
67
71
  */
68
- var functionContextFor = (ctx, hosts) => ({
69
- spaceId: ctx.spaceId,
70
- environment: ctx.environment,
71
- runId: ctx.runId,
72
- trigger: ctx.trigger,
73
- callerId: ctx.callerId,
74
- ...ctx.user ? { user: userOf(ctx.user) } : {},
75
- kv: ctx.kv,
76
- rateLimit: (bucket, limit) => countRate(ctx.kv, ctx.callerId, bucket, limit),
77
- sign: ctx.sign ?? unsigned,
78
- verify: ctx.verify ?? unsigned,
79
- fetch: functionFetch(ctx, hosts),
80
- publish: ctx.publish ?? unavailable("publish"),
81
- grant: ctx.grant ?? unavailable("grant"),
82
- revoke: ctx.revoke ?? unavailable("revoke"),
83
- log: (...values) => ctx.log(lineOf(values)),
84
- emit: ctx.emit,
85
- signal: ctx.signal
86
- });
72
+ var functionContextFor = (ctx, hosts, scope) => {
73
+ const shared = {
74
+ spaceId: ctx.spaceId,
75
+ environment: ctx.environment,
76
+ runId: ctx.runId,
77
+ trigger: ctx.trigger,
78
+ callerId: ctx.callerId,
79
+ ...ctx.user ? { user: userOf(ctx.user) } : {},
80
+ fetch: functionFetch(ctx, hosts, scope),
81
+ log: (...values) => ctx.log(lineOf(values)),
82
+ emit: ctx.emit,
83
+ signal: ctx.signal
84
+ };
85
+ if (!scope) return {
86
+ ...shared,
87
+ kv: ctx.kv,
88
+ rateLimit: (bucket, limit) => countRate(ctx.kv, ctx.callerId, bucket, limit),
89
+ sign: ctx.sign ?? unsigned,
90
+ verify: ctx.verify ?? unsigned,
91
+ publish: ctx.publish ?? unavailable("publish"),
92
+ grant: ctx.grant ?? unavailable("grant"),
93
+ revoke: ctx.revoke ?? unavailable("revoke")
94
+ };
95
+ const kv = prefixKv(ctx.kv, pluginKvPrefix(scope.plugin));
96
+ const { sign, verify } = ctx;
97
+ const noChannel = () => Promise.reject(/* @__PURE__ */ new Error("The functions of a plugin reach none of the realtime channels of its space"));
98
+ return {
99
+ ...shared,
100
+ kv,
101
+ rateLimit: (bucket, limit) => countRate(kv, ctx.callerId, bucket, limit),
102
+ sign: sign ? (value) => sign(pluginSigned(scope.plugin, value)) : unsigned,
103
+ verify: verify ? (value, signature) => verify(pluginSigned(scope.plugin, value), signature) : unsigned,
104
+ publish: noChannel,
105
+ grant: noChannel,
106
+ revoke: noChannel
107
+ };
108
+ };
87
109
  //#endregion
88
110
  export { functionContextFor, hostAllowed, lineOf };
@@ -1,15 +1,22 @@
1
1
  import { FunctionsSource } from './build';
2
2
  import { FunctionsDefinition } from './contract';
3
+ /** Whether a module's default export has a definition's shape: what a server can register without surprises. */
4
+ export declare const isFunctionsDefinition: (value: unknown) => value is FunctionsDefinition;
3
5
  /**
4
6
  * A `functions/` directory as a source — the files the platform's rule counts, by their path inside it: what a space's
5
7
  * functions are saved as, and what `loadFunctions` builds.
6
8
  */
7
9
  export declare const readFunctionsDir: (dir: string | URL) => Promise<FunctionsSource>;
8
10
  /**
9
- * A server's own functions from a directory — a project's `functions/`, the working copy `plitzi functions` keeps —
11
+ * A server's own functions from a directory — a project's `src/functions/`, the working copy `plitzi functions` keeps —
10
12
  * built exactly as the platform builds a space's (the same rules, one bundle) and loaded natively, for
11
13
  * `createServer({ functions: { native } })`. The same on `node src/main.ts` and on a compiled server: nothing here
12
14
  * depends on how the server itself runs. A directory with nothing in it is no functions; one that does not build, or
13
15
  * whose `index.ts` exports no definition, stops the server with where it is wrong.
14
16
  */
15
17
  export declare const loadFunctions: (dir: string | URL) => Promise<FunctionsDefinition[]>;
18
+ /**
19
+ * The same, from the files themselves (`FunctionsSource`, by their path): what a packed plugin carries as
20
+ * `functions.source.json`, loaded by a server that runs the plugin as it was packed.
21
+ */
22
+ export declare const loadFunctionsSource: (source: FunctionsSource) => Promise<FunctionsDefinition[]>;
@@ -26,14 +26,18 @@ var isFunctionsDefinition = (value) => {
26
26
  */
27
27
  var readFunctionsDir = (dir) => readFunctionsSource(typeof dir === "string" ? dir : fileURLToPath(dir), nodeReader);
28
28
  /**
29
- * A server's own functions from a directory — a project's `functions/`, the working copy `plitzi functions` keeps —
29
+ * A server's own functions from a directory — a project's `src/functions/`, the working copy `plitzi functions` keeps —
30
30
  * built exactly as the platform builds a space's (the same rules, one bundle) and loaded natively, for
31
31
  * `createServer({ functions: { native } })`. The same on `node src/main.ts` and on a compiled server: nothing here
32
32
  * depends on how the server itself runs. A directory with nothing in it is no functions; one that does not build, or
33
33
  * whose `index.ts` exports no definition, stops the server with where it is wrong.
34
34
  */
35
- var loadFunctions = async (dir) => {
36
- const source = await readFunctionsDir(dir);
35
+ var loadFunctions = async (dir) => loadFunctionsSource(await readFunctionsDir(dir));
36
+ /**
37
+ * The same, from the files themselves (`FunctionsSource`, by their path): what a packed plugin carries as
38
+ * `functions.source.json`, loaded by a server that runs the plugin as it was packed.
39
+ */
40
+ var loadFunctionsSource = async (source) => {
37
41
  if (!Object.keys(source).length) return [];
38
42
  const { code } = await buildFunctions(source);
39
43
  const loaded = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
@@ -42,4 +46,4 @@ var loadFunctions = async (dir) => {
42
46
  return [definition];
43
47
  };
44
48
  //#endregion
45
- export { loadFunctions, readFunctionsDir };
49
+ export { isFunctionsDefinition, loadFunctions, loadFunctionsSource, readFunctionsDir };
@@ -1,4 +1,5 @@
1
1
  import { FunctionLimits } from './protocol';
2
+ import { FunctionScope } from './scope';
2
3
  import { FunctionsManifest } from '@plitzi/sdk-shared';
3
4
  export declare const ROUTE_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE"];
4
5
  export type RouteMethod = (typeof ROUTE_METHODS)[number];
@@ -6,6 +7,8 @@ export type RouteKey = {
6
7
  method: RouteMethod;
7
8
  segments: string[];
8
9
  };
10
+ /** Why a space's (or a deployment's) route is refused: `/fn/plugins/` is where plugins answer. */
11
+ export declare const reservedRouteProblem: (route: string) => string;
9
12
  /**
10
13
  * A route key, `'<METHOD> /<path>'`: literal segments and `:params`, served under `/fn/`. Anything else — a query, a
11
14
  * wildcard, `..` — is not a key, so what a space may answer is only ever a path it spelled out.
@@ -23,4 +26,4 @@ export type ManifestReading = {
23
26
  * names a platform task always runs the platform's. `ceilings` is the most this deployment gives one invocation: what a
24
27
  * task may ask for.
25
28
  */
26
- export declare const readManifest: (value: unknown, reserved: ReadonlySet<string>, ceilings: FunctionLimits) => ManifestReading;
29
+ export declare const readManifest: (value: unknown, reserved: ReadonlySet<string>, ceilings: FunctionLimits, scope?: FunctionScope) => ManifestReading;
@@ -1,5 +1,7 @@
1
+ import { pluginTaskProblem } from "./scope.js";
1
2
  import { taskName, taskNameProblem } from "../actions/tasks/registry.js";
2
3
  import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
4
+ import { PLUGIN_ROUTES_SEGMENT } from "@plitzi/sdk-shared/actions/functions";
3
5
  //#region src/modules/functions/manifest.ts
4
6
  var ROUTE_METHODS = [
5
7
  "GET",
@@ -10,6 +12,8 @@ var ROUTE_METHODS = [
10
12
  ];
11
13
  var SEGMENT = /^(:[a-zA-Z][a-zA-Z0-9]*|[A-Za-z0-9._~-]+)$/;
12
14
  var isRouteMethod = (method) => ROUTE_METHODS.some((known) => known === method);
15
+ /** Why a space's (or a deployment's) route is refused: `/fn/plugins/` is where plugins answer. */
16
+ var reservedRouteProblem = (route) => `Route "${route}" is under /plugins/, where the plugins' routes answer (/fn/plugins/<type>/…)`;
13
17
  /**
14
18
  * A route key, `'<METHOD> /<path>'`: literal segments and `:params`, served under `/fn/`. Anything else — a query, a
15
19
  * wildcard, `..` — is not a key, so what a space may answer is only ever a path it spelled out.
@@ -117,7 +121,7 @@ var limitsOf = (value, where, ceilings, problems) => {
117
121
  * names a platform task always runs the platform's. `ceilings` is the most this deployment gives one invocation: what a
118
122
  * task may ask for.
119
123
  */
120
- var readManifest = (value, reserved, ceilings) => {
124
+ var readManifest = (value, reserved, ceilings, scope) => {
121
125
  const problems = [];
122
126
  const raw = isRecord(value) ? value : {};
123
127
  const hosts = Array.isArray(raw.hosts) ? raw.hosts.filter((host) => typeof host === "string") : [];
@@ -140,6 +144,10 @@ var readManifest = (value, reserved, ceilings) => {
140
144
  problems.push(nameProblem);
141
145
  return;
142
146
  }
147
+ if (scope && namespace !== scope.plugin) {
148
+ problems.push(pluginTaskProblem(scope.plugin, namespace, action));
149
+ return;
150
+ }
143
151
  const name = taskName({
144
152
  namespace,
145
153
  action
@@ -175,6 +183,10 @@ var readManifest = (value, reserved, ceilings) => {
175
183
  problems.push(`Route "${route}" is not "<GET|POST|PUT|PATCH|DELETE> /<path>" of literal segments and :params`);
176
184
  return;
177
185
  }
186
+ if (!scope && key.segments[0] === PLUGIN_ROUTES_SEGMENT) {
187
+ problems.push(reservedRouteProblem(route));
188
+ return;
189
+ }
178
190
  const shape = `${key.method} /${key.segments.map((segment) => segment.startsWith(":") ? ":" : segment).join("/")}`;
179
191
  if (routed.has(shape)) problems.push(`Route "${route}" answers the same requests as another one`);
180
192
  routed.add(shape);
@@ -191,4 +203,4 @@ var readManifest = (value, reserved, ceilings) => {
191
203
  };
192
204
  };
193
205
  //#endregion
194
- export { ROUTE_METHODS, parseRouteKey, readManifest };
206
+ export { ROUTE_METHODS, parseRouteKey, readManifest, reservedRouteProblem };
@@ -1,8 +1,9 @@
1
1
  import { FunctionsDefinition } from './contract';
2
+ import { FunctionScope } from './scope';
2
3
  import { ActionTask } from '../actions/types';
3
4
  /**
4
5
  * A server's own functions, loaded as they are: trusted code the deployment ships — a self-hosted server's, or a
5
6
  * platform's own — run in the process with the run's context narrowed to what a function may see. The same code a
6
7
  * space runs in the sandbox, loaded without one.
7
8
  */
8
- export declare const nativeTasks: (definitions?: readonly FunctionsDefinition[]) => ActionTask<Record<string, unknown>>[];
9
+ export declare const nativeTasks: (definitions?: readonly FunctionsDefinition[], scope?: FunctionScope) => ActionTask<Record<string, unknown>>[];
@@ -5,8 +5,8 @@ import { functionContextFor } from "./context.js";
5
5
  * platform's own — run in the process with the run's context narrowed to what a function may see. The same code a
6
6
  * space runs in the sandbox, loaded without one.
7
7
  */
8
- var nativeTasks = (definitions = []) => definitions.flatMap((definition) => (definition.tasks ?? []).map((task) => {
9
- const contextFor = (ctx) => functionContextFor(ctx, definition.allow?.hosts ?? []);
8
+ var nativeTasks = (definitions = [], scope) => definitions.flatMap((definition) => (definition.tasks ?? []).map((task) => {
9
+ const contextFor = (ctx) => functionContextFor(ctx, definition.allow?.hosts ?? [], scope);
10
10
  const { run, params } = task;
11
11
  return {
12
12
  namespace: task.namespace,
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { FunctionsConfig } from './config';
2
+ import { FunctionsDefinition } from './contract';
2
3
  import { SpaceFunctions } from './protocol';
3
4
  import { TaskContextSource } from '../actions/runtime/runAction';
4
5
  import { Environment, SpaceRevision, SSRUser } from '@plitzi/sdk-shared';
@@ -23,14 +24,18 @@ export declare const matchRoute: (keys: readonly string[], method: string, path:
23
24
  } | undefined;
24
25
  export type RouteSources = {
25
26
  config: FunctionsConfig;
27
+ /** The plugins' server halves this server ships, as they are now — a development server replaces them as they change. */
28
+ plugins?: () => ReadonlyMap<string, FunctionsDefinition>;
26
29
  getFunctions?: (spaceId: number, at?: SpaceRevision) => Promise<SpaceFunctions | undefined>;
30
+ getPluginFunctions?: (spaceId: number, at?: SpaceRevision) => Promise<Record<string, SpaceFunctions> | undefined>;
27
31
  taskContext: TaskContextSource;
28
32
  };
29
33
  /**
30
- * The route a request reaches, if one does: the deployment's own (its native functions') first, then the space's —
31
- * each run with the same context a task of theirs gets, `trigger: 'route'`.
34
+ * The route a request reaches, if one does: under `/plugins/<type>/`, that plugin's — this server's own, then the one
35
+ * the space's plugin brought; anywhere else, the deployment's own (its native functions') first, then the space's. Each
36
+ * runs with the same context a task of theirs gets, `trigger: 'route'` — a plugin's with a plugin's narrower one.
32
37
  */
33
- export declare const createRoutes: ({ config, getFunctions, taskContext }: RouteSources) => {
38
+ export declare const createRoutes: ({ config, plugins, getFunctions, getPluginFunctions, taskContext }: RouteSources) => {
34
39
  /** The route that answers `method path` (the path after `/fn`), for this visit — or none. */
35
40
  routeFor: (visit: RouteVisit, method: string, path: string) => Promise<RouteHandler | undefined>;
36
41
  };
@@ -1,11 +1,13 @@
1
1
  import { serverLog } from "../../helpers/serverLog.js";
2
2
  import { ActionRefusal } from "../actions/runtime/errors.js";
3
+ import { pluginOfPath } from "./scope.js";
3
4
  import { functionContextFor } from "./context.js";
4
5
  import { functionLimitsFor } from "./config.js";
5
- import { parseRouteKey } from "./manifest.js";
6
+ import { parseRouteKey, reservedRouteProblem } from "./manifest.js";
6
7
  import { answerCall, readCall, responseOf, wireRequestOf, wireResponseFrom } from "./capabilities.js";
7
8
  import { FunctionFailure } from "./protocol.js";
8
9
  import { randomUUID } from "node:crypto";
10
+ import { PLUGIN_ROUTES_SEGMENT } from "@plitzi/sdk-shared/actions/functions";
9
11
  //#region src/modules/functions/routes.ts
10
12
  /** A route key's params from a path it answers, or nothing when it does not answer it. */
11
13
  var matchKey = (key, method, path) => {
@@ -72,17 +74,22 @@ var failed = (visit, key, error) => {
72
74
  const stopped = error instanceof FunctionFailure && error.reason !== "error";
73
75
  return Response.json({ error: stopped ? "This route ran out of what it may spend" : "This route failed" }, { status: stopped ? 503 : 500 });
74
76
  };
77
+ /** A definition's routes, as a table this process answers from. */
78
+ var routesOf = (definition) => Object.entries(definition.routes ?? {}).map(([key, handler]) => ({
79
+ key,
80
+ handler,
81
+ hosts: definition.allow?.hosts ?? []
82
+ }));
75
83
  /**
76
- * The route a request reaches, if one does: the deployment's own (its native functions') first, then the space's —
77
- * each run with the same context a task of theirs gets, `trigger: 'route'`.
84
+ * The route a request reaches, if one does: under `/plugins/<type>/`, that plugin's — this server's own, then the one
85
+ * the space's plugin brought; anywhere else, the deployment's own (its native functions') first, then the space's. Each
86
+ * runs with the same context a task of theirs gets, `trigger: 'route'` — a plugin's with a plugin's narrower one.
78
87
  */
79
- var createRoutes = ({ config, getFunctions, taskContext }) => {
80
- const native = (config.native ?? []).flatMap((definition) => Object.entries(definition.routes ?? {}).map(([key, handler]) => ({
81
- key,
82
- handler,
83
- hosts: definition.allow?.hosts ?? []
84
- })));
85
- const contextOf = (visit, key, hosts) => {
88
+ var createRoutes = ({ config, plugins = () => new Map(Object.entries(config.plugins ?? {})), getFunctions, getPluginFunctions, taskContext }) => {
89
+ const native = (config.native ?? []).flatMap(routesOf);
90
+ const reserved = native.find((route) => parseRouteKey(route.key)?.segments[0] === PLUGIN_ROUTES_SEGMENT);
91
+ if (reserved) throw new Error(`[Functions] ${reservedRouteProblem(reserved.key)}`);
92
+ const contextOf = (visit, key, hosts, scope) => {
86
93
  const ctx = taskContext({
87
94
  runId: randomUUID(),
88
95
  spaceId: visit.spaceId,
@@ -98,21 +105,22 @@ var createRoutes = ({ config, getFunctions, taskContext }) => {
98
105
  log: (line) => {
99
106
  serverLog.info("Functions", `route ${key} of space ${String(visit.spaceId)}: ${line}`);
100
107
  }
101
- }, hosts),
108
+ }, hosts, scope),
102
109
  trigger: "route"
103
110
  };
104
111
  };
105
- const nativeRoute = (visit, method, path) => {
106
- const match = matchRoute(native.map((route) => route.key), method, path);
107
- const route = match ? native.find((entry) => entry.key === match.key) : void 0;
112
+ /** A route of code this process loaded: the deployment's own, or the server half of a plugin it ships. */
113
+ const loadedRoute = (routes, visit, method, path, scope) => {
114
+ const match = matchRoute(routes.map((route) => route.key), method, path);
115
+ const route = match ? routes.find((entry) => entry.key === match.key) : void 0;
108
116
  if (!match || !route) return;
109
- const handler = route.handler;
117
+ const { handler } = route;
110
118
  return {
111
119
  key: match.key,
112
120
  handle: async (request) => {
113
121
  try {
114
122
  return withoutCookies(await handler(withoutCredentials(request), {
115
- ...contextOf(visit, match.key, route.hosts),
123
+ ...contextOf(visit, match.key, route.hosts, scope),
116
124
  params: match.params
117
125
  }));
118
126
  } catch (error) {
@@ -121,17 +129,18 @@ var createRoutes = ({ config, getFunctions, taskContext }) => {
121
129
  }
122
130
  };
123
131
  };
124
- const spaceRoute = async (visit, method, path) => {
125
- const functions = await getFunctions?.(visit.spaceId, visit.at);
132
+ /** A route of code the runner runs: the space's own, or the server half of a plugin it uses. */
133
+ const runnerRoute = (functions, visit, method, path, scope) => {
126
134
  const runner = functions?.runner ?? config.runner;
127
135
  const match = functions ? matchRoute(functions.manifest.routes, method, path) : void 0;
128
136
  if (!runner || !functions || !match) return;
137
+ const usageName = scope ? `plugin ${scope.plugin} route ${match.key}` : `route ${match.key}`;
129
138
  return {
130
139
  key: match.key,
131
140
  handle: async (request) => {
132
141
  const refusal = await config.admit?.(visit.spaceId);
133
142
  if (refusal) return Response.json({ error: refusal }, { status: 429 });
134
- const fnCtx = contextOf(visit, match.key, functions.manifest.hosts);
143
+ const fnCtx = contextOf(visit, match.key, functions.manifest.hosts, scope);
135
144
  let ok = false;
136
145
  let spent;
137
146
  try {
@@ -165,7 +174,7 @@ var createRoutes = ({ config, getFunctions, taskContext }) => {
165
174
  } finally {
166
175
  if (spent) config.onUsage?.({
167
176
  spaceId: visit.spaceId,
168
- task: `route ${match.key}`,
177
+ task: usageName,
169
178
  ok,
170
179
  usage: spent
171
180
  });
@@ -173,9 +182,20 @@ var createRoutes = ({ config, getFunctions, taskContext }) => {
173
182
  }
174
183
  };
175
184
  };
185
+ const pluginRoute = async (visit, method, { plugin, path }) => {
186
+ const scope = { plugin };
187
+ const own = plugins().get(plugin);
188
+ if (own) return loadedRoute(routesOf(own), visit, method, path, scope);
189
+ const brought = (await getPluginFunctions?.(visit.spaceId, visit.at))?.[plugin];
190
+ return runnerRoute(brought, visit, method, path, scope);
191
+ };
176
192
  return {
177
193
  /** The route that answers `method path` (the path after `/fn`), for this visit — or none. */
178
- routeFor: async (visit, method, path) => nativeRoute(visit, method, path) ?? await spaceRoute(visit, method, path) };
194
+ routeFor: async (visit, method, path) => {
195
+ const addressed = pluginOfPath(path);
196
+ if (addressed) return pluginRoute(visit, method, addressed);
197
+ return loadedRoute(native, visit, method, path) ?? runnerRoute(await getFunctions?.(visit.spaceId, visit.at), visit, method, path);
198
+ } };
179
199
  };
180
200
  //#endregion
181
201
  export { createRoutes, matchRoute };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Whose code a function is when it is not the space's: a plugin's server half, which a plugin brings with it and a space
3
+ * only chose to use.
4
+ *
5
+ * The space trusted the plugin to draw something, not with what it keeps — so a plugin's functions are handed a narrower
6
+ * `ctx`: their own corner of the space's `kv` and rate limits, signatures that only verify as theirs, the hosts they
7
+ * declared, and none of the space's credentials or realtime channels.
8
+ */
9
+ export type FunctionScope = {
10
+ plugin: string;
11
+ };
12
+ /** Where a plugin's routes answer, under `/fn`: `/fn/plugins/<type>/…`. */
13
+ export declare const PLUGIN_ROUTES = "/plugins/";
14
+ /** A plugin's keys inside its space's `kv`: nothing of the space's, or of another plugin's, is reachable from it. */
15
+ export declare const pluginKvPrefix: (plugin: string) => string;
16
+ /**
17
+ * What a plugin signs is signed as ITS: the value is bound to the plugin before the space's key sees it, so a plugin
18
+ * cannot mint a value the space's own code — or another plugin — would accept as its own.
19
+ */
20
+ export declare const pluginSigned: (plugin: string, value: string) => string;
21
+ /**
22
+ * The plugin a route path addresses — `/plugins/board/layout` is `board`, with `/layout` left for its own routes — or
23
+ * nothing for a path that is not a plugin's.
24
+ */
25
+ export declare const pluginOfPath: (path: string) => {
26
+ plugin: string;
27
+ path: string;
28
+ } | undefined;
29
+ /** Why a plugin's task is refused: it is named after something else. */
30
+ export declare const pluginTaskProblem: (plugin: string, namespace: string, action: string) => string;
@@ -0,0 +1,28 @@
1
+ import { PLUGIN_ROUTES_SEGMENT } from "@plitzi/sdk-shared/actions/functions";
2
+ //#region src/modules/functions/scope.ts
3
+ /** Where a plugin's routes answer, under `/fn`: `/fn/plugins/<type>/…`. */
4
+ var PLUGIN_ROUTES = `/${PLUGIN_ROUTES_SEGMENT}/`;
5
+ /** A plugin's keys inside its space's `kv`: nothing of the space's, or of another plugin's, is reachable from it. */
6
+ var pluginKvPrefix = (plugin) => `plugin:${plugin}:`;
7
+ /**
8
+ * What a plugin signs is signed as ITS: the value is bound to the plugin before the space's key sees it, so a plugin
9
+ * cannot mint a value the space's own code — or another plugin — would accept as its own.
10
+ */
11
+ var pluginSigned = (plugin, value) => `plugin:${plugin}\n${value}`;
12
+ /**
13
+ * The plugin a route path addresses — `/plugins/board/layout` is `board`, with `/layout` left for its own routes — or
14
+ * nothing for a path that is not a plugin's.
15
+ */
16
+ var pluginOfPath = (path) => {
17
+ const trimmed = `/${path.replace(/^\/+/, "")}`;
18
+ if (!trimmed.startsWith(PLUGIN_ROUTES)) return;
19
+ const [plugin = "", ...rest] = trimmed.slice(PLUGIN_ROUTES.length).split("/");
20
+ return plugin ? {
21
+ plugin,
22
+ path: `/${rest.join("/")}`
23
+ } : void 0;
24
+ };
25
+ /** Why a plugin's task is refused: it is named after something else. */
26
+ var pluginTaskProblem = (plugin, namespace, action) => `Task "${namespace}.${action}" of plugin "${plugin}": a plugin's tasks are named after it — namespace "${plugin}"`;
27
+ //#endregion
28
+ export { PLUGIN_ROUTES, pluginKvPrefix, pluginOfPath, pluginSigned, pluginTaskProblem };
@@ -1,6 +1,7 @@
1
1
  import { FunctionsSource } from './build';
2
2
  import { FunctionsConfig } from './config';
3
3
  import { FunctionsBundle, FunctionLimits, FunctionRunner, SpaceFunctions } from './protocol';
4
+ import { FunctionScope } from './scope';
4
5
  import { ActionTask, ActionTaskRegistry } from '../actions/types';
5
6
  import { FunctionsManifest, FunctionsProblem, FunctionTimeLimits } from '@plitzi/sdk-shared';
6
7
  /**
@@ -8,10 +9,13 @@ import { FunctionsManifest, FunctionsProblem, FunctionTimeLimits } from '@plitzi
8
9
  * {@link FunctionContext}: the same one native functions get, built by the same function. Each is given what
9
10
  * `limitsFor` answers for what it asked: its own limits, over its functions'.
10
11
  */
11
- export declare const spaceTasks: (functions: SpaceFunctions, runner: FunctionRunner, limitsFor: (asked: FunctionTimeLimits) => FunctionLimits, { admit, onUsage }?: Pick<FunctionsConfig, "admit" | "onUsage">) => ActionTask<Record<string, unknown>>[];
12
+ export declare const spaceTasks: (functions: SpaceFunctions, runner: FunctionRunner, limitsFor: (asked: FunctionTimeLimits) => FunctionLimits, { admit, onUsage }?: Pick<FunctionsConfig, "admit" | "onUsage">, scope?: FunctionScope) => ActionTask<Record<string, unknown>>[];
12
13
  export type SpaceRegistries = {
13
- /** The tasks one run of that space can use: the deployment's, and its own functions' when it has any. */
14
- registryFor: (functions: SpaceFunctions | undefined) => ActionTaskRegistry;
14
+ /**
15
+ * The tasks one run of that space can use: the deployment's, its own functions' when it has any, and those of the
16
+ * plugins it uses that bring a server half (by plugin type).
17
+ */
18
+ registryFor: (functions: SpaceFunctions | undefined, plugins?: Readonly<Record<string, SpaceFunctions>>) => ActionTaskRegistry;
15
19
  };
16
20
  /**
17
21
  * Registries per space bundle, built once per bundle and limits — a space's functions change when they are saved, and
@@ -37,6 +41,7 @@ export type PreparedFunctions = {
37
41
  /**
38
42
  * A space's source made ready to save: built, read by the runner, and checked — the only way a space's functions are
39
43
  * 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.
44
+ * `ceilings`, the most it gives one invocation — what a task may ask for; `scope`, a plugin's server half rather than a
45
+ * space's (its tasks named after it, its routes its own).
41
46
  */
42
- export declare const prepareFunctions: (source: FunctionsSource, runner: FunctionRunner, reserved: ReadonlySet<string>, ceilings: FunctionLimits) => Promise<PreparedFunctions>;
47
+ export declare const prepareFunctions: (source: FunctionsSource, runner: FunctionRunner, reserved: ReadonlySet<string>, ceilings: FunctionLimits, scope?: FunctionScope) => Promise<PreparedFunctions>;
@@ -21,7 +21,7 @@ var invocationContextOf = ({ spaceId, environment, runId, trigger, callerId, use
21
21
  * {@link FunctionContext}: the same one native functions get, built by the same function. Each is given what
22
22
  * `limitsFor` answers for what it asked: its own limits, over its functions'.
23
23
  */
24
- var spaceTasks = (functions, runner, limitsFor, { admit, onUsage } = {}) => functions.manifest.tasks.map((task) => {
24
+ var spaceTasks = (functions, runner, limitsFor, { admit, onUsage } = {}, scope) => functions.manifest.tasks.map((task) => {
25
25
  const name = taskName(task);
26
26
  const limits = limitsFor({
27
27
  ...functions.manifest.limits,
@@ -36,7 +36,7 @@ var spaceTasks = (functions, runner, limitsFor, { admit, onUsage } = {}) => func
36
36
  run: async (params, ctx) => {
37
37
  const refusal = await admit?.(ctx.spaceId);
38
38
  if (refusal) throw new Error(refusal);
39
- const fnCtx = functionContextFor(ctx, functions.manifest.hosts);
39
+ const fnCtx = functionContextFor(ctx, functions.manifest.hosts, scope);
40
40
  let ok = false;
41
41
  let spent;
42
42
  try {
@@ -71,15 +71,18 @@ var spaceTasks = (functions, runner, limitsFor, { admit, onUsage } = {}) => func
71
71
  }
72
72
  };
73
73
  });
74
- /** The deployment's tasks and a space's, one registry: a space's never answers a name the deployment's already does. */
74
+ /**
75
+ * The deployment's tasks, a space's and its plugins', one registry: neither ever answers a name the deployment's already
76
+ * does, and a plugin's are named after it (`<type>.<action>`), so they meet nobody else's.
77
+ */
75
78
  var composeRegistry = (base, tasks) => {
76
79
  const own = /* @__PURE__ */ new Map();
77
- tasks.forEach((task) => {
80
+ tasks.forEach(({ task, origin }) => {
78
81
  const name = taskName(task);
79
- if (!base.get(name)) own.set(name, {
82
+ if (!base.get(name) && !own.has(name)) own.set(name, {
80
83
  ...task,
81
84
  name,
82
- origin: "space"
85
+ origin
83
86
  });
84
87
  });
85
88
  return {
@@ -94,14 +97,32 @@ var REGISTRY_CACHE = 500;
94
97
  */
95
98
  var createSpaceRegistries = (base, config = {}) => {
96
99
  const cache = /* @__PURE__ */ new Map();
97
- return { registryFor: (functions) => {
98
- const runner = functions?.runner ?? config.runner;
99
- if (!functions || !runner) return base;
100
- const key = `${functions.bundle.id}:${JSON.stringify(functions.limits ?? {})}`;
100
+ return { registryFor: (functions, plugins = {}) => {
101
+ const runnable = [...functions ? [{
102
+ functions,
103
+ scope: void 0
104
+ }] : [], ...Object.entries(plugins).map(([plugin, own]) => ({
105
+ functions: own,
106
+ scope: { plugin }
107
+ }))].flatMap(({ functions: found, scope }) => {
108
+ const runner = found.runner ?? config.runner;
109
+ return runner ? [{
110
+ functions: found,
111
+ runner,
112
+ scope
113
+ }] : [];
114
+ });
115
+ if (runnable.length === 0) return base;
116
+ const key = runnable.map(({ functions: found, scope }) => `${scope?.plugin ?? ""}:${found.bundle.id}:${JSON.stringify(found.limits ?? {})}`).join("|");
101
117
  const found = cache.get(key);
102
118
  if (found) return found;
103
- const limitsFor = (asked) => functionLimitsFor(config.limits, functions.limits, asked);
104
- const registry = composeRegistry(base, spaceTasks(functions, runner, limitsFor, config));
119
+ const registry = composeRegistry(base, runnable.flatMap(({ functions: own, runner, scope }) => {
120
+ const limitsFor = (asked) => functionLimitsFor(config.limits, own.limits, asked);
121
+ return spaceTasks(own, runner, limitsFor, config, scope).map((task) => ({
122
+ task,
123
+ origin: scope ? "plugin" : "space"
124
+ }));
125
+ }));
105
126
  cache.set(key, registry);
106
127
  if (cache.size > REGISTRY_CACHE) {
107
128
  const oldest = cache.keys().next().value;
@@ -121,9 +142,10 @@ var functionsInHand = ({ bundle, manifest }) => ({
121
142
  /**
122
143
  * A space's source made ready to save: built, read by the runner, and checked — the only way a space's functions are
123
144
  * 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.
145
+ * `ceilings`, the most it gives one invocation — what a task may ask for; `scope`, a plugin's server half rather than a
146
+ * space's (its tasks named after it, its routes its own).
125
147
  */
126
- var prepareFunctions = async (source, runner, reserved, ceilings) => {
148
+ var prepareFunctions = async (source, runner, reserved, ceilings, scope) => {
127
149
  let code;
128
150
  try {
129
151
  ({code} = await buildFunctions(source));
@@ -150,7 +172,7 @@ var prepareFunctions = async (source, runner, reserved, ceilings) => {
150
172
  }]
151
173
  };
152
174
  }
153
- const { manifest, problems } = readManifest(declared, reserved, ceilings);
175
+ const { manifest, problems } = readManifest(declared, reserved, ceilings, scope);
154
176
  return problems.length ? {
155
177
  ok: false,
156
178
  problems: problems.map((message) => ({
@@ -17,7 +17,7 @@ import { SSRAdapters } from '@plitzi/sdk-shared';
17
17
  *
18
18
  * A deployment that resolves server elements some other way still supplies its own `getRscData`, and that one wins.
19
19
  */
20
- export declare const connectorRscData: ({ connectors, actions, publicDir, elementTimeoutMs }: {
20
+ export declare const connectorRscData: ({ connectors, actions, publicDir, dataDir, elementTimeoutMs }: {
21
21
  connectors?: ConnectorLookups;
22
22
  actions?: {
23
23
  lookups: ActionLookups;
@@ -25,6 +25,8 @@ export declare const connectorRscData: ({ connectors, actions, publicDir, elemen
25
25
  };
26
26
  /** Where the server's static files are: a provider whose `query` is one of them is read from disk. */
27
27
  publicDir?: string;
28
+ /** The project's own data, never served: a provider whose `query` is `/data/<file>` is read from it. */
29
+ dataDir?: string;
28
30
  /** The deployment's per-element ceiling, when it set one. `resolveRscData` decides the default. */
29
31
  elementTimeoutMs?: number;
30
32
  }) => NonNullable<SSRAdapters["getRscData"]>;