@anthusai/papyrus 0.0.0 → 1.0.0-next.2

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 (254) 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 +2037 -0
  5. package/amplify/data/resource.ts +2148 -0
  6. package/amplify/data/schema.js +1850 -0
  7. package/amplify/data/schema.ts +1953 -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/email-submission-processor/handler.py +46 -0
  22. package/amplify/functions/email-submission-processor/requirements.txt +2 -0
  23. package/amplify/functions/email-submission-processor/resource.js +56 -0
  24. package/amplify/functions/email-submission-processor/resource.ts +69 -0
  25. package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
  26. package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
  27. package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
  28. package/amplify/functions/knowledge-query/handler.py +1 -0
  29. package/amplify/functions/knowledge-query/requirements.txt +3 -0
  30. package/amplify/functions/knowledge-query/resource.js +50 -0
  31. package/amplify/functions/knowledge-query/resource.ts +61 -0
  32. package/amplify/functions/manage-user-role/handler.ts +799 -0
  33. package/amplify/functions/manage-user-role/resource.js +8 -0
  34. package/amplify/functions/manage-user-role/resource.ts +9 -0
  35. package/amplify/functions/model-attachment-upload/handler.ts +387 -0
  36. package/amplify/functions/model-attachment-upload/resource.js +8 -0
  37. package/amplify/functions/model-attachment-upload/resource.ts +9 -0
  38. package/amplify/functions/newsroom-summary/handler.ts +321 -0
  39. package/amplify/functions/newsroom-summary/resource.js +8 -0
  40. package/amplify/functions/newsroom-summary/resource.ts +9 -0
  41. package/amplify/functions/procedure-action/handler.ts +621 -0
  42. package/amplify/functions/procedure-action/resource.js +8 -0
  43. package/amplify/functions/procedure-action/resource.ts +9 -0
  44. package/amplify/functions/reader-settings/handler.ts +322 -0
  45. package/amplify/functions/reader-settings/resource.js +8 -0
  46. package/amplify/functions/reader-settings/resource.ts +9 -0
  47. package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
  48. package/amplify/functions/ses-inbound-receive/resource.js +8 -0
  49. package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
  50. package/amplify/functions/shared/amplify-secrets.ts +65 -0
  51. package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
  52. package/amplify/functions/shared/email-mime-intake.ts +196 -0
  53. package/amplify/functions/shared/email-submission.ts +415 -0
  54. package/amplify/functions/shared/inbound-email-intake.ts +37 -0
  55. package/amplify/functions/shared/lambda-data-client.ts +21 -0
  56. package/amplify/functions/shared/model-payloads.ts +186 -0
  57. package/amplify/functions/shared/python-bundle.js +115 -0
  58. package/amplify/functions/shared/python-bundle.ts +141 -0
  59. package/amplify/functions/shared/slack-events.ts +111 -0
  60. package/amplify/functions/slack-delivery/handler.py +112 -0
  61. package/amplify/functions/slack-delivery/requirements.txt +2 -0
  62. package/amplify/functions/slack-delivery/resource.js +38 -0
  63. package/amplify/functions/slack-delivery/resource.ts +51 -0
  64. package/amplify/functions/slack-events/handler.ts +155 -0
  65. package/amplify/functions/slack-events/resource.js +11 -0
  66. package/amplify/functions/slack-events/resource.ts +12 -0
  67. package/amplify/inbound-email/stack.js +46 -0
  68. package/amplify/inbound-email/stack.ts +61 -0
  69. package/amplify/package.json +3 -0
  70. package/amplify/site-backend-config.js +1 -0
  71. package/amplify/site-backend-config.ts +30 -0
  72. package/amplify/site-backend.js +587 -0
  73. package/amplify/site-backend.ts +704 -0
  74. package/amplify/storage/resource.js +18 -0
  75. package/amplify/storage/resource.ts +19 -0
  76. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
  77. package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
  78. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
  79. package/app/[year]/[month]/[day]/page.tsx +22 -0
  80. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
  81. package/app/api/archive/editions/route.ts +18 -0
  82. package/app/api/media/[...path]/route.ts +35 -0
  83. package/app/api/revalidate/route.ts +94 -0
  84. package/app/archive/page.tsx +24 -0
  85. package/app/articles/[slug]/page.tsx +35 -0
  86. package/app/globals.css +10329 -0
  87. package/app/layout.tsx +146 -0
  88. package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
  89. package/app/newsroom/[section]/page.tsx +47 -0
  90. package/app/newsroom/forum/[threadId]/page.tsx +35 -0
  91. package/app/newsroom/insights/[threadId]/page.tsx +22 -0
  92. package/app/newsroom/insights/page.tsx +14 -0
  93. package/app/newsroom/layout.tsx +12 -0
  94. package/app/newsroom/page.tsx +31 -0
  95. package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
  96. package/app/page.tsx +272 -0
  97. package/app/settings/page.tsx +14 -0
  98. package/app/tailwind.css +65 -0
  99. package/bin/papyrus-app.mjs +10 -0
  100. package/bin/papyrus-infra.mjs +39 -0
  101. package/components/ai-elements/conversation.tsx +168 -0
  102. package/components/ai-elements/message.tsx +360 -0
  103. package/components/ai-elements/model-selector.tsx +30 -0
  104. package/components/ai-elements/prompt-input.tsx +1461 -0
  105. package/components/ai-elements/shimmer.tsx +76 -0
  106. package/components/ai-elements/suggestion.tsx +54 -0
  107. package/components/ai-elements/tool.tsx +173 -0
  108. package/components/amplify-client-provider.tsx +140 -0
  109. package/components/archive-grid.tsx +89 -0
  110. package/components/archive-shell.tsx +41 -0
  111. package/components/article-video.tsx +47 -0
  112. package/components/blog-page-background.tsx +20 -0
  113. package/components/dev-sandbox-editor-sign-in.tsx +60 -0
  114. package/components/news-desk-client-provider.tsx +211 -0
  115. package/components/news-desk-page.tsx +119 -0
  116. package/components/news-desk-taxonomy-client.ts +3684 -0
  117. package/components/newspaper.tsx +1929 -0
  118. package/components/newsroom-assignments-view.tsx +664 -0
  119. package/components/newsroom-ops-overview.tsx +226 -0
  120. package/components/newsroom-ops-shell.tsx +319 -0
  121. package/components/newsroom-references-view.tsx +598 -0
  122. package/components/newsroom-topics-view.tsx +425 -0
  123. package/components/papyrus-console-shell.tsx +1912 -0
  124. package/components/pictogram-figure.tsx +65 -0
  125. package/components/presentation-footer.tsx +146 -0
  126. package/components/reader-auth-control.tsx +162 -0
  127. package/components/reader-auth-state.ts +122 -0
  128. package/components/reader-settings.ts +332 -0
  129. package/components/reader-theme-control.tsx +85 -0
  130. package/components/reference-source-preview.tsx +163 -0
  131. package/components/settings-page.tsx +239 -0
  132. package/components/topic-steering-workspace.tsx +18063 -0
  133. package/components/ui/badge.tsx +30 -0
  134. package/components/ui/button-group.tsx +87 -0
  135. package/components/ui/button.tsx +58 -0
  136. package/components/ui/card.tsx +32 -0
  137. package/components/ui/command.tsx +194 -0
  138. package/components/ui/dialog.tsx +158 -0
  139. package/components/ui/dropdown-menu.tsx +266 -0
  140. package/components/ui/hover-card.tsx +51 -0
  141. package/components/ui/input-group.tsx +158 -0
  142. package/components/ui/input.tsx +20 -0
  143. package/components/ui/scroll-area.tsx +16 -0
  144. package/components/ui/select.tsx +199 -0
  145. package/components/ui/separator.tsx +23 -0
  146. package/components/ui/sheet.tsx +92 -0
  147. package/components/ui/spinner.tsx +10 -0
  148. package/components/ui/tabs.tsx +113 -0
  149. package/components/ui/textarea.tsx +18 -0
  150. package/components/ui/tooltip.tsx +66 -0
  151. package/components/use-resolved-papyrus-theme.ts +47 -0
  152. package/infra/amplify-app-shell.js +109 -0
  153. package/infra/amplify-app-shell.ts +155 -0
  154. package/infra/index.js +12 -0
  155. package/lib/amplify-outputs-guard.ts +24 -0
  156. package/lib/amplify-outputs-path.ts +16 -0
  157. package/lib/amplify-server-runtime.ts +63 -0
  158. package/lib/archive-data.ts +30 -0
  159. package/lib/archive-types.ts +15 -0
  160. package/lib/articles.ts +320 -0
  161. package/lib/cached-content-repository.ts +89 -0
  162. package/lib/category-dashboard.ts +37 -0
  163. package/lib/category-repository.ts +1907 -0
  164. package/lib/console-chat-client.ts +1316 -0
  165. package/lib/console-web-context.ts +118 -0
  166. package/lib/content-load-timing.ts +23 -0
  167. package/lib/content-repository.ts +40 -0
  168. package/lib/content-types.ts +104 -0
  169. package/lib/define-site.js +3 -0
  170. package/lib/define-site.ts +21 -0
  171. package/lib/demo-amplify-outputs.ts +15 -0
  172. package/lib/dev-sandbox-editor-auth.ts +18 -0
  173. package/lib/doctrine.ts +128 -0
  174. package/lib/edition-routes.ts +155 -0
  175. package/lib/edition-sections.ts +111 -0
  176. package/lib/empty-edition-layout-plan.ts +109 -0
  177. package/lib/excerpts.ts +8 -0
  178. package/lib/graphql-content-repository.ts +986 -0
  179. package/lib/image-url.ts +10 -0
  180. package/lib/layout-plan.ts +981 -0
  181. package/lib/layout-scenarios.ts +564 -0
  182. package/lib/markus-body.ts +168 -0
  183. package/lib/markus-ir.ts +235 -0
  184. package/lib/markus-projection.ts +243 -0
  185. package/lib/markus-to-article.ts +93 -0
  186. package/lib/news-desk-session.ts +160 -0
  187. package/lib/newspaper-layout.ts +3459 -0
  188. package/lib/newsroom-assignments.ts +173 -0
  189. package/lib/newsroom-base-path.ts +48 -0
  190. package/lib/newsroom-card-layout.ts +102 -0
  191. package/lib/newsroom-category-drilldown.ts +133 -0
  192. package/lib/newsroom-demo-dashboard.ts +267 -0
  193. package/lib/newsroom-demo-profile.ts +46 -0
  194. package/lib/newsroom-forum-routes.ts +153 -0
  195. package/lib/newsroom-index-filters.ts +202 -0
  196. package/lib/newsroom-knowledge-query-request.ts +51 -0
  197. package/lib/newsroom-list-selection.ts +42 -0
  198. package/lib/newsroom-nav.ts +42 -0
  199. package/lib/newsroom-references.ts +116 -0
  200. package/lib/newsroom-sections.ts +52 -0
  201. package/lib/newsroom-topics.ts +118 -0
  202. package/lib/papyrus-object-kinds.ts +24 -0
  203. package/lib/papyrus-web-locations.ts +480 -0
  204. package/lib/pilobol-sample.ts +167 -0
  205. package/lib/presentation-footer.ts +92 -0
  206. package/lib/pretext-layout.ts +179 -0
  207. package/lib/public-placeholder-config.ts +102 -0
  208. package/lib/publication-items.ts +181 -0
  209. package/lib/reader-route-config.ts +1 -0
  210. package/lib/reader-static-params.ts +56 -0
  211. package/lib/reader-storage-url.ts +63 -0
  212. package/lib/reference-display.ts +74 -0
  213. package/lib/reference-policy.ts +97 -0
  214. package/lib/reference-source-preview.ts +175 -0
  215. package/lib/renderer-config.ts +50 -0
  216. package/lib/renderer.ts +87 -0
  217. package/lib/reporting-story-budget.ts +638 -0
  218. package/lib/semantic-graph.ts +554 -0
  219. package/lib/site-brand-tenant.ts +41 -0
  220. package/lib/site-brand.ts +179 -0
  221. package/lib/site-renderer.ts +17 -0
  222. package/lib/site-stack.ts +195 -0
  223. package/lib/themed-image.ts +30 -0
  224. package/lib/utils.ts +6 -0
  225. package/middleware.ts +78 -0
  226. package/package.json +296 -1
  227. package/publications/anth_us/brand.ts +68 -0
  228. package/publications/anth_us/theme.css +66 -0
  229. package/publications/papyrus/theme.css +11 -0
  230. package/publications/pilobol_us/brand.ts +37 -0
  231. package/publications/pilobol_us/theme.css +140 -0
  232. package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
  233. package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
  234. package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
  235. package/publications/threat_intelligence/brand.ts +40 -0
  236. package/publications/threat_intelligence/pictograms/art.tsx +608 -0
  237. package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
  238. package/publications/threat_intelligence/pictograms/registry.ts +30 -0
  239. package/publications/threat_intelligence/pictograms/system.tsx +305 -0
  240. package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
  241. package/publications/threat_intelligence/theme.css +1440 -0
  242. package/renderers/markus/index.ts +25 -0
  243. package/renderers/markus/stubs.tsx +33 -0
  244. package/renderers/pretext/article-page.tsx +174 -0
  245. package/renderers/pretext/index.ts +30 -0
  246. package/renderers/pretext/layouts.ts +16 -0
  247. package/renderers/pretext/presentation-shell.tsx +477 -0
  248. package/routes.manifest.json +182 -0
  249. package/scripts/fixtures/pilobol-sample.ast.json +234 -0
  250. package/src/backend.js +1 -0
  251. package/src/backend.ts +3 -0
  252. package/src/sync.mjs +87 -0
  253. package/src/with-papyrus.d.mts +2 -0
  254. package/src/with-papyrus.mjs +47 -0
@@ -0,0 +1,621 @@
1
+ import { randomUUID, createHash } from "node:crypto";
2
+ import { getAmplifyDataClientConfig } from "@aws-amplify/backend/function/runtime";
3
+ import { Amplify } from "aws-amplify";
4
+ import { generateClient } from "aws-amplify/data";
5
+ import type { Schema } from "../../data/resource";
6
+
7
+ type DataClient = ReturnType<typeof generateClient<Schema>>;
8
+ type DataClientErrors = Array<{ message?: string | null } | string | null> | null | undefined;
9
+ type DataClientResult<T = unknown> = {
10
+ data?: T | null;
11
+ errors?: DataClientErrors;
12
+ nextToken?: string | null;
13
+ };
14
+
15
+ type ProcedureEvent = {
16
+ arguments: Record<string, unknown>;
17
+ fieldName?: string | null;
18
+ info?: { fieldName?: string | null } | null;
19
+ identity?: { sub?: string | null; username?: string | null; claims?: Record<string, unknown> } | null;
20
+ };
21
+
22
+ const LAMBDA_DATA_AUTH_MODE = "iam";
23
+ const IMMEDIATE_ASSIGNMENT_TYPES = new Set(["procedure.run"]);
24
+ let clientPromise: Promise<DataClient> | null = null;
25
+
26
+ export const handler = async (event: ProcedureEvent) => {
27
+ const fieldName = normalizeRequiredString(event.info?.fieldName ?? event.fieldName, "fieldName");
28
+ if (fieldName === "listNewsroomProcedureDefinitions") return listProcedureDefinitions();
29
+ if (fieldName === "getNewsroomProcedureDefinition") return getProcedureDefinition(event.arguments);
30
+ if (fieldName === "saveNewsroomProcedureDefinition") return saveProcedureDefinition(event);
31
+ if (fieldName === "saveNewsroomProcedureVersionDraft") return saveProcedureVersionDraft(event);
32
+ if (fieldName === "publishNewsroomProcedureVersion") return publishProcedureVersion(event);
33
+ if (fieldName === "startNewsroomProcedureRun") return startProcedureRun(event);
34
+ if (fieldName === "getNewsroomProcedureRun") return getProcedureRun(event.arguments);
35
+ if (fieldName === "listNewsroomProcedureRunsByProcedure") return listProcedureRunsByProcedure(event.arguments);
36
+ throw new Error(`Unsupported procedure operation ${fieldName}.`);
37
+ };
38
+
39
+ async function listProcedureDefinitions() {
40
+ const client = await getDataClient();
41
+ const definitions = await listProcedureDefinitionsPage(client);
42
+ const versions = await listProcedureVersionsPage(client);
43
+ const runs = await listProcedureRunsPage(client);
44
+ const versionsByProcedureId = groupBy(versions, (entry) => normalizeOptionalString(entry.procedureId) ?? "");
45
+ const runsByProcedureId = groupBy(runs, (entry) => normalizeOptionalString(entry.procedureId) ?? "");
46
+ return {
47
+ items: definitions.map((definition) => {
48
+ const currentVersion = normalizeOptionalString(definition.currentVersionId)
49
+ ? versions.find((entry) => entry.id === definition.currentVersionId) ?? null
50
+ : null;
51
+ const recentRuns = (runsByProcedureId.get(definition.id) ?? [])
52
+ .sort((left, right) => String(right.requestedAt ?? "").localeCompare(String(left.requestedAt ?? "")))
53
+ .slice(0, 10);
54
+ return {
55
+ ...definition,
56
+ currentVersion,
57
+ recentRuns,
58
+ versions: (versionsByProcedureId.get(definition.id) ?? [])
59
+ .sort((left, right) => (right.versionNumber ?? 0) - (left.versionNumber ?? 0)),
60
+ };
61
+ }),
62
+ };
63
+ }
64
+
65
+ async function getProcedureDefinition(args: Record<string, unknown>) {
66
+ const client = await getDataClient();
67
+ const id = normalizeOptionalString(args.id);
68
+ const procedureKey = normalizeOptionalString(args.procedureKey);
69
+ const definition = id
70
+ ? await getRequiredRecord(client.models.ProcedureDefinition, id, "ProcedureDefinition")
71
+ : await getProcedureDefinitionByKey(client, procedureKey);
72
+ if (!definition) throw new Error("ProcedureDefinition not found.");
73
+ const versions = await listProcedureVersionsByProcedure(client, definition.id);
74
+ const currentVersion = normalizeOptionalString(definition.currentVersionId)
75
+ ? versions.find((entry) => entry.id === definition.currentVersionId) ?? null
76
+ : null;
77
+ return {
78
+ ...definition,
79
+ currentVersion: currentVersion
80
+ ? {
81
+ id: currentVersion.id,
82
+ procedureId: currentVersion.procedureId,
83
+ procedureKey: currentVersion.procedureKey,
84
+ versionNumber: currentVersion.versionNumber,
85
+ status: currentVersion.status,
86
+ isCurrent: currentVersion.isCurrent,
87
+ label: currentVersion.label,
88
+ tactusSource: currentVersion.tactusSource,
89
+ parameterSchema: currentVersion.parameterSchema,
90
+ defaults: currentVersion.defaults,
91
+ changelog: currentVersion.changelog,
92
+ createdBy: currentVersion.createdBy,
93
+ createdAt: currentVersion.createdAt,
94
+ updatedBy: currentVersion.updatedBy,
95
+ updatedAt: currentVersion.updatedAt,
96
+ }
97
+ : null,
98
+ versions: [],
99
+ recentRuns: [],
100
+ };
101
+ }
102
+
103
+ async function saveProcedureDefinition(event: ProcedureEvent) {
104
+ requireAdmin(event);
105
+ const client = await getDataClient();
106
+ const now = new Date().toISOString();
107
+ const actor = getIdentityLabel(event) ?? "Papyrus admin";
108
+ const input = objectValue(event.arguments.input);
109
+ const id = normalizeOptionalString(input.id);
110
+ const procedureKey = normalizeRequiredString(input.procedureKey, "input.procedureKey");
111
+ const recordId = id ?? `procedure-definition-${safeId(procedureKey)}`;
112
+ const existing = await (async () => {
113
+ if (id) return await getOptionalRecord(client.models.ProcedureDefinition, id, "ProcedureDefinition");
114
+ return await getProcedureDefinitionByKey(client, procedureKey);
115
+ })();
116
+ const payload = {
117
+ id: existing?.id ?? recordId,
118
+ procedureKey,
119
+ title: normalizeRequiredString(input.title, "input.title"),
120
+ category: normalizeOptionalString(input.category) ?? "ingestion",
121
+ description: normalizeOptionalString(input.description),
122
+ enabled: normalizeOptionalBoolean(input.enabled, true),
123
+ enabledStatus: normalizeOptionalBoolean(input.enabled, true) ? "enabled" : "disabled",
124
+ currentVersionId: normalizeOptionalString(input.currentVersionId) ?? normalizeOptionalString(existing?.currentVersionId),
125
+ createdBy: normalizeOptionalString(existing?.createdBy) ?? actor,
126
+ createdAt: normalizeOptionalString(existing?.createdAt) ?? now,
127
+ updatedBy: actor,
128
+ updatedAt: now,
129
+ newsroomFeedKey: "procedures",
130
+ };
131
+ const response = existing
132
+ ? await client.models.ProcedureDefinition.update(payload as never, { authMode: LAMBDA_DATA_AUTH_MODE })
133
+ : await client.models.ProcedureDefinition.create(payload as never, { authMode: LAMBDA_DATA_AUTH_MODE });
134
+ assertNoDataErrors(response.errors, "save ProcedureDefinition");
135
+ if (!response.data) throw new Error("ProcedureDefinition save returned no data.");
136
+ return { ok: true, definition: response.data };
137
+ }
138
+
139
+ async function saveProcedureVersionDraft(event: ProcedureEvent) {
140
+ requireAdmin(event);
141
+ const client = await getDataClient();
142
+ const now = new Date().toISOString();
143
+ const actor = getIdentityLabel(event) ?? "Papyrus admin";
144
+ const input = objectValue(event.arguments.input);
145
+ const versionId = normalizeOptionalString(input.id);
146
+ const definition = await resolveDefinition(client, input);
147
+ if (!definition) throw new Error("ProcedureDefinition not found.");
148
+ const versions = await listProcedureVersionsByProcedure(client, definition.id);
149
+ if (versionId) {
150
+ const existing = await getRequiredRecord(client.models.ProcedureVersion, versionId, "ProcedureVersion");
151
+ if (existing.procedureId !== definition.id) throw new Error(`ProcedureVersion ${versionId} does not belong to ${definition.id}.`);
152
+ if (normalizeOptionalString(existing.status) === "published") throw new Error("Published versions are immutable; create a new draft.");
153
+ const response = await client.models.ProcedureVersion.update({
154
+ id: existing.id,
155
+ label: normalizeOptionalString(input.label) ?? existing.label ?? null,
156
+ tactusSource: normalizeRequiredString(input.tactusSource, "input.tactusSource"),
157
+ parameterSchema: normalizeJson(input.parameterSchema),
158
+ defaults: normalizeOptionalJson(input.defaults),
159
+ changelog: normalizeOptionalString(input.changelog) ?? existing.changelog ?? null,
160
+ updatedBy: actor,
161
+ updatedAt: now,
162
+ } as never, { authMode: LAMBDA_DATA_AUTH_MODE });
163
+ assertNoDataErrors(response.errors, "update ProcedureVersion");
164
+ if (!response.data) throw new Error("ProcedureVersion update returned no data.");
165
+ return { ok: true, version: response.data };
166
+ }
167
+ const nextVersionNumber = Math.max(0, ...versions.map((entry) => Number(entry.versionNumber ?? 0))) + 1;
168
+ const created = await client.models.ProcedureVersion.create({
169
+ id: `procedure-version-${safeId(definition.procedureKey)}-${nextVersionNumber}`,
170
+ procedureId: definition.id,
171
+ procedureKey: definition.procedureKey,
172
+ versionNumber: nextVersionNumber,
173
+ status: "draft",
174
+ isCurrent: false,
175
+ label: normalizeOptionalString(input.label) ?? null,
176
+ tactusSource: normalizeRequiredString(input.tactusSource, "input.tactusSource"),
177
+ parameterSchema: normalizeJson(input.parameterSchema),
178
+ defaults: normalizeOptionalJson(input.defaults),
179
+ changelog: normalizeOptionalString(input.changelog) ?? null,
180
+ createdBy: actor,
181
+ createdAt: now,
182
+ updatedBy: actor,
183
+ updatedAt: now,
184
+ } as never, { authMode: LAMBDA_DATA_AUTH_MODE });
185
+ assertNoDataErrors(created.errors, "create ProcedureVersion");
186
+ if (!created.data) throw new Error("ProcedureVersion create returned no data.");
187
+ return { ok: true, version: created.data };
188
+ }
189
+
190
+ async function publishProcedureVersion(event: ProcedureEvent) {
191
+ requireAdmin(event);
192
+ const client = await getDataClient();
193
+ const now = new Date().toISOString();
194
+ const actor = getIdentityLabel(event) ?? "Papyrus admin";
195
+ const versionId = normalizeRequiredString(event.arguments.versionId, "versionId");
196
+ const version = await getRequiredRecord(client.models.ProcedureVersion, versionId, "ProcedureVersion");
197
+ const definition = await getRequiredRecord(client.models.ProcedureDefinition, version.procedureId, "ProcedureDefinition");
198
+ const versions = await listProcedureVersionsByProcedure(client, definition.id);
199
+ for (const entry of versions) {
200
+ const isTarget = entry.id === version.id;
201
+ const response = await client.models.ProcedureVersion.update({
202
+ id: entry.id,
203
+ isCurrent: isTarget,
204
+ status: isTarget ? "published" : (normalizeOptionalString(entry.status) === "published" ? "archived" : normalizeOptionalString(entry.status) ?? "draft"),
205
+ updatedBy: actor,
206
+ updatedAt: now,
207
+ } as never, { authMode: LAMBDA_DATA_AUTH_MODE });
208
+ assertNoDataErrors(response.errors, `publish ProcedureVersion ${entry.id}`);
209
+ }
210
+ const definitionResponse = await client.models.ProcedureDefinition.update({
211
+ id: definition.id,
212
+ currentVersionId: version.id,
213
+ enabled: normalizeOptionalBoolean(definition.enabled, true),
214
+ enabledStatus: normalizeOptionalBoolean(definition.enabled, true) ? "enabled" : "disabled",
215
+ updatedBy: actor,
216
+ updatedAt: now,
217
+ } as never, { authMode: LAMBDA_DATA_AUTH_MODE });
218
+ assertNoDataErrors(definitionResponse.errors, "update ProcedureDefinition currentVersionId");
219
+ return { ok: true, procedureId: definition.id, versionId: version.id };
220
+ }
221
+
222
+ async function startProcedureRun(event: ProcedureEvent) {
223
+ const client = await getDataClient();
224
+ const now = new Date().toISOString();
225
+ const actorSub = getIdentitySub(event);
226
+ const actorLabel = normalizeOptionalString(event.arguments.actorLabel) ?? getIdentityLabel(event) ?? actorSub ?? "Papyrus newsroom";
227
+ const procedureKey = normalizeOptionalString(event.arguments.procedureKey);
228
+ const procedureId = normalizeOptionalString(event.arguments.procedureId);
229
+ const definition = procedureId
230
+ ? await getRequiredRecord(client.models.ProcedureDefinition, procedureId, "ProcedureDefinition")
231
+ : await getProcedureDefinitionByKey(client, procedureKey);
232
+ if (!definition) throw new Error(`Procedure ${procedureKey ?? procedureId ?? "unknown"} was not found.`);
233
+ if (!normalizeOptionalBoolean(definition.enabled, true)) {
234
+ throw new Error(`Procedure ${definition.procedureKey} is disabled.`);
235
+ }
236
+ const versionId = normalizeOptionalString(event.arguments.procedureVersionId) ?? normalizeOptionalString(definition.currentVersionId);
237
+ if (!versionId) throw new Error(`Procedure ${definition.procedureKey} does not have a current version.`);
238
+ const version = await getRequiredRecord(client.models.ProcedureVersion, versionId, "ProcedureVersion");
239
+ const input = normalizeJson(event.arguments.input);
240
+ const executionMode = normalizeOptionalString(input.__papyrusExecutionMode);
241
+ delete input.__papyrusExecutionMode;
242
+ const validation = validateJsonSchemaInput(normalizeJson(version.parameterSchema), input);
243
+ if (!validation.ok) throw new Error(`Procedure parameter validation failed: ${validation.error}`);
244
+ const normalizedInput = validation.normalizedInput;
245
+ const isExternalCliRun = executionMode === "external_cli";
246
+ const runId = `procedure-run-${safeId(definition.procedureKey)}-${now.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 8)}`;
247
+ const assignmentId = isExternalCliRun ? null : `assignment-procedure-run-${hashShort([runId, definition.id, version.id])}`;
248
+ const runResponse = await client.models.ProcedureRun.create({
249
+ id: runId,
250
+ procedureId: definition.id,
251
+ procedureKey: definition.procedureKey,
252
+ procedureVersionId: version.id,
253
+ procedureVersionNumber: Number(version.versionNumber ?? 1),
254
+ assignmentId,
255
+ runStatus: isExternalCliRun ? "running" : "queued",
256
+ requestedBy: actorLabel,
257
+ requestedAt: now,
258
+ startedAt: isExternalCliRun ? now : null,
259
+ finishedAt: null,
260
+ input: toAwsJson(input),
261
+ normalizedInput: toAwsJson(normalizedInput),
262
+ resultSummary: null,
263
+ errorSummary: null,
264
+ output: null,
265
+ error: null,
266
+ attempt: 1,
267
+ newsroomFeedKey: "procedures",
268
+ } as never, { authMode: LAMBDA_DATA_AUTH_MODE });
269
+ assertNoDataErrors(runResponse.errors, "create ProcedureRun");
270
+ if (!runResponse.data) throw new Error("ProcedureRun create returned no data.");
271
+ if (isExternalCliRun) {
272
+ return {
273
+ ok: true,
274
+ assignmentId: null,
275
+ assignmentStatus: null,
276
+ runId,
277
+ procedureId: definition.id,
278
+ procedureKey: definition.procedureKey,
279
+ procedureVersionId: version.id,
280
+ procedureVersionNumber: Number(version.versionNumber ?? 1),
281
+ executionMode,
282
+ };
283
+ }
284
+ const assignmentTypeKey = "procedure.run";
285
+ const queueKey = `${assignmentTypeKey}#${definition.id}`;
286
+ const assignmentCreate = await client.models.Assignment.create({
287
+ id: assignmentId,
288
+ assignmentTypeKey,
289
+ queueKey,
290
+ queueStatusKey: `${queueKey}#open`,
291
+ status: "open",
292
+ title: normalizeOptionalString(event.arguments.title) ?? `Run ${definition.title}`,
293
+ summary: normalizeOptionalString(event.arguments.summary) ?? `Procedure ${definition.procedureKey} queued`,
294
+ assigneeType: null,
295
+ assigneeId: null,
296
+ assigneeKey: null,
297
+ claimedAt: null,
298
+ claimExpiresAt: null,
299
+ completedAt: null,
300
+ canceledAt: null,
301
+ sourceSnapshotId: runId,
302
+ importRunId: null,
303
+ createdBy: actorLabel,
304
+ createdAt: now,
305
+ updatedAt: now,
306
+ newsroomFeedKey: "assignments",
307
+ } as never, { authMode: LAMBDA_DATA_AUTH_MODE });
308
+ assertNoDataErrors(assignmentCreate.errors, "create Assignment");
309
+ if (!assignmentCreate.data?.id) throw new Error("Assignment create returned no id.");
310
+ const createdEventId = `assignment-event-${assignmentId}-created-${now.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 6)}`;
311
+ const createdEvent = await client.models.AssignmentEvent.create({
312
+ id: createdEventId,
313
+ assignmentId,
314
+ assignmentTypeKey,
315
+ queueKey,
316
+ eventType: "created",
317
+ fromStatus: null,
318
+ toStatus: "open",
319
+ actorSub,
320
+ actorLabel,
321
+ note: `Procedure run created for ${definition.procedureKey}.`,
322
+ createdAt: now,
323
+ } as never, { authMode: LAMBDA_DATA_AUTH_MODE });
324
+ assertNoDataErrors(createdEvent.errors, "create AssignmentEvent created");
325
+
326
+ let assignmentStatus = "open";
327
+ if (IMMEDIATE_ASSIGNMENT_TYPES.has(assignmentTypeKey)) {
328
+ const claimResult: { data?: { status?: string | null } | null; errors?: DataClientErrors } = await (client.mutations.claimAssignment as any)({
329
+ assignmentId,
330
+ actorLabel,
331
+ assigneeType: "system",
332
+ assigneeId: actorSub ?? "system",
333
+ assigneeKey: `system#${actorSub ?? "procedure-runner"}`,
334
+ note: `Immediate execution dispatch for ${definition.procedureKey}.`,
335
+ }, { authMode: LAMBDA_DATA_AUTH_MODE });
336
+ assertNoDataErrors(claimResult.errors, "claimAssignment immediate dispatch");
337
+ assignmentStatus = normalizeOptionalString(claimResult.data?.status) ?? "open";
338
+ }
339
+ const assignment = await getRequiredRecord(client.models.Assignment, assignmentId as string, "Assignment");
340
+ return {
341
+ ok: true,
342
+ assignmentId,
343
+ assignmentStatus: normalizeOptionalString(assignment.status) ?? assignmentStatus,
344
+ runId,
345
+ procedureId: definition.id,
346
+ procedureKey: definition.procedureKey,
347
+ procedureVersionId: version.id,
348
+ procedureVersionNumber: Number(version.versionNumber ?? 1),
349
+ };
350
+ }
351
+
352
+ async function getProcedureRun(args: Record<string, unknown>) {
353
+ const client = await getDataClient();
354
+ const runId = normalizeRequiredString(args.id, "id");
355
+ const run = await getRequiredRecord(client.models.ProcedureRun, runId, "ProcedureRun");
356
+ const assignment = normalizeOptionalString(run.assignmentId)
357
+ ? await getOptionalRecord(client.models.Assignment, run.assignmentId, "Assignment")
358
+ : null;
359
+ return { ...run, assignment };
360
+ }
361
+
362
+ async function listProcedureRunsByProcedure(args: Record<string, unknown>) {
363
+ const client = await getDataClient();
364
+ const procedureId = normalizeRequiredString(args.procedureId, "procedureId");
365
+ const runs = await listProcedureRunsByProcedureId(client, procedureId);
366
+ return {
367
+ items: runs.sort((left, right) => String(right.requestedAt ?? "").localeCompare(String(left.requestedAt ?? ""))),
368
+ };
369
+ }
370
+
371
+ async function resolveDefinition(client: DataClient, input: Record<string, unknown>): Promise<any | null> {
372
+ const procedureId = normalizeOptionalString(input.procedureId);
373
+ if (procedureId) return await getOptionalRecord(client.models.ProcedureDefinition, procedureId, "ProcedureDefinition");
374
+ const procedureKey = normalizeOptionalString(input.procedureKey);
375
+ return await getProcedureDefinitionByKey(client, procedureKey);
376
+ }
377
+
378
+ async function getProcedureDefinitionByKey(client: DataClient, procedureKey: string | null): Promise<any | null> {
379
+ if (!procedureKey) return null;
380
+ const model = client.models.ProcedureDefinition as any;
381
+ if (typeof model.listProcedureDefinitionsByKeyAndUpdatedAt === "function") {
382
+ const response: DataClientResult<any[]> = await model.listProcedureDefinitionsByKeyAndUpdatedAt(
383
+ { procedureKey },
384
+ { limit: 1, sortDirection: "DESC" },
385
+ );
386
+ assertNoDataErrors(response.errors, "list ProcedureDefinitionsByKeyAndUpdatedAt");
387
+ const items = response.data ?? [];
388
+ return items[0] ?? null;
389
+ }
390
+ const all = await listProcedureDefinitionsPage(client);
391
+ return all.find((entry) => normalizeOptionalString(entry.procedureKey) === procedureKey) ?? null;
392
+ }
393
+
394
+ async function listProcedureDefinitionsPage(client: DataClient): Promise<any[]> {
395
+ return await listModelPages((client.models.ProcedureDefinition as any));
396
+ }
397
+
398
+ async function listProcedureVersionsPage(client: DataClient): Promise<any[]> {
399
+ return await listModelPages((client.models.ProcedureVersion as any));
400
+ }
401
+
402
+ async function listProcedureRunsPage(client: DataClient): Promise<any[]> {
403
+ return await listModelPages((client.models.ProcedureRun as any));
404
+ }
405
+
406
+ async function listProcedureVersionsByProcedure(client: DataClient, procedureId: string): Promise<any[]> {
407
+ const model = client.models.ProcedureVersion as any;
408
+ if (typeof model.listProcedureVersionsByProcedureAndVersion === "function") {
409
+ return await listByIndexPages(model.listProcedureVersionsByProcedureAndVersion, { procedureId });
410
+ }
411
+ const all = await listProcedureVersionsPage(client);
412
+ return all.filter((entry) => normalizeOptionalString(entry.procedureId) === procedureId);
413
+ }
414
+
415
+ async function listProcedureRunsByProcedureId(client: DataClient, procedureId: string): Promise<any[]> {
416
+ const model = client.models.ProcedureRun as any;
417
+ if (typeof model.listProcedureRunsByProcedureAndRequestedAt === "function") {
418
+ return await listByIndexPages(model.listProcedureRunsByProcedureAndRequestedAt, { procedureId });
419
+ }
420
+ const all = await listProcedureRunsPage(client);
421
+ return all.filter((entry) => normalizeOptionalString(entry.procedureId) === procedureId);
422
+ }
423
+
424
+ async function listModelPages(model: any): Promise<any[]> {
425
+ const rows: any[] = [];
426
+ let nextToken: string | null | undefined;
427
+ do {
428
+ const response: DataClientResult<any[]> = await model.list({ authMode: LAMBDA_DATA_AUTH_MODE, limit: 200, nextToken });
429
+ assertNoDataErrors(response.errors, `list ${model.name ?? "model"}`);
430
+ rows.push(...(response.data ?? []));
431
+ nextToken = response.nextToken;
432
+ } while (nextToken);
433
+ return rows;
434
+ }
435
+
436
+ async function listByIndexPages(query: (input: Record<string, unknown>, options: Record<string, unknown>) => Promise<DataClientResult<any[]>>, input: Record<string, unknown>): Promise<any[]> {
437
+ const rows: any[] = [];
438
+ let nextToken: string | null | undefined;
439
+ do {
440
+ const response = await query(input, { authMode: LAMBDA_DATA_AUTH_MODE, limit: 200, nextToken });
441
+ assertNoDataErrors(response.errors, "indexed list query");
442
+ rows.push(...(response.data ?? []));
443
+ nextToken = response.nextToken;
444
+ } while (nextToken);
445
+ return rows;
446
+ }
447
+
448
+ function validateJsonSchemaInput(schema: Record<string, unknown>, input: Record<string, unknown>): { ok: true; normalizedInput: Record<string, unknown> } | { ok: false; error: string } {
449
+ const required = arrayOfStrings(schema.required);
450
+ for (const key of required) {
451
+ if (!(key in input) || input[key] === null || input[key] === undefined || input[key] === "") {
452
+ return { ok: false, error: `missing required parameter '${key}'` };
453
+ }
454
+ }
455
+ const properties = objectValue(schema.properties);
456
+ for (const [key, propertySchemaUnknown] of Object.entries(properties)) {
457
+ if (!(key in input)) continue;
458
+ const propertySchema = objectValue(propertySchemaUnknown);
459
+ const expectedType = normalizeOptionalString(propertySchema.type);
460
+ if (!expectedType) continue;
461
+ const value = input[key];
462
+ const valid = (
463
+ (expectedType === "string" && typeof value === "string")
464
+ || (expectedType === "number" && typeof value === "number")
465
+ || (expectedType === "integer" && typeof value === "number" && Number.isInteger(value))
466
+ || (expectedType === "boolean" && typeof value === "boolean")
467
+ || (expectedType === "array" && Array.isArray(value))
468
+ || (expectedType === "object" && value !== null && typeof value === "object" && !Array.isArray(value))
469
+ );
470
+ if (!valid) return { ok: false, error: `parameter '${key}' must be ${expectedType}` };
471
+ }
472
+ return { ok: true, normalizedInput: input };
473
+ }
474
+
475
+ function requireAdmin(event: ProcedureEvent): void {
476
+ const claims = objectValue(event.identity?.claims);
477
+ const groups = [
478
+ ...normalizeClaimGroups(claims["cognito:groups"]),
479
+ ...normalizeClaimGroups(claims.groups),
480
+ ];
481
+ if (groups.includes("admin")) return;
482
+ throw new Error("Admin role required.");
483
+ }
484
+
485
+ function normalizeClaimGroups(value: unknown): string[] {
486
+ if (Array.isArray(value)) return value.map((entry) => String(entry).trim()).filter(Boolean);
487
+ if (typeof value === "string") return value.split(",").map((entry) => entry.trim()).filter(Boolean);
488
+ return [];
489
+ }
490
+
491
+ async function getDataClient(): Promise<DataClient> {
492
+ if (!clientPromise) {
493
+ clientPromise = (async () => {
494
+ const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig(process.env as never);
495
+ Amplify.configure(resourceConfig, libraryOptions);
496
+ return generateClient<Schema>();
497
+ })();
498
+ }
499
+ return clientPromise;
500
+ }
501
+
502
+ async function getRequiredRecord(
503
+ model: { get(input: { id: string }, options?: Record<string, unknown>): Promise<{ data?: unknown | null; errors?: DataClientErrors }> },
504
+ id: string,
505
+ modelName: string,
506
+ ): Promise<any> {
507
+ const response = await model.get({ id }, { authMode: LAMBDA_DATA_AUTH_MODE });
508
+ assertNoDataErrors(response.errors, `get ${modelName}`);
509
+ if (!response.data) throw new Error(`${modelName} ${id} was not found.`);
510
+ return response.data;
511
+ }
512
+
513
+ async function getOptionalRecord(
514
+ model: { get(input: { id: string }, options?: Record<string, unknown>): Promise<{ data?: unknown | null; errors?: DataClientErrors }> },
515
+ id: string,
516
+ modelName: string,
517
+ ): Promise<any | null> {
518
+ const response = await model.get({ id }, { authMode: LAMBDA_DATA_AUTH_MODE });
519
+ assertNoDataErrors(response.errors, `get ${modelName}`);
520
+ return response.data ?? null;
521
+ }
522
+
523
+ function assertNoDataErrors(errors: DataClientErrors, operation: string): void {
524
+ if (!errors?.length) return;
525
+ throw new Error(`${operation} failed: ${errors.map(formatDataError).join("; ")}`);
526
+ }
527
+
528
+ function formatDataError(error: { message?: string | null } | string | null): string {
529
+ if (typeof error === "string") return error;
530
+ return error?.message ?? "GraphQL data operation failed.";
531
+ }
532
+
533
+ function getIdentitySub(event: ProcedureEvent): string | null {
534
+ const identity = event.identity as { sub?: string | null; claims?: Record<string, unknown> } | null | undefined;
535
+ return normalizeOptionalString(identity?.sub) ?? normalizeOptionalString(identity?.claims?.sub);
536
+ }
537
+
538
+ function getIdentityLabel(event: ProcedureEvent): string | null {
539
+ const identity = event.identity as { username?: string | null; claims?: Record<string, unknown> } | null | undefined;
540
+ return normalizeOptionalString(identity?.claims?.email)
541
+ ?? normalizeOptionalString(identity?.claims?.name)
542
+ ?? normalizeOptionalString(identity?.username)
543
+ ?? getIdentitySub(event);
544
+ }
545
+
546
+ function normalizeRequiredString(value: unknown, field: string): string {
547
+ const normalized = normalizeOptionalString(value);
548
+ if (!normalized) throw new Error(`${field} is required.`);
549
+ return normalized;
550
+ }
551
+
552
+ function normalizeOptionalString(value: unknown): string | null {
553
+ return typeof value === "string" && value.trim() ? value.trim() : null;
554
+ }
555
+
556
+ function normalizeOptionalBoolean(value: unknown, fallback: boolean): boolean {
557
+ if (value === null || value === undefined) return fallback;
558
+ if (typeof value === "boolean") return value;
559
+ if (typeof value === "string") {
560
+ const normalized = value.trim().toLowerCase();
561
+ if (normalized === "true") return true;
562
+ if (normalized === "false") return false;
563
+ }
564
+ return fallback;
565
+ }
566
+
567
+ function objectValue(value: unknown): Record<string, unknown> {
568
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
569
+ }
570
+
571
+ function normalizeJson(value: unknown): Record<string, unknown> {
572
+ if (typeof value === "string") {
573
+ const parsed = JSON.parse(value);
574
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
575
+ throw new Error("JSON input must be an object.");
576
+ }
577
+ if (value && typeof value === "object" && !Array.isArray(value)) return value as Record<string, unknown>;
578
+ return {};
579
+ }
580
+
581
+ function normalizeOptionalJson(value: unknown): Record<string, unknown> | null {
582
+ if (value === null || value === undefined || value === "") return null;
583
+ return normalizeJson(value);
584
+ }
585
+
586
+ function toAwsJson(value: unknown): string | null {
587
+ if (value === null || value === undefined) return null;
588
+ return JSON.stringify(value);
589
+ }
590
+
591
+ function arrayOfStrings(value: unknown): string[] {
592
+ if (!Array.isArray(value)) return [];
593
+ return value.map((entry) => normalizeOptionalString(entry)).filter((entry): entry is string => Boolean(entry));
594
+ }
595
+
596
+ function safeId(value: unknown): string {
597
+ return String(value ?? "")
598
+ .trim()
599
+ .toLowerCase()
600
+ .replace(/[^a-z0-9]+/g, "-")
601
+ .replace(/^-+|-+$/g, "")
602
+ || "procedure";
603
+ }
604
+
605
+ function hashShort(value: unknown): string {
606
+ return createHash("sha256")
607
+ .update(JSON.stringify(value))
608
+ .digest("hex")
609
+ .slice(0, 16);
610
+ }
611
+
612
+ function groupBy<T>(rows: T[], keyFn: (entry: T) => string): Map<string, T[]> {
613
+ const map = new Map<string, T[]>();
614
+ for (const row of rows) {
615
+ const key = keyFn(row);
616
+ const current = map.get(key) ?? [];
617
+ current.push(row);
618
+ map.set(key, current);
619
+ }
620
+ return map;
621
+ }
@@ -0,0 +1,8 @@
1
+ import { defineFunction } from "@aws-amplify/backend";
2
+ export const procedureAction = defineFunction({
3
+ name: "papyrus-procedure-action",
4
+ entry: "./handler.ts",
5
+ timeoutSeconds: 30,
6
+ memoryMB: 512,
7
+ resourceGroupName: "data",
8
+ });
@@ -0,0 +1,9 @@
1
+ import { defineFunction } from "@aws-amplify/backend";
2
+
3
+ export const procedureAction = defineFunction({
4
+ name: "papyrus-procedure-action",
5
+ entry: "./handler.ts",
6
+ timeoutSeconds: 30,
7
+ memoryMB: 512,
8
+ resourceGroupName: "data",
9
+ });