queryapigate 0.43.0__tar.gz → 0.45.0__tar.gz

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 (245) hide show
  1. {queryapigate-0.43.0/queryapigate.egg-info → queryapigate-0.45.0}/PKG-INFO +1 -1
  2. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/__init__.py +1 -1
  3. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/alerts.py +10 -4
  4. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/app.py +107 -1
  5. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/cli.py +43 -0
  6. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/config.py +62 -3
  7. queryapigate-0.43.0/queryapigate/console_dist/assets/AccessMapPage-C0Ao2DXb.js → queryapigate-0.45.0/queryapigate/console_dist/assets/AccessMapPage-CNBzMwLN.js +2 -2
  8. queryapigate-0.43.0/queryapigate/console_dist/assets/AdministratorsPage-CF6Go3C2.js → queryapigate-0.45.0/queryapigate/console_dist/assets/AdministratorsPage-CLfRjojL.js +1 -1
  9. queryapigate-0.43.0/queryapigate/console_dist/assets/AgentsPage-aI5-GZDw.js → queryapigate-0.45.0/queryapigate/console_dist/assets/AgentsPage-DlmXHHcH.js +1 -1
  10. queryapigate-0.43.0/queryapigate/console_dist/assets/AlertsPage-BpDKzDi6.js → queryapigate-0.45.0/queryapigate/console_dist/assets/AlertsPage-Dt4Uwz6s.js +1 -1
  11. queryapigate-0.43.0/queryapigate/console_dist/assets/ApiKeyDetailPage-D2_U3VuH.js → queryapigate-0.45.0/queryapigate/console_dist/assets/ApiKeyDetailPage-D7Ycd4O4.js +1 -1
  12. queryapigate-0.43.0/queryapigate/console_dist/assets/ApiKeysPage-DpCXI_0g.js → queryapigate-0.45.0/queryapigate/console_dist/assets/ApiKeysPage-BGgDoVCd.js +1 -1
  13. queryapigate-0.43.0/queryapigate/console_dist/assets/ApprovalsPage-BGWpuKi6.js → queryapigate-0.45.0/queryapigate/console_dist/assets/ApprovalsPage-CKF0I-Kz.js +1 -1
  14. queryapigate-0.43.0/queryapigate/console_dist/assets/AuditLogPage-BFEnPYVL.js → queryapigate-0.45.0/queryapigate/console_dist/assets/AuditLogPage-C5YiXeK-.js +1 -1
  15. queryapigate-0.43.0/queryapigate/console_dist/assets/CachingPage-Dt3rcH86.js → queryapigate-0.45.0/queryapigate/console_dist/assets/CachingPage-Da1rD-zo.js +1 -1
  16. queryapigate-0.43.0/queryapigate/console_dist/assets/ConnectionsPage-BELxZBmR.js → queryapigate-0.45.0/queryapigate/console_dist/assets/ConnectionsPage-BajPP7ix.js +1 -1
  17. queryapigate-0.43.0/queryapigate/console_dist/assets/DesignerPage-mB7FGL4z.js → queryapigate-0.45.0/queryapigate/console_dist/assets/DesignerPage-CIzg9Apa.js +1 -1
  18. queryapigate-0.43.0/queryapigate/console_dist/assets/DestinationsPage-Dm96V2vC.js → queryapigate-0.45.0/queryapigate/console_dist/assets/DestinationsPage-BMGKQgUe.js +1 -1
  19. queryapigate-0.43.0/queryapigate/console_dist/assets/ExportsPage-DNlYFWce.js → queryapigate-0.45.0/queryapigate/console_dist/assets/ExportsPage-C9w4up5N.js +1 -1
  20. queryapigate-0.43.0/queryapigate/console_dist/assets/HelpPage-D2fka50S.js → queryapigate-0.45.0/queryapigate/console_dist/assets/HelpPage-NSJp_qwt.js +2 -2
  21. queryapigate-0.43.0/queryapigate/console_dist/assets/HomePage-CUgo9g94.js → queryapigate-0.45.0/queryapigate/console_dist/assets/HomePage-Cywjwlpb.js +2 -2
  22. queryapigate-0.43.0/queryapigate/console_dist/assets/KeyActivity-CKbbdkvM.js → queryapigate-0.45.0/queryapigate/console_dist/assets/KeyActivity-D673KcWq.js +1 -1
  23. queryapigate-0.43.0/queryapigate/console_dist/assets/Managed-CbQD1mHQ.js → queryapigate-0.45.0/queryapigate/console_dist/assets/Managed-VPfeN8Yu.js +1 -1
  24. queryapigate-0.43.0/queryapigate/console_dist/assets/McpPage-CGJFAHiN.js → queryapigate-0.45.0/queryapigate/console_dist/assets/McpPage-aH1mkjzt.js +1 -1
  25. queryapigate-0.43.0/queryapigate/console_dist/assets/MetricsPage-DgJlA8TP.js → queryapigate-0.45.0/queryapigate/console_dist/assets/MetricsPage-BqIkrlP1.js +1 -1
  26. queryapigate-0.43.0/queryapigate/console_dist/assets/QueryInfo-B9xmoHuw.js → queryapigate-0.45.0/queryapigate/console_dist/assets/QueryInfo-Bv-0Zmc5.js +1 -1
  27. queryapigate-0.43.0/queryapigate/console_dist/assets/RepositoryPage-Dbdn3QrH.js → queryapigate-0.45.0/queryapigate/console_dist/assets/RepositoryPage-lzLrJRXS.js +1 -1
  28. queryapigate-0.43.0/queryapigate/console_dist/assets/Results-fzKK4V5W.js → queryapigate-0.45.0/queryapigate/console_dist/assets/Results-XnOh21dp.js +1 -1
  29. queryapigate-0.43.0/queryapigate/console_dist/assets/RolesPage-BELto4hG.js → queryapigate-0.45.0/queryapigate/console_dist/assets/RolesPage-Cv5S5X7g.js +1 -1
  30. queryapigate-0.43.0/queryapigate/console_dist/assets/SchemaBrowser-1u2p1iY-.js → queryapigate-0.45.0/queryapigate/console_dist/assets/SchemaBrowser-8tmsr1__.js +1 -1
  31. queryapigate-0.43.0/queryapigate/console_dist/assets/SettingsPage-DJToGsgg.js → queryapigate-0.45.0/queryapigate/console_dist/assets/SettingsPage-pOVvmb4i.js +1 -1
  32. queryapigate-0.43.0/queryapigate/console_dist/assets/Time-USlo65Ii.js → queryapigate-0.45.0/queryapigate/console_dist/assets/Time-s5E5qK3s.js +1 -1
  33. queryapigate-0.43.0/queryapigate/console_dist/assets/WorkspacesPage-D_1ftIEs.js → queryapigate-0.45.0/queryapigate/console_dist/assets/WorkspacesPage-B1lg3P5d.js +1 -1
  34. queryapigate-0.43.0/queryapigate/console_dist/assets/api-DhjsrkN_.js → queryapigate-0.45.0/queryapigate/console_dist/assets/api-DIm7t7Bb.js +1 -1
  35. queryapigate-0.43.0/queryapigate/console_dist/assets/forms-BRJEHg6U.js → queryapigate-0.45.0/queryapigate/console_dist/assets/forms-B7OrYKBb.js +1 -1
  36. queryapigate-0.43.0/queryapigate/console_dist/assets/forms-Bn5QJbOg.js → queryapigate-0.45.0/queryapigate/console_dist/assets/forms-CTAVu1q5.js +1 -1
  37. queryapigate-0.43.0/queryapigate/console_dist/assets/index-uRArh6IJ.js → queryapigate-0.45.0/queryapigate/console_dist/assets/index-Cl93xBjn.js +2 -2
  38. queryapigate-0.43.0/queryapigate/console_dist/assets/shared-i1-TsKj7.js → queryapigate-0.45.0/queryapigate/console_dist/assets/shared-epEBEbJD.js +1 -1
  39. queryapigate-0.43.0/queryapigate/console_dist/assets/useMutation-Cheq0sq0.js → queryapigate-0.45.0/queryapigate/console_dist/assets/useMutation-BIoTFTZ8.js +1 -1
  40. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/console_dist/index.html +1 -1
  41. queryapigate-0.45.0/queryapigate/dataplane.py +157 -0
  42. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/db.py +54 -5
  43. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/engine.py +3 -0
  44. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/events.py +83 -9
  45. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/experimental.py +14 -0
  46. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/history.py +96 -2
  47. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/masking.py +12 -0
  48. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/metacache.py +43 -0
  49. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/metrics.py +41 -8
  50. queryapigate-0.45.0/queryapigate/runstore.py +1028 -0
  51. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/sqltools.py +29 -19
  52. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/store.py +41 -25
  53. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/usage.py +40 -6
  54. {queryapigate-0.43.0 → queryapigate-0.45.0/queryapigate.egg-info}/PKG-INFO +1 -1
  55. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate.egg-info/SOURCES.txt +38 -33
  56. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_adhoc_history.py +2 -1
  57. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_backup_restore.py +2 -1
  58. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_collections.py +3 -1
  59. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_data_protection.py +3 -1
  60. queryapigate-0.45.0/tests/test_dataplane.py +418 -0
  61. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_events_server.py +6 -3
  62. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_history.py +56 -2
  63. queryapigate-0.45.0/tests/test_runstore.py +464 -0
  64. queryapigate-0.45.0/tests/test_runstore_differential.py +139 -0
  65. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_settings.py +4 -3
  66. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_upgrades.py +3 -1
  67. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_usage.py +3 -1
  68. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_workspace_observability.py +4 -1
  69. {queryapigate-0.43.0 → queryapigate-0.45.0}/LICENSE +0 -0
  70. {queryapigate-0.43.0 → queryapigate-0.45.0}/NOTICE +0 -0
  71. {queryapigate-0.43.0 → queryapigate-0.45.0}/README.md +0 -0
  72. {queryapigate-0.43.0 → queryapigate-0.45.0}/pyproject.toml +0 -0
  73. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/__main__.py +0 -0
  74. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/accesslog.py +0 -0
  75. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/adminroles.py +0 -0
  76. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/admins.py +0 -0
  77. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/agentwrites.py +0 -0
  78. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/apikeys.py +0 -0
  79. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/auditchain.py +0 -0
  80. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/auditstream.py +0 -0
  81. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/aws.py +0 -0
  82. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/broadcast.py +0 -0
  83. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/bundle.py +0 -0
  84. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/cache.py +0 -0
  85. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/collection_admin.py +0 -0
  86. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/configcode.py +0 -0
  87. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/console.py +0 -0
  88. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/console_dist/assets/CodeBox-Cwh5KHLp.js +0 -0
  89. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/console_dist/assets/Experimental-B3dnaeV7.js +0 -0
  90. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/console_dist/assets/StatTile-Cft4kJPH.js +0 -0
  91. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/console_dist/assets/client-ZHNiMG8H.js +0 -0
  92. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/console_dist/assets/icons-CFXE8hAz.js +0 -0
  93. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/console_dist/assets/index-Cv64XKNs.css +0 -0
  94. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/console_dist/assets/maskRules-CvKQIPgN.js +0 -0
  95. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/console_dist/assets/reach-BAOLd75Y.js +0 -0
  96. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/console_dist/assets/state-O13cl7cO.js +0 -0
  97. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/console_dist/favicon.svg +0 -0
  98. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/cors.py +0 -0
  99. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/databases.py +0 -0
  100. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/definitions.py +0 -0
  101. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/deprecations.py +0 -0
  102. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/descriptions.py +0 -0
  103. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/destinations.py +0 -0
  104. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/duckfiles.py +0 -0
  105. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/embeddings.py +0 -0
  106. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/errors.py +0 -0
  107. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/examples.py +0 -0
  108. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/exporting.py +0 -0
  109. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/exports.py +0 -0
  110. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/formats.py +0 -0
  111. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/governance.py +0 -0
  112. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/gunicorn_conf.py +0 -0
  113. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/instances.py +0 -0
  114. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/jwtauth.py +0 -0
  115. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/lib/h2-2.2.224.jar +0 -0
  116. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/limits.py +0 -0
  117. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/logging_setup.py +0 -0
  118. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/mcp_server.py +0 -0
  119. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/mongodocs.py +0 -0
  120. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/mongotools.py +0 -0
  121. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/oidc.py +0 -0
  122. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/openapi.py +0 -0
  123. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/params.py +0 -0
  124. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/parquet.py +0 -0
  125. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/pool.py +0 -0
  126. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/postman.py +0 -0
  127. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/quotas.py +0 -0
  128. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/ratelimit.py +0 -0
  129. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/rediscache.py +0 -0
  130. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/rekey.py +0 -0
  131. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/runners.py +0 -0
  132. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/schema.py +0 -0
  133. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/scope.py +0 -0
  134. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/secheaders.py +0 -0
  135. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/secretrefs.py +0 -0
  136. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/services/__init__.py +0 -0
  137. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/services/access.py +0 -0
  138. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/services/administrators.py +0 -0
  139. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/services/audit.py +0 -0
  140. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/services/collections.py +0 -0
  141. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/services/connections.py +0 -0
  142. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/services/descriptions.py +0 -0
  143. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/services/destinations.py +0 -0
  144. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/services/exports.py +0 -0
  145. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/services/mcp.py +0 -0
  146. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/services/queries.py +0 -0
  147. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/services/workspaces.py +0 -0
  148. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/sqlflow.py +0 -0
  149. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/sso.py +0 -0
  150. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/tableguard.py +0 -0
  151. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/tracing.py +0 -0
  152. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/v1.py +0 -0
  153. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/v1_spec.py +0 -0
  154. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/vault.py +0 -0
  155. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate/workspaces.py +0 -0
  156. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate.egg-info/dependency_links.txt +0 -0
  157. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate.egg-info/entry_points.txt +0 -0
  158. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate.egg-info/requires.txt +0 -0
  159. {queryapigate-0.43.0 → queryapigate-0.45.0}/queryapigate.egg-info/top_level.txt +0 -0
  160. {queryapigate-0.43.0 → queryapigate-0.45.0}/setup.cfg +0 -0
  161. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_adminroles.py +0 -0
  162. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_admins.py +0 -0
  163. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_agent_writes.py +0 -0
  164. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_alerts.py +0 -0
  165. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_api_administrators.py +0 -0
  166. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_api_keys.py +0 -0
  167. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_api_v1.py +0 -0
  168. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_audit_chain.py +0 -0
  169. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_audit_log.py +0 -0
  170. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_audit_siem.py +0 -0
  171. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_audit_stream.py +0 -0
  172. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_aws.py +0 -0
  173. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_broadcast.py +0 -0
  174. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_cli.py +0 -0
  175. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_collection_bundle.py +0 -0
  176. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_collection_postman.py +0 -0
  177. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_configcode.py +0 -0
  178. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_connection_encryption.py +0 -0
  179. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_console.py +0 -0
  180. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_cors_and_rate_limit.py +0 -0
  181. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_databases.py +0 -0
  182. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_db.py +0 -0
  183. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_dependency_floors.py +0 -0
  184. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_deprecations.py +0 -0
  185. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_descriptions.py +0 -0
  186. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_destinations.py +0 -0
  187. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_docs_site.py +0 -0
  188. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_duckdb.py +0 -0
  189. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_duckdb_files.py +0 -0
  190. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_embeddings.py +0 -0
  191. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_errors.py +0 -0
  192. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_events.py +0 -0
  193. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_examples.py +0 -0
  194. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_experimental.py +0 -0
  195. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_export_security.py +0 -0
  196. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_exporting.py +0 -0
  197. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_exports.py +0 -0
  198. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_fips.py +0 -0
  199. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_helm.py +0 -0
  200. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_howto.py +0 -0
  201. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_instances.py +0 -0
  202. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_integration.py +0 -0
  203. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_jdbc.py +0 -0
  204. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_jwt.py +0 -0
  205. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_key_rotation.py +0 -0
  206. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_limits.py +0 -0
  207. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_masking.py +0 -0
  208. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_masking_requests.py +0 -0
  209. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_mcp.py +0 -0
  210. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_mcp_keycloak.py +0 -0
  211. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_mcp_oauth.py +0 -0
  212. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_metacache.py +0 -0
  213. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_mongo_streaming.py +0 -0
  214. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_mongotools.py +0 -0
  215. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_multinode.py +0 -0
  216. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_observability.py +0 -0
  217. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_oidc.py +0 -0
  218. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_params.py +0 -0
  219. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_parquet.py +0 -0
  220. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_pool.py +0 -0
  221. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_publishing.py +0 -0
  222. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_query_window.py +0 -0
  223. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_queryapigate.py +0 -0
  224. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_quotas.py +0 -0
  225. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_rekey.py +0 -0
  226. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_release_check.py +0 -0
  227. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_response_cache.py +0 -0
  228. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_roles.py +0 -0
  229. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_rolling_upgrade.py +0 -0
  230. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_secretrefs.py +0 -0
  231. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_security_headers.py +0 -0
  232. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_shared_rate_limit.py +0 -0
  233. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_sql_guard_fuzz.py +0 -0
  234. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_sql_guard_writes.py +0 -0
  235. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_sqlflow.py +0 -0
  236. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_sso.py +0 -0
  237. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_sso_keycloak.py +0 -0
  238. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_supply_chain.py +0 -0
  239. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_tableguard.py +0 -0
  240. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_tracing.py +0 -0
  241. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_vault.py +0 -0
  242. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_workspace_config_cli.py +0 -0
  243. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_workspace_isolation.py +0 -0
  244. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_workspace_store.py +0 -0
  245. {queryapigate-0.43.0 → queryapigate-0.45.0}/tests/test_workspaces_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: queryapigate
3
- Version: 0.43.0
3
+ Version: 0.45.0
4
4
  Summary: Turn SQL into governed REST APIs over MySQL, PostgreSQL, ClickHouse, SQLite, DuckDB, H2, MongoDB, and Parquet/CSV/JSON files - JSON, CSV, Parquet or Excel out, with scoped keys, an admin Console and MCP for AI agents.
5
5
  Author-email: Anantha Raju C <arcswdev@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -1,5 +1,5 @@
1
1
  """QueryAPIGate - expose SQL databases as a REST API."""
2
- __version__ = "0.43.0"
2
+ __version__ = "0.45.0"
3
3
 
4
4
  from .app import create_app # noqa: E402 (app imports __version__)
5
5
 
@@ -21,7 +21,7 @@ from collections import deque
21
21
  from datetime import datetime, timedelta
22
22
  from statistics import median
23
23
 
24
- from . import admins, apikeys, config, db, history, instances, metrics, oidc, ratelimit, scope, usage
24
+ from . import admins, apikeys, config, db, history, instances, metrics, oidc, ratelimit, runstore, scope, usage
25
25
 
26
26
  RECENT_RUNS = 5000 # newest runs the history checks look at
27
27
  QUERY_WINDOW = timedelta(days=7) # a query's error rate and speed are judged on its runs this recent ...
@@ -278,10 +278,16 @@ def _keys(now):
278
278
  def _recent_runs(now):
279
279
  """The newest runs, as (query_name, executed_at, entry) - written ones, including this process's queued."""
280
280
  history.flush()
281
- rows = db.connection().execute(
282
- 'SELECT query_name, executed_at, entry_json FROM execution_history WHERE workspace = ? '
283
- 'ORDER BY rowid DESC LIMIT ?', (scope.current(), RECENT_RUNS)).fetchall()
284
281
  oldest = (now - QUERY_WINDOW).strftime(history.TIME_FORMAT)
282
+ if runstore.enabled():
283
+ runstore.ensure_schema()
284
+ rows = [{'query_name': row['query_name'], 'executed_at': row['executed_at'], 'entry_json': row['entry']}
285
+ for row in runstore.newest_first('workspace = {ws:String}', {'ws': scope.current()}, RECENT_RUNS,
286
+ floor=oldest, columns='query_name, executed_at, entry')]
287
+ else:
288
+ rows = db.connection().execute(
289
+ 'SELECT query_name, executed_at, entry_json FROM execution_history WHERE workspace = ? '
290
+ 'ORDER BY rowid DESC LIMIT ?', (scope.current(), RECENT_RUNS)).fetchall()
285
291
  runs = []
286
292
  for row in rows:
287
293
  if row['executed_at'] < oldest:
@@ -24,6 +24,7 @@ from . import (
24
24
  config,
25
25
  console,
26
26
  cors,
27
+ dataplane,
27
28
  db,
28
29
  definitions,
29
30
  deprecations,
@@ -40,12 +41,14 @@ from . import (
40
41
  limits,
41
42
  logging_setup,
42
43
  masking,
44
+ metacache,
43
45
  metrics,
44
46
  mongodocs,
45
47
  mongotools,
46
48
  openapi,
47
49
  quotas,
48
50
  ratelimit,
51
+ runstore,
49
52
  schema,
50
53
  scope,
51
54
  secheaders,
@@ -65,8 +68,11 @@ bp = Blueprint('api', __name__)
65
68
  RESERVED_ARGS = {'format', 'page', 'page_size', 'connection_name', 'version', 'timeout', 'stream'}
66
69
  PUBLIC_ENDPOINTS = {'api.index', 'api.favicon', 'api.health', 'api.docs', 'api.openapi_spec', 'api.admin_ui',
67
70
  'api.console_page', 'api.metrics_endpoint',
71
+ # the data plane's reports and run store lease: gate() lets only its secret through (ADR 0028)
72
+ 'api.dataplane_runs', 'api.dataplane_lease',
68
73
  # Console sign-in (sso.py): each checks what it needs itself - state, the session and its CSRF token
69
74
  'auth.login', 'auth.callback', 'auth.session', 'auth.logout'}
75
+ DATAPLANE_ENDPOINTS = {'api.dataplane_runs', 'api.dataplane_lease'}
70
76
  # Monitoring keeps working while a client is throttled; the Console's static files are exempt so loading the page
71
77
  # (several hashed assets) never spends a client's quota - they carry no data and no key.
72
78
  RATE_LIMIT_EXEMPT = {'api.health', 'api.metrics_endpoint', 'api.console_page'}
@@ -167,12 +173,24 @@ def warn_env_references():
167
173
 
168
174
  class _CapturePeer:
169
175
  """Record the real socket address (`REMOTE_ADDR`) before ProxyFix can replace it with an X-Forwarded-For value,
170
- so the open-access loopback check (gate()) judges the actual connection and never a header a client can forge."""
176
+ so the open-access loopback check (gate()) judges the actual connection and never a header a client can forge.
177
+
178
+ Behind a data plane (dataplane.py, ADR 0028) every request comes from it, over loopback: one that carries its
179
+ secret names the client's own socket address in X-QAG-Peer, and that address is taken as the real one - so this
180
+ server sees the request as if the client had connected directly, ProxyFix and QUERYAPIGATE_TRUST_PROXY included.
181
+ A peer that isn't an IP address counts as remote, as a Unix socket does. Both headers are removed here, so
182
+ nothing after this layer reads, logs or passes on the secret."""
171
183
 
172
184
  def __init__(self, app):
173
185
  self.app = app
174
186
 
175
187
  def __call__(self, environ, start_response):
188
+ supplied = environ.pop(dataplane.SECRET_ENVIRON, None)
189
+ peer = environ.pop(dataplane.PEER_ENVIRON, None)
190
+ wanted = environ.pop(dataplane.WANTED_ENVIRON, None)
191
+ if supplied is not None and dataplane.matches(supplied):
192
+ environ['queryapigate.dataplane'] = 'plan' if wanted == '1' else True
193
+ environ['REMOTE_ADDR'] = peer if dataplane.is_address(peer) else ''
176
194
  environ['queryapigate.peer_addr'] = environ.get('REMOTE_ADDR')
177
195
  return self.app(environ, start_response)
178
196
 
@@ -192,6 +210,7 @@ def create_app():
192
210
  config.check_settings()
193
211
  logging_setup.configure(log) # before anything below logs - the legacy import's deprecation warning, for one
194
212
  db.init_schema()
213
+ runstore.check_marker()
195
214
  store.refuse_legacy_home()
196
215
  experimental.warn_in_use(log, os.environ, store.read_connections(), store.workspace_names())
197
216
  warn_env_references()
@@ -301,12 +320,21 @@ def create_app():
301
320
  @app.teardown_request
302
321
  def end_trace(_error):
303
322
  tracing.finish(g.pop('trace', None))
323
+ token = g.pop('dataplane_token', None)
324
+ if token is not None:
325
+ metacache.reset(token)
304
326
 
305
327
  @app.before_request
306
328
  def gate():
307
329
  # Set first so every request - including one rejected below - gets an ID and a measured duration.
308
330
  g.request_id = new_request_id(request.headers.get('X-Request-Id'))
309
331
  g.request_started = time.monotonic()
332
+ if request.endpoint in DATAPLANE_ENDPOINTS: # the data plane's own calls: nothing else applies to them
333
+ if request.environ.get('queryapigate.dataplane'):
334
+ return None
335
+ return jsonify(error_body('Not Found', 404)), 404
336
+ if request.environ.get('queryapigate.dataplane') == 'plan': # before the key is read: a plan names its row
337
+ g.dataplane_record, g.dataplane_token = metacache.capture()
310
338
  # The request's span (tracing.py, ADR 0010): named by the route rule, never the path's values or query string
311
339
  rule = request.url_rule.rule if request.url_rule is not None else None
312
340
  g.trace = tracing.start(f'{request.method} {rule}' if rule else request.method, 'server', {
@@ -346,6 +374,8 @@ def create_app():
346
374
 
347
375
  @app.after_request
348
376
  def decorate(response):
377
+ if request.endpoint in DATAPLANE_ENDPOINTS: # counted and logged as the runs it reports, not itself
378
+ return response
349
379
  if not request.path.startswith('/auth/'): # Console sign-in is same-origin only (sso.py)
350
380
  cors.add_headers(response, request.headers.get('Origin'))
351
381
  if g.get('rate_limit'):
@@ -1075,6 +1105,12 @@ def _run_saved(ref, body, url_params):
1075
1105
  **masked_entry(result.masked)})
1076
1106
  if cache_key is not None:
1077
1107
  response = cache_store(cache_key, response, ttl, cache_meta)
1108
+ elif (g.get('dataplane_record') is not None and g.get('transport', 'rest') == 'rest' and not ttl
1109
+ and output_format == 'json'): # the one format the data plane renders
1110
+ plan = dataplane.build(g.dataplane_record, g.permission, query_name, number, saved, connection_name, result,
1111
+ config.allow_writes() and effective_allow_writes)
1112
+ if plan is not None:
1113
+ response.headers[dataplane.PLAN_HEADER] = dataplane.encode(plan)
1078
1114
  return response
1079
1115
 
1080
1116
 
@@ -1192,6 +1228,76 @@ def run_named_query(name):
1192
1228
  return run_saved(name, body, url_params)
1193
1229
 
1194
1230
 
1231
+ @bp.route('/internal/dataplane/runs', methods=['POST'])
1232
+ def dataplane_runs():
1233
+ """The runs a data plane in front of this server served itself (dataplane.py, ADR 0028), batched: each recorded as
1234
+ run_saved() records one - run history, the event log, the live feed - and counted in /metrics as it would have been.
1235
+ Only the data plane reaches it: gate() answers 404 to anything without its secret. `dropped`: runs it served but
1236
+ couldn't queue for here, counted as history's own dropped runs are. `recorded`: with a run store, the data plane has
1237
+ written them there itself (ADR 0029), and only `kept` and `sampled_out` of them are counted here.
1238
+
1239
+ Each run comes with its history entry already JSON (`entry`), and the labels and timings /metrics counts it by: at
1240
+ the data plane's rates Python's own work per run - encoding it, and its labels - is what it can't keep up with, so
1241
+ a batch is queued at once (history.record_reported()) and counted once per set of labels."""
1242
+ report = request.get_json(silent=True) or {}
1243
+ runs = report.get('runs') or []
1244
+ broadcaster = current_app.extensions['queryapigate_broadcaster']
1245
+ with scope.using(scope.current()): # resolved once, not by each of a busy batch's few thousand label lookups
1246
+ if report.get('recorded'): # in the run store already: written by the data plane itself (ADR 0029 §4)
1247
+ metrics.inc_history('recorded', int(report.get('kept') or 0))
1248
+ metrics.inc_history('sampled_out', int(report.get('sampled_out') or 0))
1249
+ else:
1250
+ history.record_reported(runs)
1251
+ if broadcaster.subscriber_count(): # the live feed's events, only when anyone is listening
1252
+ for run in runs:
1253
+ broadcaster.publish(events.run_payload(run['query'], run['version'], json.loads(run['entry'])),
1254
+ run['key'], scope.current())
1255
+ labelled = {}
1256
+ for run in runs:
1257
+ seen = labelled.setdefault((run['connection'], run['dialect'], run['status'], run['key'], run['method'],
1258
+ str(run['http_status'])), ([], [], [], [0]))
1259
+ seen[0].append(run['query_seconds'])
1260
+ seen[1].append(run['seconds'])
1261
+ if run['status'] == 'success':
1262
+ seen[2].append(run['serialization_seconds'])
1263
+ seen[3][0] += run['rows']
1264
+ for (connection, dialect, status, key, method, http_status), seen in labelled.items():
1265
+ metrics.observe_queries(connection, dialect, status, seen[0], key)
1266
+ if status == 'success':
1267
+ metrics.observe_rows(connection, dialect, key, seen[3][0])
1268
+ metrics.observe_serializations('json', seen[2])
1269
+ metrics.observe_requests(method, 'api.run_named_query', http_status, seen[1], key or '-')
1270
+ if report.get('dropped'):
1271
+ metrics.inc_history('dropped', int(report['dropped']))
1272
+ return '', 204
1273
+
1274
+
1275
+ @bp.route('/internal/dataplane/lease', methods=['POST'])
1276
+ def dataplane_lease():
1277
+ """A data plane's block of the event ids it mints when it writes its runs to the run store itself (runstore.py,
1278
+ ADR 0029 §2), leased here so its store access stays read-only: `{"holder": ...}` leases one, `{"holder": ...,
1279
+ "block": n}` renews it (409 once it has lapsed: lease again). With the layout of an id and the settle window, so
1280
+ the two processes can't disagree on them. 404 without a run store."""
1281
+ if not runstore.enabled():
1282
+ return jsonify(error_body('No run store is configured', 404)), 404
1283
+ body = request.get_json(silent=True) or {}
1284
+ holder = body.get('holder')
1285
+ if not isinstance(holder, str) or not 0 < len(holder) <= 200:
1286
+ return jsonify(error_body('holder: a name for the data plane process', 400)), 400
1287
+ if body.get('block') is not None:
1288
+ if not isinstance(body['block'], int) or not runstore.renew(body['block'], holder):
1289
+ return jsonify(error_body('That lease has lapsed - lease a block again', 409)), 409
1290
+ return jsonify({'block': body['block'], 'lease_seconds': runstore.LEASE_SECONDS}), 200
1291
+ try:
1292
+ block = runstore.lease(runstore.DATAPLANE_BLOCK, holder)
1293
+ except runstore.LeaseError as error:
1294
+ return jsonify(error_body(str(error), 503)), 503
1295
+ return jsonify({'block': block, 'size': runstore.DATAPLANE_BLOCK, 'lease_seconds': runstore.LEASE_SECONDS,
1296
+ 'epoch_ms': runstore.EPOCH_MS, 'per_ms': runstore.PER_MS,
1297
+ 'settle_seconds': runstore.SETTLE_SECONDS, 'database': runstore.database(),
1298
+ 'sample_rate': config.history_sample_rate()}), 200
1299
+
1300
+
1195
1301
  # --------------------------------------------------------------------------------------
1196
1302
  # Live events
1197
1303
  # --------------------------------------------------------------------------------------
@@ -25,6 +25,7 @@ from . import (
25
25
  instances,
26
26
  logging_setup,
27
27
  postman,
28
+ runstore,
28
29
  scope,
29
30
  store,
30
31
  workspaces,
@@ -113,6 +114,7 @@ def _events(args):
113
114
  experimental.warn_in_use(logging.getLogger('queryapigate'), os.environ, store.read_connections(),
114
115
  store.workspace_names())
115
116
  db.init_schema()
117
+ runstore.check_marker()
116
118
  instances.register('events')
117
119
  args.port = args.port or config.events_port()
118
120
  from .events import run
@@ -718,6 +720,37 @@ def _history_forget(args):
718
720
  return 0
719
721
 
720
722
 
723
+ def _runs_copy(args):
724
+ """`queryapigate runs copy-to-clickhouse`: this store's run history into the run store (ADR 0029 §6)."""
725
+ try:
726
+ config.check_settings()
727
+ if not runstore.enabled():
728
+ raise ValueError('QUERYAPIGATE_RUN_STORE_URL is not set - there is no run store to copy to')
729
+ copied = runstore.copy_history()
730
+ except (ValueError, runstore.RunStoreError) as error:
731
+ print(f'queryapigate runs copy-to-clickhouse: {error}', file=sys.stderr)
732
+ return 1
733
+ print(f'Copied {copied} run(s) of history to the run store at {runstore.location()}.')
734
+ return 0
735
+
736
+
737
+ def _runs_leave(args):
738
+ """`queryapigate runs leave-clickhouse`: keep runs in the metadata store again (ADR 0029 §6)."""
739
+ try:
740
+ config.check_settings()
741
+ if not runstore.enabled():
742
+ raise ValueError('set QUERYAPIGATE_RUN_STORE_URL to the run store being left: its newest event id is read '
743
+ 'from it')
744
+ newest = runstore.leave()
745
+ except (ValueError, runstore.RunStoreError) as error:
746
+ print(f'queryapigate runs leave-clickhouse: {error}', file=sys.stderr)
747
+ return 1
748
+ print(f'Runs and events are kept in the metadata store from the next start; its event ids continue above {newest}. '
749
+ 'Unset QUERYAPIGATE_RUN_STORE_URL on every instance before starting them: the run store\'s history stays '
750
+ 'there, not copied back.')
751
+ return 0
752
+
753
+
721
754
  def _audit_verify(args):
722
755
  """`queryapigate audit verify [--file PATH]`: walk the audit log's hash chain (ADR 0012) - the store's, or an export
723
756
  file's - and say whether it's intact, or where it first breaks. Exit 0 intact, 1 broken, 2 can't be checked
@@ -959,6 +992,15 @@ def build_parser():
959
992
  history_forget.add_argument('--dry-run', action='store_true', help='say how much would go; delete nothing')
960
993
  history_forget.set_defaults(func=_history_forget)
961
994
 
995
+ runs_parser = commands.add_parser('runs', help='where run history and the event log are kept: a ClickHouse run '
996
+ 'store (ADR 0029, experimental)')
997
+ runs_parser.set_defaults(func=lambda a: runs_parser.print_help() or 2)
998
+ runs_actions = runs_parser.add_subparsers(dest='action')
999
+ runs_actions.add_parser('copy-to-clickhouse', help="copy this store's run history into the run store, once"
1000
+ ).set_defaults(func=_runs_copy)
1001
+ runs_actions.add_parser('leave-clickhouse', help='keep runs in the metadata store again, its event ids continuing '
1002
+ 'above the run store\'s').set_defaults(func=_runs_leave)
1003
+
962
1004
  rekey_parser = commands.add_parser('rekey', help='re-encrypt every stored password under the first key in '
963
1005
  'QUERYAPIGATE_SECRET_KEY, so the others can be removed (rotating '
964
1006
  'the key)')
@@ -1002,6 +1044,7 @@ def main(argv=None):
1002
1044
  try:
1003
1045
  config.check_database_url()
1004
1046
  db.init_schema()
1047
+ runstore.check_marker()
1005
1048
  store.refuse_legacy_home()
1006
1049
  except ValueError as error:
1007
1050
  print(f'queryapigate: {error}', file=sys.stderr)
@@ -170,9 +170,12 @@ def history_adhoc_sql():
170
170
  def history_retention_days():
171
171
  """QUERYAPIGATE_HISTORY_RETENTION_DAYS: keep every run for this many days instead of only each version's newest
172
172
  history_limit() runs - for watching API behaviour over time, best on a PostgreSQL store. None (the
173
- default) keeps the per-version cap. Validated at startup by check_settings()."""
173
+ default) keeps the per-version cap - except with a run store (runstore.py), where retention is always by time:
174
+ RUN_STORE_HISTORY_DAYS unless this is set. Validated at startup by check_settings()."""
174
175
  raw = os.environ.get('QUERYAPIGATE_HISTORY_RETENTION_DAYS', '').strip()
175
- return int(raw) if raw else None
176
+ if raw:
177
+ return int(raw)
178
+ return RUN_STORE_HISTORY_DAYS if run_store_url() else None
176
179
 
177
180
 
178
181
  def history_sample_rate():
@@ -387,6 +390,9 @@ def check_settings():
387
390
  for folder in os.environ.get('QUERYAPIGATE_SECRET_FILE_DIRS', '').split(','):
388
391
  if folder.strip() and not os.path.isabs(folder.strip()):
389
392
  raise ValueError(f"QUERYAPIGATE_SECRET_FILE_DIRS: '{folder.strip()}' must be an absolute path")
393
+ if dataplane_secret() is not None and len(dataplane_secret()) < DATAPLANE_SECRET_MIN:
394
+ raise ValueError(f'QUERYAPIGATE_DATAPLANE_SECRET must be at least {DATAPLANE_SECRET_MIN} characters - the data '
395
+ 'plane refuses a shorter one too')
390
396
  if metrics_token() is not None and len(metrics_token()) < METRICS_TOKEN_MIN:
391
397
  raise ValueError(f'QUERYAPIGATE_METRICS_TOKEN must be at least {METRICS_TOKEN_MIN} characters - '
392
398
  '`openssl rand -hex 32` makes one')
@@ -497,6 +503,7 @@ def check_settings():
497
503
  raise ValueError('QUERYAPIGATE_HSTS_MAX_AGE must be a whole number of seconds from 1 to '
498
504
  f'{MAX_HSTS_SECONDS} (two years) - 31536000 is a year')
499
505
  check_database_url()
506
+ check_run_store_url()
500
507
  check_jwt_settings()
501
508
  check_mcp_oauth_settings()
502
509
  check_sso_settings()
@@ -667,7 +674,7 @@ def _check_vault():
667
674
  SECRET_SETTINGS = ('QUERYAPIGATE_API_KEY', 'QUERYAPIGATE_SECRET_KEY', 'QUERYAPIGATE_DATABASE_URL',
668
675
  'QUERYAPIGATE_REDIS_URL', 'QUERYAPIGATE_AUDIT_KEY', 'QUERYAPIGATE_MASK_KEY',
669
676
  'QUERYAPIGATE_JWT_SECRET', 'QUERYAPIGATE_SSO_CLIENT_SECRET', 'QUERYAPIGATE_VAULT_SECRET_ID',
670
- 'QUERYAPIGATE_METRICS_TOKEN')
677
+ 'QUERYAPIGATE_METRICS_TOKEN', 'QUERYAPIGATE_RUN_STORE_URL')
671
678
  # QUERYAPIGATE_VAULT_TOKEN_FILE isn't one of them: vault.py reads it again at each sign-in, as Vault Agent rotates it.
672
679
  _from_files: dict[str, str] = {} # setting -> the file it was read from
673
680
 
@@ -699,6 +706,7 @@ def secret_source(name):
699
706
 
700
707
 
701
708
  METRICS_TOKEN_MIN = 16
709
+ DATAPLANE_SECRET_MIN = 16 # dataplane/src/config.rs's MIN_SECRET: it is all that lets a request speak for a client
702
710
 
703
711
 
704
712
  def metrics_token():
@@ -713,6 +721,13 @@ def mask_key():
713
721
  return os.environ.get('QUERYAPIGATE_MASK_KEY', '').strip() or None
714
722
 
715
723
 
724
+ def dataplane_secret():
725
+ """QUERYAPIGATE_DATAPLANE_SECRET: what a Rust data plane in front of this server sends to have its forwarded `/q/`
726
+ calls answered with a plan (dataplane.py, ADR 0028), or None - then no request gets one. A prototype setting:
727
+ shared by the two processes only, never by a client."""
728
+ return os.environ.get('QUERYAPIGATE_DATAPLANE_SECRET', '').strip() or None
729
+
730
+
716
731
  def secret_keys():
717
732
  """The keys connection and destination passwords are encrypted with at rest (QUERYAPIGATE_SECRET_KEY), first
718
733
  to last - empty when unset: encryption at rest is opt-in, and without it a password is stored exactly as given.
@@ -806,6 +821,43 @@ def database_url():
806
821
  return os.environ.get('QUERYAPIGATE_DATABASE_URL', '').strip() or None
807
822
 
808
823
 
824
+ RUN_STORE_HISTORY_DAYS = 30 # history's retention in the run store unless QUERYAPIGATE_HISTORY_RETENTION_DAYS says
825
+ _RUN_STORE_DATABASE_RE = re.compile(r'^[A-Za-z_][A-Za-z0-9_]{0,62}$')
826
+
827
+
828
+ def run_store_url():
829
+ """QUERYAPIGATE_RUN_STORE_URL: keep run history, the event log and usage rollups in ClickHouse, at this
830
+ http(s)://user:password@host:port/database URL, instead of in the metadata store (runstore.py, ADR 0029). None (the
831
+ default) keeps them there. Validated at startup by check_run_store_url()."""
832
+ return os.environ.get('QUERYAPIGATE_RUN_STORE_URL', '').strip() or None
833
+
834
+
835
+ def check_run_store_url():
836
+ """Part of check_settings(): the run store's URL is http(s)://, names a host and a database, and is https:// unless
837
+ the host is this machine - it carries a password, and every run's record."""
838
+ from urllib.parse import urlsplit
839
+ raw = run_store_url()
840
+ if raw is None:
841
+ return
842
+ try:
843
+ parts = urlsplit(raw)
844
+ port = parts.port
845
+ except ValueError as error:
846
+ raise ValueError(f'QUERYAPIGATE_RUN_STORE_URL is not a URL: {error}') from None
847
+ if parts.scheme not in ('http', 'https') or not parts.hostname:
848
+ raise ValueError("QUERYAPIGATE_RUN_STORE_URL must be http(s)://user:password@host:port/database - ClickHouse's "
849
+ 'HTTP interface')
850
+ if parts.scheme == 'http' and parts.hostname not in ('localhost', '127.0.0.1', '::1'):
851
+ raise ValueError('QUERYAPIGATE_RUN_STORE_URL must be https:// unless ClickHouse is on this machine: it carries '
852
+ 'a password and every run')
853
+ database = parts.path.strip('/') or 'queryapigate'
854
+ if not _RUN_STORE_DATABASE_RE.match(database):
855
+ raise ValueError(f"QUERYAPIGATE_RUN_STORE_URL: '{database}' is not a database name ClickHouse takes unquoted - "
856
+ 'letters, digits and _')
857
+ if port is not None and not 0 < port < 65536:
858
+ raise ValueError('QUERYAPIGATE_RUN_STORE_URL: the port must be from 1 to 65535')
859
+
860
+
809
861
  def redis_url():
810
862
  """QUERYAPIGATE_REDIS_URL: swaps the response cache (cache.py's ResponseCache) for a Redis-backed one
811
863
  (rediscache.RedisResponseCache) that survives a process restart and can be shared across horizontally-
@@ -1351,6 +1403,10 @@ def describe_settings():
1351
1403
  'database so several instances can run side by side.', 'QUERYAPIGATE_DATABASE_URL',
1352
1404
  'SQLite (queryapigate.db)' if database_url() is None
1353
1405
  else f'PostgreSQL ({redact_url(database_url())})', secret=True),
1406
+ row('Run store', 'Where run history, the event log and usage rollups are kept: the metadata store '
1407
+ '(default), or ClickHouse, for a data plane\'s rates (ADR 0029).', 'QUERYAPIGATE_RUN_STORE_URL',
1408
+ 'the metadata store' if run_store_url() is None
1409
+ else f'ClickHouse ({redact_url(run_store_url())})', secret=True),
1354
1410
  row('Load examples', 'Load the example APIs at startup. Idempotent.',
1355
1411
  'QUERYAPIGATE_LOAD_EXAMPLES', on_off(load_examples())),
1356
1412
  row('Configuration file', 'A configuration file applied at startup, once per file - connections, '
@@ -1401,6 +1457,9 @@ def describe_settings():
1401
1457
  secret=True),
1402
1458
  row('Masking hash key', "Key for column masking's hash action. Unset: one made once in the store.",
1403
1459
  'QUERYAPIGATE_MASK_KEY', 'configured' if mask_key() else 'from the store', secret=True),
1460
+ row('Data plane secret', 'Shared with a Rust data plane in front of this server (ADR 0028), which then '
1461
+ 'serves /q/ reads itself. Unset: no data plane can.',
1462
+ 'QUERYAPIGATE_DATAPLANE_SECRET', 'configured' if dataplane_secret() else 'not set', secret=True),
1404
1463
  row('Export folders', 'Local folders a destination may write under. Unset: object storage only.',
1405
1464
  'QUERYAPIGATE_EXPORT_DIRS', ', '.join(export_dirs()) or 'object storage only'),
1406
1465
  row('Trusted proxy hops', 'Reverse proxies whose X-Forwarded-* headers are trusted.',
@@ -1,2 +1,2 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/forms-BRJEHg6U.js","assets/client-ZHNiMG8H.js","assets/index-uRArh6IJ.js","assets/index-Cv64XKNs.css","assets/CodeBox-Cwh5KHLp.js","assets/api-DhjsrkN_.js","assets/useMutation-Cheq0sq0.js","assets/maskRules-CvKQIPgN.js","assets/SchemaBrowser-1u2p1iY-.js","assets/reach-BAOLd75Y.js"])))=>i.map(i=>d[i]);
2
- import{B as e,H as t,z as n}from"./client-ZHNiMG8H.js";import{D as r,M as i,P as a,c as o,d as s,g as c,k as l,s as u}from"./index-uRArh6IJ.js";import{t as d}from"./Time-USlo65Ii.js";import{t as f}from"./QueryInfo-B9xmoHuw.js";import{a as p}from"./reach-BAOLd75Y.js";import{i as m,n as h,o as g,r as _}from"./SchemaBrowser-1u2p1iY-.js";var v=t(e(),1),y=n(),b=(0,v.lazy)(()=>a(()=>import(`./forms-BRJEHg6U.js`).then(e=>({default:e.QueryForm})),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9]))),x={query:`Q`,collection:`C`,connection:`W`},S=[`Database`,`Created`,`Last modified`,`Last used`,`Version`],C=[`database`,`created`,`modified`,`lastUsed`,`version`],w=()=>new Date().toISOString().slice(0,10);function T(){let e=i(),{openDrawer:t}=l(),n=u(),a=o(),d=c(),x=s(),[S,C]=(0,v.useState)(m),w=e=>{let t={...S,...e};g(t),C(t)},T=x.data??[],D=e=>T.find(t=>t.name===e)?.db??null,O=h(S.conn||void 0),k=O.data?.tables??[],A=_(S.conn||void 0,D(S.conn),S.table&&!S.tableMatches?[S.table]:[]),j=S.table?S.tableMatches??(A.data?A.data[S.table]??[]:null):null,M=n.data??[],N=[...new Set(M.map(e=>D(e.connection_name)).filter(e=>!!e))].sort(),P=a.data??{},F=d.data??{},I=Object.keys(P).sort(),L=Object.keys(F).sort(),R,z=!0;S.conn?O.isPending?R=(0,y.jsx)(`option`,{value:``,children:`Loading tables…`}):O.isError?R=(0,y.jsx)(`option`,{value:``,children:`Schema unavailable`}):(z=!1,R=(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(`option`,{value:``,children:`All tables (${k.length})`}),k.map(e=>(0,y.jsx)(`option`,{value:e.name,children:`${e.name} · ${e.columns.length}${e.columns.length===1?` col`:` cols`}`},e.name))]})):R=(0,y.jsx)(`option`,{value:``,children:`Select a connection first`});let B=(e,t)=>p(e,t.name,t.collection,t.connection_name),V;if(n.isPending)V=(0,y.jsx)(r,{});else if(!M.length)V=(0,y.jsx)(`div`,{className:`empty`,children:(0,y.jsx)(`strong`,{children:`No saved queries yet`})});else if(!I.length&&!L.length)V=(0,y.jsxs)(`div`,{className:`empty`,children:[(0,y.jsx)(`strong`,{children:`No scoped API keys or roles yet`}),(0,y.jsx)(`span`,{children:`Every query is reachable by the admin key only until a scoped key is created.`})]});else{let n=S.q.trim().toLowerCase(),i=[...M].sort((e,t)=>(e.collection||`￿`)+e.name<(t.collection||`￿`)+t.name?-1:1),a=e=>e.toLowerCase().includes(n),o=n?i.filter(e=>a(e.name)||e.collection&&a(e.collection)):i,s=n?I.filter(a):I,c=n?L.filter(a):L,l=o.length||s.length||c.length,u=o.length?o:l?i:[],d=s.length?s:l?I:[],p=c.length?c:l?L:[];if(S.conn&&(u=u.filter(e=>e.connection_name===S.conn)),S.table&&j===null)V=(0,y.jsx)(r,{text:`Checking which queries reference ${S.table}…`});else if(j&&(u=u.filter(e=>j.includes(e.name))),u.length){let n=u.map(e=>({q:e,dbType:D(e.connection_name),reach:d.filter(t=>B(P[t],e).length).length}));S.db&&(n=n.filter(e=>e.dbType===S.db)),S.reach===`reachable`?n=n.filter(e=>e.reach>0):S.reach===`unreachable`&&(n=n.filter(e=>e.reach===0));let r=S.sort;if(r.field){let e=e=>{switch(r.field){case`query`:return e.q.name.toLowerCase();case`database`:return(e.dbType||``).toLowerCase();case`created`:return e.q.created_at||``;case`modified`:return e.q.updated_at||``;case`lastUsed`:return e.q.last_used_at||``;case`version`:return e.q.latest_version;case`connection`:return(e.q.connection_name||``).toLowerCase();case`reach`:return e.reach;case`col`:{let t=r.colType===`key`?P[r.colName]:F[r.colName];return t&&B(t,e.q).length?1:0}default:return``}};n=[...n].sort((t,n)=>{let i=e(t),a=e(n);return i<a?-r.dir:i>a?r.dir:0})}V=n.length?(0,y.jsx)(E,{rows:n,keyCols:d,roleCols:p,keyMap:P,roleMap:F,st:S,onSort:e=>w({sort:e}),onOpen:t=>e(`/queries/${encodeURIComponent(t)}`),onInfo:e=>t({title:e.q.name,kicker:`${e.q.collection||`uncollected`} · ${e.q.version_count}${e.q.version_count===1?` version`:` versions`}`,content:(0,y.jsx)(f,{name:e.q.name,dbType:e.dbType})})}):(0,y.jsx)(`div`,{className:`empty`,children:(0,y.jsx)(`span`,{children:`No queries match these filters.`})})}else{let i=n?`Nothing matches “${n}”.`:S.table?`“${S.table}” on ${S.conn} is not exposed by any saved query yet.`:S.conn?`No saved query uses “${S.conn}” yet.`:`Nothing matches these filters.`;V=(0,y.jsxs)(`div`,{className:`empty`,children:[(0,y.jsx)(`span`,{children:i}),!n&&S.conn?(0,y.jsx)(`button`,{type:`button`,className:`btn primary`,onClick:()=>{let n=D(S.conn)===`mongo`,i=S.table?n?JSON.stringify({collection:S.table,filter:{}},null,2):`SELECT * FROM `+S.table:void 0;e(`/queries`),t({title:`New API`,kicker:`POST /api/v1/queries`,content:(0,y.jsx)(v.Suspense,{fallback:(0,y.jsx)(r,{}),children:(0,y.jsx)(b,{starter:{connection:S.conn,sql:i}})})})},children:`New API`+(S.table?` on `+S.table:``)}):null]})}}return(0,y.jsxs)(y.Fragment,{children:[(0,y.jsxs)(`div`,{className:`page-head`,children:[(0,y.jsxs)(`div`,{className:`titles`,children:[(0,y.jsx)(`h1`,{children:`Access map`}),(0,y.jsxs)(`span`,{className:`sub`,children:[`Which API keys can reach which saved queries - the whole point of collections and connection grants, in one place.`,` `,(0,y.jsxs)(`span`,{className:`legend`,children:[(0,y.jsx)(`span`,{className:`amap-dot`,children:`Q`}),` named query \xA0`,(0,y.jsx)(`span`,{className:`amap-dot`,children:`C`}),` `,`collection \xA0`,(0,y.jsx)(`span`,{className:`amap-dot conn`,children:`W`}),` whole connection`]})]})]}),(0,y.jsx)(`span`,{className:`spacer`}),(0,y.jsxs)(`select`,{id:`accessmap-conn-filter`,"aria-label":`Connection`,value:T.some(e=>e.name===S.conn)?S.conn:``,onChange:e=>w({conn:e.target.value,table:``,tableMatches:null}),children:[(0,y.jsx)(`option`,{value:``,children:`All connections`}),[...T].sort((e,t)=>e.name<t.name?-1:1).map(e=>(0,y.jsx)(`option`,{value:e.name,children:e.name},e.name))]}),(0,y.jsx)(`select`,{id:`accessmap-table-filter`,"aria-label":`Table`,disabled:z,value:k.some(e=>e.name===S.table)?S.table:``,onChange:e=>w({table:e.target.value,tableMatches:null}),children:R}),(0,y.jsxs)(`select`,{id:`accessmap-db-filter`,"aria-label":`Database`,value:N.includes(S.db)?S.db:``,onChange:e=>w({db:e.target.value}),children:[(0,y.jsx)(`option`,{value:``,children:`All databases`}),N.map(e=>(0,y.jsx)(`option`,{value:e,children:e},e))]}),(0,y.jsxs)(`select`,{id:`accessmap-reach-filter`,"aria-label":`Reach`,value:S.reach,onChange:e=>w({reach:e.target.value}),children:[(0,y.jsx)(`option`,{value:``,children:`Any reach`}),(0,y.jsx)(`option`,{value:`reachable`,children:`Reachable by a key`}),(0,y.jsx)(`option`,{value:`unreachable`,children:`Reachable by no key`})]}),(0,y.jsx)(`input`,{id:`accessmap-filter`,className:`search`,type:`search`,placeholder:`Filter by query or key…`,value:S.q,onChange:e=>w({q:e.target.value})})]}),(0,y.jsx)(`div`,{id:`accessmap-body`,className:`panel`,children:V})]})}function E({rows:e,keyCols:t,roleCols:n,keyMap:r,roleMap:i,st:a,onSort:o,onOpen:s,onInfo:c}){let l=a.sort,u=(e,t=null,n=null)=>l.field===e&&l.colType===t&&l.colName===n,f=(e,t=null,n=null)=>u(e,t,n)?l.dir===1?` ▲`:` ▼`:``,m=(e,t=null,n=null)=>{let r=u(e,t,n);o(r&&l.dir===1?{field:e,colType:t,colName:n,dir:-1}:r?{field:null,colType:null,colName:null,dir:1}:{field:e,colType:t,colName:n,dir:1})},h=(e,t,n)=>(0,y.jsx)(`th`,{className:t,onClick:()=>m(n),children:e+f(n)},n),g=(e,t,n,r)=>(0,y.jsxs)(`th`,{className:`amap-key-head amap-sortable`+(n?` revoked`:``),title:n?e+n:e,onClick:()=>m(`col`,r,e),children:[(0,y.jsxs)(`span`,{className:`amap-key-name`,children:[e,f(`col`,r,e),n?(0,y.jsx)(`span`,{className:`dim`,children:n}):null]}),(0,y.jsxs)(`span`,{className:`amap-key-sub`,children:[(t.allow_writes?`Read/write`:`Read-only`)+` · `+(t.rate_limit||`server default`),t.mask?.length?(0,y.jsx)(`span`,{className:`tag`,title:t.mask.map(e=>`${e.column}: ${e.action}`).join(`, `),children:`masked`}):null]})]},r+`:`+e),_=(e,t,n,r)=>{let i=p(e,t.name,t.collection,t.connection_name);if(!i.length)return(0,y.jsx)(`td`,{className:`amap-cell`},r);let a=[...new Set(i.map(e=>x[e.kind]))],o=a.length===1&&a[0]===`W`;return(0,y.jsx)(`td`,{className:`amap-cell`,title:i.map(e=>e.label).join(`, `),children:(0,y.jsx)(`span`,{className:`amap-dot`+(n?` role`:``)+(o?` conn`:``),children:a.join(`·`)})},r)},v=w();return(0,y.jsx)(`div`,{style:{overflow:`auto`},children:(0,y.jsxs)(`table`,{className:`amap`,children:[(0,y.jsxs)(`thead`,{children:[(0,y.jsxs)(`tr`,{children:[(0,y.jsx)(`th`,{colSpan:3+S.length}),t.length?(0,y.jsx)(`th`,{colSpan:t.length,className:`amap-group`,children:`API KEYS`}):null,n.length?(0,y.jsx)(`th`,{colSpan:n.length,className:`amap-group role`,title:`What a key created from each role would reach - a role grants nothing on its own`,children:`ROLES`}):null]}),(0,y.jsxs)(`tr`,{children:[h(`Query`,`amap-query amap-sortable`,`query`),C.map((e,t)=>h(S[t],`amap-sortable`,e)),h(`Connection`,`amap-sortable`,`connection`),h(`Reach`,`num amap-sortable`,`reach`),t.map(e=>{let t=r[e],n=!!(t.expires_at&&t.expires_at<v);return g(e,t,t.active?n?` (expired ${t.expires_at})`:``:` (revoked)`,`key`)}),n.map(e=>g(e,i[e],``,`role`))]})]}),(0,y.jsx)(`tbody`,{children:e.map(e=>{let a=e.q;return(0,y.jsxs)(`tr`,{children:[(0,y.jsxs)(`td`,{className:`amap-query`,children:[(0,y.jsxs)(`div`,{className:`amap-q-row`,children:[(0,y.jsx)(`button`,{type:`button`,className:`amap-q-name`,title:`Open ${a.name} in API Repository`,onClick:()=>s(a.name),children:a.name}),(0,y.jsx)(`button`,{type:`button`,className:`amap-info`,title:`Details for ${a.name}`,"aria-label":`Details for ${a.name}`,onClick:()=>c(e),children:`i`})]}),a.collection?(0,y.jsx)(`span`,{className:`amap-coll`,children:a.collection}):null]}),(0,y.jsx)(`td`,{children:e.dbType?(0,y.jsx)(`span`,{className:`tag`,children:e.dbType}):(0,y.jsx)(`span`,{className:`dim`,children:`—`})}),(0,y.jsx)(`td`,{className:`mono dim`,children:(0,y.jsx)(d,{value:a.created_at})}),(0,y.jsx)(`td`,{className:`mono dim`,children:(0,y.jsx)(d,{value:a.updated_at})}),(0,y.jsx)(`td`,{className:a.last_used_at?`mono`:`mono dim`,children:(0,y.jsx)(d,{value:a.last_used_at,fallback:`never`})}),(0,y.jsx)(`td`,{className:`mono`,children:`v`+a.latest_version}),(0,y.jsx)(`td`,{className:`mono dim`,children:a.connection_name||`—`}),(0,y.jsx)(`td`,{className:`num`,title:`Reachable by an actual API key (roles alone reach nothing)`,children:e.reach?String(e.reach):`—`}),t.map(e=>_(r[e],a,!1,`k:`+e)),n.map(e=>_(i[e],a,!0,`r:`+e))]},a.name)})}),(0,y.jsx)(`tfoot`,{children:(0,y.jsxs)(`tr`,{children:[(0,y.jsx)(`td`,{className:`amap-query`,children:`Reaches`}),S.map(e=>(0,y.jsx)(`td`,{},e)),(0,y.jsx)(`td`,{}),(0,y.jsx)(`td`,{}),t.map(t=>(0,y.jsx)(`td`,{className:`num`,children:`${e.filter(e=>p(r[t],e.q.name,e.q.collection,e.q.connection_name).length).length} / ${e.length}`},t)),n.map(t=>(0,y.jsx)(`td`,{className:`num`,children:`${e.filter(e=>p(i[t],e.q.name,e.q.collection,e.q.connection_name).length).length} / ${e.length}`},t))]})})]})})}export{T as AccessMapPage};
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/forms-B7OrYKBb.js","assets/client-ZHNiMG8H.js","assets/index-Cl93xBjn.js","assets/index-Cv64XKNs.css","assets/CodeBox-Cwh5KHLp.js","assets/api-DIm7t7Bb.js","assets/useMutation-BIoTFTZ8.js","assets/maskRules-CvKQIPgN.js","assets/SchemaBrowser-8tmsr1__.js","assets/reach-BAOLd75Y.js"])))=>i.map(i=>d[i]);
2
+ import{B as e,H as t,z as n}from"./client-ZHNiMG8H.js";import{D as r,M as i,P as a,c as o,d as s,g as c,k as l,s as u}from"./index-Cl93xBjn.js";import{t as d}from"./Time-s5E5qK3s.js";import{t as f}from"./QueryInfo-Bv-0Zmc5.js";import{a as p}from"./reach-BAOLd75Y.js";import{i as m,n as h,o as g,r as _}from"./SchemaBrowser-8tmsr1__.js";var v=t(e(),1),y=n(),b=(0,v.lazy)(()=>a(()=>import(`./forms-B7OrYKBb.js`).then(e=>({default:e.QueryForm})),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9]))),x={query:`Q`,collection:`C`,connection:`W`},S=[`Database`,`Created`,`Last modified`,`Last used`,`Version`],C=[`database`,`created`,`modified`,`lastUsed`,`version`],w=()=>new Date().toISOString().slice(0,10);function T(){let e=i(),{openDrawer:t}=l(),n=u(),a=o(),d=c(),x=s(),[S,C]=(0,v.useState)(m),w=e=>{let t={...S,...e};g(t),C(t)},T=x.data??[],D=e=>T.find(t=>t.name===e)?.db??null,O=h(S.conn||void 0),k=O.data?.tables??[],A=_(S.conn||void 0,D(S.conn),S.table&&!S.tableMatches?[S.table]:[]),j=S.table?S.tableMatches??(A.data?A.data[S.table]??[]:null):null,M=n.data??[],N=[...new Set(M.map(e=>D(e.connection_name)).filter(e=>!!e))].sort(),P=a.data??{},F=d.data??{},I=Object.keys(P).sort(),L=Object.keys(F).sort(),R,z=!0;S.conn?O.isPending?R=(0,y.jsx)(`option`,{value:``,children:`Loading tables…`}):O.isError?R=(0,y.jsx)(`option`,{value:``,children:`Schema unavailable`}):(z=!1,R=(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(`option`,{value:``,children:`All tables (${k.length})`}),k.map(e=>(0,y.jsx)(`option`,{value:e.name,children:`${e.name} · ${e.columns.length}${e.columns.length===1?` col`:` cols`}`},e.name))]})):R=(0,y.jsx)(`option`,{value:``,children:`Select a connection first`});let B=(e,t)=>p(e,t.name,t.collection,t.connection_name),V;if(n.isPending)V=(0,y.jsx)(r,{});else if(!M.length)V=(0,y.jsx)(`div`,{className:`empty`,children:(0,y.jsx)(`strong`,{children:`No saved queries yet`})});else if(!I.length&&!L.length)V=(0,y.jsxs)(`div`,{className:`empty`,children:[(0,y.jsx)(`strong`,{children:`No scoped API keys or roles yet`}),(0,y.jsx)(`span`,{children:`Every query is reachable by the admin key only until a scoped key is created.`})]});else{let n=S.q.trim().toLowerCase(),i=[...M].sort((e,t)=>(e.collection||`￿`)+e.name<(t.collection||`￿`)+t.name?-1:1),a=e=>e.toLowerCase().includes(n),o=n?i.filter(e=>a(e.name)||e.collection&&a(e.collection)):i,s=n?I.filter(a):I,c=n?L.filter(a):L,l=o.length||s.length||c.length,u=o.length?o:l?i:[],d=s.length?s:l?I:[],p=c.length?c:l?L:[];if(S.conn&&(u=u.filter(e=>e.connection_name===S.conn)),S.table&&j===null)V=(0,y.jsx)(r,{text:`Checking which queries reference ${S.table}…`});else if(j&&(u=u.filter(e=>j.includes(e.name))),u.length){let n=u.map(e=>({q:e,dbType:D(e.connection_name),reach:d.filter(t=>B(P[t],e).length).length}));S.db&&(n=n.filter(e=>e.dbType===S.db)),S.reach===`reachable`?n=n.filter(e=>e.reach>0):S.reach===`unreachable`&&(n=n.filter(e=>e.reach===0));let r=S.sort;if(r.field){let e=e=>{switch(r.field){case`query`:return e.q.name.toLowerCase();case`database`:return(e.dbType||``).toLowerCase();case`created`:return e.q.created_at||``;case`modified`:return e.q.updated_at||``;case`lastUsed`:return e.q.last_used_at||``;case`version`:return e.q.latest_version;case`connection`:return(e.q.connection_name||``).toLowerCase();case`reach`:return e.reach;case`col`:{let t=r.colType===`key`?P[r.colName]:F[r.colName];return t&&B(t,e.q).length?1:0}default:return``}};n=[...n].sort((t,n)=>{let i=e(t),a=e(n);return i<a?-r.dir:i>a?r.dir:0})}V=n.length?(0,y.jsx)(E,{rows:n,keyCols:d,roleCols:p,keyMap:P,roleMap:F,st:S,onSort:e=>w({sort:e}),onOpen:t=>e(`/queries/${encodeURIComponent(t)}`),onInfo:e=>t({title:e.q.name,kicker:`${e.q.collection||`uncollected`} · ${e.q.version_count}${e.q.version_count===1?` version`:` versions`}`,content:(0,y.jsx)(f,{name:e.q.name,dbType:e.dbType})})}):(0,y.jsx)(`div`,{className:`empty`,children:(0,y.jsx)(`span`,{children:`No queries match these filters.`})})}else{let i=n?`Nothing matches “${n}”.`:S.table?`“${S.table}” on ${S.conn} is not exposed by any saved query yet.`:S.conn?`No saved query uses “${S.conn}” yet.`:`Nothing matches these filters.`;V=(0,y.jsxs)(`div`,{className:`empty`,children:[(0,y.jsx)(`span`,{children:i}),!n&&S.conn?(0,y.jsx)(`button`,{type:`button`,className:`btn primary`,onClick:()=>{let n=D(S.conn)===`mongo`,i=S.table?n?JSON.stringify({collection:S.table,filter:{}},null,2):`SELECT * FROM `+S.table:void 0;e(`/queries`),t({title:`New API`,kicker:`POST /api/v1/queries`,content:(0,y.jsx)(v.Suspense,{fallback:(0,y.jsx)(r,{}),children:(0,y.jsx)(b,{starter:{connection:S.conn,sql:i}})})})},children:`New API`+(S.table?` on `+S.table:``)}):null]})}}return(0,y.jsxs)(y.Fragment,{children:[(0,y.jsxs)(`div`,{className:`page-head`,children:[(0,y.jsxs)(`div`,{className:`titles`,children:[(0,y.jsx)(`h1`,{children:`Access map`}),(0,y.jsxs)(`span`,{className:`sub`,children:[`Which API keys can reach which saved queries - the whole point of collections and connection grants, in one place.`,` `,(0,y.jsxs)(`span`,{className:`legend`,children:[(0,y.jsx)(`span`,{className:`amap-dot`,children:`Q`}),` named query \xA0`,(0,y.jsx)(`span`,{className:`amap-dot`,children:`C`}),` `,`collection \xA0`,(0,y.jsx)(`span`,{className:`amap-dot conn`,children:`W`}),` whole connection`]})]})]}),(0,y.jsx)(`span`,{className:`spacer`}),(0,y.jsxs)(`select`,{id:`accessmap-conn-filter`,"aria-label":`Connection`,value:T.some(e=>e.name===S.conn)?S.conn:``,onChange:e=>w({conn:e.target.value,table:``,tableMatches:null}),children:[(0,y.jsx)(`option`,{value:``,children:`All connections`}),[...T].sort((e,t)=>e.name<t.name?-1:1).map(e=>(0,y.jsx)(`option`,{value:e.name,children:e.name},e.name))]}),(0,y.jsx)(`select`,{id:`accessmap-table-filter`,"aria-label":`Table`,disabled:z,value:k.some(e=>e.name===S.table)?S.table:``,onChange:e=>w({table:e.target.value,tableMatches:null}),children:R}),(0,y.jsxs)(`select`,{id:`accessmap-db-filter`,"aria-label":`Database`,value:N.includes(S.db)?S.db:``,onChange:e=>w({db:e.target.value}),children:[(0,y.jsx)(`option`,{value:``,children:`All databases`}),N.map(e=>(0,y.jsx)(`option`,{value:e,children:e},e))]}),(0,y.jsxs)(`select`,{id:`accessmap-reach-filter`,"aria-label":`Reach`,value:S.reach,onChange:e=>w({reach:e.target.value}),children:[(0,y.jsx)(`option`,{value:``,children:`Any reach`}),(0,y.jsx)(`option`,{value:`reachable`,children:`Reachable by a key`}),(0,y.jsx)(`option`,{value:`unreachable`,children:`Reachable by no key`})]}),(0,y.jsx)(`input`,{id:`accessmap-filter`,className:`search`,type:`search`,placeholder:`Filter by query or key…`,value:S.q,onChange:e=>w({q:e.target.value})})]}),(0,y.jsx)(`div`,{id:`accessmap-body`,className:`panel`,children:V})]})}function E({rows:e,keyCols:t,roleCols:n,keyMap:r,roleMap:i,st:a,onSort:o,onOpen:s,onInfo:c}){let l=a.sort,u=(e,t=null,n=null)=>l.field===e&&l.colType===t&&l.colName===n,f=(e,t=null,n=null)=>u(e,t,n)?l.dir===1?` ▲`:` ▼`:``,m=(e,t=null,n=null)=>{let r=u(e,t,n);o(r&&l.dir===1?{field:e,colType:t,colName:n,dir:-1}:r?{field:null,colType:null,colName:null,dir:1}:{field:e,colType:t,colName:n,dir:1})},h=(e,t,n)=>(0,y.jsx)(`th`,{className:t,onClick:()=>m(n),children:e+f(n)},n),g=(e,t,n,r)=>(0,y.jsxs)(`th`,{className:`amap-key-head amap-sortable`+(n?` revoked`:``),title:n?e+n:e,onClick:()=>m(`col`,r,e),children:[(0,y.jsxs)(`span`,{className:`amap-key-name`,children:[e,f(`col`,r,e),n?(0,y.jsx)(`span`,{className:`dim`,children:n}):null]}),(0,y.jsxs)(`span`,{className:`amap-key-sub`,children:[(t.allow_writes?`Read/write`:`Read-only`)+` · `+(t.rate_limit||`server default`),t.mask?.length?(0,y.jsx)(`span`,{className:`tag`,title:t.mask.map(e=>`${e.column}: ${e.action}`).join(`, `),children:`masked`}):null]})]},r+`:`+e),_=(e,t,n,r)=>{let i=p(e,t.name,t.collection,t.connection_name);if(!i.length)return(0,y.jsx)(`td`,{className:`amap-cell`},r);let a=[...new Set(i.map(e=>x[e.kind]))],o=a.length===1&&a[0]===`W`;return(0,y.jsx)(`td`,{className:`amap-cell`,title:i.map(e=>e.label).join(`, `),children:(0,y.jsx)(`span`,{className:`amap-dot`+(n?` role`:``)+(o?` conn`:``),children:a.join(`·`)})},r)},v=w();return(0,y.jsx)(`div`,{style:{overflow:`auto`},children:(0,y.jsxs)(`table`,{className:`amap`,children:[(0,y.jsxs)(`thead`,{children:[(0,y.jsxs)(`tr`,{children:[(0,y.jsx)(`th`,{colSpan:3+S.length}),t.length?(0,y.jsx)(`th`,{colSpan:t.length,className:`amap-group`,children:`API KEYS`}):null,n.length?(0,y.jsx)(`th`,{colSpan:n.length,className:`amap-group role`,title:`What a key created from each role would reach - a role grants nothing on its own`,children:`ROLES`}):null]}),(0,y.jsxs)(`tr`,{children:[h(`Query`,`amap-query amap-sortable`,`query`),C.map((e,t)=>h(S[t],`amap-sortable`,e)),h(`Connection`,`amap-sortable`,`connection`),h(`Reach`,`num amap-sortable`,`reach`),t.map(e=>{let t=r[e],n=!!(t.expires_at&&t.expires_at<v);return g(e,t,t.active?n?` (expired ${t.expires_at})`:``:` (revoked)`,`key`)}),n.map(e=>g(e,i[e],``,`role`))]})]}),(0,y.jsx)(`tbody`,{children:e.map(e=>{let a=e.q;return(0,y.jsxs)(`tr`,{children:[(0,y.jsxs)(`td`,{className:`amap-query`,children:[(0,y.jsxs)(`div`,{className:`amap-q-row`,children:[(0,y.jsx)(`button`,{type:`button`,className:`amap-q-name`,title:`Open ${a.name} in API Repository`,onClick:()=>s(a.name),children:a.name}),(0,y.jsx)(`button`,{type:`button`,className:`amap-info`,title:`Details for ${a.name}`,"aria-label":`Details for ${a.name}`,onClick:()=>c(e),children:`i`})]}),a.collection?(0,y.jsx)(`span`,{className:`amap-coll`,children:a.collection}):null]}),(0,y.jsx)(`td`,{children:e.dbType?(0,y.jsx)(`span`,{className:`tag`,children:e.dbType}):(0,y.jsx)(`span`,{className:`dim`,children:`—`})}),(0,y.jsx)(`td`,{className:`mono dim`,children:(0,y.jsx)(d,{value:a.created_at})}),(0,y.jsx)(`td`,{className:`mono dim`,children:(0,y.jsx)(d,{value:a.updated_at})}),(0,y.jsx)(`td`,{className:a.last_used_at?`mono`:`mono dim`,children:(0,y.jsx)(d,{value:a.last_used_at,fallback:`never`})}),(0,y.jsx)(`td`,{className:`mono`,children:`v`+a.latest_version}),(0,y.jsx)(`td`,{className:`mono dim`,children:a.connection_name||`—`}),(0,y.jsx)(`td`,{className:`num`,title:`Reachable by an actual API key (roles alone reach nothing)`,children:e.reach?String(e.reach):`—`}),t.map(e=>_(r[e],a,!1,`k:`+e)),n.map(e=>_(i[e],a,!0,`r:`+e))]},a.name)})}),(0,y.jsx)(`tfoot`,{children:(0,y.jsxs)(`tr`,{children:[(0,y.jsx)(`td`,{className:`amap-query`,children:`Reaches`}),S.map(e=>(0,y.jsx)(`td`,{},e)),(0,y.jsx)(`td`,{}),(0,y.jsx)(`td`,{}),t.map(t=>(0,y.jsx)(`td`,{className:`num`,children:`${e.filter(e=>p(r[t],e.q.name,e.q.collection,e.q.connection_name).length).length} / ${e.length}`},t)),n.map(t=>(0,y.jsx)(`td`,{className:`num`,children:`${e.filter(e=>p(i[t],e.q.name,e.q.collection,e.q.connection_name).length).length} / ${e.length}`},t))]})})]})})}export{T as AccessMapPage};
@@ -1 +1 @@
1
- import{B as e,H as t,R as n,c as r,h as i,l as a,n as o,s,t as c,v as l,z as u}from"./client-ZHNiMG8H.js";import{t as d}from"./useMutation-Cheq0sq0.js";import{D as f,E as p,T as m,f as h,h as g,k as _,l as v}from"./index-uRArh6IJ.js";import{t as y}from"./Time-USlo65Ii.js";import{t as b}from"./forms-Bn5QJbOg.js";import{n as x,t as S}from"./icons-CFXE8hAz.js";var C=t(e(),1),w=u(),T=[{value:`owner`,label:`Owner`,text:`everything, including administrators`},{value:`admin`,label:`Admin`,text:`connections, API keys and roles, queries, cache, settings`},{value:`developer`,label:`Developer`,text:`saved queries and SQL; no keys, no connection changes`},{value:`auditor`,label:`Auditor`,text:`read only - audit log, history, configuration; no SQL`}],E=e=>e===null?`Workspaces only`:T.find(t=>t.value===e)?.label??e,D=90,O=e=>new Date(Date.now()+e*864e5).toISOString().slice(0,10),k=`Store this now — it cannot be shown again. Paste it into the API key box at the bottom of the sidebar, or send it as the X-API-Key header.`;function A(){let{showError:e}=_();return t=>t instanceof c?e(t.message,{status:t.status}):e(t.message)}function j(e){let t=h(`admins.read`,`other administrators`);return l({queryKey:[`administrators`],queryFn:async()=>(await t.check(),s(await o.GET(`/api/v1/administrators`)).items),enabled:e,retry:!1})}function M(e){return l({queryKey:[`administrators`,e,`tokens`],queryFn:async()=>s(await o.GET(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:e}}})).items,retry:!1})}function N(){let e=g(),t=v(),n=!!e.data&&t(`admins.read`),r=j(n),{openDrawer:i}=_(),a=()=>i({title:`New administrator`,content:(0,w.jsx)(I,{})}),o;return o=e.isPending?(0,w.jsx)(f,{}):e.isError?(0,w.jsxs)(`div`,{className:`empty`,children:[(0,w.jsx)(`strong`,{children:`Not signed in as an administrator`}),(0,w.jsx)(`span`,{children:`Apply an admin token (qagadm_…) or the shared admin key in the box at the bottom of the sidebar.`})]}):n?r.isPending?(0,w.jsx)(f,{text:`Loading administrators…`}):r.isError?(0,w.jsx)(`div`,{className:`hint`,children:r.error.message}):r.data.length?(0,w.jsx)(P,{items:r.data,self:e.data.via===`token`||e.data.via===`sso`?e.data.name:null}):(0,w.jsxs)(`div`,{className:`empty`,children:[(0,w.jsx)(`strong`,{children:`No administrators yet`}),(0,w.jsx)(`span`,{children:`Everyone shares QUERYAPIGATE_API_KEY today, so the audit log can only say “admin”. Give each person their own sign-in, with a role that fits what they do — then the shared key becomes a break-glass key.`}),(0,w.jsx)(`button`,{type:`button`,className:`btn primary`,onClick:a,children:`New administrator`})]}):(0,w.jsx)(F,{name:e.data.name??``,role:e.data.role}),(0,w.jsxs)(w.Fragment,{children:[(0,w.jsxs)(`div`,{className:`page-head`,children:[(0,w.jsxs)(`div`,{className:`titles`,children:[(0,w.jsx)(`h1`,{children:`Administrators`}),(0,w.jsx)(`span`,{className:`sub`,id:`administrators-sub`,children:n?`The people and pipelines that manage this server, each with a role and their own tokens.`:`Your account and your admin tokens.`})]}),(0,w.jsx)(`span`,{className:`spacer`}),n&&(0,w.jsx)(`button`,{id:`new-administrator`,type:`button`,className:`btn primary`,onClick:a,children:`New administrator`})]}),(0,w.jsx)(`div`,{id:`administrators-table`,className:`panel`,children:o})]})}function P({items:e,self:t}){let{openDrawer:i,toast:a}=_(),s=A(),c=n(),l=async e=>{if(window.confirm(`Remove administrator '${e}'? Every token of theirs stops working immediately.`))try{r(await o.DELETE(`/api/v1/administrators/{name}`,{params:{path:{name:e}}})),a(`Removed `+e),c.invalidateQueries({queryKey:[`administrators`]})}catch(e){s(e)}};return(0,w.jsx)(`div`,{style:{overflowX:`auto`},children:(0,w.jsxs)(`table`,{className:`grid`,children:[(0,w.jsx)(`thead`,{children:(0,w.jsx)(`tr`,{children:[`Name`,`Role`,`Email`,`Last seen`,`Tokens`,``].map((e,t)=>(0,w.jsx)(`th`,{children:e},t))})}),(0,w.jsx)(`tbody`,{children:e.map(e=>(0,w.jsxs)(`tr`,{"data-name":e.name,children:[(0,w.jsx)(`td`,{children:(0,w.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:8},children:[(0,w.jsx)(`span`,{className:`dot ${e.active?`ok`:`off`}`,title:e.active?`Active`:`Inactive`}),(0,w.jsx)(`span`,{className:`name`,children:e.name}),e.name===t&&(0,w.jsx)(`span`,{className:`tag`,children:`you`}),e.sso?.status===`linked`&&(0,w.jsx)(`span`,{className:`tag`,title:`Signs in through ${e.sso.issuer??`SSO`}; role from their groups`,children:`SSO`}),e.sso?.status===`pending`&&(0,w.jsx)(`span`,{className:`tag`,title:`Linked at their next SSO sign-in, by their verified email`,children:`SSO pending`})]})}),(0,w.jsx)(`td`,{children:E(e.role)}),(0,w.jsx)(`td`,{className:e.email?void 0:`dim`,children:e.email??`—`}),(0,w.jsx)(`td`,{className:`mono dim`,style:{whiteSpace:`nowrap`},children:(0,w.jsx)(y,{value:e.last_seen_at,fallback:`never`})}),(0,w.jsx)(`td`,{className:`mono`,children:e.tokens}),(0,w.jsx)(`td`,{children:(0,w.jsxs)(`div`,{className:`actions`,children:[(0,w.jsx)(`button`,{type:`button`,className:`btn ghost sm`,onClick:()=>i({title:`Admin tokens`,kicker:e.name,wide:!0,content:(0,w.jsx)(R,{name:e.name})}),children:`Tokens`}),(0,w.jsx)(`button`,{type:`button`,className:`btn ghost sm icon`,title:`Edit ${e.name}`,"aria-label":`Edit`,onClick:()=>i({title:`Edit administrator`,kicker:e.name,content:(0,w.jsx)(I,{name:e.name})}),children:(0,w.jsx)(S,{})}),(0,w.jsx)(`button`,{type:`button`,className:`btn ghost sm icon danger`,title:`Remove ${e.name}`,"aria-label":`Remove`,onClick:()=>void l(e.name),children:(0,w.jsx)(x,{})})]})})]},e.name))})]})})}function F({name:e,role:t}){return(0,w.jsxs)(`div`,{className:`form`,style:{padding:16},children:[(0,w.jsxs)(`div`,{children:[`Signed in as `,(0,w.jsx)(`strong`,{children:e}),` · `,E(t),` — `,T.find(e=>e.value===t)?.text,`.`]}),(0,w.jsx)(R,{name:e})]})}function I({name:e}){let t=l({queryKey:[`administrators`,`detail`,e],queryFn:async()=>a(await o.GET(`/api/v1/administrators/{name}`,{params:{path:{name:e}}})),enabled:!!e,retry:!1,gcTime:0});return e&&t.isPending?(0,w.jsx)(f,{}):e&&t.isError?(0,w.jsx)(`div`,{className:`hint`,children:t.error.message}):(0,w.jsx)(L,{name:e,existing:t.data?.data,etag:t.data?.etag??null},e??``)}function L({name:e,existing:t,etag:r}){let a=!!e,{closeDrawer:c,openDrawer:l,showError:u,toast:f}=_(),h=A(),g=n(),[v,y]=(0,C.useState)(e??``),[x,S]=(0,C.useState)(t?.role??`developer`),[E,j]=(0,C.useState)(t?.email??``),[M,N]=(0,C.useState)(!t||t.active),P=i(),F=t?.sso?.status===`linked`,[I,L]=(0,C.useState)(t?.sso?.status??`none`),R=d({mutationFn:async()=>{let n={role:x,email:E.trim()||null};if(a)return s(await o.PATCH(`/api/v1/administrators/{name}`,{params:{path:{name:e},header:r?{"If-Match":r}:{}},body:{...F?{email:n.email}:n,active:M,...I===(t?.sso?.status??`none`)?{}:{sso:I===`pending`?`pending`:null}}})),null;let i=s(await o.POST(`/api/v1/administrators`,{body:{name:v.trim(),...n}}));return s(await o.POST(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:i.name}},body:{label:`first token`,expires_at:O(D)}}))},onSuccess:t=>{if(u(``),g.invalidateQueries({queryKey:[`administrators`]}),!t){c(),f(`Updated `+e);return}l({title:`Administrator created`,kicker:t.admin,content:(0,w.jsx)(b,{secret:t.secret,label:`Admin token for ${t.admin} (expires ${t.expires_at})`,hint:k})})},onError:h});return(0,w.jsxs)(`form`,{className:`form`,noValidate:!0,onSubmit:e=>{if(e.preventDefault(),!a&&!v.trim()){u(`A name is required.`,{errors:{name:`This field is required`}});return}R.mutate()},children:[(0,w.jsx)(m,{id:`a-name`,label:`Name`,hint:a?void 0:`A person (alice) or a pipeline (ci-deploy). Shown in the audit log.`,children:(0,w.jsx)(`input`,{id:`a-name`,value:v,placeholder:`alice`,autoComplete:`off`,spellCheck:!1,disabled:a,autoFocus:!a,onChange:e=>y(e.target.value)})}),(0,w.jsx)(m,{id:`a-role`,label:`Role`,hint:F?`From their groups at the identity provider, at every sign-in - change it there.`:T.find(e=>e.value===x)?.text,children:(0,w.jsx)(`select`,{id:`a-role`,value:x,disabled:F,onChange:e=>S(e.target.value),children:T.map(e=>(0,w.jsx)(`option`,{value:e.value,children:e.label},e.value))})}),(0,w.jsx)(m,{id:`a-email`,label:`Email`,hint:`Optional.`,children:(0,w.jsx)(`input`,{id:`a-email`,type:`email`,value:E,autoComplete:`off`,onChange:e=>j(e.target.value)})}),a&&(0,w.jsxs)(`label`,{className:`switch`,children:[(0,w.jsx)(`input`,{id:`a-active`,type:`checkbox`,checked:M,onChange:e=>N(e.target.checked)}),`Active`,(0,w.jsx)(`span`,{className:`hint`,children:`— unchecking stops every token of theirs immediately`})]}),a&&P.data?.sso&&(0,w.jsx)(m,{id:`a-sso`,label:`Sign-in through SSO`,hint:I===`linked`?`They sign in through the identity provider. Unlinking ends their sessions; their next SSO sign-in makes a new administrator.`:`Pending: their next SSO sign-in, with a verified email matching the one above, is bound to this account - once.`,children:(0,w.jsxs)(`select`,{id:`a-sso`,value:I,onChange:e=>L(e.target.value),children:[F&&(0,w.jsx)(`option`,{value:`linked`,children:`Linked`}),!F&&(0,w.jsx)(`option`,{value:`pending`,children:`Link at their next sign-in`}),(0,w.jsx)(`option`,{value:`none`,children:F?`Unlink`:`Not through SSO`})]})}),!a&&(0,w.jsxs)(`div`,{className:`hint`,children:[`Their first admin token, valid for `,D,` days, is shown once after you create them.`]}),(0,w.jsx)(p,{onCancel:c,children:(0,w.jsx)(`button`,{type:`submit`,className:`btn primary`,disabled:R.isPending,children:a?`Save`:`Create`})})]})}function R({name:e}){let t=M(e),i=n(),{toast:a}=_(),c=A(),[l,u]=(0,C.useState)(``),[p,h]=(0,C.useState)(O(D)),[g,v]=(0,C.useState)(null),x=()=>void i.invalidateQueries({queryKey:[`administrators`]}),S=d({mutationFn:async()=>s(await o.POST(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:e}},body:{label:l.trim()||null,expires_at:p||null}})),onSuccess:e=>{v(e),u(``),x()},onError:c}),T=async t=>{if(window.confirm(`Revoke ${t.label?`'${t.label}'`:t.id}? Anything using it stops working now.`))try{r(await o.DELETE(`/api/v1/administrators/{name}/tokens/{token_id}`,{params:{path:{name:e,token_id:t.id}}})),a(`Revoked `+(t.label??t.id)),x()}catch(e){c(e)}},E=new Date().toISOString().slice(0,10);return(0,w.jsxs)(`div`,{className:`form`,id:`admin-tokens`,children:[t.isPending?(0,w.jsx)(f,{text:`Loading tokens…`}):t.isError?(0,w.jsx)(`div`,{className:`hint`,children:t.error.message}):t.data.length?(0,w.jsxs)(`table`,{className:`grid`,children:[(0,w.jsx)(`thead`,{children:(0,w.jsx)(`tr`,{children:[`Label`,`Expires`,`Last used`,``].map((e,t)=>(0,w.jsx)(`th`,{children:e},t))})}),(0,w.jsx)(`tbody`,{children:t.data.map(e=>(0,w.jsxs)(`tr`,{"data-token":e.id,children:[(0,w.jsx)(`td`,{title:e.id,children:e.label??(0,w.jsx)(`span`,{className:`dim mono`,children:e.id})}),(0,w.jsx)(`td`,{style:{whiteSpace:`nowrap`},children:e.expires_at?(0,w.jsx)(`span`,{style:e.expired?{color:`var(--danger)`}:void 0,children:e.expires_at+(e.expired?` (expired)`:e.expires_at===E?` (today)`:``)}):(0,w.jsx)(`span`,{className:`dim`,children:`never`})}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(y,{value:e.last_used_at,fallback:`never`})}),(0,w.jsx)(`td`,{children:(0,w.jsx)(`button`,{type:`button`,className:`btn ghost sm danger`,onClick:()=>void T(e),children:`Revoke`})})]},e.id))})]}):(0,w.jsx)(`div`,{className:`hint`,children:`No tokens. Issue one below to sign in with.`}),g?(0,w.jsx)(b,{secret:g.secret,label:`New token${g.label?` '${g.label}'`:``} (${g.expires_at?`expires ${g.expires_at}`:`never expires`})`,hint:k,onDone:()=>v(null)}):(0,w.jsxs)(`form`,{className:`grid2`,noValidate:!0,onSubmit:e=>{e.preventDefault(),S.mutate()},children:[(0,w.jsx)(m,{id:`t-label`,label:`New token: label`,hint:`What it's for - laptop, ci.`,children:(0,w.jsx)(`input`,{id:`t-label`,value:l,autoComplete:`off`,onChange:e=>u(e.target.value)})}),(0,w.jsx)(m,{id:`t-expires`,label:`Expires`,hint:`Leave blank for no expiry.`,children:(0,w.jsx)(`input`,{id:`t-expires`,type:`date`,value:p,onChange:e=>h(e.target.value)})}),(0,w.jsx)(`div`,{children:(0,w.jsx)(`button`,{type:`submit`,className:`btn`,disabled:S.isPending,children:`Issue token`})})]})]})}export{N as AdministratorsPage};
1
+ import{B as e,H as t,R as n,c as r,h as i,l as a,n as o,s,t as c,v as l,z as u}from"./client-ZHNiMG8H.js";import{t as d}from"./useMutation-BIoTFTZ8.js";import{D as f,E as p,T as m,f as h,h as g,k as _,l as v}from"./index-Cl93xBjn.js";import{t as y}from"./Time-s5E5qK3s.js";import{t as b}from"./forms-CTAVu1q5.js";import{n as x,t as S}from"./icons-CFXE8hAz.js";var C=t(e(),1),w=u(),T=[{value:`owner`,label:`Owner`,text:`everything, including administrators`},{value:`admin`,label:`Admin`,text:`connections, API keys and roles, queries, cache, settings`},{value:`developer`,label:`Developer`,text:`saved queries and SQL; no keys, no connection changes`},{value:`auditor`,label:`Auditor`,text:`read only - audit log, history, configuration; no SQL`}],E=e=>e===null?`Workspaces only`:T.find(t=>t.value===e)?.label??e,D=90,O=e=>new Date(Date.now()+e*864e5).toISOString().slice(0,10),k=`Store this now — it cannot be shown again. Paste it into the API key box at the bottom of the sidebar, or send it as the X-API-Key header.`;function A(){let{showError:e}=_();return t=>t instanceof c?e(t.message,{status:t.status}):e(t.message)}function j(e){let t=h(`admins.read`,`other administrators`);return l({queryKey:[`administrators`],queryFn:async()=>(await t.check(),s(await o.GET(`/api/v1/administrators`)).items),enabled:e,retry:!1})}function M(e){return l({queryKey:[`administrators`,e,`tokens`],queryFn:async()=>s(await o.GET(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:e}}})).items,retry:!1})}function N(){let e=g(),t=v(),n=!!e.data&&t(`admins.read`),r=j(n),{openDrawer:i}=_(),a=()=>i({title:`New administrator`,content:(0,w.jsx)(I,{})}),o;return o=e.isPending?(0,w.jsx)(f,{}):e.isError?(0,w.jsxs)(`div`,{className:`empty`,children:[(0,w.jsx)(`strong`,{children:`Not signed in as an administrator`}),(0,w.jsx)(`span`,{children:`Apply an admin token (qagadm_…) or the shared admin key in the box at the bottom of the sidebar.`})]}):n?r.isPending?(0,w.jsx)(f,{text:`Loading administrators…`}):r.isError?(0,w.jsx)(`div`,{className:`hint`,children:r.error.message}):r.data.length?(0,w.jsx)(P,{items:r.data,self:e.data.via===`token`||e.data.via===`sso`?e.data.name:null}):(0,w.jsxs)(`div`,{className:`empty`,children:[(0,w.jsx)(`strong`,{children:`No administrators yet`}),(0,w.jsx)(`span`,{children:`Everyone shares QUERYAPIGATE_API_KEY today, so the audit log can only say “admin”. Give each person their own sign-in, with a role that fits what they do — then the shared key becomes a break-glass key.`}),(0,w.jsx)(`button`,{type:`button`,className:`btn primary`,onClick:a,children:`New administrator`})]}):(0,w.jsx)(F,{name:e.data.name??``,role:e.data.role}),(0,w.jsxs)(w.Fragment,{children:[(0,w.jsxs)(`div`,{className:`page-head`,children:[(0,w.jsxs)(`div`,{className:`titles`,children:[(0,w.jsx)(`h1`,{children:`Administrators`}),(0,w.jsx)(`span`,{className:`sub`,id:`administrators-sub`,children:n?`The people and pipelines that manage this server, each with a role and their own tokens.`:`Your account and your admin tokens.`})]}),(0,w.jsx)(`span`,{className:`spacer`}),n&&(0,w.jsx)(`button`,{id:`new-administrator`,type:`button`,className:`btn primary`,onClick:a,children:`New administrator`})]}),(0,w.jsx)(`div`,{id:`administrators-table`,className:`panel`,children:o})]})}function P({items:e,self:t}){let{openDrawer:i,toast:a}=_(),s=A(),c=n(),l=async e=>{if(window.confirm(`Remove administrator '${e}'? Every token of theirs stops working immediately.`))try{r(await o.DELETE(`/api/v1/administrators/{name}`,{params:{path:{name:e}}})),a(`Removed `+e),c.invalidateQueries({queryKey:[`administrators`]})}catch(e){s(e)}};return(0,w.jsx)(`div`,{style:{overflowX:`auto`},children:(0,w.jsxs)(`table`,{className:`grid`,children:[(0,w.jsx)(`thead`,{children:(0,w.jsx)(`tr`,{children:[`Name`,`Role`,`Email`,`Last seen`,`Tokens`,``].map((e,t)=>(0,w.jsx)(`th`,{children:e},t))})}),(0,w.jsx)(`tbody`,{children:e.map(e=>(0,w.jsxs)(`tr`,{"data-name":e.name,children:[(0,w.jsx)(`td`,{children:(0,w.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:8},children:[(0,w.jsx)(`span`,{className:`dot ${e.active?`ok`:`off`}`,title:e.active?`Active`:`Inactive`}),(0,w.jsx)(`span`,{className:`name`,children:e.name}),e.name===t&&(0,w.jsx)(`span`,{className:`tag`,children:`you`}),e.sso?.status===`linked`&&(0,w.jsx)(`span`,{className:`tag`,title:`Signs in through ${e.sso.issuer??`SSO`}; role from their groups`,children:`SSO`}),e.sso?.status===`pending`&&(0,w.jsx)(`span`,{className:`tag`,title:`Linked at their next SSO sign-in, by their verified email`,children:`SSO pending`})]})}),(0,w.jsx)(`td`,{children:E(e.role)}),(0,w.jsx)(`td`,{className:e.email?void 0:`dim`,children:e.email??`—`}),(0,w.jsx)(`td`,{className:`mono dim`,style:{whiteSpace:`nowrap`},children:(0,w.jsx)(y,{value:e.last_seen_at,fallback:`never`})}),(0,w.jsx)(`td`,{className:`mono`,children:e.tokens}),(0,w.jsx)(`td`,{children:(0,w.jsxs)(`div`,{className:`actions`,children:[(0,w.jsx)(`button`,{type:`button`,className:`btn ghost sm`,onClick:()=>i({title:`Admin tokens`,kicker:e.name,wide:!0,content:(0,w.jsx)(R,{name:e.name})}),children:`Tokens`}),(0,w.jsx)(`button`,{type:`button`,className:`btn ghost sm icon`,title:`Edit ${e.name}`,"aria-label":`Edit`,onClick:()=>i({title:`Edit administrator`,kicker:e.name,content:(0,w.jsx)(I,{name:e.name})}),children:(0,w.jsx)(S,{})}),(0,w.jsx)(`button`,{type:`button`,className:`btn ghost sm icon danger`,title:`Remove ${e.name}`,"aria-label":`Remove`,onClick:()=>void l(e.name),children:(0,w.jsx)(x,{})})]})})]},e.name))})]})})}function F({name:e,role:t}){return(0,w.jsxs)(`div`,{className:`form`,style:{padding:16},children:[(0,w.jsxs)(`div`,{children:[`Signed in as `,(0,w.jsx)(`strong`,{children:e}),` · `,E(t),` — `,T.find(e=>e.value===t)?.text,`.`]}),(0,w.jsx)(R,{name:e})]})}function I({name:e}){let t=l({queryKey:[`administrators`,`detail`,e],queryFn:async()=>a(await o.GET(`/api/v1/administrators/{name}`,{params:{path:{name:e}}})),enabled:!!e,retry:!1,gcTime:0});return e&&t.isPending?(0,w.jsx)(f,{}):e&&t.isError?(0,w.jsx)(`div`,{className:`hint`,children:t.error.message}):(0,w.jsx)(L,{name:e,existing:t.data?.data,etag:t.data?.etag??null},e??``)}function L({name:e,existing:t,etag:r}){let a=!!e,{closeDrawer:c,openDrawer:l,showError:u,toast:f}=_(),h=A(),g=n(),[v,y]=(0,C.useState)(e??``),[x,S]=(0,C.useState)(t?.role??`developer`),[E,j]=(0,C.useState)(t?.email??``),[M,N]=(0,C.useState)(!t||t.active),P=i(),F=t?.sso?.status===`linked`,[I,L]=(0,C.useState)(t?.sso?.status??`none`),R=d({mutationFn:async()=>{let n={role:x,email:E.trim()||null};if(a)return s(await o.PATCH(`/api/v1/administrators/{name}`,{params:{path:{name:e},header:r?{"If-Match":r}:{}},body:{...F?{email:n.email}:n,active:M,...I===(t?.sso?.status??`none`)?{}:{sso:I===`pending`?`pending`:null}}})),null;let i=s(await o.POST(`/api/v1/administrators`,{body:{name:v.trim(),...n}}));return s(await o.POST(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:i.name}},body:{label:`first token`,expires_at:O(D)}}))},onSuccess:t=>{if(u(``),g.invalidateQueries({queryKey:[`administrators`]}),!t){c(),f(`Updated `+e);return}l({title:`Administrator created`,kicker:t.admin,content:(0,w.jsx)(b,{secret:t.secret,label:`Admin token for ${t.admin} (expires ${t.expires_at})`,hint:k})})},onError:h});return(0,w.jsxs)(`form`,{className:`form`,noValidate:!0,onSubmit:e=>{if(e.preventDefault(),!a&&!v.trim()){u(`A name is required.`,{errors:{name:`This field is required`}});return}R.mutate()},children:[(0,w.jsx)(m,{id:`a-name`,label:`Name`,hint:a?void 0:`A person (alice) or a pipeline (ci-deploy). Shown in the audit log.`,children:(0,w.jsx)(`input`,{id:`a-name`,value:v,placeholder:`alice`,autoComplete:`off`,spellCheck:!1,disabled:a,autoFocus:!a,onChange:e=>y(e.target.value)})}),(0,w.jsx)(m,{id:`a-role`,label:`Role`,hint:F?`From their groups at the identity provider, at every sign-in - change it there.`:T.find(e=>e.value===x)?.text,children:(0,w.jsx)(`select`,{id:`a-role`,value:x,disabled:F,onChange:e=>S(e.target.value),children:T.map(e=>(0,w.jsx)(`option`,{value:e.value,children:e.label},e.value))})}),(0,w.jsx)(m,{id:`a-email`,label:`Email`,hint:`Optional.`,children:(0,w.jsx)(`input`,{id:`a-email`,type:`email`,value:E,autoComplete:`off`,onChange:e=>j(e.target.value)})}),a&&(0,w.jsxs)(`label`,{className:`switch`,children:[(0,w.jsx)(`input`,{id:`a-active`,type:`checkbox`,checked:M,onChange:e=>N(e.target.checked)}),`Active`,(0,w.jsx)(`span`,{className:`hint`,children:`— unchecking stops every token of theirs immediately`})]}),a&&P.data?.sso&&(0,w.jsx)(m,{id:`a-sso`,label:`Sign-in through SSO`,hint:I===`linked`?`They sign in through the identity provider. Unlinking ends their sessions; their next SSO sign-in makes a new administrator.`:`Pending: their next SSO sign-in, with a verified email matching the one above, is bound to this account - once.`,children:(0,w.jsxs)(`select`,{id:`a-sso`,value:I,onChange:e=>L(e.target.value),children:[F&&(0,w.jsx)(`option`,{value:`linked`,children:`Linked`}),!F&&(0,w.jsx)(`option`,{value:`pending`,children:`Link at their next sign-in`}),(0,w.jsx)(`option`,{value:`none`,children:F?`Unlink`:`Not through SSO`})]})}),!a&&(0,w.jsxs)(`div`,{className:`hint`,children:[`Their first admin token, valid for `,D,` days, is shown once after you create them.`]}),(0,w.jsx)(p,{onCancel:c,children:(0,w.jsx)(`button`,{type:`submit`,className:`btn primary`,disabled:R.isPending,children:a?`Save`:`Create`})})]})}function R({name:e}){let t=M(e),i=n(),{toast:a}=_(),c=A(),[l,u]=(0,C.useState)(``),[p,h]=(0,C.useState)(O(D)),[g,v]=(0,C.useState)(null),x=()=>void i.invalidateQueries({queryKey:[`administrators`]}),S=d({mutationFn:async()=>s(await o.POST(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:e}},body:{label:l.trim()||null,expires_at:p||null}})),onSuccess:e=>{v(e),u(``),x()},onError:c}),T=async t=>{if(window.confirm(`Revoke ${t.label?`'${t.label}'`:t.id}? Anything using it stops working now.`))try{r(await o.DELETE(`/api/v1/administrators/{name}/tokens/{token_id}`,{params:{path:{name:e,token_id:t.id}}})),a(`Revoked `+(t.label??t.id)),x()}catch(e){c(e)}},E=new Date().toISOString().slice(0,10);return(0,w.jsxs)(`div`,{className:`form`,id:`admin-tokens`,children:[t.isPending?(0,w.jsx)(f,{text:`Loading tokens…`}):t.isError?(0,w.jsx)(`div`,{className:`hint`,children:t.error.message}):t.data.length?(0,w.jsxs)(`table`,{className:`grid`,children:[(0,w.jsx)(`thead`,{children:(0,w.jsx)(`tr`,{children:[`Label`,`Expires`,`Last used`,``].map((e,t)=>(0,w.jsx)(`th`,{children:e},t))})}),(0,w.jsx)(`tbody`,{children:t.data.map(e=>(0,w.jsxs)(`tr`,{"data-token":e.id,children:[(0,w.jsx)(`td`,{title:e.id,children:e.label??(0,w.jsx)(`span`,{className:`dim mono`,children:e.id})}),(0,w.jsx)(`td`,{style:{whiteSpace:`nowrap`},children:e.expires_at?(0,w.jsx)(`span`,{style:e.expired?{color:`var(--danger)`}:void 0,children:e.expires_at+(e.expired?` (expired)`:e.expires_at===E?` (today)`:``)}):(0,w.jsx)(`span`,{className:`dim`,children:`never`})}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(y,{value:e.last_used_at,fallback:`never`})}),(0,w.jsx)(`td`,{children:(0,w.jsx)(`button`,{type:`button`,className:`btn ghost sm danger`,onClick:()=>void T(e),children:`Revoke`})})]},e.id))})]}):(0,w.jsx)(`div`,{className:`hint`,children:`No tokens. Issue one below to sign in with.`}),g?(0,w.jsx)(b,{secret:g.secret,label:`New token${g.label?` '${g.label}'`:``} (${g.expires_at?`expires ${g.expires_at}`:`never expires`})`,hint:k,onDone:()=>v(null)}):(0,w.jsxs)(`form`,{className:`grid2`,noValidate:!0,onSubmit:e=>{e.preventDefault(),S.mutate()},children:[(0,w.jsx)(m,{id:`t-label`,label:`New token: label`,hint:`What it's for - laptop, ci.`,children:(0,w.jsx)(`input`,{id:`t-label`,value:l,autoComplete:`off`,onChange:e=>u(e.target.value)})}),(0,w.jsx)(m,{id:`t-expires`,label:`Expires`,hint:`Leave blank for no expiry.`,children:(0,w.jsx)(`input`,{id:`t-expires`,type:`date`,value:p,onChange:e=>h(e.target.value)})}),(0,w.jsx)(`div`,{children:(0,w.jsx)(`button`,{type:`submit`,className:`btn`,disabled:S.isPending,children:`Issue token`})})]})]})}export{N as AdministratorsPage};
@@ -1 +1 @@
1
- import{B as e,H as t,n,s as r,v as i,z as a}from"./client-ZHNiMG8H.js";import{D as o}from"./index-uRArh6IJ.js";import{t as s}from"./Time-USlo65Ii.js";import{t as c}from"./CodeBox-Cwh5KHLp.js";var l=t(e(),1),u=a(),d=[{id:`1h`,label:`Last hour`,hours:1},{id:`24h`,label:`Last 24 hours`,hours:24},{id:`7d`,label:`Last 7 days`,hours:168},{id:`30d`,label:`Last 30 days`,hours:720}],f=e=>e.toLocaleString();function p({refusals:e}){let t=Object.entries(e);return t.length?(0,u.jsx)(`span`,{style:{display:`inline-flex`,flexWrap:`wrap`,gap:4},children:t.map(([e,t])=>(0,u.jsx)(`span`,{className:`tag`,title:`${t} call${t===1?``:`s`} failed with ${e}`,children:`${e} × ${t}`},e))}):(0,u.jsx)(`span`,{className:`dim`,children:`none`})}function m({writes:e}){let t=Object.entries(e??{}).filter(([,e])=>e).map(([e,t])=>`${t} ${e}`);return(0,u.jsx)(`div`,{className:`hint`,children:t.length?t.join(` · `):`None.`})}function h({agent:e}){let t=Object.entries(e.queries);return(0,u.jsxs)(`div`,{className:`agent-detail`,children:[(0,u.jsx)(`p`,{className:`sub-h`,children:`Writes asked for`}),(0,u.jsx)(m,{writes:e.writes}),(0,u.jsx)(`p`,{className:`sub-h`,style:{marginTop:12},children:`Saved queries`}),t.length?(0,u.jsx)(`div`,{className:`hint`,children:t.map(([e,t])=>`${e} × ${t}`).join(` · `)}):(0,u.jsx)(`div`,{className:`hint`,children:`None - only ad-hoc SQL.`}),(0,u.jsx)(`p`,{className:`sub-h`,style:{marginTop:12},children:`Most frequent ad-hoc statements`}),e.top_statements.length?e.top_statements.map((e,t)=>(0,u.jsxs)(`div`,{className:`agent-statement`,children:[(0,u.jsx)(`div`,{className:`hint`,children:`${e.count} call${e.count===1?``:`s`} · ${f(e.rows)} rows`+(e.errors?` · ${e.errors} failed`:``)}),e.sql?(0,u.jsx)(c,{sql:e.sql}):(0,u.jsx)(`span`,{className:`mono dim`,children:`sha256 ${e.sql_sha256}`})]},t)):(0,u.jsx)(`div`,{className:`hint`,children:e.adhoc_calls?`Run history keeps no SQL text here (QUERYAPIGATE_HISTORY_ADHOC_SQL is none).`:`No ad-hoc SQL.`})]})}function g(){let[e,t]=(0,l.useState)(`24h`),[a,c]=(0,l.useState)(null),m=d.find(t=>t.id===e).hours,g=i({queryKey:[`usage`,`agents`,e],queryFn:async()=>r(await n.GET(`/api/v1/usage/agents`,{params:{query:{hours:m}}})),retry:!1}),_=g.data?.items??[],v;v=g.isPending?(0,u.jsx)(o,{text:`Loading agents…`}):g.isError?(0,u.jsxs)(`div`,{className:`empty`,children:[(0,u.jsx)(`strong`,{children:`Couldn’t load agent usage`}),(0,u.jsx)(`span`,{children:g.error.message})]}):_.length?(0,u.jsx)(`div`,{style:{overflowX:`auto`},children:(0,u.jsxs)(`table`,{className:`grid`,children:[(0,u.jsx)(`thead`,{children:(0,u.jsx)(`tr`,{children:[`Agent`,`Calls`,`Rows`,`Failed`,`Masked`,`Refused or failed with`,`Last seen`].map(e=>(0,u.jsx)(`th`,{children:e},e))})}),(0,u.jsx)(`tbody`,{children:_.map(e=>(0,u.jsxs)(l.Fragment,{children:[(0,u.jsxs)(`tr`,{children:[(0,u.jsxs)(`td`,{children:[(0,u.jsx)(`button`,{type:`button`,className:`agent-name`,"aria-expanded":a===e.agent,onClick:()=>c(a===e.agent?null:e.agent),children:e.agent}),(0,u.jsx)(`div`,{className:`dim`,style:{fontSize:12},children:e.connections.join(`, `)})]}),(0,u.jsx)(`td`,{className:`num`,children:f(e.calls)}),(0,u.jsx)(`td`,{className:`num`,children:f(e.rows)}),(0,u.jsx)(`td`,{className:`num`,children:f(e.errors)}),(0,u.jsx)(`td`,{className:`num`,children:f(e.masked_calls)}),(0,u.jsx)(`td`,{children:(0,u.jsx)(p,{refusals:e.refusals})}),(0,u.jsx)(`td`,{className:`mono dim`,style:{whiteSpace:`nowrap`},children:(0,u.jsx)(s,{value:e.last_seen,fallback:``})})]}),a===e.agent&&(0,u.jsx)(`tr`,{children:(0,u.jsx)(`td`,{colSpan:7,children:(0,u.jsx)(h,{agent:e})})})]},e.agent))})]})}):(0,u.jsxs)(`div`,{className:`empty`,children:[(0,u.jsx)(`strong`,{children:`No agent called the MCP server in this window`}),(0,u.jsx)(`span`,{children:`Runs over MCP - saved-query tools and execute_sql - appear here, per API key or signed-in user.`})]});let y=[];return g.data&&g.data.sample_rate<1&&y.push(`Run history keeps ${Math.round(g.data.sample_rate*100)}% of successful runs, so successful calls and rows are a sample; failures are all here.`),g.data?.truncated&&y.push(`More runs than one summary reads (100,000) - narrow the window.`),(0,u.jsxs)(u.Fragment,{children:[(0,u.jsxs)(`div`,{className:`page-head`,children:[(0,u.jsxs)(`div`,{className:`titles`,children:[(0,u.jsx)(`h1`,{children:`Agents`}),(0,u.jsx)(`span`,{className:`sub`,children:`What each AI agent did over MCP - per API key or signed-in user, from run history.`})]}),(0,u.jsx)(`span`,{className:`spacer`}),(0,u.jsx)(`button`,{type:`button`,className:`btn`,onClick:()=>void g.refetch(),children:`Refresh`})]}),(0,u.jsxs)(`div`,{className:`panel`,style:{overflow:`hidden`},children:[(0,u.jsxs)(`div`,{className:`panel-bar`,children:[(0,u.jsx)(`select`,{"aria-label":`Window`,value:e,onChange:e=>t(e.target.value),children:d.map(e=>(0,u.jsx)(`option`,{value:e.id,children:e.label},e.id))}),(0,u.jsx)(`span`,{className:`spacer`}),(0,u.jsx)(`span`,{className:`panel-count`,children:g.data?`${_.length} agent${_.length===1?``:`s`}`:``})]}),y.map(e=>(0,u.jsx)(`div`,{className:`hint`,style:{padding:`6px 12px`},children:e},e)),v]})]})}export{g as AgentsPage};
1
+ import{B as e,H as t,n,s as r,v as i,z as a}from"./client-ZHNiMG8H.js";import{D as o}from"./index-Cl93xBjn.js";import{t as s}from"./Time-s5E5qK3s.js";import{t as c}from"./CodeBox-Cwh5KHLp.js";var l=t(e(),1),u=a(),d=[{id:`1h`,label:`Last hour`,hours:1},{id:`24h`,label:`Last 24 hours`,hours:24},{id:`7d`,label:`Last 7 days`,hours:168},{id:`30d`,label:`Last 30 days`,hours:720}],f=e=>e.toLocaleString();function p({refusals:e}){let t=Object.entries(e);return t.length?(0,u.jsx)(`span`,{style:{display:`inline-flex`,flexWrap:`wrap`,gap:4},children:t.map(([e,t])=>(0,u.jsx)(`span`,{className:`tag`,title:`${t} call${t===1?``:`s`} failed with ${e}`,children:`${e} × ${t}`},e))}):(0,u.jsx)(`span`,{className:`dim`,children:`none`})}function m({writes:e}){let t=Object.entries(e??{}).filter(([,e])=>e).map(([e,t])=>`${t} ${e}`);return(0,u.jsx)(`div`,{className:`hint`,children:t.length?t.join(` · `):`None.`})}function h({agent:e}){let t=Object.entries(e.queries);return(0,u.jsxs)(`div`,{className:`agent-detail`,children:[(0,u.jsx)(`p`,{className:`sub-h`,children:`Writes asked for`}),(0,u.jsx)(m,{writes:e.writes}),(0,u.jsx)(`p`,{className:`sub-h`,style:{marginTop:12},children:`Saved queries`}),t.length?(0,u.jsx)(`div`,{className:`hint`,children:t.map(([e,t])=>`${e} × ${t}`).join(` · `)}):(0,u.jsx)(`div`,{className:`hint`,children:`None - only ad-hoc SQL.`}),(0,u.jsx)(`p`,{className:`sub-h`,style:{marginTop:12},children:`Most frequent ad-hoc statements`}),e.top_statements.length?e.top_statements.map((e,t)=>(0,u.jsxs)(`div`,{className:`agent-statement`,children:[(0,u.jsx)(`div`,{className:`hint`,children:`${e.count} call${e.count===1?``:`s`} · ${f(e.rows)} rows`+(e.errors?` · ${e.errors} failed`:``)}),e.sql?(0,u.jsx)(c,{sql:e.sql}):(0,u.jsx)(`span`,{className:`mono dim`,children:`sha256 ${e.sql_sha256}`})]},t)):(0,u.jsx)(`div`,{className:`hint`,children:e.adhoc_calls?`Run history keeps no SQL text here (QUERYAPIGATE_HISTORY_ADHOC_SQL is none).`:`No ad-hoc SQL.`})]})}function g(){let[e,t]=(0,l.useState)(`24h`),[a,c]=(0,l.useState)(null),m=d.find(t=>t.id===e).hours,g=i({queryKey:[`usage`,`agents`,e],queryFn:async()=>r(await n.GET(`/api/v1/usage/agents`,{params:{query:{hours:m}}})),retry:!1}),_=g.data?.items??[],v;v=g.isPending?(0,u.jsx)(o,{text:`Loading agents…`}):g.isError?(0,u.jsxs)(`div`,{className:`empty`,children:[(0,u.jsx)(`strong`,{children:`Couldn’t load agent usage`}),(0,u.jsx)(`span`,{children:g.error.message})]}):_.length?(0,u.jsx)(`div`,{style:{overflowX:`auto`},children:(0,u.jsxs)(`table`,{className:`grid`,children:[(0,u.jsx)(`thead`,{children:(0,u.jsx)(`tr`,{children:[`Agent`,`Calls`,`Rows`,`Failed`,`Masked`,`Refused or failed with`,`Last seen`].map(e=>(0,u.jsx)(`th`,{children:e},e))})}),(0,u.jsx)(`tbody`,{children:_.map(e=>(0,u.jsxs)(l.Fragment,{children:[(0,u.jsxs)(`tr`,{children:[(0,u.jsxs)(`td`,{children:[(0,u.jsx)(`button`,{type:`button`,className:`agent-name`,"aria-expanded":a===e.agent,onClick:()=>c(a===e.agent?null:e.agent),children:e.agent}),(0,u.jsx)(`div`,{className:`dim`,style:{fontSize:12},children:e.connections.join(`, `)})]}),(0,u.jsx)(`td`,{className:`num`,children:f(e.calls)}),(0,u.jsx)(`td`,{className:`num`,children:f(e.rows)}),(0,u.jsx)(`td`,{className:`num`,children:f(e.errors)}),(0,u.jsx)(`td`,{className:`num`,children:f(e.masked_calls)}),(0,u.jsx)(`td`,{children:(0,u.jsx)(p,{refusals:e.refusals})}),(0,u.jsx)(`td`,{className:`mono dim`,style:{whiteSpace:`nowrap`},children:(0,u.jsx)(s,{value:e.last_seen,fallback:``})})]}),a===e.agent&&(0,u.jsx)(`tr`,{children:(0,u.jsx)(`td`,{colSpan:7,children:(0,u.jsx)(h,{agent:e})})})]},e.agent))})]})}):(0,u.jsxs)(`div`,{className:`empty`,children:[(0,u.jsx)(`strong`,{children:`No agent called the MCP server in this window`}),(0,u.jsx)(`span`,{children:`Runs over MCP - saved-query tools and execute_sql - appear here, per API key or signed-in user.`})]});let y=[];return g.data&&g.data.sample_rate<1&&y.push(`Run history keeps ${Math.round(g.data.sample_rate*100)}% of successful runs, so successful calls and rows are a sample; failures are all here.`),g.data?.truncated&&y.push(`More runs than one summary reads (100,000) - narrow the window.`),(0,u.jsxs)(u.Fragment,{children:[(0,u.jsxs)(`div`,{className:`page-head`,children:[(0,u.jsxs)(`div`,{className:`titles`,children:[(0,u.jsx)(`h1`,{children:`Agents`}),(0,u.jsx)(`span`,{className:`sub`,children:`What each AI agent did over MCP - per API key or signed-in user, from run history.`})]}),(0,u.jsx)(`span`,{className:`spacer`}),(0,u.jsx)(`button`,{type:`button`,className:`btn`,onClick:()=>void g.refetch(),children:`Refresh`})]}),(0,u.jsxs)(`div`,{className:`panel`,style:{overflow:`hidden`},children:[(0,u.jsxs)(`div`,{className:`panel-bar`,children:[(0,u.jsx)(`select`,{"aria-label":`Window`,value:e,onChange:e=>t(e.target.value),children:d.map(e=>(0,u.jsx)(`option`,{value:e.id,children:e.label},e.id))}),(0,u.jsx)(`span`,{className:`spacer`}),(0,u.jsx)(`span`,{className:`panel-count`,children:g.data?`${_.length} agent${_.length===1?``:`s`}`:``})]}),y.map(e=>(0,u.jsx)(`div`,{className:`hint`,style:{padding:`6px 12px`},children:e},e)),v]})]})}export{g as AgentsPage};
@@ -1 +1 @@
1
- import{B as e,H as t,z as n}from"./client-ZHNiMG8H.js";import{C as r,D as i,M as a,N as o,S as s,x as c,y as l}from"./index-uRArh6IJ.js";import{t as u}from"./Time-USlo65Ii.js";import{n as d}from"./state-O13cl7cO.js";var f=t(e(),1),p=n(),m=[{id:`critical`,label:`Critical`},{id:`warning`,label:`Warning`},{id:`info`,label:`For your information`}],h={key:`Open API keys`,query:`Open query`,connection:`Open connections`,settings:`Open settings`,export:`Open exports`,agent:`Open agents`,approvals:`Open approvals`,audit:`Open audit log`};function g(){let e=a();return t=>{t.type===`query`?e(`/queries/${encodeURIComponent(t.name)}`):t.type===`key`?e(`/api-keys`):t.type===`connection`?e(`/connections`):t.type===`export`?e(`/exports`):t.type===`agent`?e(`/agents`):t.type===`approvals`?e(`/approvals`):t.type===`audit`?e(`/audit-log`):(d(t.name),e(`/settings`))}}function _({alert:e,dismissed:t}){let n=g();return(0,p.jsxs)(`div`,{className:`alert-row `+e.severity,"data-alert":e.id,children:[(0,p.jsx)(`span`,{className:`sev `+e.severity,children:e.severity}),(0,p.jsxs)(`div`,{className:`alert-what`,children:[(0,p.jsx)(`strong`,{children:e.title}),(0,p.jsx)(`span`,{className:`alert-detail`,children:e.detail}),e.since&&!e.detail.includes(e.since)&&(0,p.jsxs)(`span`,{className:`alert-since`,children:[e.kind===`key_expiring`?`Expires `:`Since `,(0,p.jsx)(u,{value:e.since})]})]}),(0,p.jsxs)(`div`,{className:`actions`,children:[e.target&&(0,p.jsx)(`button`,{type:`button`,className:`btn sm outlined`,onClick:()=>n(e.target),children:h[e.target.type]??`Open`}),t?(0,p.jsx)(`button`,{type:`button`,className:`btn ghost sm`,onClick:()=>s(e.id),children:`Restore`}):(0,p.jsx)(`button`,{type:`button`,className:`btn ghost sm`,title:`Hide it in this browser until it clears`,onClick:()=>c(e.id),children:`Dismiss`})]})]})}function v({alerts:e}){return(0,p.jsx)(p.Fragment,{children:m.map(({id:t,label:n})=>{let r=e.filter(e=>e.severity===t);return r.length?(0,p.jsxs)(`div`,{className:`panel alerts-group`,id:`alerts-`+t,children:[(0,p.jsxs)(`h2`,{children:[n,` `,(0,p.jsx)(`span`,{className:`count`,children:r.length})]}),r.map(e=>(0,p.jsx)(_,{alert:e},e.id))]},t):null})})}function y(){let e=a(),t=o(),n=l.find(e=>e.id===t.tab),{query:s,active:c,dismissed:d}=r(),[m,h]=(0,f.useState)(!1),g=e=>!n||n.kinds.includes(e.kind),y=c.filter(g),b=d.filter(g),x=e=>c.filter(t=>!e||e.includes(t.kind)),S;S=s.isPending?(0,p.jsx)(i,{text:`Checking…`}):s.isError?(0,p.jsx)(`div`,{className:`panel`,children:(0,p.jsxs)(`div`,{className:`empty`,children:[(0,p.jsx)(`strong`,{children:`Couldn’t check for alerts`}),(0,p.jsx)(`span`,{children:s.error.message}),(0,p.jsx)(`button`,{type:`button`,className:`btn sm`,onClick:()=>void s.refetch(),children:`Retry`})]})}):(0,p.jsxs)(p.Fragment,{children:[n&&(0,p.jsx)(`p`,{className:`alerts-about`,children:n.about}),y.length?(0,p.jsx)(v,{alerts:y}):(0,p.jsx)(`div`,{className:`panel`,id:`alerts-clear`,children:(0,p.jsxs)(`div`,{className:`empty`,children:[(0,p.jsx)(`strong`,{children:`All clear`}),(0,p.jsx)(`span`,{children:n?`Nothing to report from this check right now.`:`Nothing needs your attention right now.`})]})}),b.length>0&&(0,p.jsxs)(`div`,{className:`alerts-dismissed`,children:[(0,p.jsx)(`button`,{type:`button`,className:`btn ghost sm`,onClick:()=>h(e=>!e),children:`${b.length} dismissed in this browser · ${m?`Hide`:`Show`}`}),m&&(0,p.jsx)(`div`,{className:`panel alerts-group`,id:`alerts-dismissed`,children:b.map(e=>(0,p.jsx)(_,{alert:e,dismissed:!0},e.id))})]})]});let C=(t,r,i)=>(0,p.jsxs)(`button`,{type:`button`,role:`tab`,"aria-selected":(n?.id??`all`)===t,className:`minitab`+((n?.id??`all`)===t?` active`:``),onClick:()=>e(t===`all`?`/alerts`:`/alerts/`+t),children:[r,s.data?(0,p.jsx)(`span`,{className:`n`+(i.length?` on`:``)+(i.some(e=>e.severity===`critical`)?` critical`:``),children:i.length}):null]},t);return(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(`div`,{className:`page-head`,children:[(0,p.jsxs)(`div`,{className:`titles`,children:[(0,p.jsx)(`h1`,{children:`Alerts`}),(0,p.jsxs)(`span`,{className:`sub`,children:[`What needs attention now, checked every minute. Each clears by itself once its cause does.`,s.data?(0,p.jsxs)(p.Fragment,{children:[` Last checked `,(0,p.jsx)(u,{value:s.data.checked_at}),`.`]}):null]})]}),(0,p.jsx)(`span`,{className:`spacer`}),(0,p.jsx)(`button`,{id:`refresh-alerts`,type:`button`,className:`btn`,onClick:()=>void s.refetch(),children:`Check now`})]}),(0,p.jsxs)(`div`,{className:`minitabs alerts-tabs`,role:`tablist`,"aria-label":`Checks`,children:[C(`all`,`All`,x()),l.map(e=>C(e.id,e.label,x(e.kinds)))]}),S]})}export{y as AlertsPage};
1
+ import{B as e,H as t,z as n}from"./client-ZHNiMG8H.js";import{C as r,D as i,M as a,N as o,S as s,x as c,y as l}from"./index-Cl93xBjn.js";import{t as u}from"./Time-s5E5qK3s.js";import{n as d}from"./state-O13cl7cO.js";var f=t(e(),1),p=n(),m=[{id:`critical`,label:`Critical`},{id:`warning`,label:`Warning`},{id:`info`,label:`For your information`}],h={key:`Open API keys`,query:`Open query`,connection:`Open connections`,settings:`Open settings`,export:`Open exports`,agent:`Open agents`,approvals:`Open approvals`,audit:`Open audit log`};function g(){let e=a();return t=>{t.type===`query`?e(`/queries/${encodeURIComponent(t.name)}`):t.type===`key`?e(`/api-keys`):t.type===`connection`?e(`/connections`):t.type===`export`?e(`/exports`):t.type===`agent`?e(`/agents`):t.type===`approvals`?e(`/approvals`):t.type===`audit`?e(`/audit-log`):(d(t.name),e(`/settings`))}}function _({alert:e,dismissed:t}){let n=g();return(0,p.jsxs)(`div`,{className:`alert-row `+e.severity,"data-alert":e.id,children:[(0,p.jsx)(`span`,{className:`sev `+e.severity,children:e.severity}),(0,p.jsxs)(`div`,{className:`alert-what`,children:[(0,p.jsx)(`strong`,{children:e.title}),(0,p.jsx)(`span`,{className:`alert-detail`,children:e.detail}),e.since&&!e.detail.includes(e.since)&&(0,p.jsxs)(`span`,{className:`alert-since`,children:[e.kind===`key_expiring`?`Expires `:`Since `,(0,p.jsx)(u,{value:e.since})]})]}),(0,p.jsxs)(`div`,{className:`actions`,children:[e.target&&(0,p.jsx)(`button`,{type:`button`,className:`btn sm outlined`,onClick:()=>n(e.target),children:h[e.target.type]??`Open`}),t?(0,p.jsx)(`button`,{type:`button`,className:`btn ghost sm`,onClick:()=>s(e.id),children:`Restore`}):(0,p.jsx)(`button`,{type:`button`,className:`btn ghost sm`,title:`Hide it in this browser until it clears`,onClick:()=>c(e.id),children:`Dismiss`})]})]})}function v({alerts:e}){return(0,p.jsx)(p.Fragment,{children:m.map(({id:t,label:n})=>{let r=e.filter(e=>e.severity===t);return r.length?(0,p.jsxs)(`div`,{className:`panel alerts-group`,id:`alerts-`+t,children:[(0,p.jsxs)(`h2`,{children:[n,` `,(0,p.jsx)(`span`,{className:`count`,children:r.length})]}),r.map(e=>(0,p.jsx)(_,{alert:e},e.id))]},t):null})})}function y(){let e=a(),t=o(),n=l.find(e=>e.id===t.tab),{query:s,active:c,dismissed:d}=r(),[m,h]=(0,f.useState)(!1),g=e=>!n||n.kinds.includes(e.kind),y=c.filter(g),b=d.filter(g),x=e=>c.filter(t=>!e||e.includes(t.kind)),S;S=s.isPending?(0,p.jsx)(i,{text:`Checking…`}):s.isError?(0,p.jsx)(`div`,{className:`panel`,children:(0,p.jsxs)(`div`,{className:`empty`,children:[(0,p.jsx)(`strong`,{children:`Couldn’t check for alerts`}),(0,p.jsx)(`span`,{children:s.error.message}),(0,p.jsx)(`button`,{type:`button`,className:`btn sm`,onClick:()=>void s.refetch(),children:`Retry`})]})}):(0,p.jsxs)(p.Fragment,{children:[n&&(0,p.jsx)(`p`,{className:`alerts-about`,children:n.about}),y.length?(0,p.jsx)(v,{alerts:y}):(0,p.jsx)(`div`,{className:`panel`,id:`alerts-clear`,children:(0,p.jsxs)(`div`,{className:`empty`,children:[(0,p.jsx)(`strong`,{children:`All clear`}),(0,p.jsx)(`span`,{children:n?`Nothing to report from this check right now.`:`Nothing needs your attention right now.`})]})}),b.length>0&&(0,p.jsxs)(`div`,{className:`alerts-dismissed`,children:[(0,p.jsx)(`button`,{type:`button`,className:`btn ghost sm`,onClick:()=>h(e=>!e),children:`${b.length} dismissed in this browser · ${m?`Hide`:`Show`}`}),m&&(0,p.jsx)(`div`,{className:`panel alerts-group`,id:`alerts-dismissed`,children:b.map(e=>(0,p.jsx)(_,{alert:e,dismissed:!0},e.id))})]})]});let C=(t,r,i)=>(0,p.jsxs)(`button`,{type:`button`,role:`tab`,"aria-selected":(n?.id??`all`)===t,className:`minitab`+((n?.id??`all`)===t?` active`:``),onClick:()=>e(t===`all`?`/alerts`:`/alerts/`+t),children:[r,s.data?(0,p.jsx)(`span`,{className:`n`+(i.length?` on`:``)+(i.some(e=>e.severity===`critical`)?` critical`:``),children:i.length}):null]},t);return(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(`div`,{className:`page-head`,children:[(0,p.jsxs)(`div`,{className:`titles`,children:[(0,p.jsx)(`h1`,{children:`Alerts`}),(0,p.jsxs)(`span`,{className:`sub`,children:[`What needs attention now, checked every minute. Each clears by itself once its cause does.`,s.data?(0,p.jsxs)(p.Fragment,{children:[` Last checked `,(0,p.jsx)(u,{value:s.data.checked_at}),`.`]}):null]})]}),(0,p.jsx)(`span`,{className:`spacer`}),(0,p.jsx)(`button`,{id:`refresh-alerts`,type:`button`,className:`btn`,onClick:()=>void s.refetch(),children:`Check now`})]}),(0,p.jsxs)(`div`,{className:`minitabs alerts-tabs`,role:`tablist`,"aria-label":`Checks`,children:[C(`all`,`All`,x()),l.map(e=>C(e.id,e.label,x(e.kinds)))]}),S]})}export{y as AlertsPage};