create-windy 0.3.2 → 0.3.5

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 (343) hide show
  1. package/README.md +27 -9
  2. package/dist/cli.js +825 -117
  3. package/package.json +1 -1
  4. package/template/.agents/skills/windy-business-module/SKILL.md +16 -3
  5. package/template/.windy-template.json +2 -2
  6. package/template/AGENTS.md +4 -0
  7. package/template/README.md +18 -11
  8. package/template/_gitignore +1 -0
  9. package/template/apps/agent-server/README.md +2 -0
  10. package/template/apps/agent-server/pyproject.toml +8 -0
  11. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +87 -10
  12. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +18 -3
  13. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +78 -11
  14. package/template/apps/agent-server/src/southwind_agent_server/app.py +120 -115
  15. package/template/apps/agent-server/src/southwind_agent_server/artifact_input.py +153 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/config.py +29 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +81 -1
  18. package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +8 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/engine.py +28 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/fingerprint.py +41 -0
  21. package/template/apps/agent-server/src/southwind_agent_server/http_helpers.py +62 -0
  22. package/template/apps/agent-server/src/southwind_agent_server/lease.py +15 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/multimodal.py +76 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +112 -20
  25. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +14 -1
  26. package/template/apps/agent-server/src/southwind_agent_server/postgres_store.py +329 -0
  27. package/template/apps/agent-server/src/southwind_agent_server/postgres_store_backend.py +332 -0
  28. package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +64 -3
  29. package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +68 -0
  30. package/template/apps/agent-server/src/southwind_agent_server/service.py +218 -56
  31. package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +36 -0
  32. package/template/apps/agent-server/src/southwind_agent_server/sqlite_catalog.py +107 -0
  33. package/template/apps/agent-server/src/southwind_agent_server/sqlite_schema.py +65 -0
  34. package/template/apps/agent-server/src/southwind_agent_server/sqlite_serialization.py +16 -0
  35. package/template/apps/agent-server/src/southwind_agent_server/sqlite_store.py +89 -57
  36. package/template/apps/agent-server/src/southwind_agent_server/store.py +105 -2
  37. package/template/apps/agent-server/src/southwind_agent_server/store_cipher.py +40 -0
  38. package/template/apps/agent-server/src/southwind_agent_server/telemetry.py +89 -0
  39. package/template/apps/agent-server/src/southwind_agent_server/tool_host.py +72 -0
  40. package/template/apps/agent-server/tests/test_adk_adapter.py +6 -2
  41. package/template/apps/agent-server/tests/test_api.py +24 -1
  42. package/template/apps/agent-server/tests/test_artifact_input.py +110 -0
  43. package/template/apps/agent-server/tests/test_config.py +4 -0
  44. package/template/apps/agent-server/tests/test_fingerprint.py +10 -0
  45. package/template/apps/agent-server/tests/test_multimodal.py +51 -0
  46. package/template/apps/agent-server/tests/test_openai_provider.py +6 -2
  47. package/template/apps/agent-server/tests/test_postgres_store.py +131 -0
  48. package/template/apps/agent-server/tests/test_provider_governance.py +61 -0
  49. package/template/apps/agent-server/tests/test_sqlite_store.py +95 -10
  50. package/template/apps/agent-server/tests/test_telemetry.py +25 -0
  51. package/template/apps/agent-server/tests/test_tool_approval.py +28 -0
  52. package/template/apps/agent-server/uv.lock +165 -0
  53. package/template/apps/document-server/Dockerfile +19 -0
  54. package/template/apps/document-server/pyproject.toml +43 -0
  55. package/template/apps/document-server/src/southwind_document_server/__init__.py +1 -0
  56. package/template/apps/document-server/src/southwind_document_server/adapter.py +337 -0
  57. package/template/apps/document-server/src/southwind_document_server/app.py +151 -0
  58. package/template/apps/document-server/src/southwind_document_server/config.py +94 -0
  59. package/template/apps/document-server/src/southwind_document_server/contracts.py +103 -0
  60. package/template/apps/document-server/src/southwind_document_server/service.py +149 -0
  61. package/template/apps/document-server/src/southwind_document_server/store.py +185 -0
  62. package/template/apps/document-server/tests/__init__.py +1 -0
  63. package/template/apps/document-server/tests/test_config.py +42 -0
  64. package/template/apps/document-server/tests/test_service.py +140 -0
  65. package/template/apps/document-server/uv.lock +430 -0
  66. package/template/apps/embedding-server/Dockerfile +17 -0
  67. package/template/apps/embedding-server/pyproject.toml +44 -0
  68. package/template/apps/embedding-server/src/southwind_embedding_server/__init__.py +1 -0
  69. package/template/apps/embedding-server/src/southwind_embedding_server/adapter.py +254 -0
  70. package/template/apps/embedding-server/src/southwind_embedding_server/app.py +119 -0
  71. package/template/apps/embedding-server/src/southwind_embedding_server/config.py +120 -0
  72. package/template/apps/embedding-server/src/southwind_embedding_server/contracts.py +106 -0
  73. package/template/apps/embedding-server/src/southwind_embedding_server/error.py +6 -0
  74. package/template/apps/embedding-server/src/southwind_embedding_server/image_input.py +87 -0
  75. package/template/apps/embedding-server/tests/__init__.py +1 -0
  76. package/template/apps/embedding-server/tests/test_adapter.py +92 -0
  77. package/template/apps/embedding-server/tests/test_config.py +38 -0
  78. package/template/apps/embedding-server/uv.lock +449 -0
  79. package/template/apps/server/src/agent/background-operation-port.ts +37 -0
  80. package/template/apps/server/src/agent/background-operation.test.ts +231 -0
  81. package/template/apps/server/src/agent/background-operation.ts +252 -0
  82. package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +114 -0
  83. package/template/apps/server/src/agent/execution-grants.ts +7 -1
  84. package/template/apps/server/src/agent/fake-runtime.test-support.ts +43 -0
  85. package/template/apps/server/src/agent/idempotency-key.test.ts +28 -0
  86. package/template/apps/server/src/agent/idempotency-key.ts +11 -0
  87. package/template/apps/server/src/agent/remote-runtime.test.ts +115 -1
  88. package/template/apps/server/src/agent/remote-runtime.ts +173 -5
  89. package/template/apps/server/src/agent/run-authorization.test-support.ts +9 -0
  90. package/template/apps/server/src/agent/run-authorization.test.ts +126 -0
  91. package/template/apps/server/src/agent/run-authorization.ts +180 -0
  92. package/template/apps/server/src/agent/run-binding-repository.ts +93 -0
  93. package/template/apps/server/src/agent/run-contracts.ts +88 -1
  94. package/template/apps/server/src/agent/run-definition-support.ts +58 -0
  95. package/template/apps/server/src/agent/run-errors.ts +25 -0
  96. package/template/apps/server/src/agent/run-facade.test.ts +57 -46
  97. package/template/apps/server/src/agent/run-facade.ts +185 -65
  98. package/template/apps/server/src/agent/run-release.ts +78 -0
  99. package/template/apps/server/src/agent/run-route-input.ts +169 -0
  100. package/template/apps/server/src/agent/run-routes.ts +63 -76
  101. package/template/apps/server/src/agent/runtime-bootstrap.ts +169 -13
  102. package/template/apps/server/src/agent/sandbox-client.ts +200 -0
  103. package/template/apps/server/src/agent/sandbox-protocol.ts +76 -0
  104. package/template/apps/server/src/agent/sandbox-registry.ts +205 -0
  105. package/template/apps/server/src/agent/sandbox-runtime.test.ts +137 -0
  106. package/template/apps/server/src/agent/sandbox-worker-server.ts +195 -0
  107. package/template/apps/server/src/agent/sandbox-worker.ts +12 -0
  108. package/template/apps/server/src/agent/tool-approval.test.ts +75 -0
  109. package/template/apps/server/src/agent/tool-approval.ts +115 -0
  110. package/template/apps/server/src/agent/tool-audit.ts +62 -0
  111. package/template/apps/server/src/agent/tool-host.test.ts +39 -11
  112. package/template/apps/server/src/agent/tool-host.ts +184 -17
  113. package/template/apps/server/src/agent/tool-registry.ts +52 -47
  114. package/template/apps/server/src/agent/vision-facade.test.ts +256 -0
  115. package/template/apps/server/src/agent/vision-input.ts +120 -0
  116. package/template/apps/server/src/ai-artifact/codec.test.ts +44 -0
  117. package/template/apps/server/src/ai-artifact/codec.ts +137 -0
  118. package/template/apps/server/src/ai-artifact/routes.ts +37 -0
  119. package/template/apps/server/src/ai-artifact/runtime.ts +205 -0
  120. package/template/apps/server/src/application-services.ts +62 -2
  121. package/template/apps/server/src/auth/reauthentication.test.ts +40 -0
  122. package/template/apps/server/src/auth/reauthentication.ts +19 -0
  123. package/template/apps/server/src/auth/runtime.ts +9 -1
  124. package/template/apps/server/src/data-access/context.ts +6 -4
  125. package/template/apps/server/src/data-access/scope-snapshot.ts +27 -0
  126. package/template/apps/server/src/data-governance/export/approval-port.ts +6 -3
  127. package/template/apps/server/src/data-governance/export/authorization-port.test.ts +23 -4
  128. package/template/apps/server/src/data-governance/export/authorization-port.ts +89 -25
  129. package/template/apps/server/src/data-governance/export/drizzle-repository.integration.test.ts +2 -0
  130. package/template/apps/server/src/data-governance/export/drizzle-repository.ts +1 -0
  131. package/template/apps/server/src/data-governance/export/job-runtime.test.ts +15 -4
  132. package/template/apps/server/src/data-governance/export/job-runtime.ts +68 -15
  133. package/template/apps/server/src/data-governance/export/ndjson-stream.ts +25 -6
  134. package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +4 -2
  135. package/template/apps/server/src/data-governance/export/renderer-job-runtime.test.ts +235 -0
  136. package/template/apps/server/src/data-governance/export/renderer-runtime.ts +293 -0
  137. package/template/apps/server/src/data-governance/export/renderer-selection.test.ts +67 -0
  138. package/template/apps/server/src/data-governance/export/repository.test.ts +2 -0
  139. package/template/apps/server/src/data-governance/export/request-fingerprint.test.ts +13 -0
  140. package/template/apps/server/src/data-governance/export/request-fingerprint.ts +2 -0
  141. package/template/apps/server/src/data-governance/export/routes.ts +11 -1
  142. package/template/apps/server/src/data-governance/export/scope-snapshot.ts +6 -18
  143. package/template/apps/server/src/data-governance/export/service-support.ts +188 -0
  144. package/template/apps/server/src/data-governance/export/service.ts +90 -113
  145. package/template/apps/server/src/data-governance/export/source.ts +101 -0
  146. package/template/apps/server/src/data-governance/export/types.ts +27 -0
  147. package/template/apps/server/src/document/contracts.ts +76 -0
  148. package/template/apps/server/src/document/facade.ts +339 -0
  149. package/template/apps/server/src/document/remote-runtime.test.ts +96 -0
  150. package/template/apps/server/src/document/remote-runtime.ts +223 -0
  151. package/template/apps/server/src/document/routes.ts +193 -0
  152. package/template/apps/server/src/embedding/contracts.ts +83 -0
  153. package/template/apps/server/src/embedding/error.ts +10 -0
  154. package/template/apps/server/src/embedding/facade.ts +217 -0
  155. package/template/apps/server/src/embedding/input.ts +172 -0
  156. package/template/apps/server/src/embedding/remote-runtime.test.ts +66 -0
  157. package/template/apps/server/src/embedding/remote-runtime.ts +218 -0
  158. package/template/apps/server/src/embedding/routes.ts +152 -0
  159. package/template/apps/server/src/index.ts +40 -43
  160. package/template/apps/server/src/jobs/artifact-service.test.ts +14 -2
  161. package/template/apps/server/src/jobs/artifact-service.ts +10 -0
  162. package/template/apps/server/src/jobs/drizzle-execution-repository.ts +17 -18
  163. package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +19 -1
  164. package/template/apps/server/src/jobs/drizzle-mappers.ts +4 -1
  165. package/template/apps/server/src/jobs/drizzle-recovery-repository.ts +49 -2
  166. package/template/apps/server/src/jobs/drizzle-repository.ts +1 -0
  167. package/template/apps/server/src/jobs/execution-repository.ts +17 -0
  168. package/template/apps/server/src/jobs/execution.test.ts +34 -0
  169. package/template/apps/server/src/jobs/in-memory-control.ts +135 -0
  170. package/template/apps/server/src/jobs/in-memory-execution.ts +34 -65
  171. package/template/apps/server/src/jobs/legacy-service-runtime.ts +5 -2
  172. package/template/apps/server/src/jobs/repository.ts +3 -0
  173. package/template/apps/server/src/jobs/worker.ts +80 -41
  174. package/template/apps/server/src/module-host/ai-operation-port.ts +170 -0
  175. package/template/apps/server/src/module-host/durable-context.test.ts +312 -0
  176. package/template/apps/server/src/module-host/durable-context.ts +124 -0
  177. package/template/apps/server/src/module-host/durable-handler-registration.ts +72 -0
  178. package/template/apps/server/src/module-host/governed-export-route-support.ts +171 -0
  179. package/template/apps/server/src/module-host/governed-export-routes.test.ts +327 -0
  180. package/template/apps/server/src/module-host/governed-export-routes.ts +238 -0
  181. package/template/apps/server/src/module-host/governed-renderer-registration.test.ts +247 -0
  182. package/template/apps/server/src/module-host/governed-resource-routes.ts +331 -0
  183. package/template/apps/server/src/module-host/governed-resources.test.ts +344 -0
  184. package/template/apps/server/src/module-host/governed-resources.ts +203 -0
  185. package/template/apps/server/src/module-host/http-runtime.ts +95 -0
  186. package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
  187. package/template/apps/server/src/module-host/initialize.test.ts +49 -1
  188. package/template/apps/server/src/module-host/initialize.ts +39 -16
  189. package/template/apps/server/src/module-host/job-port.test.ts +262 -0
  190. package/template/apps/server/src/module-host/job-port.ts +334 -0
  191. package/template/apps/server/src/module-host/request-context.test.ts +88 -2
  192. package/template/apps/server/src/module-host/request-context.ts +56 -22
  193. package/template/apps/server/src/module-host/route-installer.test.ts +10 -0
  194. package/template/apps/server/src/module-host/route-installer.ts +36 -1
  195. package/template/apps/server/src/module-host/sandbox-tool-registration.ts +42 -0
  196. package/template/apps/server/src/module-host/tool-handlers.ts +67 -31
  197. package/template/apps/server/src/module-host.ts +1 -0
  198. package/template/apps/server/src/module-runtime-validation.ts +7 -3
  199. package/template/apps/server/src/speech/contracts.ts +76 -0
  200. package/template/apps/server/src/speech/facade.ts +316 -0
  201. package/template/apps/server/src/speech/remote-runtime.test.ts +88 -0
  202. package/template/apps/server/src/speech/remote-runtime.ts +227 -0
  203. package/template/apps/server/src/speech/routes.ts +180 -0
  204. package/template/apps/server/src/system/actor.ts +39 -0
  205. package/template/apps/server/src/system/route-helpers.ts +1 -0
  206. package/template/apps/server/src/workflow/service.ts +2 -1
  207. package/template/apps/speech-server/Dockerfile +22 -0
  208. package/template/apps/speech-server/pyproject.toml +43 -0
  209. package/template/apps/speech-server/src/southwind_speech_server/__init__.py +1 -0
  210. package/template/apps/speech-server/src/southwind_speech_server/adapter.py +249 -0
  211. package/template/apps/speech-server/src/southwind_speech_server/app.py +183 -0
  212. package/template/apps/speech-server/src/southwind_speech_server/config.py +100 -0
  213. package/template/apps/speech-server/src/southwind_speech_server/contracts.py +103 -0
  214. package/template/apps/speech-server/src/southwind_speech_server/errors.py +6 -0
  215. package/template/apps/speech-server/src/southwind_speech_server/normalizer.py +108 -0
  216. package/template/apps/speech-server/src/southwind_speech_server/output_validation.py +128 -0
  217. package/template/apps/speech-server/src/southwind_speech_server/service.py +162 -0
  218. package/template/apps/speech-server/src/southwind_speech_server/store.py +209 -0
  219. package/template/apps/speech-server/tests/__init__.py +1 -0
  220. package/template/apps/speech-server/tests/test_config.py +33 -0
  221. package/template/apps/speech-server/tests/test_service.py +154 -0
  222. package/template/apps/speech-server/uv.lock +430 -0
  223. package/template/apps/web/src/app/business-routes.ts +22 -0
  224. package/template/apps/web/src/components/common/AccessSnapshotBoundary.vue +112 -0
  225. package/template/apps/web/src/components/common/AccessSnapshotBoundary.webtest.ts +130 -0
  226. package/template/apps/web/src/components/common/AccessSnapshotNotice.vue +117 -0
  227. package/template/apps/web/src/components/common/FormDialog.vue +6 -0
  228. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +13 -0
  229. package/template/apps/web/src/composables/access-snapshot-change.ts +131 -0
  230. package/template/apps/web/src/composables/access-snapshot-change.webtest.ts +103 -0
  231. package/template/apps/web/src/composables/useAccessSnapshot.ts +48 -2
  232. package/template/apps/web/src/composables/useAccessSnapshot.webtest.ts +54 -0
  233. package/template/apps/web/src/composables/useAccessSnapshotMonitor.ts +42 -0
  234. package/template/apps/web/src/layout/AppShell.vue +28 -25
  235. package/template/apps/web/src/layout/AppShell.webtest.ts +1 -0
  236. package/template/apps/web/src/router/business-routes.ts +68 -0
  237. package/template/apps/web/src/router/business-routes.webtest.ts +85 -0
  238. package/template/apps/web/src/router/index.ts +7 -0
  239. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  240. package/template/apps/web/src/services/access-api.ts +5 -1
  241. package/template/apps/web/src/services/access-api.webtest.ts +35 -0
  242. package/template/docker-compose.yml +127 -0
  243. package/template/docs/architecture/ai-egress.md +12 -9
  244. package/template/docs/architecture/ai-runtime.md +23 -11
  245. package/template/docs/architecture/durable-jobs.md +25 -3
  246. package/template/docs/architecture/foundation-infrastructure.md +163 -0
  247. package/template/docs/architecture/module-host.md +115 -0
  248. package/template/docs/architecture/server-module-interface.md +39 -0
  249. package/template/docs/architecture/web-access-snapshot-resilience.md +60 -0
  250. package/template/docs/development/business-web-routes.md +66 -0
  251. package/template/docs/development/custom-login-page.md +3 -3
  252. package/template/docs/development/module-lifecycle.md +71 -0
  253. package/template/docs/operations/observability.md +5 -1
  254. package/template/docs/platform/agent-runtime.md +21 -11
  255. package/template/docs/platform/agent-tool-sandbox.md +58 -0
  256. package/template/docs/platform/agent-tools.md +5 -0
  257. package/template/docs/platform/document-ai-runtime.md +204 -0
  258. package/template/docs/platform/governed-export-renderers.md +132 -0
  259. package/template/docs/platform/navigation-registry.md +5 -0
  260. package/template/docs/platform/organization-membership.md +2 -1
  261. package/template/docs/platform/speech-ai-runtime.md +156 -0
  262. package/template/docs/platform/system-crud-api.md +7 -1
  263. package/template/docs/platform/vision-ai-runtime.md +97 -0
  264. package/template/docs/platform/web-system-crud.md +6 -0
  265. package/template/package.json +16 -2
  266. package/template/packages/ai-client/index.ts +4 -0
  267. package/template/packages/ai-client/package.json +5 -2
  268. package/template/packages/ai-client/src/agent/client/artifact.ts +26 -0
  269. package/template/packages/ai-client/src/agent/client/attach.test.ts +124 -0
  270. package/template/packages/ai-client/src/agent/client/client.ts +119 -7
  271. package/template/packages/ai-client/src/agent/client/run.ts +14 -0
  272. package/template/packages/ai-client/src/agent/client/types.ts +18 -0
  273. package/template/packages/ai-client/src/agent/client/vision.test.ts +53 -0
  274. package/template/packages/ai-client/src/agent/contracts/contracts.test.ts +1 -0
  275. package/template/packages/ai-client/src/agent/contracts/events.ts +33 -0
  276. package/template/packages/ai-client/src/agent/contracts/runs.ts +71 -0
  277. package/template/packages/ai-client/src/agent/index.ts +1 -0
  278. package/template/packages/ai-client/src/artifact-ref.ts +8 -0
  279. package/template/packages/ai-client/src/document/client.test.ts +114 -0
  280. package/template/packages/ai-client/src/document/client.ts +134 -0
  281. package/template/packages/ai-client/src/document/contracts.ts +152 -0
  282. package/template/packages/ai-client/src/document/error.ts +43 -0
  283. package/template/packages/ai-client/src/document/index.ts +3 -0
  284. package/template/packages/ai-client/src/document/run.ts +104 -0
  285. package/template/packages/ai-client/src/document/validation.ts +115 -0
  286. package/template/packages/ai-client/src/embedding/client.test.ts +77 -0
  287. package/template/packages/ai-client/src/embedding/client.ts +96 -0
  288. package/template/packages/ai-client/src/embedding/contracts.ts +46 -0
  289. package/template/packages/ai-client/src/embedding/error.ts +61 -0
  290. package/template/packages/ai-client/src/embedding/index.ts +3 -0
  291. package/template/packages/ai-client/src/embedding/validation.ts +117 -0
  292. package/template/packages/ai-client/src/speech/client.test.ts +135 -0
  293. package/template/packages/ai-client/src/speech/client.ts +104 -0
  294. package/template/packages/ai-client/src/speech/contracts.ts +129 -0
  295. package/template/packages/ai-client/src/speech/error.ts +43 -0
  296. package/template/packages/ai-client/src/speech/index.ts +3 -0
  297. package/template/packages/ai-client/src/speech/run.ts +133 -0
  298. package/template/packages/ai-client/src/speech/validation.ts +132 -0
  299. package/template/packages/config/package.json +2 -2
  300. package/template/packages/database/drizzle/0039_early_ezekiel_stane.sql +1 -0
  301. package/template/packages/database/drizzle/0040_colossal_baron_strucker.sql +63 -0
  302. package/template/packages/database/drizzle/0041_yummy_prowler.sql +21 -0
  303. package/template/packages/database/drizzle/0042_puzzling_rumiko_fujikawa.sql +27 -0
  304. package/template/packages/database/drizzle/meta/0039_snapshot.json +7755 -0
  305. package/template/packages/database/drizzle/meta/0040_snapshot.json +8150 -0
  306. package/template/packages/database/drizzle/meta/0041_snapshot.json +8323 -0
  307. package/template/packages/database/drizzle/meta/0042_snapshot.json +8541 -0
  308. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  309. package/template/packages/database/package.json +2 -2
  310. package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
  311. package/template/packages/database/src/schema/agent-runtime.ts +149 -0
  312. package/template/packages/database/src/schema/common.ts +2 -14
  313. package/template/packages/database/src/schema/durable-jobs.ts +5 -0
  314. package/template/packages/database/src/schema/governed-exports.ts +17 -5
  315. package/template/packages/database/src/schema/index.ts +3 -0
  316. package/template/packages/database/src/schema-durable-recovery.test.ts +31 -0
  317. package/template/packages/database/src/schema-governed-exports.test.ts +21 -1
  318. package/template/packages/database/src/schema.test.ts +6 -23
  319. package/template/packages/jobs/package.json +3 -2
  320. package/template/packages/jobs/src/handlers.test.ts +13 -0
  321. package/template/packages/jobs/src/handlers.ts +7 -1
  322. package/template/packages/jobs/src/types.ts +4 -0
  323. package/template/packages/modules/package.json +2 -2
  324. package/template/packages/modules/src/ai-operation-composition.test-support.ts +165 -0
  325. package/template/packages/modules/src/ai-operation-composition.test.ts +91 -127
  326. package/template/packages/modules/src/ai-operation-composition.ts +59 -1
  327. package/template/packages/modules/src/manifest.ts +25 -1
  328. package/template/packages/modules/src/tool-composition.ts +18 -0
  329. package/template/packages/server-sdk/index.ts +50 -0
  330. package/template/packages/server-sdk/package.json +10 -4
  331. package/template/packages/server-sdk/src/ai-artifact-port.ts +34 -0
  332. package/template/packages/server-sdk/src/ai-operation-port.ts +65 -0
  333. package/template/packages/server-sdk/src/ai-operation-registration.ts +42 -1
  334. package/template/packages/server-sdk/src/background-ai-port.ts +43 -0
  335. package/template/packages/server-sdk/src/data-governance-registration.ts +116 -0
  336. package/template/packages/server-sdk/src/durable-handler.ts +29 -0
  337. package/template/packages/server-sdk/src/jobs-port.ts +69 -0
  338. package/template/packages/server-sdk/src/module-host.ts +16 -2
  339. package/template/packages/server-sdk/src/request-context.ts +6 -2
  340. package/template/packages/server-sdk/src/sandbox-tool.ts +21 -0
  341. package/template/packages/server-sdk/src/tool-registration.ts +2 -0
  342. package/template/packages/shared/package.json +1 -1
  343. package/template/packages/shared/src/rbac.ts +30 -0
@@ -1,27 +1,15 @@
1
- import { createHash } from "node:crypto";
2
1
  import type { GovernedExportScopeSnapshot } from "@southwind-ai/database";
3
- import { snapshotServerScopeContext } from "../../data-access/context.js";
2
+ import {
3
+ captureAuthorizationScope,
4
+ hashAuthorizationScope,
5
+ } from "../../data-access/scope-snapshot.js";
4
6
 
5
7
  export function captureGovernedExportScope(value: unknown) {
6
- const snapshot = snapshotServerScopeContext(value);
7
- return { snapshot, hash: hashGovernedExportScope(snapshot) };
8
+ return captureAuthorizationScope(value);
8
9
  }
9
10
 
10
11
  export function hashGovernedExportScope(
11
12
  snapshot: GovernedExportScopeSnapshot,
12
13
  ): string {
13
- return createHash("sha256").update(stableJson(snapshot)).digest("hex");
14
- }
15
-
16
- function stableJson(value: unknown): string {
17
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
18
- if (value && typeof value === "object") {
19
- const entries = Object.entries(value as Record<string, unknown>)
20
- .filter(([, item]) => item !== undefined)
21
- .sort(([left], [right]) => left.localeCompare(right));
22
- return `{${entries
23
- .map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`)
24
- .join(",")}}`;
25
- }
26
- return JSON.stringify(value);
14
+ return hashAuthorizationScope(snapshot);
27
15
  }
@@ -0,0 +1,188 @@
1
+ import type { GovernedExportQuerySnapshot } from "@southwind-ai/database";
2
+ import type { AuditEvent } from "@southwind-ai/shared";
3
+ import { auditActor, type RequestGuardContext } from "../../guards.js";
4
+ import type { DurableArtifactService } from "../../jobs/artifact-service.js";
5
+ import {
6
+ legacyGovernedExportRendererSnapshot,
7
+ type GovernedExportRequest,
8
+ type GovernedExportStatus,
9
+ } from "./types.js";
10
+
11
+ export interface CreateGovernedExportInput {
12
+ idempotencyKey: string;
13
+ reason: string;
14
+ query: GovernedExportQuerySnapshot;
15
+ formatKey?: string;
16
+ }
17
+
18
+ export interface GovernedExportPolicy {
19
+ approvalRequired: boolean;
20
+ retentionSeconds: number;
21
+ }
22
+
23
+ export interface GovernedExportTarget {
24
+ readonly resourceType: string;
25
+ readonly policyKey: string;
26
+ readonly policyVersion: string;
27
+ readonly fieldKeys: readonly string[];
28
+ readonly approvalRequired?: boolean;
29
+ readonly retentionSeconds?: number;
30
+ readonly renderers?: readonly GovernedExportRendererTarget[];
31
+ }
32
+
33
+ export interface GovernedExportRendererTarget {
34
+ readonly formatKey: string;
35
+ readonly templateKey: string;
36
+ readonly templateVersion: string;
37
+ readonly inputSchemaKey: string;
38
+ readonly contentType: string;
39
+ readonly filenameExtension: string;
40
+ readonly maxRecords: number;
41
+ readonly maxOutputBytes: number;
42
+ readonly timeoutSeconds: number;
43
+ }
44
+
45
+ export const legacyNdjsonRenderer: GovernedExportRendererTarget = {
46
+ ...legacyGovernedExportRendererSnapshot,
47
+ };
48
+
49
+ export const governedUserTarget: GovernedExportTarget = {
50
+ resourceType: "system.user",
51
+ policyKey: "system.user.fields",
52
+ policyVersion: "1",
53
+ fieldKeys: ["email", "phone"],
54
+ };
55
+
56
+ export class GovernedExportServiceError extends Error {
57
+ constructor(
58
+ readonly code: string,
59
+ readonly status: number,
60
+ ) {
61
+ super(code);
62
+ this.name = "GovernedExportServiceError";
63
+ }
64
+ }
65
+
66
+ export function validateCreate(input: CreateGovernedExportInput) {
67
+ if (!input.idempotencyKey.trim() || input.idempotencyKey.length > 120) {
68
+ throw new GovernedExportServiceError("EXPORT_IDEMPOTENCY_KEY_INVALID", 400);
69
+ }
70
+ if (input.reason.trim().length < 5 || input.reason.length > 200) {
71
+ throw new GovernedExportServiceError("EXPORT_REASON_INVALID", 400);
72
+ }
73
+ }
74
+
75
+ export function selectRenderer(
76
+ input: CreateGovernedExportInput,
77
+ target: GovernedExportTarget,
78
+ ): GovernedExportRendererTarget {
79
+ const renderers = target.renderers;
80
+ if (!renderers?.length) {
81
+ if (input.formatKey !== undefined && input.formatKey !== "ndjson") {
82
+ throw new GovernedExportServiceError("EXPORT_FORMAT_NOT_REGISTERED", 400);
83
+ }
84
+ return legacyNdjsonRenderer;
85
+ }
86
+ if (!input.formatKey?.trim()) {
87
+ throw new GovernedExportServiceError("EXPORT_FORMAT_REQUIRED", 400);
88
+ }
89
+ const renderer = renderers.find(
90
+ ({ formatKey }) => formatKey === input.formatKey,
91
+ );
92
+ if (!renderer) {
93
+ throw new GovernedExportServiceError("EXPORT_FORMAT_NOT_REGISTERED", 400);
94
+ }
95
+ return renderer;
96
+ }
97
+
98
+ export function validateTarget(target: GovernedExportTarget) {
99
+ if (
100
+ !target.resourceType.trim() ||
101
+ !target.policyKey.trim() ||
102
+ !target.policyVersion.trim() ||
103
+ target.fieldKeys.length === 0 ||
104
+ new Set(target.fieldKeys).size !== target.fieldKeys.length ||
105
+ target.fieldKeys.some((key) => !key.trim()) ||
106
+ (target.retentionSeconds !== undefined &&
107
+ (!Number.isInteger(target.retentionSeconds) ||
108
+ target.retentionSeconds < 60))
109
+ ) {
110
+ throw new GovernedExportServiceError("EXPORT_TARGET_INVALID", 500);
111
+ }
112
+ }
113
+
114
+ export function durableStatus(
115
+ status: string,
116
+ ): GovernedExportStatus | undefined {
117
+ return ["queued", "running", "succeeded", "failed", "cancelled"].includes(
118
+ status,
119
+ )
120
+ ? (status as GovernedExportStatus)
121
+ : undefined;
122
+ }
123
+
124
+ export function assertArtifactMetadata(
125
+ artifact: Awaited<ReturnType<DurableArtifactService["inspect"]>>,
126
+ request: GovernedExportRequest,
127
+ ) {
128
+ if (
129
+ artifact.jobId !== request.durableJobId ||
130
+ artifact.ownerId !== request.requesterId ||
131
+ artifact.contentType !== request.renderer.contentType ||
132
+ !artifact.filename.endsWith(`.${request.renderer.filenameExtension}`) ||
133
+ artifact.metadata?.formatKey !== request.renderer.formatKey ||
134
+ artifact.metadata?.templateKey !== request.renderer.templateKey ||
135
+ artifact.metadata?.templateVersion !== request.renderer.templateVersion ||
136
+ artifact.metadata?.inputSchemaKey !== request.renderer.inputSchemaKey ||
137
+ artifact.byteSize < 0 ||
138
+ artifact.byteSize > request.renderer.maxOutputBytes ||
139
+ !/^[a-f0-9]{64}$/.test(artifact.sha256)
140
+ ) {
141
+ throw new GovernedExportServiceError("EXPORT_ARTIFACT_INVALID", 409);
142
+ }
143
+ }
144
+
145
+ export function exportEvent(
146
+ context: RequestGuardContext,
147
+ action: AuditEvent["action"],
148
+ id: string,
149
+ metadata: Record<string, unknown>,
150
+ resourceType: string,
151
+ ): AuditEvent {
152
+ return {
153
+ id: Bun.randomUUIDv7(),
154
+ action,
155
+ outcome: "success",
156
+ actor: auditActor(context),
157
+ target: { type: `${resourceType}.governed-export`, id },
158
+ requestId: context.requestId,
159
+ metadata,
160
+ occurredAt: new Date().toISOString(),
161
+ };
162
+ }
163
+
164
+ export function downloadEvent(
165
+ context: RequestGuardContext,
166
+ request: GovernedExportRequest,
167
+ artifact: Awaited<ReturnType<DurableArtifactService["inspect"]>>,
168
+ phase: "intent" | "authorized",
169
+ ) {
170
+ return exportEvent(
171
+ context,
172
+ "data.governed-export.download",
173
+ request.id,
174
+ {
175
+ phase,
176
+ scopeHash: request.scopeHash,
177
+ jobId: request.durableJobId,
178
+ sha256: artifact.sha256,
179
+ byteSize: artifact.byteSize,
180
+ filename: artifact.filename,
181
+ contentType: artifact.contentType,
182
+ formatKey: request.renderer.formatKey,
183
+ templateKey: request.renderer.templateKey,
184
+ templateVersion: request.renderer.templateVersion,
185
+ },
186
+ request.resourceType,
187
+ );
188
+ }
@@ -1,6 +1,4 @@
1
- import type { GovernedExportQuerySnapshot } from "@southwind-ai/database";
2
- import type { AuditEvent } from "@southwind-ai/shared";
3
- import { auditActor, type RequestGuardContext } from "../../guards.js";
1
+ import type { RequestGuardContext } from "../../guards.js";
4
2
  import {
5
3
  DurableArtifactError,
6
4
  type DurableArtifactService,
@@ -14,17 +12,27 @@ import type { GovernedExportRepository } from "./repository.js";
14
12
  import { captureGovernedExportScope } from "./scope-snapshot.js";
15
13
  import type { GovernedExportRequest, GovernedExportStatus } from "./types.js";
16
14
  import { fingerprintGovernedExportRequest } from "./request-fingerprint.js";
15
+ import {
16
+ assertArtifactMetadata,
17
+ downloadEvent,
18
+ durableStatus,
19
+ exportEvent,
20
+ GovernedExportServiceError,
21
+ governedUserTarget,
22
+ selectRenderer,
23
+ validateCreate,
24
+ validateTarget,
25
+ type CreateGovernedExportInput,
26
+ type GovernedExportPolicy,
27
+ type GovernedExportTarget,
28
+ } from "./service-support.js";
17
29
 
18
- export interface CreateGovernedExportInput {
19
- idempotencyKey: string;
20
- reason: string;
21
- query: GovernedExportQuerySnapshot;
22
- }
23
-
24
- export interface GovernedExportPolicy {
25
- approvalRequired: boolean;
26
- retentionSeconds: number;
27
- }
30
+ export {
31
+ GovernedExportServiceError,
32
+ type CreateGovernedExportInput,
33
+ type GovernedExportPolicy,
34
+ type GovernedExportTarget,
35
+ } from "./service-support.js";
28
36
 
29
37
  type AuditSink = ReturnType<typeof createServerRuntime>["auditSink"];
30
38
 
@@ -43,19 +51,26 @@ export class GovernedExportService {
43
51
  async request(
44
52
  input: CreateGovernedExportInput,
45
53
  context: RequestGuardContext,
54
+ target: GovernedExportTarget = governedUserTarget,
46
55
  ) {
47
56
  validateCreate(input);
57
+ validateTarget(target);
58
+ const renderer = selectRenderer(input, target);
48
59
  const scope = captureGovernedExportScope(context.actor.scopeContext);
49
60
  const id = Bun.randomUUIDv7();
61
+ const retentionSeconds = Math.min(
62
+ target.retentionSeconds ?? this.policy.retentionSeconds,
63
+ this.policy.retentionSeconds,
64
+ );
50
65
  const expiresAt = new Date(
51
- this.now().getTime() + this.policy.retentionSeconds * 1_000,
66
+ this.now().getTime() + retentionSeconds * 1_000,
52
67
  ).toISOString();
53
68
  const draft = {
54
69
  id,
55
- resourceType: "system.user",
56
- policyKey: "system.user.fields",
57
- policyVersion: "1",
58
- fieldKeys: ["email", "phone"],
70
+ resourceType: target.resourceType,
71
+ policyKey: target.policyKey,
72
+ policyVersion: target.policyVersion,
73
+ fieldKeys: [...target.fieldKeys].sort(),
59
74
  requesterId: context.actor.id,
60
75
  requesterName: context.actor.displayName || context.actor.name,
61
76
  idempotencyKey: input.idempotencyKey,
@@ -63,7 +78,8 @@ export class GovernedExportService {
63
78
  scopeSnapshot: scope.snapshot,
64
79
  scopeHash: scope.hash,
65
80
  querySnapshot: structuredClone(input.query),
66
- approvalRequired: this.policy.approvalRequired,
81
+ renderer: structuredClone(renderer),
82
+ approvalRequired: target.approvalRequired ?? this.policy.approvalRequired,
67
83
  expiresAt,
68
84
  };
69
85
  const created = await this.requests.create(
@@ -77,29 +93,53 @@ export class GovernedExportService {
77
93
  throw new GovernedExportServiceError("EXPORT_IDEMPOTENCY_CONFLICT", 409);
78
94
  }
79
95
  await this.audit.recordCriticalEvent(
80
- event(context, "data.governed-export.request", created.request.id, {
81
- policyKey: created.request.policyKey,
82
- policyVersion: created.request.policyVersion,
83
- scopeHash: created.request.scopeHash,
84
- requestFingerprint: created.request.requestFingerprint,
85
- replayed: !created.created,
86
- }),
96
+ exportEvent(
97
+ context,
98
+ "data.governed-export.request",
99
+ created.request.id,
100
+ {
101
+ policyKey: created.request.policyKey,
102
+ policyVersion: created.request.policyVersion,
103
+ scopeHash: created.request.scopeHash,
104
+ requestFingerprint: created.request.requestFingerprint,
105
+ formatKey: created.request.renderer.formatKey,
106
+ templateKey: created.request.renderer.templateKey,
107
+ templateVersion: created.request.renderer.templateVersion,
108
+ replayed: !created.created,
109
+ },
110
+ created.request.resourceType,
111
+ ),
87
112
  );
88
113
  return this.reconcile(created.request, context);
89
114
  }
90
115
 
91
- async status(id: string, context: RequestGuardContext) {
92
- return this.reconcile(await this.owned(id, context), context);
116
+ async status(
117
+ id: string,
118
+ context: RequestGuardContext,
119
+ target?: GovernedExportTarget,
120
+ ) {
121
+ return this.reconcile(await this.owned(id, context, target), context);
93
122
  }
94
123
 
95
- async cancel(id: string, context: RequestGuardContext, reason: string) {
96
- const request = await this.owned(id, context);
124
+ async cancel(
125
+ id: string,
126
+ context: RequestGuardContext,
127
+ reason: string,
128
+ target?: GovernedExportTarget,
129
+ ) {
130
+ const request = await this.owned(id, context, target);
97
131
  if (request.status === "cancelled") return request;
98
132
  await this.audit.recordCriticalEvent(
99
- event(context, "data.governed-export.cancel", request.id, {
100
- scopeHash: request.scopeHash,
101
- jobId: request.durableJobId,
102
- }),
133
+ exportEvent(
134
+ context,
135
+ "data.governed-export.cancel",
136
+ request.id,
137
+ {
138
+ scopeHash: request.scopeHash,
139
+ jobId: request.durableJobId,
140
+ },
141
+ request.resourceType,
142
+ ),
103
143
  );
104
144
  await this.approvals.cancel(
105
145
  request,
@@ -121,9 +161,10 @@ export class GovernedExportService {
121
161
  async download(
122
162
  id: string,
123
163
  context: RequestGuardContext,
164
+ target?: GovernedExportTarget,
124
165
  ): Promise<OpenedDurableArtifact> {
125
166
  const request = await this.reconcile(
126
- await this.owned(id, context),
167
+ await this.owned(id, context, target),
127
168
  context,
128
169
  );
129
170
  if (request.status !== "succeeded" || !request.durableJobId) {
@@ -138,12 +179,12 @@ export class GovernedExportService {
138
179
  );
139
180
  assertArtifactMetadata(artifact, request);
140
181
  await this.audit.recordCriticalEvent(
141
- downloadEvent(context, request, artifact.sha256, "intent"),
182
+ downloadEvent(context, request, artifact, "intent"),
142
183
  );
143
184
  const opened = await this.artifacts.openInspected(artifact);
144
185
  try {
145
186
  await this.audit.recordCriticalEvent(
146
- downloadEvent(context, request, artifact.sha256, "authorized"),
187
+ downloadEvent(context, request, artifact, "authorized"),
147
188
  );
148
189
  } catch (error) {
149
190
  await opened.body.cancel().catch(() => undefined);
@@ -152,10 +193,20 @@ export class GovernedExportService {
152
193
  return opened;
153
194
  }
154
195
 
155
- private async owned(id: string, context: RequestGuardContext) {
196
+ private async owned(
197
+ id: string,
198
+ context: RequestGuardContext,
199
+ target?: GovernedExportTarget,
200
+ ) {
156
201
  const request = await this.requests.get(id);
157
202
  if (!request) throw new GovernedExportServiceError("EXPORT_NOT_FOUND", 404);
158
- if (request.requesterId !== context.actor.id) {
203
+ if (
204
+ request.requesterId !== context.actor.id ||
205
+ (target &&
206
+ (request.resourceType !== target.resourceType ||
207
+ request.policyKey !== target.policyKey ||
208
+ request.policyVersion !== target.policyVersion))
209
+ ) {
159
210
  throw new GovernedExportServiceError("EXPORT_FORBIDDEN", 403);
160
211
  }
161
212
  const current = captureGovernedExportScope(context.actor.scopeContext);
@@ -257,77 +308,3 @@ export class GovernedExportService {
257
308
  }
258
309
  }
259
310
  }
260
-
261
- export class GovernedExportServiceError extends Error {
262
- constructor(
263
- readonly code: string,
264
- readonly status: number,
265
- ) {
266
- super(code);
267
- this.name = "GovernedExportServiceError";
268
- }
269
- }
270
-
271
- function validateCreate(input: CreateGovernedExportInput) {
272
- if (!input.idempotencyKey.trim() || input.idempotencyKey.length > 120) {
273
- throw new GovernedExportServiceError("EXPORT_IDEMPOTENCY_KEY_INVALID", 400);
274
- }
275
- if (input.reason.trim().length < 5 || input.reason.length > 200) {
276
- throw new GovernedExportServiceError("EXPORT_REASON_INVALID", 400);
277
- }
278
- }
279
-
280
- function durableStatus(status: string): GovernedExportStatus | undefined {
281
- return ["queued", "running", "succeeded", "failed", "cancelled"].includes(
282
- status,
283
- )
284
- ? (status as GovernedExportStatus)
285
- : undefined;
286
- }
287
-
288
- function assertArtifactMetadata(
289
- artifact: Awaited<ReturnType<DurableArtifactService["inspect"]>>,
290
- request: GovernedExportRequest,
291
- ) {
292
- if (
293
- artifact.jobId !== request.durableJobId ||
294
- artifact.ownerId !== request.requesterId ||
295
- artifact.contentType !== "application/x-ndjson" ||
296
- artifact.byteSize < 0 ||
297
- !/^[a-f0-9]{64}$/.test(artifact.sha256)
298
- ) {
299
- throw new GovernedExportServiceError("EXPORT_ARTIFACT_INVALID", 409);
300
- }
301
- }
302
-
303
- function event(
304
- context: RequestGuardContext,
305
- action: AuditEvent["action"],
306
- id: string,
307
- metadata: Record<string, unknown>,
308
- ): AuditEvent {
309
- return {
310
- id: Bun.randomUUIDv7(),
311
- action,
312
- outcome: "success",
313
- actor: auditActor(context),
314
- target: { type: "system.user.governed-export", id },
315
- requestId: context.requestId,
316
- metadata,
317
- occurredAt: new Date().toISOString(),
318
- };
319
- }
320
-
321
- function downloadEvent(
322
- context: RequestGuardContext,
323
- request: GovernedExportRequest,
324
- sha256: string,
325
- phase: "intent" | "authorized",
326
- ) {
327
- return event(context, "data.governed-export.download", request.id, {
328
- phase,
329
- scopeHash: request.scopeHash,
330
- jobId: request.durableJobId,
331
- sha256,
332
- });
333
- }
@@ -1,6 +1,9 @@
1
1
  import type { GovernedExportQuerySnapshot } from "@southwind-ai/database";
2
+ import type { DataGovernancePolicyDefinition } from "@southwind-ai/shared";
3
+ import type { ModuleGovernedResourceRegistration } from "@southwind-ai/server-sdk";
2
4
  import { restoreServerScopeContext } from "../../data-access/context.js";
3
5
  import type { RequestActor } from "../../guards.js";
6
+ import { toModuleActor } from "../../module-host/request-context.js";
4
7
  import type { ManagedUser } from "../../system/entities.js";
5
8
  import type { ResourceRouteConfig } from "../../system/route-types.js";
6
9
  import type { GovernedExportRequest } from "./types.js";
@@ -9,6 +12,7 @@ export interface GovernedExportSource {
9
12
  readonly resourceType: string;
10
13
  pages(
11
14
  request: GovernedExportRequest,
15
+ signal?: AbortSignal,
12
16
  ): AsyncIterable<readonly Record<string, unknown>[]>;
13
17
  }
14
18
 
@@ -44,6 +48,103 @@ export class GovernedUserExportSource implements GovernedExportSource {
44
48
  }
45
49
  }
46
50
 
51
+ export class ModuleGovernedExportSource implements GovernedExportSource {
52
+ readonly resourceType: string;
53
+
54
+ constructor(
55
+ private readonly registration: ModuleGovernedResourceRegistration,
56
+ policy: DataGovernancePolicyDefinition,
57
+ private readonly resolveActor: (
58
+ actorId: string,
59
+ ) => Promise<RequestActor | undefined>,
60
+ ) {
61
+ this.resourceType = policy.resourceType;
62
+ }
63
+
64
+ async *pages(request: GovernedExportRequest, signal?: AbortSignal) {
65
+ const actor = await this.resolveActor(request.requesterId);
66
+ if (!actor) throw new Error("EXPORT_ACTOR_UNAVAILABLE");
67
+ const source = this.registration.export;
68
+ if (!source) throw new Error("EXPORT_SOURCE_NOT_REGISTERED");
69
+ let cursor: string | undefined;
70
+ let pageCount = 0;
71
+ const seen = new Set<string>();
72
+ do {
73
+ if (signal?.aborted) throw new Error("EXPORT_ABORTED");
74
+ pageCount += 1;
75
+ if (pageCount > 10_000) throw new Error("EXPORT_PAGE_BUDGET_EXCEEDED");
76
+ const page = await source.readPage(
77
+ {
78
+ query: request.querySnapshot,
79
+ cursor,
80
+ limit: 100,
81
+ },
82
+ {
83
+ actor: toModuleActor(actor),
84
+ requestId: request.id,
85
+ signal: signal ?? new AbortController().signal,
86
+ },
87
+ );
88
+ validatePage(page.records, page.nextCursor, cursor, seen);
89
+ yield page.records;
90
+ cursor = page.nextCursor;
91
+ } while (cursor);
92
+ }
93
+ }
94
+
95
+ function validatePage(
96
+ records: readonly Readonly<Record<string, unknown>>[],
97
+ nextCursor: string | undefined,
98
+ currentCursor: string | undefined,
99
+ seen: Set<string>,
100
+ ) {
101
+ if (records.length > 100) throw new Error("EXPORT_PAGE_LIMIT_EXCEEDED");
102
+ for (const record of records) {
103
+ if (
104
+ !record ||
105
+ Array.isArray(record) ||
106
+ typeof record !== "object" ||
107
+ !isJsonValue(record)
108
+ ) {
109
+ throw new Error("EXPORT_RECORD_INVALID");
110
+ }
111
+ }
112
+ if (nextCursor !== undefined) {
113
+ if (
114
+ !nextCursor ||
115
+ nextCursor.length > 500 ||
116
+ nextCursor === currentCursor ||
117
+ seen.has(nextCursor)
118
+ ) {
119
+ throw new Error("EXPORT_CURSOR_INVALID");
120
+ }
121
+ seen.add(nextCursor);
122
+ }
123
+ }
124
+
125
+ function isJsonValue(value: unknown, depth = 0): boolean {
126
+ if (depth > 10) return false;
127
+ if (
128
+ value === null ||
129
+ typeof value === "string" ||
130
+ typeof value === "boolean"
131
+ ) {
132
+ return true;
133
+ }
134
+ if (typeof value === "number") return Number.isFinite(value);
135
+ if (Array.isArray(value)) {
136
+ return value.every((item) => isJsonValue(item, depth + 1));
137
+ }
138
+ if (
139
+ typeof value !== "object" ||
140
+ (Object.getPrototypeOf(value) !== Object.prototype &&
141
+ Object.getPrototypeOf(value) !== null)
142
+ ) {
143
+ return false;
144
+ }
145
+ return Object.values(value).every((item) => isJsonValue(item, depth + 1));
146
+ }
147
+
47
148
  function exportActor(request: GovernedExportRequest): RequestActor {
48
149
  return {
49
150
  id: request.requesterId,
@@ -26,6 +26,7 @@ export interface GovernedExportRequest {
26
26
  scopeSnapshot: GovernedExportScopeSnapshot;
27
27
  scopeHash: string;
28
28
  querySnapshot: GovernedExportQuerySnapshot;
29
+ renderer: GovernedExportRendererSnapshot;
29
30
  status: GovernedExportStatus;
30
31
  approvalRequired: boolean;
31
32
  approvalId?: string;
@@ -49,7 +50,33 @@ export interface CreateGovernedExportRequest {
49
50
  scopeSnapshot: GovernedExportScopeSnapshot;
50
51
  scopeHash: string;
51
52
  querySnapshot: GovernedExportQuerySnapshot;
53
+ renderer: GovernedExportRendererSnapshot;
52
54
  approvalRequired: boolean;
53
55
  approvalId?: string;
54
56
  expiresAt: string;
55
57
  }
58
+
59
+ export interface GovernedExportRendererSnapshot {
60
+ formatKey: string;
61
+ templateKey: string;
62
+ templateVersion: string;
63
+ inputSchemaKey: string;
64
+ contentType: string;
65
+ filenameExtension: string;
66
+ maxRecords: number;
67
+ maxOutputBytes: number;
68
+ timeoutSeconds: number;
69
+ }
70
+
71
+ export const legacyGovernedExportRendererSnapshot: GovernedExportRendererSnapshot =
72
+ {
73
+ formatKey: "ndjson",
74
+ templateKey: "system.governed-export.ndjson",
75
+ templateVersion: "1",
76
+ inputSchemaKey: "system.governed-export.record.v1",
77
+ contentType: "application/x-ndjson",
78
+ filenameExtension: "ndjson",
79
+ maxRecords: 50_000,
80
+ maxOutputBytes: 10 * 1024 * 1024,
81
+ timeoutSeconds: 900,
82
+ };