@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.
Files changed (282) hide show
  1. package/LICENSE +21 -0
  2. package/amplify/auth/resource.js +61 -0
  3. package/amplify/auth/resource.ts +65 -0
  4. package/amplify/data/resource.js +2069 -0
  5. package/amplify/data/resource.ts +2185 -0
  6. package/amplify/data/schema.js +1882 -0
  7. package/amplify/data/schema.ts +1990 -0
  8. package/amplify/functions/assignment-action/handler.ts +1416 -0
  9. package/amplify/functions/assignment-action/resource.js +8 -0
  10. package/amplify/functions/assignment-action/resource.ts +9 -0
  11. package/amplify/functions/category-action/handler.ts +2259 -0
  12. package/amplify/functions/category-action/resource.js +8 -0
  13. package/amplify/functions/category-action/resource.ts +9 -0
  14. package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
  15. package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
  16. package/amplify/functions/console-chat-responder/Dockerfile +19 -0
  17. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
  18. package/amplify/functions/console-chat-responder/resource.js +153 -0
  19. package/amplify/functions/console-chat-responder/resource.ts +179 -0
  20. package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
  21. package/amplify/functions/content-actions/handler.py +149 -0
  22. package/amplify/functions/content-actions/requirements.txt +4 -0
  23. package/amplify/functions/content-actions/resource.js +40 -0
  24. package/amplify/functions/content-actions/resource.ts +54 -0
  25. package/amplify/functions/email-submission-processor/handler.py +46 -0
  26. package/amplify/functions/email-submission-processor/requirements.txt +2 -0
  27. package/amplify/functions/email-submission-processor/resource.js +56 -0
  28. package/amplify/functions/email-submission-processor/resource.ts +69 -0
  29. package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
  30. package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
  31. package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
  32. package/amplify/functions/knowledge-query/handler.py +1 -0
  33. package/amplify/functions/knowledge-query/requirements.txt +3 -0
  34. package/amplify/functions/knowledge-query/resource.js +50 -0
  35. package/amplify/functions/knowledge-query/resource.ts +61 -0
  36. package/amplify/functions/manage-user-role/handler.ts +799 -0
  37. package/amplify/functions/manage-user-role/resource.js +8 -0
  38. package/amplify/functions/manage-user-role/resource.ts +9 -0
  39. package/amplify/functions/model-attachment-upload/handler.ts +387 -0
  40. package/amplify/functions/model-attachment-upload/resource.js +8 -0
  41. package/amplify/functions/model-attachment-upload/resource.ts +9 -0
  42. package/amplify/functions/newsroom-summary/handler.ts +321 -0
  43. package/amplify/functions/newsroom-summary/resource.js +8 -0
  44. package/amplify/functions/newsroom-summary/resource.ts +9 -0
  45. package/amplify/functions/procedure-action/handler.ts +621 -0
  46. package/amplify/functions/procedure-action/resource.js +8 -0
  47. package/amplify/functions/procedure-action/resource.ts +9 -0
  48. package/amplify/functions/reader-settings/handler.ts +322 -0
  49. package/amplify/functions/reader-settings/resource.js +8 -0
  50. package/amplify/functions/reader-settings/resource.ts +9 -0
  51. package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
  52. package/amplify/functions/ses-inbound-receive/resource.js +8 -0
  53. package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
  54. package/amplify/functions/shared/amplify-secrets.ts +65 -0
  55. package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
  56. package/amplify/functions/shared/email-mime-intake.ts +196 -0
  57. package/amplify/functions/shared/email-submission.ts +415 -0
  58. package/amplify/functions/shared/inbound-email-intake.ts +37 -0
  59. package/amplify/functions/shared/lambda-data-client.ts +21 -0
  60. package/amplify/functions/shared/model-payloads.ts +186 -0
  61. package/amplify/functions/shared/python-bundle.js +130 -0
  62. package/amplify/functions/shared/python-bundle.ts +162 -0
  63. package/amplify/functions/shared/slack-events.ts +111 -0
  64. package/amplify/functions/slack-delivery/handler.py +112 -0
  65. package/amplify/functions/slack-delivery/requirements.txt +2 -0
  66. package/amplify/functions/slack-delivery/resource.js +38 -0
  67. package/amplify/functions/slack-delivery/resource.ts +51 -0
  68. package/amplify/functions/slack-events/handler.ts +155 -0
  69. package/amplify/functions/slack-events/resource.js +11 -0
  70. package/amplify/functions/slack-events/resource.ts +12 -0
  71. package/amplify/inbound-email/stack.js +46 -0
  72. package/amplify/inbound-email/stack.ts +61 -0
  73. package/amplify/package.json +3 -0
  74. package/amplify/rebuild-trigger-settings.js +22 -0
  75. package/amplify/rebuild-trigger-settings.ts +42 -0
  76. package/amplify/site-backend-config.js +1 -0
  77. package/amplify/site-backend-config.ts +36 -0
  78. package/amplify/site-backend.js +615 -0
  79. package/amplify/site-backend.ts +739 -0
  80. package/amplify/storage/resource.js +21 -0
  81. package/amplify/storage/resource.ts +22 -0
  82. package/app/%5F_preview/[[...path]]/route.ts +8 -0
  83. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
  84. package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
  85. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
  86. package/app/[year]/[month]/[day]/page.tsx +22 -0
  87. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
  88. package/app/api/archive/editions/route.ts +18 -0
  89. package/app/api/media/[...path]/route.ts +35 -0
  90. package/app/api/revalidate/route.ts +94 -0
  91. package/app/archive/page.tsx +24 -0
  92. package/app/articles/[slug]/page.tsx +35 -0
  93. package/app/globals.css +10345 -0
  94. package/app/layout.tsx +142 -0
  95. package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
  96. package/app/newsroom/[section]/page.tsx +47 -0
  97. package/app/newsroom/articles/[id]/page.tsx +15 -0
  98. package/app/newsroom/articles/page.tsx +13 -0
  99. package/app/newsroom/forum/[threadId]/page.tsx +35 -0
  100. package/app/newsroom/insights/[threadId]/page.tsx +22 -0
  101. package/app/newsroom/insights/page.tsx +14 -0
  102. package/app/newsroom/layout.tsx +12 -0
  103. package/app/newsroom/page.tsx +31 -0
  104. package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
  105. package/app/page.tsx +272 -0
  106. package/app/papyrus-theme.css +11 -0
  107. package/app/robots.ts +11 -0
  108. package/app/settings/page.tsx +14 -0
  109. package/app/tailwind.css +65 -0
  110. package/bin/papyrus-app.mjs +20 -0
  111. package/bin/papyrus-infra.mjs +95 -0
  112. package/components/ai-elements/conversation.tsx +168 -0
  113. package/components/ai-elements/message.tsx +360 -0
  114. package/components/ai-elements/model-selector.tsx +30 -0
  115. package/components/ai-elements/prompt-input.tsx +1461 -0
  116. package/components/ai-elements/shimmer.tsx +76 -0
  117. package/components/ai-elements/suggestion.tsx +54 -0
  118. package/components/ai-elements/tool.tsx +173 -0
  119. package/components/amplify-client-provider.tsx +140 -0
  120. package/components/archive-grid.tsx +89 -0
  121. package/components/archive-shell.tsx +41 -0
  122. package/components/article-video.tsx +156 -0
  123. package/components/blog-page-background.tsx +20 -0
  124. package/components/dev-sandbox-editor-sign-in.tsx +60 -0
  125. package/components/markus-ir-preview.tsx +300 -0
  126. package/components/news-desk-client-provider.tsx +211 -0
  127. package/components/news-desk-page.tsx +116 -0
  128. package/components/news-desk-taxonomy-client.ts +3684 -0
  129. package/components/newspaper.tsx +1929 -0
  130. package/components/newsroom-access-gate.tsx +66 -0
  131. package/components/newsroom-article-editor.tsx +741 -0
  132. package/components/newsroom-articles-view.tsx +129 -0
  133. package/components/newsroom-assignments-view.tsx +664 -0
  134. package/components/newsroom-ops-overview.tsx +226 -0
  135. package/components/newsroom-ops-shell.tsx +321 -0
  136. package/components/newsroom-references-view.tsx +598 -0
  137. package/components/newsroom-topics-view.tsx +425 -0
  138. package/components/papyrus-console-shell.tsx +1912 -0
  139. package/components/pictogram-figure.tsx +65 -0
  140. package/components/presentation-footer.tsx +146 -0
  141. package/components/reader-auth-control.tsx +162 -0
  142. package/components/reader-auth-state.ts +122 -0
  143. package/components/reader-settings.ts +332 -0
  144. package/components/reader-theme-control.tsx +85 -0
  145. package/components/reference-source-preview.tsx +163 -0
  146. package/components/settings-page.tsx +239 -0
  147. package/components/staging-banner.tsx +11 -0
  148. package/components/topic-steering-workspace.tsx +18005 -0
  149. package/components/ui/badge.tsx +30 -0
  150. package/components/ui/button-group.tsx +87 -0
  151. package/components/ui/button.tsx +58 -0
  152. package/components/ui/card.tsx +32 -0
  153. package/components/ui/command.tsx +194 -0
  154. package/components/ui/dialog.tsx +158 -0
  155. package/components/ui/dropdown-menu.tsx +266 -0
  156. package/components/ui/hover-card.tsx +51 -0
  157. package/components/ui/input-group.tsx +158 -0
  158. package/components/ui/input.tsx +20 -0
  159. package/components/ui/scroll-area.tsx +16 -0
  160. package/components/ui/select.tsx +199 -0
  161. package/components/ui/separator.tsx +23 -0
  162. package/components/ui/sheet.tsx +92 -0
  163. package/components/ui/spinner.tsx +10 -0
  164. package/components/ui/tabs.tsx +113 -0
  165. package/components/ui/textarea.tsx +18 -0
  166. package/components/ui/tooltip.tsx +66 -0
  167. package/components/use-resolved-papyrus-theme.ts +47 -0
  168. package/infra/amplify-app-shell.js +234 -0
  169. package/infra/amplify-app-shell.ts +284 -0
  170. package/infra/build-specs.js +85 -0
  171. package/infra/build-specs.ts +131 -0
  172. package/infra/github-oidc-provider.js +24 -0
  173. package/infra/github-oidc-provider.ts +30 -0
  174. package/infra/index.js +15 -0
  175. package/infra/site-config.js +186 -0
  176. package/infra/site-config.ts +235 -0
  177. package/lib/amplify-outputs-guard.ts +24 -0
  178. package/lib/amplify-outputs-path.ts +16 -0
  179. package/lib/amplify-server-runtime.ts +63 -0
  180. package/lib/archive-data.ts +30 -0
  181. package/lib/archive-types.ts +15 -0
  182. package/lib/articles.ts +320 -0
  183. package/lib/cached-content-repository.ts +90 -0
  184. package/lib/category-dashboard.ts +37 -0
  185. package/lib/category-repository.ts +1907 -0
  186. package/lib/console-chat-client.ts +1316 -0
  187. package/lib/console-web-context.ts +118 -0
  188. package/lib/content-actions-client.ts +98 -0
  189. package/lib/content-actions-demo.ts +190 -0
  190. package/lib/content-load-timing.ts +23 -0
  191. package/lib/content-repository.ts +43 -0
  192. package/lib/content-source-context.ts +26 -0
  193. package/lib/content-types.ts +104 -0
  194. package/lib/define-site.js +3 -0
  195. package/lib/define-site.ts +21 -0
  196. package/lib/demo-amplify-outputs.ts +15 -0
  197. package/lib/dev-sandbox-editor-auth.ts +18 -0
  198. package/lib/doctrine.ts +128 -0
  199. package/lib/edition-routes.ts +155 -0
  200. package/lib/edition-sections.ts +111 -0
  201. package/lib/empty-edition-layout-plan.ts +109 -0
  202. package/lib/empty-theme.css +1 -0
  203. package/lib/excerpts.ts +8 -0
  204. package/lib/graphql-content-repository.ts +1140 -0
  205. package/lib/image-url.ts +10 -0
  206. package/lib/layout-plan.ts +981 -0
  207. package/lib/layout-scenarios.ts +564 -0
  208. package/lib/markus-body.ts +168 -0
  209. package/lib/markus-ir.ts +235 -0
  210. package/lib/markus-projection.ts +243 -0
  211. package/lib/markus-to-article.ts +93 -0
  212. package/lib/news-desk-session.ts +160 -0
  213. package/lib/newspaper-layout.ts +3459 -0
  214. package/lib/newsroom-articles.ts +325 -0
  215. package/lib/newsroom-assignments.ts +173 -0
  216. package/lib/newsroom-base-path.ts +48 -0
  217. package/lib/newsroom-card-layout.ts +102 -0
  218. package/lib/newsroom-category-drilldown.ts +133 -0
  219. package/lib/newsroom-demo-articles.ts +90 -0
  220. package/lib/newsroom-demo-dashboard.ts +267 -0
  221. package/lib/newsroom-demo-profile.ts +46 -0
  222. package/lib/newsroom-forum-routes.ts +153 -0
  223. package/lib/newsroom-index-filters.ts +202 -0
  224. package/lib/newsroom-knowledge-query-request.ts +51 -0
  225. package/lib/newsroom-list-selection.ts +42 -0
  226. package/lib/newsroom-media-backend.ts +142 -0
  227. package/lib/newsroom-media.ts +182 -0
  228. package/lib/newsroom-nav.ts +44 -0
  229. package/lib/newsroom-references.ts +116 -0
  230. package/lib/newsroom-sections.ts +52 -0
  231. package/lib/newsroom-topics.ts +118 -0
  232. package/lib/papyrus-object-kinds.ts +24 -0
  233. package/lib/papyrus-web-locations.ts +480 -0
  234. package/lib/pilobol-sample.ts +167 -0
  235. package/lib/presentation-footer.ts +92 -0
  236. package/lib/pretext-layout.ts +179 -0
  237. package/lib/public-placeholder-config.ts +102 -0
  238. package/lib/publication-items.ts +181 -0
  239. package/lib/reader-route-config.ts +1 -0
  240. package/lib/reader-static-params.ts +56 -0
  241. package/lib/reader-storage-url.ts +63 -0
  242. package/lib/reference-display.ts +74 -0
  243. package/lib/reference-policy.ts +97 -0
  244. package/lib/reference-source-preview.ts +175 -0
  245. package/lib/renderer-config.ts +50 -0
  246. package/lib/renderer.ts +87 -0
  247. package/lib/reporting-story-budget.ts +638 -0
  248. package/lib/semantic-graph.ts +554 -0
  249. package/lib/site-brand-tenant.ts +22 -0
  250. package/lib/site-brand.ts +194 -0
  251. package/lib/site-env.ts +52 -0
  252. package/lib/site-renderer.ts +17 -0
  253. package/lib/site-stack.ts +195 -0
  254. package/lib/staging-gate.ts +37 -0
  255. package/lib/staging-preview-object.ts +98 -0
  256. package/lib/themed-image.ts +30 -0
  257. package/lib/utils.ts +6 -0
  258. package/lib/video-mode.ts +42 -0
  259. package/lib/video-script.ts +63 -0
  260. package/middleware.ts +103 -0
  261. package/package.json +318 -1
  262. package/renderers/markus/index.ts +25 -0
  263. package/renderers/markus/stubs.tsx +33 -0
  264. package/renderers/pretext/article-page.tsx +169 -0
  265. package/renderers/pretext/index.ts +30 -0
  266. package/renderers/pretext/layouts.ts +16 -0
  267. package/renderers/pretext/presentation-shell.tsx +470 -0
  268. package/routes.manifest.json +208 -0
  269. package/scripts/fixtures/pilobol-sample.ast.json +234 -0
  270. package/scripts/videoml/build-browser-bundle.mjs +22 -0
  271. package/scripts/videoml/build-preview-bundle.mjs +50 -0
  272. package/scripts/videoml/bundle-common.mjs +129 -0
  273. package/scripts/videoml/preview/preview-entry.tsx +381 -0
  274. package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
  275. package/scripts/videoml/preview/preview-timing.ts +42 -0
  276. package/scripts/videoml/preview.html +77 -0
  277. package/scripts/videoml/standard-entry.tsx +46 -0
  278. package/src/backend.js +1 -0
  279. package/src/backend.ts +3 -0
  280. package/src/sync.mjs +87 -0
  281. package/src/with-papyrus.d.mts +2 -0
  282. package/src/with-papyrus.mjs +60 -0
@@ -0,0 +1,8 @@
1
+ import { defineFunction } from "@aws-amplify/backend";
2
+ export const manageUserRole = defineFunction({
3
+ name: "papyrus-manage-user-role",
4
+ entry: "./handler.ts",
5
+ timeoutSeconds: 10,
6
+ memoryMB: 256,
7
+ resourceGroupName: "auth",
8
+ });
@@ -0,0 +1,9 @@
1
+ import { defineFunction } from "@aws-amplify/backend";
2
+
3
+ export const manageUserRole = defineFunction({
4
+ name: "papyrus-manage-user-role",
5
+ entry: "./handler.ts",
6
+ timeoutSeconds: 10,
7
+ memoryMB: 256,
8
+ resourceGroupName: "auth",
9
+ });
@@ -0,0 +1,387 @@
1
+ import { createHash } from "node:crypto";
2
+ import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
3
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
4
+ import { getAmplifyDataClientConfig } from "@aws-amplify/backend/function/runtime";
5
+ import { Amplify } from "aws-amplify";
6
+ import { generateClient } from "aws-amplify/data";
7
+ import type { Schema } from "../../data/resource";
8
+
9
+ type CreateUploadHandler = Schema["createModelAttachmentUpload"]["functionHandler"];
10
+ type CompleteUploadHandler = Schema["completeModelAttachmentUpload"]["functionHandler"];
11
+ type AbortUploadHandler = Schema["abortModelAttachmentUpload"]["functionHandler"];
12
+ type CreateDownloadHandler = Schema["createModelAttachmentDownload"]["functionHandler"];
13
+ type UploadEvent =
14
+ | Parameters<CreateUploadHandler>[0]
15
+ | Parameters<CompleteUploadHandler>[0]
16
+ | Parameters<AbortUploadHandler>[0]
17
+ | Parameters<CreateDownloadHandler>[0];
18
+ type DataClient = ReturnType<typeof generateClient<Schema>>;
19
+ type DataClientErrors = Array<{ message?: string | null } | string | null> | null | undefined;
20
+
21
+ const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
22
+ const MAX_GRAPH_EXPORT_ATTACHMENT_BYTES = 512 * 1024 * 1024;
23
+ const UPLOAD_EXPIRES_SECONDS = 10 * 60;
24
+ const DOWNLOAD_EXPIRES_SECONDS = 10 * 60;
25
+ const ALLOWED_OWNER_KINDS = new Set([
26
+ "assignment",
27
+ "assignmentEvent",
28
+ "item",
29
+ "knowledgeRawPayload",
30
+ "message",
31
+ "publishedItem",
32
+ "procedureVersion",
33
+ "reference",
34
+ "semanticNode",
35
+ "semanticRelation",
36
+ ]);
37
+ const ALLOWED_ROLES = new Set([
38
+ "assignment_brief",
39
+ "assignment_instructions",
40
+ "item_body",
41
+ "item_excerpt",
42
+ "message_body",
43
+ "metadata",
44
+ "ontology_concept_profile",
45
+ "ontology_relation_explanation",
46
+ "published_item_body",
47
+ "published_item_excerpt",
48
+ "code",
49
+ "graph_export",
50
+ "raw_payload",
51
+ ]);
52
+ const ALLOWED_MEDIA_TYPES = new Set([
53
+ "application/json",
54
+ "application/gzip",
55
+ "application/x-ndjson",
56
+ "text/csv",
57
+ "text/markdown",
58
+ "text/plain",
59
+ "text/tab-separated-values",
60
+ ]);
61
+
62
+ const s3 = new S3Client({});
63
+ let clientPromise: Promise<DataClient> | null = null;
64
+
65
+ export const handler = async (event: UploadEvent) => {
66
+ const fieldName = normalizeRequiredString(event.info?.fieldName ?? (event as { fieldName?: string | null }).fieldName, "fieldName");
67
+ if (fieldName === "createModelAttachmentUpload") return createModelAttachmentUpload(event as Parameters<CreateUploadHandler>[0]);
68
+ if (fieldName === "completeModelAttachmentUpload") return completeModelAttachmentUpload(event as Parameters<CompleteUploadHandler>[0]);
69
+ if (fieldName === "abortModelAttachmentUpload") return abortModelAttachmentUpload(event as Parameters<AbortUploadHandler>[0]);
70
+ if (fieldName === "createModelAttachmentDownload") return createModelAttachmentDownload(event as Parameters<CreateDownloadHandler>[0]);
71
+ throw new Error(`Unsupported model attachment upload action ${fieldName}.`);
72
+ };
73
+
74
+ async function createModelAttachmentUpload(event: Parameters<CreateUploadHandler>[0]) {
75
+ const input = normalizeAttachmentInput(event.arguments);
76
+ await assertOwnerExists(input.ownerKind, input.ownerId);
77
+ const put = new PutObjectCommand({
78
+ Bucket: payloadBucketName(),
79
+ Key: input.storagePath,
80
+ ContentType: input.mediaType,
81
+ });
82
+ const uploadUrl = await getSignedUrl(s3, put, { expiresIn: UPLOAD_EXPIRES_SECONDS });
83
+ const expiresAt = new Date(Date.now() + UPLOAD_EXPIRES_SECONDS * 1000).toISOString();
84
+ const requiredHeaders: Record<string, string> = { "content-type": input.mediaType };
85
+ return {
86
+ ok: true,
87
+ uploadId: uploadIdFor(input),
88
+ attachmentId: input.id,
89
+ ownerKind: input.ownerKind,
90
+ ownerId: input.ownerId,
91
+ role: input.role,
92
+ sortKey: input.sortKey,
93
+ method: "PUT",
94
+ uploadUrl,
95
+ storagePath: input.storagePath,
96
+ mediaType: input.mediaType,
97
+ byteSize: input.byteSize,
98
+ sha256: input.sha256,
99
+ expiresAt,
100
+ requiredHeaders,
101
+ };
102
+ }
103
+
104
+ async function completeModelAttachmentUpload(event: Parameters<CompleteUploadHandler>[0]) {
105
+ const input = normalizeAttachmentInput(event.arguments);
106
+ assertUploadId(event.arguments.uploadId, input);
107
+ await assertOwnerExists(input.ownerKind, input.ownerId);
108
+ const head = await s3.send(new HeadObjectCommand({
109
+ Bucket: payloadBucketName(),
110
+ Key: input.storagePath,
111
+ ChecksumMode: "ENABLED",
112
+ }));
113
+ if (typeof head.ContentLength === "number" && head.ContentLength !== input.byteSize) {
114
+ throw new Error(`Uploaded payload size mismatch for ${input.storagePath}: expected ${input.byteSize}, got ${head.ContentLength}.`);
115
+ }
116
+ const actualMediaType = normalizeOptionalString(head.ContentType)?.split(";")[0].trim().toLowerCase() ?? null;
117
+ if (actualMediaType && actualMediaType !== input.mediaType) {
118
+ throw new Error(`Uploaded payload media type mismatch for ${input.storagePath}: expected ${input.mediaType}, got ${actualMediaType}.`);
119
+ }
120
+ if (input.sha256) {
121
+ const actualChecksum = await readObjectSha256(input.storagePath);
122
+ if (actualChecksum !== input.sha256) {
123
+ throw new Error(`Uploaded payload checksum mismatch for ${input.storagePath}.`);
124
+ }
125
+ }
126
+ const now = new Date().toISOString();
127
+ const client = await getDataClient();
128
+ const current = await client.models.ModelAttachment.get({ id: input.id });
129
+ assertNoDataErrors(current.errors, "get ModelAttachment");
130
+ const record = {
131
+ id: input.id,
132
+ ownerKind: input.ownerKind,
133
+ ownerId: input.ownerId,
134
+ ownerLineageId: input.ownerLineageId,
135
+ ownerVersionNumber: input.ownerVersionNumber,
136
+ ownerVersionKey: input.ownerVersionKey,
137
+ role: input.role,
138
+ sortKey: input.sortKey,
139
+ storagePath: input.storagePath,
140
+ filename: input.filename,
141
+ mediaType: input.mediaType,
142
+ byteSize: input.byteSize,
143
+ sha256: input.sha256,
144
+ etag: normalizeOptionalString(head.ETag)?.replace(/^"|"$/g, "") ?? null,
145
+ importRunId: input.importRunId,
146
+ createdAt: current.data?.createdAt ?? now,
147
+ updatedAt: now,
148
+ status: input.status,
149
+ };
150
+ if (current.data) {
151
+ await requireDataResult(client.models.ModelAttachment.update(record), "update ModelAttachment");
152
+ } else {
153
+ await requireDataResult(client.models.ModelAttachment.create(record), "create ModelAttachment");
154
+ }
155
+ return record;
156
+ }
157
+
158
+ async function abortModelAttachmentUpload(event: Parameters<AbortUploadHandler>[0]) {
159
+ const input = normalizeAttachmentInput(event.arguments);
160
+ assertUploadId(event.arguments.uploadId, input);
161
+ await assertOwnerExists(input.ownerKind, input.ownerId);
162
+ await s3.send(new DeleteObjectCommand({
163
+ Bucket: payloadBucketName(),
164
+ Key: input.storagePath,
165
+ }));
166
+ return {
167
+ ok: true,
168
+ uploadId: event.arguments.uploadId,
169
+ attachmentId: input.id,
170
+ storagePath: input.storagePath,
171
+ status: "aborted",
172
+ };
173
+ }
174
+
175
+ async function createModelAttachmentDownload(event: Parameters<CreateDownloadHandler>[0]) {
176
+ const attachmentId = normalizeRequiredString(event.arguments.attachmentId, "attachmentId");
177
+ const client = await getDataClient();
178
+ const result = await client.models.ModelAttachment.get({ id: attachmentId });
179
+ assertNoDataErrors(result.errors, "get ModelAttachment");
180
+ const attachment = result.data;
181
+ if (!attachment) throw new Error(`ModelAttachment ${attachmentId} was not found.`);
182
+ const storagePath = normalizeRequiredString(attachment.storagePath, "storagePath");
183
+ if (!storagePath.startsWith("newsroom/payloads/") && !storagePath.startsWith("media/payloads/")) {
184
+ throw new Error(`ModelAttachment ${attachmentId} is not a supported payload attachment.`);
185
+ }
186
+ if (attachment.status && attachment.status !== "active") {
187
+ throw new Error(`ModelAttachment ${attachmentId} is ${attachment.status}, not active.`);
188
+ }
189
+ const get = new GetObjectCommand({
190
+ Bucket: payloadBucketName(),
191
+ Key: storagePath,
192
+ });
193
+ const downloadUrl = await getSignedUrl(s3, get, { expiresIn: DOWNLOAD_EXPIRES_SECONDS });
194
+ return {
195
+ ok: true,
196
+ attachmentId,
197
+ method: "GET",
198
+ downloadUrl,
199
+ storagePath,
200
+ mediaType: normalizeRequiredString(attachment.mediaType, "mediaType"),
201
+ byteSize: attachment.byteSize ?? 0,
202
+ sha256: attachment.sha256 ?? null,
203
+ expiresAt: new Date(Date.now() + DOWNLOAD_EXPIRES_SECONDS * 1000).toISOString(),
204
+ requiredHeaders: {},
205
+ };
206
+ }
207
+
208
+ async function assertOwnerExists(ownerKind: string, ownerId: string): Promise<void> {
209
+ const client = await getDataClient();
210
+ const modelName = ownerModelName(ownerKind);
211
+ const result = await (client.models as any)[modelName].get({ id: ownerId });
212
+ assertNoDataErrors(result.errors, `get ${modelName}`);
213
+ if (!result.data) throw new Error(`Cannot attach payload to missing ${modelName} ${ownerId}.`);
214
+ }
215
+
216
+ function normalizeAttachmentInput(input: Record<string, unknown>) {
217
+ const ownerKind = normalizeRequiredString(input.ownerKind, "ownerKind");
218
+ if (!ALLOWED_OWNER_KINDS.has(ownerKind)) throw new Error(`Unsupported ModelAttachment ownerKind ${ownerKind}.`);
219
+ const ownerId = normalizeRequiredString(input.ownerId, "ownerId");
220
+ const role = normalizeRequiredString(input.role, "role");
221
+ if (!ALLOWED_ROLES.has(role)) throw new Error(`Unsupported ModelAttachment role ${role}.`);
222
+ const sortKey = normalizeOptionalString(input.sortKey) ?? role;
223
+ const filename = normalizeFilename(input.filename);
224
+ const mediaType = normalizeRequiredString(input.mediaType, "mediaType").toLowerCase();
225
+ if (!ALLOWED_MEDIA_TYPES.has(mediaType)) throw new Error(`Unsupported ModelAttachment mediaType ${mediaType}.`);
226
+ const byteSize = normalizeByteSize(input.byteSize, role);
227
+ const sha256 = normalizeSha256(input.sha256);
228
+ const id = modelAttachmentId(ownerKind, ownerId, role, sortKey);
229
+ const storagePath = modelPayloadStoragePath(ownerKind, ownerId, role, filename);
230
+ return {
231
+ id,
232
+ ownerKind,
233
+ ownerId,
234
+ ownerLineageId: normalizeOptionalString(input.ownerLineageId),
235
+ ownerVersionNumber: normalizeOptionalInteger(input.ownerVersionNumber),
236
+ ownerVersionKey: normalizeOptionalString(input.ownerVersionKey),
237
+ role,
238
+ sortKey,
239
+ storagePath,
240
+ filename,
241
+ mediaType,
242
+ byteSize,
243
+ sha256,
244
+ importRunId: normalizeOptionalString(input.importRunId),
245
+ status: normalizeOptionalString(input.status) ?? "active",
246
+ };
247
+ }
248
+
249
+ function assertUploadId(value: unknown, input: ReturnType<typeof normalizeAttachmentInput>): void {
250
+ const actual = normalizeRequiredString(value, "uploadId");
251
+ const expected = uploadIdFor(input);
252
+ if (actual !== expected) throw new Error("ModelAttachment uploadId does not match the requested attachment slot.");
253
+ }
254
+
255
+ function uploadIdFor(input: ReturnType<typeof normalizeAttachmentInput>): string {
256
+ const fingerprint = [
257
+ input.ownerKind,
258
+ input.ownerId,
259
+ input.role,
260
+ input.sortKey,
261
+ input.filename,
262
+ input.mediaType,
263
+ input.byteSize,
264
+ input.sha256 ?? "",
265
+ ].join("\n");
266
+ return `model-attachment-upload-${createHash("sha256").update(fingerprint).digest("hex").slice(0, 32)}`;
267
+ }
268
+
269
+ function ownerModelName(ownerKind: string): string {
270
+ if (ownerKind === "assignment") return "Assignment";
271
+ if (ownerKind === "assignmentEvent") return "AssignmentEvent";
272
+ if (ownerKind === "item") return "Item";
273
+ if (ownerKind === "knowledgeRawPayload") return "KnowledgeRawPayload";
274
+ if (ownerKind === "message") return "Message";
275
+ if (ownerKind === "publishedItem") return "PublishedItem";
276
+ if (ownerKind === "procedureVersion") return "ProcedureVersion";
277
+ if (ownerKind === "reference") return "Reference";
278
+ if (ownerKind === "semanticNode") return "SemanticNode";
279
+ if (ownerKind === "semanticRelation") return "SemanticRelation";
280
+ throw new Error(`Unsupported ModelAttachment ownerKind ${ownerKind}.`);
281
+ }
282
+
283
+ async function getDataClient(): Promise<DataClient> {
284
+ if (!clientPromise) {
285
+ clientPromise = (async () => {
286
+ const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig(process.env as never);
287
+ Amplify.configure(resourceConfig, libraryOptions);
288
+ return generateClient<Schema>();
289
+ })();
290
+ }
291
+ return clientPromise;
292
+ }
293
+
294
+ function payloadBucketName(): string {
295
+ const bucket = process.env.papyrusMedia_BUCKET_NAME
296
+ ?? process.env.PAPYRUS_MEDIA_BUCKET_NAME
297
+ ?? process.env.STORAGE_BUCKET_NAME
298
+ ?? process.env.AMPLIFY_STORAGE_BUCKET_NAME;
299
+ if (!bucket) throw new Error("Missing Papyrus storage bucket environment variable for model payload attachments.");
300
+ return bucket;
301
+ }
302
+
303
+ function modelPayloadStoragePath(ownerKind: string, ownerId: string, role: string, filename: string): string {
304
+ const rootPrefix = ownerKind === "publishedItem" ? "media/payloads" : "newsroom/payloads";
305
+ return `${rootPrefix}/${safeId(ownerKind)}/${safeId(ownerId)}/${safeId(role)}/${filename}`;
306
+ }
307
+
308
+ function modelAttachmentId(ownerKind: string, ownerId: string, role: string, sortKey: string): string {
309
+ return `model-attachment-${safeId(ownerKind)}-${safeId(ownerId)}-${safeId(role)}-${safeId(sortKey)}`;
310
+ }
311
+
312
+ function safeId(value: unknown): string {
313
+ const raw = String(value ?? "payload").trim();
314
+ const normalized = raw
315
+ .trim()
316
+ .toLowerCase()
317
+ .replace(/[^a-z0-9]+/g, "-")
318
+ .replace(/^-+|-+$/g, "") || "payload";
319
+ if (normalized.length <= 80) return normalized;
320
+ const digest = createHash("sha256").update(raw).digest("hex").slice(0, 12);
321
+ return `${normalized.slice(0, 67).replace(/-+$/g, "")}-${digest}`;
322
+ }
323
+
324
+ function normalizeFilename(value: unknown): string {
325
+ const filename = normalizeRequiredString(value, "filename");
326
+ if (filename.includes("/") || filename.includes("\\") || filename === "." || filename === "..") {
327
+ throw new Error("ModelAttachment filename must be a simple basename.");
328
+ }
329
+ if (!/^[A-Za-z0-9._-]+$/.test(filename)) throw new Error("ModelAttachment filename contains unsupported characters.");
330
+ return filename;
331
+ }
332
+
333
+ function normalizeByteSize(value: unknown, role: string): number {
334
+ const size = Number(value);
335
+ if (!Number.isInteger(size) || size < 0) throw new Error("ModelAttachment byteSize must be a non-negative integer.");
336
+ const maxBytes = role === "graph_export" ? MAX_GRAPH_EXPORT_ATTACHMENT_BYTES : MAX_ATTACHMENT_BYTES;
337
+ if (size > maxBytes) throw new Error(`ModelAttachment byteSize exceeds ${maxBytes} bytes for role ${role}.`);
338
+ return size;
339
+ }
340
+
341
+ function normalizeOptionalInteger(value: unknown): number | null {
342
+ if (value === null || value === undefined || value === "") return null;
343
+ const parsed = Number(value);
344
+ if (!Number.isInteger(parsed)) throw new Error("Expected integer value.");
345
+ return parsed;
346
+ }
347
+
348
+ function normalizeSha256(value: unknown): string | null {
349
+ const normalized = normalizeOptionalString(value);
350
+ if (!normalized) return null;
351
+ if (!/^[a-f0-9]{64}$/i.test(normalized)) throw new Error("ModelAttachment sha256 must be a 64-character hex digest.");
352
+ return normalized.toLowerCase();
353
+ }
354
+
355
+ async function readObjectSha256(storagePath: string): Promise<string> {
356
+ const result = await s3.send(new GetObjectCommand({
357
+ Bucket: payloadBucketName(),
358
+ Key: storagePath,
359
+ }));
360
+ const hash = createHash("sha256");
361
+ const body = result.Body as AsyncIterable<Uint8Array> | undefined;
362
+ if (!body) return hash.digest("hex");
363
+ for await (const chunk of body) hash.update(chunk);
364
+ return hash.digest("hex");
365
+ }
366
+
367
+ function normalizeRequiredString(value: unknown, label: string): string {
368
+ const normalized = normalizeOptionalString(value);
369
+ if (!normalized) throw new Error(`Missing required ${label}.`);
370
+ return normalized;
371
+ }
372
+
373
+ function normalizeOptionalString(value: unknown): string | null {
374
+ return typeof value === "string" && value.trim() ? value.trim() : null;
375
+ }
376
+
377
+ async function requireDataResult<T>(promise: Promise<{ data?: T | null; errors?: DataClientErrors }>, context: string): Promise<T | null | undefined> {
378
+ const result = await promise;
379
+ assertNoDataErrors(result.errors, context);
380
+ return result.data;
381
+ }
382
+
383
+ function assertNoDataErrors(errors: DataClientErrors, context: string): void {
384
+ if (Array.isArray(errors) && errors.length) {
385
+ throw new Error(`${context}: ${errors.map((error) => typeof error === "string" ? error : JSON.stringify(error)).join("; ")}`);
386
+ }
387
+ }
@@ -0,0 +1,8 @@
1
+ import { defineFunction } from "@aws-amplify/backend";
2
+ export const modelAttachmentUpload = defineFunction({
3
+ name: "papyrus-model-attachment-upload",
4
+ entry: "./handler.ts",
5
+ timeoutSeconds: 15,
6
+ memoryMB: 256,
7
+ resourceGroupName: "data",
8
+ });
@@ -0,0 +1,9 @@
1
+ import { defineFunction } from "@aws-amplify/backend";
2
+
3
+ export const modelAttachmentUpload = defineFunction({
4
+ name: "papyrus-model-attachment-upload",
5
+ entry: "./handler.ts",
6
+ timeoutSeconds: 15,
7
+ memoryMB: 256,
8
+ resourceGroupName: "data",
9
+ });