create-safest-tools 0.2.0

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 (240) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +71 -0
  3. package/bin/create-safest-tools.mjs +12 -0
  4. package/package.json +34 -0
  5. package/src/cli.mjs +128 -0
  6. package/src/config.mjs +151 -0
  7. package/src/scaffold.mjs +30 -0
  8. package/template/LICENSE +201 -0
  9. package/template/README.md +35 -0
  10. package/template/console/README.md +28 -0
  11. package/template/console/analytics/AnalyticsWorkspace.tsx +55 -0
  12. package/template/console/analytics/controller.ts +133 -0
  13. package/template/console/analytics/events.ts +33 -0
  14. package/template/console/analytics/store.ts +33 -0
  15. package/template/console/analytics/types.ts +38 -0
  16. package/template/console/appeals/AppealWorkspace.tsx +73 -0
  17. package/template/console/appeals/controller.ts +125 -0
  18. package/template/console/appeals/events.ts +33 -0
  19. package/template/console/appeals/store.ts +35 -0
  20. package/template/console/appeals/types.ts +37 -0
  21. package/template/console/assistant/AssistantWorkspace.tsx +133 -0
  22. package/template/console/assistant/types.ts +34 -0
  23. package/template/console/auth/AccountJourney.tsx +69 -0
  24. package/template/console/auth/InvitationAcceptance.tsx +145 -0
  25. package/template/console/auth/OAuthButtons.tsx +78 -0
  26. package/template/console/auth/OperatorLogin.tsx +110 -0
  27. package/template/console/auth/PasswordRecovery.tsx +132 -0
  28. package/template/console/auth/account-route.ts +15 -0
  29. package/template/console/auth/browser-navigation.ts +7 -0
  30. package/template/console/auth/events.ts +15 -0
  31. package/template/console/command/CommandCentre.tsx +119 -0
  32. package/template/console/command/controller.ts +267 -0
  33. package/template/console/command/events.ts +19 -0
  34. package/template/console/command/store.ts +36 -0
  35. package/template/console/command/types.ts +61 -0
  36. package/template/console/components/AnalystIdentity.tsx +25 -0
  37. package/template/console/components/PageHeader.tsx +27 -0
  38. package/template/console/configuration/ConfigurationDialog.tsx +200 -0
  39. package/template/console/configuration/ConfigurationWorkspace.tsx +66 -0
  40. package/template/console/configuration/FormBuilder.tsx +143 -0
  41. package/template/console/configuration/api.ts +40 -0
  42. package/template/console/configuration/events.ts +14 -0
  43. package/template/console/configuration/types.ts +79 -0
  44. package/template/console/lib/format.ts +49 -0
  45. package/template/console/lib/http.ts +96 -0
  46. package/template/console/main.tsx +225 -0
  47. package/template/console/operations/OperationsWorkspace.tsx +126 -0
  48. package/template/console/operations/controller.ts +237 -0
  49. package/template/console/operations/events.ts +37 -0
  50. package/template/console/operations/store.ts +32 -0
  51. package/template/console/operations/types.ts +107 -0
  52. package/template/console/people/PeopleWorkspace.tsx +81 -0
  53. package/template/console/people/types.ts +25 -0
  54. package/template/console/profile/ProfileWorkspace.tsx +136 -0
  55. package/template/console/profile/events.ts +9 -0
  56. package/template/console/profile/types.ts +6 -0
  57. package/template/console/quality/QualityWorkspace.tsx +87 -0
  58. package/template/console/quality/controller.ts +154 -0
  59. package/template/console/quality/events.ts +34 -0
  60. package/template/console/quality/store.ts +36 -0
  61. package/template/console/quality/types.ts +70 -0
  62. package/template/console/queues/QueueEditor.tsx +173 -0
  63. package/template/console/queues/QueueWorkspace.tsx +109 -0
  64. package/template/console/queues/controller.ts +194 -0
  65. package/template/console/queues/events.ts +34 -0
  66. package/template/console/queues/store.ts +37 -0
  67. package/template/console/queues/types.ts +102 -0
  68. package/template/console/registry/RegistryDialog.tsx +198 -0
  69. package/template/console/registry/RegistryWorkspace.tsx +108 -0
  70. package/template/console/registry/events.ts +14 -0
  71. package/template/console/registry/types.ts +52 -0
  72. package/template/console/reports/ReportDrawer.tsx +159 -0
  73. package/template/console/reports/ReportWorkspace.tsx +90 -0
  74. package/template/console/reports/controller.ts +517 -0
  75. package/template/console/reports/events.ts +42 -0
  76. package/template/console/reports/store.ts +39 -0
  77. package/template/console/reports/types.ts +230 -0
  78. package/template/console/settings/BrandEditor.tsx +126 -0
  79. package/template/console/settings/ChannelDialog.tsx +241 -0
  80. package/template/console/settings/SettingsWorkspace.tsx +155 -0
  81. package/template/console/settings/events.ts +19 -0
  82. package/template/console/settings/handoff.ts +22 -0
  83. package/template/console/settings/types.ts +93 -0
  84. package/template/console/shell/WorkspaceShell.tsx +160 -0
  85. package/template/console/shell/controller.ts +182 -0
  86. package/template/console/shell/events.ts +25 -0
  87. package/template/console/shell/navigation.ts +61 -0
  88. package/template/console/shell/store.ts +56 -0
  89. package/template/console/shell/types.ts +86 -0
  90. package/template/console/workflows/CreateWorkflowDialog.tsx +87 -0
  91. package/template/console/workflows/WorkflowCanvas.tsx +42 -0
  92. package/template/console/workflows/WorkflowDialogs.tsx +6 -0
  93. package/template/console/workflows/WorkflowStudio.tsx +331 -0
  94. package/template/console/workflows/WorkflowWorkspace.tsx +82 -0
  95. package/template/console/workflows/events.ts +17 -0
  96. package/template/console/workflows/graph.ts +156 -0
  97. package/template/console/workflows/templates.ts +70 -0
  98. package/template/console/workflows/types.ts +201 -0
  99. package/template/gitignore.template +18 -0
  100. package/template/migrations/0001_reports_foundation.sql +444 -0
  101. package/template/migrations/0002_human_report_loop.sql +65 -0
  102. package/template/migrations/0003_delivery_reliability.sql +18 -0
  103. package/template/migrations/0004_public_intake.sql +14 -0
  104. package/template/migrations/0005_operations_visibility.sql +24 -0
  105. package/template/migrations/0006_ai_governance.sql +198 -0
  106. package/template/migrations/0007_ai_release_gates.sql +6 -0
  107. package/template/migrations/0008_retention_analytics_exports.sql +52 -0
  108. package/template/migrations/0009_retention_derived_copies.sql +14 -0
  109. package/template/migrations/0010_ai_quality_controls.sql +26 -0
  110. package/template/migrations/0011_analyst_presence.sql +28 -0
  111. package/template/migrations/0012_queue_policies.sql +87 -0
  112. package/template/migrations/0013_routing_agents.sql +63 -0
  113. package/template/migrations/0014_webhook_enrichments.sql +122 -0
  114. package/template/migrations/0015_queue_owned_ai.sql +55 -0
  115. package/template/migrations/0016_operator_accounts.sql +67 -0
  116. package/template/migrations/0017_operator_profiles_and_recovery.sql +33 -0
  117. package/template/migrations/0018_platform_configuration.sql +382 -0
  118. package/template/migrations/0019_workflow_authoring_runtime.sql +372 -0
  119. package/template/migrations/0020_tasks_findings_assistant_budgets.sql +427 -0
  120. package/template/migrations/0021_abuse_evidence_operations.sql +324 -0
  121. package/template/migrations/0022_workflow_dispatch_operations.sql +42 -0
  122. package/template/migrations/0023_component_connection_execution.sql +75 -0
  123. package/template/migrations/0024_access_runtime_integrity.sql +72 -0
  124. package/template/migrations/0025_installation_timezone.sql +11 -0
  125. package/template/migrations/0026_ai_and_egress_execution_controls.sql +49 -0
  126. package/template/migrations/0027_prompt_and_ai_registry.sql +37 -0
  127. package/template/migrations/0028_step_attempt_ai_provenance.sql +13 -0
  128. package/template/migrations/0029_evidence_fetch_transport.sql +5 -0
  129. package/template/migrations/0030_evidence_dlq_incidents.sql +45 -0
  130. package/template/migrations/0031_shadow_quality_integrity.sql +7 -0
  131. package/template/migrations/0032_action_delivery_outbox.sql +55 -0
  132. package/template/migrations/0033_configuration_and_assistant_drafts.sql +43 -0
  133. package/template/migrations/0034_installation_integrations.sql +31 -0
  134. package/template/migrations/0035_workspace_governance.sql +21 -0
  135. package/template/migrations/0036_published_routing_baseline.sql +15 -0
  136. package/template/migrations/0037_builtin_phishing_specialist.sql +71 -0
  137. package/template/migrations/0038_remove_deprecated_enrichment_runtime.sql +228 -0
  138. package/template/migrations/0039_secure_reporting_channels.sql +45 -0
  139. package/template/migrations/0040_notification_only_reporting.sql +24 -0
  140. package/template/migrations/0041_better_auth_credentials.sql +17 -0
  141. package/template/package.json +47 -0
  142. package/template/public/_headers +27 -0
  143. package/template/public/app-icon-192.png +0 -0
  144. package/template/public/app-icon-512.png +0 -0
  145. package/template/public/brand-icon.svg +7 -0
  146. package/template/public/brand-tokens.css +80 -0
  147. package/template/public/console/auth-shell.js +19340 -0
  148. package/template/public/customer-brand.js +58 -0
  149. package/template/public/embed/embed.css +139 -0
  150. package/template/public/embed/embed.js +408 -0
  151. package/template/public/embed/index.html +91 -0
  152. package/template/public/favicon.svg +7 -0
  153. package/template/public/fonts/Manrope-Variable.ttf +0 -0
  154. package/template/public/fonts/Newsreader-Italic-Variable.ttf +0 -0
  155. package/template/public/fonts/Newsreader-Variable.ttf +0 -0
  156. package/template/public/index.html +123 -0
  157. package/template/public/logo-primary.svg +7 -0
  158. package/template/public/logo-reversed.svg +7 -0
  159. package/template/public/manifest.webmanifest +21 -0
  160. package/template/public/public-report.js +184 -0
  161. package/template/public/report/index.html +39 -0
  162. package/template/public/report/public-report.css +20 -0
  163. package/template/public/social-card.png +0 -0
  164. package/template/public/styles.css +1521 -0
  165. package/template/public/widget.css +80 -0
  166. package/template/public/widget.js +220 -0
  167. package/template/reports.config.example.json +38 -0
  168. package/template/reports.schema.json +89 -0
  169. package/template/scripts/reports-auth-onboarding.mjs +180 -0
  170. package/template/scripts/reports-backup.mjs +276 -0
  171. package/template/scripts/reports-cloudflare-preflight.mjs +152 -0
  172. package/template/scripts/reports-deploy.mjs +173 -0
  173. package/template/scripts/reports-plan.mjs +226 -0
  174. package/template/scripts/reports-restore.mjs +180 -0
  175. package/template/scripts/reports-secrets.mjs +73 -0
  176. package/template/scripts/reports-uninstall-plan.mjs +32 -0
  177. package/template/src/agent-executor.ts +330 -0
  178. package/template/src/ai-observability.ts +163 -0
  179. package/template/src/ai-registry-validation.ts +244 -0
  180. package/template/src/ai-registry.ts +292 -0
  181. package/template/src/api-cursor.ts +84 -0
  182. package/template/src/assistant.ts +554 -0
  183. package/template/src/audit.ts +39 -0
  184. package/template/src/backup-service.ts +269 -0
  185. package/template/src/budget-control.ts +127 -0
  186. package/template/src/component-executor.ts +845 -0
  187. package/template/src/configuration-registry.ts +550 -0
  188. package/template/src/connection-egress.ts +430 -0
  189. package/template/src/connection-oauth.ts +368 -0
  190. package/template/src/evidence-service.ts +335 -0
  191. package/template/src/human-tasks.ts +316 -0
  192. package/template/src/index.ts +4114 -0
  193. package/template/src/installation-admin.ts +301 -0
  194. package/template/src/intake-abuse.ts +156 -0
  195. package/template/src/platform-registry-validation.ts +367 -0
  196. package/template/src/platform-registry.ts +753 -0
  197. package/template/src/report-admin.ts +342 -0
  198. package/template/src/report-ai-quality.ts +390 -0
  199. package/template/src/report-ai-validation.ts +80 -0
  200. package/template/src/report-ai.ts +857 -0
  201. package/template/src/report-auth.ts +292 -0
  202. package/template/src/report-better-auth.ts +583 -0
  203. package/template/src/report-context-schema.ts +132 -0
  204. package/template/src/report-context.ts +126 -0
  205. package/template/src/report-crypto.ts +142 -0
  206. package/template/src/report-delivery.ts +579 -0
  207. package/template/src/report-form-validation.ts +51 -0
  208. package/template/src/report-governance.ts +510 -0
  209. package/template/src/report-http.ts +90 -0
  210. package/template/src/report-operations.ts +685 -0
  211. package/template/src/report-operator-accounts.ts +810 -0
  212. package/template/src/report-presence.ts +398 -0
  213. package/template/src/report-queue-validation.ts +309 -0
  214. package/template/src/report-queues.ts +478 -0
  215. package/template/src/report-repository.ts +972 -0
  216. package/template/src/report-router-agent.ts +328 -0
  217. package/template/src/report-router-validation.ts +81 -0
  218. package/template/src/report-routing.ts +178 -0
  219. package/template/src/report-turnstile.ts +142 -0
  220. package/template/src/report-types.ts +224 -0
  221. package/template/src/report-validation.ts +279 -0
  222. package/template/src/report-workflow-validation.ts +119 -0
  223. package/template/src/report-workflow.ts +886 -0
  224. package/template/src/shadow-quality.ts +278 -0
  225. package/template/src/workflow-actions.ts +782 -0
  226. package/template/src/workflow-compiler.ts +230 -0
  227. package/template/src/workflow-dynamic-runtime.ts +613 -0
  228. package/template/src/workflow-effects.ts +316 -0
  229. package/template/src/workflow-expressions.ts +191 -0
  230. package/template/src/workflow-platform-types.ts +121 -0
  231. package/template/src/workflow-platform-validation.ts +540 -0
  232. package/template/src/workflow-repository.ts +916 -0
  233. package/template/src/workflow-runs.ts +686 -0
  234. package/template/src/workflow-simulator.ts +211 -0
  235. package/template/src/workspace-branding.ts +128 -0
  236. package/template/src/workspace-governance.ts +279 -0
  237. package/template/tsconfig.console.json +23 -0
  238. package/template/tsconfig.json +22 -0
  239. package/template/vite.console.config.ts +20 -0
  240. package/template/worker-configuration.d.ts +65 -0
@@ -0,0 +1,316 @@
1
+ import { canonicalJson } from "./audit";
2
+ import { loadWorkflowExecutionContext } from "./component-executor";
3
+ import { decryptReportContact, sha256 } from "./report-crypto";
4
+ import type { DeliveryJob } from "./report-types";
5
+ import { evaluateInputMapping } from "./workflow-expressions";
6
+ import type { JsonObject, JsonValue, WorkflowNodeV1 } from "./workflow-platform-types";
7
+
8
+ interface EffectRunRow {
9
+ id: string;
10
+ installationId: string;
11
+ reportId: string;
12
+ workflowVersionId: string;
13
+ authorityMode: string;
14
+ queueId: string | null;
15
+ queueVersionId: string | null;
16
+ publicReference: string;
17
+ reportState: string;
18
+ policyVersionId: string | null;
19
+ locale: string;
20
+ }
21
+
22
+ export class WorkflowEffectError extends Error {
23
+ constructor(readonly code: string, readonly retryable = false, readonly delaySeconds = 5) {
24
+ super(code);
25
+ this.name = "WorkflowEffectError";
26
+ }
27
+ }
28
+
29
+ function now(): string {
30
+ return new Date().toISOString();
31
+ }
32
+
33
+ async function effectRun(db: D1Database, runId: string): Promise<EffectRunRow> {
34
+ const row = await db.prepare(`
35
+ SELECT wr.id, wr.installation_id AS installationId, wr.report_id AS reportId,
36
+ wr.workflow_version_id AS workflowVersionId, wr.authority_mode AS authorityMode,
37
+ wr.queue_id AS queueId, wr.queue_version_id AS queueVersionId,
38
+ r.public_reference AS publicReference, r.state AS reportState,
39
+ r.policy_version_id AS policyVersionId, r.locale
40
+ FROM workflow_runs wr JOIN reports r ON r.id = wr.report_id WHERE wr.id = ?1 LIMIT 1
41
+ `).bind(runId).first<EffectRunRow>();
42
+ if (!row) throw new WorkflowEffectError("workflow_effect_context_missing");
43
+ return row;
44
+ }
45
+
46
+ function primitive(value: JsonValue, name: string): string {
47
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
48
+ throw new WorkflowEffectError(`workflow_message_${name}_invalid`);
49
+ }
50
+
51
+ function renderTemplate(template: string, variables: JsonObject): string {
52
+ const used = new Set<string>();
53
+ const rendered = template.replace(/\{\{\s*([A-Za-z0-9_-]{1,80})\s*\}\}/gu, (_match, key: string) => {
54
+ used.add(key);
55
+ if (!(key in variables)) throw new WorkflowEffectError("workflow_message_variable_missing");
56
+ return primitive(variables[key]!, "variable");
57
+ });
58
+ if (/\{\{|\}\}/u.test(rendered)) throw new WorkflowEffectError("workflow_message_template_invalid");
59
+ if (rendered.trim().length < 1 || rendered.length > 10_000) throw new WorkflowEffectError("workflow_message_render_invalid");
60
+ if (Object.keys(variables).some((key) => !used.has(key) && key !== "body")) throw new WorkflowEffectError("workflow_message_variable_unused");
61
+ return rendered;
62
+ }
63
+
64
+ export async function sendWorkflowMessage(
65
+ env: Env,
66
+ input: { runId: string; node: WorkflowNodeV1; branchKey: string },
67
+ ): Promise<Record<string, unknown>> {
68
+ const run = await effectRun(env.DB, input.runId);
69
+ if (run.authorityMode === "shadow") throw new WorkflowEffectError("shadow_effect_prohibited");
70
+ const audience = typeof input.node.config.audience === "string" ? input.node.config.audience : null;
71
+ const templateVersionId = typeof input.node.config.template_version_id === "string" ? input.node.config.template_version_id : null;
72
+ if (!audience || !["reporter", "affected_user", "internal"].includes(audience) || !templateVersionId) throw new WorkflowEffectError("workflow_message_configuration_invalid");
73
+ const operationKey = `workflow-message:${run.id}:${input.node.id}:${input.branchKey || "main"}`;
74
+ const existing = await env.DB.prepare(`SELECT id, delivery_state AS deliveryState FROM case_messages WHERE report_id = ?1 AND idempotency_key = ?2 LIMIT 1`)
75
+ .bind(run.reportId, operationKey).first<{ id: string; deliveryState: string }>();
76
+ if (existing) return { port: "sent", messageId: existing.id, deliveryState: existing.deliveryState, idempotentReplay: true };
77
+ const context = await loadWorkflowExecutionContext(env, run.id, input.branchKey);
78
+ const variables = evaluateInputMapping(input.node.inputMapping, context.context);
79
+ let body: string;
80
+ if (audience === "internal") {
81
+ body = typeof variables.body === "string" ? variables.body.trim() : "";
82
+ if (!body || body.length > 10_000) throw new WorkflowEffectError("workflow_message_body_invalid");
83
+ } else {
84
+ const template = await env.DB.prepare(`
85
+ SELECT body_template AS bodyTemplate FROM message_template_versions
86
+ WHERE id = ?1 AND installation_id = ?2 AND audience = ?3
87
+ AND published_at IS NOT NULL LIMIT 1
88
+ `).bind(templateVersionId, run.installationId, audience).first<{ bodyTemplate: string }>();
89
+ if (!template) throw new WorkflowEffectError("workflow_message_template_unavailable");
90
+ body = renderTemplate(template.bodyTemplate, variables);
91
+ }
92
+ const participant = audience === "internal" ? null : await env.DB.prepare(`
93
+ SELECT id, reference, contact_mode AS contactMode, contact_ciphertext AS contactCiphertext FROM report_participants
94
+ WHERE report_id = ?1 AND audience = ?2 LIMIT 1
95
+ `).bind(run.reportId, audience).first<{ id: string; reference: string | null; contactMode: string; contactCiphertext: string | null }>();
96
+ const deliveryChannels = Array.isArray(input.node.config.delivery_channels)
97
+ ? input.node.config.delivery_channels.filter((entry): entry is string => typeof entry === "string")
98
+ : audience === "internal" ? [] : ["customer_webhook", "email"];
99
+ const emailBinding = (env as unknown as { EMAIL?: { send(message: Record<string, unknown>): Promise<{ messageId?: string }> } }).EMAIL;
100
+ const emailAddress = participant?.contactCiphertext
101
+ ? (await decryptReportContact(env.CREDENTIAL_ENCRYPTION_KEY, participant.contactCiphertext, run.reportId, audience as "reporter" | "affected_user")).trim().toLowerCase()
102
+ : "";
103
+ const emailFrom = env.EMAIL_FROM_ADDRESS?.trim().toLowerCase() ?? "";
104
+ const emailEnabled = Boolean(
105
+ audience !== "internal" && deliveryChannels.includes("email")
106
+ && participant?.contactMode === "customer_notification"
107
+ && /^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(emailAddress)
108
+ && /^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(emailFrom)
109
+ && emailBinding,
110
+ );
111
+ const notificationEnabled = !emailEnabled && deliveryChannels.includes("customer_webhook") && Boolean((participant?.reference || participant?.contactCiphertext) && participant.contactMode === "customer_notification"
112
+ && env.NOTIFICATION_WEBHOOK_URL?.trim() && env.NOTIFICATION_WEBHOOK_SECRET?.trim());
113
+ const externalDeliveryEnabled = emailEnabled || notificationEnabled;
114
+ const messageId = crypto.randomUUID();
115
+ const conversationId = `conversation:${audience}:${run.reportId}`;
116
+ const timestamp = now();
117
+ const statements: D1PreparedStatement[] = [
118
+ env.DB.prepare(`
119
+ INSERT OR IGNORE INTO conversations (
120
+ id, installation_id, report_id, audience, participant_id, status,
121
+ locale, created_at, updated_at
122
+ ) VALUES (?1, ?2, ?3, ?4, ?5, 'open', ?6, ?7, ?7)
123
+ `).bind(conversationId, run.installationId, run.reportId, audience, participant?.id ?? null, run.locale, timestamp),
124
+ env.DB.prepare(`
125
+ INSERT INTO case_messages (
126
+ id, report_id, audience, direction, sender_type, sender_id, body,
127
+ template_version_id, automated, delivery_state, created_at,
128
+ idempotency_key, conversation_id, workflow_run_id, audience_checked_at
129
+ ) VALUES (?1, ?2, ?3, ?4, 'system', ?5, ?6, ?7, 1, ?8, ?9, ?10, ?11, ?5, ?9)
130
+ `).bind(
131
+ messageId, run.reportId, audience, audience === "internal" ? "internal" : "outbound",
132
+ run.id, body, templateVersionId, audience === "internal" ? "not_required" : externalDeliveryEnabled ? "queued" : "suppressed",
133
+ timestamp, operationKey, conversationId,
134
+ ),
135
+ ];
136
+ let job: DeliveryJob | null = null;
137
+ if (audience !== "internal") {
138
+ const noticeId = crypto.randomUUID();
139
+ statements.push(env.DB.prepare(`
140
+ INSERT INTO notices (
141
+ id, report_id, audience, template_version_id, rendered_body,
142
+ delivery_state, created_at, updated_at, idempotency_key, message_id
143
+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?7, ?8, ?9)
144
+ `).bind(
145
+ noticeId, run.reportId, audience, templateVersionId, body,
146
+ externalDeliveryEnabled ? "queued" : "suppressed", timestamp, operationKey, messageId,
147
+ ));
148
+ if (emailEnabled && participant) {
149
+ const emailDeliveryId = crypto.randomUUID();
150
+ statements.push(env.DB.prepare(`
151
+ INSERT INTO email_deliveries (
152
+ id, installation_id, message_id, audience, recipient_hash,
153
+ provider, state, attempt_count, next_attempt_at, created_at, updated_at
154
+ ) VALUES (?1, ?2, ?3, ?4, ?5, 'cloudflare_email_service', 'pending', 0, ?6, ?6, ?6)
155
+ `).bind(
156
+ emailDeliveryId, run.installationId, messageId, audience,
157
+ `sha256:${await sha256(`email-recipient-v1:${env.RATE_LIMIT_PEPPER}:${emailAddress}`)}`,
158
+ timestamp,
159
+ ));
160
+ job = { version: 1, jobId: `email:${emailDeliveryId}`, type: "deliver_email", outboxId: emailDeliveryId };
161
+ } else if (notificationEnabled && participant) {
162
+ const notificationId = crypto.randomUUID();
163
+ statements.push(env.DB.prepare(`
164
+ INSERT INTO notification_outbox (
165
+ id, notice_id, idempotency_key, channel, payload_json, status,
166
+ next_attempt_at, created_at, updated_at
167
+ ) VALUES (?1, ?2, ?3, 'customer_webhook', ?4, 'pending', ?5, ?5, ?5)
168
+ `).bind(notificationId, noticeId, `notice:${noticeId}`, canonicalJson({
169
+ version: 1,
170
+ event: "report.message.available",
171
+ notification_id: notificationId,
172
+ report_reference: run.publicReference,
173
+ ...(participant.reference ? { recipient_reference: participant.reference } : {}),
174
+ audience,
175
+ message: body,
176
+ }), timestamp));
177
+ job = { version: 1, jobId: `notification:${notificationId}`, type: "deliver_notification", outboxId: notificationId };
178
+ }
179
+ }
180
+ statements.push(env.DB.prepare(`
181
+ INSERT OR IGNORE INTO audit_events (
182
+ id, idempotency_key, action, actor_type, actor_id, target_type,
183
+ target_id, details_json, created_at
184
+ ) VALUES (?1, ?2, 'workflow.message_created', 'system', ?3, 'message', ?4, ?5, ?6)
185
+ `).bind(crypto.randomUUID(), operationKey, run.id, messageId, canonicalJson({
186
+ reportId: run.reportId, audience, templateVersionId,
187
+ deliveryChannels, emailEnabled, notificationEnabled,
188
+ }), timestamp));
189
+ await env.DB.batch(statements);
190
+ if (job) await env.REPORT_JOBS.send(job, { contentType: "json" }).catch(() => {});
191
+ return { port: "sent", messageId, deliveryState: audience === "internal" ? "not_required" : externalDeliveryEnabled ? "queued" : "suppressed", idempotentReplay: false };
192
+ }
193
+
194
+ function stringList(value: JsonValue | undefined, code: string): string[] {
195
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new WorkflowEffectError(code);
196
+ return value as string[];
197
+ }
198
+
199
+ export async function recordWorkflowDecision(
200
+ env: Env,
201
+ input: { runId: string; node: WorkflowNodeV1; branchKey: string },
202
+ ): Promise<Record<string, unknown>> {
203
+ const run = await effectRun(env.DB, input.runId);
204
+ if (run.authorityMode === "shadow") throw new WorkflowEffectError("shadow_effect_prohibited");
205
+ const operationKey = `workflow-decision:${run.id}:${input.node.id}:${input.branchKey || "main"}`;
206
+ const existing = await env.DB.prepare(`SELECT id, decision_code AS decisionCode FROM decisions WHERE report_id = ?1 AND idempotency_key = ?2 LIMIT 1`)
207
+ .bind(run.reportId, operationKey).first<{ id: string; decisionCode: string }>();
208
+ if (existing) return { port: "recorded", decisionId: existing.id, decisionCode: existing.decisionCode, idempotentReplay: true };
209
+ const policyVersionId = typeof input.node.config.policy_version_id === "string" ? input.node.config.policy_version_id : null;
210
+ if (!policyVersionId || policyVersionId !== run.policyVersionId) throw new WorkflowEffectError("workflow_decision_policy_mismatch");
211
+ if (!await env.DB.prepare(`SELECT id FROM policy_versions WHERE id = ?1 AND published_at IS NOT NULL`).bind(policyVersionId).first()) {
212
+ throw new WorkflowEffectError("workflow_decision_policy_unavailable");
213
+ }
214
+ const context = await loadWorkflowExecutionContext(env, run.id, input.branchKey);
215
+ const mapped = evaluateInputMapping(input.node.inputMapping, context.context);
216
+ const decisionCode = typeof mapped.decision_code === "string" ? mapped.decision_code : null;
217
+ const policyCode = typeof mapped.policy_code === "string" ? mapped.policy_code : null;
218
+ const rationale = typeof mapped.rationale === "string" ? mapped.rationale.trim() : null;
219
+ if (!decisionCode || decisionCode.length > 100 || !policyCode || policyCode.length > 100 || !rationale || rationale.length > 2_000) {
220
+ throw new WorkflowEffectError("workflow_decision_input_invalid");
221
+ }
222
+ const evidenceReferences = stringList(mapped.evidence_references ?? [], "workflow_decision_evidence_invalid");
223
+ const findingIds = stringList(mapped.finding_ids ?? [], "workflow_decision_findings_invalid");
224
+ const [evidence, findings] = await Promise.all([
225
+ evidenceReferences.length ? env.DB.prepare(`SELECT reference FROM evidence_references WHERE report_id = ?1`).bind(run.reportId).all<{ reference: string }>() : Promise.resolve({ results: [] as { reference: string }[] }),
226
+ findingIds.length ? env.DB.prepare(`SELECT id FROM findings WHERE report_id = ?1 AND run_id = ?2 AND status = 'available'`).bind(run.reportId, run.id).all<{ id: string }>() : Promise.resolve({ results: [] as { id: string }[] }),
227
+ ]);
228
+ const evidenceSet = new Set(evidence.results.map((entry) => entry.reference));
229
+ const findingSet = new Set(findings.results.map((entry) => entry.id));
230
+ if (evidenceReferences.some((entry) => !evidenceSet.has(entry)) || findingIds.some((entry) => !findingSet.has(entry))) {
231
+ throw new WorkflowEffectError("workflow_decision_evidence_not_visible");
232
+ }
233
+ const decisionId = crypto.randomUUID();
234
+ const timestamp = now();
235
+ const nextState = typeof input.node.config.report_state === "string" ? input.node.config.report_state : null;
236
+ const allowedStates = ["human_review", "awaiting_reporter", "resolved_by_ai", "human_review_requested", "closed"];
237
+ if (nextState && !allowedStates.includes(nextState)) throw new WorkflowEffectError("workflow_decision_state_invalid");
238
+ const statements: D1PreparedStatement[] = [
239
+ env.DB.prepare(`
240
+ INSERT INTO decisions (
241
+ id, report_id, decision_code, policy_version_id, policy_code, rationale,
242
+ evidence_references_json, maker_type, maker_id, queue_version_id,
243
+ proposed_action_code, created_at, idempotency_key
244
+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 'ai', ?8, ?9, ?10, ?11, ?12)
245
+ `).bind(
246
+ decisionId, run.reportId, decisionCode, policyVersionId, policyCode,
247
+ rationale, canonicalJson([...evidenceReferences, ...findingIds.map((id) => `finding:${id}`)]),
248
+ `workflow:${run.id}`, run.queueVersionId,
249
+ typeof mapped.proposed_action_code === "string" ? mapped.proposed_action_code : null,
250
+ timestamp, operationKey,
251
+ ),
252
+ env.DB.prepare(`
253
+ INSERT OR IGNORE INTO audit_events (
254
+ id, idempotency_key, action, actor_type, actor_id, target_type,
255
+ target_id, details_json, created_at
256
+ ) VALUES (?1, ?2, 'workflow.decision_recorded', 'system', ?3, 'decision', ?4, ?5, ?6)
257
+ `).bind(crypto.randomUUID(), operationKey, run.id, decisionId, canonicalJson({
258
+ reportId: run.reportId, decisionCode, policyVersionId, policyCode,
259
+ evidenceReferences, findingIds, authorityMode: run.authorityMode,
260
+ }), timestamp),
261
+ ];
262
+ if (nextState && nextState !== run.reportState) {
263
+ statements.push(
264
+ env.DB.prepare(`UPDATE reports SET state = ?2, resolved_at = CASE WHEN ?2 IN ('resolved_by_ai','closed') THEN ?3 ELSE resolved_at END, updated_at = ?3 WHERE id = ?1`)
265
+ .bind(run.reportId, nextState, timestamp),
266
+ env.DB.prepare(`
267
+ INSERT INTO report_state_history (
268
+ id, report_id, from_state, to_state, reason_code, actor_type, actor_id, created_at
269
+ ) VALUES (?1, ?2, ?3, ?4, ?5, 'system', ?6, ?7)
270
+ `).bind(crypto.randomUUID(), run.reportId, run.reportState, nextState, decisionCode, `workflow:${run.id}`, timestamp),
271
+ );
272
+ }
273
+ await env.DB.batch(statements);
274
+ return { port: "recorded", decisionId, decisionCode, idempotentReplay: false };
275
+ }
276
+
277
+ export async function handoffWorkflowReport(
278
+ env: Env,
279
+ input: { runId: string; node: WorkflowNodeV1; branchKey: string },
280
+ ): Promise<Record<string, unknown>> {
281
+ const run = await effectRun(env.DB, input.runId);
282
+ if (run.authorityMode === "shadow") throw new WorkflowEffectError("shadow_effect_prohibited");
283
+ const context = await loadWorkflowExecutionContext(env, run.id, input.branchKey);
284
+ const mapped = evaluateInputMapping(input.node.inputMapping, context.context);
285
+ const queueId = typeof mapped.queue_id === "string" ? mapped.queue_id : null;
286
+ const allowed = Array.isArray(input.node.config.allowed_queue_ids)
287
+ ? input.node.config.allowed_queue_ids.filter((entry): entry is string => typeof entry === "string") : [];
288
+ if (!queueId || !allowed.includes(queueId)) throw new WorkflowEffectError("workflow_handoff_queue_denied");
289
+ const queue = await env.DB.prepare(`SELECT id, active_version_id AS activeVersionId FROM queue_definitions WHERE id = ?1 AND status = 'active' LIMIT 1`)
290
+ .bind(queueId).first<{ id: string; activeVersionId: string | null }>();
291
+ if (!queue || !queue.activeVersionId) throw new WorkflowEffectError("workflow_handoff_queue_unavailable");
292
+ const operationKey = `workflow-handoff:${run.id}:${input.node.id}:${input.branchKey || "main"}`;
293
+ const existing = await env.DB.prepare(`SELECT id FROM report_queue_assignments WHERE report_id = ?1 AND assigned_by_id = ?2 LIMIT 1`)
294
+ .bind(run.reportId, operationKey).first<{ id: string }>();
295
+ if (existing) return { port: "handed_off", queueId, queueVersionId: queue.activeVersionId, assignmentId: existing.id, idempotentReplay: true };
296
+ const assignmentId = crypto.randomUUID();
297
+ const timestamp = now();
298
+ await env.DB.batch([
299
+ env.DB.prepare(`UPDATE report_queue_assignments SET ended_at = ?2 WHERE report_id = ?1 AND ended_at IS NULL`).bind(run.reportId, timestamp),
300
+ env.DB.prepare(`
301
+ INSERT INTO report_queue_assignments (
302
+ id, report_id, queue_id, queue_version_id, assignment_kind,
303
+ reason_json, assigned_by_type, assigned_by_id, assigned_at
304
+ ) VALUES (?1, ?2, ?3, ?4, 'handoff', ?5, 'system', ?6, ?7)
305
+ `).bind(assignmentId, run.reportId, queueId, queue.activeVersionId, canonicalJson({ workflow_run_id: run.id, node_id: input.node.id }), operationKey, timestamp),
306
+ env.DB.prepare(`UPDATE reports SET queue_id = ?2, queue_version_id = ?3, updated_at = ?4 WHERE id = ?1`)
307
+ .bind(run.reportId, queueId, queue.activeVersionId, timestamp),
308
+ env.DB.prepare(`UPDATE workflow_runs SET queue_id = ?2, queue_version_id = ?3, updated_at = ?4, revision = revision + 1 WHERE id = ?1`)
309
+ .bind(run.id, queueId, queue.activeVersionId, timestamp),
310
+ ]);
311
+ return { port: "handed_off", queueId, queueVersionId: queue.activeVersionId, assignmentId, idempotentReplay: false };
312
+ }
313
+
314
+ export async function outputDigest(value: unknown): Promise<string> {
315
+ return `sha256:${await sha256(canonicalJson(value))}`;
316
+ }
@@ -0,0 +1,191 @@
1
+ import type { JsonObject, JsonValue } from "./workflow-platform-types";
2
+
3
+ export class WorkflowExpressionError extends Error {
4
+ constructor(readonly code: string) {
5
+ super(code);
6
+ this.name = "WorkflowExpressionError";
7
+ }
8
+ }
9
+
10
+ const pathPattern = /^\$\.(?:[A-Za-z0-9_-]+)(?:\.[A-Za-z0-9_-]+)*$/u;
11
+ const interpolationPattern = /\{\{\s*(\$\.(?:[A-Za-z0-9_-]+)(?:\.[A-Za-z0-9_-]+)*)\s*\}\}/gu;
12
+ const prohibitedSegments = new Set(["__proto__", "prototype", "constructor"]);
13
+
14
+ function directPath(context: JsonObject, path: string): JsonValue | undefined {
15
+ if (!pathPattern.test(path)) throw new WorkflowExpressionError("expression_path_invalid");
16
+ let current: JsonValue | undefined = context;
17
+ for (const segment of path.slice(2).split(".")) {
18
+ if (prohibitedSegments.has(segment) || !current || typeof current !== "object" || Array.isArray(current)) return undefined;
19
+ current = current[segment];
20
+ }
21
+ return current;
22
+ }
23
+
24
+ function primitive(value: JsonValue, code: string): string | number | boolean | null {
25
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
26
+ throw new WorkflowExpressionError(code);
27
+ }
28
+
29
+ function numeric(value: JsonValue, code: string): number {
30
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new WorkflowExpressionError(code);
31
+ return value;
32
+ }
33
+
34
+ function truth(value: JsonValue): boolean {
35
+ if (typeof value !== "boolean") throw new WorkflowExpressionError("expression_boolean_required");
36
+ return value;
37
+ }
38
+
39
+ function operatorObject(value: JsonObject): [string, JsonValue] | null {
40
+ const entries = Object.entries(value);
41
+ return entries.length === 1 && entries[0]![0].startsWith("$") ? entries[0]! : null;
42
+ }
43
+
44
+ export function evaluateWorkflowExpression(
45
+ expression: JsonValue,
46
+ context: JsonObject,
47
+ depth = 0,
48
+ ): JsonValue {
49
+ if (depth > 20) throw new WorkflowExpressionError("expression_depth_exceeded");
50
+ if (expression === null || typeof expression === "number" || typeof expression === "boolean") return expression;
51
+ if (typeof expression === "string") {
52
+ if (expression.startsWith("$.") && pathPattern.test(expression)) return directPath(context, expression) ?? null;
53
+ if (expression.length > 10_000) throw new WorkflowExpressionError("expression_string_too_long");
54
+ return expression.replace(interpolationPattern, (_match, path: string) => {
55
+ const value = directPath(context, path);
56
+ const result = value === undefined || value === null ? "" : primitive(value, "expression_interpolation_primitive_required");
57
+ return String(result);
58
+ });
59
+ }
60
+ if (Array.isArray(expression)) {
61
+ if (expression.length > 100) throw new WorkflowExpressionError("expression_array_too_large");
62
+ return expression.map((entry) => evaluateWorkflowExpression(entry, context, depth + 1));
63
+ }
64
+ const operation = operatorObject(expression);
65
+ if (!operation) {
66
+ const output: JsonObject = {};
67
+ for (const [key, value] of Object.entries(expression)) {
68
+ if (prohibitedSegments.has(key)) throw new WorkflowExpressionError("expression_property_prohibited");
69
+ output[key] = evaluateWorkflowExpression(value, context, depth + 1);
70
+ }
71
+ return output;
72
+ }
73
+ const [operator, operand] = operation;
74
+ if (operator === "$literal") return operand;
75
+ if (operator === "$path") {
76
+ if (typeof operand !== "string") throw new WorkflowExpressionError("expression_path_invalid");
77
+ return directPath(context, operand) ?? null;
78
+ }
79
+ const list = Array.isArray(operand) ? operand : [operand];
80
+ const values = () => list.map((entry) => evaluateWorkflowExpression(entry, context, depth + 1));
81
+ if (operator === "$coalesce") return values().find((entry) => entry !== null) ?? null;
82
+ if (operator === "$concat") return values().map((entry) => String(primitive(entry, "expression_concat_primitive_required") ?? "")).join("");
83
+ if (operator === "$lower" || operator === "$upper") {
84
+ const value = evaluateWorkflowExpression(operand, context, depth + 1);
85
+ if (typeof value !== "string") throw new WorkflowExpressionError("expression_string_required");
86
+ return operator === "$lower" ? value.toLocaleLowerCase("en-US") : value.toLocaleUpperCase("en-US");
87
+ }
88
+ if (operator === "$length") {
89
+ const value = evaluateWorkflowExpression(operand, context, depth + 1);
90
+ if (typeof value !== "string" && !Array.isArray(value)) throw new WorkflowExpressionError("expression_length_value_invalid");
91
+ return value.length;
92
+ }
93
+ if (operator === "$not") return !truth(evaluateWorkflowExpression(operand, context, depth + 1));
94
+ if (operator === "$and" || operator === "$or") {
95
+ const booleans = values().map(truth);
96
+ return operator === "$and" ? booleans.every(Boolean) : booleans.some(Boolean);
97
+ }
98
+ if (["$eq", "$neq", "$gt", "$gte", "$lt", "$lte"].includes(operator)) {
99
+ const [left, right, ...rest] = values();
100
+ if (left === undefined || right === undefined || rest.length) throw new WorkflowExpressionError("expression_binary_arity_invalid");
101
+ if (operator === "$eq" || operator === "$neq") {
102
+ const equal = JSON.stringify(left) === JSON.stringify(right);
103
+ return operator === "$eq" ? equal : !equal;
104
+ }
105
+ const a = numeric(left, "expression_numeric_required");
106
+ const b = numeric(right, "expression_numeric_required");
107
+ if (operator === "$gt") return a > b;
108
+ if (operator === "$gte") return a >= b;
109
+ if (operator === "$lt") return a < b;
110
+ return a <= b;
111
+ }
112
+ if (operator === "$in") {
113
+ const [needle, haystack, ...rest] = values();
114
+ if (needle === undefined || !Array.isArray(haystack) || rest.length || haystack.length > 100) throw new WorkflowExpressionError("expression_membership_invalid");
115
+ return haystack.some((entry) => JSON.stringify(entry) === JSON.stringify(needle));
116
+ }
117
+ if (["$add", "$subtract", "$multiply", "$divide"].includes(operator)) {
118
+ const numbers = values().map((entry) => numeric(entry, "expression_numeric_required"));
119
+ if (numbers.length < 2 || numbers.length > 20) throw new WorkflowExpressionError("expression_arithmetic_arity_invalid");
120
+ let result = numbers[0]!;
121
+ for (const value of numbers.slice(1)) {
122
+ if (operator === "$add") result += value;
123
+ else if (operator === "$subtract") result -= value;
124
+ else if (operator === "$multiply") result *= value;
125
+ else {
126
+ if (value === 0) throw new WorkflowExpressionError("expression_division_by_zero");
127
+ result /= value;
128
+ }
129
+ if (!Number.isFinite(result) || Math.abs(result) > Number.MAX_SAFE_INTEGER) throw new WorkflowExpressionError("expression_numeric_overflow");
130
+ }
131
+ return result;
132
+ }
133
+ throw new WorkflowExpressionError("expression_operator_unsupported");
134
+ }
135
+
136
+ export function evaluateInputMapping(
137
+ mapping: Record<string, JsonValue>,
138
+ context: JsonObject,
139
+ ): JsonObject {
140
+ const output: JsonObject = {};
141
+ for (const [key, expression] of Object.entries(mapping)) output[key] = evaluateWorkflowExpression(expression, context);
142
+ return output;
143
+ }
144
+
145
+ export function validateJsonSchemaValue(
146
+ value: JsonValue,
147
+ schema: JsonObject,
148
+ path = "$",
149
+ errors: string[] = [],
150
+ ): string[] {
151
+ const type = schema.type;
152
+ const matches = type === undefined
153
+ || (type === "object" && value !== null && typeof value === "object" && !Array.isArray(value))
154
+ || (type === "array" && Array.isArray(value))
155
+ || (type === "string" && typeof value === "string")
156
+ || (type === "number" && typeof value === "number" && Number.isFinite(value))
157
+ || (type === "integer" && typeof value === "number" && Number.isInteger(value))
158
+ || (type === "boolean" && typeof value === "boolean")
159
+ || (type === "null" && value === null);
160
+ if (!matches) {
161
+ errors.push(`${path}:type`);
162
+ return errors;
163
+ }
164
+ if (Array.isArray(schema.enum) && !schema.enum.some((entry) => JSON.stringify(entry) === JSON.stringify(value))) errors.push(`${path}:enum`);
165
+ if (typeof value === "string") {
166
+ if (typeof schema.minLength === "number" && value.length < schema.minLength) errors.push(`${path}:minLength`);
167
+ if (typeof schema.maxLength === "number" && value.length > schema.maxLength) errors.push(`${path}:maxLength`);
168
+ }
169
+ if (typeof value === "number") {
170
+ if (typeof schema.minimum === "number" && value < schema.minimum) errors.push(`${path}:minimum`);
171
+ if (typeof schema.maximum === "number" && value > schema.maximum) errors.push(`${path}:maximum`);
172
+ }
173
+ if (Array.isArray(value)) {
174
+ if (typeof schema.maxItems === "number" && value.length > schema.maxItems) errors.push(`${path}:maxItems`);
175
+ if (schema.items && typeof schema.items === "object" && !Array.isArray(schema.items)) {
176
+ value.forEach((entry, index) => validateJsonSchemaValue(entry, schema.items as JsonObject, `${path}[${index}]`, errors));
177
+ }
178
+ }
179
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
180
+ const properties = schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties)
181
+ ? schema.properties as JsonObject : {};
182
+ const required = Array.isArray(schema.required) ? schema.required.filter((entry): entry is string => typeof entry === "string") : [];
183
+ for (const key of required) if (!(key in value)) errors.push(`${path}.${key}:required`);
184
+ for (const [key, entry] of Object.entries(value)) {
185
+ const propertySchema = properties[key];
186
+ if (propertySchema && typeof propertySchema === "object" && !Array.isArray(propertySchema)) validateJsonSchemaValue(entry, propertySchema, `${path}.${key}`, errors);
187
+ else if (schema.additionalProperties === false) errors.push(`${path}.${key}:additionalProperty`);
188
+ }
189
+ }
190
+ return errors;
191
+ }
@@ -0,0 +1,121 @@
1
+ export type WorkflowPurpose = "report" | "appeal" | "quality" | "operations";
2
+ export type WorkflowAuthorityMode = "off" | "shadow" | "assist" | "bounded_auto";
3
+ export type WorkflowNodeKind =
4
+ | "start"
5
+ | "enrichment"
6
+ | "parallel_group"
7
+ | "join"
8
+ | "condition"
9
+ | "ai_proposal"
10
+ | "human_task"
11
+ | "send_message"
12
+ | "wait_for_event"
13
+ | "delay"
14
+ | "delay_until"
15
+ | "decision"
16
+ | "action"
17
+ | "handoff"
18
+ | "start_linked_workflow"
19
+ | "end";
20
+
21
+ export type JsonPrimitive = string | number | boolean | null;
22
+ export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
23
+ export type JsonObject = { [key: string]: JsonValue };
24
+
25
+ export interface WorkflowNodeV1 {
26
+ id: string;
27
+ kind: WorkflowNodeKind;
28
+ name: string;
29
+ description?: string;
30
+ inputMapping: Record<string, JsonValue>;
31
+ timeoutPolicy?: JsonObject;
32
+ retryPolicy?: JsonObject;
33
+ errorPort?: string;
34
+ config: JsonObject;
35
+ metadata?: { x?: number; y?: number; groupId?: string };
36
+ }
37
+
38
+ export interface WorkflowEdgeV1 {
39
+ id: string;
40
+ from: { nodeId: string; port: string };
41
+ to: { nodeId: string; port: string };
42
+ label?: string;
43
+ }
44
+
45
+ export interface WorkflowGraphV1 {
46
+ schemaVersion: "1";
47
+ purpose: WorkflowPurpose;
48
+ entryNodeId: string;
49
+ nodes: WorkflowNodeV1[];
50
+ edges: WorkflowEdgeV1[];
51
+ inputSchema: JsonObject;
52
+ terminalCodes: string[];
53
+ defaultPolicies: {
54
+ timeout?: JsonValue;
55
+ retry?: JsonObject;
56
+ dataRetentionClass?: string;
57
+ };
58
+ }
59
+
60
+ export interface WorkflowValidationIssue {
61
+ code: string;
62
+ message: string;
63
+ path?: string;
64
+ nodeId?: string;
65
+ edgeId?: string;
66
+ }
67
+
68
+ export interface WorkflowDependencyReference {
69
+ type: "component" | "connection" | "policy" | "template" | "model_alias" | "agent" | "workflow";
70
+ key: string;
71
+ versionId: string;
72
+ }
73
+
74
+ export interface WorkflowCapabilityManifest {
75
+ version: 1;
76
+ capabilities: string[];
77
+ components: string[];
78
+ connections: string[];
79
+ eventTypes: string[];
80
+ audiences: string[];
81
+ actionCodes: string[];
82
+ maximumParallelBranches: number;
83
+ maximumStepExecutions: number;
84
+ }
85
+
86
+ export interface WorkflowValidationResult {
87
+ valid: boolean;
88
+ errors: WorkflowValidationIssue[];
89
+ warnings: WorkflowValidationIssue[];
90
+ dependencies: WorkflowDependencyReference[];
91
+ capabilityManifest: WorkflowCapabilityManifest;
92
+ reachableNodeIds: string[];
93
+ topologicalOrder: string[];
94
+ estimatedMaximumSteps: number;
95
+ }
96
+
97
+ export interface CompiledWorkflowArtifact {
98
+ compilerVersion: string;
99
+ compatibilityDate: string;
100
+ graphDigest: string;
101
+ sourceDigest: string;
102
+ bundleDigest: string;
103
+ manifestDigest: string;
104
+ source: string;
105
+ capabilityManifest: WorkflowCapabilityManifest;
106
+ dependencyManifest: WorkflowDependencyReference[];
107
+ warnings: WorkflowValidationIssue[];
108
+ }
109
+
110
+ export interface WorkflowDefinitionInput {
111
+ key: string;
112
+ name: string;
113
+ description: string;
114
+ purpose: WorkflowPurpose;
115
+ }
116
+
117
+ export interface WorkflowPublishInput {
118
+ expectedRevision: number;
119
+ authorityMode: WorkflowAuthorityMode;
120
+ testEvidence: JsonObject;
121
+ }