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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/amplify/data/resource.js +32 -0
  2. package/amplify/data/resource.ts +37 -0
  3. package/amplify/data/schema.js +32 -0
  4. package/amplify/data/schema.ts +37 -0
  5. package/amplify/functions/content-actions/handler.py +149 -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/rebuild-trigger-settings.js +22 -0
  12. package/amplify/rebuild-trigger-settings.ts +42 -0
  13. package/amplify/site-backend-config.ts +6 -0
  14. package/amplify/site-backend.js +28 -0
  15. package/amplify/site-backend.ts +35 -0
  16. package/amplify/storage/resource.js +3 -0
  17. package/amplify/storage/resource.ts +3 -0
  18. package/app/%5F_preview/[[...path]]/route.ts +8 -0
  19. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -2
  20. package/app/globals.css +20 -4
  21. package/app/layout.tsx +8 -12
  22. package/app/newsroom/articles/[id]/page.tsx +15 -0
  23. package/app/newsroom/articles/page.tsx +13 -0
  24. package/app/robots.ts +11 -0
  25. package/bin/papyrus-app.mjs +12 -2
  26. package/bin/papyrus-infra.mjs +74 -18
  27. package/components/article-video.tsx +112 -3
  28. package/components/markus-ir-preview.tsx +300 -0
  29. package/components/news-desk-page.tsx +1 -4
  30. package/components/newsroom-access-gate.tsx +66 -0
  31. package/components/newsroom-article-editor.tsx +741 -0
  32. package/components/newsroom-articles-view.tsx +129 -0
  33. package/components/newsroom-ops-shell.tsx +3 -1
  34. package/components/staging-banner.tsx +11 -0
  35. package/components/topic-steering-workspace.tsx +1 -59
  36. package/infra/amplify-app-shell.js +205 -80
  37. package/infra/amplify-app-shell.ts +242 -113
  38. package/infra/build-specs.js +85 -0
  39. package/infra/build-specs.ts +131 -0
  40. package/infra/github-oidc-provider.js +24 -0
  41. package/infra/github-oidc-provider.ts +30 -0
  42. package/infra/index.js +3 -0
  43. package/infra/site-config.js +186 -0
  44. package/infra/site-config.ts +235 -0
  45. package/lib/cached-content-repository.ts +2 -1
  46. package/lib/content-actions-client.ts +98 -0
  47. package/lib/content-actions-demo.ts +190 -0
  48. package/lib/content-repository.ts +3 -0
  49. package/lib/content-source-context.ts +26 -0
  50. package/lib/empty-theme.css +1 -0
  51. package/lib/graphql-content-repository.ts +193 -39
  52. package/lib/layout-scenarios.ts +8 -8
  53. package/lib/newsroom-articles.ts +325 -0
  54. package/lib/newsroom-demo-articles.ts +90 -0
  55. package/lib/newsroom-media-backend.ts +142 -0
  56. package/lib/newsroom-media.ts +182 -0
  57. package/lib/newsroom-nav.ts +2 -0
  58. package/lib/site-brand-tenant.ts +3 -22
  59. package/lib/site-brand.ts +57 -42
  60. package/lib/site-env.ts +52 -0
  61. package/lib/staging-gate.ts +37 -0
  62. package/lib/staging-preview-object.ts +98 -0
  63. package/lib/video-mode.ts +42 -0
  64. package/lib/video-script.ts +63 -0
  65. package/middleware.ts +58 -33
  66. package/package.json +23 -1
  67. package/renderers/pretext/article-page.tsx +1 -6
  68. package/renderers/pretext/presentation-shell.tsx +2 -9
  69. package/routes.manifest.json +27 -1
  70. package/scripts/videoml/build-browser-bundle.mjs +22 -0
  71. package/scripts/videoml/build-preview-bundle.mjs +50 -0
  72. package/scripts/videoml/bundle-common.mjs +129 -0
  73. package/scripts/videoml/preview/preview-entry.tsx +381 -0
  74. package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
  75. package/scripts/videoml/preview/preview-timing.ts +42 -0
  76. package/scripts/videoml/preview.html +77 -0
  77. package/scripts/videoml/standard-entry.tsx +46 -0
  78. package/src/with-papyrus.mjs +15 -2
  79. package/publications/anth_us/brand.ts +0 -68
  80. package/publications/anth_us/theme.css +0 -66
  81. package/publications/pilobol_us/brand.ts +0 -37
  82. package/publications/pilobol_us/theme.css +0 -140
  83. package/publications/threat_intelligence/blog-defense/graph.ts +0 -636
  84. package/publications/threat_intelligence/blog-defense/layout.ts +0 -934
  85. package/publications/threat_intelligence/blog-defense/page-background.tsx +0 -590
  86. package/publications/threat_intelligence/brand.ts +0 -40
  87. package/publications/threat_intelligence/pictograms/art.tsx +0 -608
  88. package/publications/threat_intelligence/pictograms/figure.tsx +0 -82
  89. package/publications/threat_intelligence/pictograms/registry.ts +0 -30
  90. package/publications/threat_intelligence/pictograms/system.tsx +0 -305
  91. package/publications/threat_intelligence/seed/seed-edition-content.json +0 -629
  92. package/publications/threat_intelligence/theme.css +0 -1440
  93. /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,36 @@ 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)),
496
+ requestStagingBuild: a
497
+ .mutation()
498
+ .arguments({ input: a.json().required() })
499
+ .returns(a.json())
500
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
501
+ .handler(a.handler.function(contentActions)),
471
502
  knowledgeQuery: a
472
503
  .query()
473
504
  .arguments({
@@ -2012,6 +2043,7 @@ const schema = a.schema({
2012
2043
  ]),
2013
2044
  }).authorization((allow) => [
2014
2045
  allow.resource(categoryAction),
2046
+ allow.resource(contentActions),
2015
2047
  allow.resource(manageUserRole),
2016
2048
  allow.resource(modelAttachmentUpload),
2017
2049
  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,41 @@ 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
+
540
+ requestStagingBuild: a
541
+ .mutation()
542
+ .arguments({ input: a.json().required() })
543
+ .returns(a.json())
544
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
545
+ .handler(a.handler.function(contentActions)),
546
+
511
547
  knowledgeQuery: a
512
548
  .query()
513
549
  .arguments({
@@ -2120,6 +2156,7 @@ const schema = a.schema({
2120
2156
  ]),
2121
2157
  }).authorization((allow) => [
2122
2158
  allow.resource(categoryAction),
2159
+ allow.resource(contentActions),
2123
2160
  allow.resource(manageUserRole),
2124
2161
  allow.resource(modelAttachmentUpload),
2125
2162
  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,36 @@ 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)),
396
+ requestStagingBuild: a
397
+ .mutation()
398
+ .arguments({ input: a.json().required() })
399
+ .returns(a.json())
400
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
401
+ .handler(a.handler.function(contentActions)),
371
402
  knowledgeQuery: a
372
403
  .query()
373
404
  .arguments({
@@ -1843,6 +1874,7 @@ export const schema = a.schema({
1843
1874
  ]),
1844
1875
  }).authorization((allow) => [
1845
1876
  allow.resource(categoryAction),
1877
+ allow.resource(contentActions),
1846
1878
  allow.resource(manageUserRole),
1847
1879
  allow.resource(modelAttachmentUpload),
1848
1880
  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,41 @@ 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
+
434
+ requestStagingBuild: a
435
+ .mutation()
436
+ .arguments({ input: a.json().required() })
437
+ .returns(a.json())
438
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
439
+ .handler(a.handler.function(contentActions)),
440
+
405
441
  knowledgeQuery: a
406
442
  .query()
407
443
  .arguments({
@@ -1944,6 +1980,7 @@ export const schema = a.schema({
1944
1980
  ]),
1945
1981
  }).authorization((allow) => [
1946
1982
  allow.resource(categoryAction),
1983
+ allow.resource(contentActions),
1947
1984
  allow.resource(manageUserRole),
1948
1985
  allow.resource(modelAttachmentUpload),
1949
1986
  allow.resource(newsroomSummary),
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from typing import Any
6
+
7
+ from papyrus_content.graphql_authoring import create_authoring_client
8
+ from papyrus_content.markus_renderer.derive import derive_body
9
+ from papyrus_content.rebuild_trigger import (
10
+ DEFAULT_STAGING_BRANCH,
11
+ reader_target_from_environment,
12
+ trigger_pretext_revalidation,
13
+ trigger_rebuild,
14
+ trigger_staging_build,
15
+ )
16
+ from papyrus_content.publishing import (
17
+ ItemFields,
18
+ PublishError,
19
+ publish_item,
20
+ save_item,
21
+ unpublish_item,
22
+ )
23
+
24
+
25
+ def parse_input(raw: Any) -> dict:
26
+ parsed = json.loads(raw) if isinstance(raw, (str, bytes)) else raw
27
+ if not isinstance(parsed, dict):
28
+ raise PublishError("invalid-input", "The input must be a JSON object.")
29
+ return parsed
30
+
31
+
32
+ def actor_from_event(event: dict) -> str:
33
+ identity = event.get("identity") or {}
34
+ return identity.get("username") or identity.get("sub") or "unknown"
35
+
36
+
37
+ def failure_response(error: PublishError) -> dict:
38
+ if error.errors:
39
+ errors = [body_error.to_dict() for body_error in error.errors]
40
+ else:
41
+ errors = [{"code": error.code, "message": error.message, "line": None}]
42
+ return {"ok": False, "errors": errors}
43
+
44
+
45
+ def derive_markus_action(arguments: dict) -> dict:
46
+ derivation = derive_body(arguments.get("frontMatterYaml"), arguments.get("bodyMarkus") or "")
47
+ if not derivation.ok:
48
+ return {"ok": False, "errors": [body_error.to_dict() for body_error in derivation.errors]}
49
+ response = {
50
+ "ok": True,
51
+ "bodyIrBytes": len(json.dumps(derivation.body_ir, separators=(",", ":")).encode("utf-8")),
52
+ "errors": [],
53
+ }
54
+ if arguments.get("includeIr") is True:
55
+ response["bodyIr"] = derivation.body_ir
56
+ return response
57
+
58
+
59
+ def save_item_draft_action(arguments: dict, actor: str) -> dict:
60
+ client, _claims = create_authoring_client()
61
+ fields = ItemFields(
62
+ type=arguments["type"],
63
+ slug=arguments["slug"],
64
+ section=arguments.get("section"),
65
+ front_matter_yaml=arguments.get("frontMatterYaml"),
66
+ body_markus=arguments.get("bodyMarkus") or "",
67
+ aliases=list(arguments.get("aliases") or []),
68
+ id=arguments.get("id"),
69
+ )
70
+ record = save_item(client, fields, actor=actor, expected_content_hash=arguments.get("expectedContentHash"))
71
+ return {
72
+ "ok": True,
73
+ "item": {
74
+ "id": record["id"],
75
+ "contentHash": record["contentHash"],
76
+ "status": record["status"],
77
+ "slug": record["slug"],
78
+ "versionNumber": record.get("versionNumber"),
79
+ },
80
+ }
81
+
82
+
83
+ def site_triggers_for(client, item_id: str) -> dict:
84
+ triggers: dict = {}
85
+ reader_app_id, reader_branch = reader_target_from_environment()
86
+ revalidate_base_url = (os.environ.get("PAPYRUS_REVALIDATE_BASE_URL") or "").strip()
87
+ if revalidate_base_url:
88
+ try:
89
+ item = client.get_record("Item", item_id) or {}
90
+ slugs = [item["slug"]] if item.get("slug") else []
91
+ revalidated = trigger_pretext_revalidation(slugs, None, base_url=revalidate_base_url)
92
+ triggers["revalidated"] = revalidated if revalidated is not None else {"ok": False, "reason": "not-configured"}
93
+ except Exception as error: # noqa: BLE001 - a trigger failure never fails the publish
94
+ triggers["revalidated"] = {"ok": False, "error": f"{type(error).__name__}: {error}"}
95
+ if reader_app_id:
96
+ triggers["rebuild"] = trigger_rebuild(reader_app_id=reader_app_id, reader_branch=reader_branch)
97
+ return triggers
98
+
99
+
100
+ def publish_item_action(arguments: dict, actor: str) -> dict:
101
+ client, _claims = create_authoring_client()
102
+ result = publish_item(client, arguments["id"], actor=actor)
103
+ response = {
104
+ "ok": True,
105
+ "changed": result.changed,
106
+ "publishedId": result.published_item_id,
107
+ "versionNumber": result.version_number,
108
+ }
109
+ if result.changed:
110
+ response.update(site_triggers_for(client, arguments["id"]))
111
+ return response
112
+
113
+
114
+ def unpublish_item_action(arguments: dict, actor: str) -> dict:
115
+ client, _claims = create_authoring_client()
116
+ result = unpublish_item(client, arguments["id"], actor=actor)
117
+ response = {"ok": True, "changed": result.changed}
118
+ if result.changed:
119
+ response.update(site_triggers_for(client, arguments["id"]))
120
+ return response
121
+
122
+
123
+ def request_staging_build_action() -> dict:
124
+ cms_app_id = (os.environ.get("PAPYRUS_CMS_AMPLIFY_APP_ID") or "").strip()
125
+ if not cms_app_id:
126
+ return {"ok": False, "errors": [{"code": "staging-build-not-configured", "message": "This site has no staging build configured.", "line": None}]}
127
+ staging_branch = (os.environ.get("PAPYRUS_STAGING_BRANCH") or "").strip() or DEFAULT_STAGING_BRANCH
128
+ build = trigger_staging_build(cms_app_id=cms_app_id, staging_branch=staging_branch)
129
+ return {"ok": True, "build": build}
130
+
131
+
132
+ def handler(event, context):
133
+ field = event["fieldName"]
134
+ arguments = parse_input(event["arguments"]["input"])
135
+ actor = actor_from_event(event)
136
+ try:
137
+ if field == "deriveMarkus":
138
+ return derive_markus_action(arguments)
139
+ if field == "saveItemDraft":
140
+ return save_item_draft_action(arguments, actor)
141
+ if field == "publishItem":
142
+ return publish_item_action(arguments, actor)
143
+ if field == "unpublishItem":
144
+ return unpublish_item_action(arguments, actor)
145
+ if field == "requestStagingBuild":
146
+ return request_staging_build_action()
147
+ except PublishError as error:
148
+ return failure_response(error)
149
+ 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"),
@@ -0,0 +1,22 @@
1
+ export const REBUILD_TRIGGER_ACTIONS = ["amplify:StartJob", "amplify:ListJobs"];
2
+ function branchJobsArn(input, appId, branch) {
3
+ return `arn:aws:amplify:${input.region}:${input.account}:apps/${appId}/branches/${branch}/jobs/*`;
4
+ }
5
+ export function rebuildTriggerSettings(input) {
6
+ const environment = {};
7
+ const resources = [];
8
+ const readerAppId = (input.reader?.amplifyAppId ?? "").trim();
9
+ if (readerAppId !== "") {
10
+ const readerBranch = (input.reader?.branchName ?? "").trim() || "main";
11
+ environment.PAPYRUS_READER_AMPLIFY_APP_ID = readerAppId;
12
+ environment.PAPYRUS_READER_BRANCH = readerBranch;
13
+ resources.push(branchJobsArn(input, readerAppId, readerBranch));
14
+ }
15
+ const cmsAppId = input.cmsAppId.trim();
16
+ if (input.stagingBuildEnabled && cmsAppId !== "") {
17
+ environment.PAPYRUS_CMS_AMPLIFY_APP_ID = cmsAppId;
18
+ environment.PAPYRUS_STAGING_BRANCH = "staging";
19
+ resources.push(branchJobsArn(input, cmsAppId, "staging"));
20
+ }
21
+ return { environment, actions: REBUILD_TRIGGER_ACTIONS, resources };
22
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Environment and IAM policy for the content-actions Lambda's site triggers.
3
+ * Pure (no CDK imports) so the policy can be tested without synthesizing.
4
+ */
5
+ export type RebuildTriggerInput = {
6
+ reader?: { amplifyAppId: string; branchName?: string };
7
+ stagingBuildEnabled: boolean;
8
+ cmsAppId: string;
9
+ region: string;
10
+ account: string;
11
+ };
12
+
13
+ export type RebuildTriggerSettings = {
14
+ environment: Record<string, string>;
15
+ actions: string[];
16
+ resources: string[];
17
+ };
18
+
19
+ export const REBUILD_TRIGGER_ACTIONS = ["amplify:StartJob", "amplify:ListJobs"];
20
+
21
+ function branchJobsArn(input: RebuildTriggerInput, appId: string, branch: string): string {
22
+ return `arn:aws:amplify:${input.region}:${input.account}:apps/${appId}/branches/${branch}/jobs/*`;
23
+ }
24
+
25
+ export function rebuildTriggerSettings(input: RebuildTriggerInput): RebuildTriggerSettings {
26
+ const environment: Record<string, string> = {};
27
+ const resources: string[] = [];
28
+ const readerAppId = (input.reader?.amplifyAppId ?? "").trim();
29
+ if (readerAppId !== "") {
30
+ const readerBranch = (input.reader?.branchName ?? "").trim() || "main";
31
+ environment.PAPYRUS_READER_AMPLIFY_APP_ID = readerAppId;
32
+ environment.PAPYRUS_READER_BRANCH = readerBranch;
33
+ resources.push(branchJobsArn(input, readerAppId, readerBranch));
34
+ }
35
+ const cmsAppId = input.cmsAppId.trim();
36
+ if (input.stagingBuildEnabled && cmsAppId !== "") {
37
+ environment.PAPYRUS_CMS_AMPLIFY_APP_ID = cmsAppId;
38
+ environment.PAPYRUS_STAGING_BRANCH = "staging";
39
+ resources.push(branchJobsArn(input, cmsAppId, "staging"));
40
+ }
41
+ return { environment, actions: REBUILD_TRIGGER_ACTIONS, resources };
42
+ }
@@ -21,6 +21,12 @@ export type PapyrusSiteBackendConfig = {
21
21
  */
22
22
  productionAppId?: string;
23
23
  auth?: PapyrusAuthConfig;
24
+ /** Static reader app (Markus-static sites) that a publish rebuilds through IAM `amplify:StartJob`. */
25
+ reader?: { amplifyAppId: string; branchName?: string };
26
+ /** Lets the newsroom start the `staging` branch build of this CMS app. */
27
+ stagingBuild?: { enabled: boolean };
28
+ /** Base URL of a Pretext reader whose `/api/revalidate` a publish calls (secret `PAPYRUS_REVALIDATE_SECRET`). */
29
+ revalidateBaseUrl?: string;
24
30
  features?: {
25
31
  consoleResponder?: boolean;
26
32
  inboundEmail?: boolean;