@plitzi/sdk-server 0.37.9 → 0.38.1
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 +615 -0
- package/README.md +17 -0
- package/dist/adapters/cloudAdapters.d.ts +5 -3
- package/dist/adapters/cloudAdapters.js +120 -34
- package/dist/adapters/space/types.d.ts +3 -0
- package/dist/core/configSeam.d.ts +14 -0
- package/dist/core/configSeam.js +114 -0
- package/dist/core/configSeam.test.d.ts +1 -0
- package/dist/core/createServer.js +11 -5
- package/dist/core/createServer.publicData.test.d.ts +1 -0
- package/dist/core/freePort.d.ts +10 -0
- package/dist/core/freePort.js +36 -0
- package/dist/core/freePort.test.d.ts +1 -0
- package/dist/core/http/stages/images.d.ts +6 -0
- package/dist/core/http/stages/images.js +47 -0
- package/dist/core/http/stages/images.test.d.ts +1 -0
- package/dist/core/server/drain.js +9 -4
- package/dist/core/server/fleet/channel.js +1 -1
- package/dist/core/server/fleet/relay.js +1 -1
- package/dist/core/services/action.js +5 -4
- package/dist/core/services/registry.js +2 -0
- package/dist/helpers/cache/keys.d.ts +23 -10
- package/dist/helpers/cache/keys.js +16 -2
- package/dist/helpers/flagOverrides.d.ts +27 -0
- package/dist/helpers/flagOverrides.js +25 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/modules/actions/moduleFor.js +5 -3
- package/dist/modules/actions/runtime/flags.d.ts +16 -0
- package/dist/modules/actions/runtime/flags.js +49 -0
- package/dist/modules/actions/runtime/flags.test.d.ts +1 -0
- package/dist/modules/actions/runtime/kvList.js +1 -1
- package/dist/modules/actions/runtime/runAction.js +2 -0
- package/dist/modules/actions/transport/callHandler.js +8 -0
- package/dist/modules/actions/types.d.ts +14 -1
- package/dist/modules/connectors/engine.js +1 -1
- package/dist/modules/functions/capabilities.js +1 -1
- package/dist/modules/functions/load.js +2 -2
- package/dist/modules/functions/manifest.js +1 -1
- package/dist/modules/functions/runner/messages.js +1 -1
- package/dist/modules/images/allowlist.d.ts +5 -0
- package/dist/modules/images/allowlist.js +18 -0
- package/dist/modules/images/imageProxy.d.ts +36 -0
- package/dist/modules/images/imageProxy.js +273 -0
- package/dist/modules/images/imageProxy.test.d.ts +1 -0
- package/dist/modules/images/transform.d.ts +10 -0
- package/dist/modules/images/transform.js +28 -0
- package/dist/modules/images/transform.test.d.ts +1 -0
- package/dist/modules/rsc/connectorRscData.d.ts +12 -7
- package/dist/modules/rsc/connectorRscData.js +12 -8
- package/dist/modules/rsc/handler.js +18 -4
- package/dist/modules/rsc/publicFileResolver.d.ts +13 -0
- package/dist/modules/rsc/publicFileResolver.js +39 -0
- package/dist/modules/rsc/publicFileResolver.test.d.ts +1 -0
- package/dist/modules/rsc/resolveRscData.d.ts +4 -2
- package/dist/modules/rsc/resolveRscData.js +10 -2
- package/dist/modules/runtime/bundle.js +2 -2
- package/dist/modules/ssr/Component.d.ts +5 -1
- package/dist/modules/ssr/Component.js +3 -1
- package/dist/modules/ssr/prepareRender.js +36 -8
- package/package.json +19 -14
- package/skills/plitzi-authoring/CHEATSHEET.md +135 -0
- package/skills/plitzi-authoring/SKILL.md +71 -71
- package/skills/plitzi-authoring/recipes/carousel.ts +114 -0
- package/skills/plitzi-authoring/recipes/detail-page.ts +70 -0
- package/skills/plitzi-authoring/recipes/embed-and-svg.ts +48 -0
- package/skills/plitzi-authoring/recipes/every-few-seconds.ts +119 -0
- package/skills/plitzi-authoring/recipes/feature-flag.ts +24 -0
- package/skills/plitzi-authoring/recipes/filter-a-list.ts +63 -0
- package/skills/plitzi-authoring/recipes/forms-and-modals.ts +64 -0
- package/skills/plitzi-authoring/recipes/from-tailwind.ts +52 -0
- package/skills/plitzi-authoring/recipes/link-to-a-section.ts +35 -0
- package/skills/plitzi-authoring/recipes/link-with-data.ts +40 -0
- package/skills/plitzi-authoring/recipes/marquee.ts +57 -0
- package/skills/plitzi-authoring/recipes/plugin.ts +53 -0
- package/skills/plitzi-authoring/recipes/scroll-a-row.ts +79 -0
- package/skills/plitzi-authoring/recipes/show-data.ts +73 -0
- package/skills/plitzi-authoring/recipes/style-a-page.ts +45 -0
- package/skills/plitzi-authoring/recipes/typed-data.ts +64 -0
- package/skills/plitzi-authoring/recipes/usable-without-sight.ts +39 -0
- package/skills/plitzi-authoring/reference/accessibility.md +2 -2
- package/skills/plitzi-authoring/reference/authoring-errors.md +195 -53
- package/skills/plitzi-authoring/reference/colours-and-motion.md +63 -0
- package/skills/plitzi-authoring/reference/components.md +9 -1
- package/skills/plitzi-authoring/reference/data-and-visibility.md +29 -29
- package/skills/plitzi-authoring/reference/efficiency.md +53 -0
- package/skills/plitzi-authoring/reference/elements-and-styles.md +39 -24
- package/skills/plitzi-authoring/reference/feature-flags.md +78 -0
- package/skills/plitzi-authoring/reference/flows.md +25 -36
- package/skills/plitzi-authoring/reference/kept-state.md +28 -0
- package/skills/plitzi-authoring/reference/layouts.md +20 -3
- package/skills/plitzi-authoring/reference/lists.md +45 -16
- package/skills/plitzi-authoring/reference/plugins.md +15 -0
- package/skills/plitzi-authoring/reference/realtime.md +35 -0
- package/skills/plitzi-authoring/reference/review-checklist.md +8 -3
- package/skills/plitzi-authoring/reference/{templates-and-export.md → snippets-and-export.md} +9 -8
- package/skills/plitzi-authoring/reference/structure.md +29 -20
- package/skills/plitzi-authoring/reference/tailwind.md +19 -0
- package/skills/plitzi-authoring/reference/templates.md +6 -5
- package/skills/plitzi-authoring/reference/testing.md +11 -0
- package/skills/plitzi-authoring/reference/typed-sources.md +20 -0
- package/skills/plitzi-authoring/reference/validation.md +6 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { serverLog } from "../../../helpers/serverLog.js";
|
|
2
2
|
import { broadcast, onBroadcast } from "./link.js";
|
|
3
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
3
4
|
//#region src/core/server/fleet/relay.ts
|
|
4
5
|
/**
|
|
5
6
|
* `server.cache` and `server.plugins` in a fleet, whose workers each keep their own: what one process is told, every
|
|
@@ -10,7 +11,6 @@ import { broadcast, onBroadcast } from "./link.js";
|
|
|
10
11
|
* Code that runs in every process (a pub/sub subscription made where the server is created) is told N times; what
|
|
11
12
|
* is relayed is safe to apply again, and cheap: it only drops entries from memory.
|
|
12
13
|
*/
|
|
13
|
-
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
14
14
|
var PLUGIN_ACTIONS = [
|
|
15
15
|
"copy",
|
|
16
16
|
"compile",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { connectorLookupsOf } from "../configSeam.js";
|
|
1
2
|
import { handleActionCall } from "../../modules/actions/transport/callHandler.js";
|
|
2
3
|
import { handleActionCatalog } from "../../modules/actions/transport/catalogHandler.js";
|
|
3
4
|
import { handleActionWebhook } from "../../modules/actions/transport/webhookHandler.js";
|
|
@@ -38,8 +39,8 @@ var lineageOf = (req) => {
|
|
|
38
39
|
var actionStage = async (ctx) => {
|
|
39
40
|
const { config, req, actions } = ctx;
|
|
40
41
|
const actionPath = config.action?.path ?? "/_action";
|
|
41
|
-
const
|
|
42
|
-
if (!
|
|
42
|
+
const { connectors } = config;
|
|
43
|
+
if (!connectors && !actions) return false;
|
|
43
44
|
if (actions && req.method === "GET" && req.path === `${actionPath}/catalog`) {
|
|
44
45
|
ctx.operation = "action:catalog";
|
|
45
46
|
if (!req.ctx.user) {
|
|
@@ -104,7 +105,7 @@ var actionStage = async (ctx) => {
|
|
|
104
105
|
});
|
|
105
106
|
return true;
|
|
106
107
|
}
|
|
107
|
-
if (addressesAction || !
|
|
108
|
+
if (addressesAction || !connectors) {
|
|
108
109
|
ctx.operation = "action";
|
|
109
110
|
ctx.res.setStatus(404);
|
|
110
111
|
ctx.res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
@@ -115,7 +116,7 @@ var actionStage = async (ctx) => {
|
|
|
115
116
|
return true;
|
|
116
117
|
}
|
|
117
118
|
ctx.operation = "action";
|
|
118
|
-
await handleAction(req, ctx.res, config,
|
|
119
|
+
await handleAction(req, ctx.res, config, connectorLookupsOf(connectors));
|
|
119
120
|
return true;
|
|
120
121
|
};
|
|
121
122
|
//#endregion
|
|
@@ -2,6 +2,7 @@ import { actionStage } from "./action.js";
|
|
|
2
2
|
import { functionRoutesStage } from "./functionRoutes.js";
|
|
3
3
|
import { realtimeStage } from "./realtime.js";
|
|
4
4
|
import { rscStage } from "./rsc.js";
|
|
5
|
+
import { imagesStage } from "../http/stages/images.js";
|
|
5
6
|
import { notFoundStage, ssrStage } from "./ssr.js";
|
|
6
7
|
import { authRoutesStages } from "../http/stages/authRoutes.js";
|
|
7
8
|
import { fontAssetsStage } from "../http/stages/fontAssets.js";
|
|
@@ -17,6 +18,7 @@ var buildPagePipeline = (services, extensions = {}) => {
|
|
|
17
18
|
wellKnownStage,
|
|
18
19
|
pluginAssetsStage,
|
|
19
20
|
fontAssetsStage,
|
|
21
|
+
imagesStage,
|
|
20
22
|
...authRoutesStages,
|
|
21
23
|
configStaticStage,
|
|
22
24
|
sdkAssetsStage
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/** What a cache key reads off a request: where it is, and — from the resolved deployment — what the flags are at. */
|
|
2
|
+
type CacheKeyRequest = {
|
|
3
|
+
hostname: string;
|
|
4
|
+
path: string;
|
|
5
|
+
search: string;
|
|
6
|
+
ctx?: {
|
|
7
|
+
spaceDeployment?: {
|
|
8
|
+
flagsVersion?: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
};
|
|
1
12
|
/**
|
|
2
13
|
* The key a rendered document is cached under.
|
|
3
14
|
*
|
|
@@ -14,13 +25,17 @@
|
|
|
14
25
|
* values, so a page drawn with one visitor's toolbar must not be served to the next. Keyed by its raw value — this
|
|
15
26
|
* key does not know which keys a space declares, and a space that declares none has no such cookie to split on.
|
|
16
27
|
*
|
|
28
|
+
* So are the feature flags a tester forced from the dev tools, on a page that authorizes debugging: drawn with them,
|
|
29
|
+
* the page is not everybody's, and a tester holding the cookie must never be handed a page drawn without it. Keyed
|
|
30
|
+
* by its raw value, like the kept state.
|
|
31
|
+
*
|
|
32
|
+
* So is what the space's flags are at (`flagsVersion` on the resolved deployment), for a deployment that changes them
|
|
33
|
+
* apart from its revisions: a flag turned in production must reach the next visitor, while the revision is the same.
|
|
34
|
+
*
|
|
17
35
|
* Read from the request here rather than handed in, so a call site cannot key the page without it. Only those
|
|
18
36
|
* cookies are read: keying the whole header would split the cache on every analytics cookie a visitor carries.
|
|
19
37
|
*/
|
|
20
|
-
export declare const buildHtmlCacheKey: (accessToken: string | undefined, spaceId: number | string | null, environment: string, revision: number, req: {
|
|
21
|
-
hostname: string;
|
|
22
|
-
path: string;
|
|
23
|
-
search: string;
|
|
38
|
+
export declare const buildHtmlCacheKey: (accessToken: string | undefined, spaceId: number | string | null, environment: string, revision: number, req: CacheKeyRequest & {
|
|
24
39
|
headers: {
|
|
25
40
|
cookie?: string;
|
|
26
41
|
host?: string;
|
|
@@ -36,9 +51,7 @@ export declare const readHtmlCacheKey: (key: string) => {
|
|
|
36
51
|
environment: string;
|
|
37
52
|
hostname: string;
|
|
38
53
|
};
|
|
39
|
-
|
|
40
|
-
export declare const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
search: string;
|
|
44
|
-
}) => string;
|
|
54
|
+
/** The space as read for a render: its revision, and what its flags are at when they change apart from it. */
|
|
55
|
+
export declare const buildOfflineDataCacheKey: (spaceId: number, environment: string, revision: number, flagsVersion: string | undefined) => string;
|
|
56
|
+
export declare const buildRscCacheKey: (spaceId: number, environment: string, revision: number, userId: string | number | undefined, idsParam: string | undefined, req: CacheKeyRequest) => string;
|
|
57
|
+
export {};
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { ssrPaintedCookieName } from "../paintedCookie.js";
|
|
2
2
|
import { readCookie } from "../readCookie.js";
|
|
3
|
+
import { flagsCookieName } from "@plitzi/sdk-shared/flags";
|
|
3
4
|
import { debugCookieName } from "@plitzi/sdk-shared/devTools";
|
|
4
5
|
import { themeFromCookies } from "@plitzi/sdk-shared/theme";
|
|
5
6
|
//#region src/helpers/cache/keys.ts
|
|
7
|
+
var flagsVersionOf = (req) => req.ctx?.spaceDeployment?.flagsVersion ?? "";
|
|
6
8
|
/** The fields of an HTML key, in the order they are joined. */
|
|
7
9
|
var HTML_KEY_FIELDS = [
|
|
8
10
|
"accessToken",
|
|
9
11
|
"spaceId",
|
|
10
12
|
"environment",
|
|
11
13
|
"revision",
|
|
14
|
+
"flags",
|
|
12
15
|
"theme",
|
|
13
16
|
"painted",
|
|
14
17
|
"debugHidden",
|
|
18
|
+
"forcedFlags",
|
|
15
19
|
"hostname",
|
|
16
20
|
"path",
|
|
17
21
|
"search"
|
|
@@ -32,6 +36,13 @@ var HTML_KEY_FIELDS = [
|
|
|
32
36
|
* values, so a page drawn with one visitor's toolbar must not be served to the next. Keyed by its raw value — this
|
|
33
37
|
* key does not know which keys a space declares, and a space that declares none has no such cookie to split on.
|
|
34
38
|
*
|
|
39
|
+
* So are the feature flags a tester forced from the dev tools, on a page that authorizes debugging: drawn with them,
|
|
40
|
+
* the page is not everybody's, and a tester holding the cookie must never be handed a page drawn without it. Keyed
|
|
41
|
+
* by its raw value, like the kept state.
|
|
42
|
+
*
|
|
43
|
+
* So is what the space's flags are at (`flagsVersion` on the resolved deployment), for a deployment that changes them
|
|
44
|
+
* apart from its revisions: a flag turned in production must reach the next visitor, while the revision is the same.
|
|
45
|
+
*
|
|
35
46
|
* Read from the request here rather than handed in, so a call site cannot key the page without it. Only those
|
|
36
47
|
* cookies are read: keying the whole header would split the cache on every analytics cookie a visitor carries.
|
|
37
48
|
*/
|
|
@@ -41,9 +52,11 @@ var buildHtmlCacheKey = (accessToken = "anonymous", spaceId, environment, revisi
|
|
|
41
52
|
spaceId: String(spaceId ?? 1),
|
|
42
53
|
environment,
|
|
43
54
|
revision: String(revision),
|
|
55
|
+
flags: flagsVersionOf(req),
|
|
44
56
|
theme: themeFromCookies(req.headers.cookie) ?? "",
|
|
45
57
|
painted: readCookie(req.headers.cookie, ssrPaintedCookieName(req.headers.host)) ?? "",
|
|
46
58
|
debugHidden: readCookie(req.headers.cookie, debugCookieName(req.headers.host)) === "false" ? "debug-off" : "",
|
|
59
|
+
forcedFlags: readCookie(req.headers.cookie, flagsCookieName(req.headers.host)) ?? "",
|
|
47
60
|
hostname: req.hostname,
|
|
48
61
|
path: req.path,
|
|
49
62
|
search: req.search
|
|
@@ -64,7 +77,8 @@ var readHtmlCacheKey = (key) => {
|
|
|
64
77
|
hostname: at("hostname")
|
|
65
78
|
};
|
|
66
79
|
};
|
|
67
|
-
|
|
68
|
-
var
|
|
80
|
+
/** The space as read for a render: its revision, and what its flags are at when they change apart from it. */
|
|
81
|
+
var buildOfflineDataCacheKey = (spaceId, environment, revision, flagsVersion) => `${spaceId}|${environment}|${revision}|${flagsVersion ?? ""}`;
|
|
82
|
+
var buildRscCacheKey = (spaceId, environment, revision, userId, idsParam, req) => `${spaceId}\0${environment}\0${revision}\0${flagsVersionOf(req)}\0${userId ?? "anon"}\0${idsParam ?? ""}\0${req.hostname}\0${req.path}\0${req.search}`;
|
|
69
83
|
//#endregion
|
|
70
84
|
export { buildHtmlCacheKey, buildOfflineDataCacheKey, buildRscCacheKey, readHtmlCacheKey };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Environment, FlagOverrides, SSRServerConfig } from '@plitzi/sdk-shared';
|
|
2
|
+
/** The deployment's own layer for one space: `config.flags`, asked per space when it is a function. */
|
|
3
|
+
export declare const serverFlagsFor: (config: Pick<SSRServerConfig, "flags">, space: {
|
|
4
|
+
spaceId: number;
|
|
5
|
+
environment: Environment;
|
|
6
|
+
}) => Record<string, boolean> | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* The flags a tester forced from the dev tools, for a request allowed to debug — undefined for everybody else, and for
|
|
9
|
+
* a tester who forced nothing. Authorization is asked only when there is a cookie to honour, because answering it can
|
|
10
|
+
* mean reading the space.
|
|
11
|
+
*/
|
|
12
|
+
export declare const forcedFlagsFor: (req: {
|
|
13
|
+
headers: {
|
|
14
|
+
cookie?: string;
|
|
15
|
+
host?: string;
|
|
16
|
+
};
|
|
17
|
+
}, authorized: boolean | (() => Promise<boolean>)) => Promise<Record<string, boolean> | undefined>;
|
|
18
|
+
/** Both layers this server answers for, for one request. */
|
|
19
|
+
export declare const requestFlagOverrides: (config: Pick<SSRServerConfig, "flags">, req: {
|
|
20
|
+
headers: {
|
|
21
|
+
cookie?: string;
|
|
22
|
+
host?: string;
|
|
23
|
+
};
|
|
24
|
+
}, space: {
|
|
25
|
+
spaceId: number;
|
|
26
|
+
environment: Environment;
|
|
27
|
+
}, authorized: boolean | (() => Promise<boolean>)) => Promise<FlagOverrides>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { forcedFlagsFromCookies } from "@plitzi/sdk-shared/flags";
|
|
2
|
+
//#region src/helpers/flagOverrides.ts
|
|
3
|
+
/** The deployment's own layer for one space: `config.flags`, asked per space when it is a function. */
|
|
4
|
+
var serverFlagsFor = (config, space) => typeof config.flags === "function" ? config.flags(space) : config.flags;
|
|
5
|
+
/**
|
|
6
|
+
* The flags a tester forced from the dev tools, for a request allowed to debug — undefined for everybody else, and for
|
|
7
|
+
* a tester who forced nothing. Authorization is asked only when there is a cookie to honour, because answering it can
|
|
8
|
+
* mean reading the space.
|
|
9
|
+
*/
|
|
10
|
+
var forcedFlagsFor = async (req, authorized) => {
|
|
11
|
+
const forced = forcedFlagsFromCookies(req.headers.cookie, req.headers.host);
|
|
12
|
+
if (Object.keys(forced).length === 0) return;
|
|
13
|
+
return (typeof authorized === "boolean" ? authorized : await authorized()) ? forced : void 0;
|
|
14
|
+
};
|
|
15
|
+
/** Both layers this server answers for, for one request. */
|
|
16
|
+
var requestFlagOverrides = async (config, req, space, authorized) => {
|
|
17
|
+
const server = serverFlagsFor(config, space);
|
|
18
|
+
const qa = await forcedFlagsFor(req, authorized);
|
|
19
|
+
return {
|
|
20
|
+
...server ? { server } : {},
|
|
21
|
+
...qa ? { qa } : {}
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { forcedFlagsFor, requestFlagOverrides, serverFlagsFor };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { createServer, resolveServices } from './core/createServer';
|
|
2
2
|
export { closeOnSignals } from './core/server/closeOnSignals';
|
|
3
3
|
export { loadFunctions, readFunctionsDir } from './modules/functions/load';
|
|
4
|
+
export { freePort } from './core/freePort';
|
|
4
5
|
export { registerHealthCheck, buildHealthPayload } from './core/health';
|
|
5
6
|
export { consoleLogger, isLogged, logLevelOf, renderLogEvent, serverLog } from './helpers/serverLog';
|
|
6
7
|
export type { HealthCheckApp, HealthIdentity } from './core/health';
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { buildHealthPayload, registerHealthCheck } from "./core/health.js";
|
|
|
10
10
|
import { createServer } from "./core/createServer.js";
|
|
11
11
|
import { closeOnSignals } from "./core/server/closeOnSignals.js";
|
|
12
12
|
import { loadFunctions, readFunctionsDir } from "./modules/functions/load.js";
|
|
13
|
+
import { freePort } from "./core/freePort.js";
|
|
13
14
|
import { createCloudAdapters } from "./adapters/cloudAdapters.js";
|
|
14
15
|
import { createJsonAdapters } from "./adapters/jsonAdapters.js";
|
|
15
16
|
import { createAuthAdapters } from "./adapters/authAdapters.js";
|
|
@@ -18,4 +19,4 @@ import { authoringPreview, fixedSpace, presentsCredential, verifiedDomain, wildc
|
|
|
18
19
|
import { createSpaceAdapters } from "./adapters/space/index.js";
|
|
19
20
|
import { createLocalFontStore, hostedPathsOf } from "./core/fontStore.js";
|
|
20
21
|
import { createRedisPubSub } from "./modules/realtime/redisPubSub.js";
|
|
21
|
-
export { PREVIEW_TOKEN_PARAM, authoringPreview, buildHealthPayload, closeOnSignals, consoleLogger, createAuthAdapters, createCloudAdapters, createConnectorResolver, createJsonAdapters, createLocalFontStore, createMemoryCache, createMemoryDraftStore, createMemoryPubSub, createRedisPubSub, createServer, createSpaceAdapters, fetchConnectorRecords, fixedSpace, hostedPathsOf, isLogged, loadFunctions, logLevelOf, presentsCredential, readFunctionsDir, rebaseMedia, refuse, registerHealthCheck, renderLogEvent, resolveRscData, resolveServices, serverLog, takeDraftOverride, verifiedDomain, wildcardSubdomain, writeConnectorRecord };
|
|
22
|
+
export { PREVIEW_TOKEN_PARAM, authoringPreview, buildHealthPayload, closeOnSignals, consoleLogger, createAuthAdapters, createCloudAdapters, createConnectorResolver, createJsonAdapters, createLocalFontStore, createMemoryCache, createMemoryDraftStore, createMemoryPubSub, createRedisPubSub, createServer, createSpaceAdapters, fetchConnectorRecords, fixedSpace, freePort, hostedPathsOf, isLogged, loadFunctions, logLevelOf, presentsCredential, readFunctionsDir, rebaseMedia, refuse, registerHealthCheck, renderLogEvent, resolveRscData, resolveServices, serverLog, takeDraftOverride, verifiedDomain, wildcardSubdomain, writeConnectorRecord };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { actionLookupsOf, dbDriversOf, functionsConfigOf } from "../../core/configSeam.js";
|
|
1
2
|
import { createActionsModule } from "./index.js";
|
|
2
3
|
import { realtimeModuleFor } from "../realtime/index.js";
|
|
3
4
|
//#region src/modules/actions/moduleFor.ts
|
|
@@ -19,8 +20,9 @@ var actionsModuleFor = (config) => {
|
|
|
19
20
|
const existing = modules.get(config);
|
|
20
21
|
if (existing) return existing;
|
|
21
22
|
const module = createActionsModule({
|
|
22
|
-
lookups,
|
|
23
|
-
|
|
23
|
+
lookups: actionLookupsOf(lookups),
|
|
24
|
+
flags: config.flags,
|
|
25
|
+
functions: functionsConfigOf(config.functions),
|
|
24
26
|
limits: config.action?.limits,
|
|
25
27
|
concurrency: config.action?.concurrency,
|
|
26
28
|
kv: config.action?.kv,
|
|
@@ -28,7 +30,7 @@ var actionsModuleFor = (config) => {
|
|
|
28
30
|
rateLimit: config.action?.rateLimit,
|
|
29
31
|
idempotency: config.action?.idempotency,
|
|
30
32
|
signingSecret: config.action?.signingSecret,
|
|
31
|
-
dbDrivers: config.action?.dbDrivers,
|
|
33
|
+
dbDrivers: dbDriversOf(config.action?.dbDrivers),
|
|
32
34
|
email: config.action?.email,
|
|
33
35
|
onRun: config.action?.onRun,
|
|
34
36
|
realtime: realtimeModuleFor(config)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ActionRunRequest, ActionsConfig } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a flow names `flags` anywhere — a template, a `when` rule's field. Only those are worth a lookup: most flows
|
|
4
|
+
* read no flag, and asking for the space's flags on every run of every one of them would be a read per click.
|
|
5
|
+
*
|
|
6
|
+
* Over the serialized document, so a template in any step's params counts without knowing every step's shape. A word
|
|
7
|
+
* `flags` that is not the source costs one lookup that was not needed, never a flow without its flags. Kept per
|
|
8
|
+
* document, which the lookups hand out once per version.
|
|
9
|
+
*/
|
|
10
|
+
export declare const readsFlags: (document: object) => boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The space's flags as this run sees them: what the space declares at the run's revision, matched against the run's
|
|
13
|
+
* environment and visitor, then the server's own layer and a tester's forced one — the same order a page resolves
|
|
14
|
+
* them in. A run has no URL, so a rule on the host or the route does not match here.
|
|
15
|
+
*/
|
|
16
|
+
export declare const flagsForRun: (config: Pick<ActionsConfig, "lookups" | "flags">, request: Pick<ActionRunRequest, "entry" | "spaceId" | "environment" | "at" | "user" | "forcedFlags">) => Promise<Record<string, boolean>>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { serverLog } from "../../../helpers/serverLog.js";
|
|
2
|
+
import { serverFlagsFor } from "../../../helpers/flagOverrides.js";
|
|
3
|
+
import { flagUserFromSSR, flagValues, resolveFlags, undeclaredFlagOverrides } from "@plitzi/sdk-shared/flags";
|
|
4
|
+
//#region src/modules/actions/runtime/flags.ts
|
|
5
|
+
var reading = /* @__PURE__ */ new WeakMap();
|
|
6
|
+
/**
|
|
7
|
+
* Whether a flow names `flags` anywhere — a template, a `when` rule's field. Only those are worth a lookup: most flows
|
|
8
|
+
* read no flag, and asking for the space's flags on every run of every one of them would be a read per click.
|
|
9
|
+
*
|
|
10
|
+
* Over the serialized document, so a template in any step's params counts without knowing every step's shape. A word
|
|
11
|
+
* `flags` that is not the source costs one lookup that was not needed, never a flow without its flags. Kept per
|
|
12
|
+
* document, which the lookups hand out once per version.
|
|
13
|
+
*/
|
|
14
|
+
var readsFlags = (document) => {
|
|
15
|
+
const known = reading.get(document);
|
|
16
|
+
if (known !== void 0) return known;
|
|
17
|
+
const reads = /\bflags\b/.test(JSON.stringify(document));
|
|
18
|
+
reading.set(document, reads);
|
|
19
|
+
return reads;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The space's flags as this run sees them: what the space declares at the run's revision, matched against the run's
|
|
23
|
+
* environment and visitor, then the server's own layer and a tester's forced one — the same order a page resolves
|
|
24
|
+
* them in. A run has no URL, so a rule on the host or the route does not match here.
|
|
25
|
+
*/
|
|
26
|
+
var flagsForRun = async (config, request) => {
|
|
27
|
+
if (!readsFlags(request.entry.document)) return {};
|
|
28
|
+
const { getFlags } = config.lookups;
|
|
29
|
+
if (!getFlags) {
|
|
30
|
+
serverLog.warn("Actions", `Action "${request.entry.id}" reads flags, but this deployment's action lookups have no getFlags: it sees none.`);
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
const declared = await getFlags(request.spaceId, request.at);
|
|
34
|
+
const { spaceId, environment, user } = request;
|
|
35
|
+
const overrides = {
|
|
36
|
+
server: serverFlagsFor(config, {
|
|
37
|
+
spaceId,
|
|
38
|
+
environment
|
|
39
|
+
}),
|
|
40
|
+
qa: request.forcedFlags
|
|
41
|
+
};
|
|
42
|
+
undeclaredFlagOverrides(declared, { server: overrides.server }).forEach(({ name }) => serverLog.warn("Actions", `The server sets the flag "${name}", which space ${spaceId} does not declare.`));
|
|
43
|
+
return flagValues(resolveFlags(declared, {
|
|
44
|
+
environment,
|
|
45
|
+
user: flagUserFromSSR(user)
|
|
46
|
+
}, overrides));
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
export { flagsForRun, readsFlags };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
1
2
|
//#region src/modules/actions/runtime/kvList.ts
|
|
2
3
|
/**
|
|
3
4
|
* The most a list holds. A list is one value of the store, read and written whole, so it is for what a page shows at
|
|
@@ -11,7 +12,6 @@ var MAX_LIST_ENTRIES = 500;
|
|
|
11
12
|
var MAX_LIST_BYTES = 128e3;
|
|
12
13
|
var ID = /^[A-Za-z0-9:_.@-]{1,128}$/;
|
|
13
14
|
var listIdProblem = (id) => typeof id === "string" && ID.test(id) ? void 0 : "A list entry’s id is 1-128 of A-Z a-z 0-9 : _ . @ -";
|
|
14
|
-
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
15
15
|
var isEntry = (value) => isRecord(value) && typeof value.id === "string" && typeof value.score === "number";
|
|
16
16
|
/** The entries a stored list holds — none for a key that holds nothing, or something that is not a list. */
|
|
17
17
|
var parseList = (raw) => {
|
|
@@ -5,6 +5,7 @@ import { createKvStore } from "./kvStore.js";
|
|
|
5
5
|
import { resolveLimits } from "./limits.js";
|
|
6
6
|
import { createMemoryKv } from "./memoryKv.js";
|
|
7
7
|
import { createEmailSender } from "./email.js";
|
|
8
|
+
import { flagsForRun } from "./flags.js";
|
|
8
9
|
import { createRedactor, projectUser } from "./scope.js";
|
|
9
10
|
import { precheckRun } from "./precheck.js";
|
|
10
11
|
import { createSigning } from "./signing.js";
|
|
@@ -240,6 +241,7 @@ var createActionRunner = (config, registryFor, baseFetch = fetch) => {
|
|
|
240
241
|
const scope = {
|
|
241
242
|
input: values,
|
|
242
243
|
user: projectUser(request.user),
|
|
244
|
+
flags: await flagsForRun(config, request),
|
|
243
245
|
spaceId: request.spaceId,
|
|
244
246
|
environment: request.environment,
|
|
245
247
|
trigger: request.trigger,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { serverLog } from "../../../helpers/serverLog.js";
|
|
2
2
|
import { triggerParams } from "../runtime/triggers.js";
|
|
3
3
|
import { ActionRunError } from "../runtime/errors.js";
|
|
4
|
+
import { forcedFlagsFor } from "../../../helpers/flagOverrides.js";
|
|
4
5
|
import { precheckRun } from "../runtime/precheck.js";
|
|
5
6
|
import onAbort from "../../../helpers/onAbort.js";
|
|
6
7
|
import { openStream, wantsStream } from "./stream.js";
|
|
@@ -176,6 +177,12 @@ var handleActionCall = async (deps) => {
|
|
|
176
177
|
const readsRun = mayReadTrace(entryPoint, req.ctx.user);
|
|
177
178
|
const fullTrace = readsRun && (authoring === true || config.devMode === true);
|
|
178
179
|
const showsOutline = async () => readsRun && (fullTrace || await resolveDebugAuthorization(config, async () => (await config.adapters.getOfflineData(spaceId, environment, revision))?.schema.settings));
|
|
180
|
+
/**
|
|
181
|
+
* The flags a tester forced on the page this call came from, so the flow sees the feature the page was drawn with.
|
|
182
|
+
* Only for a page allowed to debug — the cookie is the visitor's own, and anywhere else it would let any of them
|
|
183
|
+
* switch a feature on for the server too. The authorization is asked only when there is a cookie to honour.
|
|
184
|
+
*/
|
|
185
|
+
const forcedFlags = await forcedFlagsFor(req, () => resolveDebugAuthorization(config, async () => (await config.adapters.getOfflineData(spaceId, environment, revision))?.schema.settings));
|
|
179
186
|
let outcome;
|
|
180
187
|
try {
|
|
181
188
|
const result = await module.runAction({
|
|
@@ -192,6 +199,7 @@ var handleActionCall = async (deps) => {
|
|
|
192
199
|
environment,
|
|
193
200
|
revision
|
|
194
201
|
},
|
|
202
|
+
...forcedFlags ? { forcedFlags } : {},
|
|
195
203
|
signal: run.controller.signal,
|
|
196
204
|
...stream ? {
|
|
197
205
|
emit: (chunk) => stream.send({
|
|
@@ -4,7 +4,7 @@ import { SpaceFunctions } from '../functions/protocol';
|
|
|
4
4
|
import { KvChangeLifetime } from './runtime/kvChange';
|
|
5
5
|
import { KvListEntry, KvListPut, KvListPutOptions, KvListRange } from './runtime/kvList';
|
|
6
6
|
import { SpaceSigning } from './runtime/signing';
|
|
7
|
-
import { ActionEmailConfig, ActionJobQueue, ActionEntry, ActionLimits, ActionRejectRecord, ActionRunRecord, ActionRunStatus, ActionRunStep, ActionTriggerType, ConnectorManifest, Environment, InteractionCallbackParam, InteractionNode, InteractionNodeStatus, SpaceRevision, SSRUser } from '@plitzi/sdk-shared';
|
|
7
|
+
import { ActionEmailConfig, ActionJobQueue, ActionEntry, ActionLimits, ActionRejectRecord, ActionRunRecord, ActionRunStatus, ActionRunStep, ActionTriggerType, ConnectorManifest, Environment, InteractionCallbackParam, InteractionNode, InteractionNodeStatus, Schema, SpaceRevision, SSRServerConfig, SSRUser } from '@plitzi/sdk-shared';
|
|
8
8
|
/** Resolved secret material, as the connector engine already models it. */
|
|
9
9
|
export type ActionCredential = Record<string, string>;
|
|
10
10
|
/**
|
|
@@ -120,6 +120,11 @@ export type ActionLookups = {
|
|
|
120
120
|
* asked when a run names a task the deployment does not have, or a request a path under `/api/`.
|
|
121
121
|
*/
|
|
122
122
|
getFunctions?: (spaceId: number, at?: SpaceRevision) => Promise<SpaceFunctions | undefined>;
|
|
123
|
+
/**
|
|
124
|
+
* The feature flags the space declares as of that revision, for a flow that reads `{{ flags.<name> }}`. Only asked
|
|
125
|
+
* for a run whose document names `flags` at all. Absent, such a flow sees no flags — and the server says so.
|
|
126
|
+
*/
|
|
127
|
+
getFlags?: (spaceId: number, at?: SpaceRevision) => Promise<Schema['flags']>;
|
|
123
128
|
};
|
|
124
129
|
/**
|
|
125
130
|
* The only door a task has to the outside world.
|
|
@@ -235,6 +240,8 @@ export type ActionTaskRegistry = {
|
|
|
235
240
|
/** What a deployment hands to `createServer` under `actions`. Absent → the module is never constructed. */
|
|
236
241
|
export type ActionsConfig = {
|
|
237
242
|
lookups: ActionLookups;
|
|
243
|
+
/** The flags this deployment decides — the server's own `flags`, which a flow sees the way a page does. */
|
|
244
|
+
flags?: SSRServerConfig['flags'];
|
|
238
245
|
/** Code of the server's own, and the sandbox for the spaces' — see `FunctionsConfig`. */
|
|
239
246
|
functions?: FunctionsConfig;
|
|
240
247
|
/** Ceilings a per-action document may tighten but never exceed. */
|
|
@@ -386,6 +393,12 @@ export type ActionRunRequest = {
|
|
|
386
393
|
* miniature.
|
|
387
394
|
*/
|
|
388
395
|
at?: SpaceRevision;
|
|
396
|
+
/**
|
|
397
|
+
* The flags a tester forced, for a run started from a page allowed to debug — the same `qa` layer the page was
|
|
398
|
+
* drawn with, so a forced feature is not on in the page and off in the flow behind it. Never what the SDK's own
|
|
399
|
+
* `flags` say: those are the browser's claim, and a flow does not take a claim as a fact.
|
|
400
|
+
*/
|
|
401
|
+
forcedFlags?: Record<string, boolean>;
|
|
389
402
|
emit?: (chunk: unknown) => void;
|
|
390
403
|
/** Reports each step as it settles, for a caller watching the run happen. Absent for a plain request/response. */
|
|
391
404
|
onNode?: (id: string, status: InteractionNodeStatus) => void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fetchOutbound } from "../../helpers/outboundGuard.js";
|
|
2
2
|
import { getByPath } from "./getByPath.js";
|
|
3
|
-
import { processTwig } from "@plitzi/sdk-shared/helpers/twigWrapper";
|
|
4
3
|
import { BODYLESS_METHODS, DEFAULT_READ_ENDPOINT, EMPTY_RESPONSE_METHODS } from "@plitzi/sdk-shared/connectors";
|
|
4
|
+
import { processTwig } from "@plitzi/sdk-shared/helpers/twigWrapper";
|
|
5
5
|
//#region src/modules/connectors/engine.ts
|
|
6
6
|
var DEFAULT_LIMIT = 10;
|
|
7
7
|
/** Only scalars can become a URL fragment, a header or a record key. Anything else is treated as absent. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
1
2
|
//#region src/modules/functions/capabilities.ts
|
|
2
3
|
var TEXTUAL = /^(text\/|application\/([\w.+-]*\+)?(json|xml|javascript|x-www-form-urlencoded))/i;
|
|
3
4
|
/** A body as the wire carries it: as text when its type says it is text, base64 otherwise. */
|
|
@@ -42,7 +43,6 @@ var numberArg = (value, what) => {
|
|
|
42
43
|
return value;
|
|
43
44
|
};
|
|
44
45
|
var optionalNumber = (value, what) => value === void 0 || value === null ? void 0 : numberArg(value, what);
|
|
45
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46
46
|
var headerPairsOf = (value) => Array.isArray(value) ? value.flatMap((pair) => Array.isArray(pair) && typeof pair[0] === "string" && typeof pair[1] === "string" ? [[pair[0], pair[1]]] : []) : [];
|
|
47
47
|
var wireBodyFrom = (value) => {
|
|
48
48
|
const body = isRecord(value) ? value : void 0;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { buildFunctions } from "./build.js";
|
|
2
2
|
import { readFunctionsSource } from "@plitzi/sdk-shared/actions";
|
|
3
|
-
import {
|
|
3
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
4
4
|
import { readFile, readdir } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
//#region src/modules/functions/load.ts
|
|
6
7
|
var nodeReader = {
|
|
7
8
|
list: async (dir) => (await readdir(dir, { withFileTypes: true })).map((entry) => ({
|
|
@@ -10,7 +11,6 @@ var nodeReader = {
|
|
|
10
11
|
})),
|
|
11
12
|
read: (file) => readFile(file, "utf8")
|
|
12
13
|
};
|
|
13
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
14
|
/** Whether a module's default export has a definition's shape: what a server can register without surprises. */
|
|
15
15
|
var isFunctionsDefinition = (value) => {
|
|
16
16
|
if (!isRecord(value)) return false;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { taskName, taskNameProblem } from "../actions/tasks/registry.js";
|
|
2
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
2
3
|
//#region src/modules/functions/manifest.ts
|
|
3
4
|
var ROUTE_METHODS = [
|
|
4
5
|
"GET",
|
|
@@ -24,7 +25,6 @@ var parseRouteKey = (key) => {
|
|
|
24
25
|
};
|
|
25
26
|
};
|
|
26
27
|
var HOST = /^(\*\.)?([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
27
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
28
|
var stringOf = (value) => typeof value === "string" ? value : void 0;
|
|
29
29
|
var optionsOf = (value) => Array.isArray(value) ? value.flatMap((option) => {
|
|
30
30
|
const label = isRecord(option) ? stringOf(option.label) : void 0;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
1
2
|
//#region src/modules/functions/runner/messages.ts
|
|
2
3
|
var ENVIRONMENTS = {
|
|
3
4
|
production: true,
|
|
@@ -6,7 +7,6 @@ var ENVIRONMENTS = {
|
|
|
6
7
|
main: true
|
|
7
8
|
};
|
|
8
9
|
var isEnvironment = (value) => typeof value === "string" && Object.hasOwn(ENVIRONMENTS, value);
|
|
9
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
10
|
var parse = (data) => {
|
|
11
11
|
try {
|
|
12
12
|
const value = JSON.parse(data);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a host is one a picture may come from: named exactly (`images.example.com`), or under a wildcard
|
|
3
|
+
* (`*.example.com` — its subdomains, not `example.com` itself, which is named on its own when it is meant).
|
|
4
|
+
*/
|
|
5
|
+
export declare const isAllowedImageHost: (domains: readonly string[], hostname: string) => boolean;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/modules/images/allowlist.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether a host is one a picture may come from: named exactly (`images.example.com`), or under a wildcard
|
|
4
|
+
* (`*.example.com` — its subdomains, not `example.com` itself, which is named on its own when it is meant).
|
|
5
|
+
*/
|
|
6
|
+
var isAllowedImageHost = (domains, hostname) => {
|
|
7
|
+
const host = hostname.toLowerCase().replace(/\.$/, "");
|
|
8
|
+
return domains.some((entry) => {
|
|
9
|
+
const domain = entry.toLowerCase().trim();
|
|
10
|
+
if (domain.startsWith("*.")) {
|
|
11
|
+
const parent = domain.slice(2);
|
|
12
|
+
return host.endsWith(`.${parent}`);
|
|
13
|
+
}
|
|
14
|
+
return host === domain;
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { isAllowedImageHost };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ImageTransform } from './transform';
|
|
2
|
+
import { HostLookup } from '../../helpers/outboundGuard';
|
|
3
|
+
/** What a request for a picture asked: the picture, the width, and what the browser takes (its `Accept`). */
|
|
4
|
+
export type ImageRequest = {
|
|
5
|
+
url?: string;
|
|
6
|
+
width?: string;
|
|
7
|
+
accept?: string;
|
|
8
|
+
};
|
|
9
|
+
export type ImageAnswer = {
|
|
10
|
+
status: number;
|
|
11
|
+
headers: Record<string, string>;
|
|
12
|
+
body: Buffer | string;
|
|
13
|
+
};
|
|
14
|
+
export type ImageProxyOptions = {
|
|
15
|
+
domains: readonly string[];
|
|
16
|
+
cacheDir: string;
|
|
17
|
+
/** The transport every hop goes through. */
|
|
18
|
+
fetch: typeof fetch;
|
|
19
|
+
/** `sharp`'s, when it is installed; without one a picture is passed through, cached. */
|
|
20
|
+
transform?: ImageTransform;
|
|
21
|
+
lookup?: HostLookup;
|
|
22
|
+
now?: () => number;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Another site's picture, at a width this server makes, in the best format the browser takes.
|
|
26
|
+
*
|
|
27
|
+
* Each original is fetched once and kept, and each size of it is made once from the kept original and kept too: a page
|
|
28
|
+
* asking for eight widths in two formats costs one download and sixteen resizes, ever — not one of each per request,
|
|
29
|
+
* nor again a week later. Past a week the kept files still answer at once while the other site is asked, with the
|
|
30
|
+
* original's `ETag` / `Last-Modified`, whether it changed; only a changed original makes its sizes again.
|
|
31
|
+
*
|
|
32
|
+
* Only from the hosts the deployment allows, with every hop (redirects included) held to the allowlist and to the
|
|
33
|
+
* rule every outbound request is: a picture endpoint that fetched anything would be a way to make this server read
|
|
34
|
+
* its own network.
|
|
35
|
+
*/
|
|
36
|
+
export declare const createImageProxy: (options: ImageProxyOptions) => ((request: ImageRequest) => Promise<ImageAnswer>);
|