@anthusai/papyrus 1.0.0-next.33 → 1.0.0-next.35
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/auth/redirect-urls.js +54 -0
- package/amplify/auth/redirect-urls.ts +66 -0
- package/amplify/auth/resource.js +3 -1
- package/amplify/auth/resource.ts +6 -1
- package/amplify/data/schema.js +5 -0
- package/amplify/data/schema.ts +5 -0
- package/amplify/site-backend-config.ts +6 -0
- package/amplify/site-backend-names.js +28 -13
- package/amplify/site-backend-names.ts +36 -17
- package/amplify/site-backend.js +7 -2
- package/amplify/site-backend.ts +6 -2
- package/components/amplify-client-provider.tsx +4 -22
- package/infra/site-config.js +14 -5
- package/infra/site-config.ts +14 -5
- package/lib/oauth-redirect-priority.ts +40 -0
- package/package.json +2 -1
- package/routes.manifest.json +1 -1
- package/src/with-papyrus.mjs +11 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const DEFAULT_NEWSROOM_BASE_PATH = "/newsroom";
|
|
2
|
+
function parseRedirectUrl(value) {
|
|
3
|
+
try {
|
|
4
|
+
const parsed = new URL(value);
|
|
5
|
+
return { original: value, origin: parsed.origin, path: parsed.pathname.replace(/\/+$/, "") };
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Expands every origin in an OAuth redirect list to both the newsroom return
|
|
13
|
+
* path and the site root, newsroom first. Amplify signs in with the first
|
|
14
|
+
* entry that matches the current origin, so a list that names only
|
|
15
|
+
* `https://host/` would land a signed-in editor on the public reader.
|
|
16
|
+
*
|
|
17
|
+
* With `newsroomBasePath` of `""` (a CMS-only host that mounts the newsroom at
|
|
18
|
+
* `/`), the root already is the newsroom and nothing is added. Explicit
|
|
19
|
+
* entries are kept; unparseable entries pass through unchanged.
|
|
20
|
+
*/
|
|
21
|
+
export function expandNewsroomRedirectUrls(redirectUrls, newsroomBasePath = DEFAULT_NEWSROOM_BASE_PATH) {
|
|
22
|
+
const newsroomPath = newsroomBasePath.replace(/\/+$/, "");
|
|
23
|
+
const originOrder = [];
|
|
24
|
+
const explicitByOrigin = new Map();
|
|
25
|
+
const passthrough = [];
|
|
26
|
+
for (const value of redirectUrls) {
|
|
27
|
+
const parsed = parseRedirectUrl(value);
|
|
28
|
+
if (!parsed) {
|
|
29
|
+
passthrough.push(value);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!explicitByOrigin.has(parsed.origin)) {
|
|
33
|
+
originOrder.push(parsed.origin);
|
|
34
|
+
explicitByOrigin.set(parsed.origin, []);
|
|
35
|
+
}
|
|
36
|
+
explicitByOrigin.get(parsed.origin)?.push(value);
|
|
37
|
+
}
|
|
38
|
+
const expanded = [];
|
|
39
|
+
for (const origin of originOrder) {
|
|
40
|
+
const explicit = explicitByOrigin.get(origin) ?? [];
|
|
41
|
+
if (newsroomPath === "") {
|
|
42
|
+
expanded.push(...explicit);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const newsroomEntries = explicit.filter((value) => parseRedirectUrl(value)?.path === newsroomPath);
|
|
46
|
+
const otherEntries = explicit.filter((value) => {
|
|
47
|
+
const path = parseRedirectUrl(value)?.path;
|
|
48
|
+
return path !== newsroomPath && path !== "";
|
|
49
|
+
});
|
|
50
|
+
expanded.push(...(newsroomEntries.length > 0 ? newsroomEntries : [`${origin}${newsroomPath}`]), `${origin}/`, ...otherEntries);
|
|
51
|
+
}
|
|
52
|
+
expanded.push(...passthrough);
|
|
53
|
+
return [...new Set(expanded)];
|
|
54
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const DEFAULT_NEWSROOM_BASE_PATH = "/newsroom";
|
|
2
|
+
|
|
3
|
+
type ParsedRedirectUrl = { original: string; origin: string; path: string } | null;
|
|
4
|
+
|
|
5
|
+
function parseRedirectUrl(value: string): ParsedRedirectUrl {
|
|
6
|
+
try {
|
|
7
|
+
const parsed = new URL(value);
|
|
8
|
+
return { original: value, origin: parsed.origin, path: parsed.pathname.replace(/\/+$/, "") };
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Expands every origin in an OAuth redirect list to both the newsroom return
|
|
16
|
+
* path and the site root, newsroom first. Amplify signs in with the first
|
|
17
|
+
* entry that matches the current origin, so a list that names only
|
|
18
|
+
* `https://host/` would land a signed-in editor on the public reader.
|
|
19
|
+
*
|
|
20
|
+
* With `newsroomBasePath` of `""` (a CMS-only host that mounts the newsroom at
|
|
21
|
+
* `/`), the root already is the newsroom and nothing is added. Explicit
|
|
22
|
+
* entries are kept; unparseable entries pass through unchanged.
|
|
23
|
+
*/
|
|
24
|
+
export function expandNewsroomRedirectUrls(
|
|
25
|
+
redirectUrls: readonly string[],
|
|
26
|
+
newsroomBasePath: string = DEFAULT_NEWSROOM_BASE_PATH,
|
|
27
|
+
): string[] {
|
|
28
|
+
const newsroomPath = newsroomBasePath.replace(/\/+$/, "");
|
|
29
|
+
const originOrder: string[] = [];
|
|
30
|
+
const explicitByOrigin = new Map<string, string[]>();
|
|
31
|
+
const passthrough: string[] = [];
|
|
32
|
+
|
|
33
|
+
for (const value of redirectUrls) {
|
|
34
|
+
const parsed = parseRedirectUrl(value);
|
|
35
|
+
if (!parsed) {
|
|
36
|
+
passthrough.push(value);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!explicitByOrigin.has(parsed.origin)) {
|
|
40
|
+
originOrder.push(parsed.origin);
|
|
41
|
+
explicitByOrigin.set(parsed.origin, []);
|
|
42
|
+
}
|
|
43
|
+
explicitByOrigin.get(parsed.origin)?.push(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const expanded: string[] = [];
|
|
47
|
+
for (const origin of originOrder) {
|
|
48
|
+
const explicit = explicitByOrigin.get(origin) ?? [];
|
|
49
|
+
if (newsroomPath === "") {
|
|
50
|
+
expanded.push(...explicit);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const newsroomEntries = explicit.filter((value) => parseRedirectUrl(value)?.path === newsroomPath);
|
|
54
|
+
const otherEntries = explicit.filter((value) => {
|
|
55
|
+
const path = parseRedirectUrl(value)?.path;
|
|
56
|
+
return path !== newsroomPath && path !== "";
|
|
57
|
+
});
|
|
58
|
+
expanded.push(
|
|
59
|
+
...(newsroomEntries.length > 0 ? newsroomEntries : [`${origin}${newsroomPath}`]),
|
|
60
|
+
`${origin}/`,
|
|
61
|
+
...otherEntries,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
expanded.push(...passthrough);
|
|
65
|
+
return [...new Set(expanded)];
|
|
66
|
+
}
|
package/amplify/auth/resource.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { defineAuth, secret } from "@aws-amplify/backend";
|
|
2
2
|
import { manageUserRole } from "../functions/manage-user-role/resource.js";
|
|
3
3
|
import { CfnUserPoolDomain } from "aws-cdk-lib/aws-cognito";
|
|
4
|
+
import { expandNewsroomRedirectUrls } from "./redirect-urls.js";
|
|
4
5
|
// Default OAuth redirect URLs for the p.apyr.us app. Other publications pass
|
|
5
6
|
// `auth.redirectUrls` to `defineSiteBackend` (or, for Papyrus's own backend,
|
|
6
7
|
// the PAPYRUS_OAUTH_REDIRECT_URLS branch env var, comma-separated).
|
|
8
|
+
// `defineSiteAuth` adds the newsroom and root variants of every origin listed.
|
|
7
9
|
// See docs/google-oauth-setup.md.
|
|
8
10
|
const DEFAULT_AUTH_REDIRECT_URLS = [
|
|
9
11
|
"http://localhost:3001/",
|
|
@@ -31,7 +33,7 @@ export function authConfigFromEnv() {
|
|
|
31
33
|
};
|
|
32
34
|
}
|
|
33
35
|
export function defineSiteAuth(config) {
|
|
34
|
-
const authRedirectUrls = config.redirectUrls ?? DEFAULT_AUTH_REDIRECT_URLS;
|
|
36
|
+
const authRedirectUrls = expandNewsroomRedirectUrls(config.redirectUrls ?? DEFAULT_AUTH_REDIRECT_URLS, config.newsroomBasePath);
|
|
35
37
|
const disableGoogleOAuth = config.disableGoogleOAuth === true;
|
|
36
38
|
return defineAuth({
|
|
37
39
|
loginWith: {
|
package/amplify/auth/resource.ts
CHANGED
|
@@ -3,10 +3,12 @@ import { manageUserRole } from "../functions/manage-user-role/resource";
|
|
|
3
3
|
import { Stack } from "aws-cdk-lib";
|
|
4
4
|
import { CfnUserPoolDomain } from "aws-cdk-lib/aws-cognito";
|
|
5
5
|
import type { PapyrusAuthConfig } from "../site-backend-config";
|
|
6
|
+
import { expandNewsroomRedirectUrls } from "./redirect-urls";
|
|
6
7
|
|
|
7
8
|
// Default OAuth redirect URLs for the p.apyr.us app. Other publications pass
|
|
8
9
|
// `auth.redirectUrls` to `defineSiteBackend` (or, for Papyrus's own backend,
|
|
9
10
|
// the PAPYRUS_OAUTH_REDIRECT_URLS branch env var, comma-separated).
|
|
11
|
+
// `defineSiteAuth` adds the newsroom and root variants of every origin listed.
|
|
10
12
|
// See docs/google-oauth-setup.md.
|
|
11
13
|
const DEFAULT_AUTH_REDIRECT_URLS = [
|
|
12
14
|
"http://localhost:3001/",
|
|
@@ -36,7 +38,10 @@ export function authConfigFromEnv(): PapyrusAuthConfig {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export function defineSiteAuth(config: PapyrusAuthConfig) {
|
|
39
|
-
const authRedirectUrls =
|
|
41
|
+
const authRedirectUrls = expandNewsroomRedirectUrls(
|
|
42
|
+
config.redirectUrls ?? DEFAULT_AUTH_REDIRECT_URLS,
|
|
43
|
+
config.newsroomBasePath,
|
|
44
|
+
);
|
|
40
45
|
const disableGoogleOAuth = config.disableGoogleOAuth === true;
|
|
41
46
|
return defineAuth({
|
|
42
47
|
loginWith: {
|
package/amplify/data/schema.js
CHANGED
|
@@ -1290,6 +1290,11 @@ export const schema = a.schema({
|
|
|
1290
1290
|
displayName: a.string(),
|
|
1291
1291
|
description: a.string(),
|
|
1292
1292
|
aliases: a.string().array(),
|
|
1293
|
+
authorityScore: a.float(),
|
|
1294
|
+
authorityRank: a.integer(),
|
|
1295
|
+
acceptedReferenceMentionCount: a.integer(),
|
|
1296
|
+
distinctSourceKindCount: a.integer(),
|
|
1297
|
+
relationCount: a.integer(),
|
|
1293
1298
|
status: a.string().required(),
|
|
1294
1299
|
importRunId: a.id(),
|
|
1295
1300
|
createdAt: a.datetime(),
|
package/amplify/data/schema.ts
CHANGED
|
@@ -1379,6 +1379,11 @@ export const schema = a.schema({
|
|
|
1379
1379
|
displayName: a.string(),
|
|
1380
1380
|
description: a.string(),
|
|
1381
1381
|
aliases: a.string().array(),
|
|
1382
|
+
authorityScore: a.float(),
|
|
1383
|
+
authorityRank: a.integer(),
|
|
1384
|
+
acceptedReferenceMentionCount: a.integer(),
|
|
1385
|
+
distinctSourceKindCount: a.integer(),
|
|
1386
|
+
relationCount: a.integer(),
|
|
1382
1387
|
status: a.string().required(),
|
|
1383
1388
|
importRunId: a.id(),
|
|
1384
1389
|
createdAt: a.datetime(),
|
|
@@ -7,6 +7,12 @@ export type PapyrusAuthConfig = {
|
|
|
7
7
|
/** OAuth callback/logout URLs for Google sign-in (ignored when Google is disabled). */
|
|
8
8
|
redirectUrls?: string[];
|
|
9
9
|
disableGoogleOAuth?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Public newsroom path prefix for sign-in return URLs. Default `/newsroom`;
|
|
12
|
+
* `""` on a CMS-only host where the newsroom is the root. Filled from the
|
|
13
|
+
* active brand's `newsroomBasePath` by `defineSiteBackend` when unset.
|
|
14
|
+
*/
|
|
15
|
+
newsroomBasePath?: string;
|
|
10
16
|
/** Desired Cognito hosted-UI domain prefix. Only applied when `applyCognitoDomainPrefix` is true. */
|
|
11
17
|
cognitoDomainPrefix?: string;
|
|
12
18
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { Token } from "aws-cdk-lib";
|
|
2
3
|
export function sanitizeAwsName(value, maxLength = 50) {
|
|
3
4
|
return value
|
|
4
5
|
.toLowerCase()
|
|
@@ -7,6 +8,18 @@ export function sanitizeAwsName(value, maxLength = 50) {
|
|
|
7
8
|
.replace(/^-|-$/g, "")
|
|
8
9
|
.slice(0, maxLength);
|
|
9
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Account-global names are immutable in AWS, so they must be built from literal
|
|
13
|
+
* synth-time inputs only. A CDK token (or text that came from one, such as
|
|
14
|
+
* `token-token-17565`) makes the name change on every synth and fail with
|
|
15
|
+
* AlreadyExists, so this throws instead.
|
|
16
|
+
*/
|
|
17
|
+
export function assertLiteralResourceName(label, name) {
|
|
18
|
+
if (Token.isUnresolved(name) || /token|\$\{/i.test(name)) {
|
|
19
|
+
throw new Error(`${label} "${name}" is derived from an unresolved CDK token. Build account-global names from literal site config values only.`);
|
|
20
|
+
}
|
|
21
|
+
return name;
|
|
22
|
+
}
|
|
10
23
|
export function readBooleanEnvFlag(environment, name, defaultValue) {
|
|
11
24
|
const raw = (environment[name] ?? "").trim().toLowerCase();
|
|
12
25
|
if (["1", "true", "yes", "on"].includes(raw))
|
|
@@ -36,19 +49,21 @@ export function resolveSiteBackendFeatureFlags(features, environment, identity)
|
|
|
36
49
|
};
|
|
37
50
|
}
|
|
38
51
|
export function deriveKnowledgeVectorIndexName(identity) {
|
|
39
|
-
|
|
52
|
+
const name = isLegacyProductionApp(identity)
|
|
40
53
|
? "papyrus-knowledge"
|
|
41
54
|
: `papyrus-knowledge-${sanitizeAwsName(identity.brandId, 40)}`;
|
|
55
|
+
return assertLiteralResourceName("Knowledge vector index name", name);
|
|
42
56
|
}
|
|
43
|
-
export function deriveStorageBackupVaultName(identity,
|
|
57
|
+
export function deriveStorageBackupVaultName(identity, explicitName) {
|
|
44
58
|
const explicit = (explicitName ?? "").trim();
|
|
45
59
|
if (explicit !== "")
|
|
46
|
-
return sanitizeAwsName(explicit);
|
|
60
|
+
return assertLiteralResourceName("Backup vault name", sanitizeAwsName(explicit));
|
|
47
61
|
if (isLegacyProductionPipeline(identity)) {
|
|
48
|
-
return sanitizeAwsName(`papyrus-${identity.productionAppId}-main-media-backup-vault`);
|
|
62
|
+
return assertLiteralResourceName("Backup vault name", sanitizeAwsName(`papyrus-${identity.productionAppId}-main-media-backup-vault`));
|
|
49
63
|
}
|
|
50
|
-
const
|
|
51
|
-
|
|
64
|
+
const literalSiteKey = [identity.brandId, identity.amplifyAppId, identity.amplifyBranch].join("|");
|
|
65
|
+
const siteHash = createHash("sha256").update(literalSiteKey).digest("hex").slice(0, 8);
|
|
66
|
+
return assertLiteralResourceName("Backup vault name", `papyrus-${sanitizeAwsName(identity.brandId, 20)}-${siteHash}-media-vault`);
|
|
52
67
|
}
|
|
53
68
|
/**
|
|
54
69
|
* The `papyrus-site-inbound-` prefix keeps a brand named `p-apyr-us` from
|
|
@@ -57,16 +72,16 @@ export function deriveStorageBackupVaultName(identity, backupsStackName, explici
|
|
|
57
72
|
* its own brand-named rule set and never activates it from CloudFormation.
|
|
58
73
|
*/
|
|
59
74
|
export function deriveReceiptRuleSetName(identity, inboundEmailDomain) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return
|
|
75
|
+
const name = isLegacyProductionApp(identity)
|
|
76
|
+
? `papyrus-inbound-${inboundEmailDomain.replace(/\./g, "-")}`
|
|
77
|
+
: `papyrus-site-inbound-${sanitizeAwsName(identity.brandId, 40)}`;
|
|
78
|
+
return assertLiteralResourceName("Receipt rule set name", name);
|
|
64
79
|
}
|
|
65
80
|
/** `undefined` keeps the legacy auto-generated rule name of the live p.apyr.us rule. */
|
|
66
81
|
export function deriveReceiptRuleName(identity) {
|
|
67
82
|
if (isLegacyProductionApp(identity))
|
|
68
83
|
return undefined;
|
|
69
|
-
return `${sanitizeAwsName(identity.brandId, 40)}-inbound-submissions
|
|
84
|
+
return assertLiteralResourceName("Receipt rule name", `${sanitizeAwsName(identity.brandId, 40)}-inbound-submissions`);
|
|
70
85
|
}
|
|
71
86
|
export function planInboundEmailSes(identity, inboundEmailEnabled, options) {
|
|
72
87
|
const legacyPipeline = isLegacyProductionPipeline(identity);
|
|
@@ -78,10 +93,10 @@ export function planInboundEmailSes(identity, inboundEmailEnabled, options) {
|
|
|
78
93
|
manageDomainIdentity: createReceiptRules && (legacyPipeline || options?.manageDomainIdentity === true),
|
|
79
94
|
};
|
|
80
95
|
}
|
|
81
|
-
export function deriveSiteGlobalResourceNames(identity, inboundEmailDomain
|
|
96
|
+
export function deriveSiteGlobalResourceNames(identity, inboundEmailDomain) {
|
|
82
97
|
return {
|
|
83
98
|
knowledgeVectorIndexName: deriveKnowledgeVectorIndexName(identity),
|
|
84
|
-
storageBackupVaultName: deriveStorageBackupVaultName(identity
|
|
99
|
+
storageBackupVaultName: deriveStorageBackupVaultName(identity),
|
|
85
100
|
receiptRuleSetName: deriveReceiptRuleSetName(identity, inboundEmailDomain),
|
|
86
101
|
receiptRuleName: deriveReceiptRuleName(identity),
|
|
87
102
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { Token } from "aws-cdk-lib";
|
|
2
3
|
|
|
3
4
|
export type SiteBackendFeatureFlags = {
|
|
4
5
|
consoleResponder: boolean;
|
|
@@ -18,6 +19,21 @@ export function sanitizeAwsName(value: string, maxLength = 50): string {
|
|
|
18
19
|
.slice(0, maxLength);
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Account-global names are immutable in AWS, so they must be built from literal
|
|
24
|
+
* synth-time inputs only. A CDK token (or text that came from one, such as
|
|
25
|
+
* `token-token-17565`) makes the name change on every synth and fail with
|
|
26
|
+
* AlreadyExists, so this throws instead.
|
|
27
|
+
*/
|
|
28
|
+
export function assertLiteralResourceName(label: string, name: string): string {
|
|
29
|
+
if (Token.isUnresolved(name) || /token|\$\{/i.test(name)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`${label} "${name}" is derived from an unresolved CDK token. Build account-global names from literal site config values only.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return name;
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
export function readBooleanEnvFlag(
|
|
22
38
|
environment: Record<string, string | undefined>,
|
|
23
39
|
name: string,
|
|
@@ -67,23 +83,27 @@ export function resolveSiteBackendFeatureFlags(
|
|
|
67
83
|
}
|
|
68
84
|
|
|
69
85
|
export function deriveKnowledgeVectorIndexName(identity: SiteBackendIdentity): string {
|
|
70
|
-
|
|
86
|
+
const name = isLegacyProductionApp(identity)
|
|
71
87
|
? "papyrus-knowledge"
|
|
72
88
|
: `papyrus-knowledge-${sanitizeAwsName(identity.brandId, 40)}`;
|
|
89
|
+
return assertLiteralResourceName("Knowledge vector index name", name);
|
|
73
90
|
}
|
|
74
91
|
|
|
75
|
-
export function deriveStorageBackupVaultName(
|
|
76
|
-
identity: SiteBackendIdentity,
|
|
77
|
-
backupsStackName: string,
|
|
78
|
-
explicitName?: string,
|
|
79
|
-
): string {
|
|
92
|
+
export function deriveStorageBackupVaultName(identity: SiteBackendIdentity, explicitName?: string): string {
|
|
80
93
|
const explicit = (explicitName ?? "").trim();
|
|
81
|
-
if (explicit !== "") return sanitizeAwsName(explicit);
|
|
94
|
+
if (explicit !== "") return assertLiteralResourceName("Backup vault name", sanitizeAwsName(explicit));
|
|
82
95
|
if (isLegacyProductionPipeline(identity)) {
|
|
83
|
-
return
|
|
96
|
+
return assertLiteralResourceName(
|
|
97
|
+
"Backup vault name",
|
|
98
|
+
sanitizeAwsName(`papyrus-${identity.productionAppId}-main-media-backup-vault`),
|
|
99
|
+
);
|
|
84
100
|
}
|
|
85
|
-
const
|
|
86
|
-
|
|
101
|
+
const literalSiteKey = [identity.brandId, identity.amplifyAppId, identity.amplifyBranch].join("|");
|
|
102
|
+
const siteHash = createHash("sha256").update(literalSiteKey).digest("hex").slice(0, 8);
|
|
103
|
+
return assertLiteralResourceName(
|
|
104
|
+
"Backup vault name",
|
|
105
|
+
`papyrus-${sanitizeAwsName(identity.brandId, 20)}-${siteHash}-media-vault`,
|
|
106
|
+
);
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
/**
|
|
@@ -93,16 +113,16 @@ export function deriveStorageBackupVaultName(
|
|
|
93
113
|
* its own brand-named rule set and never activates it from CloudFormation.
|
|
94
114
|
*/
|
|
95
115
|
export function deriveReceiptRuleSetName(identity: SiteBackendIdentity, inboundEmailDomain: string): string {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return
|
|
116
|
+
const name = isLegacyProductionApp(identity)
|
|
117
|
+
? `papyrus-inbound-${inboundEmailDomain.replace(/\./g, "-")}`
|
|
118
|
+
: `papyrus-site-inbound-${sanitizeAwsName(identity.brandId, 40)}`;
|
|
119
|
+
return assertLiteralResourceName("Receipt rule set name", name);
|
|
100
120
|
}
|
|
101
121
|
|
|
102
122
|
/** `undefined` keeps the legacy auto-generated rule name of the live p.apyr.us rule. */
|
|
103
123
|
export function deriveReceiptRuleName(identity: SiteBackendIdentity): string | undefined {
|
|
104
124
|
if (isLegacyProductionApp(identity)) return undefined;
|
|
105
|
-
return `${sanitizeAwsName(identity.brandId, 40)}-inbound-submissions
|
|
125
|
+
return assertLiteralResourceName("Receipt rule name", `${sanitizeAwsName(identity.brandId, 40)}-inbound-submissions`);
|
|
106
126
|
}
|
|
107
127
|
|
|
108
128
|
export type InboundEmailSesPlan = {
|
|
@@ -136,11 +156,10 @@ export type SiteGlobalResourceNames = {
|
|
|
136
156
|
export function deriveSiteGlobalResourceNames(
|
|
137
157
|
identity: SiteBackendIdentity,
|
|
138
158
|
inboundEmailDomain: string,
|
|
139
|
-
backupsStackName: string,
|
|
140
159
|
): SiteGlobalResourceNames {
|
|
141
160
|
return {
|
|
142
161
|
knowledgeVectorIndexName: deriveKnowledgeVectorIndexName(identity),
|
|
143
|
-
storageBackupVaultName: deriveStorageBackupVaultName(identity
|
|
162
|
+
storageBackupVaultName: deriveStorageBackupVaultName(identity),
|
|
144
163
|
receiptRuleSetName: deriveReceiptRuleSetName(identity, inboundEmailDomain),
|
|
145
164
|
receiptRuleName: deriveReceiptRuleName(identity),
|
|
146
165
|
};
|
package/amplify/site-backend.js
CHANGED
|
@@ -54,7 +54,12 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
54
54
|
const enableInboundEmail = featureFlags.inboundEmail;
|
|
55
55
|
const enableSlackAgent = featureFlags.slack;
|
|
56
56
|
const enableStorageBackups = featureFlags.storageBackups;
|
|
57
|
-
const
|
|
57
|
+
const baseAuthConfig = site.auth ?? authConfigFromEnv();
|
|
58
|
+
const activeBrand = (papyrusSite.brands ?? []).find((brand) => brand.id.toLowerCase() === siteBackendIdentity.brandId);
|
|
59
|
+
const authConfig = {
|
|
60
|
+
...baseAuthConfig,
|
|
61
|
+
newsroomBasePath: baseAuthConfig.newsroomBasePath ?? activeBrand?.newsroomBasePath,
|
|
62
|
+
};
|
|
58
63
|
const backend = defineBackend({
|
|
59
64
|
assignmentAction,
|
|
60
65
|
auth: defineSiteAuth(authConfig),
|
|
@@ -215,7 +220,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
215
220
|
}
|
|
216
221
|
if (enableStorageBackups) {
|
|
217
222
|
const storageBackupsStack = backend.createStack("storage-backups");
|
|
218
|
-
const storageBackupVaultName = deriveStorageBackupVaultName(siteBackendIdentity,
|
|
223
|
+
const storageBackupVaultName = deriveStorageBackupVaultName(siteBackendIdentity, process.env.PAPYRUS_STORAGE_BACKUP_VAULT_NAME);
|
|
219
224
|
const storageBackups = addStorageBackups(storageBackupsStack, {
|
|
220
225
|
storageBucket,
|
|
221
226
|
backupVaultName: storageBackupVaultName,
|
package/amplify/site-backend.ts
CHANGED
|
@@ -71,7 +71,12 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
71
71
|
const enableSlackAgent = featureFlags.slack;
|
|
72
72
|
const enableStorageBackups = featureFlags.storageBackups;
|
|
73
73
|
|
|
74
|
-
const
|
|
74
|
+
const baseAuthConfig = site.auth ?? authConfigFromEnv();
|
|
75
|
+
const activeBrand = (papyrusSite.brands ?? []).find((brand) => brand.id.toLowerCase() === siteBackendIdentity.brandId);
|
|
76
|
+
const authConfig = {
|
|
77
|
+
...baseAuthConfig,
|
|
78
|
+
newsroomBasePath: baseAuthConfig.newsroomBasePath ?? activeBrand?.newsroomBasePath,
|
|
79
|
+
};
|
|
75
80
|
const backend = defineBackend({
|
|
76
81
|
assignmentAction,
|
|
77
82
|
auth: defineSiteAuth(authConfig),
|
|
@@ -277,7 +282,6 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
277
282
|
const storageBackupsStack = backend.createStack("storage-backups");
|
|
278
283
|
const storageBackupVaultName = deriveStorageBackupVaultName(
|
|
279
284
|
siteBackendIdentity,
|
|
280
|
-
storageBackupsStack.stackName,
|
|
281
285
|
process.env.PAPYRUS_STORAGE_BACKUP_VAULT_NAME,
|
|
282
286
|
);
|
|
283
287
|
|
|
@@ -8,6 +8,8 @@ import { CookieStorage, Hub } from "aws-amplify/utils";
|
|
|
8
8
|
import { useEffect } from "react";
|
|
9
9
|
import { assertSandboxAmplifyOutputsForDev } from "../lib/amplify-outputs-guard";
|
|
10
10
|
import amplifyOutputs from "papyrus-amplify-outputs";
|
|
11
|
+
import { getPublicNewsroomBasePath } from "../lib/newsroom-base-path";
|
|
12
|
+
import { prioritizeRedirectUrlsForOrigin } from "../lib/oauth-redirect-priority";
|
|
11
13
|
|
|
12
14
|
let configured = false;
|
|
13
15
|
|
|
@@ -72,14 +74,10 @@ function prioritizeCurrentOrigin(config: ResourcesConfig): ResourcesConfig {
|
|
|
72
74
|
const auth = (config as { auth?: { oauth?: Record<string, unknown> } }).auth;
|
|
73
75
|
const oauth = auth?.oauth;
|
|
74
76
|
if (!auth || !oauth) return config;
|
|
75
|
-
const origin = normalizeUrlOrigin(window.location.origin);
|
|
76
77
|
const reorder = (urls: unknown) => {
|
|
77
78
|
if (!Array.isArray(urls)) return urls;
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
if (!head.length) return normalized;
|
|
81
|
-
const tail = normalized.filter((value) => normalizeUrlOrigin(value) !== origin);
|
|
82
|
-
return [...head, ...tail];
|
|
79
|
+
const strings = urls.filter((value): value is string => typeof value === "string");
|
|
80
|
+
return prioritizeRedirectUrlsForOrigin(strings, window.location.origin, getPublicNewsroomBasePath());
|
|
83
81
|
};
|
|
84
82
|
return {
|
|
85
83
|
...config,
|
|
@@ -111,22 +109,6 @@ function redirectLoopbackToLocalhost(): void {
|
|
|
111
109
|
window.location.replace(target);
|
|
112
110
|
}
|
|
113
111
|
|
|
114
|
-
function normalizeUrlOrigin(value: string): string {
|
|
115
|
-
const trimmed = value.replace(/\/+$/, "");
|
|
116
|
-
try {
|
|
117
|
-
const parsed = new URL(trimmed);
|
|
118
|
-
const hostname = normalizeLoopbackHostname(parsed.hostname);
|
|
119
|
-
return `${parsed.protocol}//${hostname}${parsed.port ? `:${parsed.port}` : ""}`;
|
|
120
|
-
} catch {
|
|
121
|
-
return trimmed;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function normalizeLoopbackHostname(hostname: string): string {
|
|
126
|
-
if (hostname === "127.0.0.1" || hostname === "::1") return "localhost";
|
|
127
|
-
return hostname;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
112
|
function extractAuthFailureDetail(error: unknown): string | null {
|
|
131
113
|
if (!error) return null;
|
|
132
114
|
if (typeof error === "string") return error;
|
package/infra/site-config.js
CHANGED
|
@@ -13,6 +13,14 @@ const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
|
13
13
|
const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
14
14
|
const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
|
|
15
15
|
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER"];
|
|
16
|
+
function originOfRedirectUrl(value) {
|
|
17
|
+
try {
|
|
18
|
+
return new URL(value).origin;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
16
24
|
export function resolveRevalidateSecretParameterName(config) {
|
|
17
25
|
return `/papyrus/${config.siteId}/revalidate-secret`;
|
|
18
26
|
}
|
|
@@ -259,14 +267,15 @@ export function parseSiteConfig(raw) {
|
|
|
259
267
|
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
|
|
260
268
|
const redirectUrls = redirectValue.split(",").map((url) => url.trim());
|
|
261
269
|
const stagingDomainName = resolveStagingDomainName(config);
|
|
270
|
+
const listedOrigins = new Set(redirectUrls.map(originOfRedirectUrl));
|
|
262
271
|
const requiredOrigins = [
|
|
263
|
-
...(resolveCmsHostName(config) ? [`https://${resolveCmsHostName(config)}
|
|
264
|
-
...(stagingDomainName ? [`https://${stagingDomainName}
|
|
265
|
-
LOCAL_DEVELOPMENT_ORIGIN,
|
|
272
|
+
...(resolveCmsHostName(config) ? [`https://${resolveCmsHostName(config)}`] : []),
|
|
273
|
+
...(stagingDomainName ? [`https://${stagingDomainName}`] : []),
|
|
274
|
+
originOfRedirectUrl(LOCAL_DEVELOPMENT_ORIGIN),
|
|
266
275
|
];
|
|
267
276
|
for (const origin of requiredOrigins) {
|
|
268
|
-
if (!
|
|
269
|
-
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}`);
|
|
277
|
+
if (!listedOrigins.has(origin)) {
|
|
278
|
+
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}/ (or just the origin: the backend also registers its newsroom path)`);
|
|
270
279
|
}
|
|
271
280
|
}
|
|
272
281
|
return config;
|
package/infra/site-config.ts
CHANGED
|
@@ -57,6 +57,14 @@ const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
|
57
57
|
const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
58
58
|
const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
|
|
59
59
|
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER"];
|
|
60
|
+
function originOfRedirectUrl(value: string): string {
|
|
61
|
+
try {
|
|
62
|
+
return new URL(value).origin;
|
|
63
|
+
} catch {
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
export function resolveRevalidateSecretParameterName(config: Pick<AmplifyAppShellSiteConfig, "siteId">): string {
|
|
61
69
|
return `/papyrus/${config.siteId}/revalidate-secret`;
|
|
62
70
|
}
|
|
@@ -321,14 +329,15 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
321
329
|
if (redirectValue === undefined) fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
|
|
322
330
|
const redirectUrls = redirectValue.split(",").map((url) => url.trim());
|
|
323
331
|
const stagingDomainName = resolveStagingDomainName(config);
|
|
332
|
+
const listedOrigins = new Set(redirectUrls.map(originOfRedirectUrl));
|
|
324
333
|
const requiredOrigins = [
|
|
325
|
-
...(resolveCmsHostName(config) ? [`https://${resolveCmsHostName(config)}
|
|
326
|
-
...(stagingDomainName ? [`https://${stagingDomainName}
|
|
327
|
-
LOCAL_DEVELOPMENT_ORIGIN,
|
|
334
|
+
...(resolveCmsHostName(config) ? [`https://${resolveCmsHostName(config)}`] : []),
|
|
335
|
+
...(stagingDomainName ? [`https://${stagingDomainName}`] : []),
|
|
336
|
+
originOfRedirectUrl(LOCAL_DEVELOPMENT_ORIGIN),
|
|
328
337
|
];
|
|
329
338
|
for (const origin of requiredOrigins) {
|
|
330
|
-
if (!
|
|
331
|
-
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}`);
|
|
339
|
+
if (!listedOrigins.has(origin)) {
|
|
340
|
+
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}/ (or just the origin: the backend also registers its newsroom path)`);
|
|
332
341
|
}
|
|
333
342
|
}
|
|
334
343
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
function originOf(value: string): string {
|
|
2
|
+
const trimmed = value.replace(/\/+$/, "");
|
|
3
|
+
try {
|
|
4
|
+
const parsed = new URL(trimmed);
|
|
5
|
+
const hostname = parsed.hostname === "127.0.0.1" || parsed.hostname === "::1" ? "localhost" : parsed.hostname;
|
|
6
|
+
return `${parsed.protocol}//${hostname}${parsed.port ? `:${parsed.port}` : ""}`;
|
|
7
|
+
} catch {
|
|
8
|
+
return trimmed;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function pathOf(value: string): string | null {
|
|
13
|
+
try {
|
|
14
|
+
return new URL(value).pathname.replace(/\/+$/, "");
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Puts the redirect URLs of the current origin first; among those, the
|
|
22
|
+
* newsroom return path leads when the site has one, so Amplify (which signs in
|
|
23
|
+
* with the first entry) returns an editor to the newsroom.
|
|
24
|
+
*/
|
|
25
|
+
export function prioritizeRedirectUrlsForOrigin(
|
|
26
|
+
urls: readonly string[],
|
|
27
|
+
currentOrigin: string,
|
|
28
|
+
publicNewsroomBasePath: string,
|
|
29
|
+
): string[] {
|
|
30
|
+
const origin = originOf(currentOrigin);
|
|
31
|
+
const newsroomPath = publicNewsroomBasePath.replace(/\/+$/, "");
|
|
32
|
+
const head = urls.filter((value) => originOf(value) === origin);
|
|
33
|
+
if (head.length === 0) return [...urls];
|
|
34
|
+
const tail = urls.filter((value) => originOf(value) !== origin);
|
|
35
|
+
const newsroomFirst =
|
|
36
|
+
newsroomPath === ""
|
|
37
|
+
? head
|
|
38
|
+
: [...head.filter((value) => pathOf(value) === newsroomPath), ...head.filter((value) => pathOf(value) !== newsroomPath)];
|
|
39
|
+
return [...newsroomFirst, ...tail];
|
|
40
|
+
}
|
package/package.json
CHANGED
|
@@ -186,6 +186,7 @@
|
|
|
186
186
|
"./lib/newsroom-references": "./lib/newsroom-references.ts",
|
|
187
187
|
"./lib/newsroom-sections": "./lib/newsroom-sections.ts",
|
|
188
188
|
"./lib/newsroom-topics": "./lib/newsroom-topics.ts",
|
|
189
|
+
"./lib/oauth-redirect-priority": "./lib/oauth-redirect-priority.ts",
|
|
189
190
|
"./lib/papyrus-object-kinds": "./lib/papyrus-object-kinds.ts",
|
|
190
191
|
"./lib/papyrus-web-locations": "./lib/papyrus-web-locations.ts",
|
|
191
192
|
"./lib/pilobol-sample": "./lib/pilobol-sample.ts",
|
|
@@ -299,7 +300,7 @@
|
|
|
299
300
|
"optional": true
|
|
300
301
|
}
|
|
301
302
|
},
|
|
302
|
-
"version": "1.0.0-next.
|
|
303
|
+
"version": "1.0.0-next.35",
|
|
303
304
|
"dependencies": {
|
|
304
305
|
"@aws-amplify/adapter-nextjs": "^1.7.3",
|
|
305
306
|
"@aws-sdk/client-cognito-identity-provider": "3.1045.0",
|
package/routes.manifest.json
CHANGED
package/src/with-papyrus.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
* `papyrus-amplify-outputs` -> ./amplify_outputs.json and
|
|
10
10
|
* `papyrus-site-theme` -> ./publication/theme.css when it exists, else the shipped
|
|
11
11
|
* empty lib/empty-theme.css (both webpack and turbopack),
|
|
12
|
+
* - trace the corpora/ YAML and amplify_outputs.json that the reader reads via fs at runtime,
|
|
12
13
|
* - carry Papyrus's own server-external packages and image settings.
|
|
13
14
|
*/
|
|
14
15
|
const SERVER_EXTERNAL = [
|
|
@@ -22,6 +23,15 @@ const SERVER_EXTERNAL = [
|
|
|
22
23
|
"@aws-amplify/data-schema",
|
|
23
24
|
];
|
|
24
25
|
|
|
26
|
+
export const RUNTIME_TRACED_FILES = ["./corpora/**/*", "./amplify_outputs.json"];
|
|
27
|
+
|
|
28
|
+
export function mergeOutputFileTracingIncludes(existing = {}) {
|
|
29
|
+
const merged = { ...existing };
|
|
30
|
+
const current = merged["/**"] ?? [];
|
|
31
|
+
merged["/**"] = [...new Set([...current, ...RUNTIME_TRACED_FILES])];
|
|
32
|
+
return merged;
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
const EMPTY_THEME = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../lib/empty-theme.css");
|
|
26
36
|
|
|
27
37
|
export function withPapyrus(nextConfig = {}, options = {}) {
|
|
@@ -41,6 +51,7 @@ export function withPapyrus(nextConfig = {}, options = {}) {
|
|
|
41
51
|
...nextConfig,
|
|
42
52
|
transpilePackages: [...new Set([...(nextConfig.transpilePackages ?? []), "@anthusai/papyrus"])],
|
|
43
53
|
serverExternalPackages: [...new Set([...(nextConfig.serverExternalPackages ?? []), ...SERVER_EXTERNAL])],
|
|
54
|
+
outputFileTracingIncludes: mergeOutputFileTracingIncludes(nextConfig.outputFileTracingIncludes),
|
|
44
55
|
images: nextConfig.images ?? { remotePatterns: [{ protocol: "https", hostname: "**" }] },
|
|
45
56
|
experimental: { devtoolSegmentExplorer: false, ...nextConfig.experimental },
|
|
46
57
|
turbopack: {
|