@anthusai/papyrus 0.0.0 → 1.0.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/amplify/auth/resource.js +61 -0
- package/amplify/auth/resource.ts +65 -0
- package/amplify/data/resource.js +2069 -0
- package/amplify/data/resource.ts +2185 -0
- package/amplify/data/schema.js +1882 -0
- package/amplify/data/schema.ts +1990 -0
- package/amplify/functions/assignment-action/handler.ts +1416 -0
- package/amplify/functions/assignment-action/resource.js +8 -0
- package/amplify/functions/assignment-action/resource.ts +9 -0
- package/amplify/functions/category-action/handler.ts +2259 -0
- package/amplify/functions/category-action/resource.js +8 -0
- package/amplify/functions/category-action/resource.ts +9 -0
- package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
- package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
- package/amplify/functions/console-chat-responder/Dockerfile +19 -0
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
- package/amplify/functions/console-chat-responder/resource.js +153 -0
- package/amplify/functions/console-chat-responder/resource.ts +179 -0
- package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
- package/amplify/functions/content-actions/handler.py +149 -0
- package/amplify/functions/content-actions/requirements.txt +4 -0
- package/amplify/functions/content-actions/resource.js +40 -0
- package/amplify/functions/content-actions/resource.ts +54 -0
- package/amplify/functions/email-submission-processor/handler.py +46 -0
- package/amplify/functions/email-submission-processor/requirements.txt +2 -0
- package/amplify/functions/email-submission-processor/resource.js +56 -0
- package/amplify/functions/email-submission-processor/resource.ts +69 -0
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
- package/amplify/functions/knowledge-query/handler.py +1 -0
- package/amplify/functions/knowledge-query/requirements.txt +3 -0
- package/amplify/functions/knowledge-query/resource.js +50 -0
- package/amplify/functions/knowledge-query/resource.ts +61 -0
- package/amplify/functions/manage-user-role/handler.ts +799 -0
- package/amplify/functions/manage-user-role/resource.js +8 -0
- package/amplify/functions/manage-user-role/resource.ts +9 -0
- package/amplify/functions/model-attachment-upload/handler.ts +387 -0
- package/amplify/functions/model-attachment-upload/resource.js +8 -0
- package/amplify/functions/model-attachment-upload/resource.ts +9 -0
- package/amplify/functions/newsroom-summary/handler.ts +321 -0
- package/amplify/functions/newsroom-summary/resource.js +8 -0
- package/amplify/functions/newsroom-summary/resource.ts +9 -0
- package/amplify/functions/procedure-action/handler.ts +621 -0
- package/amplify/functions/procedure-action/resource.js +8 -0
- package/amplify/functions/procedure-action/resource.ts +9 -0
- package/amplify/functions/reader-settings/handler.ts +322 -0
- package/amplify/functions/reader-settings/resource.js +8 -0
- package/amplify/functions/reader-settings/resource.ts +9 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
- package/amplify/functions/ses-inbound-receive/resource.js +8 -0
- package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
- package/amplify/functions/shared/amplify-secrets.ts +65 -0
- package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
- package/amplify/functions/shared/email-mime-intake.ts +196 -0
- package/amplify/functions/shared/email-submission.ts +415 -0
- package/amplify/functions/shared/inbound-email-intake.ts +37 -0
- package/amplify/functions/shared/lambda-data-client.ts +21 -0
- package/amplify/functions/shared/model-payloads.ts +186 -0
- package/amplify/functions/shared/python-bundle.js +130 -0
- package/amplify/functions/shared/python-bundle.ts +162 -0
- package/amplify/functions/shared/slack-events.ts +111 -0
- package/amplify/functions/slack-delivery/handler.py +112 -0
- package/amplify/functions/slack-delivery/requirements.txt +2 -0
- package/amplify/functions/slack-delivery/resource.js +38 -0
- package/amplify/functions/slack-delivery/resource.ts +51 -0
- package/amplify/functions/slack-events/handler.ts +155 -0
- package/amplify/functions/slack-events/resource.js +11 -0
- package/amplify/functions/slack-events/resource.ts +12 -0
- package/amplify/inbound-email/stack.js +46 -0
- package/amplify/inbound-email/stack.ts +61 -0
- package/amplify/package.json +3 -0
- package/amplify/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/site-backend-config.js +1 -0
- package/amplify/site-backend-config.ts +36 -0
- package/amplify/site-backend.js +615 -0
- package/amplify/site-backend.ts +739 -0
- package/amplify/storage/resource.js +21 -0
- package/amplify/storage/resource.ts +22 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
- package/app/[year]/[month]/[day]/page.tsx +22 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
- package/app/api/archive/editions/route.ts +18 -0
- package/app/api/media/[...path]/route.ts +35 -0
- package/app/api/revalidate/route.ts +94 -0
- package/app/archive/page.tsx +24 -0
- package/app/articles/[slug]/page.tsx +35 -0
- package/app/globals.css +10345 -0
- package/app/layout.tsx +142 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
- package/app/newsroom/[section]/page.tsx +47 -0
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/newsroom/forum/[threadId]/page.tsx +35 -0
- package/app/newsroom/insights/[threadId]/page.tsx +22 -0
- package/app/newsroom/insights/page.tsx +14 -0
- package/app/newsroom/layout.tsx +12 -0
- package/app/newsroom/page.tsx +31 -0
- package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
- package/app/page.tsx +272 -0
- package/app/papyrus-theme.css +11 -0
- package/app/robots.ts +11 -0
- package/app/settings/page.tsx +14 -0
- package/app/tailwind.css +65 -0
- package/bin/papyrus-app.mjs +20 -0
- package/bin/papyrus-infra.mjs +95 -0
- package/components/ai-elements/conversation.tsx +168 -0
- package/components/ai-elements/message.tsx +360 -0
- package/components/ai-elements/model-selector.tsx +30 -0
- package/components/ai-elements/prompt-input.tsx +1461 -0
- package/components/ai-elements/shimmer.tsx +76 -0
- package/components/ai-elements/suggestion.tsx +54 -0
- package/components/ai-elements/tool.tsx +173 -0
- package/components/amplify-client-provider.tsx +140 -0
- package/components/archive-grid.tsx +89 -0
- package/components/archive-shell.tsx +41 -0
- package/components/article-video.tsx +156 -0
- package/components/blog-page-background.tsx +20 -0
- package/components/dev-sandbox-editor-sign-in.tsx +60 -0
- package/components/markus-ir-preview.tsx +300 -0
- package/components/news-desk-client-provider.tsx +211 -0
- package/components/news-desk-page.tsx +116 -0
- package/components/news-desk-taxonomy-client.ts +3684 -0
- package/components/newspaper.tsx +1929 -0
- package/components/newsroom-access-gate.tsx +66 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-assignments-view.tsx +664 -0
- package/components/newsroom-ops-overview.tsx +226 -0
- package/components/newsroom-ops-shell.tsx +321 -0
- package/components/newsroom-references-view.tsx +598 -0
- package/components/newsroom-topics-view.tsx +425 -0
- package/components/papyrus-console-shell.tsx +1912 -0
- package/components/pictogram-figure.tsx +65 -0
- package/components/presentation-footer.tsx +146 -0
- package/components/reader-auth-control.tsx +162 -0
- package/components/reader-auth-state.ts +122 -0
- package/components/reader-settings.ts +332 -0
- package/components/reader-theme-control.tsx +85 -0
- package/components/reference-source-preview.tsx +163 -0
- package/components/settings-page.tsx +239 -0
- package/components/staging-banner.tsx +11 -0
- package/components/topic-steering-workspace.tsx +18005 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button-group.tsx +87 -0
- package/components/ui/button.tsx +58 -0
- package/components/ui/card.tsx +32 -0
- package/components/ui/command.tsx +194 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +266 -0
- package/components/ui/hover-card.tsx +51 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/scroll-area.tsx +16 -0
- package/components/ui/select.tsx +199 -0
- package/components/ui/separator.tsx +23 -0
- package/components/ui/sheet.tsx +92 -0
- package/components/ui/spinner.tsx +10 -0
- package/components/ui/tabs.tsx +113 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/use-resolved-papyrus-theme.ts +47 -0
- package/infra/amplify-app-shell.js +234 -0
- package/infra/amplify-app-shell.ts +284 -0
- package/infra/build-specs.js +85 -0
- package/infra/build-specs.ts +131 -0
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +15 -0
- package/infra/site-config.js +186 -0
- package/infra/site-config.ts +235 -0
- package/lib/amplify-outputs-guard.ts +24 -0
- package/lib/amplify-outputs-path.ts +16 -0
- package/lib/amplify-server-runtime.ts +63 -0
- package/lib/archive-data.ts +30 -0
- package/lib/archive-types.ts +15 -0
- package/lib/articles.ts +320 -0
- package/lib/cached-content-repository.ts +90 -0
- package/lib/category-dashboard.ts +37 -0
- package/lib/category-repository.ts +1907 -0
- package/lib/console-chat-client.ts +1316 -0
- package/lib/console-web-context.ts +118 -0
- package/lib/content-actions-client.ts +98 -0
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-load-timing.ts +23 -0
- package/lib/content-repository.ts +43 -0
- package/lib/content-source-context.ts +26 -0
- package/lib/content-types.ts +104 -0
- package/lib/define-site.js +3 -0
- package/lib/define-site.ts +21 -0
- package/lib/demo-amplify-outputs.ts +15 -0
- package/lib/dev-sandbox-editor-auth.ts +18 -0
- package/lib/doctrine.ts +128 -0
- package/lib/edition-routes.ts +155 -0
- package/lib/edition-sections.ts +111 -0
- package/lib/empty-edition-layout-plan.ts +109 -0
- package/lib/empty-theme.css +1 -0
- package/lib/excerpts.ts +8 -0
- package/lib/graphql-content-repository.ts +1140 -0
- package/lib/image-url.ts +10 -0
- package/lib/layout-plan.ts +981 -0
- package/lib/layout-scenarios.ts +564 -0
- package/lib/markus-body.ts +168 -0
- package/lib/markus-ir.ts +235 -0
- package/lib/markus-projection.ts +243 -0
- package/lib/markus-to-article.ts +93 -0
- package/lib/news-desk-session.ts +160 -0
- package/lib/newspaper-layout.ts +3459 -0
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-assignments.ts +173 -0
- package/lib/newsroom-base-path.ts +48 -0
- package/lib/newsroom-card-layout.ts +102 -0
- package/lib/newsroom-category-drilldown.ts +133 -0
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-demo-dashboard.ts +267 -0
- package/lib/newsroom-demo-profile.ts +46 -0
- package/lib/newsroom-forum-routes.ts +153 -0
- package/lib/newsroom-index-filters.ts +202 -0
- package/lib/newsroom-knowledge-query-request.ts +51 -0
- package/lib/newsroom-list-selection.ts +42 -0
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +44 -0
- package/lib/newsroom-references.ts +116 -0
- package/lib/newsroom-sections.ts +52 -0
- package/lib/newsroom-topics.ts +118 -0
- package/lib/papyrus-object-kinds.ts +24 -0
- package/lib/papyrus-web-locations.ts +480 -0
- package/lib/pilobol-sample.ts +167 -0
- package/lib/presentation-footer.ts +92 -0
- package/lib/pretext-layout.ts +179 -0
- package/lib/public-placeholder-config.ts +102 -0
- package/lib/publication-items.ts +181 -0
- package/lib/reader-route-config.ts +1 -0
- package/lib/reader-static-params.ts +56 -0
- package/lib/reader-storage-url.ts +63 -0
- package/lib/reference-display.ts +74 -0
- package/lib/reference-policy.ts +97 -0
- package/lib/reference-source-preview.ts +175 -0
- package/lib/renderer-config.ts +50 -0
- package/lib/renderer.ts +87 -0
- package/lib/reporting-story-budget.ts +638 -0
- package/lib/semantic-graph.ts +554 -0
- package/lib/site-brand-tenant.ts +22 -0
- package/lib/site-brand.ts +194 -0
- package/lib/site-env.ts +52 -0
- package/lib/site-renderer.ts +17 -0
- package/lib/site-stack.ts +195 -0
- package/lib/staging-gate.ts +37 -0
- package/lib/staging-preview-object.ts +98 -0
- package/lib/themed-image.ts +30 -0
- package/lib/utils.ts +6 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +63 -0
- package/middleware.ts +103 -0
- package/package.json +318 -1
- package/renderers/markus/index.ts +25 -0
- package/renderers/markus/stubs.tsx +33 -0
- package/renderers/pretext/article-page.tsx +169 -0
- package/renderers/pretext/index.ts +30 -0
- package/renderers/pretext/layouts.ts +16 -0
- package/renderers/pretext/presentation-shell.tsx +470 -0
- package/routes.manifest.json +208 -0
- package/scripts/fixtures/pilobol-sample.ast.json +234 -0
- package/scripts/videoml/build-browser-bundle.mjs +22 -0
- package/scripts/videoml/build-preview-bundle.mjs +50 -0
- package/scripts/videoml/bundle-common.mjs +129 -0
- package/scripts/videoml/preview/preview-entry.tsx +381 -0
- package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
- package/scripts/videoml/preview/preview-timing.ts +42 -0
- package/scripts/videoml/preview.html +77 -0
- package/scripts/videoml/standard-entry.tsx +46 -0
- package/src/backend.js +1 -0
- package/src/backend.ts +3 -0
- package/src/sync.mjs +87 -0
- package/src/with-papyrus.d.mts +2 -0
- package/src/with-papyrus.mjs +60 -0
|
@@ -0,0 +1,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,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
|
+
);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def handler(event: dict[str, Any], _context: Any = None) -> dict[str, Any]:
|
|
9
|
+
if isinstance(event.get("Records"), list):
|
|
10
|
+
record = event["Records"][0]
|
|
11
|
+
body = record.get("body") if isinstance(record, dict) else None
|
|
12
|
+
if isinstance(body, str):
|
|
13
|
+
event = json.loads(body)
|
|
14
|
+
elif isinstance(body, dict):
|
|
15
|
+
event = body
|
|
16
|
+
|
|
17
|
+
message_id = str(event.get("messageId") or event.get("message_id") or "").strip()
|
|
18
|
+
if not message_id:
|
|
19
|
+
raise ValueError("messageId is required.")
|
|
20
|
+
|
|
21
|
+
if event.get("sendFeedbackOnly") or event.get("send_feedback_only"):
|
|
22
|
+
from papyrus_content.graphql_authoring import PapyrusGraphQLAuthoringClient
|
|
23
|
+
from papyrus_newsroom.email_submissions import send_submission_feedback_for_message
|
|
24
|
+
|
|
25
|
+
endpoint = os.environ.get("PAPYRUS_GRAPHQL_ENDPOINT", "").strip()
|
|
26
|
+
client = PapyrusGraphQLAuthoringClient(endpoint=endpoint or None)
|
|
27
|
+
return send_submission_feedback_for_message(client, message_id=message_id)
|
|
28
|
+
|
|
29
|
+
corpus_key = str(
|
|
30
|
+
event.get("corpusKey")
|
|
31
|
+
or event.get("corpus_key")
|
|
32
|
+
or os.environ.get("PAPYRUS_INBOUND_EMAIL_CORPUS_KEY")
|
|
33
|
+
or "AI-ML-research"
|
|
34
|
+
).strip()
|
|
35
|
+
|
|
36
|
+
from papyrus_content.graphql_authoring import PapyrusGraphQLAuthoringClient
|
|
37
|
+
from papyrus_newsroom.email_submissions import process_email_submission_message
|
|
38
|
+
|
|
39
|
+
endpoint = os.environ.get("PAPYRUS_GRAPHQL_ENDPOINT", "").strip()
|
|
40
|
+
client = PapyrusGraphQLAuthoringClient(endpoint=endpoint or None)
|
|
41
|
+
return process_email_submission_message(
|
|
42
|
+
client,
|
|
43
|
+
message_id=message_id,
|
|
44
|
+
corpus_key=corpus_key,
|
|
45
|
+
apply=True,
|
|
46
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
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 fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { pythonLambdaCode } from "../shared/python-bundle.js";
|
|
9
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const projectRoot = path.resolve(dirname, "../../..");
|
|
11
|
+
function copyCheckoutCorpora(outputDir) {
|
|
12
|
+
const corporaDir = path.join(outputDir, "corpora");
|
|
13
|
+
fs.mkdirSync(corporaDir, { recursive: true });
|
|
14
|
+
for (const entry of fs.readdirSync(path.join(projectRoot, "corpora"))) {
|
|
15
|
+
if (!entry.endsWith(".yml"))
|
|
16
|
+
continue;
|
|
17
|
+
fs.copyFileSync(path.join(projectRoot, "corpora", entry), path.join(corporaDir, entry));
|
|
18
|
+
}
|
|
19
|
+
installPythonRequirements(outputDir);
|
|
20
|
+
}
|
|
21
|
+
function installPythonRequirements(outputDir) {
|
|
22
|
+
const requirementsPath = path.join(projectRoot, "amplify/functions/email-submission-processor/requirements.txt");
|
|
23
|
+
execSync(`python3 -m pip install -r "${requirementsPath}" -t "${outputDir}" --no-cache-dir`, {
|
|
24
|
+
cwd: projectRoot,
|
|
25
|
+
stdio: "inherit",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export const emailSubmissionProcessor = defineFunction((scope) => {
|
|
29
|
+
return new Function(scope, "papyrus-email-submission-processor", {
|
|
30
|
+
runtime: Runtime.PYTHON_3_12,
|
|
31
|
+
architecture: Architecture.ARM_64,
|
|
32
|
+
handler: "handler.handler",
|
|
33
|
+
timeout: Duration.minutes(5),
|
|
34
|
+
memorySize: 1024,
|
|
35
|
+
code: pythonLambdaCode({
|
|
36
|
+
functionDir: "email-submission-processor",
|
|
37
|
+
modules: ["papyrus_newsroom", "papyrus_content", "papyrus_knowledge_query"],
|
|
38
|
+
requirements: ["PyYAML>=6.0.2,<7", "requests>=2.32.3,<3", "boto3>=1.34.0,<2.0.0"],
|
|
39
|
+
corpora: true,
|
|
40
|
+
checkoutExtras: copyCheckoutCorpora,
|
|
41
|
+
}, [
|
|
42
|
+
"bash",
|
|
43
|
+
"-c",
|
|
44
|
+
[
|
|
45
|
+
"set -euo pipefail",
|
|
46
|
+
"mkdir -p /asset-output/papyrus_newsroom /asset-output/papyrus_content /asset-output/papyrus_knowledge_query /asset-output/corpora",
|
|
47
|
+
"cp amplify/functions/email-submission-processor/handler.py /asset-output/handler.py",
|
|
48
|
+
"cp -R src/papyrus_newsroom/. /asset-output/papyrus_newsroom/",
|
|
49
|
+
"cp -R src/papyrus_content/. /asset-output/papyrus_content/",
|
|
50
|
+
"cp -R src/papyrus_knowledge_query/. /asset-output/papyrus_knowledge_query/",
|
|
51
|
+
"cp corpora/*.yml /asset-output/corpora/",
|
|
52
|
+
"python3 -m pip install -r amplify/functions/email-submission-processor/requirements.txt -t /asset-output --no-cache-dir",
|
|
53
|
+
].join(" && "),
|
|
54
|
+
]),
|
|
55
|
+
});
|
|
56
|
+
}, { resourceGroupName: "data" });
|
|
@@ -0,0 +1,69 @@
|
|
|
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 fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { pythonLambdaCode } from "../shared/python-bundle";
|
|
10
|
+
|
|
11
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const projectRoot = path.resolve(dirname, "../../..");
|
|
13
|
+
|
|
14
|
+
function copyCheckoutCorpora(outputDir: string): void {
|
|
15
|
+
const corporaDir = path.join(outputDir, "corpora");
|
|
16
|
+
fs.mkdirSync(corporaDir, { recursive: true });
|
|
17
|
+
for (const entry of fs.readdirSync(path.join(projectRoot, "corpora"))) {
|
|
18
|
+
if (!entry.endsWith(".yml")) continue;
|
|
19
|
+
fs.copyFileSync(path.join(projectRoot, "corpora", entry), path.join(corporaDir, entry));
|
|
20
|
+
}
|
|
21
|
+
installPythonRequirements(outputDir);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function installPythonRequirements(outputDir: string): void {
|
|
25
|
+
const requirementsPath = path.join(
|
|
26
|
+
projectRoot,
|
|
27
|
+
"amplify/functions/email-submission-processor/requirements.txt",
|
|
28
|
+
);
|
|
29
|
+
execSync(`python3 -m pip install -r "${requirementsPath}" -t "${outputDir}" --no-cache-dir`, {
|
|
30
|
+
cwd: projectRoot,
|
|
31
|
+
stdio: "inherit",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const emailSubmissionProcessor = defineFunction(
|
|
36
|
+
(scope: Construct) => {
|
|
37
|
+
return new Function(scope, "papyrus-email-submission-processor", {
|
|
38
|
+
runtime: Runtime.PYTHON_3_12,
|
|
39
|
+
architecture: Architecture.ARM_64,
|
|
40
|
+
handler: "handler.handler",
|
|
41
|
+
timeout: Duration.minutes(5),
|
|
42
|
+
memorySize: 1024,
|
|
43
|
+
code: pythonLambdaCode(
|
|
44
|
+
{
|
|
45
|
+
functionDir: "email-submission-processor",
|
|
46
|
+
modules: ["papyrus_newsroom", "papyrus_content", "papyrus_knowledge_query"],
|
|
47
|
+
requirements: ["PyYAML>=6.0.2,<7", "requests>=2.32.3,<3", "boto3>=1.34.0,<2.0.0"],
|
|
48
|
+
corpora: true,
|
|
49
|
+
checkoutExtras: copyCheckoutCorpora,
|
|
50
|
+
},
|
|
51
|
+
[
|
|
52
|
+
"bash",
|
|
53
|
+
"-c",
|
|
54
|
+
[
|
|
55
|
+
"set -euo pipefail",
|
|
56
|
+
"mkdir -p /asset-output/papyrus_newsroom /asset-output/papyrus_content /asset-output/papyrus_knowledge_query /asset-output/corpora",
|
|
57
|
+
"cp amplify/functions/email-submission-processor/handler.py /asset-output/handler.py",
|
|
58
|
+
"cp -R src/papyrus_newsroom/. /asset-output/papyrus_newsroom/",
|
|
59
|
+
"cp -R src/papyrus_content/. /asset-output/papyrus_content/",
|
|
60
|
+
"cp -R src/papyrus_knowledge_query/. /asset-output/papyrus_knowledge_query/",
|
|
61
|
+
"cp corpora/*.yml /asset-output/corpora/",
|
|
62
|
+
"python3 -m pip install -r amplify/functions/email-submission-processor/requirements.txt -t /asset-output --no-cache-dir",
|
|
63
|
+
].join(" && "),
|
|
64
|
+
],
|
|
65
|
+
),
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
{ resourceGroupName: "data" },
|
|
69
|
+
);
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
|
|
3
|
+
|
|
4
|
+
type AppSyncAuthorizerEvent = {
|
|
5
|
+
authorizationToken?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type JwtHeader = {
|
|
9
|
+
alg?: string;
|
|
10
|
+
typ?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type JwtClaims = {
|
|
14
|
+
iss?: string;
|
|
15
|
+
sub?: string;
|
|
16
|
+
aud?: string | string[];
|
|
17
|
+
exp?: number;
|
|
18
|
+
nbf?: number;
|
|
19
|
+
iat?: number;
|
|
20
|
+
scope?: string;
|
|
21
|
+
scp?: string | string[];
|
|
22
|
+
groups?: string | string[];
|
|
23
|
+
roles?: string | string[];
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const handler = async (event: AppSyncAuthorizerEvent) => {
|
|
28
|
+
try {
|
|
29
|
+
const token = normalizeAuthorizationToken(event.authorizationToken);
|
|
30
|
+
const jwtSecret = await getJwtSecret();
|
|
31
|
+
const claims = verifyHs256Jwt(token, jwtSecret);
|
|
32
|
+
assertExpectedIssuer(claims);
|
|
33
|
+
assertExpectedAudience(claims);
|
|
34
|
+
assertRequiredScope(claims);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
isAuthorized: true,
|
|
38
|
+
resolverContext: {
|
|
39
|
+
sub: claims.sub ?? "unknown",
|
|
40
|
+
iss: claims.iss ?? "unknown",
|
|
41
|
+
scope: getClaimValues(claims.scope).join(" "),
|
|
42
|
+
},
|
|
43
|
+
ttlOverride: 300,
|
|
44
|
+
};
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.warn(`JWT authorizer rejected request: ${(error as Error).message}`);
|
|
47
|
+
return {
|
|
48
|
+
isAuthorized: false,
|
|
49
|
+
ttlOverride: 0,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function normalizeAuthorizationToken(value: string | undefined): string {
|
|
55
|
+
const token = value
|
|
56
|
+
?.replace(/^PapyrusJwt\s+/i, "")
|
|
57
|
+
.replace(/^papyrus-jwt:/i, "")
|
|
58
|
+
.replace(/^Bearer\s+/i, "")
|
|
59
|
+
.trim();
|
|
60
|
+
if (!token) throw new Error("Missing Authorization token.");
|
|
61
|
+
return token;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function verifyHs256Jwt(token: string, jwtSecret: string): JwtClaims {
|
|
65
|
+
const [encodedHeader, encodedPayload, encodedSignature] = token.split(".");
|
|
66
|
+
if (!encodedHeader || !encodedPayload || !encodedSignature) {
|
|
67
|
+
throw new Error("JWT must have header, payload, and signature.");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const header = decodeJson<JwtHeader>(encodedHeader);
|
|
71
|
+
if (header.alg !== "HS256") {
|
|
72
|
+
throw new Error(`Unsupported JWT alg ${header.alg ?? "unknown"}.`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const claims = decodeJson<JwtClaims>(encodedPayload);
|
|
76
|
+
const expectedSignature = createHmac("sha256", jwtSecret)
|
|
77
|
+
.update(`${encodedHeader}.${encodedPayload}`)
|
|
78
|
+
.digest("base64url");
|
|
79
|
+
|
|
80
|
+
if (!constantTimeEqual(encodedSignature, expectedSignature)) {
|
|
81
|
+
throw new Error("JWT signature verification failed.");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const now = Math.floor(Date.now() / 1000);
|
|
85
|
+
if (typeof claims.exp === "number" && claims.exp <= now) {
|
|
86
|
+
throw new Error("JWT is expired.");
|
|
87
|
+
}
|
|
88
|
+
if (typeof claims.nbf === "number" && claims.nbf > now) {
|
|
89
|
+
throw new Error("JWT is not yet valid.");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return claims;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function decodeJson<T>(encoded: string): T {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")) as T;
|
|
98
|
+
} catch {
|
|
99
|
+
throw new Error("JWT contains invalid base64url JSON.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let cachedJwtSecret: string | null = null;
|
|
104
|
+
let ssmClient: SSMClient | null = null;
|
|
105
|
+
|
|
106
|
+
async function getJwtSecret(): Promise<string> {
|
|
107
|
+
if (cachedJwtSecret) return cachedJwtSecret;
|
|
108
|
+
|
|
109
|
+
const directSecret = normalizeOptionalString(process.env.PAPYRUS_JWT_SECRET);
|
|
110
|
+
if (directSecret && !isAmplifySecretPlaceholder(directSecret)) {
|
|
111
|
+
cachedJwtSecret = directSecret;
|
|
112
|
+
return directSecret;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const parameterName = resolveAmplifySsmSecretPath("PAPYRUS_JWT_SECRET");
|
|
116
|
+
if (!parameterName) throw new Error("PAPYRUS_JWT_SECRET is not configured.");
|
|
117
|
+
ssmClient ??= new SSMClient({});
|
|
118
|
+
const response = await ssmClient.send(new GetParameterCommand({
|
|
119
|
+
Name: parameterName,
|
|
120
|
+
WithDecryption: true,
|
|
121
|
+
}));
|
|
122
|
+
const secret = normalizeOptionalString(response.Parameter?.Value);
|
|
123
|
+
if (!secret) throw new Error(`SSM parameter ${parameterName} did not return a value.`);
|
|
124
|
+
cachedJwtSecret = secret;
|
|
125
|
+
return secret;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isAmplifySecretPlaceholder(value: string): boolean {
|
|
129
|
+
return /^<.*will be resolved.*>$/i.test(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveAmplifySsmSecretPath(name: string): string | null {
|
|
133
|
+
const rawConfig = normalizeOptionalString(process.env.AMPLIFY_SSM_ENV_CONFIG);
|
|
134
|
+
if (!rawConfig) return null;
|
|
135
|
+
try {
|
|
136
|
+
const config = JSON.parse(rawConfig) as Record<string, { path?: string; sharedPath?: string } | undefined>;
|
|
137
|
+
return normalizeOptionalString(config[name]?.path) ?? normalizeOptionalString(config[name]?.sharedPath);
|
|
138
|
+
} catch {
|
|
139
|
+
throw new Error("AMPLIFY_SSM_ENV_CONFIG contains invalid JSON.");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeOptionalString(value: unknown): string | null {
|
|
144
|
+
if (typeof value !== "string") return null;
|
|
145
|
+
const trimmed = value.trim();
|
|
146
|
+
return trimmed ? trimmed : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function assertExpectedIssuer(claims: JwtClaims): void {
|
|
150
|
+
const expectedIssuer = process.env.PAPYRUS_JWT_ISSUER;
|
|
151
|
+
if (expectedIssuer && claims.iss !== expectedIssuer) {
|
|
152
|
+
throw new Error("JWT issuer is not authorized.");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function assertExpectedAudience(claims: JwtClaims): void {
|
|
157
|
+
const expectedAudience = process.env.PAPYRUS_JWT_AUDIENCE;
|
|
158
|
+
if (!expectedAudience) return;
|
|
159
|
+
|
|
160
|
+
const audiences = Array.isArray(claims.aud) ? claims.aud : claims.aud ? [claims.aud] : [];
|
|
161
|
+
if (!audiences.includes(expectedAudience)) {
|
|
162
|
+
throw new Error("JWT audience is not authorized.");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function assertRequiredScope(claims: JwtClaims): void {
|
|
167
|
+
const requiredScope = process.env.PAPYRUS_JWT_REQUIRED_SCOPE;
|
|
168
|
+
if (!requiredScope) return;
|
|
169
|
+
|
|
170
|
+
const values = new Set([
|
|
171
|
+
...getClaimValues(claims.scope),
|
|
172
|
+
...getClaimValues(claims.scp),
|
|
173
|
+
...getClaimValues(claims.groups),
|
|
174
|
+
...getClaimValues(claims.roles),
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
if (!values.has(requiredScope)) {
|
|
178
|
+
throw new Error("JWT does not include the required authoring scope.");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getClaimValues(value: unknown): string[] {
|
|
183
|
+
if (Array.isArray(value)) return value.flatMap(getClaimValues);
|
|
184
|
+
if (typeof value === "string") return value.split(/[,\s]+/).filter(Boolean);
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function constantTimeEqual(left: string, right: string): boolean {
|
|
189
|
+
const leftBuffer = Buffer.from(left);
|
|
190
|
+
const rightBuffer = Buffer.from(right);
|
|
191
|
+
if (leftBuffer.length !== rightBuffer.length) return false;
|
|
192
|
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
193
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineFunction, secret } from "@aws-amplify/backend";
|
|
2
|
+
export const graphqlJwtAuthorizer = defineFunction({
|
|
3
|
+
name: "papyrus-graphql-jwt-authorizer",
|
|
4
|
+
entry: "./handler.ts",
|
|
5
|
+
timeoutSeconds: 5,
|
|
6
|
+
memoryMB: 256,
|
|
7
|
+
resourceGroupName: "data",
|
|
8
|
+
environment: {
|
|
9
|
+
PAPYRUS_JWT_SECRET: secret("PAPYRUS_JWT_SECRET"),
|
|
10
|
+
PAPYRUS_JWT_ISSUER: "papyrus-cli",
|
|
11
|
+
PAPYRUS_JWT_AUDIENCE: "papyrus-authoring",
|
|
12
|
+
PAPYRUS_JWT_REQUIRED_SCOPE: "papyrus:write",
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineFunction, secret } from "@aws-amplify/backend";
|
|
2
|
+
|
|
3
|
+
export const graphqlJwtAuthorizer = defineFunction({
|
|
4
|
+
name: "papyrus-graphql-jwt-authorizer",
|
|
5
|
+
entry: "./handler.ts",
|
|
6
|
+
timeoutSeconds: 5,
|
|
7
|
+
memoryMB: 256,
|
|
8
|
+
resourceGroupName: "data",
|
|
9
|
+
environment: {
|
|
10
|
+
PAPYRUS_JWT_SECRET: secret("PAPYRUS_JWT_SECRET"),
|
|
11
|
+
PAPYRUS_JWT_ISSUER: "papyrus-cli",
|
|
12
|
+
PAPYRUS_JWT_AUDIENCE: "papyrus-authoring",
|
|
13
|
+
PAPYRUS_JWT_REQUIRED_SCOPE: "papyrus:write",
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from papyrus_knowledge_query.lambda_handler import handler
|