@anthusai/papyrus 1.0.0-next.1 → 1.0.0-next.3

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.
Files changed (42) hide show
  1. package/amplify/data/resource.js +26 -0
  2. package/amplify/data/resource.ts +30 -0
  3. package/amplify/data/schema.js +26 -0
  4. package/amplify/data/schema.ts +30 -0
  5. package/amplify/functions/content-actions/handler.py +107 -0
  6. package/amplify/functions/content-actions/requirements.txt +4 -0
  7. package/amplify/functions/content-actions/resource.js +40 -0
  8. package/amplify/functions/content-actions/resource.ts +54 -0
  9. package/amplify/functions/shared/python-bundle.js +17 -2
  10. package/amplify/functions/shared/python-bundle.ts +23 -2
  11. package/amplify/site-backend.js +10 -0
  12. package/amplify/site-backend.ts +15 -0
  13. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -2
  14. package/app/globals.css +1 -4
  15. package/app/layout.tsx +4 -12
  16. package/components/news-desk-page.tsx +1 -4
  17. package/lib/content-actions-client.ts +98 -0
  18. package/lib/empty-theme.css +1 -0
  19. package/lib/layout-scenarios.ts +8 -8
  20. package/lib/site-brand-tenant.ts +3 -22
  21. package/lib/site-brand.ts +44 -42
  22. package/middleware.ts +10 -35
  23. package/package.json +2 -1
  24. package/renderers/pretext/article-page.tsx +1 -6
  25. package/renderers/pretext/presentation-shell.tsx +2 -9
  26. package/routes.manifest.json +1 -1
  27. package/src/with-papyrus.mjs +15 -2
  28. package/publications/anth_us/brand.ts +0 -68
  29. package/publications/anth_us/theme.css +0 -66
  30. package/publications/pilobol_us/brand.ts +0 -37
  31. package/publications/pilobol_us/theme.css +0 -140
  32. package/publications/threat_intelligence/blog-defense/graph.ts +0 -636
  33. package/publications/threat_intelligence/blog-defense/layout.ts +0 -934
  34. package/publications/threat_intelligence/blog-defense/page-background.tsx +0 -590
  35. package/publications/threat_intelligence/brand.ts +0 -40
  36. package/publications/threat_intelligence/pictograms/art.tsx +0 -608
  37. package/publications/threat_intelligence/pictograms/figure.tsx +0 -82
  38. package/publications/threat_intelligence/pictograms/registry.ts +0 -30
  39. package/publications/threat_intelligence/pictograms/system.tsx +0 -305
  40. package/publications/threat_intelligence/seed/seed-edition-content.json +0 -629
  41. package/publications/threat_intelligence/theme.css +0 -1440
  42. /package/{publications/papyrus/theme.css → app/papyrus-theme.css} +0 -0
@@ -2,6 +2,7 @@ import { a, defineData } from "@aws-amplify/backend";
2
2
  import { assignmentAction } from "../functions/assignment-action/resource.js";
3
3
  import { categoryAction } from "../functions/category-action/resource.js";
4
4
  import { graphqlJwtAuthorizer } from "../functions/graphql-jwt-authorizer/resource.js";
5
+ import { contentActions } from "../functions/content-actions/resource.js";
5
6
  import { knowledgeQuery } from "../functions/knowledge-query/resource.js";
6
7
  import { manageUserRole } from "../functions/manage-user-role/resource.js";
7
8
  import { modelAttachmentUpload } from "../functions/model-attachment-upload/resource.js";
@@ -468,6 +469,30 @@ const schema = a.schema({
468
469
  .returns(a.ref("NewsroomSummary"))
469
470
  .authorization((allow) => [allow.groups(categoryWriteGroups), allow.custom()])
470
471
  .handler(a.handler.function(newsroomSummary)),
472
+ deriveMarkus: a
473
+ .query()
474
+ .arguments({ input: a.json().required() })
475
+ .returns(a.json())
476
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
477
+ .handler(a.handler.function(contentActions)),
478
+ saveItemDraft: a
479
+ .mutation()
480
+ .arguments({ input: a.json().required() })
481
+ .returns(a.json())
482
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
483
+ .handler(a.handler.function(contentActions)),
484
+ publishItem: a
485
+ .mutation()
486
+ .arguments({ input: a.json().required() })
487
+ .returns(a.json())
488
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
489
+ .handler(a.handler.function(contentActions)),
490
+ unpublishItem: a
491
+ .mutation()
492
+ .arguments({ input: a.json().required() })
493
+ .returns(a.json())
494
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
495
+ .handler(a.handler.function(contentActions)),
471
496
  knowledgeQuery: a
472
497
  .query()
473
498
  .arguments({
@@ -2012,6 +2037,7 @@ const schema = a.schema({
2012
2037
  ]),
2013
2038
  }).authorization((allow) => [
2014
2039
  allow.resource(categoryAction),
2040
+ allow.resource(contentActions),
2015
2041
  allow.resource(manageUserRole),
2016
2042
  allow.resource(modelAttachmentUpload),
2017
2043
  allow.resource(newsroomSummary),
@@ -2,6 +2,7 @@ import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
2
2
  import { assignmentAction } from "../functions/assignment-action/resource";
3
3
  import { categoryAction } from "../functions/category-action/resource";
4
4
  import { graphqlJwtAuthorizer } from "../functions/graphql-jwt-authorizer/resource";
5
+ import { contentActions } from "../functions/content-actions/resource";
5
6
  import { knowledgeQuery } from "../functions/knowledge-query/resource";
6
7
  import { manageUserRole } from "../functions/manage-user-role/resource";
7
8
  import { modelAttachmentUpload } from "../functions/model-attachment-upload/resource";
@@ -508,6 +509,34 @@ const schema = a.schema({
508
509
  .authorization((allow) => [allow.groups(categoryWriteGroups), allow.custom()])
509
510
  .handler(a.handler.function(newsroomSummary)),
510
511
 
512
+ deriveMarkus: a
513
+ .query()
514
+ .arguments({ input: a.json().required() })
515
+ .returns(a.json())
516
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
517
+ .handler(a.handler.function(contentActions)),
518
+
519
+ saveItemDraft: a
520
+ .mutation()
521
+ .arguments({ input: a.json().required() })
522
+ .returns(a.json())
523
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
524
+ .handler(a.handler.function(contentActions)),
525
+
526
+ publishItem: a
527
+ .mutation()
528
+ .arguments({ input: a.json().required() })
529
+ .returns(a.json())
530
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
531
+ .handler(a.handler.function(contentActions)),
532
+
533
+ unpublishItem: a
534
+ .mutation()
535
+ .arguments({ input: a.json().required() })
536
+ .returns(a.json())
537
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
538
+ .handler(a.handler.function(contentActions)),
539
+
511
540
  knowledgeQuery: a
512
541
  .query()
513
542
  .arguments({
@@ -2120,6 +2149,7 @@ const schema = a.schema({
2120
2149
  ]),
2121
2150
  }).authorization((allow) => [
2122
2151
  allow.resource(categoryAction),
2152
+ allow.resource(contentActions),
2123
2153
  allow.resource(manageUserRole),
2124
2154
  allow.resource(modelAttachmentUpload),
2125
2155
  allow.resource(newsroomSummary),
@@ -1,6 +1,7 @@
1
1
  import { a } from "@aws-amplify/backend";
2
2
  import { assignmentAction } from "../functions/assignment-action/resource.js";
3
3
  import { categoryAction } from "../functions/category-action/resource.js";
4
+ import { contentActions } from "../functions/content-actions/resource.js";
4
5
  import { knowledgeQuery } from "../functions/knowledge-query/resource.js";
5
6
  import { manageUserRole } from "../functions/manage-user-role/resource.js";
6
7
  import { modelAttachmentUpload } from "../functions/model-attachment-upload/resource.js";
@@ -368,6 +369,30 @@ export const schema = a.schema({
368
369
  .returns(a.ref("NewsroomSummary"))
369
370
  .authorization((allow) => [allow.groups(categoryWriteGroups), allow.custom()])
370
371
  .handler(a.handler.function(newsroomSummary)),
372
+ deriveMarkus: a
373
+ .query()
374
+ .arguments({ input: a.json().required() })
375
+ .returns(a.json())
376
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
377
+ .handler(a.handler.function(contentActions)),
378
+ saveItemDraft: a
379
+ .mutation()
380
+ .arguments({ input: a.json().required() })
381
+ .returns(a.json())
382
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
383
+ .handler(a.handler.function(contentActions)),
384
+ publishItem: a
385
+ .mutation()
386
+ .arguments({ input: a.json().required() })
387
+ .returns(a.json())
388
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
389
+ .handler(a.handler.function(contentActions)),
390
+ unpublishItem: a
391
+ .mutation()
392
+ .arguments({ input: a.json().required() })
393
+ .returns(a.json())
394
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
395
+ .handler(a.handler.function(contentActions)),
371
396
  knowledgeQuery: a
372
397
  .query()
373
398
  .arguments({
@@ -1843,6 +1868,7 @@ export const schema = a.schema({
1843
1868
  ]),
1844
1869
  }).authorization((allow) => [
1845
1870
  allow.resource(categoryAction),
1871
+ allow.resource(contentActions),
1846
1872
  allow.resource(manageUserRole),
1847
1873
  allow.resource(modelAttachmentUpload),
1848
1874
  allow.resource(newsroomSummary),
@@ -1,6 +1,7 @@
1
1
  import { type ClientSchema, a } from "@aws-amplify/backend";
2
2
  import { assignmentAction } from "../functions/assignment-action/resource";
3
3
  import { categoryAction } from "../functions/category-action/resource";
4
+ import { contentActions } from "../functions/content-actions/resource";
4
5
  import { knowledgeQuery } from "../functions/knowledge-query/resource";
5
6
  import { manageUserRole } from "../functions/manage-user-role/resource";
6
7
  import { modelAttachmentUpload } from "../functions/model-attachment-upload/resource";
@@ -402,6 +403,34 @@ export const schema = a.schema({
402
403
  .authorization((allow) => [allow.groups(categoryWriteGroups), allow.custom()])
403
404
  .handler(a.handler.function(newsroomSummary)),
404
405
 
406
+ deriveMarkus: a
407
+ .query()
408
+ .arguments({ input: a.json().required() })
409
+ .returns(a.json())
410
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
411
+ .handler(a.handler.function(contentActions)),
412
+
413
+ saveItemDraft: a
414
+ .mutation()
415
+ .arguments({ input: a.json().required() })
416
+ .returns(a.json())
417
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
418
+ .handler(a.handler.function(contentActions)),
419
+
420
+ publishItem: a
421
+ .mutation()
422
+ .arguments({ input: a.json().required() })
423
+ .returns(a.json())
424
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
425
+ .handler(a.handler.function(contentActions)),
426
+
427
+ unpublishItem: a
428
+ .mutation()
429
+ .arguments({ input: a.json().required() })
430
+ .returns(a.json())
431
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
432
+ .handler(a.handler.function(contentActions)),
433
+
405
434
  knowledgeQuery: a
406
435
  .query()
407
436
  .arguments({
@@ -1944,6 +1973,7 @@ export const schema = a.schema({
1944
1973
  ]),
1945
1974
  }).authorization((allow) => [
1946
1975
  allow.resource(categoryAction),
1976
+ allow.resource(contentActions),
1947
1977
  allow.resource(manageUserRole),
1948
1978
  allow.resource(modelAttachmentUpload),
1949
1979
  allow.resource(newsroomSummary),
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from papyrus_content.graphql_authoring import create_authoring_client
7
+ from papyrus_content.markus_renderer.derive import derive_body
8
+ from papyrus_content.publishing import (
9
+ ItemFields,
10
+ PublishError,
11
+ publish_item,
12
+ save_item,
13
+ unpublish_item,
14
+ )
15
+
16
+
17
+ def parse_input(raw: Any) -> dict:
18
+ parsed = json.loads(raw) if isinstance(raw, (str, bytes)) else raw
19
+ if not isinstance(parsed, dict):
20
+ raise PublishError("invalid-input", "The input must be a JSON object.")
21
+ return parsed
22
+
23
+
24
+ def actor_from_event(event: dict) -> str:
25
+ identity = event.get("identity") or {}
26
+ return identity.get("username") or identity.get("sub") or "unknown"
27
+
28
+
29
+ def failure_response(error: PublishError) -> dict:
30
+ if error.errors:
31
+ errors = [body_error.to_dict() for body_error in error.errors]
32
+ else:
33
+ errors = [{"code": error.code, "message": error.message, "line": None}]
34
+ return {"ok": False, "errors": errors}
35
+
36
+
37
+ def derive_markus_action(arguments: dict) -> dict:
38
+ derivation = derive_body(arguments.get("frontMatterYaml"), arguments.get("bodyMarkus") or "")
39
+ if not derivation.ok:
40
+ return {"ok": False, "errors": [body_error.to_dict() for body_error in derivation.errors]}
41
+ response = {
42
+ "ok": True,
43
+ "bodyIrBytes": len(json.dumps(derivation.body_ir, separators=(",", ":")).encode("utf-8")),
44
+ "errors": [],
45
+ }
46
+ if arguments.get("includeIr") is True:
47
+ response["bodyIr"] = derivation.body_ir
48
+ return response
49
+
50
+
51
+ def save_item_draft_action(arguments: dict, actor: str) -> dict:
52
+ client, _claims = create_authoring_client()
53
+ fields = ItemFields(
54
+ type=arguments["type"],
55
+ slug=arguments["slug"],
56
+ section=arguments.get("section"),
57
+ front_matter_yaml=arguments.get("frontMatterYaml"),
58
+ body_markus=arguments.get("bodyMarkus") or "",
59
+ aliases=list(arguments.get("aliases") or []),
60
+ id=arguments.get("id"),
61
+ )
62
+ record = save_item(client, fields, actor=actor, expected_content_hash=arguments.get("expectedContentHash"))
63
+ return {
64
+ "ok": True,
65
+ "item": {
66
+ "id": record["id"],
67
+ "contentHash": record["contentHash"],
68
+ "status": record["status"],
69
+ "slug": record["slug"],
70
+ "versionNumber": record.get("versionNumber"),
71
+ },
72
+ }
73
+
74
+
75
+ def publish_item_action(arguments: dict, actor: str) -> dict:
76
+ client, _claims = create_authoring_client()
77
+ result = publish_item(client, arguments["id"], actor=actor)
78
+ return {
79
+ "ok": True,
80
+ "changed": result.changed,
81
+ "publishedId": result.published_item_id,
82
+ "versionNumber": result.version_number,
83
+ }
84
+
85
+
86
+ def unpublish_item_action(arguments: dict, actor: str) -> dict:
87
+ client, _claims = create_authoring_client()
88
+ result = unpublish_item(client, arguments["id"], actor=actor)
89
+ return {"ok": True, "changed": result.changed}
90
+
91
+
92
+ def handler(event, context):
93
+ field = event["info"]["fieldName"]
94
+ arguments = parse_input(event["arguments"]["input"])
95
+ actor = actor_from_event(event)
96
+ try:
97
+ if field == "deriveMarkus":
98
+ return derive_markus_action(arguments)
99
+ if field == "saveItemDraft":
100
+ return save_item_draft_action(arguments, actor)
101
+ if field == "publishItem":
102
+ return publish_item_action(arguments, actor)
103
+ if field == "unpublishItem":
104
+ return unpublish_item_action(arguments, actor)
105
+ except PublishError as error:
106
+ return failure_response(error)
107
+ raise ValueError(f"Unsupported field {field}")
@@ -0,0 +1,4 @@
1
+ PyYAML>=6.0.2,<7
2
+ citeproc-py>=0.8.2,<1
3
+ anthus-markus==0.5.1
4
+ Pillow>=10,<12
@@ -0,0 +1,40 @@
1
+ import { defineFunction } from "@aws-amplify/backend";
2
+ import { Duration } from "aws-cdk-lib";
3
+ import { Architecture, Function, Runtime } from "aws-cdk-lib/aws-lambda";
4
+ import { execSync } from "node:child_process";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { pythonLambdaCode } from "../shared/python-bundle.js";
8
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const projectRoot = path.resolve(dirname, "../../..");
10
+ export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.5.1", "Pillow>=10,<12"];
11
+ function installCheckoutRequirements(outputDir) {
12
+ const requirementsPath = path.join(projectRoot, "amplify/functions/content-actions/requirements.txt");
13
+ execSync(`python3 -m pip install -r "${requirementsPath}" -t "${outputDir}" --no-cache-dir --platform manylinux2014_aarch64 --implementation cp --python-version 3.12 --only-binary=:all: --ignore-requires-python`, { cwd: projectRoot, stdio: "inherit" });
14
+ }
15
+ export const contentActions = defineFunction((scope) => {
16
+ return new Function(scope, "papyrus-content-actions", {
17
+ runtime: Runtime.PYTHON_3_12,
18
+ architecture: Architecture.ARM_64,
19
+ handler: "handler.handler",
20
+ timeout: Duration.seconds(60),
21
+ memorySize: 1024,
22
+ code: pythonLambdaCode({
23
+ functionDir: "content-actions",
24
+ modules: ["papyrus_content"],
25
+ requirements: contentActionsRequirements,
26
+ noDeps: true,
27
+ checkoutExtras: installCheckoutRequirements,
28
+ }, [
29
+ "bash",
30
+ "-c",
31
+ [
32
+ "set -euo pipefail",
33
+ "mkdir -p /asset-output/papyrus_content",
34
+ "cp amplify/functions/content-actions/handler.py /asset-output/handler.py",
35
+ "cp -R src/papyrus_content/. /asset-output/papyrus_content/",
36
+ "python3 -m pip install -r amplify/functions/content-actions/requirements.txt -t /asset-output --no-cache-dir",
37
+ ].join(" && "),
38
+ ]),
39
+ });
40
+ }, { resourceGroupName: "data" });
@@ -0,0 +1,54 @@
1
+ import { defineFunction } from "@aws-amplify/backend";
2
+ import { Duration } from "aws-cdk-lib";
3
+ import { Architecture, Function, Runtime } from "aws-cdk-lib/aws-lambda";
4
+ import { Construct } from "constructs";
5
+ import { execSync } from "node:child_process";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { pythonLambdaCode } from "../shared/python-bundle";
9
+
10
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const projectRoot = path.resolve(dirname, "../../..");
12
+
13
+ export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.5.1", "Pillow>=10,<12"];
14
+
15
+ function installCheckoutRequirements(outputDir: string): void {
16
+ const requirementsPath = path.join(projectRoot, "amplify/functions/content-actions/requirements.txt");
17
+ execSync(
18
+ `python3 -m pip install -r "${requirementsPath}" -t "${outputDir}" --no-cache-dir --platform manylinux2014_aarch64 --implementation cp --python-version 3.12 --only-binary=:all: --ignore-requires-python`,
19
+ { cwd: projectRoot, stdio: "inherit" },
20
+ );
21
+ }
22
+
23
+ export const contentActions = defineFunction(
24
+ (scope: Construct) => {
25
+ return new Function(scope, "papyrus-content-actions", {
26
+ runtime: Runtime.PYTHON_3_12,
27
+ architecture: Architecture.ARM_64,
28
+ handler: "handler.handler",
29
+ timeout: Duration.seconds(60),
30
+ memorySize: 1024,
31
+ code: pythonLambdaCode(
32
+ {
33
+ functionDir: "content-actions",
34
+ modules: ["papyrus_content"],
35
+ requirements: contentActionsRequirements,
36
+ noDeps: true,
37
+ checkoutExtras: installCheckoutRequirements,
38
+ },
39
+ [
40
+ "bash",
41
+ "-c",
42
+ [
43
+ "set -euo pipefail",
44
+ "mkdir -p /asset-output/papyrus_content",
45
+ "cp amplify/functions/content-actions/handler.py /asset-output/handler.py",
46
+ "cp -R src/papyrus_content/. /asset-output/papyrus_content/",
47
+ "python3 -m pip install -r amplify/functions/content-actions/requirements.txt -t /asset-output --no-cache-dir",
48
+ ].join(" && "),
49
+ ],
50
+ ),
51
+ });
52
+ },
53
+ { resourceGroupName: "data" },
54
+ );
@@ -58,9 +58,24 @@ function bundleFromPip(outputDir, options) {
58
58
  // Older pip (e.g. a Python 3.9 build image) checks Requires-Python against the host interpreter, not --python-version.
59
59
  "--ignore-requires-python",
60
60
  ...extra,
61
- pipSpec(),
61
+ ...(options.noDeps ? [] : [pipSpec()]),
62
62
  ...(options.requirements ?? []),
63
63
  ], { stdio: "inherit" });
64
+ if (options.noDeps) {
65
+ execFileSync(python, [
66
+ "-m", "pip", "install",
67
+ "--target", outputDir,
68
+ "--no-cache-dir",
69
+ "--no-deps",
70
+ "--platform", "manylinux2014_aarch64",
71
+ "--implementation", "cp",
72
+ "--python-version", "3.12",
73
+ "--only-binary=:all:",
74
+ "--ignore-requires-python",
75
+ ...extra,
76
+ pipSpec(),
77
+ ], { stdio: "inherit" });
78
+ }
64
79
  fs.copyFileSync(path.join(packageRoot, "amplify/functions", options.functionDir, "handler.py"), path.join(outputDir, "handler.py"));
65
80
  if (options.corpora) {
66
81
  const corporaDir = path.join(outputDir, "corpora");
@@ -74,7 +89,7 @@ function bundleFromPip(outputDir, options) {
74
89
  }
75
90
  }
76
91
  }
77
- function bundleFromCheckout(outputDir, options) {
92
+ export function bundleFromCheckout(outputDir, options) {
78
93
  fs.copyFileSync(path.join(packageRoot, "amplify/functions", options.functionDir, "handler.py"), path.join(outputDir, "handler.py"));
79
94
  for (const module of options.modules) {
80
95
  fs.mkdirSync(path.join(outputDir, module), { recursive: true });
@@ -53,6 +53,8 @@ export type PythonBundleOptions = {
53
53
  checkoutExtras?: (outputDir: string) => void;
54
54
  /** Extra pip requirements in package mode, beyond `papyrus-newsroom`. */
55
55
  requirements?: string[];
56
+ /** Package mode: install `papyrus-newsroom` with `--no-deps`, then only `requirements`. */
57
+ noDeps?: boolean;
56
58
  /** Copy `<cwd>/corpora/*.yml` into the asset (publication-owned steering config). */
57
59
  corpora?: boolean;
58
60
  };
@@ -77,11 +79,30 @@ function bundleFromPip(outputDir: string, options: PythonBundleOptions): void {
77
79
  // Older pip (e.g. a Python 3.9 build image) checks Requires-Python against the host interpreter, not --python-version.
78
80
  "--ignore-requires-python",
79
81
  ...extra,
80
- pipSpec(),
82
+ ...(options.noDeps ? [] : [pipSpec()]),
81
83
  ...(options.requirements ?? []),
82
84
  ],
83
85
  { stdio: "inherit" },
84
86
  );
87
+ if (options.noDeps) {
88
+ execFileSync(
89
+ python,
90
+ [
91
+ "-m", "pip", "install",
92
+ "--target", outputDir,
93
+ "--no-cache-dir",
94
+ "--no-deps",
95
+ "--platform", "manylinux2014_aarch64",
96
+ "--implementation", "cp",
97
+ "--python-version", "3.12",
98
+ "--only-binary=:all:",
99
+ "--ignore-requires-python",
100
+ ...extra,
101
+ pipSpec(),
102
+ ],
103
+ { stdio: "inherit" },
104
+ );
105
+ }
85
106
  fs.copyFileSync(
86
107
  path.join(packageRoot, "amplify/functions", options.functionDir, "handler.py"),
87
108
  path.join(outputDir, "handler.py"),
@@ -98,7 +119,7 @@ function bundleFromPip(outputDir: string, options: PythonBundleOptions): void {
98
119
  }
99
120
  }
100
121
 
101
- function bundleFromCheckout(outputDir: string, options: PythonBundleOptions): void {
122
+ export function bundleFromCheckout(outputDir: string, options: PythonBundleOptions): void {
102
123
  fs.copyFileSync(
103
124
  path.join(packageRoot, "amplify/functions", options.functionDir, "handler.py"),
104
125
  path.join(outputDir, "handler.py"),
@@ -18,6 +18,7 @@ import { assignmentAction } from "./functions/assignment-action/resource.js";
18
18
  import { categoryAction } from "./functions/category-action/resource.js";
19
19
  import { ConsoleChatResponderStack } from "./functions/console-chat-responder/resource.js";
20
20
  import { graphqlJwtAuthorizer } from "./functions/graphql-jwt-authorizer/resource.js";
21
+ import { contentActions } from "./functions/content-actions/resource.js";
21
22
  import { knowledgeQuery } from "./functions/knowledge-query/resource.js";
22
23
  import { manageUserRole } from "./functions/manage-user-role/resource.js";
23
24
  import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource.js";
@@ -79,6 +80,7 @@ export function defineSiteBackend(papyrusSite) {
79
80
  assignmentAction,
80
81
  auth: defineSiteAuth(site.auth ?? authConfigFromEnv()),
81
82
  categoryAction,
83
+ contentActions,
82
84
  data,
83
85
  graphqlJwtAuthorizer,
84
86
  knowledgeQuery,
@@ -310,6 +312,7 @@ export function defineSiteBackend(papyrusSite) {
310
312
  for (const resource of [
311
313
  backend.assignmentAction,
312
314
  backend.categoryAction,
315
+ backend.contentActions,
313
316
  backend.emailSubmissionProcessor,
314
317
  backend.knowledgeQuery,
315
318
  backend.modelAttachmentUpload,
@@ -323,6 +326,13 @@ export function defineSiteBackend(papyrusSite) {
323
326
  grantNewsroomReadWrite(backend.assignmentAction.resources.lambda);
324
327
  grantNewsroomReadWrite(backend.categoryAction.resources.lambda);
325
328
  grantNewsroomReadWrite(backend.newsroomSummary.resources.lambda);
329
+ const contentActionsLambda = backend.contentActions.resources.lambda;
330
+ grantNewsroomReadWriteDelete(contentActionsLambda);
331
+ contentActionsLambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl);
332
+ contentActionsLambda.addToRolePolicy(new PolicyStatement({
333
+ actions: ["appsync:GraphQL"],
334
+ resources: ["*"],
335
+ }));
326
336
  grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
327
337
  grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
328
338
  if (backend.sesInboundReceive) {
@@ -19,6 +19,7 @@ import { assignmentAction } from "./functions/assignment-action/resource";
19
19
  import { categoryAction } from "./functions/category-action/resource";
20
20
  import { ConsoleChatResponderStack } from "./functions/console-chat-responder/resource";
21
21
  import { graphqlJwtAuthorizer } from "./functions/graphql-jwt-authorizer/resource";
22
+ import { contentActions } from "./functions/content-actions/resource";
22
23
  import { knowledgeQuery } from "./functions/knowledge-query/resource";
23
24
  import { manageUserRole } from "./functions/manage-user-role/resource";
24
25
  import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource";
@@ -90,6 +91,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
90
91
  assignmentAction,
91
92
  auth: defineSiteAuth(site.auth ?? authConfigFromEnv()),
92
93
  categoryAction,
94
+ contentActions,
93
95
  data,
94
96
  graphqlJwtAuthorizer,
95
97
  knowledgeQuery,
@@ -386,6 +388,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
386
388
  for (const resource of [
387
389
  backend.assignmentAction,
388
390
  backend.categoryAction,
391
+ backend.contentActions,
389
392
  backend.emailSubmissionProcessor,
390
393
  backend.knowledgeQuery,
391
394
  backend.modelAttachmentUpload,
@@ -399,6 +402,18 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
399
402
  grantNewsroomReadWrite(backend.assignmentAction.resources.lambda as LambdaFunction);
400
403
  grantNewsroomReadWrite(backend.categoryAction.resources.lambda as LambdaFunction);
401
404
  grantNewsroomReadWrite(backend.newsroomSummary.resources.lambda as LambdaFunction);
405
+ const contentActionsLambda = backend.contentActions.resources.lambda as LambdaFunction;
406
+ grantNewsroomReadWriteDelete(contentActionsLambda);
407
+ contentActionsLambda.addEnvironment(
408
+ "PAPYRUS_GRAPHQL_ENDPOINT",
409
+ backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl,
410
+ );
411
+ contentActionsLambda.addToRolePolicy(
412
+ new PolicyStatement({
413
+ actions: ["appsync:GraphQL"],
414
+ resources: ["*"],
415
+ }),
416
+ );
402
417
  grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
403
418
  grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
404
419
  if (backend.sesInboundReceive) {
@@ -52,8 +52,8 @@ export default async function DateScopedArticlePage({ params }: DateScopedArticl
52
52
  editionFooter={{
53
53
  editionBasePath,
54
54
  entries: buildPresentationFooterEntries(content),
55
- subtitle: SITE_BRAND.id === "threat-intelligence" ? "" : (content.description?.trim() || "Inside Papyrus"),
56
- title: SITE_BRAND.id === "threat-intelligence" ? "ANTHUS THREAT INTELLIGENCE" : undefined,
55
+ subtitle: SITE_BRAND.footerSubtitleOverride ?? (content.description?.trim() || "Inside Papyrus"),
56
+ title: SITE_BRAND.footerTitle,
57
57
  }}
58
58
  item={item}
59
59
  backHref={`${editionBasePath}#${item.slug}`}
package/app/globals.css CHANGED
@@ -1,7 +1,4 @@
1
- @import "../publications/threat_intelligence/theme.css";
2
- @import "../publications/papyrus/theme.css";
3
- @import "../publications/pilobol_us/theme.css";
4
- @import "../publications/anth_us/theme.css";
1
+ @import "./papyrus-theme.css";
5
2
 
6
3
  @import "tailwindcss";
7
4
  @import "tw-animate-css";
package/app/layout.tsx CHANGED
@@ -1,14 +1,14 @@
1
1
  import type { Metadata } from "next";
2
2
  import { Playfair_Display, Plus_Jakarta_Sans } from "next/font/google";
3
- import { cookies } from "next/headers";
4
3
  import Script from "next/script";
5
4
  import { AmplifyClientProvider } from "../components/amplify-client-provider";
6
5
  import { PapyrusConsoleShell } from "../components/papyrus-console-shell";
7
6
  import { TooltipProvider } from "../components/ui/tooltip";
8
- import { getPresentationChoices, getSiteBrand, resolveRuntimeSiteBrandId } from "../lib/site-brand";
7
+ import { getPresentationChoices, getSiteBrand } from "../lib/site-brand";
9
8
  import { getSiteStack } from "../lib/site-stack";
10
9
  import "./tailwind.css";
11
10
  import "./globals.css";
11
+ import "papyrus-site-theme";
12
12
 
13
13
  const playfairDisplay = Playfair_Display({
14
14
  subsets: ["latin"],
@@ -22,16 +22,8 @@ const plusJakartaSans = Plus_Jakarta_Sans({
22
22
 
23
23
  const faviconVersion = "20260517-1";
24
24
  const defaultTheme = "system";
25
- const BRAND_OVERRIDE_COOKIE = "papyrus-site-brand-override";
26
-
27
- async function resolveLayoutSiteBrand() {
28
- const cookieStore = await cookies();
29
- const brandId = resolveRuntimeSiteBrandId(cookieStore.get(BRAND_OVERRIDE_COOKIE)?.value ?? null);
30
- return getSiteBrand(brandId);
31
- }
32
-
33
25
  export async function generateMetadata(): Promise<Metadata> {
34
- const siteBrand = await resolveLayoutSiteBrand();
26
+ const siteBrand = getSiteBrand();
35
27
  return {
36
28
  title: siteBrand.appTitle,
37
29
  description: siteBrand.appDescription,
@@ -45,7 +37,7 @@ export async function generateMetadata(): Promise<Metadata> {
45
37
  }
46
38
 
47
39
  export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
48
- const siteBrand = await resolveLayoutSiteBrand();
40
+ const siteBrand = getSiteBrand();
49
41
  const presentationChoices = getPresentationChoices(siteBrand);
50
42
  const siteStack = getSiteStack(siteBrand);
51
43
 
@@ -1,4 +1,3 @@
1
- import { cookies } from "next/headers";
2
1
  import { NewsDeskWorkspace, type NewsDeskTab } from "./topic-steering-workspace";
3
2
  import {
4
3
  createDemoCategorySteeringDashboard,
@@ -7,7 +6,6 @@ import {
7
6
  loadConfiguredCorpusSummaries,
8
7
  } from "../lib/category-repository";
9
8
  import {
10
- BRAND_OVERRIDE_COOKIE,
11
9
  getBrandAnalysisProfilesPath,
12
10
  getBrandNewsroomSectionsConfigPath,
13
11
  getBrandSteeringConfigPath,
@@ -65,8 +63,7 @@ export async function NewsDeskPage({ section: routeSection, sectionPageId, selec
65
63
  proposal: getFirstSearchParam(resolvedSearchParams, "proposal"),
66
64
  };
67
65
  const useDemoDashboard = getFirstSearchParam(resolvedSearchParams, "demo") === "1";
68
- const brandOverride = (await cookies()).get(BRAND_OVERRIDE_COOKIE)?.value ?? null;
69
- const brand = resolveActiveSiteBrand(brandOverride);
66
+ const brand = resolveActiveSiteBrand();
70
67
  const analysisProfiles = await loadAnalysisProfileSummaries(getBrandAnalysisProfilesPath(brand));
71
68
  const configuredCorpora = await loadConfiguredCorpusSummaries(getBrandSteeringConfigPath(brand));
72
69
  const dashboard = useDemoDashboard