@anthusai/papyrus 1.0.0-next.2 → 1.0.0-next.21
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/resource.js +17 -4
- package/amplify/auth/resource.ts +21 -4
- package/amplify/data/resource.js +52 -70
- package/amplify/data/resource.ts +57 -70
- package/amplify/data/schema.js +52 -58
- package/amplify/data/schema.ts +57 -58
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
- package/amplify/functions/console-chat-responder/resource.js +0 -7
- package/amplify/functions/console-chat-responder/resource.ts +0 -8
- package/amplify/functions/console-chat-responder/src/main.rs +0 -9
- 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/shared/python-bundle.js +17 -2
- package/amplify/functions/shared/python-bundle.ts +23 -2
- package/amplify/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/site-backend-config.ts +12 -1
- package/amplify/site-backend.js +51 -27
- package/amplify/site-backend.ts +66 -34
- package/amplify/storage/resource.js +3 -0
- package/amplify/storage/resource.ts +3 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +4 -2
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/api/revalidate/route.ts +8 -7
- package/app/articles/[slug]/page.tsx +6 -4
- package/app/blog-rhythm.css +278 -0
- package/app/globals.css +22 -4
- package/app/layout.tsx +24 -12
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/page.tsx +12 -4
- package/app/robots.ts +11 -0
- package/bin/papyrus-app.mjs +12 -2
- package/bin/papyrus-infra.mjs +74 -18
- package/components/archive-shell.tsx +2 -22
- package/components/article-video.tsx +247 -3
- package/components/blog-page-background.tsx +23 -6
- package/components/markus-ir-preview.tsx +300 -0
- package/components/news-desk-page.tsx +1 -4
- package/components/newspaper.tsx +6 -2
- 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-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +3 -1
- package/components/pictogram-figure.tsx +9 -2
- package/components/presentation-header.tsx +135 -0
- package/components/presentation-rhythm-hrule.tsx +17 -0
- package/components/settings-page.tsx +2 -1
- package/components/staging-banner.tsx +11 -0
- package/components/topic-steering-workspace.tsx +1 -59
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +28 -0
- package/components/use-rhythm-overlay.ts +29 -0
- package/infra/amplify-app-shell.js +285 -80
- package/infra/amplify-app-shell.ts +326 -113
- package/infra/build-specs.js +94 -0
- package/infra/build-specs.ts +141 -0
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +3 -0
- package/infra/site-config.js +273 -0
- package/infra/site-config.ts +336 -0
- package/lib/blog-feature-solver.ts +459 -0
- package/lib/blog-rhythm.ts +134 -0
- package/lib/brand-fonts-watch.ts +43 -0
- package/lib/brand-fonts.ts +112 -0
- package/lib/cached-content-repository.ts +14 -1
- package/lib/category-repository.ts +1 -93
- package/lib/content-actions-client.ts +98 -0
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-repository.ts +6 -0
- package/lib/content-source-context.ts +26 -0
- package/lib/content-types.ts +4 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/empty-theme.css +1 -0
- package/lib/graphql-content-repository.ts +239 -43
- package/lib/layout-scenarios.ts +8 -8
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +3 -0
- package/lib/papyrus-web-locations.ts +7 -6
- package/lib/presentation-footer.ts +3 -2
- package/lib/pretext-layout.ts +12 -3
- package/lib/reader-base-path.ts +24 -0
- package/lib/reader-static-params.ts +8 -0
- package/lib/renderer.ts +7 -0
- package/lib/revalidate-secret.ts +59 -0
- package/lib/root-route-redirect.ts +16 -0
- package/lib/site-brand-tenant.ts +3 -22
- package/lib/site-brand.ts +95 -42
- package/lib/site-env.ts +52 -0
- package/lib/staging-gate.ts +37 -0
- package/lib/staging-gated-path.ts +22 -0
- package/lib/staging-preview-object.ts +98 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +77 -0
- package/middleware.ts +56 -42
- package/package.json +40 -1
- package/renderers/pretext/article-page.tsx +40 -9
- package/renderers/pretext/presentation-shared.tsx +113 -0
- package/renderers/pretext/presentation-shell.tsx +18 -112
- package/renderers/pretext/rhythm-article-page.tsx +165 -0
- package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
- package/renderers/pretext/rhythm-shell-style.ts +35 -0
- package/routes.manifest.json +37 -2
- 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/sync.mjs +30 -6
- package/src/with-papyrus.mjs +15 -2
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
- package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
- package/publications/anth_us/brand.ts +0 -68
- package/publications/anth_us/theme.css +0 -66
- package/publications/pilobol_us/brand.ts +0 -37
- package/publications/pilobol_us/theme.css +0 -140
- package/publications/threat_intelligence/blog-defense/graph.ts +0 -636
- package/publications/threat_intelligence/blog-defense/layout.ts +0 -934
- package/publications/threat_intelligence/blog-defense/page-background.tsx +0 -590
- package/publications/threat_intelligence/brand.ts +0 -40
- package/publications/threat_intelligence/pictograms/art.tsx +0 -608
- package/publications/threat_intelligence/pictograms/figure.tsx +0 -82
- package/publications/threat_intelligence/pictograms/registry.ts +0 -30
- package/publications/threat_intelligence/pictograms/system.tsx +0 -305
- package/publications/threat_intelligence/seed/seed-edition-content.json +0 -629
- package/publications/threat_intelligence/theme.css +0 -1440
- /package/{publications/papyrus/theme.css → app/papyrus-theme.css} +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import {
|
|
4
|
+
ComposableRenderer,
|
|
5
|
+
RendererProvider,
|
|
6
|
+
interpolate,
|
|
7
|
+
spring,
|
|
8
|
+
useCurrentFrame,
|
|
9
|
+
useVideoConfig,
|
|
10
|
+
} from "@videoml/toolchain/renderer";
|
|
11
|
+
import { getComponent, listComponents, registerComponent } from "@videoml/toolchain/renderer/components/registry";
|
|
12
|
+
|
|
13
|
+
type RenderFrameRequest = {
|
|
14
|
+
script: unknown;
|
|
15
|
+
frame: number;
|
|
16
|
+
config: { fps: number; width: number; height: number; durationFrames: number };
|
|
17
|
+
inputProps?: Record<string, unknown>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const root = document.getElementById("root");
|
|
21
|
+
let reactRoot: ReturnType<typeof createRoot> | null = null;
|
|
22
|
+
|
|
23
|
+
const videoApi = {
|
|
24
|
+
registerComponent,
|
|
25
|
+
getComponent,
|
|
26
|
+
listComponents,
|
|
27
|
+
ComposableRenderer,
|
|
28
|
+
RendererProvider,
|
|
29
|
+
useCurrentFrame,
|
|
30
|
+
useVideoConfig,
|
|
31
|
+
interpolate,
|
|
32
|
+
spring,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
(window as unknown as { Babulus: typeof videoApi }).Babulus = videoApi;
|
|
36
|
+
|
|
37
|
+
(window as unknown as { renderFrame: (request: RenderFrameRequest) => Promise<void> }).renderFrame = (request) => {
|
|
38
|
+
if (!root) throw new Error("VideoML render shell has no #root element.");
|
|
39
|
+
reactRoot ??= createRoot(root);
|
|
40
|
+
reactRoot.render(
|
|
41
|
+
<RendererProvider frame={request.frame} config={request.config}>
|
|
42
|
+
<ComposableRenderer script={request.script as never} {...(request.inputProps ?? {})} />
|
|
43
|
+
</RendererProvider>,
|
|
44
|
+
);
|
|
45
|
+
return new Promise((resolve) => setTimeout(resolve, 100));
|
|
46
|
+
};
|
package/src/sync.mjs
CHANGED
|
@@ -37,18 +37,42 @@ function shimFor(route) {
|
|
|
37
37
|
return lines.join("\n") + "\n";
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function middlewareShim(manifest) {
|
|
40
|
+
export function middlewareShim(manifest) {
|
|
41
41
|
const matcher = manifest.middleware.matcher ?? '["/((?!_next/static|_next/image|favicon.ico|icon).*)"]';
|
|
42
42
|
return [
|
|
43
43
|
MARKER,
|
|
44
44
|
`export { middleware } from "${PKG}/middleware";`,
|
|
45
|
-
`export const config = { matcher: ${matcher} };`,
|
|
45
|
+
`export const config = { ${manifest.middleware.runtime ? `runtime: ${JSON.stringify(manifest.middleware.runtime)}, ` : ""}matcher: ${matcher} };`,
|
|
46
46
|
"",
|
|
47
47
|
].join("\n");
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const RESERVED_FIRST_SEGMENTS = new Set(["newsroom", "api", "_next", "_preview", "%5F_preview"]);
|
|
51
|
+
|
|
52
|
+
export function normalizeReaderBasePath(value) {
|
|
53
|
+
const trimmed = (value ?? "").trim().replace(/\/+$/, "");
|
|
54
|
+
if (!trimmed) return "";
|
|
55
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function readReaderBasePath(root) {
|
|
59
|
+
let text;
|
|
60
|
+
try {
|
|
61
|
+
text = fs.readFileSync(path.join(root, "papyrus.config.ts"), "utf8");
|
|
62
|
+
} catch {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
const match = text.match(/\breaderBasePath\s*:\s*(["'`])([^"'`]*)\1/);
|
|
66
|
+
return normalizeReaderBasePath(match ? match[2] : "");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function sync({ root = process.cwd(), log = console.log, manifest: manifestOverride, readerBasePath: readerBasePathOverride } = {}) {
|
|
70
|
+
const manifest = manifestOverride ?? JSON.parse(fs.readFileSync(path.join(pkgRoot, "routes.manifest.json"), "utf8"));
|
|
71
|
+
const readerBasePath = normalizeReaderBasePath(readerBasePathOverride ?? readReaderBasePath(root));
|
|
72
|
+
if (readerBasePath && RESERVED_FIRST_SEGMENTS.has(readerBasePath.split("/")[1])) {
|
|
73
|
+
throw new Error(`readerBasePath "${readerBasePath}" collides with a Papyrus-owned route (newsroom, api, _preview)`);
|
|
74
|
+
}
|
|
75
|
+
const readerRouteFile = (file) => (readerBasePath ? file.replace(/^app\//, `app${readerBasePath}/`) : file);
|
|
52
76
|
const generated = [];
|
|
53
77
|
const skipped = [];
|
|
54
78
|
const write = (relPath, content) => {
|
|
@@ -61,7 +85,7 @@ export function sync({ root = process.cwd(), log = console.log } = {}) {
|
|
|
61
85
|
fs.writeFileSync(dest, content);
|
|
62
86
|
generated.push(relPath);
|
|
63
87
|
};
|
|
64
|
-
for (const route of manifest.routes) write(route.file, shimFor(route));
|
|
88
|
+
for (const route of manifest.routes) write(route.reader ? readerRouteFile(route.file) : route.file, shimFor(route));
|
|
65
89
|
write("middleware.ts", middlewareShim(manifest));
|
|
66
90
|
|
|
67
91
|
// Remove shims that are no longer in the manifest (e.g. after an upgrade).
|
|
@@ -81,7 +105,7 @@ export function sync({ root = process.cwd(), log = console.log } = {}) {
|
|
|
81
105
|
const re = new RegExp(`${GITIGNORE_BEGIN.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${GITIGNORE_END}`);
|
|
82
106
|
fs.writeFileSync(giFile, re.test(gi) ? gi.replace(re, block) : `${gi}${gi.endsWith("\n") || !gi ? "" : "\n"}${block}\n`);
|
|
83
107
|
|
|
84
|
-
log(`papyrus-app sync: ${generated.length} shim(s) written, ${skipped.length} site-owned file(s) kept (${PKG}@${manifest.version})`);
|
|
108
|
+
log(`papyrus-app sync: ${generated.length} shim(s) written, ${skipped.length} site-owned file(s) kept (${PKG}@${manifest.version})${readerBasePath ? `, reader mounted at ${readerBasePath}` : ""}`);
|
|
85
109
|
for (const rel of skipped) log(` site-owned: ${rel}`);
|
|
86
110
|
return { generated, skipped };
|
|
87
111
|
}
|
package/src/with-papyrus.mjs
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Wrap a publication repo's Next config for Papyrus (SPIKE, PPY-82be6c).
|
|
5
7
|
* - transpile the package (it ships TypeScript source),
|
|
6
|
-
* - alias `papyrus-site` -> ./papyrus.config.ts
|
|
7
|
-
* `papyrus-amplify-outputs` -> ./amplify_outputs.json
|
|
8
|
+
* - alias `papyrus-site` -> ./papyrus.config.ts,
|
|
9
|
+
* `papyrus-amplify-outputs` -> ./amplify_outputs.json and
|
|
10
|
+
* `papyrus-site-theme` -> ./publication/theme.css when it exists, else the shipped
|
|
11
|
+
* empty lib/empty-theme.css (both webpack and turbopack),
|
|
8
12
|
* - carry Papyrus's own server-external packages and image settings.
|
|
9
13
|
*/
|
|
10
14
|
const SERVER_EXTERNAL = [
|
|
@@ -18,11 +22,19 @@ const SERVER_EXTERNAL = [
|
|
|
18
22
|
"@aws-amplify/data-schema",
|
|
19
23
|
];
|
|
20
24
|
|
|
25
|
+
const EMPTY_THEME = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../lib/empty-theme.css");
|
|
26
|
+
|
|
21
27
|
export function withPapyrus(nextConfig = {}, options = {}) {
|
|
22
28
|
const root = options.root ?? process.cwd();
|
|
29
|
+
const hasPublicationTheme = fs.existsSync(path.join(root, "publication/theme.css"));
|
|
30
|
+
const toProjectRelative = (absolute) => {
|
|
31
|
+
const relative = path.relative(root, absolute).split(path.sep).join("/");
|
|
32
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
33
|
+
};
|
|
23
34
|
const aliases = {
|
|
24
35
|
"papyrus-site": path.join(root, "papyrus.config.ts"),
|
|
25
36
|
"papyrus-amplify-outputs": path.join(root, "amplify_outputs.json"),
|
|
37
|
+
"papyrus-site-theme": hasPublicationTheme ? path.join(root, "publication/theme.css") : EMPTY_THEME,
|
|
26
38
|
};
|
|
27
39
|
return {
|
|
28
40
|
distDir: process.env.NEXT_DIST_DIR || ".next",
|
|
@@ -37,6 +49,7 @@ export function withPapyrus(nextConfig = {}, options = {}) {
|
|
|
37
49
|
...nextConfig.turbopack?.resolveAlias,
|
|
38
50
|
"papyrus-site": "./papyrus.config.ts",
|
|
39
51
|
"papyrus-amplify-outputs": "./amplify_outputs.json",
|
|
52
|
+
"papyrus-site-theme": toProjectRelative(hasPublicationTheme ? path.join(root, "publication/theme.css") : EMPTY_THEME),
|
|
40
53
|
},
|
|
41
54
|
},
|
|
42
55
|
webpack(config, context) {
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
|
|
3
|
-
|
|
4
|
-
type AppSyncAuthorizerEvent = {
|
|
5
|
-
authorizationToken?: string;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
type JwtHeader = {
|
|
9
|
-
alg?: string;
|
|
10
|
-
typ?: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
type JwtClaims = {
|
|
14
|
-
iss?: string;
|
|
15
|
-
sub?: string;
|
|
16
|
-
aud?: string | string[];
|
|
17
|
-
exp?: number;
|
|
18
|
-
nbf?: number;
|
|
19
|
-
iat?: number;
|
|
20
|
-
scope?: string;
|
|
21
|
-
scp?: string | string[];
|
|
22
|
-
groups?: string | string[];
|
|
23
|
-
roles?: string | string[];
|
|
24
|
-
[key: string]: unknown;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export const handler = async (event: AppSyncAuthorizerEvent) => {
|
|
28
|
-
try {
|
|
29
|
-
const token = normalizeAuthorizationToken(event.authorizationToken);
|
|
30
|
-
const jwtSecret = await getJwtSecret();
|
|
31
|
-
const claims = verifyHs256Jwt(token, jwtSecret);
|
|
32
|
-
assertExpectedIssuer(claims);
|
|
33
|
-
assertExpectedAudience(claims);
|
|
34
|
-
assertRequiredScope(claims);
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
isAuthorized: true,
|
|
38
|
-
resolverContext: {
|
|
39
|
-
sub: claims.sub ?? "unknown",
|
|
40
|
-
iss: claims.iss ?? "unknown",
|
|
41
|
-
scope: getClaimValues(claims.scope).join(" "),
|
|
42
|
-
},
|
|
43
|
-
ttlOverride: 300,
|
|
44
|
-
};
|
|
45
|
-
} catch (error) {
|
|
46
|
-
console.warn(`JWT authorizer rejected request: ${(error as Error).message}`);
|
|
47
|
-
return {
|
|
48
|
-
isAuthorized: false,
|
|
49
|
-
ttlOverride: 0,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
function normalizeAuthorizationToken(value: string | undefined): string {
|
|
55
|
-
const token = value
|
|
56
|
-
?.replace(/^PapyrusJwt\s+/i, "")
|
|
57
|
-
.replace(/^papyrus-jwt:/i, "")
|
|
58
|
-
.replace(/^Bearer\s+/i, "")
|
|
59
|
-
.trim();
|
|
60
|
-
if (!token) throw new Error("Missing Authorization token.");
|
|
61
|
-
return token;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function verifyHs256Jwt(token: string, jwtSecret: string): JwtClaims {
|
|
65
|
-
const [encodedHeader, encodedPayload, encodedSignature] = token.split(".");
|
|
66
|
-
if (!encodedHeader || !encodedPayload || !encodedSignature) {
|
|
67
|
-
throw new Error("JWT must have header, payload, and signature.");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const header = decodeJson<JwtHeader>(encodedHeader);
|
|
71
|
-
if (header.alg !== "HS256") {
|
|
72
|
-
throw new Error(`Unsupported JWT alg ${header.alg ?? "unknown"}.`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const claims = decodeJson<JwtClaims>(encodedPayload);
|
|
76
|
-
const expectedSignature = createHmac("sha256", jwtSecret)
|
|
77
|
-
.update(`${encodedHeader}.${encodedPayload}`)
|
|
78
|
-
.digest("base64url");
|
|
79
|
-
|
|
80
|
-
if (!constantTimeEqual(encodedSignature, expectedSignature)) {
|
|
81
|
-
throw new Error("JWT signature verification failed.");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const now = Math.floor(Date.now() / 1000);
|
|
85
|
-
if (typeof claims.exp === "number" && claims.exp <= now) {
|
|
86
|
-
throw new Error("JWT is expired.");
|
|
87
|
-
}
|
|
88
|
-
if (typeof claims.nbf === "number" && claims.nbf > now) {
|
|
89
|
-
throw new Error("JWT is not yet valid.");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return claims;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function decodeJson<T>(encoded: string): T {
|
|
96
|
-
try {
|
|
97
|
-
return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")) as T;
|
|
98
|
-
} catch {
|
|
99
|
-
throw new Error("JWT contains invalid base64url JSON.");
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
let cachedJwtSecret: string | null = null;
|
|
104
|
-
let ssmClient: SSMClient | null = null;
|
|
105
|
-
|
|
106
|
-
async function getJwtSecret(): Promise<string> {
|
|
107
|
-
if (cachedJwtSecret) return cachedJwtSecret;
|
|
108
|
-
|
|
109
|
-
const directSecret = normalizeOptionalString(process.env.PAPYRUS_JWT_SECRET);
|
|
110
|
-
if (directSecret && !isAmplifySecretPlaceholder(directSecret)) {
|
|
111
|
-
cachedJwtSecret = directSecret;
|
|
112
|
-
return directSecret;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const parameterName = resolveAmplifySsmSecretPath("PAPYRUS_JWT_SECRET");
|
|
116
|
-
if (!parameterName) throw new Error("PAPYRUS_JWT_SECRET is not configured.");
|
|
117
|
-
ssmClient ??= new SSMClient({});
|
|
118
|
-
const response = await ssmClient.send(new GetParameterCommand({
|
|
119
|
-
Name: parameterName,
|
|
120
|
-
WithDecryption: true,
|
|
121
|
-
}));
|
|
122
|
-
const secret = normalizeOptionalString(response.Parameter?.Value);
|
|
123
|
-
if (!secret) throw new Error(`SSM parameter ${parameterName} did not return a value.`);
|
|
124
|
-
cachedJwtSecret = secret;
|
|
125
|
-
return secret;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function isAmplifySecretPlaceholder(value: string): boolean {
|
|
129
|
-
return /^<.*will be resolved.*>$/i.test(value);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function resolveAmplifySsmSecretPath(name: string): string | null {
|
|
133
|
-
const rawConfig = normalizeOptionalString(process.env.AMPLIFY_SSM_ENV_CONFIG);
|
|
134
|
-
if (!rawConfig) return null;
|
|
135
|
-
try {
|
|
136
|
-
const config = JSON.parse(rawConfig) as Record<string, { path?: string; sharedPath?: string } | undefined>;
|
|
137
|
-
return normalizeOptionalString(config[name]?.path) ?? normalizeOptionalString(config[name]?.sharedPath);
|
|
138
|
-
} catch {
|
|
139
|
-
throw new Error("AMPLIFY_SSM_ENV_CONFIG contains invalid JSON.");
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function normalizeOptionalString(value: unknown): string | null {
|
|
144
|
-
if (typeof value !== "string") return null;
|
|
145
|
-
const trimmed = value.trim();
|
|
146
|
-
return trimmed ? trimmed : null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function assertExpectedIssuer(claims: JwtClaims): void {
|
|
150
|
-
const expectedIssuer = process.env.PAPYRUS_JWT_ISSUER;
|
|
151
|
-
if (expectedIssuer && claims.iss !== expectedIssuer) {
|
|
152
|
-
throw new Error("JWT issuer is not authorized.");
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function assertExpectedAudience(claims: JwtClaims): void {
|
|
157
|
-
const expectedAudience = process.env.PAPYRUS_JWT_AUDIENCE;
|
|
158
|
-
if (!expectedAudience) return;
|
|
159
|
-
|
|
160
|
-
const audiences = Array.isArray(claims.aud) ? claims.aud : claims.aud ? [claims.aud] : [];
|
|
161
|
-
if (!audiences.includes(expectedAudience)) {
|
|
162
|
-
throw new Error("JWT audience is not authorized.");
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function assertRequiredScope(claims: JwtClaims): void {
|
|
167
|
-
const requiredScope = process.env.PAPYRUS_JWT_REQUIRED_SCOPE;
|
|
168
|
-
if (!requiredScope) return;
|
|
169
|
-
|
|
170
|
-
const values = new Set([
|
|
171
|
-
...getClaimValues(claims.scope),
|
|
172
|
-
...getClaimValues(claims.scp),
|
|
173
|
-
...getClaimValues(claims.groups),
|
|
174
|
-
...getClaimValues(claims.roles),
|
|
175
|
-
]);
|
|
176
|
-
|
|
177
|
-
if (!values.has(requiredScope)) {
|
|
178
|
-
throw new Error("JWT does not include the required authoring scope.");
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function getClaimValues(value: unknown): string[] {
|
|
183
|
-
if (Array.isArray(value)) return value.flatMap(getClaimValues);
|
|
184
|
-
if (typeof value === "string") return value.split(/[,\s]+/).filter(Boolean);
|
|
185
|
-
return [];
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function constantTimeEqual(left: string, right: string): boolean {
|
|
189
|
-
const leftBuffer = Buffer.from(left);
|
|
190
|
-
const rightBuffer = Buffer.from(right);
|
|
191
|
-
if (leftBuffer.length !== rightBuffer.length) return false;
|
|
192
|
-
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
193
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { defineFunction, secret } from "@aws-amplify/backend";
|
|
2
|
-
export const graphqlJwtAuthorizer = defineFunction({
|
|
3
|
-
name: "papyrus-graphql-jwt-authorizer",
|
|
4
|
-
entry: "./handler.ts",
|
|
5
|
-
timeoutSeconds: 5,
|
|
6
|
-
memoryMB: 256,
|
|
7
|
-
resourceGroupName: "data",
|
|
8
|
-
environment: {
|
|
9
|
-
PAPYRUS_JWT_SECRET: secret("PAPYRUS_JWT_SECRET"),
|
|
10
|
-
PAPYRUS_JWT_ISSUER: "papyrus-cli",
|
|
11
|
-
PAPYRUS_JWT_AUDIENCE: "papyrus-authoring",
|
|
12
|
-
PAPYRUS_JWT_REQUIRED_SCOPE: "papyrus:write",
|
|
13
|
-
},
|
|
14
|
-
});
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { defineFunction, secret } from "@aws-amplify/backend";
|
|
2
|
-
|
|
3
|
-
export const graphqlJwtAuthorizer = defineFunction({
|
|
4
|
-
name: "papyrus-graphql-jwt-authorizer",
|
|
5
|
-
entry: "./handler.ts",
|
|
6
|
-
timeoutSeconds: 5,
|
|
7
|
-
memoryMB: 256,
|
|
8
|
-
resourceGroupName: "data",
|
|
9
|
-
environment: {
|
|
10
|
-
PAPYRUS_JWT_SECRET: secret("PAPYRUS_JWT_SECRET"),
|
|
11
|
-
PAPYRUS_JWT_ISSUER: "papyrus-cli",
|
|
12
|
-
PAPYRUS_JWT_AUDIENCE: "papyrus-authoring",
|
|
13
|
-
PAPYRUS_JWT_REQUIRED_SCOPE: "papyrus:write",
|
|
14
|
-
},
|
|
15
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { SiteBrand } from "../../lib/site-brand";
|
|
2
|
-
import { ANTH_US_THEME_PACK_TOKENS } from "../../lib/site-stack";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Site definition for the Anth.us publication on Papyrus.
|
|
6
|
-
*
|
|
7
|
-
* This is the **site**: masthead, theme pack, corpus wiring, deployment
|
|
8
|
-
* metadata. It is a sibling of `publications/anthus/`, which is the
|
|
9
|
-
* **editorial identity** (`style-profile.yml`, `editorial-rewrite-skill.yml`,
|
|
10
|
-
* `reference-samples/`) and is not touched by this file.
|
|
11
|
-
*
|
|
12
|
-
* Every string below is read off the live Gatsby site at `AnthusAI/Anth.us`:
|
|
13
|
-
* `gatsby-config.mjs` `siteMetadata` for the title/description/author,
|
|
14
|
-
* `src/components/header.js` for the wordmark, `gatsby-plugin-manifest` for
|
|
15
|
-
* "Anth.us AI Solutions", and `src/components/seo.js` for the
|
|
16
|
-
* `"<page> | Anthus"` title pattern.
|
|
17
|
-
*/
|
|
18
|
-
export const anthUsBrand: SiteBrand = {
|
|
19
|
-
id: "anth-us",
|
|
20
|
-
appTitle: "Anthus",
|
|
21
|
-
appDescription:
|
|
22
|
-
"Anthus builds and operates self-aligning AI systems — custom models, agent harnesses, and evaluation loops with a human in the loop, grounded in 14 years of production operations.",
|
|
23
|
-
// The live header renders the bare word "Anthus" (Jersey 25, weight 900),
|
|
24
|
-
// not a dotted domain wordmark, so this is not "ANTH.US".
|
|
25
|
-
mastheadTitle: "Anthus",
|
|
26
|
-
mastheadSubtitle: "AI Solutions",
|
|
27
|
-
backToHomeLabel: "Back to Anthus",
|
|
28
|
-
articleTitleSuffix: "Anthus",
|
|
29
|
-
placeholderByline: "Ryan Porter",
|
|
30
|
-
// The source site is a blog with a long single-column measure, not a
|
|
31
|
-
// newsprint grid.
|
|
32
|
-
defaultPresentation: "blog",
|
|
33
|
-
textFont:
|
|
34
|
-
'"Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
|
|
35
|
-
mastheadWordSplit: false,
|
|
36
|
-
mastheadDateFormat: "formatted",
|
|
37
|
-
mastheadSource: "brand",
|
|
38
|
-
sectionLinkStrategy: "route",
|
|
39
|
-
themePack: "anth-us",
|
|
40
|
-
themeTokens: ANTH_US_THEME_PACK_TOKENS,
|
|
41
|
-
renderer: { kind: "markus" },
|
|
42
|
-
hosting: { kind: "amplify-ssr" },
|
|
43
|
-
readerDeployment: {
|
|
44
|
-
kind: "amplify-static",
|
|
45
|
-
repository: "https://github.com/AnthusAI/Anth.us-Papyrus",
|
|
46
|
-
// `domain` and `amplifyAppId` are deliberately unset.
|
|
47
|
-
//
|
|
48
|
-
// This reader is a **staging** port of a live marketing site. Production
|
|
49
|
-
// `anth.us` stays pointed at the existing Gatsby build until the port has
|
|
50
|
-
// been reviewed, so writing `domain: "anth.us"` here would assert a
|
|
51
|
-
// canonical host this build does not serve. Staging runs on whatever
|
|
52
|
-
// hostname Amplify hands out. No DNS or domain association is configured
|
|
53
|
-
// anywhere in this repo for it.
|
|
54
|
-
//
|
|
55
|
-
// The repository is private while the port is half-finished; the URL above
|
|
56
|
-
// will 404 for anyone without access. That is expected, not a typo.
|
|
57
|
-
},
|
|
58
|
-
rootRoute: { kind: "newsroom" },
|
|
59
|
-
newsroomBasePath: "",
|
|
60
|
-
opsChrome: "app",
|
|
61
|
-
corpusKey: "anth-us",
|
|
62
|
-
// Declared paths. The YAML files themselves are not in the repo yet — see
|
|
63
|
-
// README.md "Not wired yet".
|
|
64
|
-
steeringConfigPath: "corpora/anth-us-steering.yml",
|
|
65
|
-
newsroomSectionsConfigPath: "corpora/anth-us-newsroom-sections.yml",
|
|
66
|
-
analysisProfilesPath: "corpora/anth-us-analysis-profiles.yml",
|
|
67
|
-
publicationName: "Anthus",
|
|
68
|
-
};
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/* Anthus theme pack — colors only.
|
|
2
|
-
Hex values are read off the live Gatsby site at AnthusAI/Anth.us
|
|
3
|
-
(`src/styles/variables.scss` `:root`). That file is not loaded at runtime.
|
|
4
|
-
Shadcn owns typography. Do not set a font stack here.
|
|
5
|
-
|
|
6
|
-
Slot-by-slot provenance (including the three derived values) is documented
|
|
7
|
-
on ANTH_US_THEME_PACK_TOKENS in lib/site-stack.ts — keep the two in sync.
|
|
8
|
-
|
|
9
|
-
NO DARK BLOCK ON PURPOSE. The source site has no dark mode: its only
|
|
10
|
-
`prefers-color-scheme: dark` rule swaps two code-highlight tints. Porting a
|
|
11
|
-
light-only publication means pinning `color-scheme: light` rather than
|
|
12
|
-
inventing a dark palette. If Anthus later designs one, add it here and to
|
|
13
|
-
`ANTH_US_THEME_PACK_TOKENS.dark` together. */
|
|
14
|
-
|
|
15
|
-
:root[data-theme-pack="anth-us"],
|
|
16
|
-
:root[data-site-brand="anth-us"] {
|
|
17
|
-
color-scheme: light;
|
|
18
|
-
--theme-ink: #333333;
|
|
19
|
-
--theme-muted: #5f6b73;
|
|
20
|
-
--theme-paper: #f1f9fe;
|
|
21
|
-
--theme-card: #ffffff;
|
|
22
|
-
--theme-line: #cccccc;
|
|
23
|
-
--theme-moss: #dc5497;
|
|
24
|
-
--theme-ochre: #ff57b4;
|
|
25
|
-
--theme-quote: #9b165d;
|
|
26
|
-
--theme-tip: #0389d7;
|
|
27
|
-
--theme-caution: #e95800;
|
|
28
|
-
--theme-stage: #fedded;
|
|
29
|
-
--background: var(--theme-paper);
|
|
30
|
-
--foreground: var(--theme-ink);
|
|
31
|
-
--foreground-strong: var(--theme-ink);
|
|
32
|
-
--foreground-muted: var(--theme-muted);
|
|
33
|
-
--card: var(--theme-card);
|
|
34
|
-
--card-foreground: var(--theme-ink);
|
|
35
|
-
--popover: var(--theme-card);
|
|
36
|
-
--popover-foreground: var(--theme-ink);
|
|
37
|
-
--primary: var(--theme-moss);
|
|
38
|
-
--primary-foreground: var(--theme-card);
|
|
39
|
-
--secondary: var(--theme-stage);
|
|
40
|
-
--secondary-foreground: var(--theme-ink);
|
|
41
|
-
--muted: var(--theme-stage);
|
|
42
|
-
--muted-foreground: var(--theme-muted);
|
|
43
|
-
--accent: var(--theme-ochre);
|
|
44
|
-
--accent-foreground: var(--theme-card);
|
|
45
|
-
--border: var(--theme-line);
|
|
46
|
-
--input: var(--theme-line);
|
|
47
|
-
--ring: var(--theme-moss);
|
|
48
|
-
--destructive: var(--theme-caution);
|
|
49
|
-
--destructive-foreground: var(--theme-card);
|
|
50
|
-
--danger: var(--theme-caution);
|
|
51
|
-
--danger-foreground: var(--theme-card);
|
|
52
|
-
--surface: var(--theme-card);
|
|
53
|
-
--surface-foreground: var(--theme-ink);
|
|
54
|
-
--paper: var(--theme-paper);
|
|
55
|
-
--ink: var(--theme-ink);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/* The source site is light-only, so a dark-preferring browser must not be
|
|
59
|
-
handed a half-dark Anthus. Re-assert the light tokens inside the dark media
|
|
60
|
-
query for parity with how the other packs are structured. */
|
|
61
|
-
@media (prefers-color-scheme: dark) {
|
|
62
|
-
:root[data-theme-pack="anth-us"],
|
|
63
|
-
:root[data-site-brand="anth-us"] {
|
|
64
|
-
color-scheme: light;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { SiteBrand } from "../../lib/site-brand";
|
|
2
|
-
import { PILOBOL_US_THEME_PACK_TOKENS } from "../../lib/site-stack";
|
|
3
|
-
|
|
4
|
-
export const pilobolUsBrand: SiteBrand = {
|
|
5
|
-
id: "pilobol-us",
|
|
6
|
-
appTitle: "Pilobolus",
|
|
7
|
-
appDescription: "Pilobolus publication on Papyrus (Pilobol.us).",
|
|
8
|
-
mastheadTitle: "PILOBOL.US",
|
|
9
|
-
mastheadSubtitle: "Field notes",
|
|
10
|
-
backToHomeLabel: "Back to Pilobolus",
|
|
11
|
-
articleTitleSuffix: "Pilobolus",
|
|
12
|
-
placeholderByline: "Pilobolus",
|
|
13
|
-
defaultPresentation: "magazine",
|
|
14
|
-
textFont: 'ui-sans-serif, system-ui, "Plus Jakarta Sans", sans-serif',
|
|
15
|
-
mastheadWordSplit: false,
|
|
16
|
-
mastheadDateFormat: "formatted",
|
|
17
|
-
mastheadSource: "brand",
|
|
18
|
-
sectionLinkStrategy: "route",
|
|
19
|
-
themePack: "pilobol-us",
|
|
20
|
-
themeTokens: PILOBOL_US_THEME_PACK_TOKENS,
|
|
21
|
-
renderer: { kind: "markus" },
|
|
22
|
-
hosting: { kind: "amplify-ssr" },
|
|
23
|
-
readerDeployment: {
|
|
24
|
-
kind: "amplify-static",
|
|
25
|
-
repository: "https://github.com/AnthusAI/Pilobol.us",
|
|
26
|
-
domain: "pilobol.us",
|
|
27
|
-
amplifyAppId: "d1od6t7lzbwanr",
|
|
28
|
-
},
|
|
29
|
-
rootRoute: { kind: "newsroom" },
|
|
30
|
-
newsroomBasePath: "",
|
|
31
|
-
opsChrome: "app",
|
|
32
|
-
corpusKey: "pilobol-us",
|
|
33
|
-
steeringConfigPath: "corpora/pilobol-us-steering.yml",
|
|
34
|
-
newsroomSectionsConfigPath: "corpora/pilobol-us-newsroom-sections.yml",
|
|
35
|
-
analysisProfilesPath: "corpora/pilobol-us-analysis-profiles.yml",
|
|
36
|
-
publicationName: "Pilobolus",
|
|
37
|
-
};
|