@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.
- package/CHANGELOG.md +201 -0
- package/README.md +100 -14
- package/dist/adapters/jsonAdapters.d.ts +8 -4
- package/dist/adapters/jsonAdapters.js +5 -9
- package/dist/core/auth/api.d.ts +4 -1
- package/dist/core/auth/api.js +14 -5
- package/dist/core/auth/createAuth.d.ts +7 -1
- package/dist/core/auth/createAuth.js +14 -0
- package/dist/core/auth/credentials.d.ts +2 -0
- package/dist/core/auth/credentials.js +4 -2
- package/dist/core/auth/session.d.ts +2 -0
- package/dist/core/auth/session.js +6 -5
- package/dist/core/createServer.js +25 -4
- package/dist/core/http/stages/devReload.d.ts +2 -2
- package/dist/core/http/stages/devReload.js +2 -2
- package/dist/env.d.ts +1 -0
- package/dist/env.js +19 -0
- package/dist/env.test.d.ts +1 -0
- package/dist/functions-api.d.ts +5 -0
- package/dist/helpers/buildServerInfo.js +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/modules/actions/runtime/projectData.d.ts +18 -0
- package/dist/modules/actions/runtime/projectData.js +69 -0
- package/dist/modules/actions/runtime/taskContext.d.ts +1 -1
- package/dist/modules/actions/runtime/taskContext.js +6 -3
- package/dist/modules/actions/types.d.ts +7 -1
- package/dist/modules/functions/build.js +10 -0
- package/dist/modules/functions/capabilities.js +5 -0
- package/dist/modules/functions/context.js +10 -2
- package/dist/modules/functions/contract.d.ts +5 -0
- package/dist/modules/functions/driver.js +4 -0
- package/dist/modules/functions/local.d.ts +3 -1
- package/dist/modules/functions/local.js +4 -2
- package/dist/modules/functions/protocol.d.ts +3 -0
- package/dist/modules/functions/scope.d.ts +1 -1
- package/dist/modules/images/imageProxy.js +1 -1
- package/dist/modules/mysql/accounts.js +1 -5
- package/dist/modules/project/plugins.d.ts +31 -0
- package/dist/modules/project/plugins.js +162 -0
- package/dist/modules/project/serveProject.d.ts +78 -0
- package/dist/modules/project/serveProject.js +200 -0
- package/dist/modules/project/serveProject.test.d.ts +1 -0
- package/dist/modules/project/watchSpace.d.ts +14 -0
- package/dist/modules/project/watchSpace.js +77 -0
- package/dist/modules/realtime/failure.d.ts +6 -0
- package/dist/modules/realtime/failure.js +11 -0
- package/dist/modules/realtime/handlers.js +13 -3
- package/dist/modules/realtime/hub.js +4 -1
- package/dist/modules/realtime/socket.js +3 -2
- package/dist/modules/rsc/connectorRscData.d.ts +7 -3
- package/dist/modules/rsc/connectorRscData.js +3 -4
- package/dist/modules/rsc/handler.d.ts +1 -0
- package/dist/modules/rsc/handler.js +11 -1
- package/dist/modules/rsc/publicFileResolver.d.ts +4 -8
- package/dist/modules/rsc/publicFileResolver.js +6 -10
- package/dist/modules/runtime/bundle.js +1 -1
- package/dist/modules/ssr/prepareRender.js +10 -2
- package/dist/modules/ssr/registerExternalPlugins.js +1 -1
- package/dist/modules/ssr/vendor.test.d.ts +1 -0
- package/dist/modules/ssr/views/template.ejs +7 -3
- package/dist/plugins/compile.js +1 -1
- package/dist/plugins/copy.js +1 -1
- package/dist/plugins/manager.js +1 -1
- package/dist/project.d.ts +9 -0
- package/dist/project.js +2 -0
- package/package.json +16 -4
- package/skills/plitzi-authoring/CHEATSHEET.md +2 -2
- package/skills/plitzi-authoring/SKILL.md +3 -2
- package/skills/plitzi-authoring/reference/accessibility.md +2 -2
- package/skills/plitzi-authoring/reference/auth.md +75 -0
- package/skills/plitzi-authoring/reference/authoring-errors.md +18 -4
- package/skills/plitzi-authoring/reference/colours-and-motion.md +3 -2
- package/skills/plitzi-authoring/reference/efficiency.md +5 -2
- package/skills/plitzi-authoring/reference/elements-and-styles.md +23 -23
- package/skills/plitzi-authoring/reference/flows.md +11 -6
- package/skills/plitzi-authoring/reference/layouts.md +15 -18
- package/skills/plitzi-authoring/reference/plugins.md +1 -1
- package/skills/plitzi-authoring/reference/review-checklist.md +2 -1
- 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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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: `
|
|
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: `
|
|
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 {};
|
package/dist/functions-api.d.ts
CHANGED
|
@@ -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) =>
|
|
69
|
+
var taskContextFor = (config, { kv, email, redactor, signing }, request, signal, runFetch) => {
|
|
69
70
|
const { realtime } = config;
|
|
70
|
-
|
|
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
|
|
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. */
|
|
@@ -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
|
|
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);
|