@anthusai/papyrus 2.0.0-next.59 → 2.0.0-next.60
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/amplify/functions/content-actions/handler.py +8 -0
- package/amplify/plugins/newsletter/config.js +32 -3
- package/amplify/plugins/newsletter/config.ts +44 -3
- package/amplify/publication/projection-lease.js +30 -0
- package/amplify/publication/projection-lease.ts +39 -0
- package/amplify/site-backend.js +2 -0
- package/amplify/site-backend.ts +2 -0
- package/infra/site-config.js +2 -1
- package/infra/site-config.ts +7 -1
- package/package.json +1 -1
- package/routes.manifest.json +1 -1
|
@@ -8,6 +8,7 @@ from papyrus_content.edge_invalidation import distribution_ids_from_environment,
|
|
|
8
8
|
from papyrus_content.graphql_authoring import create_authoring_client
|
|
9
9
|
from papyrus_content.markus_renderer.derive import derive_body
|
|
10
10
|
from papyrus_content.projection.deploy_render import deploy_render, drift_metric_line
|
|
11
|
+
from papyrus_content.projection.lease import ProjectionLeaseHeld
|
|
11
12
|
from papyrus_content.projection.engine import project, project_all, project_item_pages
|
|
12
13
|
from papyrus_content.reader_steering.task import TASK_NAME as READER_SNAPSHOT_TASK, run_reader_snapshot
|
|
13
14
|
from papyrus_content.projection.runtime import (
|
|
@@ -186,6 +187,13 @@ def run_deploy_render_task(client) -> dict:
|
|
|
186
187
|
|
|
187
188
|
def run_projection_task(task: dict) -> dict:
|
|
188
189
|
"""An asynchronous projection task. Raising makes Lambda retry it, then send the event to the dead letter queue."""
|
|
190
|
+
try:
|
|
191
|
+
return _run_projection_task(task)
|
|
192
|
+
except ProjectionLeaseHeld as held: # another projection of this publication outlasted the wait: retry later
|
|
193
|
+
raise ProjectionTaskFailed(str(held)) from held
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _run_projection_task(task: dict) -> dict:
|
|
189
197
|
client, _claims = create_authoring_client()
|
|
190
198
|
if task.get("task") == "item-pages":
|
|
191
199
|
report = project_item_pages(projection_context(client), except_item_id=task.get("exceptItemId"))
|
|
@@ -11,6 +11,13 @@ const SCHEDULER_CRON = /^cron\(\S+ \S+ \S+ \S+ \S+ \S+\)$/;
|
|
|
11
11
|
const UNSAFE_CSS = /[<>{};"\\`]|url\(|expression\(|@import/i;
|
|
12
12
|
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
|
|
13
13
|
export const ITEM_URL_STYLES = ["html", "directory", "clean"];
|
|
14
|
+
const THEME_STYLE_KEYS = [
|
|
15
|
+
"surface", "muted", "fontFamily", "headerBand", "headerText", "buttonFill", "buttonText", "buttonBorder", "footerBand",
|
|
16
|
+
"footerText", "labelFont",
|
|
17
|
+
];
|
|
18
|
+
const THEME_KEYS = [
|
|
19
|
+
"accent", "text", "background", ...THEME_STYLE_KEYS, "logoUrl", "wordmarkImageUrl", "wordmarkAlt", "wordmarkWidth", "wordmarkHeight",
|
|
20
|
+
];
|
|
14
21
|
const KNOWN_KEYS = new Set([
|
|
15
22
|
"name", "siteId", "siteOrigin", "basePath", "computeRoleArn", "sendingDomain", "fromAddress", "fromName", "instantAlerts", "theme", "lists",
|
|
16
23
|
"configurationSet", "eventTopicName", "itemUrlStyle",
|
|
@@ -97,18 +104,18 @@ function itemUrlStyle(raw) {
|
|
|
97
104
|
return raw;
|
|
98
105
|
}
|
|
99
106
|
function resolveTheme(raw) {
|
|
100
|
-
const theme = record(raw, "theme",
|
|
107
|
+
const theme = record(raw, "theme", THEME_KEYS);
|
|
101
108
|
const resolved = {
|
|
102
109
|
accent: text(theme.accent, "theme.accent", 80),
|
|
103
110
|
text: text(theme.text, "theme.text", 80),
|
|
104
111
|
background: text(theme.background, "theme.background", 80),
|
|
105
112
|
};
|
|
106
|
-
for (const key of
|
|
113
|
+
for (const key of THEME_STYLE_KEYS) {
|
|
107
114
|
if (theme[key] !== undefined)
|
|
108
115
|
resolved[key] = text(theme[key], `theme.${key}`, 200);
|
|
109
116
|
}
|
|
110
117
|
for (const [key, value] of Object.entries(resolved)) {
|
|
111
|
-
if (UNSAFE_CSS.test(value))
|
|
118
|
+
if (typeof value === "string" && UNSAFE_CSS.test(value))
|
|
112
119
|
fail(`theme.${key} contains characters that are not allowed in a style value`);
|
|
113
120
|
}
|
|
114
121
|
if (theme.logoUrl !== undefined) {
|
|
@@ -117,6 +124,28 @@ function resolveTheme(raw) {
|
|
|
117
124
|
fail("theme.logoUrl must be an https URL");
|
|
118
125
|
resolved.logoUrl = logoUrl;
|
|
119
126
|
}
|
|
127
|
+
if (theme.wordmarkImageUrl !== undefined) {
|
|
128
|
+
const url = text(theme.wordmarkImageUrl, "theme.wordmarkImageUrl", 500);
|
|
129
|
+
if (!/^https:\/\/[^\s"'<>\\`]+$/.test(url))
|
|
130
|
+
fail("theme.wordmarkImageUrl must be an absolute https URL");
|
|
131
|
+
resolved.wordmarkImageUrl = url;
|
|
132
|
+
for (const key of ["wordmarkWidth", "wordmarkHeight"]) {
|
|
133
|
+
const value = theme[key];
|
|
134
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 2000) {
|
|
135
|
+
fail(`theme.${key} is required with theme.wordmarkImageUrl and must be a whole number of pixels from 1 to 2000`);
|
|
136
|
+
}
|
|
137
|
+
resolved[key] = value;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (theme.wordmarkWidth !== undefined || theme.wordmarkHeight !== undefined) {
|
|
141
|
+
fail("theme.wordmarkWidth and theme.wordmarkHeight need theme.wordmarkImageUrl");
|
|
142
|
+
}
|
|
143
|
+
if (theme.wordmarkAlt !== undefined) {
|
|
144
|
+
const alt = text(theme.wordmarkAlt, "theme.wordmarkAlt", 200);
|
|
145
|
+
if (/[\u0000-\u001f\u007f]/.test(alt))
|
|
146
|
+
fail("theme.wordmarkAlt must not contain control characters");
|
|
147
|
+
resolved.wordmarkAlt = alt;
|
|
148
|
+
}
|
|
120
149
|
return resolved;
|
|
121
150
|
}
|
|
122
151
|
function resolveRule(raw, label) {
|
|
@@ -24,8 +24,30 @@ export type NewsletterTheme = {
|
|
|
24
24
|
muted?: string;
|
|
25
25
|
fontFamily?: string;
|
|
26
26
|
logoUrl?: string;
|
|
27
|
+
/** Branding, all optional: a plain theme keeps the plain layout. Colours are CSS values; the border is 1.5px when set. */
|
|
28
|
+
headerBand?: string;
|
|
29
|
+
headerText?: string;
|
|
30
|
+
buttonFill?: string;
|
|
31
|
+
buttonText?: string;
|
|
32
|
+
buttonBorder?: string;
|
|
33
|
+
footerBand?: string;
|
|
34
|
+
footerText?: string;
|
|
35
|
+
/** An absolute https image, drawn with alt text and explicit pixel size (web fonts do not load in email). */
|
|
36
|
+
wordmarkImageUrl?: string;
|
|
37
|
+
wordmarkAlt?: string;
|
|
38
|
+
wordmarkWidth?: number;
|
|
39
|
+
wordmarkHeight?: number;
|
|
40
|
+
labelFont?: string;
|
|
27
41
|
};
|
|
28
42
|
|
|
43
|
+
const THEME_STYLE_KEYS = [
|
|
44
|
+
"surface", "muted", "fontFamily", "headerBand", "headerText", "buttonFill", "buttonText", "buttonBorder", "footerBand",
|
|
45
|
+
"footerText", "labelFont",
|
|
46
|
+
] as const;
|
|
47
|
+
const THEME_KEYS = [
|
|
48
|
+
"accent", "text", "background", ...THEME_STYLE_KEYS, "logoUrl", "wordmarkImageUrl", "wordmarkAlt", "wordmarkWidth", "wordmarkHeight",
|
|
49
|
+
];
|
|
50
|
+
|
|
29
51
|
export type NewsletterRule = { sections?: string[]; tags?: string[]; types?: string[] };
|
|
30
52
|
|
|
31
53
|
type ListBase = { id: string; name: string; description: string };
|
|
@@ -152,23 +174,42 @@ function itemUrlStyle(raw: unknown): ItemUrlStyle {
|
|
|
152
174
|
}
|
|
153
175
|
|
|
154
176
|
function resolveTheme(raw: unknown): NewsletterTheme {
|
|
155
|
-
const theme = record(raw, "theme",
|
|
177
|
+
const theme = record(raw, "theme", THEME_KEYS);
|
|
156
178
|
const resolved: NewsletterTheme = {
|
|
157
179
|
accent: text(theme.accent, "theme.accent", 80),
|
|
158
180
|
text: text(theme.text, "theme.text", 80),
|
|
159
181
|
background: text(theme.background, "theme.background", 80),
|
|
160
182
|
};
|
|
161
|
-
for (const key of
|
|
183
|
+
for (const key of THEME_STYLE_KEYS) {
|
|
162
184
|
if (theme[key] !== undefined) resolved[key] = text(theme[key], `theme.${key}`, 200);
|
|
163
185
|
}
|
|
164
186
|
for (const [key, value] of Object.entries(resolved)) {
|
|
165
|
-
if (UNSAFE_CSS.test(value)) fail(`theme.${key} contains characters that are not allowed in a style value`);
|
|
187
|
+
if (typeof value === "string" && UNSAFE_CSS.test(value)) fail(`theme.${key} contains characters that are not allowed in a style value`);
|
|
166
188
|
}
|
|
167
189
|
if (theme.logoUrl !== undefined) {
|
|
168
190
|
const logoUrl = text(theme.logoUrl, "theme.logoUrl", 500);
|
|
169
191
|
if (!/^https:\/\//.test(logoUrl)) fail("theme.logoUrl must be an https URL");
|
|
170
192
|
resolved.logoUrl = logoUrl;
|
|
171
193
|
}
|
|
194
|
+
if (theme.wordmarkImageUrl !== undefined) {
|
|
195
|
+
const url = text(theme.wordmarkImageUrl, "theme.wordmarkImageUrl", 500);
|
|
196
|
+
if (!/^https:\/\/[^\s"'<>\\`]+$/.test(url)) fail("theme.wordmarkImageUrl must be an absolute https URL");
|
|
197
|
+
resolved.wordmarkImageUrl = url;
|
|
198
|
+
for (const key of ["wordmarkWidth", "wordmarkHeight"] as const) {
|
|
199
|
+
const value = theme[key];
|
|
200
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 2000) {
|
|
201
|
+
fail(`theme.${key} is required with theme.wordmarkImageUrl and must be a whole number of pixels from 1 to 2000`);
|
|
202
|
+
}
|
|
203
|
+
resolved[key] = value;
|
|
204
|
+
}
|
|
205
|
+
} else if (theme.wordmarkWidth !== undefined || theme.wordmarkHeight !== undefined) {
|
|
206
|
+
fail("theme.wordmarkWidth and theme.wordmarkHeight need theme.wordmarkImageUrl");
|
|
207
|
+
}
|
|
208
|
+
if (theme.wordmarkAlt !== undefined) {
|
|
209
|
+
const alt = text(theme.wordmarkAlt, "theme.wordmarkAlt", 200);
|
|
210
|
+
if (/[\u0000-\u001f\u007f]/.test(alt)) fail("theme.wordmarkAlt must not contain control characters");
|
|
211
|
+
resolved.wordmarkAlt = alt;
|
|
212
|
+
}
|
|
172
213
|
return resolved;
|
|
173
214
|
}
|
|
174
215
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Stack } from "aws-cdk-lib";
|
|
2
|
+
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
3
|
+
/** The shared lock table's name (created by the cloud knowledge run stack). The Lambda finds it through this SSM parameter. */
|
|
4
|
+
export const PROJECTION_LOCK_TABLE_PARAMETER = "/papyrus/projection-lock/table-name";
|
|
5
|
+
export const PROJECTION_LOCK_TABLE_NAME = "papyrus-projection-locks";
|
|
6
|
+
export const PROJECTION_LOCK_TABLE_ACTIONS = ["dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:UpdateItem", "dynamodb:GetItem"];
|
|
7
|
+
export function projectionLeaseParameterArn(region, account) {
|
|
8
|
+
return `arn:aws:ssm:${region}:${account}:parameter${PROJECTION_LOCK_TABLE_PARAMETER}`;
|
|
9
|
+
}
|
|
10
|
+
export function projectionLeaseTableArn(region, account) {
|
|
11
|
+
return `arn:aws:dynamodb:${region}:${account}:table/${PROJECTION_LOCK_TABLE_NAME}*`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Let a projecting Lambda take and release the per-publication projection lease: read the table-name
|
|
15
|
+
* parameter and write items in the lock table, nothing broader. The table itself is not created here;
|
|
16
|
+
* a deployment without it keeps projecting without a lease.
|
|
17
|
+
*/
|
|
18
|
+
export function addProjectionLeaseAccess(lambda) {
|
|
19
|
+
const stack = Stack.of(lambda);
|
|
20
|
+
lambda.addToRolePolicy(new PolicyStatement({
|
|
21
|
+
effect: Effect.ALLOW,
|
|
22
|
+
actions: ["ssm:GetParameter"],
|
|
23
|
+
resources: [projectionLeaseParameterArn(stack.region, stack.account)],
|
|
24
|
+
}));
|
|
25
|
+
lambda.addToRolePolicy(new PolicyStatement({
|
|
26
|
+
effect: Effect.ALLOW,
|
|
27
|
+
actions: PROJECTION_LOCK_TABLE_ACTIONS,
|
|
28
|
+
resources: [projectionLeaseTableArn(stack.region, stack.account)],
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Stack } from "aws-cdk-lib";
|
|
2
|
+
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
3
|
+
import type { IFunction } from "aws-cdk-lib/aws-lambda";
|
|
4
|
+
|
|
5
|
+
/** The shared lock table's name (created by the cloud knowledge run stack). The Lambda finds it through this SSM parameter. */
|
|
6
|
+
export const PROJECTION_LOCK_TABLE_PARAMETER = "/papyrus/projection-lock/table-name";
|
|
7
|
+
export const PROJECTION_LOCK_TABLE_NAME = "papyrus-projection-locks";
|
|
8
|
+
export const PROJECTION_LOCK_TABLE_ACTIONS = ["dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:UpdateItem", "dynamodb:GetItem"];
|
|
9
|
+
|
|
10
|
+
export function projectionLeaseParameterArn(region: string, account: string): string {
|
|
11
|
+
return `arn:aws:ssm:${region}:${account}:parameter${PROJECTION_LOCK_TABLE_PARAMETER}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function projectionLeaseTableArn(region: string, account: string): string {
|
|
15
|
+
return `arn:aws:dynamodb:${region}:${account}:table/${PROJECTION_LOCK_TABLE_NAME}*`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Let a projecting Lambda take and release the per-publication projection lease: read the table-name
|
|
20
|
+
* parameter and write items in the lock table, nothing broader. The table itself is not created here;
|
|
21
|
+
* a deployment without it keeps projecting without a lease.
|
|
22
|
+
*/
|
|
23
|
+
export function addProjectionLeaseAccess(lambda: IFunction): void {
|
|
24
|
+
const stack = Stack.of(lambda);
|
|
25
|
+
lambda.addToRolePolicy(
|
|
26
|
+
new PolicyStatement({
|
|
27
|
+
effect: Effect.ALLOW,
|
|
28
|
+
actions: ["ssm:GetParameter"],
|
|
29
|
+
resources: [projectionLeaseParameterArn(stack.region, stack.account)],
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
lambda.addToRolePolicy(
|
|
33
|
+
new PolicyStatement({
|
|
34
|
+
effect: Effect.ALLOW,
|
|
35
|
+
actions: PROJECTION_LOCK_TABLE_ACTIONS,
|
|
36
|
+
resources: [projectionLeaseTableArn(stack.region, stack.account)],
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
}
|
package/amplify/site-backend.js
CHANGED
|
@@ -11,6 +11,7 @@ import { resolveSitePublication } from "./publication/default-publication.js";
|
|
|
11
11
|
import { REPROJECT_ON_DEPLOY_TIMEOUT_SECONDS, addReprojectOnDeploy, reprojectDeployToken } from "./publication/reproject-on-deploy.js";
|
|
12
12
|
import { addProjectionRetries, addS3ProjectionPublication } from "./publication/s3-projection.js";
|
|
13
13
|
import { planRenditions } from "./publication/renditions-settings.js";
|
|
14
|
+
import { addProjectionLeaseAccess } from "./publication/projection-lease.js";
|
|
14
15
|
import { contentActionsPublicationWiring, planS3ProjectionBackend } from "./publication/s3-projection-settings.js";
|
|
15
16
|
import { rebuildTriggerSettings } from "./rebuild-trigger-settings.js";
|
|
16
17
|
import { assertValidPublicationConfig } from "./site-backend-config.js";
|
|
@@ -360,6 +361,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
360
361
|
}
|
|
361
362
|
if (s3ProjectionPublication !== undefined) {
|
|
362
363
|
addProjectionRetries(contentActionsLambda, s3ProjectionPublication.deadLetterQueue);
|
|
364
|
+
addProjectionLeaseAccess(contentActionsLambda);
|
|
363
365
|
}
|
|
364
366
|
const publicationWiring = contentActionsPublicationWiring(s3ProjectionPublication);
|
|
365
367
|
for (const [name, value] of Object.entries(publicationWiring.environment)) {
|
package/amplify/site-backend.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { resolveSitePublication } from "./publication/default-publication";
|
|
|
14
14
|
import { REPROJECT_ON_DEPLOY_TIMEOUT_SECONDS, addReprojectOnDeploy, reprojectDeployToken } from "./publication/reproject-on-deploy";
|
|
15
15
|
import { addProjectionRetries, addS3ProjectionPublication, type S3ProjectionPublication } from "./publication/s3-projection";
|
|
16
16
|
import { planRenditions } from "./publication/renditions-settings";
|
|
17
|
+
import { addProjectionLeaseAccess } from "./publication/projection-lease";
|
|
17
18
|
import { contentActionsPublicationWiring, planS3ProjectionBackend } from "./publication/s3-projection-settings";
|
|
18
19
|
import { rebuildTriggerSettings } from "./rebuild-trigger-settings";
|
|
19
20
|
import { assertValidPublicationConfig } from "./site-backend-config";
|
|
@@ -456,6 +457,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
456
457
|
}
|
|
457
458
|
if (s3ProjectionPublication !== undefined) {
|
|
458
459
|
addProjectionRetries(contentActionsLambda, s3ProjectionPublication.deadLetterQueue);
|
|
460
|
+
addProjectionLeaseAccess(contentActionsLambda);
|
|
459
461
|
}
|
|
460
462
|
const publicationWiring = contentActionsPublicationWiring(s3ProjectionPublication);
|
|
461
463
|
for (const [name, value] of Object.entries(publicationWiring.environment)) {
|
package/infra/site-config.js
CHANGED
|
@@ -350,7 +350,7 @@ function parseAws(value) {
|
|
|
350
350
|
}
|
|
351
351
|
export function parseSiteConfig(raw) {
|
|
352
352
|
const record = requireObject(raw, "site");
|
|
353
|
-
rejectUnknownKeys(record, ["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "aws", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "autoDeploy", "github"], "site");
|
|
353
|
+
rejectUnknownKeys(record, ["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "aws", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "knowledgeRun", "autoDeploy", "github"], "site");
|
|
354
354
|
const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
|
|
355
355
|
const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
|
|
356
356
|
const repositoryAccessTokenSecretArn = record.repositoryAccessTokenSecretArn === undefined
|
|
@@ -465,6 +465,7 @@ export function parseSiteConfig(raw) {
|
|
|
465
465
|
storagePreviewPrefix,
|
|
466
466
|
autoDeploy: autoDeploy,
|
|
467
467
|
github,
|
|
468
|
+
...(record.knowledgeRun !== undefined ? { knowledgeRun: record.knowledgeRun } : {}),
|
|
468
469
|
};
|
|
469
470
|
if (hasCmsApp(config))
|
|
470
471
|
validateOauthRedirectUrls(config);
|
package/infra/site-config.ts
CHANGED
|
@@ -78,6 +78,11 @@ export type AmplifyAppShellSiteConfig = {
|
|
|
78
78
|
};
|
|
79
79
|
papyrusVersion: string;
|
|
80
80
|
storagePreviewPrefix?: string;
|
|
81
|
+
/**
|
|
82
|
+
* The scheduled cloud knowledge run for this publication. The app-shell stack ignores it; it is validated by
|
|
83
|
+
* infra/scheduled-jobs/lib/knowledge-run-config.ts, which owns the schema.
|
|
84
|
+
*/
|
|
85
|
+
knowledgeRun?: unknown;
|
|
81
86
|
/**
|
|
82
87
|
* Merge-to-main deploy from GitHub Actions through OIDC. When true (the default) the stack creates the
|
|
83
88
|
* `<siteId>-github-deploy` role and turns off Amplify's own auto-build on the production branches, so the
|
|
@@ -485,7 +490,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
485
490
|
const record = requireObject(raw, "site");
|
|
486
491
|
rejectUnknownKeys(
|
|
487
492
|
record,
|
|
488
|
-
["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "aws", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "autoDeploy", "github"],
|
|
493
|
+
["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "aws", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "knowledgeRun", "autoDeploy", "github"],
|
|
489
494
|
"site",
|
|
490
495
|
);
|
|
491
496
|
|
|
@@ -600,6 +605,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
600
605
|
storagePreviewPrefix,
|
|
601
606
|
autoDeploy: autoDeploy as boolean,
|
|
602
607
|
github,
|
|
608
|
+
...(record.knowledgeRun !== undefined ? { knowledgeRun: record.knowledgeRun } : {}),
|
|
603
609
|
};
|
|
604
610
|
|
|
605
611
|
if (hasCmsApp(config)) validateOauthRedirectUrls(config);
|
package/package.json
CHANGED
package/routes.manifest.json
CHANGED