@plitzi/sdk-server 0.38.6 → 0.38.7

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 (79) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +100 -14
  3. package/dist/adapters/jsonAdapters.d.ts +8 -4
  4. package/dist/adapters/jsonAdapters.js +5 -9
  5. package/dist/core/auth/api.d.ts +4 -1
  6. package/dist/core/auth/api.js +14 -5
  7. package/dist/core/auth/createAuth.d.ts +7 -1
  8. package/dist/core/auth/createAuth.js +14 -0
  9. package/dist/core/auth/credentials.d.ts +2 -0
  10. package/dist/core/auth/credentials.js +4 -2
  11. package/dist/core/auth/session.d.ts +2 -0
  12. package/dist/core/auth/session.js +6 -5
  13. package/dist/core/createServer.js +25 -4
  14. package/dist/core/http/stages/devReload.d.ts +2 -2
  15. package/dist/core/http/stages/devReload.js +2 -2
  16. package/dist/env.d.ts +1 -0
  17. package/dist/env.js +19 -0
  18. package/dist/env.test.d.ts +1 -0
  19. package/dist/functions-api.d.ts +5 -0
  20. package/dist/helpers/buildServerInfo.js +6 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/modules/actions/runtime/projectData.d.ts +18 -0
  23. package/dist/modules/actions/runtime/projectData.js +69 -0
  24. package/dist/modules/actions/runtime/taskContext.d.ts +1 -1
  25. package/dist/modules/actions/runtime/taskContext.js +6 -3
  26. package/dist/modules/actions/types.d.ts +7 -1
  27. package/dist/modules/functions/build.js +10 -0
  28. package/dist/modules/functions/capabilities.js +5 -0
  29. package/dist/modules/functions/context.js +10 -2
  30. package/dist/modules/functions/contract.d.ts +5 -0
  31. package/dist/modules/functions/driver.js +4 -0
  32. package/dist/modules/functions/local.d.ts +3 -1
  33. package/dist/modules/functions/local.js +4 -2
  34. package/dist/modules/functions/protocol.d.ts +3 -0
  35. package/dist/modules/functions/scope.d.ts +1 -1
  36. package/dist/modules/images/imageProxy.js +1 -1
  37. package/dist/modules/mysql/accounts.js +1 -5
  38. package/dist/modules/project/plugins.d.ts +31 -0
  39. package/dist/modules/project/plugins.js +162 -0
  40. package/dist/modules/project/serveProject.d.ts +78 -0
  41. package/dist/modules/project/serveProject.js +200 -0
  42. package/dist/modules/project/serveProject.test.d.ts +1 -0
  43. package/dist/modules/project/watchSpace.d.ts +14 -0
  44. package/dist/modules/project/watchSpace.js +77 -0
  45. package/dist/modules/realtime/failure.d.ts +6 -0
  46. package/dist/modules/realtime/failure.js +11 -0
  47. package/dist/modules/realtime/handlers.js +13 -3
  48. package/dist/modules/realtime/hub.js +4 -1
  49. package/dist/modules/realtime/socket.js +3 -2
  50. package/dist/modules/rsc/connectorRscData.d.ts +7 -3
  51. package/dist/modules/rsc/connectorRscData.js +3 -4
  52. package/dist/modules/rsc/handler.d.ts +1 -0
  53. package/dist/modules/rsc/handler.js +11 -1
  54. package/dist/modules/rsc/publicFileResolver.d.ts +4 -8
  55. package/dist/modules/rsc/publicFileResolver.js +6 -10
  56. package/dist/modules/runtime/bundle.js +1 -1
  57. package/dist/modules/ssr/prepareRender.js +10 -2
  58. package/dist/modules/ssr/registerExternalPlugins.js +1 -1
  59. package/dist/modules/ssr/vendor.test.d.ts +1 -0
  60. package/dist/modules/ssr/views/template.ejs +7 -3
  61. package/dist/plugins/compile.js +1 -1
  62. package/dist/plugins/copy.js +1 -1
  63. package/dist/plugins/manager.js +1 -1
  64. package/dist/project.d.ts +9 -0
  65. package/dist/project.js +2 -0
  66. package/package.json +16 -4
  67. package/skills/plitzi-authoring/CHEATSHEET.md +2 -2
  68. package/skills/plitzi-authoring/SKILL.md +3 -2
  69. package/skills/plitzi-authoring/reference/accessibility.md +2 -2
  70. package/skills/plitzi-authoring/reference/auth.md +75 -0
  71. package/skills/plitzi-authoring/reference/authoring-errors.md +18 -4
  72. package/skills/plitzi-authoring/reference/colours-and-motion.md +3 -2
  73. package/skills/plitzi-authoring/reference/efficiency.md +5 -2
  74. package/skills/plitzi-authoring/reference/elements-and-styles.md +23 -23
  75. package/skills/plitzi-authoring/reference/flows.md +11 -6
  76. package/skills/plitzi-authoring/reference/layouts.md +15 -18
  77. package/skills/plitzi-authoring/reference/plugins.md +1 -1
  78. package/skills/plitzi-authoring/reference/review-checklist.md +2 -1
  79. package/skills/plitzi-authoring/reference/structure.md +3 -2
@@ -42,6 +42,8 @@ var sessionCookieParams = (hostname, config = {}) => {
42
42
  hintSuffix: config.hintSuffix ?? "_hint"
43
43
  };
44
44
  };
45
+ /** The readable companion cookie's name — the one a page is told to look for (`server.auth.sessionHintCookie`). */
46
+ var sessionHintCookieName = (params) => `${params.name}${params.hintSuffix}`;
45
47
  /**
46
48
  * The value of the readable companion cookie: `<access expiry>.<refresh expiry>`, unix seconds, refresh optional.
47
49
  *
@@ -95,7 +97,7 @@ var writeSessionCookies = (req, res, session, config) => {
95
97
  const now = nowInSeconds();
96
98
  const cookies = [serializeCookie(params.name, session.token, session.expiresAt - now, params)];
97
99
  if (session.refreshToken) cookies.push(serializeCookie(`${params.name}_refresh`, session.refreshToken, (session.refreshExpiresAt ?? session.expiresAt) - now, params, { path: params.refreshPath }));
98
- cookies.push(serializeCookie(`${params.name}${params.hintSuffix}`, sessionHintValue(session.expiresAt, session.refreshExpiresAt), (session.refreshExpiresAt ?? session.expiresAt) - now, params, { httpOnly: false }));
100
+ cookies.push(serializeCookie(sessionHintCookieName(params), sessionHintValue(session.expiresAt, session.refreshExpiresAt), (session.refreshExpiresAt ?? session.expiresAt) - now, params, { httpOnly: false }));
99
101
  appendCookies(res, cookies);
100
102
  };
101
103
  /** Ends the session in this browser. Every cookie the grant wrote is cleared, the readable one included. */
@@ -104,7 +106,7 @@ var clearSessionCookies = (req, res, config) => {
104
106
  appendCookies(res, [
105
107
  serializeCookie(params.name, "", 0, params),
106
108
  serializeCookie(`${params.name}_refresh`, "", 0, params, { path: params.refreshPath }),
107
- serializeCookie(`${params.name}${params.hintSuffix}`, "", 0, params, { httpOnly: false })
109
+ serializeCookie(sessionHintCookieName(params), "", 0, params, { httpOnly: false })
108
110
  ]);
109
111
  };
110
112
  /**
@@ -141,8 +143,7 @@ var readSessionToken = (req, config) => readCookie(req, sessionCookieParams(req.
141
143
  var readRefreshToken = (req, config) => readCookie(req, `${sessionCookieParams(req.hostname, config).name}_refresh`);
142
144
  /** What the readable companion cookie says about this browser's session, without presenting any credential. */
143
145
  var readSessionHint = (req, config) => {
144
- const params = sessionCookieParams(req.hostname, config);
145
- return parseSessionHint(readCookie(req, `${params.name}${params.hintSuffix}`));
146
+ return parseSessionHint(readCookie(req, sessionHintCookieName(sessionCookieParams(req.hostname, config))));
146
147
  };
147
148
  /**
148
149
  * Marks a browser as having just been sent to renew its session, so the page it comes back to is rendered rather
@@ -208,4 +209,4 @@ var createSessionCookies = (config) => ({
208
209
  }
209
210
  });
210
211
  //#endregion
211
- export { appendCookies, clearFlowCookie, clearSessionCookies, createSessionCookies, isLocalHost, parseSessionHint, readFlowCookie, readRefreshToken, readRenewalGuard, readSessionHint, readSessionToken, registrableDomain, sessionCookieParams, sessionHintValue, sessionReturnTarget, writeFlowCookie, writeRenewalGuard, writeSessionCookies };
212
+ export { appendCookies, clearFlowCookie, clearSessionCookies, createSessionCookies, isLocalHost, parseSessionHint, readFlowCookie, readRefreshToken, readRenewalGuard, readSessionHint, readSessionToken, registrableDomain, sessionCookieParams, sessionHintCookieName, sessionHintValue, sessionReturnTarget, writeFlowCookie, writeRenewalGuard, writeSessionCookies };
@@ -1,4 +1,5 @@
1
1
  import { actionLookupsOf, connectorLookupsOf } from "./configSeam.js";
2
+ import { dataDirLookup } from "../modules/actions/runtime/projectData.js";
2
3
  import { actionsModuleFor } from "../modules/actions/moduleFor.js";
3
4
  import { connectorRscData } from "../modules/rsc/connectorRscData.js";
4
5
  import { createAuthApiStage } from "./http/stages/authApi.js";
@@ -6,6 +7,25 @@ import { createRenewalEndpointStage, createSessionRenewalStage } from "./http/st
6
7
  import { resolveServices } from "./services/resolve.js";
7
8
  import { createPageServer } from "./server/pageServer.js";
8
9
  //#region src/core/createServer.ts
10
+ /**
11
+ * The space's data as one lookup, for every reader of it: the deployment's `action.lookups.getData`, or — given none —
12
+ * the files of `dataDir`. A `/data/<file>` provider and a function's `ctx.data` then read the same files the same way,
13
+ * instead of the folder through one path and the lookup through another.
14
+ */
15
+ var withDataLookup = (config) => {
16
+ const lookups = config.action?.lookups;
17
+ if (!config.dataDir || !lookups || lookups.getData) return config;
18
+ return {
19
+ ...config,
20
+ action: {
21
+ ...config.action,
22
+ lookups: {
23
+ ...lookups,
24
+ getData: dataDirLookup(config.dataDir)
25
+ }
26
+ }
27
+ };
28
+ };
9
29
  /** The server this package makes: pages and RSC, mounting whatever the config enables, plus any stages a
10
30
  * companion package contributes through `extensions`. The MCP endpoint, the widget proxy and draft-preview
11
31
  * arrive that way from `@plitzi/sdk-mcp`, which a page-only deployment never installs.
@@ -39,7 +59,7 @@ var withConnectorRsc = (config) => {
39
59
  connectors: config.connectors ? connectorLookupsOf(config.connectors) : void 0,
40
60
  actions,
41
61
  publicDir: config.publicDir,
42
- dataDir: config.dataDir,
62
+ data: actions?.lookups.getData ?? (config.dataDir ? dataDirLookup(config.dataDir) : void 0),
43
63
  elementTimeoutMs: config.rsc?.elementTimeoutMs
44
64
  });
45
65
  return resolved;
@@ -51,22 +71,23 @@ var withRenewal = (extensions, url) => ({
51
71
  });
52
72
  var createServer = ({ auth, sessionRenewal, ...config }, extensions) => {
53
73
  if (!auth) {
54
- const resolvedConfig = withConnectorRsc(config);
74
+ const resolvedConfig = withConnectorRsc(withDataLookup(config));
55
75
  return createPageServer(resolvedConfig, resolveServices(resolvedConfig), withRenewal(extensions, sessionRenewal ? sessionRenewal.url : void 0));
56
76
  }
57
77
  const entries = Object.entries(config.adapters);
58
78
  const supplied = Object.fromEntries(entries.filter(([, value]) => value !== void 0));
59
- const resolved = withConnectorRsc({
79
+ const resolved = withConnectorRsc(withDataLookup({
60
80
  loginPath: false,
61
81
  logoutPath: false,
62
82
  exchangePath: false,
63
83
  ...config,
64
84
  authCookie: config.authCookie ?? auth.cookieConfig,
85
+ pageAuth: config.pageAuth ?? auth.pageAuth,
65
86
  adapters: {
66
87
  ...auth.ssrAdapters,
67
88
  ...supplied
68
89
  }
69
- });
90
+ }));
70
91
  const withAuthRoutes = {
71
92
  ...extensions,
72
93
  preAuth: [
@@ -4,8 +4,8 @@ import { PluginEntry } from '@plitzi/sdk-shared';
4
4
  export declare const DEV_RELOAD_PATH = "/__plitzi/reload";
5
5
  /**
6
6
  * The page reloading itself when the space it shows changed — the edit loop of a project that authors its space in
7
- * code, without restarting the server: `main.ts` re-authors in a process of its own and calls `reloadPages()`, and
8
- * every page open on this server loads again.
7
+ * code, without restarting the server: `serveProject` re-authors in a process of its own, swaps the documents and calls
8
+ * `reloadPages()`, and every page open on this server loads again.
9
9
  *
10
10
  * Only with `devReload` on: every open page keeps a connection for it, which a server that never calls `reloadPages`
11
11
  * — a published one, or a platform's — would pay for and get nothing from. The template listens only then too.
@@ -5,8 +5,8 @@ import { randomUUID } from "node:crypto";
5
5
  var DEV_RELOAD_PATH = "/__plitzi/reload";
6
6
  /**
7
7
  * The page reloading itself when the space it shows changed — the edit loop of a project that authors its space in
8
- * code, without restarting the server: `main.ts` re-authors in a process of its own and calls `reloadPages()`, and
9
- * every page open on this server loads again.
8
+ * code, without restarting the server: `serveProject` re-authors in a process of its own, swaps the documents and calls
9
+ * `reloadPages()`, and every page open on this server loads again.
10
10
  *
11
11
  * Only with `devReload` on: every open page keeps a connection for it, which a server that never calls `reloadPages`
12
12
  * — a published one, or a platform's — would pay for and get nothing from. The template listens only then too.
package/dist/env.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/env.js ADDED
@@ -0,0 +1,19 @@
1
+ //#region src/env.ts
2
+ /**
3
+ * The project's `.env` in `process.env`, when there is one, before any module of it is evaluated — preloaded by the
4
+ * one script Node's own `--env-file-if-exists=.env` cannot be given to: `start:dev`, run as
5
+ * `node --import @plitzi/sdk-server/env --watch-path=… src/main.ts`.
6
+ *
7
+ * Node's watcher, given `--watch-path`, watches the folder an env file is in — the project's root — for any change at
8
+ * all, and on macOS everything under it: the server writing `tmp/dev-server.json` restarted it, which wrote it again,
9
+ * and it never stopped. Preloaded, the file is read by the watched process alone, as Node reads it: a variable the
10
+ * environment already sets is kept. The project's other scripts, which watch nothing, keep Node's flag.
11
+ */
12
+ var ENV_FILE = ".env";
13
+ try {
14
+ process.loadEnvFile(ENV_FILE);
15
+ } catch (error) {
16
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
17
+ }
18
+ //#endregion
19
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -94,6 +94,11 @@ export declare type FunctionContext = {
94
94
  grant: (topic: string, ttlSeconds?: number) => Promise<string>;
95
95
  /** Takes a grant back — or every grant for the topic, naming none. */
96
96
  revoke: (topic: string, grant?: string) => Promise<void>;
97
+ /**
98
+ * One file of the space's data — a project's `src/data/<file>` — parsed, as of the version the run belongs to: what
99
+ * a page must not carry, read here and answered only as far as it is shown. Read-only. `ctx.data('products.json')`.
100
+ */
101
+ data: (file: string) => Promise<unknown>;
97
102
  /** A line for whoever reads the run: the builder's Try, and the deployment's logs. */
98
103
  log: (...values: unknown[]) => void;
99
104
  /** Progress for a caller that asked for a stream — a no-op when nobody did. */
@@ -1,4 +1,5 @@
1
1
  import { requestOrigin } from "../core/requestParser.js";
2
+ import { sessionCookieParams, sessionHintCookieName } from "../core/auth/session.js";
2
3
  //#region src/helpers/buildServerInfo.ts
3
4
  var getEnvironment = (env = "production", server) => {
4
5
  switch (env) {
@@ -40,6 +41,7 @@ var buildServerInfo = (req, config, ssr) => {
40
41
  const origin = requestOrigin(req) || `${req.protocol}://${req.hostname}`;
41
42
  const host = origin.slice(`${req.protocol}://`.length);
42
43
  const user = req.ctx.user;
44
+ const { pageAuth } = config;
43
45
  return getEnvironment(config.environment, {
44
46
  basePath: "/",
45
47
  requestUrl: req.url || "/",
@@ -73,6 +75,10 @@ var buildServerInfo = (req, config, ssr) => {
73
75
  accessToken: user.token,
74
76
  expiresAt: user.expiresAt
75
77
  } : void 0,
78
+ auth: pageAuth ? {
79
+ ...pageAuth,
80
+ sessionHintCookie: sessionHintCookieName(sessionCookieParams(req.hostname, config.authCookie))
81
+ } : void 0,
76
82
  ssr
77
83
  });
78
84
  };
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@ export { PREVIEW_TOKEN_PARAM } from './core/previewToken';
17
17
  export { resolveRscData } from './modules/rsc/resolveRscData';
18
18
  export { fetchConnectorRecords, rebaseMedia, writeConnectorRecord, createConnectorResolver } from './modules/connectors';
19
19
  export type { CloudAdaptersConfig, CloudSpaceCache } from './adapters/cloudAdapters';
20
- export type { JsonAdaptersConfig } from './adapters/jsonAdapters';
20
+ export type { JsonAdaptersConfig, OfflineDataSource } from './adapters/jsonAdapters';
21
21
  export type { AuthAdapters, AuthAdaptersConfig } from './adapters/authAdapters';
22
22
  export type { AuthoringPreviewOptions, DeploymentRow, FrameAncestorsConfig, SpaceAdapters, SpaceAdaptersConfig, SpaceCache, SpaceRefusal, SpaceResolution, SpaceResolver, WildcardSubdomainOptions } from './adapters/space';
23
23
  export type { RscElementResolver, RscResolveContext, ResolveRscDataOptions } from './modules/rsc/resolveRscData';
@@ -0,0 +1,18 @@
1
+ import { ActionLookups, SpaceRevision } from '../types';
2
+ /** How a server reaches a space's data: its files' text by path (`products.json`), as of a revision. */
3
+ export type ProjectDataLookup = NonNullable<ActionLookups['getData']>;
4
+ /**
5
+ * A data folder as the lookup a deployment would otherwise write — what a self-hosted server derives from `dataDir`, so
6
+ * a `/data/<file>` provider and a function's `ctx.data` read the folder one way. A folder has no revisions: every
7
+ * revision is what is on disk now. A file is read again only once it changed, so a page asking per render costs the
8
+ * folder's listing and nothing more.
9
+ */
10
+ export declare const dataDirLookup: (dataDir: string) => ProjectDataLookup;
11
+ /**
12
+ * `ctx.data` for one run: one file of the space's data, as of the run's revision, parsed.
13
+ *
14
+ * The lookup is asked once per run, whatever the code reads and however often — every read of one run sees the same
15
+ * version of the data — and each read is parsed afresh, so code that changes what it was handed changes nothing the
16
+ * next read gets.
17
+ */
18
+ export declare const projectDataReader: (getData: ProjectDataLookup | undefined, spaceId: number, at: SpaceRevision | undefined) => ((file: string) => Promise<unknown>);
@@ -0,0 +1,69 @@
1
+ import { readFile, readdir, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ //#region src/modules/actions/runtime/projectData.ts
4
+ /** What a project keeps in its data folder: JSON, the only files `plitzi push` sends as the space's data. */
5
+ var DATA_EXTENSION = ".json";
6
+ /**
7
+ * A data folder as the lookup a deployment would otherwise write — what a self-hosted server derives from `dataDir`, so
8
+ * a `/data/<file>` provider and a function's `ctx.data` read the folder one way. A folder has no revisions: every
9
+ * revision is what is on disk now. A file is read again only once it changed, so a page asking per render costs the
10
+ * folder's listing and nothing more.
11
+ */
12
+ var dataDirLookup = (dataDir) => {
13
+ const root = path.resolve(dataDir);
14
+ const kept = /* @__PURE__ */ new Map();
15
+ return async () => {
16
+ let names;
17
+ try {
18
+ names = await readdir(root, { recursive: true });
19
+ } catch {
20
+ return;
21
+ }
22
+ const read = await Promise.all(names.filter((name) => name.endsWith(DATA_EXTENSION)).map(async (name) => {
23
+ const file = path.join(root, name);
24
+ const key = name.split(path.sep).join("/");
25
+ try {
26
+ const { mtimeMs, size } = await stat(file);
27
+ const known = kept.get(key);
28
+ const text = known && known.mtimeMs === mtimeMs && known.size === size ? known.text : await readFile(file, "utf8");
29
+ kept.set(key, {
30
+ mtimeMs,
31
+ size,
32
+ text
33
+ });
34
+ return [key, text];
35
+ } catch {
36
+ kept.delete(key);
37
+ return;
38
+ }
39
+ }));
40
+ const files = Object.fromEntries(read.filter((entry) => entry !== void 0));
41
+ kept.forEach((_entry, key) => {
42
+ if (!(key in files)) kept.delete(key);
43
+ });
44
+ return files;
45
+ };
46
+ };
47
+ /**
48
+ * `ctx.data` for one run: one file of the space's data, as of the run's revision, parsed.
49
+ *
50
+ * The lookup is asked once per run, whatever the code reads and however often — every read of one run sees the same
51
+ * version of the data — and each read is parsed afresh, so code that changes what it was handed changes nothing the
52
+ * next read gets.
53
+ */
54
+ var projectDataReader = (getData, spaceId, at) => {
55
+ let files;
56
+ return async (file) => {
57
+ if (!getData) throw new Error("This server keeps no data for its spaces: ctx.data reads what a deployment answers with action.lookups.getData, or the files of dataDir");
58
+ files ??= getData(spaceId, at);
59
+ const text = (await files)?.[file];
60
+ if (text === void 0) throw new Error(`"${file}" is not a file of this space's data — ctx.data takes its path under src/data/`);
61
+ try {
62
+ return JSON.parse(text);
63
+ } catch {
64
+ throw new Error(`"${file}" of this space's data is not JSON`);
65
+ }
66
+ };
67
+ };
68
+ //#endregion
69
+ export { dataDirLookup, projectDataReader };
@@ -36,4 +36,4 @@ export type TaskContextRequest = Pick<ActionRunRequest, 'runId' | 'spaceId' | 'e
36
36
  * every secret it resolves registered with the redactor, the space's `kv` namespace, the run's outbound budget, the
37
37
  * space's channels. Everything but `log`, which is the STEP's: each step keeps what it logged.
38
38
  */
39
- export declare const taskContextFor: (config: ActionsConfig, { kv, email, redactor, signing }: TaskContextDeps, request: TaskContextRequest, signal: AbortSignal, runFetch: typeof fetch) => (scope: Record<string, unknown>) => Omit<ActionTaskContext, "log">;
39
+ export declare const taskContextFor: (config: ActionsConfig, { kv, email, redactor, signing }: TaskContextDeps, request: TaskContextRequest, signal: AbortSignal, runFetch: typeof fetch) => ((scope: Record<string, unknown>) => Omit<ActionTaskContext, "log">);
@@ -1,5 +1,6 @@
1
1
  import { ActionRunError } from "./errors.js";
2
2
  import { namespaceKv } from "./namespaceKv.js";
3
+ import { projectDataReader } from "./projectData.js";
3
4
  //#region src/modules/actions/runtime/taskContext.ts
4
5
  /**
5
6
  * Holds an answer to what the run is allowed to carry.
@@ -65,9 +66,10 @@ var createRunFetch = (base, signal, limits, lineage, onBudgetSpent) => {
65
66
  * every secret it resolves registered with the redactor, the space's `kv` namespace, the run's outbound budget, the
66
67
  * space's channels. Everything but `log`, which is the STEP's: each step keeps what it logged.
67
68
  */
68
- var taskContextFor = (config, { kv, email, redactor, signing }, request, signal, runFetch) => (scope) => {
69
+ var taskContextFor = (config, { kv, email, redactor, signing }, request, signal, runFetch) => {
69
70
  const { realtime } = config;
70
- return {
71
+ const readData = projectDataReader(config.lookups.getData, request.spaceId, request.at);
72
+ return (scope) => ({
71
73
  runId: request.runId,
72
74
  spaceId: request.spaceId,
73
75
  environment: request.environment,
@@ -104,6 +106,7 @@ var taskContextFor = (config, { kv, email, redactor, signing }, request, signal,
104
106
  dbDrivers: config.dbDrivers ?? [],
105
107
  email,
106
108
  emit: (chunk) => request.emit?.(redactor.redact(chunk)),
109
+ data: readData,
107
110
  ...signing?.({
108
111
  spaceId: request.spaceId,
109
112
  environment: request.environment
@@ -122,7 +125,7 @@ var taskContextFor = (config, { kv, email, redactor, signing }, request, signal,
122
125
  environment: request.environment
123
126
  }, topic, grant)
124
127
  } : {}
125
- };
128
+ });
126
129
  };
127
130
  //#endregion
128
131
  export { createRunFetch, taskContextFor };
@@ -129,7 +129,8 @@ export type ActionLookups = {
129
129
  getPluginFunctions?: (spaceId: number, at?: SpaceRevision) => Promise<Record<string, SpaceFunctions> | undefined>;
130
130
  /**
131
131
  * The space's own data as of that revision: its files' text by path (`products.json`), for a provider resolved on the
132
- * server whose `query` is `/data/<file>` — the platform's `dataDir`. Absent, such a provider reads nothing here.
132
+ * server whose `query` is `/data/<file>` and for a function's `ctx.data` — the platform's `dataDir`, and what a
133
+ * self-hosted server derives from its own when it is given none. Absent, neither reads anything here.
133
134
  */
134
135
  getData?: (spaceId: number, at?: SpaceRevision) => Promise<Record<string, string> | undefined>;
135
136
  /**
@@ -198,6 +199,11 @@ export type ActionTaskContext = {
198
199
  /** Signs with the space's own key (`ActionsConfig.signingSecret`) — `undefined` when the deployment gave none. */
199
200
  sign?: SpaceSigning['sign'];
200
201
  verify?: SpaceSigning['verify'];
202
+ /**
203
+ * One file of the space's data (`products.json`), parsed, as of the run's revision — read-only, and refused when
204
+ * the deployment keeps no data (`lookups.getData`, or `dataDir`). One lookup per run, whichever step reads it.
205
+ */
206
+ data: (file: string) => Promise<unknown>;
201
207
  };
202
208
  /** How the actions module reaches the server's realtime channels. Set by `createServer`, never by a deployment. */
203
209
  export type ActionRealtime = {
@@ -37,6 +37,15 @@ var loaderOf = (file) => {
37
37
  if (file.endsWith(".json")) return "json";
38
38
  return file.endsWith(".ts") ? "ts" : "js";
39
39
  };
40
+ /** Where a project keeps its data, seen from its functions: `src/data/` beside `src/functions/`. */
41
+ var DATA_FROM_FUNCTIONS = "../data/";
42
+ /**
43
+ * Why an import that climbs out of `functions/` is refused, and what to do instead — the space's data is the one thing
44
+ * outside it code reaches for, and it is read at run time (`ctx.data`), as of the run's version, not built in.
45
+ */
46
+ var outsideFunctions = (specifier, wanted) => {
47
+ return `"${specifier}" is outside functions/: functions import only files under functions/ — read the space's data with ctx.data('${wanted.startsWith(DATA_FROM_FUNCTIONS) ? wanted.slice(8) : "<file>"}')`;
48
+ };
40
49
  /** Resolves every import against the space's own files, and the contract; nothing on the disk is ever read. */
41
50
  var sourcePlugin = (files) => ({
42
51
  name: "plitzi-functions-source",
@@ -49,6 +58,7 @@ var sourcePlugin = (files) => ({
49
58
  if (args.kind !== "entry-point" && !args.path.startsWith(".")) return { errors: [{ text: `"${args.path}" cannot be imported: functions import their own files and ${CONTRACT_SPECIFIER}, nothing else` }] };
50
59
  const base = args.kind === "entry-point" ? "." : path.posix.dirname(args.importer);
51
60
  const wanted = normalize(path.posix.join(base, args.path));
61
+ if (wanted === ".." || wanted.startsWith("../")) return { errors: [{ text: outsideFunctions(args.path, wanted) }] };
52
62
  const found = [
53
63
  wanted,
54
64
  `${wanted}.ts`,
@@ -191,6 +191,10 @@ var readCall = (value) => {
191
191
  value: stringArg(call.value, "A value"),
192
192
  signature: stringArg(call.signature, "A signature")
193
193
  };
194
+ case "data": return {
195
+ op: "data",
196
+ file: stringArg(call.file, "A file")
197
+ };
194
198
  case "log": return {
195
199
  op: "log",
196
200
  values: Array.isArray(call.values) ? call.values : []
@@ -216,6 +220,7 @@ var answerCall = async (ctx, call) => {
216
220
  case "rateLimit": return ctx.rateLimit(call.bucket, call.limit);
217
221
  case "sign": return ctx.sign(call.value);
218
222
  case "verify": return ctx.verify(call.value, call.signature);
223
+ case "data": return ctx.data(call.file);
219
224
  case "log":
220
225
  ctx.log(...call.values);
221
226
  return;
@@ -65,6 +65,12 @@ var lineOf = (values) => values.map((value) => {
65
65
  var unavailable = (what) => () => Promise.reject(/* @__PURE__ */ new Error(`This server has no realtime channels (${what})`));
66
66
  var unsigned = () => Promise.reject(/* @__PURE__ */ new Error("This server signs nothing: its actions were given no signingSecret"));
67
67
  /**
68
+ * A plugin reads none of its space's data. The space keeps there what its pages must not carry, and a plugin was trusted
69
+ * to draw, not with what the space keeps — the same line as its credentials: handed the data, its `fetch` could send
70
+ * it to any host the plugin declared.
71
+ */
72
+ var noData = (file) => Promise.reject(/* @__PURE__ */ new Error(`The functions of a plugin read none of the data of its space ("${file}")`));
73
+ /**
68
74
  * What a space's code is handed, built from the run's own context: the one place a {@link FunctionContext} is made.
69
75
  * A self-hosted server hands it to the code directly; the platform answers the sandbox's calls with it. With a `scope`,
70
76
  * it is a plugin's code, and gets the plugin's corner of the space (`./scope`).
@@ -90,7 +96,8 @@ var functionContextFor = (ctx, hosts, scope) => {
90
96
  verify: ctx.verify ?? unsigned,
91
97
  publish: ctx.publish ?? unavailable("publish"),
92
98
  grant: ctx.grant ?? unavailable("grant"),
93
- revoke: ctx.revoke ?? unavailable("revoke")
99
+ revoke: ctx.revoke ?? unavailable("revoke"),
100
+ data: ctx.data
94
101
  };
95
102
  const kv = prefixKv(ctx.kv, pluginKvPrefix(scope.plugin));
96
103
  const { sign, verify } = ctx;
@@ -103,7 +110,8 @@ var functionContextFor = (ctx, hosts, scope) => {
103
110
  verify: verify ? (value, signature) => verify(pluginSigned(scope.plugin, value), signature) : unsigned,
104
111
  publish: noChannel,
105
112
  grant: noChannel,
106
- revoke: noChannel
113
+ revoke: noChannel,
114
+ data: noData
107
115
  };
108
116
  };
109
117
  //#endregion
@@ -67,6 +67,11 @@ export type FunctionContext = {
67
67
  grant: (topic: string, ttlSeconds?: number) => Promise<string>;
68
68
  /** Takes a grant back — or every grant for the topic, naming none. */
69
69
  revoke: (topic: string, grant?: string) => Promise<void>;
70
+ /**
71
+ * One file of the space's data — a project's `src/data/<file>` — parsed, as of the version the run belongs to: what
72
+ * a page must not carry, read here and answered only as far as it is shown. Read-only. `ctx.data('products.json')`.
73
+ */
74
+ data: (file: string) => Promise<unknown>;
70
75
  /** A line for whoever reads the run: the builder's Try, and the deployment's logs. */
71
76
  log: (...values: unknown[]) => void;
72
77
  /** Progress for a caller that asked for a stream — a no-op when nobody did. */
@@ -113,6 +113,10 @@ var createFunctionsDriver = (environment) => {
113
113
  value,
114
114
  signature
115
115
  }),
116
+ data: (file) => call({
117
+ op: "data",
118
+ file
119
+ }),
116
120
  log,
117
121
  emit: (chunk) => {
118
122
  call({
@@ -7,6 +7,8 @@ export type LocalFunctionsOptions = {
7
7
  credentials?: Record<string, Record<string, string>>;
8
8
  /** As the platform's: a plan's limits, for code that must fit them there. */
9
9
  limits?: Partial<FunctionLimits>;
10
+ /** The project's data folder (`src/data/`), what `ctx.data` reads — as the project's server reads it. */
11
+ dataDir?: string;
10
12
  fetchImpl?: typeof fetch;
11
13
  };
12
14
  export type LocalFunctions = {
@@ -26,4 +28,4 @@ export type LocalFunctions = {
26
28
  * isolates and limits, `ctx` over the same actions module — so what works here works there. `kv` lives as long as this
27
29
  * does. What `plitzi functions dev` is made of; needs `isolated-vm` and `core-js` installed, as any runner does.
28
30
  */
29
- export declare const createLocalFunctions: ({ credentials, limits, fetchImpl }?: LocalFunctionsOptions) => LocalFunctions;
31
+ export declare const createLocalFunctions: ({ credentials, limits, dataDir, fetchImpl }?: LocalFunctionsOptions) => LocalFunctions;
@@ -1,3 +1,4 @@
1
+ import { dataDirLookup } from "../actions/runtime/projectData.js";
1
2
  import { functionsInHand } from "./space.js";
2
3
  import { createActionsModule } from "../actions/index.js";
3
4
  import { functionTryEntry } from "./tryEntry.js";
@@ -19,14 +20,15 @@ var LOCAL_USER = {
19
20
  * isolates and limits, `ctx` over the same actions module — so what works here works there. `kv` lives as long as this
20
21
  * does. What `plitzi functions dev` is made of; needs `isolated-vm` and `core-js` installed, as any runner does.
21
22
  */
22
- var createLocalFunctions = ({ credentials = {}, limits, fetchImpl } = {}) => {
23
+ var createLocalFunctions = ({ credentials = {}, limits, dataDir, fetchImpl } = {}) => {
23
24
  const runner = createIsolateRunner({ concurrency: 1 });
24
25
  let loaded;
25
26
  const module = createActionsModule({
26
27
  lookups: {
27
28
  getAction: () => Promise.resolve(void 0),
28
29
  getCredential: (_spaceId, identifier) => Promise.resolve(Object.hasOwn(credentials, identifier) ? credentials[identifier] : void 0),
29
- getFunctions: () => Promise.resolve(loaded)
30
+ getFunctions: () => Promise.resolve(loaded),
31
+ ...dataDir ? { getData: dataDirLookup(dataDir) } : {}
30
32
  },
31
33
  functions: {
32
34
  runner,
@@ -104,6 +104,9 @@ export type FunctionCall = {
104
104
  op: 'verify';
105
105
  value: string;
106
106
  signature: string;
107
+ } | {
108
+ op: 'data';
109
+ file: string;
107
110
  }
108
111
  /** Each value as the guest could send it: JSON, with what JSON cannot say already said as text. */
109
112
  | {
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * The space trusted the plugin to draw something, not with what it keeps — so a plugin's functions are handed a narrower
6
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.
7
+ * declared, and none of the space's credentials, realtime channels or data.
8
8
  */
9
9
  export type FunctionScope = {
10
10
  plugin: string;
@@ -2,8 +2,8 @@ import { fetchOutbound } from "../../helpers/outboundGuard.js";
2
2
  import { writeFileAtomic } from "../../helpers/atomicFile.js";
3
3
  import { isAllowedImageHost } from "./allowlist.js";
4
4
  import { createHash } from "node:crypto";
5
- import path from "node:path";
6
5
  import fs from "node:fs/promises";
6
+ import path from "node:path";
7
7
  import { IMAGE_WIDTHS, isImageWidth } from "@plitzi/sdk-shared/helpers/images";
8
8
  //#region src/modules/images/imageProxy.ts
9
9
  /** The formats a picture may arrive in, as the file extension it is kept under. SVG is not one: served from this
@@ -170,11 +170,7 @@ var createAccountStore = (db, t) => {
170
170
  const row = await findBy("email", email);
171
171
  return row ? toRecord(row) : void 0;
172
172
  },
173
- /**
174
- * `refreshExpiresAt` comes off the ROW. A store that keeps it inside a session object and forgets to lift it out
175
- * has every renewal refused as expired — which does not fail at login, it fails a day later, silently, when the
176
- * access token ages out and the session that looked fine simply ends.
177
- */
173
+ /** `refreshExpiresAt` comes off the ROW: it is the deadline renewal honours, over the token's own expiry. */
178
174
  findByRefreshToken: async (token) => {
179
175
  const row = await selectOne(db, `SELECT ${ACCOUNT_COLUMNS}, s.refresh_expires_at, UNIX_TIMESTAMP(s.created_at) AS session_started_at
180
176
  FROM ${t.session} s
@@ -0,0 +1,31 @@
1
+ import { FunctionsDefinition } from '../functions/contract';
2
+ import { PluginSource, SSRServer } from '@plitzi/sdk-shared';
3
+ /**
4
+ * A project's plugins, as its server registers them: every folder of `src/plugins` built from its source, and every
5
+ * plugin of `vendor/plugins` run as it was built — each with its server half, when it has one.
6
+ */
7
+ export type ProjectPlugins = {
8
+ /** How each is built, by the name a space's `custom({ renderType })` renders it by. */
9
+ sources: Record<string, PluginSource>;
10
+ /**
11
+ * Which of them the space renders with — the deployment's `pluginNames`. Registering a plugin is not turning it on:
12
+ * a server can host many spaces, and nothing hands it this list on its own. Changed in place while developing, as
13
+ * folders come and go, so the deployment holding it renders with them.
14
+ */
15
+ names: string[];
16
+ /** Their server halves, by plugin name: `functions.plugins`. */
17
+ functions: Record<string, FunctionsDefinition>;
18
+ /** The names that are folders of `src/plugins`: the only ones a folder coming or going turns on or off. */
19
+ fromFolders: Set<string>;
20
+ };
21
+ /** Every plugin of the project at `root`, as its server registers them at boot. */
22
+ export declare const projectPlugins: (root: string) => Promise<ProjectPlugins>;
23
+ /**
24
+ * A plugin edited while developing is built again by the server, and the open pages swap it where it is drawn — the
25
+ * rest of the page, its state included, stays as it was (`devReload`). Its server half (`functions/`) is loaded again
26
+ * here, in place. A plugin ADDED — a new folder with an `index.ts`, what `plitzi add plugin` writes — is registered
27
+ * here, and the pages load again to render it; one removed is turned off. A folder the server cannot build, or whose
28
+ * server half has no entry, is said in the terminal as the layout check says it at boot — once, until it changes — and
29
+ * the server goes on. Answers what stops watching.
30
+ */
31
+ export declare const watchProjectPlugins: (root: string, server: SSRServer, plugins: ProjectPlugins) => (() => void);