@anthusai/papyrus 1.0.0-next.61 → 1.0.0-next.62

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.
@@ -0,0 +1,11 @@
1
+ import { NextResponse } from "next/server";
2
+ import { approvedPrimingProofReport } from "../../../../lib/priming-proof-report";
3
+ import { SITE_BRAND } from "../../../../lib/site-brand";
4
+
5
+ export function GET() {
6
+ const report = approvedPrimingProofReport(SITE_BRAND.id);
7
+ if (report === null) {
8
+ return NextResponse.json({ error: "No approved priming proof report for this publication yet." }, { status: 404 });
9
+ }
10
+ return NextResponse.json(report);
11
+ }
@@ -0,0 +1,13 @@
1
+ import type { Metadata } from "next";
2
+ import { PrimingProofPage } from "../../components/priming-proof-page";
3
+ import { approvedPrimingProofReport } from "../../lib/priming-proof-report";
4
+ import { SITE_BRAND } from "../../lib/site-brand";
5
+
6
+ export const metadata: Metadata = {
7
+ title: `Cyclotron proof | ${SITE_BRAND.articleTitleSuffix}`,
8
+ description: "How often the relevance classifier agrees with editors on a fixed held-out set, before and after editor labels.",
9
+ };
10
+
11
+ export default function CyclotronProof() {
12
+ return <PrimingProofPage report={approvedPrimingProofReport(SITE_BRAND.id)} />;
13
+ }
@@ -0,0 +1,92 @@
1
+ import {
2
+ formatProofPercentage,
3
+ primingProofHeadline,
4
+ type PrimingProofReport,
5
+ } from "../lib/priming-proof-report";
6
+
7
+ function ProofMethod({ heldOutSize }: { heldOutSize: number | null }) {
8
+ return (
9
+ <section aria-labelledby="priming-proof-method" className="space-y-2 text-sm leading-relaxed">
10
+ <h2 id="priming-proof-method" className="text-base font-semibold uppercase tracking-wide">How the number is made</h2>
11
+ <p>
12
+ Editors already decided which candidate sources belong in this publication. A fixed, balanced sample of those
13
+ decisions{heldOutSize === null ? "" : ` (${heldOutSize} sources)`} is frozen as the held-out set and never used
14
+ for teaching. A classifier that always gives the same answer agrees on half of it.
15
+ </p>
16
+ <p>
17
+ Editors teach the classifier by pasting a source URL, reviewing what Papyrus extracted, and choosing publish or
18
+ reject. Each point below starts a fresh classifier that sees only the first N of those labels, then decides
19
+ every held-out source once. Nothing else changes between points: the same decision model, the publication&apos;s
20
+ mission and policies as the only rubric, and no automatic rubric rewriting.
21
+ </p>
22
+ </section>
23
+ );
24
+ }
25
+
26
+ export function PrimingProofPage({ report }: { report: PrimingProofReport | null }) {
27
+ if (report === null) {
28
+ return (
29
+ <main className="mx-auto max-w-3xl space-y-6 px-4 py-10" data-priming-proof="pending">
30
+ <header className="space-y-2 border-b pb-4">
31
+ <h1 className="text-3xl font-bold">Does teaching the classifier work?</h1>
32
+ <p className="text-muted-foreground">No approved proof run has been published for this publication yet.</p>
33
+ </header>
34
+ <ProofMethod heldOutSize={null} />
35
+ </main>
36
+ );
37
+ }
38
+ const { baseline, latest, agreementChangeInPoints } = primingProofHeadline(report);
39
+ const [positiveLabel, negativeLabel] = [report.positiveLabel, report.labels.find((label) => label !== report.positiveLabel)];
40
+ return (
41
+ <main className="mx-auto max-w-3xl space-y-8 px-4 py-10" data-priming-proof="published">
42
+ <header className="space-y-3 border-b pb-4">
43
+ <h1 className="text-3xl font-bold">Does teaching the classifier work?</h1>
44
+ <p className="text-lg" data-priming-proof-headline>
45
+ Agreement with editors went from <strong>{formatProofPercentage(baseline.agreement)}</strong> with no labels to{" "}
46
+ <strong>{formatProofPercentage(latest.agreement)}</strong> after {latest.labels} labels
47
+ {agreementChangeInPoints === null ? "" : ` (${agreementChangeInPoints >= 0 ? "+" : ""}${agreementChangeInPoints} points)`}.
48
+ </p>
49
+ <p className="text-sm text-muted-foreground">
50
+ {report.publication} · question: “{report.question}” · held-out set of {report.heldOut.size} sources frozen{" "}
51
+ {report.heldOut.frozenAt.slice(0, 10)} · run {report.ranAt.slice(0, 10)}
52
+ {report.decisionModel ? ` · decision model ${report.decisionModel}` : ""}
53
+ </p>
54
+ </header>
55
+ <section aria-labelledby="priming-proof-points" className="space-y-2">
56
+ <h2 id="priming-proof-points" className="text-base font-semibold uppercase tracking-wide">Before and after</h2>
57
+ <div className="overflow-x-auto">
58
+ <table className="w-full text-left text-sm tabular-nums">
59
+ <thead>
60
+ <tr className="border-b">
61
+ <th scope="col" className="py-2 pr-4">Editor labels</th>
62
+ <th scope="col" className="py-2 pr-4">Agreement</th>
63
+ <th scope="col" className="py-2 pr-4">Precision ({positiveLabel})</th>
64
+ <th scope="col" className="py-2 pr-4">Recall ({positiveLabel})</th>
65
+ <th scope="col" className="py-2">Labels used</th>
66
+ </tr>
67
+ </thead>
68
+ <tbody>
69
+ {report.points.map((point) => (
70
+ <tr key={point.labels} className="border-b" data-priming-proof-point={point.labels}>
71
+ <td className="py-2 pr-4">{point.labels}</td>
72
+ <td className="py-2 pr-4">{formatProofPercentage(point.agreement)}</td>
73
+ <td className="py-2 pr-4">{formatProofPercentage(point.precision)}</td>
74
+ <td className="py-2 pr-4">{formatProofPercentage(point.recall)}</td>
75
+ <td className="py-2">
76
+ {point.labelsByClass[positiveLabel] ?? 0} {positiveLabel}
77
+ {negativeLabel ? `, ${point.labelsByClass[negativeLabel] ?? 0} ${negativeLabel}` : ""}
78
+ </td>
79
+ </tr>
80
+ ))}
81
+ </tbody>
82
+ </table>
83
+ </div>
84
+ </section>
85
+ <ProofMethod heldOutSize={report.heldOut.size} />
86
+ <p className="break-all text-xs text-muted-foreground">
87
+ Held-out fingerprint {report.heldOut.fingerprint}
88
+ {report.priming.fingerprint ? ` · labels fingerprint ${report.priming.fingerprint}` : ""}
89
+ </p>
90
+ </main>
91
+ );
92
+ }
@@ -3,7 +3,7 @@ import * as iam from "aws-cdk-lib/aws-iam";
3
3
  import * as amplify from "aws-cdk-lib/aws-amplify";
4
4
  import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider.js";
5
5
  import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs.js";
6
- import { hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
6
+ import { PROSPECTUS_PAPYRUS_SITE_ROUTES, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveProspectusAccountsEndpoint, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
7
7
  function environmentVariables(variables) {
8
8
  return Object.entries(variables).map(([name, value]) => ({ name, value }));
9
9
  }
@@ -85,6 +85,17 @@ export class AmplifyAppShellStack extends Stack {
85
85
  ],
86
86
  }));
87
87
  }
88
+ const prospectusAccountsApiStageArn = config.cms.prospectusAccountsApiStageArn;
89
+ const prospectusEnvironment = prospectusAccountsApiStageArn
90
+ ? { PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT: resolveProspectusAccountsEndpoint(prospectusAccountsApiStageArn) }
91
+ : {};
92
+ if (prospectusAccountsApiStageArn) {
93
+ computeRole.addToPolicy(new iam.PolicyStatement({
94
+ sid: "InvokeProspectusAccountsAsRegisteredSite",
95
+ actions: ["execute-api:Invoke"],
96
+ resources: PROSPECTUS_PAPYRUS_SITE_ROUTES.map((route) => `${prospectusAccountsApiStageArn}/${route}`),
97
+ }));
98
+ }
88
99
  const cmsApp = new amplify.CfnApp(this, "App", {
89
100
  name: cmsAppName,
90
101
  description: `Papyrus newsroom CMS for ${config.siteId} (WEB_COMPUTE)`,
@@ -134,6 +145,7 @@ export class AmplifyAppShellStack extends Stack {
134
145
  SITE_ENV: "production",
135
146
  PAPYRUS_CONTENT_SOURCE: "published",
136
147
  ...(revalidateSecretParameterName ? { PAPYRUS_REVALIDATE_SECRET_PARAMETER: revalidateSecretParameterName } : {}),
148
+ ...prospectusEnvironment,
137
149
  }),
138
150
  });
139
151
  let stagingBranch;
@@ -153,6 +165,7 @@ export class AmplifyAppShellStack extends Stack {
153
165
  PAPYRUS_SITE_BRAND: config.brand,
154
166
  SITE_ENV: "staging",
155
167
  ...stagingMode,
168
+ ...prospectusEnvironment,
156
169
  NEXT_PUBLIC_PAPYRUS_STAGING_URL: stagingDomainName
157
170
  ? `https://${stagingDomainName}`
158
171
  : Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]),
@@ -191,6 +204,10 @@ export class AmplifyAppShellStack extends Stack {
191
204
  this.cmsAppId = cmsApp.attrAppId;
192
205
  new CfnOutput(this, "CmsAppId", { value: cmsApp.attrAppId, description: `Amplify app id for ${cmsAppName}` });
193
206
  new CfnOutput(this, "CmsOrigin", { value: productionOrigin });
207
+ new CfnOutput(this, "CmsComputeRoleArn", {
208
+ value: computeRole.roleArn,
209
+ description: `SSR compute role for ${cmsAppName}; the roleArn of its Prospectus site registration`,
210
+ });
194
211
  if (stagingEnabled) {
195
212
  new CfnOutput(this, "StagingOrigin", { value: stagingOrigin });
196
213
  }
@@ -4,7 +4,7 @@ import * as iam from "aws-cdk-lib/aws-iam";
4
4
  import * as amplify from "aws-cdk-lib/aws-amplify";
5
5
  import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider";
6
6
  import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs";
7
- import { AmplifyAppShellSiteConfig, ReaderCustomHeaderRule, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
7
+ import { AmplifyAppShellSiteConfig, PROSPECTUS_PAPYRUS_SITE_ROUTES, ReaderCustomHeaderRule, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveProspectusAccountsEndpoint, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
8
8
 
9
9
  function environmentVariables(variables: Record<string, string>): amplify.CfnBranch.EnvironmentVariableProperty[] {
10
10
  return Object.entries(variables).map(([name, value]) => ({ name, value }));
@@ -96,6 +96,20 @@ export class AmplifyAppShellStack extends Stack {
96
96
  );
97
97
  }
98
98
 
99
+ const prospectusAccountsApiStageArn = config.cms.prospectusAccountsApiStageArn;
100
+ const prospectusEnvironment = prospectusAccountsApiStageArn
101
+ ? { PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT: resolveProspectusAccountsEndpoint(prospectusAccountsApiStageArn) }
102
+ : {};
103
+ if (prospectusAccountsApiStageArn) {
104
+ computeRole.addToPolicy(
105
+ new iam.PolicyStatement({
106
+ sid: "InvokeProspectusAccountsAsRegisteredSite",
107
+ actions: ["execute-api:Invoke"],
108
+ resources: PROSPECTUS_PAPYRUS_SITE_ROUTES.map((route) => `${prospectusAccountsApiStageArn}/${route}`),
109
+ }),
110
+ );
111
+ }
112
+
99
113
  const cmsApp = new amplify.CfnApp(this, "App", {
100
114
  name: cmsAppName,
101
115
  description: `Papyrus newsroom CMS for ${config.siteId} (WEB_COMPUTE)`,
@@ -148,6 +162,7 @@ export class AmplifyAppShellStack extends Stack {
148
162
  SITE_ENV: "production",
149
163
  PAPYRUS_CONTENT_SOURCE: "published",
150
164
  ...(revalidateSecretParameterName ? { PAPYRUS_REVALIDATE_SECRET_PARAMETER: revalidateSecretParameterName } : {}),
165
+ ...prospectusEnvironment,
151
166
  }),
152
167
  });
153
168
 
@@ -168,6 +183,7 @@ export class AmplifyAppShellStack extends Stack {
168
183
  PAPYRUS_SITE_BRAND: config.brand,
169
184
  SITE_ENV: "staging",
170
185
  ...stagingMode,
186
+ ...prospectusEnvironment,
171
187
  NEXT_PUBLIC_PAPYRUS_STAGING_URL: stagingDomainName
172
188
  ? `https://${stagingDomainName}`
173
189
  : Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]),
@@ -209,6 +225,10 @@ export class AmplifyAppShellStack extends Stack {
209
225
  this.cmsAppId = cmsApp.attrAppId;
210
226
  new CfnOutput(this, "CmsAppId", { value: cmsApp.attrAppId, description: `Amplify app id for ${cmsAppName}` });
211
227
  new CfnOutput(this, "CmsOrigin", { value: productionOrigin });
228
+ new CfnOutput(this, "CmsComputeRoleArn", {
229
+ value: computeRole.roleArn,
230
+ description: `SSR compute role for ${cmsAppName}; the roleArn of its Prospectus site registration`,
231
+ });
212
232
  if (stagingEnabled) {
213
233
  new CfnOutput(this, "StagingOrigin", { value: stagingOrigin });
214
234
  }
@@ -16,7 +16,24 @@ const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}
16
16
  const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
17
17
  const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
18
18
  const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
19
- const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER"];
19
+ const PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN = /^arn:aws:execute-api:[a-z0-9-]+:\d{12}:[a-z0-9]+\/api$/;
20
+ const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT"];
21
+ /** Routes Prospectus admits for a registered Papyrus site (Prospectus accounts/papyrus-sites.ts). */
22
+ export const PROSPECTUS_PAPYRUS_SITE_ROUTES = [
23
+ "GET/v1/identities/me",
24
+ "POST/v1/identities/google/challenge",
25
+ "POST/v1/identities/google/resolve",
26
+ "GET/v1/accounts",
27
+ "GET/v1/accounts/*",
28
+ "POST/v1/accounts/*/authorize",
29
+ "GET/v1/account-directory",
30
+ "GET/v1/account-directory/*",
31
+ ];
32
+ export function resolveProspectusAccountsEndpoint(prospectusAccountsApiStageArn) {
33
+ const [, , , region, , apiPath] = prospectusAccountsApiStageArn.split(":");
34
+ const [apiId, stageName] = apiPath.split("/");
35
+ return `https://${apiId}.execute-api.${region}.amazonaws.com/${stageName}/`;
36
+ }
20
37
  function originOfRedirectUrl(value) {
21
38
  try {
22
39
  return new URL(value).origin;
@@ -181,7 +198,7 @@ function parseStagingPreview(value) {
181
198
  }
182
199
  function parseCms(value) {
183
200
  const cmsRecord = requireObject(value, "cms");
184
- rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview"], "cms");
201
+ rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn"], "cms");
185
202
  const buildComputeType = cmsRecord.buildComputeType;
186
203
  if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
187
204
  fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
@@ -205,6 +222,7 @@ function parseCms(value) {
205
222
  stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
206
223
  redirects: parseRedirects(cmsRecord.redirects),
207
224
  stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
225
+ prospectusAccountsApiStageArn: optionalString(cmsRecord.prospectusAccountsApiStageArn, "cms.prospectusAccountsApiStageArn", PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN, "arn:aws:execute-api:<region>:<account>:<api id>/api"),
208
226
  };
209
227
  const reservedWord = COGNITO_RESERVED_WORDS.find((word) => cms.cognitoDomainPrefix.includes(word));
210
228
  if (reservedWord) {
@@ -212,7 +230,7 @@ function parseCms(value) {
212
230
  }
213
231
  for (const key of TEMPLATE_MANAGED_ENVIRONMENT_KEYS) {
214
232
  if (key in cms.environment) {
215
- fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix and the revalidation secret parameter is derived from siteId");
233
+ fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix, the revalidation secret parameter is derived from siteId and the Prospectus endpoint comes from cms.prospectusAccountsApiStageArn");
216
234
  }
217
235
  }
218
236
  if (cms.staging === false && cms.stagingDomainName) {
@@ -41,6 +41,7 @@ export type CmsConfig = {
41
41
  stagingDomainName?: string;
42
42
  stagingDomainPrefix?: string;
43
43
  redirects?: CmsRedirect[];
44
+ prospectusAccountsApiStageArn?: string;
44
45
  };
45
46
 
46
47
  export type AmplifyAppShellSiteConfig = {
@@ -95,7 +96,26 @@ const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}
95
96
  const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
96
97
  const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
97
98
  const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
98
- const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER"];
99
+ const PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN = /^arn:aws:execute-api:[a-z0-9-]+:\d{12}:[a-z0-9]+\/api$/;
100
+ const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT"];
101
+
102
+ /** Routes Prospectus admits for a registered Papyrus site (Prospectus accounts/papyrus-sites.ts). */
103
+ export const PROSPECTUS_PAPYRUS_SITE_ROUTES = [
104
+ "GET/v1/identities/me",
105
+ "POST/v1/identities/google/challenge",
106
+ "POST/v1/identities/google/resolve",
107
+ "GET/v1/accounts",
108
+ "GET/v1/accounts/*",
109
+ "POST/v1/accounts/*/authorize",
110
+ "GET/v1/account-directory",
111
+ "GET/v1/account-directory/*",
112
+ ];
113
+
114
+ export function resolveProspectusAccountsEndpoint(prospectusAccountsApiStageArn: string): string {
115
+ const [, , , region, , apiPath] = prospectusAccountsApiStageArn.split(":");
116
+ const [apiId, stageName] = apiPath.split("/");
117
+ return `https://${apiId}.execute-api.${region}.amazonaws.com/${stageName}/`;
118
+ }
99
119
  function originOfRedirectUrl(value: string): string {
100
120
  try {
101
121
  return new URL(value).origin;
@@ -259,7 +279,7 @@ function parseStagingPreview(value: unknown): StagingPreviewConfig | undefined {
259
279
 
260
280
  function parseCms(value: unknown): CmsConfig {
261
281
  const cmsRecord = requireObject(value, "cms");
262
- rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview"], "cms");
282
+ rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn"], "cms");
263
283
  const buildComputeType = cmsRecord.buildComputeType;
264
284
  if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
265
285
  fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
@@ -288,6 +308,12 @@ function parseCms(value: unknown): CmsConfig {
288
308
  stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
289
309
  redirects: parseRedirects(cmsRecord.redirects),
290
310
  stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
311
+ prospectusAccountsApiStageArn: optionalString(
312
+ cmsRecord.prospectusAccountsApiStageArn,
313
+ "cms.prospectusAccountsApiStageArn",
314
+ PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN,
315
+ "arn:aws:execute-api:<region>:<account>:<api id>/api",
316
+ ),
291
317
  };
292
318
 
293
319
  const reservedWord = COGNITO_RESERVED_WORDS.find((word) => cms.cognitoDomainPrefix.includes(word));
@@ -296,7 +322,7 @@ function parseCms(value: unknown): CmsConfig {
296
322
  }
297
323
  for (const key of TEMPLATE_MANAGED_ENVIRONMENT_KEYS) {
298
324
  if (key in cms.environment) {
299
- fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix and the revalidation secret parameter is derived from siteId");
325
+ fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix, the revalidation secret parameter is derived from siteId and the Prospectus endpoint comes from cms.prospectusAccountsApiStageArn");
300
326
  }
301
327
  }
302
328
 
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Priming proof reports a publication owner approved for visitors, keyed by site brand id.
3
+ * A report reaches this map only through a reviewed commit of its
4
+ * `publications/priming-proofs/<brand id>.json` file; see docs/priming-proof.md.
5
+ */
6
+ export const APPROVED_PRIMING_PROOF_REPORTS: Record<string, unknown> = {};
@@ -0,0 +1,89 @@
1
+ import { z } from "zod";
2
+ import { APPROVED_PRIMING_PROOF_REPORTS } from "./approved-priming-proof-reports";
3
+
4
+ export const PRIMING_PROOF_REPORT_VERSION = "cyclotron-priming-proof/v1";
5
+
6
+ const nullableRatio = z.number().min(0).max(1).nullable();
7
+
8
+ const primingProofPointSchema = z.object({
9
+ labels: z.number().int().min(0),
10
+ labelsByClass: z.record(z.string(), z.number().int().min(0)),
11
+ decided: z.number().int().min(0),
12
+ agreement: nullableRatio,
13
+ precision: nullableRatio,
14
+ recall: nullableRatio,
15
+ confusion: z.object({
16
+ truePositives: z.number().int().min(0),
17
+ falsePositives: z.number().int().min(0),
18
+ trueNegatives: z.number().int().min(0),
19
+ falseNegatives: z.number().int().min(0),
20
+ }),
21
+ });
22
+
23
+ export const primingProofReportSchema = z.object({
24
+ version: z.literal(PRIMING_PROOF_REPORT_VERSION),
25
+ proofId: z.string().min(1),
26
+ publication: z.string().min(1),
27
+ cyclotronId: z.string().min(1),
28
+ question: z.string().min(1),
29
+ labels: z.array(z.string().min(1)).length(2),
30
+ positiveLabel: z.string().min(1),
31
+ decisionModel: z.string().nullable(),
32
+ ranAt: z.string().min(1),
33
+ heldOut: z.object({
34
+ size: z.number().int().min(1),
35
+ byLabel: z.record(z.string(), z.number().int().min(0)),
36
+ fingerprint: z.string().min(1),
37
+ frozenAt: z.string().min(1),
38
+ source: z.string().min(1),
39
+ }),
40
+ priming: z.object({
41
+ acceptedExamples: z.number().int().min(0),
42
+ excludedBecauseInHeldOut: z.number().int().min(0),
43
+ fingerprint: z.string().nullable(),
44
+ order: z.string().min(1),
45
+ }),
46
+ conditions: z.object({
47
+ freshStorePerPoint: z.boolean(),
48
+ rubric: z.string().min(1),
49
+ optimizer: z.boolean(),
50
+ reviewProgram: z.boolean(),
51
+ }),
52
+ points: z.array(primingProofPointSchema).min(1),
53
+ });
54
+
55
+ export type PrimingProofReport = z.infer<typeof primingProofReportSchema>;
56
+ export type PrimingProofPoint = z.infer<typeof primingProofPointSchema>;
57
+
58
+ export type PrimingProofHeadline = {
59
+ baseline: PrimingProofPoint;
60
+ latest: PrimingProofPoint;
61
+ agreementChangeInPoints: number | null;
62
+ };
63
+
64
+ export function parsePrimingProofReport(value: unknown): PrimingProofReport {
65
+ const report = primingProofReportSchema.parse(value);
66
+ if (report.points[0].labels !== 0) {
67
+ throw new Error("A priming proof report must start with the zero-label baseline.");
68
+ }
69
+ return report;
70
+ }
71
+
72
+ export function approvedPrimingProofReport(brandId: string): PrimingProofReport | null {
73
+ const approvedReport = APPROVED_PRIMING_PROOF_REPORTS[brandId];
74
+ return approvedReport === undefined ? null : parsePrimingProofReport(approvedReport);
75
+ }
76
+
77
+ export function primingProofHeadline(report: PrimingProofReport): PrimingProofHeadline {
78
+ const baseline = report.points[0];
79
+ const latest = report.points[report.points.length - 1];
80
+ const agreementChangeInPoints =
81
+ baseline.agreement === null || latest.agreement === null
82
+ ? null
83
+ : Math.round((latest.agreement - baseline.agreement) * 1000) / 10;
84
+ return { baseline, latest, agreementChangeInPoints };
85
+ }
86
+
87
+ export function formatProofPercentage(ratio: number | null): string {
88
+ return ratio === null ? "n/a" : `${Math.round(ratio * 1000) / 10}%`;
89
+ }
@@ -0,0 +1,303 @@
1
+
2
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
3
+ import { defaultProvider } from "@aws-sdk/credential-provider-node";
4
+ import { SignatureV4 } from "@smithy/signature-v4";
5
+ import { HttpRequest } from "@smithy/protocol-http";
6
+ import { Sha256 } from "@aws-crypto/sha256-js";
7
+ import { CognitoJwtVerifier } from "aws-jwt-verify";
8
+ import type { JwksCache } from "aws-jwt-verify/jwk";
9
+
10
+ /**
11
+ * A Papyrus site's server-side view of Prospectus, the single account service.
12
+ *
13
+ * The site signs requests with its own runtime role (the Amplify SSR compute role
14
+ * registered in Prospectus). People sign in through the site's Google-only Cognito
15
+ * client; Prospectus binds that sign-in to a canonical person through a one-time
16
+ * challenge nonce. Papyrus keeps no account or membership tables: every answer here
17
+ * comes from Prospectus, and publication-level grants are never inferred from an
18
+ * account role.
19
+ */
20
+
21
+ export type ProspectusRole = "owner" | "admin" | "member" | "viewer";
22
+ export type ProspectusOperation = "console:read" | "console:write" | "members:read" | "members:write";
23
+
24
+ export interface ProspectusPerson {
25
+ id: string;
26
+ createdAt: string;
27
+ }
28
+
29
+ export interface ProspectusAccountAuthorization {
30
+ allowed: true;
31
+ accountId: string;
32
+ personId: string;
33
+ product: string;
34
+ role: ProspectusRole;
35
+ operation: ProspectusOperation;
36
+ revision: number;
37
+ }
38
+
39
+ export interface ProspectusDirectoryAccount {
40
+ id: string;
41
+ name: string;
42
+ role: ProspectusRole;
43
+ products: Array<{ product: string; status: "active" | "disabled" }>;
44
+ }
45
+
46
+ export interface ProspectusRequest {
47
+ path: string;
48
+ method: "GET" | "POST";
49
+ body?: unknown;
50
+ humanAccessToken?: string;
51
+ identityToken?: string;
52
+ }
53
+
54
+ export type ProspectusTransport = (request: ProspectusRequest) => Promise<unknown>;
55
+
56
+ export class ProspectusAccountsError extends Error {
57
+ constructor(
58
+ readonly status: number,
59
+ readonly code: string,
60
+ message: string,
61
+ ) {
62
+ super(message);
63
+ }
64
+ }
65
+
66
+ export interface TemporaryRuntimeCredentials {
67
+ accessKeyId: string;
68
+ secretAccessKey: string;
69
+ sessionToken?: string;
70
+ expiration?: Date;
71
+ }
72
+
73
+ export interface SignedProspectusTransportOptions {
74
+ endpoint: string;
75
+ region: string;
76
+ runtimeCredentials?: () => Promise<TemporaryRuntimeCredentials>;
77
+ fetcher?: typeof fetch;
78
+ }
79
+
80
+ export function signedProspectusTransport(options: SignedProspectusTransportOptions): ProspectusTransport {
81
+ const endpoint = new URL(options.endpoint);
82
+ if (endpoint.protocol !== "https:" || endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
83
+ throw new Error("Use an HTTPS Prospectus accounts endpoint.");
84
+ }
85
+ const runtimeCredentials = options.runtimeCredentials ?? defaultProvider();
86
+ const fetcher = options.fetcher ?? fetch;
87
+ return async ({ path, method, body, humanAccessToken, identityToken }) => {
88
+ const credentials = await runtimeCredentials();
89
+ if (!credentials.sessionToken || (credentials.expiration && credentials.expiration.getTime() <= Date.now())) {
90
+ throw new Error("Prospectus calls require the site's temporary runtime credentials; static access keys are refused.");
91
+ }
92
+ const url = new URL(`${endpoint.toString().replace(/\/$/, "")}${path}`);
93
+ const payload = body === undefined ? undefined : JSON.stringify(body);
94
+ const headers: Record<string, string> = {
95
+ host: url.host,
96
+ ...(humanAccessToken ? { "x-prospectus-human-token": humanAccessToken } : {}),
97
+ ...(identityToken ? { "x-prospectus-identity-token": identityToken } : {}),
98
+ ...(payload ? { "content-type": "application/json" } : {}),
99
+ };
100
+ const signer = new SignatureV4({ service: "execute-api", region: options.region, credentials, sha256: Sha256 });
101
+ const signed = await signer.sign(
102
+ new HttpRequest({ protocol: url.protocol, hostname: url.hostname, path: url.pathname, method, headers, body: payload }),
103
+ );
104
+ const response = await fetcher(url, { method, headers: signed.headers, body: payload, cache: "no-store", redirect: "error" });
105
+ const responseBody = await response.json().catch(() => ({}));
106
+ if (!response.ok) {
107
+ throw new ProspectusAccountsError(response.status, responseBody.error ?? "request_failed", responseBody.message ?? "Prospectus request failed.");
108
+ }
109
+ return responseBody;
110
+ };
111
+ }
112
+
113
+ export class ProspectusSiteAccounts {
114
+ constructor(private readonly transport: ProspectusTransport) {}
115
+
116
+ requestGoogleSignInChallenge(): Promise<{ challengeId: string; nonce: string; expiresAt: number }> {
117
+ return this.transport({ path: "/v1/identities/google/challenge", method: "POST", body: {} }) as Promise<{
118
+ challengeId: string;
119
+ nonce: string;
120
+ expiresAt: number;
121
+ }>;
122
+ }
123
+
124
+ resolveGoogleSignIn(challengeId: string, humanAccessToken: string, identityToken: string): Promise<{ person: ProspectusPerson }> {
125
+ return this.transport({
126
+ path: "/v1/identities/google/resolve",
127
+ method: "POST",
128
+ body: { challengeId },
129
+ humanAccessToken,
130
+ identityToken,
131
+ }) as Promise<{ person: ProspectusPerson }>;
132
+ }
133
+
134
+ currentPerson(humanAccessToken: string): Promise<{ person: ProspectusPerson | null }> {
135
+ return this.transport({ path: "/v1/identities/me", method: "GET", humanAccessToken }) as Promise<{ person: ProspectusPerson | null }>;
136
+ }
137
+
138
+ authorizeAccountOperation(humanAccessToken: string, accountId: string, operation: ProspectusOperation): Promise<ProspectusAccountAuthorization> {
139
+ return this.transport({
140
+ path: `/v1/accounts/${encodeURIComponent(accountId)}/authorize`,
141
+ method: "POST",
142
+ body: { operation },
143
+ humanAccessToken,
144
+ }) as Promise<ProspectusAccountAuthorization>;
145
+ }
146
+
147
+ accountDirectory(humanAccessToken: string): Promise<{ person: ProspectusPerson | null; accounts: ProspectusDirectoryAccount[] }> {
148
+ return this.transport({ path: "/v1/account-directory", method: "GET", humanAccessToken }) as Promise<{
149
+ person: ProspectusPerson | null;
150
+ accounts: ProspectusDirectoryAccount[];
151
+ }>;
152
+ }
153
+ }
154
+
155
+ export interface GoogleOnlyCognitoClient {
156
+ hostedUiOrigin: string;
157
+ userPoolId: string;
158
+ clientId: string;
159
+ redirectUri: string;
160
+ }
161
+
162
+ export interface PendingProspectusSignIn {
163
+ challengeId: string;
164
+ nonce: string;
165
+ state: string;
166
+ codeVerifier: string;
167
+ expiresAt: number;
168
+ }
169
+
170
+ export interface CognitoTokens {
171
+ accessToken: string;
172
+ idToken: string;
173
+ refreshToken?: string;
174
+ expiresInSeconds: number;
175
+ }
176
+
177
+ export type CognitoTokenVerifier = (tokens: CognitoTokens, expectedNonce: string) => Promise<void>;
178
+
179
+ function base64UrlRandom(byteLength: number): string {
180
+ return randomBytes(byteLength).toString("base64url");
181
+ }
182
+
183
+ function constantTimeEquals(left: string, right: string): boolean {
184
+ const leftBytes = Buffer.from(left);
185
+ const rightBytes = Buffer.from(right);
186
+ return leftBytes.length === rightBytes.length && timingSafeEqual(leftBytes, rightBytes);
187
+ }
188
+
189
+ export async function beginProspectusGoogleSignIn(
190
+ accounts: ProspectusSiteAccounts,
191
+ cognitoClient: GoogleOnlyCognitoClient,
192
+ ): Promise<{ authorizationUrl: string; pendingSignIn: PendingProspectusSignIn }> {
193
+ const challenge = await accounts.requestGoogleSignInChallenge();
194
+ const state = base64UrlRandom(32);
195
+ const codeVerifier = base64UrlRandom(48);
196
+ const authorizationUrl = new URL("/oauth2/authorize", cognitoClient.hostedUiOrigin);
197
+ authorizationUrl.search = new URLSearchParams({
198
+ response_type: "code",
199
+ client_id: cognitoClient.clientId,
200
+ redirect_uri: cognitoClient.redirectUri,
201
+ scope: "openid email profile",
202
+ identity_provider: "Google",
203
+ state,
204
+ nonce: challenge.nonce,
205
+ code_challenge: createHash("sha256").update(codeVerifier).digest("base64url"),
206
+ code_challenge_method: "S256",
207
+ }).toString();
208
+ return {
209
+ authorizationUrl: authorizationUrl.toString(),
210
+ pendingSignIn: { challengeId: challenge.challengeId, nonce: challenge.nonce, state, codeVerifier, expiresAt: challenge.expiresAt },
211
+ };
212
+ }
213
+
214
+ export function cognitoTokenVerifier(cognitoClient: GoogleOnlyCognitoClient, signingKeyCache?: JwksCache): CognitoTokenVerifier {
215
+ const verifierOptions = signingKeyCache ? { jwksCache: signingKeyCache } : undefined;
216
+ const accessTokenVerifier = CognitoJwtVerifier.create({ userPoolId: cognitoClient.userPoolId, tokenUse: "access", clientId: cognitoClient.clientId }, verifierOptions);
217
+ const identityTokenVerifier = CognitoJwtVerifier.create({ userPoolId: cognitoClient.userPoolId, tokenUse: "id", clientId: cognitoClient.clientId }, verifierOptions);
218
+ return async (tokens, expectedNonce) => {
219
+ const accessClaims = await accessTokenVerifier.verify(tokens.accessToken);
220
+ const identityClaims = await identityTokenVerifier.verify(tokens.idToken);
221
+ if (identityClaims.nonce !== expectedNonce) throw new ProspectusAccountsError(401, "nonce_mismatch", "Sign-in did not carry this site's Prospectus challenge.");
222
+ if (identityClaims.sub !== accessClaims.sub) throw new ProspectusAccountsError(401, "token_pair_mismatch", "Access and identity tokens describe different people.");
223
+ };
224
+ }
225
+
226
+ export interface CompleteProspectusGoogleSignInInput {
227
+ accounts: ProspectusSiteAccounts;
228
+ cognitoClient: GoogleOnlyCognitoClient;
229
+ pendingSignIn: PendingProspectusSignIn;
230
+ returnedState: string;
231
+ authorizationCode: string;
232
+ verifyCognitoTokens?: CognitoTokenVerifier;
233
+ fetcher?: typeof fetch;
234
+ nowInSeconds?: number;
235
+ }
236
+
237
+ export async function completeProspectusGoogleSignIn(
238
+ input: CompleteProspectusGoogleSignInInput,
239
+ ): Promise<{ person: ProspectusPerson; accessToken: string; refreshToken?: string; accessTokenExpiresAt: number }> {
240
+ const nowInSeconds = input.nowInSeconds ?? Math.floor(Date.now() / 1000);
241
+ if (nowInSeconds >= input.pendingSignIn.expiresAt) {
242
+ throw new ProspectusAccountsError(401, "sign_in_expired", "Sign-in took too long. Start again.");
243
+ }
244
+ if (!constantTimeEquals(input.returnedState, input.pendingSignIn.state)) {
245
+ throw new ProspectusAccountsError(401, "state_mismatch", "Sign-in response does not belong to this browser's sign-in.");
246
+ }
247
+ const tokenResponse = await (input.fetcher ?? fetch)(new URL("/oauth2/token", input.cognitoClient.hostedUiOrigin), {
248
+ method: "POST",
249
+ headers: { "content-type": "application/x-www-form-urlencoded" },
250
+ body: new URLSearchParams({
251
+ grant_type: "authorization_code",
252
+ client_id: input.cognitoClient.clientId,
253
+ code: input.authorizationCode,
254
+ redirect_uri: input.cognitoClient.redirectUri,
255
+ code_verifier: input.pendingSignIn.codeVerifier,
256
+ }).toString(),
257
+ cache: "no-store",
258
+ redirect: "error",
259
+ });
260
+ const tokenBody = await tokenResponse.json().catch(() => ({}));
261
+ if (!tokenResponse.ok || typeof tokenBody.access_token !== "string" || typeof tokenBody.id_token !== "string") {
262
+ throw new ProspectusAccountsError(401, "code_exchange_failed", "Cognito did not complete the sign-in.");
263
+ }
264
+ const tokens: CognitoTokens = {
265
+ accessToken: tokenBody.access_token,
266
+ idToken: tokenBody.id_token,
267
+ refreshToken: typeof tokenBody.refresh_token === "string" ? tokenBody.refresh_token : undefined,
268
+ expiresInSeconds: Number(tokenBody.expires_in) || 0,
269
+ };
270
+ await (input.verifyCognitoTokens ?? cognitoTokenVerifier(input.cognitoClient))(tokens, input.pendingSignIn.nonce);
271
+ const { person } = await input.accounts.resolveGoogleSignIn(input.pendingSignIn.challengeId, tokens.accessToken, tokens.idToken);
272
+ return {
273
+ person,
274
+ accessToken: tokens.accessToken,
275
+ refreshToken: tokens.refreshToken,
276
+ accessTokenExpiresAt: nowInSeconds + tokens.expiresInSeconds,
277
+ };
278
+ }
279
+
280
+ export async function authorizeSignedInPersonForAccount(
281
+ accounts: ProspectusSiteAccounts,
282
+ humanAccessToken: string,
283
+ accountId: string,
284
+ operation: ProspectusOperation,
285
+ ): Promise<ProspectusAccountAuthorization> {
286
+ const { person } = await accounts.currentPerson(humanAccessToken);
287
+ if (!person) throw new ProspectusAccountsError(401, "unknown_person", "Sign in again: Prospectus does not recognize this session.");
288
+ const authorization = await accounts.authorizeAccountOperation(humanAccessToken, accountId, operation);
289
+ if (authorization.allowed !== true || authorization.personId !== person.id || authorization.accountId !== accountId || authorization.operation !== operation) {
290
+ throw new ProspectusAccountsError(403, "not_authorized", "Prospectus did not authorize this account operation.");
291
+ }
292
+ return authorization;
293
+ }
294
+
295
+ export const CYCLOTRON_LAB_PRODUCT = "cyclotron";
296
+
297
+ export async function signedInPersonHasCyclotronLabAccess(accounts: ProspectusSiteAccounts, humanAccessToken: string): Promise<boolean> {
298
+ const directory = await accounts.accountDirectory(humanAccessToken);
299
+ if (!directory.person) return false;
300
+ return directory.accounts.some((account) =>
301
+ account.products.some((product) => product.product === CYCLOTRON_LAB_PRODUCT && product.status === "active"),
302
+ );
303
+ }
package/package.json CHANGED
@@ -99,6 +99,7 @@
99
99
  "./components/presentation-footer": "./components/presentation-footer.tsx",
100
100
  "./components/presentation-header": "./components/presentation-header.tsx",
101
101
  "./components/presentation-rhythm-hrule": "./components/presentation-rhythm-hrule.tsx",
102
+ "./components/priming-proof-page": "./components/priming-proof-page.tsx",
102
103
  "./components/publication-cyclotron-panel": "./components/publication-cyclotron-panel.tsx",
103
104
  "./components/reader-auth-control": "./components/reader-auth-control.tsx",
104
105
  "./components/reader-auth-state": "./components/reader-auth-state.ts",
@@ -139,6 +140,7 @@
139
140
  "./lib/amplify-outputs-guard": "./lib/amplify-outputs-guard.ts",
140
141
  "./lib/amplify-outputs-path": "./lib/amplify-outputs-path.ts",
141
142
  "./lib/amplify-server-runtime": "./lib/amplify-server-runtime.ts",
143
+ "./lib/approved-priming-proof-reports": "./lib/approved-priming-proof-reports.ts",
142
144
  "./lib/archive-data": "./lib/archive-data.ts",
143
145
  "./lib/archive-types": "./lib/archive-types.ts",
144
146
  "./lib/articles": "./lib/articles.ts",
@@ -205,6 +207,8 @@
205
207
  "./lib/plugin-settings": "./lib/plugin-settings.ts",
206
208
  "./lib/presentation-footer": "./lib/presentation-footer.ts",
207
209
  "./lib/pretext-layout": "./lib/pretext-layout.ts",
210
+ "./lib/priming-proof-report": "./lib/priming-proof-report.ts",
211
+ "./lib/prospectus-site-identity": "./lib/prospectus-site-identity.ts",
208
212
  "./lib/public-placeholder-config": "./lib/public-placeholder-config.ts",
209
213
  "./lib/publication-cyclotron": "./lib/publication-cyclotron.ts",
210
214
  "./lib/publication-items": "./lib/publication-items.ts",
@@ -259,10 +263,12 @@
259
263
  "./app/[year]/[month]/[day]/page/[pageNumber]/page": "./app/[year]/[month]/[day]/page/[pageNumber]/page.tsx",
260
264
  "./app/[year]/[month]/[day]/section/[sectionKey]/page": "./app/[year]/[month]/[day]/section/[sectionKey]/page.tsx",
261
265
  "./app/api/archive/editions/route": "./app/api/archive/editions/route.ts",
266
+ "./app/api/cyclotron/priming-proof/route": "./app/api/cyclotron/priming-proof/route.ts",
262
267
  "./app/api/media/[...path]/route": "./app/api/media/[...path]/route.ts",
263
268
  "./app/api/revalidate/route": "./app/api/revalidate/route.ts",
264
269
  "./app/archive/page": "./app/archive/page.tsx",
265
270
  "./app/articles/[slug]/page": "./app/articles/[slug]/page.tsx",
271
+ "./app/cyclotron/page": "./app/cyclotron/page.tsx",
266
272
  "./app/layout": "./app/layout.tsx",
267
273
  "./app/newsroom/[section]/[...selection]/page": "./app/newsroom/[section]/[...selection]/page.tsx",
268
274
  "./app/newsroom/[section]/page": "./app/newsroom/[section]/page.tsx",
@@ -325,18 +331,22 @@
325
331
  "optional": true
326
332
  }
327
333
  },
328
- "version": "1.0.0-next.61",
334
+ "version": "1.0.0-next.62",
329
335
  "dependencies": {
330
336
  "@aws-amplify/adapter-nextjs": "^1.7.3",
337
+ "@aws-crypto/sha256-js": "5.2.0",
331
338
  "@aws-sdk/client-cognito-identity-provider": "3.1045.0",
332
339
  "@aws-sdk/client-lambda": "^3.1045.0",
333
340
  "@aws-sdk/client-s3": "^3.1045.0",
334
341
  "@aws-sdk/client-ssm": "^3.1045.0",
342
+ "@aws-sdk/credential-provider-node": "3.972.84",
335
343
  "@aws-sdk/s3-request-presigner": "^3.1045.0",
336
344
  "@base-ui/react": "^1.5.0",
337
345
  "@chenglou/pretext": "^0.0.7",
338
346
  "@kanbus/ui": "https://github.com/AnthusAI/Kanbus/releases/download/ui-v0.2.0/kanbus-ui-0.2.0.tgz",
339
347
  "@radix-ui/colors": "3.0.0",
348
+ "@smithy/protocol-http": "5.4.1",
349
+ "@smithy/signature-v4": "5.7.4",
340
350
  "@streamdown/cjk": "^1.0.3",
341
351
  "@streamdown/code": "^1.1.1",
342
352
  "@streamdown/math": "^1.0.2",
@@ -345,6 +355,7 @@
345
355
  "ai": "^6.0.191",
346
356
  "anthus-vultus": "github:AnthusAI/Vultus#af98d57",
347
357
  "aws-amplify": "^6.17.0",
358
+ "aws-jwt-verify": "4.0.1",
348
359
  "class-variance-authority": "^0.7.1",
349
360
  "clsx": "^2.1.1",
350
361
  "cmdk": "^1.1.1",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-next.61",
2
+ "version": "1.0.0-next.62",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",
@@ -66,6 +66,14 @@
66
66
  "revalidate": "60"
67
67
  }
68
68
  },
69
+ {
70
+ "file": "app/api/cyclotron/priming-proof/route.ts",
71
+ "hasDefault": false,
72
+ "exports": [
73
+ "GET"
74
+ ],
75
+ "config": {}
76
+ },
69
77
  {
70
78
  "file": "app/api/media/[...path]/route.ts",
71
79
  "hasDefault": false,
@@ -107,6 +115,14 @@
107
115
  "revalidate": "60"
108
116
  }
109
117
  },
118
+ {
119
+ "file": "app/cyclotron/page.tsx",
120
+ "hasDefault": true,
121
+ "exports": [
122
+ "metadata"
123
+ ],
124
+ "config": {}
125
+ },
110
126
  {
111
127
  "file": "app/layout.tsx",
112
128
  "hasDefault": true,