@anthusai/papyrus 5.17.0 → 5.19.0

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.
@@ -91,6 +91,22 @@ function markdownTwin(uri) {
91
91
  return uri.substring(0, uri.length - 5) + ".md";
92
92
  }
93
93
 
94
+ // Directory-style sites keep a page's twin at /x/index.md, so a bare /x.md is rewritten there. The edge cannot test for
95
+ // existence, hence the rule: a last segment <name>.md with a name other than index. A path prefix (a base path) is just
96
+ // part of <path>. The flag is spliced in by redirect-rules.ts at deploy time from urls.style.
97
+ const directoryTwins = /*PAPYRUS_DIRECTORY_TWINS*/false/*END_PAPYRUS_DIRECTORY_TWINS*/;
98
+
99
+ function directoryTwin(uri) {
100
+ if (uri.length < 5 || uri.substring(uri.length - 3) !== ".md") {
101
+ return null;
102
+ }
103
+ const lastSegment = uri.substring(uri.lastIndexOf("/") + 1);
104
+ if (lastSegment === "index.md" || lastSegment === ".md") {
105
+ return null;
106
+ }
107
+ return uri.substring(0, uri.length - 3) + "/index.md";
108
+ }
109
+
94
110
  async function handler(event) {
95
111
  const request = event.request;
96
112
  const redirectTarget = await lookUpRedirectTarget(request.uri);
@@ -110,6 +126,12 @@ async function handler(event) {
110
126
  };
111
127
  }
112
128
  request.uri = rewriteToIndexDocument(request.uri);
129
+ if (directoryTwins) {
130
+ const bareTwin = directoryTwin(request.uri);
131
+ if (bareTwin !== null) {
132
+ request.uri = bareTwin;
133
+ }
134
+ }
113
135
  if (negotiateMarkdown && prefersMarkdown(request)) {
114
136
  const twin = markdownTwin(request.uri);
115
137
  if (twin !== null) {
@@ -27,20 +27,34 @@ export function edgeRulesFromSettings(settings) {
27
27
  export function negotiatesMarkdown(settings) {
28
28
  return settings.site !== undefined && settings.agents?.markdown !== false;
29
29
  }
30
+ /** A directory-style site keeps a page's Markdown twin at `/x/index.md`; the edge then serves a bare `/x.md` from there. */
31
+ export function usesDirectoryTwins(settings) {
32
+ return negotiatesMarkdown(settings) && settings.urls?.style === "directory";
33
+ }
30
34
  export function readEdgeConfig(staticDir, baseDirectory = process.cwd()) {
31
35
  if (staticDir === undefined)
32
- return { rules: [], negotiateMarkdown: false };
36
+ return { rules: [], negotiateMarkdown: false, directoryTwins: false };
33
37
  const settingsPath = resolve(baseDirectory, staticDir, "projection.json");
34
38
  if (!existsSync(settingsPath))
35
- return { rules: [], negotiateMarkdown: false };
39
+ return { rules: [], negotiateMarkdown: false, directoryTwins: false };
36
40
  const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
37
- return { rules: edgeRulesFromSettings(settings), negotiateMarkdown: negotiatesMarkdown(settings) };
41
+ return { rules: edgeRulesFromSettings(settings), negotiateMarkdown: negotiatesMarkdown(settings), directoryTwins: usesDirectoryTwins(settings) };
38
42
  }
39
43
  export function readEdgeRules(staticDir, baseDirectory = process.cwd()) {
40
44
  return readEdgeConfig(staticDir, baseDirectory).rules;
41
45
  }
42
46
  const NEGOTIATE_START = "/*PAPYRUS_NEGOTIATE_MARKDOWN*/";
43
47
  const NEGOTIATE_END = "/*END_PAPYRUS_NEGOTIATE_MARKDOWN*/";
48
+ const TWINS_START = "/*PAPYRUS_DIRECTORY_TWINS*/";
49
+ const TWINS_END = "/*END_PAPYRUS_DIRECTORY_TWINS*/";
50
+ function spliceFlag(source, start, end, label) {
51
+ const from = source.indexOf(start);
52
+ const to = source.indexOf(end);
53
+ if (from === -1 || to === -1 || to < from) {
54
+ throw new Error(`redirect function source has no ${label} markers`);
55
+ }
56
+ return source.slice(0, from + start.length) + "true" + source.slice(to);
57
+ }
44
58
  /** The CloudFront Function source with the site's pattern rules and Markdown negotiation flag spliced in. */
45
59
  export function compileRedirectFunctionSource(source, rules, options = {}) {
46
60
  const start = source.indexOf(START_MARKER);
@@ -50,12 +64,10 @@ export function compileRedirectFunctionSource(source, rules, options = {}) {
50
64
  }
51
65
  let compiled = rules.length === 0 ? source : source.slice(0, start + START_MARKER.length) + JSON.stringify(rules) + source.slice(end);
52
66
  if (options.negotiateMarkdown === true) {
53
- const from = compiled.indexOf(NEGOTIATE_START);
54
- const to = compiled.indexOf(NEGOTIATE_END);
55
- if (from === -1 || to === -1 || to < from) {
56
- throw new Error("redirect function source has no markdown negotiation markers");
57
- }
58
- compiled = compiled.slice(0, from + NEGOTIATE_START.length) + "true" + compiled.slice(to);
67
+ compiled = spliceFlag(compiled, NEGOTIATE_START, NEGOTIATE_END, "markdown negotiation");
68
+ }
69
+ if (options.directoryTwins === true) {
70
+ compiled = spliceFlag(compiled, TWINS_START, TWINS_END, "directory twin");
59
71
  }
60
72
  return compiled;
61
73
  }
@@ -38,14 +38,19 @@ export function negotiatesMarkdown(settings: ProjectionSettings): boolean {
38
38
  return settings.site !== undefined && settings.agents?.markdown !== false;
39
39
  }
40
40
 
41
- export type EdgeConfig = { rules: EdgeRedirectRule[]; negotiateMarkdown: boolean };
41
+ /** A directory-style site keeps a page's Markdown twin at `/x/index.md`; the edge then serves a bare `/x.md` from there. */
42
+ export function usesDirectoryTwins(settings: ProjectionSettings): boolean {
43
+ return negotiatesMarkdown(settings) && settings.urls?.style === "directory";
44
+ }
45
+
46
+ export type EdgeConfig = { rules: EdgeRedirectRule[]; negotiateMarkdown: boolean; directoryTwins: boolean };
42
47
 
43
48
  export function readEdgeConfig(staticDir: string | undefined, baseDirectory: string = process.cwd()): EdgeConfig {
44
- if (staticDir === undefined) return { rules: [], negotiateMarkdown: false };
49
+ if (staticDir === undefined) return { rules: [], negotiateMarkdown: false, directoryTwins: false };
45
50
  const settingsPath = resolve(baseDirectory, staticDir, "projection.json");
46
- if (!existsSync(settingsPath)) return { rules: [], negotiateMarkdown: false };
51
+ if (!existsSync(settingsPath)) return { rules: [], negotiateMarkdown: false, directoryTwins: false };
47
52
  const settings = JSON.parse(readFileSync(settingsPath, "utf8")) as ProjectionSettings;
48
- return { rules: edgeRulesFromSettings(settings), negotiateMarkdown: negotiatesMarkdown(settings) };
53
+ return { rules: edgeRulesFromSettings(settings), negotiateMarkdown: negotiatesMarkdown(settings), directoryTwins: usesDirectoryTwins(settings) };
49
54
  }
50
55
 
51
56
  export function readEdgeRules(staticDir: string | undefined, baseDirectory: string = process.cwd()): EdgeRedirectRule[] {
@@ -54,12 +59,23 @@ export function readEdgeRules(staticDir: string | undefined, baseDirectory: stri
54
59
 
55
60
  const NEGOTIATE_START = "/*PAPYRUS_NEGOTIATE_MARKDOWN*/";
56
61
  const NEGOTIATE_END = "/*END_PAPYRUS_NEGOTIATE_MARKDOWN*/";
62
+ const TWINS_START = "/*PAPYRUS_DIRECTORY_TWINS*/";
63
+ const TWINS_END = "/*END_PAPYRUS_DIRECTORY_TWINS*/";
64
+
65
+ function spliceFlag(source: string, start: string, end: string, label: string): string {
66
+ const from = source.indexOf(start);
67
+ const to = source.indexOf(end);
68
+ if (from === -1 || to === -1 || to < from) {
69
+ throw new Error(`redirect function source has no ${label} markers`);
70
+ }
71
+ return source.slice(0, from + start.length) + "true" + source.slice(to);
72
+ }
57
73
 
58
74
  /** The CloudFront Function source with the site's pattern rules and Markdown negotiation flag spliced in. */
59
75
  export function compileRedirectFunctionSource(
60
76
  source: string,
61
77
  rules: EdgeRedirectRule[],
62
- options: { negotiateMarkdown?: boolean } = {},
78
+ options: { negotiateMarkdown?: boolean; directoryTwins?: boolean } = {},
63
79
  ): string {
64
80
  const start = source.indexOf(START_MARKER);
65
81
  const end = source.indexOf(END_MARKER);
@@ -68,12 +84,10 @@ export function compileRedirectFunctionSource(
68
84
  }
69
85
  let compiled = rules.length === 0 ? source : source.slice(0, start + START_MARKER.length) + JSON.stringify(rules) + source.slice(end);
70
86
  if (options.negotiateMarkdown === true) {
71
- const from = compiled.indexOf(NEGOTIATE_START);
72
- const to = compiled.indexOf(NEGOTIATE_END);
73
- if (from === -1 || to === -1 || to < from) {
74
- throw new Error("redirect function source has no markdown negotiation markers");
75
- }
76
- compiled = compiled.slice(0, from + NEGOTIATE_START.length) + "true" + compiled.slice(to);
87
+ compiled = spliceFlag(compiled, NEGOTIATE_START, NEGOTIATE_END, "markdown negotiation");
88
+ }
89
+ if (options.directoryTwins === true) {
90
+ compiled = spliceFlag(compiled, TWINS_START, TWINS_END, "directory twin");
77
91
  }
78
92
  return compiled;
79
93
  }
@@ -70,10 +70,12 @@ export function addS3ProjectionPublication(scope, props, media) {
70
70
  assertValidPublicationConfig({ kind: "s3-projection", ...props });
71
71
  const bucket = createSiteBucket(scope, props.retainBucketOnDelete !== false);
72
72
  const keyValueStore = new cloudfront.KeyValueStore(scope, "RedirectsKeyValueStore");
73
+ const edgeConfig = readEdgeConfig(props.staticDir);
73
74
  const redirectFunction = new cloudfront.Function(scope, "RedirectAndIndexFunction", {
74
75
  runtime: cloudfront.FunctionRuntime.JS_2_0,
75
- code: cloudfront.FunctionCode.fromInline(compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"), readEdgeConfig(props.staticDir).rules, {
76
- negotiateMarkdown: readEdgeConfig(props.staticDir).negotiateMarkdown,
76
+ code: cloudfront.FunctionCode.fromInline(compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"), edgeConfig.rules, {
77
+ negotiateMarkdown: edgeConfig.negotiateMarkdown,
78
+ directoryTwins: edgeConfig.directoryTwins,
77
79
  })),
78
80
  keyValueStore,
79
81
  });
@@ -112,11 +112,13 @@ export function addS3ProjectionPublication(
112
112
  const bucket = createSiteBucket(scope, props.retainBucketOnDelete !== false);
113
113
 
114
114
  const keyValueStore = new cloudfront.KeyValueStore(scope, "RedirectsKeyValueStore");
115
+ const edgeConfig = readEdgeConfig(props.staticDir);
115
116
  const redirectFunction = new cloudfront.Function(scope, "RedirectAndIndexFunction", {
116
117
  runtime: cloudfront.FunctionRuntime.JS_2_0,
117
118
  code: cloudfront.FunctionCode.fromInline(
118
- compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"), readEdgeConfig(props.staticDir).rules, {
119
- negotiateMarkdown: readEdgeConfig(props.staticDir).negotiateMarkdown,
119
+ compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"), edgeConfig.rules, {
120
+ negotiateMarkdown: edgeConfig.negotiateMarkdown,
121
+ directoryTwins: edgeConfig.directoryTwins,
120
122
  }),
121
123
  ),
122
124
  keyValueStore,
@@ -53,7 +53,7 @@ if (command === "compile-edge-function") {
53
53
  const { compileRedirectFunctionSource, readEdgeConfig } = await import("../amplify/publication/redirect-rules.js");
54
54
  const functionSource = fs.readFileSync(new URL("../amplify/publication/redirect-and-index.js", import.meta.url), "utf8");
55
55
  const config = readEdgeConfig(staticDir);
56
- const compiled = compileRedirectFunctionSource(functionSource, config.rules, { negotiateMarkdown: config.negotiateMarkdown });
56
+ const compiled = compileRedirectFunctionSource(functionSource, config.rules, { negotiateMarkdown: config.negotiateMarkdown, directoryTwins: config.directoryTwins });
57
57
  const out = opt("out");
58
58
  if (out) fs.writeFileSync(path.resolve(out), compiled);
59
59
  else process.stdout.write(compiled);
@@ -4,7 +4,7 @@ import * as amplify from "aws-cdk-lib/aws-amplify";
4
4
  import { SecureStringParameter } from "./secure-string-parameter.js";
5
5
  import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider.js";
6
6
  import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs.js";
7
- import { PROSPECTUS_PAPYRUS_SITE_ROUTES, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, githubOidcSubjects, resolveGithubRepository, resolveProspectusAccountsEndpoint, resolveProspectusSessionSealingParameterName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
7
+ import { PROSPECTUS_PAPYRUS_SITE_ROUTES, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveSiteOrigin, githubOidcSubjects, resolveGithubRepository, resolveProspectusAccountsEndpoint, resolveProspectusSessionSealingParameterName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
8
8
  function environmentVariables(variables) {
9
9
  return Object.entries(variables).map(([name, value]) => ({ name, value }));
10
10
  }
@@ -159,11 +159,14 @@ export class AmplifyAppShellStack extends Stack {
159
159
  ? `https://${stagingDomainName}/`
160
160
  : Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain, "/"]);
161
161
  // The compute adapter's request URL is https://localhost:3000, so the site names its own public origin.
162
- const productionSiteOrigin = cmsHostName ? `https://${cmsHostName}` : Fn.join("", ["https://main.", cmsApp.attrDefaultDomain]);
162
+ // A site served by its own edge (cms.siteOrigin) has no Amplify domain, so the edge host is its origin.
163
+ const siteOrigin = resolveSiteOrigin(config);
164
+ const productionSiteOrigin = cmsHostName ? `https://${cmsHostName}` : siteOrigin ?? Fn.join("", ["https://main.", cmsApp.attrDefaultDomain]);
163
165
  const stagingSiteOrigin = stagingDomainName ? `https://${stagingDomainName}` : Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]);
164
166
  const sharedRootDomain = config.cms.domainPrefix !== undefined;
165
167
  const defaultOriginsToAllow = [
166
168
  ...(config.cms.domainName ? [] : [productionOrigin]),
169
+ ...(siteOrigin ? [`${siteOrigin}/`] : []),
167
170
  ...(stagingEnabled && !stagingDomainName ? [stagingOrigin] : []),
168
171
  ];
169
172
  const oauthRedirectUrls = defaultOriginsToAllow.length === 0
@@ -5,7 +5,7 @@ import * as amplify from "aws-cdk-lib/aws-amplify";
5
5
  import { SecureStringParameter } from "./secure-string-parameter";
6
6
  import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider";
7
7
  import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs";
8
- import { AmplifyAppShellSiteConfig, PROSPECTUS_PAPYRUS_SITE_ROUTES, ReaderCustomHeaderRule, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, githubOidcSubjects, resolveGithubRepository, resolveProspectusAccountsEndpoint, resolveProspectusSessionSealingParameterName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
8
+ import { AmplifyAppShellSiteConfig, PROSPECTUS_PAPYRUS_SITE_ROUTES, ReaderCustomHeaderRule, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveSiteOrigin, githubOidcSubjects, resolveGithubRepository, resolveProspectusAccountsEndpoint, resolveProspectusSessionSealingParameterName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
9
9
 
10
10
  function environmentVariables(variables: Record<string, string>): amplify.CfnBranch.EnvironmentVariableProperty[] {
11
11
  return Object.entries(variables).map(([name, value]) => ({ name, value }));
@@ -178,11 +178,14 @@ export class AmplifyAppShellStack extends Stack {
178
178
  ? `https://${stagingDomainName}/`
179
179
  : Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain, "/"]);
180
180
  // The compute adapter's request URL is https://localhost:3000, so the site names its own public origin.
181
- const productionSiteOrigin = cmsHostName ? `https://${cmsHostName}` : Fn.join("", ["https://main.", cmsApp.attrDefaultDomain]);
181
+ // A site served by its own edge (cms.siteOrigin) has no Amplify domain, so the edge host is its origin.
182
+ const siteOrigin = resolveSiteOrigin(config);
183
+ const productionSiteOrigin = cmsHostName ? `https://${cmsHostName}` : siteOrigin ?? Fn.join("", ["https://main.", cmsApp.attrDefaultDomain]);
182
184
  const stagingSiteOrigin = stagingDomainName ? `https://${stagingDomainName}` : Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]);
183
185
  const sharedRootDomain = config.cms.domainPrefix !== undefined;
184
186
  const defaultOriginsToAllow = [
185
187
  ...(config.cms.domainName ? [] : [productionOrigin]),
188
+ ...(siteOrigin ? [`${siteOrigin}/`] : []),
186
189
  ...(stagingEnabled && !stagingDomainName ? [stagingOrigin] : []),
187
190
  ];
188
191
  const oauthRedirectUrls = defaultOriginsToAllow.length === 0
@@ -24,6 +24,7 @@ const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_
24
24
  const SECRETS_MANAGER_SECRET_ARN_PATTERN = /^arn:aws:secretsmanager:[a-z0-9-]+:\d{12}:secret:[A-Za-z0-9/_+=.@-]+$/;
25
25
  const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
26
26
  const HOST_NAME_PATTERN = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
27
+ const SITE_ORIGIN_PATTERN = /^https:\/\/([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}(:\d{1,5})?$/;
27
28
  const DOMAIN_PREFIX_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
28
29
  const APP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 _.-]*$/;
29
30
  const STACK_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,127}$/;
@@ -187,6 +188,9 @@ export function resolveCmsHostName(config) {
187
188
  return undefined;
188
189
  return config.cms.domainPrefix ? `${config.cms.domainPrefix}.${config.cms.domainName}` : config.cms.domainName;
189
190
  }
191
+ export function resolveSiteOrigin(config) {
192
+ return config.cms.siteOrigin;
193
+ }
190
194
  export function resolveStagingDomainName(config) {
191
195
  if (!isStagingEnabled(config))
192
196
  return undefined;
@@ -218,7 +222,7 @@ function parseStagingPreview(value) {
218
222
  }
219
223
  function parseCms(value) {
220
224
  const cmsRecord = requireObject(value, "cms");
221
- rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "deployStackInBuild"], "cms");
225
+ rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "siteOrigin", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "deployStackInBuild"], "cms");
222
226
  const buildComputeType = cmsRecord.buildComputeType;
223
227
  if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
224
228
  fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
@@ -236,6 +240,7 @@ function parseCms(value) {
236
240
  appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
237
241
  domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
238
242
  domainPrefix: optionalString(cmsRecord.domainPrefix, "cms.domainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
243
+ siteOrigin: optionalString(cmsRecord.siteOrigin, "cms.siteOrigin", SITE_ORIGIN_PATTERN, "https origin with no path, query or trailing slash, such as https://shipped.anth.us"),
239
244
  staging: cmsRecord.staging,
240
245
  cognitoDomainPrefix: requireString(cmsRecord.cognitoDomainPrefix, "cms.cognitoDomainPrefix", COGNITO_DOMAIN_PREFIX_PATTERN, "lowercase letters, digits and hyphens; globally unique per region"),
241
246
  applyCognitoDomainPrefix: cmsRecord.applyCognitoDomainPrefix,
@@ -257,6 +262,9 @@ function parseCms(value) {
257
262
  fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix, the revalidation secret parameter is derived from siteId and the Prospectus endpoint comes from cms.prospectusAccountsApiStageArn and the public site origin comes from the site host");
258
263
  }
259
264
  }
265
+ if (cms.siteOrigin && cms.domainName) {
266
+ fail("cms.siteOrigin", "is for sites whose host is served by their own edge; remove it when cms.domainName is set");
267
+ }
260
268
  if (cms.staging === false && cms.stagingDomainName) {
261
269
  fail("cms.stagingDomainName", "must not be set when cms.staging is false");
262
270
  }
@@ -32,6 +32,8 @@ export type CmsConfig = {
32
32
  appName?: string;
33
33
  domainName?: string;
34
34
  domainPrefix?: string;
35
+ /** Public https origin of a site whose host is served by its own edge (no Amplify domain association). */
36
+ siteOrigin?: string;
35
37
  staging?: boolean;
36
38
  stagingPreview?: StagingPreviewConfig;
37
39
  cognitoDomainPrefix: string;
@@ -133,6 +135,7 @@ const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_
133
135
  const SECRETS_MANAGER_SECRET_ARN_PATTERN = /^arn:aws:secretsmanager:[a-z0-9-]+:\d{12}:secret:[A-Za-z0-9/_+=.@-]+$/;
134
136
  const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
135
137
  const HOST_NAME_PATTERN = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
138
+ const SITE_ORIGIN_PATTERN = /^https:\/\/([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}(:\d{1,5})?$/;
136
139
  const DOMAIN_PREFIX_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
137
140
  const APP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 _.-]*$/;
138
141
  const STACK_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,127}$/;
@@ -298,6 +301,10 @@ export function resolveCmsHostName(config: Pick<CmsSiteConfig, "cms">): string |
298
301
  return config.cms.domainPrefix ? `${config.cms.domainPrefix}.${config.cms.domainName}` : config.cms.domainName;
299
302
  }
300
303
 
304
+ export function resolveSiteOrigin(config: Pick<CmsSiteConfig, "cms">): string | undefined {
305
+ return config.cms.siteOrigin;
306
+ }
307
+
301
308
  export function resolveStagingDomainName(config: Pick<CmsSiteConfig, "cms">): string | undefined {
302
309
  if (!isStagingEnabled(config)) return undefined;
303
310
  if (config.cms.domainPrefix) {
@@ -329,7 +336,7 @@ function parseStagingPreview(value: unknown): StagingPreviewConfig | undefined {
329
336
 
330
337
  function parseCms(value: unknown): CmsConfig {
331
338
  const cmsRecord = requireObject(value, "cms");
332
- rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "deployStackInBuild"], "cms");
339
+ rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "siteOrigin", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "deployStackInBuild"], "cms");
333
340
  const buildComputeType = cmsRecord.buildComputeType;
334
341
  if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
335
342
  fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
@@ -347,6 +354,7 @@ function parseCms(value: unknown): CmsConfig {
347
354
  appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
348
355
  domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
349
356
  domainPrefix: optionalString(cmsRecord.domainPrefix, "cms.domainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
357
+ siteOrigin: optionalString(cmsRecord.siteOrigin, "cms.siteOrigin", SITE_ORIGIN_PATTERN, "https origin with no path, query or trailing slash, such as https://shipped.anth.us"),
350
358
  staging: cmsRecord.staging as boolean | undefined,
351
359
  cognitoDomainPrefix: requireString(
352
360
  cmsRecord.cognitoDomainPrefix,
@@ -380,6 +388,9 @@ function parseCms(value: unknown): CmsConfig {
380
388
  }
381
389
  }
382
390
 
391
+ if (cms.siteOrigin && cms.domainName) {
392
+ fail("cms.siteOrigin", "is for sites whose host is served by their own edge; remove it when cms.domainName is set");
393
+ }
383
394
  if (cms.staging === false && cms.stagingDomainName) {
384
395
  fail("cms.stagingDomainName", "must not be set when cms.staging is false");
385
396
  }
package/package.json CHANGED
@@ -407,7 +407,7 @@
407
407
  "optional": true
408
408
  }
409
409
  },
410
- "version": "5.17.0",
410
+ "version": "5.19.0",
411
411
  "dependencies": {
412
412
  "@aws-amplify/adapter-nextjs": "^1.7.3",
413
413
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.17.0",
2
+ "version": "5.19.0",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",