@anthusai/papyrus 0.0.0 → 1.0.0-next.10
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/LICENSE +21 -0
- package/amplify/auth/resource.js +61 -0
- package/amplify/auth/resource.ts +65 -0
- package/amplify/data/resource.js +2069 -0
- package/amplify/data/resource.ts +2185 -0
- package/amplify/data/schema.js +1882 -0
- package/amplify/data/schema.ts +1990 -0
- package/amplify/functions/assignment-action/handler.ts +1416 -0
- package/amplify/functions/assignment-action/resource.js +8 -0
- package/amplify/functions/assignment-action/resource.ts +9 -0
- package/amplify/functions/category-action/handler.ts +2259 -0
- package/amplify/functions/category-action/resource.js +8 -0
- package/amplify/functions/category-action/resource.ts +9 -0
- package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
- package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
- package/amplify/functions/console-chat-responder/Dockerfile +19 -0
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
- package/amplify/functions/console-chat-responder/resource.js +153 -0
- package/amplify/functions/console-chat-responder/resource.ts +179 -0
- package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
- package/amplify/functions/content-actions/handler.py +149 -0
- package/amplify/functions/content-actions/requirements.txt +4 -0
- package/amplify/functions/content-actions/resource.js +40 -0
- package/amplify/functions/content-actions/resource.ts +54 -0
- package/amplify/functions/email-submission-processor/handler.py +46 -0
- package/amplify/functions/email-submission-processor/requirements.txt +2 -0
- package/amplify/functions/email-submission-processor/resource.js +56 -0
- package/amplify/functions/email-submission-processor/resource.ts +69 -0
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
- package/amplify/functions/knowledge-query/handler.py +1 -0
- package/amplify/functions/knowledge-query/requirements.txt +3 -0
- package/amplify/functions/knowledge-query/resource.js +50 -0
- package/amplify/functions/knowledge-query/resource.ts +61 -0
- package/amplify/functions/manage-user-role/handler.ts +799 -0
- package/amplify/functions/manage-user-role/resource.js +8 -0
- package/amplify/functions/manage-user-role/resource.ts +9 -0
- package/amplify/functions/model-attachment-upload/handler.ts +387 -0
- package/amplify/functions/model-attachment-upload/resource.js +8 -0
- package/amplify/functions/model-attachment-upload/resource.ts +9 -0
- package/amplify/functions/newsroom-summary/handler.ts +321 -0
- package/amplify/functions/newsroom-summary/resource.js +8 -0
- package/amplify/functions/newsroom-summary/resource.ts +9 -0
- package/amplify/functions/procedure-action/handler.ts +621 -0
- package/amplify/functions/procedure-action/resource.js +8 -0
- package/amplify/functions/procedure-action/resource.ts +9 -0
- package/amplify/functions/reader-settings/handler.ts +322 -0
- package/amplify/functions/reader-settings/resource.js +8 -0
- package/amplify/functions/reader-settings/resource.ts +9 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
- package/amplify/functions/ses-inbound-receive/resource.js +8 -0
- package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
- package/amplify/functions/shared/amplify-secrets.ts +65 -0
- package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
- package/amplify/functions/shared/email-mime-intake.ts +196 -0
- package/amplify/functions/shared/email-submission.ts +415 -0
- package/amplify/functions/shared/inbound-email-intake.ts +37 -0
- package/amplify/functions/shared/lambda-data-client.ts +21 -0
- package/amplify/functions/shared/model-payloads.ts +186 -0
- package/amplify/functions/shared/python-bundle.js +130 -0
- package/amplify/functions/shared/python-bundle.ts +162 -0
- package/amplify/functions/shared/slack-events.ts +111 -0
- package/amplify/functions/slack-delivery/handler.py +112 -0
- package/amplify/functions/slack-delivery/requirements.txt +2 -0
- package/amplify/functions/slack-delivery/resource.js +38 -0
- package/amplify/functions/slack-delivery/resource.ts +51 -0
- package/amplify/functions/slack-events/handler.ts +155 -0
- package/amplify/functions/slack-events/resource.js +11 -0
- package/amplify/functions/slack-events/resource.ts +12 -0
- package/amplify/inbound-email/stack.js +46 -0
- package/amplify/inbound-email/stack.ts +61 -0
- package/amplify/package.json +3 -0
- package/amplify/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/site-backend-config.js +1 -0
- package/amplify/site-backend-config.ts +36 -0
- package/amplify/site-backend.js +615 -0
- package/amplify/site-backend.ts +739 -0
- package/amplify/storage/resource.js +21 -0
- package/amplify/storage/resource.ts +22 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
- package/app/[year]/[month]/[day]/page.tsx +22 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
- package/app/api/archive/editions/route.ts +18 -0
- package/app/api/media/[...path]/route.ts +35 -0
- package/app/api/revalidate/route.ts +94 -0
- package/app/archive/page.tsx +24 -0
- package/app/articles/[slug]/page.tsx +35 -0
- package/app/globals.css +10345 -0
- package/app/layout.tsx +142 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
- package/app/newsroom/[section]/page.tsx +47 -0
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/newsroom/forum/[threadId]/page.tsx +35 -0
- package/app/newsroom/insights/[threadId]/page.tsx +22 -0
- package/app/newsroom/insights/page.tsx +14 -0
- package/app/newsroom/layout.tsx +12 -0
- package/app/newsroom/page.tsx +31 -0
- package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
- package/app/page.tsx +272 -0
- package/app/papyrus-theme.css +11 -0
- package/app/robots.ts +11 -0
- package/app/settings/page.tsx +14 -0
- package/app/tailwind.css +65 -0
- package/bin/papyrus-app.mjs +20 -0
- package/bin/papyrus-infra.mjs +95 -0
- package/components/ai-elements/conversation.tsx +168 -0
- package/components/ai-elements/message.tsx +360 -0
- package/components/ai-elements/model-selector.tsx +30 -0
- package/components/ai-elements/prompt-input.tsx +1461 -0
- package/components/ai-elements/shimmer.tsx +76 -0
- package/components/ai-elements/suggestion.tsx +54 -0
- package/components/ai-elements/tool.tsx +173 -0
- package/components/amplify-client-provider.tsx +140 -0
- package/components/archive-grid.tsx +89 -0
- package/components/archive-shell.tsx +41 -0
- package/components/article-video.tsx +156 -0
- package/components/blog-page-background.tsx +20 -0
- package/components/dev-sandbox-editor-sign-in.tsx +60 -0
- package/components/markus-ir-preview.tsx +300 -0
- package/components/news-desk-client-provider.tsx +211 -0
- package/components/news-desk-page.tsx +116 -0
- package/components/news-desk-taxonomy-client.ts +3684 -0
- package/components/newspaper.tsx +1929 -0
- package/components/newsroom-access-gate.tsx +66 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-assignments-view.tsx +664 -0
- package/components/newsroom-ops-overview.tsx +226 -0
- package/components/newsroom-ops-shell.tsx +321 -0
- package/components/newsroom-references-view.tsx +598 -0
- package/components/newsroom-topics-view.tsx +425 -0
- package/components/papyrus-console-shell.tsx +1912 -0
- package/components/pictogram-figure.tsx +65 -0
- package/components/presentation-footer.tsx +146 -0
- package/components/reader-auth-control.tsx +162 -0
- package/components/reader-auth-state.ts +122 -0
- package/components/reader-settings.ts +332 -0
- package/components/reader-theme-control.tsx +85 -0
- package/components/reference-source-preview.tsx +163 -0
- package/components/settings-page.tsx +239 -0
- package/components/staging-banner.tsx +11 -0
- package/components/topic-steering-workspace.tsx +18005 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button-group.tsx +87 -0
- package/components/ui/button.tsx +58 -0
- package/components/ui/card.tsx +32 -0
- package/components/ui/command.tsx +194 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +266 -0
- package/components/ui/hover-card.tsx +51 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/scroll-area.tsx +16 -0
- package/components/ui/select.tsx +199 -0
- package/components/ui/separator.tsx +23 -0
- package/components/ui/sheet.tsx +92 -0
- package/components/ui/spinner.tsx +10 -0
- package/components/ui/tabs.tsx +113 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/use-resolved-papyrus-theme.ts +47 -0
- package/infra/amplify-app-shell.js +234 -0
- package/infra/amplify-app-shell.ts +284 -0
- package/infra/build-specs.js +85 -0
- package/infra/build-specs.ts +131 -0
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +15 -0
- package/infra/site-config.js +186 -0
- package/infra/site-config.ts +235 -0
- package/lib/amplify-outputs-guard.ts +24 -0
- package/lib/amplify-outputs-path.ts +16 -0
- package/lib/amplify-server-runtime.ts +63 -0
- package/lib/archive-data.ts +30 -0
- package/lib/archive-types.ts +15 -0
- package/lib/articles.ts +320 -0
- package/lib/cached-content-repository.ts +90 -0
- package/lib/category-dashboard.ts +37 -0
- package/lib/category-repository.ts +1907 -0
- package/lib/console-chat-client.ts +1316 -0
- package/lib/console-web-context.ts +118 -0
- package/lib/content-actions-client.ts +98 -0
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-load-timing.ts +23 -0
- package/lib/content-repository.ts +43 -0
- package/lib/content-source-context.ts +26 -0
- package/lib/content-types.ts +104 -0
- package/lib/define-site.js +3 -0
- package/lib/define-site.ts +21 -0
- package/lib/demo-amplify-outputs.ts +15 -0
- package/lib/dev-sandbox-editor-auth.ts +18 -0
- package/lib/doctrine.ts +128 -0
- package/lib/edition-routes.ts +155 -0
- package/lib/edition-sections.ts +111 -0
- package/lib/empty-edition-layout-plan.ts +109 -0
- package/lib/empty-theme.css +1 -0
- package/lib/excerpts.ts +8 -0
- package/lib/graphql-content-repository.ts +1140 -0
- package/lib/image-url.ts +10 -0
- package/lib/layout-plan.ts +981 -0
- package/lib/layout-scenarios.ts +564 -0
- package/lib/markus-body.ts +168 -0
- package/lib/markus-ir.ts +235 -0
- package/lib/markus-projection.ts +243 -0
- package/lib/markus-to-article.ts +93 -0
- package/lib/news-desk-session.ts +160 -0
- package/lib/newspaper-layout.ts +3459 -0
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-assignments.ts +173 -0
- package/lib/newsroom-base-path.ts +48 -0
- package/lib/newsroom-card-layout.ts +102 -0
- package/lib/newsroom-category-drilldown.ts +133 -0
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-demo-dashboard.ts +267 -0
- package/lib/newsroom-demo-profile.ts +46 -0
- package/lib/newsroom-forum-routes.ts +153 -0
- package/lib/newsroom-index-filters.ts +202 -0
- package/lib/newsroom-knowledge-query-request.ts +51 -0
- package/lib/newsroom-list-selection.ts +42 -0
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +44 -0
- package/lib/newsroom-references.ts +116 -0
- package/lib/newsroom-sections.ts +52 -0
- package/lib/newsroom-topics.ts +118 -0
- package/lib/papyrus-object-kinds.ts +24 -0
- package/lib/papyrus-web-locations.ts +480 -0
- package/lib/pilobol-sample.ts +167 -0
- package/lib/presentation-footer.ts +92 -0
- package/lib/pretext-layout.ts +179 -0
- package/lib/public-placeholder-config.ts +102 -0
- package/lib/publication-items.ts +181 -0
- package/lib/reader-route-config.ts +1 -0
- package/lib/reader-static-params.ts +56 -0
- package/lib/reader-storage-url.ts +63 -0
- package/lib/reference-display.ts +74 -0
- package/lib/reference-policy.ts +97 -0
- package/lib/reference-source-preview.ts +175 -0
- package/lib/renderer-config.ts +50 -0
- package/lib/renderer.ts +87 -0
- package/lib/reporting-story-budget.ts +638 -0
- package/lib/semantic-graph.ts +554 -0
- package/lib/site-brand-tenant.ts +22 -0
- package/lib/site-brand.ts +194 -0
- package/lib/site-env.ts +52 -0
- package/lib/site-renderer.ts +17 -0
- package/lib/site-stack.ts +195 -0
- package/lib/staging-gate.ts +37 -0
- package/lib/staging-preview-object.ts +98 -0
- package/lib/themed-image.ts +30 -0
- package/lib/utils.ts +6 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +63 -0
- package/middleware.ts +103 -0
- package/package.json +318 -1
- package/renderers/markus/index.ts +25 -0
- package/renderers/markus/stubs.tsx +33 -0
- package/renderers/pretext/article-page.tsx +169 -0
- package/renderers/pretext/index.ts +30 -0
- package/renderers/pretext/layouts.ts +16 -0
- package/renderers/pretext/presentation-shell.tsx +470 -0
- package/routes.manifest.json +208 -0
- package/scripts/fixtures/pilobol-sample.ast.json +234 -0
- package/scripts/videoml/build-browser-bundle.mjs +22 -0
- package/scripts/videoml/build-preview-bundle.mjs +50 -0
- package/scripts/videoml/bundle-common.mjs +129 -0
- package/scripts/videoml/preview/preview-entry.tsx +381 -0
- package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
- package/scripts/videoml/preview/preview-timing.ts +42 -0
- package/scripts/videoml/preview.html +77 -0
- package/scripts/videoml/standard-entry.tsx +46 -0
- package/src/backend.js +1 -0
- package/src/backend.ts +3 -0
- package/src/sync.mjs +87 -0
- package/src/with-papyrus.d.mts +2 -0
- package/src/with-papyrus.mjs +60 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { AmplifyAppShellSiteConfig, resolveStoragePreviewPrefix } from "./site-config";
|
|
2
|
+
|
|
3
|
+
export type BuildSpecAppName = "cms-production" | "cms-staging" | "reader";
|
|
4
|
+
|
|
5
|
+
export function papyrusVersionToPep440(version: string): string {
|
|
6
|
+
const match = /^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z-]+\.(\d+))?$/.exec(version);
|
|
7
|
+
if (!match) throw new Error(`cannot map ${version} to PEP 440`);
|
|
8
|
+
return match[2] === undefined ? match[1] : `${match[1]}.dev${match[2]}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function yamlScalar(command: string): string {
|
|
12
|
+
const needsQuoting = /: | #|^[-?:,\[\]{}#&*!|>'"%@`]|:$/.test(command);
|
|
13
|
+
return needsQuoting ? `'${command.replace(/'/g, "''")}'` : command;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function commandLines(commands: string[], indent: string): string[] {
|
|
17
|
+
return commands.map((command) => `${indent}- ${yamlScalar(command)}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const NPM_CLEAN_INSTALL = "npm ci --cache .npm --prefer-offline";
|
|
21
|
+
|
|
22
|
+
const FRONTEND_ENVIRONMENT_CAPTURE =
|
|
23
|
+
"env | grep -e '^PAPYRUS_' -e '^SITE_ENV=' -e '^NEXT_PUBLIC_' >> .env.production";
|
|
24
|
+
|
|
25
|
+
function pythonProvisioningCommands(config: AmplifyAppShellSiteConfig): string[] {
|
|
26
|
+
return [
|
|
27
|
+
"export UV_CACHE_DIR=\"$PWD/.uv-cache\"",
|
|
28
|
+
"curl -LsSf https://astral.sh/uv/install.sh | sh",
|
|
29
|
+
"export PATH=\"$HOME/.local/bin:$PATH\"",
|
|
30
|
+
"uv python install 3.12",
|
|
31
|
+
"uv venv --python 3.12 .venv",
|
|
32
|
+
`uv pip install --python .venv "papyrus-newsroom[markus]==${papyrusVersionToPep440(config.papyrusVersion)}"`,
|
|
33
|
+
"export PATH=\"$PWD/.venv/bin:$PATH\"",
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function reader(config: AmplifyAppShellSiteConfig): NonNullable<AmplifyAppShellSiteConfig["reader"]> {
|
|
38
|
+
if (!config.reader) throw new Error(`site ${config.siteId} has no reader app (frontend ${config.frontend})`);
|
|
39
|
+
return config.reader;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function frontendSection(preBuildCommands: string[], buildCommands: string[], artifactsDirectory: string, cachePaths: string[]): string[] {
|
|
43
|
+
const lines = ["frontend:", " phases:"];
|
|
44
|
+
if (preBuildCommands.length > 0) {
|
|
45
|
+
lines.push(" preBuild:", " commands:", ...commandLines(preBuildCommands, " "));
|
|
46
|
+
}
|
|
47
|
+
lines.push(
|
|
48
|
+
" build:",
|
|
49
|
+
" commands:",
|
|
50
|
+
...commandLines(buildCommands, " "),
|
|
51
|
+
" artifacts:",
|
|
52
|
+
` baseDirectory: ${artifactsDirectory}`,
|
|
53
|
+
" files:",
|
|
54
|
+
" - '**/*'",
|
|
55
|
+
" cache:",
|
|
56
|
+
" paths:",
|
|
57
|
+
...cachePaths.map((cachePath) => ` - ${cachePath}`),
|
|
58
|
+
);
|
|
59
|
+
return lines;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function cmsProductionBuildSpec(config: AmplifyAppShellSiteConfig): string {
|
|
63
|
+
return [
|
|
64
|
+
"version: 1",
|
|
65
|
+
"backend:",
|
|
66
|
+
" phases:",
|
|
67
|
+
" build:",
|
|
68
|
+
" commands:",
|
|
69
|
+
...commandLines(
|
|
70
|
+
[
|
|
71
|
+
NPM_CLEAN_INSTALL,
|
|
72
|
+
"if [ -z \"${PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI:-}\" ]; then export PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD=true; fi",
|
|
73
|
+
"npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID",
|
|
74
|
+
],
|
|
75
|
+
" ",
|
|
76
|
+
),
|
|
77
|
+
...frontendSection(
|
|
78
|
+
[],
|
|
79
|
+
[FRONTEND_ENVIRONMENT_CAPTURE, "npx papyrus-app sync && npm run build"],
|
|
80
|
+
".next",
|
|
81
|
+
[".next/cache/**/*", ".npm/**/*"],
|
|
82
|
+
),
|
|
83
|
+
].join("\n") + "\n";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function cmsStagingBuildSpec(config: AmplifyAppShellSiteConfig): string {
|
|
87
|
+
const preBuild = [NPM_CLEAN_INSTALL, "npx ampx generate outputs --branch main --app-id $AWS_APP_ID"];
|
|
88
|
+
const cachePaths = [".next/cache/**/*", ".npm/**/*"];
|
|
89
|
+
if (config.frontend === "markus-static") {
|
|
90
|
+
const readerConfig = reader(config);
|
|
91
|
+
preBuild.push(
|
|
92
|
+
...pythonProvisioningCommands(config),
|
|
93
|
+
"papyrus auth refresh-jwt --write-env .env",
|
|
94
|
+
"papyrus ops content export-published --drafts --out content-export --clean",
|
|
95
|
+
readerConfig.buildCommand,
|
|
96
|
+
`papyrus ops content upload-preview --dir ${readerConfig.baseDirectory} --prefix ${resolveStoragePreviewPrefix(config)}`,
|
|
97
|
+
);
|
|
98
|
+
cachePaths.push(".uv-cache/**/*");
|
|
99
|
+
}
|
|
100
|
+
return [
|
|
101
|
+
"version: 1",
|
|
102
|
+
...frontendSection(
|
|
103
|
+
preBuild,
|
|
104
|
+
[FRONTEND_ENVIRONMENT_CAPTURE, "npx papyrus-app sync && npm run build"],
|
|
105
|
+
".next",
|
|
106
|
+
cachePaths,
|
|
107
|
+
),
|
|
108
|
+
].join("\n") + "\n";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function readerBuildSpec(config: AmplifyAppShellSiteConfig): string {
|
|
112
|
+
const readerConfig = reader(config);
|
|
113
|
+
return [
|
|
114
|
+
"version: 1",
|
|
115
|
+
...frontendSection(
|
|
116
|
+
[
|
|
117
|
+
...pythonProvisioningCommands(config),
|
|
118
|
+
"papyrus ops content export-published --auth guest --out content-export --clean",
|
|
119
|
+
],
|
|
120
|
+
[readerConfig.buildCommand],
|
|
121
|
+
readerConfig.baseDirectory,
|
|
122
|
+
[".uv-cache/**/*"],
|
|
123
|
+
),
|
|
124
|
+
].join("\n") + "\n";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function buildSpecFor(config: AmplifyAppShellSiteConfig, app: BuildSpecAppName): string {
|
|
128
|
+
if (app === "cms-production") return cmsProductionBuildSpec(config);
|
|
129
|
+
if (app === "cms-staging") return cmsStagingBuildSpec(config);
|
|
130
|
+
return readerBuildSpec(config);
|
|
131
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { CfnOutput, Stack } from "aws-cdk-lib";
|
|
2
|
+
import * as iam from "aws-cdk-lib/aws-iam";
|
|
3
|
+
export const GITHUB_OIDC_PROVIDER_HOST = "token.actions.githubusercontent.com";
|
|
4
|
+
/**
|
|
5
|
+
* Account-global singleton: AWS allows one IAM OIDC provider per issuer URL per
|
|
6
|
+
* account, so this never belongs in a per-site stack. Deploy it once per
|
|
7
|
+
* account, and only after `aws iam list-open-id-connect-providers` shows no
|
|
8
|
+
* provider for token.actions.githubusercontent.com. When one already exists,
|
|
9
|
+
* do not deploy this stack: per-site CI roles only reference the provider by
|
|
10
|
+
* its ARN (arn:aws:iam::<account>:oidc-provider/token.actions.githubusercontent.com)
|
|
11
|
+
* and work against an existing provider unchanged.
|
|
12
|
+
*/
|
|
13
|
+
export class GithubOidcProviderStack extends Stack {
|
|
14
|
+
providerArn;
|
|
15
|
+
constructor(scope, id, props) {
|
|
16
|
+
super(scope, id, props);
|
|
17
|
+
const provider = new iam.OpenIdConnectProvider(this, "GithubOidc", {
|
|
18
|
+
url: `https://${GITHUB_OIDC_PROVIDER_HOST}`,
|
|
19
|
+
clientIds: ["sts.amazonaws.com"],
|
|
20
|
+
});
|
|
21
|
+
this.providerArn = provider.openIdConnectProviderArn;
|
|
22
|
+
new CfnOutput(this, "GithubOidcProviderArn", { value: provider.openIdConnectProviderArn });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
import { CfnOutput, Stack, StackProps } from "aws-cdk-lib";
|
|
3
|
+
import * as iam from "aws-cdk-lib/aws-iam";
|
|
4
|
+
|
|
5
|
+
export const GITHUB_OIDC_PROVIDER_HOST = "token.actions.githubusercontent.com";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Account-global singleton: AWS allows one IAM OIDC provider per issuer URL per
|
|
9
|
+
* account, so this never belongs in a per-site stack. Deploy it once per
|
|
10
|
+
* account, and only after `aws iam list-open-id-connect-providers` shows no
|
|
11
|
+
* provider for token.actions.githubusercontent.com. When one already exists,
|
|
12
|
+
* do not deploy this stack: per-site CI roles only reference the provider by
|
|
13
|
+
* its ARN (arn:aws:iam::<account>:oidc-provider/token.actions.githubusercontent.com)
|
|
14
|
+
* and work against an existing provider unchanged.
|
|
15
|
+
*/
|
|
16
|
+
export class GithubOidcProviderStack extends Stack {
|
|
17
|
+
public readonly providerArn: string;
|
|
18
|
+
|
|
19
|
+
constructor(scope: Construct, id: string, props?: StackProps) {
|
|
20
|
+
super(scope, id, props);
|
|
21
|
+
|
|
22
|
+
const provider = new iam.OpenIdConnectProvider(this, "GithubOidc", {
|
|
23
|
+
url: `https://${GITHUB_OIDC_PROVIDER_HOST}`,
|
|
24
|
+
clientIds: ["sts.amazonaws.com"],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
this.providerArn = provider.openIdConnectProviderArn;
|
|
28
|
+
new CfnOutput(this, "GithubOidcProviderArn", { value: provider.openIdConnectProviderArn });
|
|
29
|
+
}
|
|
30
|
+
}
|
package/infra/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// aws-cdk-lib and constructs are OPTIONAL peers of @anthusai/papyrus: only a publication's infra/ package installs them.
|
|
2
|
+
try {
|
|
3
|
+
await import("aws-cdk-lib");
|
|
4
|
+
await import("constructs");
|
|
5
|
+
} catch (error) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
"@anthusai/papyrus/infra needs aws-cdk-lib and constructs, which are optional peer dependencies. " +
|
|
8
|
+
"Install them in the package that imports this entry (your infra/ folder): npm install aws-cdk-lib constructs. " +
|
|
9
|
+
"(" + (error && error.code) + ")",
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
export const { AmplifyAppShellStack } = await import("./amplify-app-shell.js");
|
|
13
|
+
export const { GithubOidcProviderStack } = await import("./github-oidc-provider.js");
|
|
14
|
+
export { parseSiteConfig } from "./site-config.js";
|
|
15
|
+
export { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec, buildSpecFor } from "./build-specs.js";
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
export const DEFAULT_CI_BRANCHES = ["main", "staging"];
|
|
2
|
+
const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
3
|
+
const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
4
|
+
const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
|
|
5
|
+
const HOST_NAME_PATTERN = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
6
|
+
const APP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 _.-]*$/;
|
|
7
|
+
const STACK_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,127}$/;
|
|
8
|
+
const BRANCH_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9/_.-]*$/;
|
|
9
|
+
const PAPYRUS_VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+\.\d+)?$/;
|
|
10
|
+
const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
11
|
+
const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
12
|
+
const LOCAL_DEVELOPMENT_ORIGIN = "http://localhost:3001/";
|
|
13
|
+
function fail(field, problem) {
|
|
14
|
+
throw new Error(`site.json: ${field} ${problem}`);
|
|
15
|
+
}
|
|
16
|
+
function requireObject(value, field) {
|
|
17
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
18
|
+
fail(field, "must be an object");
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function requireString(value, field, pattern, patternHint) {
|
|
23
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
24
|
+
fail(field, "must be a non-empty string");
|
|
25
|
+
}
|
|
26
|
+
if (pattern && !pattern.test(value)) {
|
|
27
|
+
fail(field, `is invalid${patternHint ? ` (${patternHint})` : ""}: ${JSON.stringify(value)}`);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function optionalString(value, field, pattern, patternHint) {
|
|
32
|
+
return value === undefined ? undefined : requireString(value, field, pattern, patternHint);
|
|
33
|
+
}
|
|
34
|
+
function requireStringRecord(value, field) {
|
|
35
|
+
const record = requireObject(value, field);
|
|
36
|
+
const result = {};
|
|
37
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
38
|
+
if (typeof entry !== "string")
|
|
39
|
+
fail(`${field}.${key}`, "must be a string");
|
|
40
|
+
result[key] = entry;
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
function rejectUnknownKeys(record, allowed, field) {
|
|
45
|
+
for (const key of Object.keys(record)) {
|
|
46
|
+
if (!allowed.includes(key))
|
|
47
|
+
fail(`${field}.${key}`, `is not a known field (known: ${allowed.join(", ")})`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function isStagingEnabled(config) {
|
|
51
|
+
return config.cms.staging !== false;
|
|
52
|
+
}
|
|
53
|
+
export function resolveStagingDomainName(config) {
|
|
54
|
+
if (!isStagingEnabled(config))
|
|
55
|
+
return undefined;
|
|
56
|
+
if (config.cms.stagingDomainName)
|
|
57
|
+
return config.cms.stagingDomainName;
|
|
58
|
+
if (!config.cms.domainName)
|
|
59
|
+
return undefined;
|
|
60
|
+
const [, ...rest] = config.cms.domainName.split(".");
|
|
61
|
+
return ["staging", ...rest].join(".");
|
|
62
|
+
}
|
|
63
|
+
export function resolveStackName(config) {
|
|
64
|
+
return config.stackName ?? `amplify-app-shell-${config.siteId}`;
|
|
65
|
+
}
|
|
66
|
+
export function resolveStoragePreviewPrefix(config) {
|
|
67
|
+
return config.storagePreviewPrefix ?? "preview/";
|
|
68
|
+
}
|
|
69
|
+
export function parseSiteConfig(raw) {
|
|
70
|
+
const record = requireObject(raw, "site");
|
|
71
|
+
rejectUnknownKeys(record, ["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"], "site");
|
|
72
|
+
const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
|
|
73
|
+
const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
|
|
74
|
+
const brand = requireString(record.brand, "brand");
|
|
75
|
+
const frontend = requireString(record.frontend, "frontend");
|
|
76
|
+
if (frontend !== "pretext" && frontend !== "markus-static") {
|
|
77
|
+
fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
|
|
78
|
+
}
|
|
79
|
+
const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
|
|
80
|
+
const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
|
|
81
|
+
const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
|
|
82
|
+
const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
|
|
83
|
+
const cmsRecord = requireObject(record.cms, "cms");
|
|
84
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "buildComputeType", "environment", "stagingDomainName"], "cms");
|
|
85
|
+
const buildComputeType = cmsRecord.buildComputeType;
|
|
86
|
+
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
87
|
+
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
88
|
+
}
|
|
89
|
+
if (cmsRecord.staging !== undefined && typeof cmsRecord.staging !== "boolean") {
|
|
90
|
+
fail("cms.staging", `must be a boolean, got ${JSON.stringify(cmsRecord.staging)}`);
|
|
91
|
+
}
|
|
92
|
+
const cms = {
|
|
93
|
+
appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
|
|
94
|
+
domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
|
|
95
|
+
staging: cmsRecord.staging,
|
|
96
|
+
buildComputeType: buildComputeType,
|
|
97
|
+
environment: requireStringRecord(cmsRecord.environment, "cms.environment"),
|
|
98
|
+
stagingDomainName: optionalString(cmsRecord.stagingDomainName, "cms.stagingDomainName", HOST_NAME_PATTERN, "host name"),
|
|
99
|
+
};
|
|
100
|
+
if (cms.staging === false && cms.stagingDomainName) {
|
|
101
|
+
fail("cms.stagingDomainName", "must not be set when cms.staging is false");
|
|
102
|
+
}
|
|
103
|
+
if (cms.stagingDomainName && !cms.domainName) {
|
|
104
|
+
fail("cms.stagingDomainName", "requires cms.domainName");
|
|
105
|
+
}
|
|
106
|
+
let reader;
|
|
107
|
+
if (record.reader !== undefined) {
|
|
108
|
+
if (frontend !== "markus-static")
|
|
109
|
+
fail("reader", `is only allowed when frontend is "markus-static"`);
|
|
110
|
+
const readerRecord = requireObject(record.reader, "reader");
|
|
111
|
+
rejectUnknownKeys(readerRecord, ["appName", "domainName", "branchName", "buildCommand", "baseDirectory", "environment"], "reader");
|
|
112
|
+
reader = {
|
|
113
|
+
appName: optionalString(readerRecord.appName, "reader.appName", APP_NAME_PATTERN),
|
|
114
|
+
domainName: optionalString(readerRecord.domainName, "reader.domainName", HOST_NAME_PATTERN, "host name"),
|
|
115
|
+
branchName: optionalString(readerRecord.branchName, "reader.branchName", BRANCH_NAME_PATTERN),
|
|
116
|
+
buildCommand: requireString(readerRecord.buildCommand, "reader.buildCommand"),
|
|
117
|
+
baseDirectory: requireString(readerRecord.baseDirectory, "reader.baseDirectory"),
|
|
118
|
+
environment: readerRecord.environment === undefined ? undefined : requireStringRecord(readerRecord.environment, "reader.environment"),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
else if (frontend === "markus-static") {
|
|
122
|
+
fail("reader", `is required when frontend is "markus-static"`);
|
|
123
|
+
}
|
|
124
|
+
let github;
|
|
125
|
+
if (record.github !== undefined) {
|
|
126
|
+
const githubRecord = requireObject(record.github, "github");
|
|
127
|
+
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
|
|
128
|
+
const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
|
|
129
|
+
const repo = requireString(githubRecord.repo, "github.repo", GITHUB_REPO_PATTERN);
|
|
130
|
+
if (repository !== `https://github.com/${owner}/${repo}`) {
|
|
131
|
+
fail("github", `owner/repo must match repository ${repository}`);
|
|
132
|
+
}
|
|
133
|
+
let branches = cms.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
|
|
134
|
+
if (githubRecord.branches !== undefined) {
|
|
135
|
+
if (!Array.isArray(githubRecord.branches) || githubRecord.branches.length === 0) {
|
|
136
|
+
fail("github.branches", "must be a non-empty array of branch names");
|
|
137
|
+
}
|
|
138
|
+
branches = githubRecord.branches.map((branch, index) => {
|
|
139
|
+
if (typeof branch === "string" && /[*?]/.test(branch)) {
|
|
140
|
+
fail(`github.branches[${index}]`, `must not contain wildcards: ${JSON.stringify(branch)}`);
|
|
141
|
+
}
|
|
142
|
+
return requireString(branch, `github.branches[${index}]`, BRANCH_NAME_PATTERN);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const ciCanDeployInfra = githubRecord.ciCanDeployInfra ?? false;
|
|
146
|
+
if (typeof ciCanDeployInfra !== "boolean")
|
|
147
|
+
fail("github.ciCanDeployInfra", "must be a boolean");
|
|
148
|
+
github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra };
|
|
149
|
+
}
|
|
150
|
+
const hasDomain = cms.domainName !== undefined || reader?.domainName !== undefined;
|
|
151
|
+
if (hasDomain && hostedZoneId === undefined) {
|
|
152
|
+
fail("hostedZoneId", "is required when cms.domainName or reader.domainName is set");
|
|
153
|
+
}
|
|
154
|
+
if (!hasDomain && hostedZoneId !== undefined) {
|
|
155
|
+
fail("hostedZoneId", "is only allowed when cms.domainName or reader.domainName is set");
|
|
156
|
+
}
|
|
157
|
+
const config = {
|
|
158
|
+
siteId,
|
|
159
|
+
repository,
|
|
160
|
+
brand,
|
|
161
|
+
frontend,
|
|
162
|
+
hostedZoneId,
|
|
163
|
+
stackName,
|
|
164
|
+
cms,
|
|
165
|
+
reader,
|
|
166
|
+
papyrusVersion,
|
|
167
|
+
storagePreviewPrefix,
|
|
168
|
+
github,
|
|
169
|
+
};
|
|
170
|
+
const redirectValue = cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS;
|
|
171
|
+
if (redirectValue === undefined)
|
|
172
|
+
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
|
|
173
|
+
const redirectUrls = redirectValue.split(",").map((url) => url.trim());
|
|
174
|
+
const stagingDomainName = resolveStagingDomainName(config);
|
|
175
|
+
const requiredOrigins = [
|
|
176
|
+
...(cms.domainName ? [`https://${cms.domainName}/`] : []),
|
|
177
|
+
...(stagingDomainName ? [`https://${stagingDomainName}/`] : []),
|
|
178
|
+
LOCAL_DEVELOPMENT_ORIGIN,
|
|
179
|
+
];
|
|
180
|
+
for (const origin of requiredOrigins) {
|
|
181
|
+
if (!redirectUrls.includes(origin)) {
|
|
182
|
+
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return config;
|
|
186
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
export type AmplifyAppShellSiteConfig = {
|
|
2
|
+
siteId: string;
|
|
3
|
+
repository: string;
|
|
4
|
+
brand: string;
|
|
5
|
+
frontend: "pretext" | "markus-static";
|
|
6
|
+
hostedZoneId?: string;
|
|
7
|
+
stackName?: string;
|
|
8
|
+
cms: {
|
|
9
|
+
appName?: string;
|
|
10
|
+
domainName?: string;
|
|
11
|
+
staging?: boolean;
|
|
12
|
+
buildComputeType?: "STANDARD" | "STANDARD_8GB";
|
|
13
|
+
environment: Record<string, string>;
|
|
14
|
+
stagingDomainName?: string;
|
|
15
|
+
};
|
|
16
|
+
reader?: {
|
|
17
|
+
appName?: string;
|
|
18
|
+
domainName?: string;
|
|
19
|
+
branchName?: string;
|
|
20
|
+
buildCommand: string;
|
|
21
|
+
baseDirectory: string;
|
|
22
|
+
environment?: Record<string, string>;
|
|
23
|
+
};
|
|
24
|
+
papyrusVersion: string;
|
|
25
|
+
storagePreviewPrefix?: string;
|
|
26
|
+
github?: {
|
|
27
|
+
owner: string;
|
|
28
|
+
repo: string;
|
|
29
|
+
branches: string[];
|
|
30
|
+
ciCanDeployInfra: boolean;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const DEFAULT_CI_BRANCHES = ["main", "staging"];
|
|
35
|
+
|
|
36
|
+
const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
37
|
+
const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
38
|
+
const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
|
|
39
|
+
const HOST_NAME_PATTERN = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
40
|
+
const APP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 _.-]*$/;
|
|
41
|
+
const STACK_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,127}$/;
|
|
42
|
+
const BRANCH_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9/_.-]*$/;
|
|
43
|
+
const PAPYRUS_VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+\.\d+)?$/;
|
|
44
|
+
const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
45
|
+
const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
46
|
+
const LOCAL_DEVELOPMENT_ORIGIN = "http://localhost:3001/";
|
|
47
|
+
|
|
48
|
+
function fail(field: string, problem: string): never {
|
|
49
|
+
throw new Error(`site.json: ${field} ${problem}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function requireObject(value: unknown, field: string): Record<string, unknown> {
|
|
53
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
54
|
+
fail(field, "must be an object");
|
|
55
|
+
}
|
|
56
|
+
return value as Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function requireString(value: unknown, field: string, pattern?: RegExp, patternHint?: string): string {
|
|
60
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
61
|
+
fail(field, "must be a non-empty string");
|
|
62
|
+
}
|
|
63
|
+
if (pattern && !pattern.test(value as string)) {
|
|
64
|
+
fail(field, `is invalid${patternHint ? ` (${patternHint})` : ""}: ${JSON.stringify(value)}`);
|
|
65
|
+
}
|
|
66
|
+
return value as string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function optionalString(value: unknown, field: string, pattern?: RegExp, patternHint?: string): string | undefined {
|
|
70
|
+
return value === undefined ? undefined : requireString(value, field, pattern, patternHint);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function requireStringRecord(value: unknown, field: string): Record<string, string> {
|
|
74
|
+
const record = requireObject(value, field);
|
|
75
|
+
const result: Record<string, string> = {};
|
|
76
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
77
|
+
if (typeof entry !== "string") fail(`${field}.${key}`, "must be a string");
|
|
78
|
+
result[key] = entry as string;
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function rejectUnknownKeys(record: Record<string, unknown>, allowed: string[], field: string): void {
|
|
84
|
+
for (const key of Object.keys(record)) {
|
|
85
|
+
if (!allowed.includes(key)) fail(`${field}.${key}`, `is not a known field (known: ${allowed.join(", ")})`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function isStagingEnabled(config: Pick<AmplifyAppShellSiteConfig, "cms">): boolean {
|
|
90
|
+
return config.cms.staging !== false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resolveStagingDomainName(config: Pick<AmplifyAppShellSiteConfig, "cms">): string | undefined {
|
|
94
|
+
if (!isStagingEnabled(config)) return undefined;
|
|
95
|
+
if (config.cms.stagingDomainName) return config.cms.stagingDomainName;
|
|
96
|
+
if (!config.cms.domainName) return undefined;
|
|
97
|
+
const [, ...rest] = config.cms.domainName.split(".");
|
|
98
|
+
return ["staging", ...rest].join(".");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function resolveStackName(config: Pick<AmplifyAppShellSiteConfig, "siteId" | "stackName">): string {
|
|
102
|
+
return config.stackName ?? `amplify-app-shell-${config.siteId}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function resolveStoragePreviewPrefix(config: Pick<AmplifyAppShellSiteConfig, "storagePreviewPrefix">): string {
|
|
106
|
+
return config.storagePreviewPrefix ?? "preview/";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
110
|
+
const record = requireObject(raw, "site");
|
|
111
|
+
rejectUnknownKeys(
|
|
112
|
+
record,
|
|
113
|
+
["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"],
|
|
114
|
+
"site",
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
|
|
118
|
+
const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
|
|
119
|
+
const brand = requireString(record.brand, "brand");
|
|
120
|
+
const frontend = requireString(record.frontend, "frontend");
|
|
121
|
+
if (frontend !== "pretext" && frontend !== "markus-static") {
|
|
122
|
+
fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
|
|
123
|
+
}
|
|
124
|
+
const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
|
|
125
|
+
const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
|
|
126
|
+
const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
|
|
127
|
+
const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
|
|
128
|
+
|
|
129
|
+
const cmsRecord = requireObject(record.cms, "cms");
|
|
130
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "buildComputeType", "environment", "stagingDomainName"], "cms");
|
|
131
|
+
const buildComputeType = cmsRecord.buildComputeType;
|
|
132
|
+
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
133
|
+
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
134
|
+
}
|
|
135
|
+
if (cmsRecord.staging !== undefined && typeof cmsRecord.staging !== "boolean") {
|
|
136
|
+
fail("cms.staging", `must be a boolean, got ${JSON.stringify(cmsRecord.staging)}`);
|
|
137
|
+
}
|
|
138
|
+
const cms: AmplifyAppShellSiteConfig["cms"] = {
|
|
139
|
+
appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
|
|
140
|
+
domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
|
|
141
|
+
staging: cmsRecord.staging as boolean | undefined,
|
|
142
|
+
buildComputeType: buildComputeType as "STANDARD" | "STANDARD_8GB" | undefined,
|
|
143
|
+
environment: requireStringRecord(cmsRecord.environment, "cms.environment"),
|
|
144
|
+
stagingDomainName: optionalString(cmsRecord.stagingDomainName, "cms.stagingDomainName", HOST_NAME_PATTERN, "host name"),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (cms.staging === false && cms.stagingDomainName) {
|
|
148
|
+
fail("cms.stagingDomainName", "must not be set when cms.staging is false");
|
|
149
|
+
}
|
|
150
|
+
if (cms.stagingDomainName && !cms.domainName) {
|
|
151
|
+
fail("cms.stagingDomainName", "requires cms.domainName");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let reader: AmplifyAppShellSiteConfig["reader"];
|
|
155
|
+
if (record.reader !== undefined) {
|
|
156
|
+
if (frontend !== "markus-static") fail("reader", `is only allowed when frontend is "markus-static"`);
|
|
157
|
+
const readerRecord = requireObject(record.reader, "reader");
|
|
158
|
+
rejectUnknownKeys(readerRecord, ["appName", "domainName", "branchName", "buildCommand", "baseDirectory", "environment"], "reader");
|
|
159
|
+
reader = {
|
|
160
|
+
appName: optionalString(readerRecord.appName, "reader.appName", APP_NAME_PATTERN),
|
|
161
|
+
domainName: optionalString(readerRecord.domainName, "reader.domainName", HOST_NAME_PATTERN, "host name"),
|
|
162
|
+
branchName: optionalString(readerRecord.branchName, "reader.branchName", BRANCH_NAME_PATTERN),
|
|
163
|
+
buildCommand: requireString(readerRecord.buildCommand, "reader.buildCommand"),
|
|
164
|
+
baseDirectory: requireString(readerRecord.baseDirectory, "reader.baseDirectory"),
|
|
165
|
+
environment: readerRecord.environment === undefined ? undefined : requireStringRecord(readerRecord.environment, "reader.environment"),
|
|
166
|
+
};
|
|
167
|
+
} else if (frontend === "markus-static") {
|
|
168
|
+
fail("reader", `is required when frontend is "markus-static"`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let github: AmplifyAppShellSiteConfig["github"];
|
|
172
|
+
if (record.github !== undefined) {
|
|
173
|
+
const githubRecord = requireObject(record.github, "github");
|
|
174
|
+
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
|
|
175
|
+
const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
|
|
176
|
+
const repo = requireString(githubRecord.repo, "github.repo", GITHUB_REPO_PATTERN);
|
|
177
|
+
if (repository !== `https://github.com/${owner}/${repo}`) {
|
|
178
|
+
fail("github", `owner/repo must match repository ${repository}`);
|
|
179
|
+
}
|
|
180
|
+
let branches = cms.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
|
|
181
|
+
if (githubRecord.branches !== undefined) {
|
|
182
|
+
if (!Array.isArray(githubRecord.branches) || githubRecord.branches.length === 0) {
|
|
183
|
+
fail("github.branches", "must be a non-empty array of branch names");
|
|
184
|
+
}
|
|
185
|
+
branches = (githubRecord.branches as unknown[]).map((branch, index) => {
|
|
186
|
+
if (typeof branch === "string" && /[*?]/.test(branch)) {
|
|
187
|
+
fail(`github.branches[${index}]`, `must not contain wildcards: ${JSON.stringify(branch)}`);
|
|
188
|
+
}
|
|
189
|
+
return requireString(branch, `github.branches[${index}]`, BRANCH_NAME_PATTERN);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const ciCanDeployInfra = githubRecord.ciCanDeployInfra ?? false;
|
|
193
|
+
if (typeof ciCanDeployInfra !== "boolean") fail("github.ciCanDeployInfra", "must be a boolean");
|
|
194
|
+
github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra as boolean };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const hasDomain = cms.domainName !== undefined || reader?.domainName !== undefined;
|
|
198
|
+
if (hasDomain && hostedZoneId === undefined) {
|
|
199
|
+
fail("hostedZoneId", "is required when cms.domainName or reader.domainName is set");
|
|
200
|
+
}
|
|
201
|
+
if (!hasDomain && hostedZoneId !== undefined) {
|
|
202
|
+
fail("hostedZoneId", "is only allowed when cms.domainName or reader.domainName is set");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const config: AmplifyAppShellSiteConfig = {
|
|
206
|
+
siteId,
|
|
207
|
+
repository,
|
|
208
|
+
brand,
|
|
209
|
+
frontend,
|
|
210
|
+
hostedZoneId,
|
|
211
|
+
stackName,
|
|
212
|
+
cms,
|
|
213
|
+
reader,
|
|
214
|
+
papyrusVersion,
|
|
215
|
+
storagePreviewPrefix,
|
|
216
|
+
github,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const redirectValue = cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS;
|
|
220
|
+
if (redirectValue === undefined) fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
|
|
221
|
+
const redirectUrls = redirectValue.split(",").map((url) => url.trim());
|
|
222
|
+
const stagingDomainName = resolveStagingDomainName(config);
|
|
223
|
+
const requiredOrigins = [
|
|
224
|
+
...(cms.domainName ? [`https://${cms.domainName}/`] : []),
|
|
225
|
+
...(stagingDomainName ? [`https://${stagingDomainName}/`] : []),
|
|
226
|
+
LOCAL_DEVELOPMENT_ORIGIN,
|
|
227
|
+
];
|
|
228
|
+
for (const origin of requiredOrigins) {
|
|
229
|
+
if (!redirectUrls.includes(origin)) {
|
|
230
|
+
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return config;
|
|
235
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const PRODUCTION_GRAPHQL_HOST = "64hviw";
|
|
2
|
+
const PRODUCTION_USER_POOL_ID = "us-east-1_40Uot7WSv";
|
|
3
|
+
|
|
4
|
+
export function isProductionAmplifyOutputs(outputs: Record<string, unknown> | null): boolean {
|
|
5
|
+
if (!outputs) return false;
|
|
6
|
+
const graphqlUrl = String((outputs.data as { url?: string } | undefined)?.url ?? "");
|
|
7
|
+
const userPoolId = String((outputs.auth as { user_pool_id?: string } | undefined)?.user_pool_id ?? "");
|
|
8
|
+
return graphqlUrl.includes(PRODUCTION_GRAPHQL_HOST) || userPoolId === PRODUCTION_USER_POOL_ID;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function assertSandboxAmplifyOutputsForDev(outputs: Record<string, unknown>): void {
|
|
12
|
+
if (process.env.NODE_ENV === "production") return;
|
|
13
|
+
if (!isProductionAmplifyOutputs(outputs)) return;
|
|
14
|
+
const graphqlUrl = String((outputs.data as { url?: string } | undefined)?.url ?? "");
|
|
15
|
+
throw new Error(
|
|
16
|
+
[
|
|
17
|
+
"Papyrus dev is configured for production Amplify (amplify_outputs.json → " +
|
|
18
|
+
(graphqlUrl || "production AppSync") +
|
|
19
|
+
").",
|
|
20
|
+
"Run `npm run outputs:sandbox` or restart with `npm run dev` (runs ensure-sandbox-amplify-outputs first).",
|
|
21
|
+
"Use ~/Projects/Papyrus-production for production deploys only.",
|
|
22
|
+
].join(" "),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function getAmplifyOutputsPath(): string {
|
|
5
|
+
const explicit = process.env.PAPYRUS_AMPLIFY_OUTPUTS?.trim();
|
|
6
|
+
if (explicit) {
|
|
7
|
+
return path.isAbsolute(explicit) ? explicit : path.join(process.cwd(), explicit);
|
|
8
|
+
}
|
|
9
|
+
return path.join(process.cwd(), "amplify_outputs.json");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function readAmplifyOutputsFile(): Record<string, unknown> | null {
|
|
13
|
+
const outputsPath = getAmplifyOutputsPath();
|
|
14
|
+
if (!fs.existsSync(outputsPath)) return null;
|
|
15
|
+
return JSON.parse(fs.readFileSync(outputsPath, "utf8")) as Record<string, unknown>;
|
|
16
|
+
}
|