queryapigate 0.28.0__tar.gz → 0.30.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 (221) hide show
  1. {queryapigate-0.28.0/queryapigate.egg-info → queryapigate-0.30.0}/PKG-INFO +7 -5
  2. {queryapigate-0.28.0 → queryapigate-0.30.0}/README.md +2 -1
  3. {queryapigate-0.28.0 → queryapigate-0.30.0}/pyproject.toml +6 -4
  4. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/__init__.py +1 -1
  5. queryapigate-0.30.0/queryapigate/accesslog.py +29 -0
  6. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/adminroles.py +3 -1
  7. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/app.py +39 -7
  8. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/cli.py +34 -0
  9. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/config.py +49 -3
  10. queryapigate-0.28.0/queryapigate/console_dist/assets/AccessMapPage-DIu936ZH.js → queryapigate-0.30.0/queryapigate/console_dist/assets/AccessMapPage-CU9b_-fo.js +3 -3
  11. queryapigate-0.28.0/queryapigate/console_dist/assets/AdministratorsPage-B2IYmP_x.js → queryapigate-0.30.0/queryapigate/console_dist/assets/AdministratorsPage-Bwjtg36U.js +1 -1
  12. queryapigate-0.30.0/queryapigate/console_dist/assets/AgentsPage-BmXNCpQ0.js +1 -0
  13. queryapigate-0.28.0/queryapigate/console_dist/assets/AlertsPage-CzC-qhMh.js → queryapigate-0.30.0/queryapigate/console_dist/assets/AlertsPage-C7tvw3Hr.js +1 -1
  14. queryapigate-0.28.0/queryapigate/console_dist/assets/ApiKeysPage-C0VW5NrS.js → queryapigate-0.30.0/queryapigate/console_dist/assets/ApiKeysPage-B-TKdW-V.js +1 -1
  15. queryapigate-0.30.0/queryapigate/console_dist/assets/ApprovalsPage-Bqsr5dHt.js +1 -0
  16. queryapigate-0.28.0/queryapigate/console_dist/assets/AuditLogPage-GdbYovis.js → queryapigate-0.30.0/queryapigate/console_dist/assets/AuditLogPage-hAKiebHz.js +1 -1
  17. queryapigate-0.28.0/queryapigate/console_dist/assets/CachingPage-4stBdbWQ.js → queryapigate-0.30.0/queryapigate/console_dist/assets/CachingPage-WFUpWs_W.js +1 -1
  18. queryapigate-0.28.0/queryapigate/console_dist/assets/CodeBox-DRfv6yXf.js → queryapigate-0.30.0/queryapigate/console_dist/assets/CodeBox-DuaKuoNl.js +1 -1
  19. queryapigate-0.28.0/queryapigate/console_dist/assets/ConnectionsPage-XYubobx_.js → queryapigate-0.30.0/queryapigate/console_dist/assets/ConnectionsPage-DwdiiWsf.js +1 -1
  20. queryapigate-0.28.0/queryapigate/console_dist/assets/DesignerPage-B1Sc83US.js → queryapigate-0.30.0/queryapigate/console_dist/assets/DesignerPage-BfhC69pJ.js +1 -1
  21. queryapigate-0.28.0/queryapigate/console_dist/assets/DestinationsPage-C30f018_.js → queryapigate-0.30.0/queryapigate/console_dist/assets/DestinationsPage-yaGxeOXB.js +1 -1
  22. queryapigate-0.28.0/queryapigate/console_dist/assets/ExportsPage-BMRj3VOx.js → queryapigate-0.30.0/queryapigate/console_dist/assets/ExportsPage-DlkNfYMZ.js +1 -1
  23. queryapigate-0.28.0/queryapigate/console_dist/assets/HelpPage-DCkoTqa5.js → queryapigate-0.30.0/queryapigate/console_dist/assets/HelpPage-Bey5KI_-.js +2 -2
  24. queryapigate-0.28.0/queryapigate/console_dist/assets/HomePage-CcpGDBK_.js → queryapigate-0.30.0/queryapigate/console_dist/assets/HomePage-DeF8Zaq6.js +2 -2
  25. queryapigate-0.28.0/queryapigate/console_dist/assets/Managed-CNWDZi6e.js → queryapigate-0.30.0/queryapigate/console_dist/assets/Managed-CQVNTWrU.js +1 -1
  26. queryapigate-0.28.0/queryapigate/console_dist/assets/MetricsPage-Dxdebop4.js → queryapigate-0.30.0/queryapigate/console_dist/assets/MetricsPage-DqeoN2tZ.js +1 -1
  27. queryapigate-0.28.0/queryapigate/console_dist/assets/RepositoryPage-DmazL70u.js → queryapigate-0.30.0/queryapigate/console_dist/assets/RepositoryPage-QqouaqLm.js +1 -1
  28. queryapigate-0.28.0/queryapigate/console_dist/assets/Results-BdoTFMC_.js → queryapigate-0.30.0/queryapigate/console_dist/assets/Results-BhwS6Q_T.js +1 -1
  29. queryapigate-0.28.0/queryapigate/console_dist/assets/RolesPage-Dn-c3szF.js → queryapigate-0.30.0/queryapigate/console_dist/assets/RolesPage-DhkAkZ1v.js +1 -1
  30. queryapigate-0.28.0/queryapigate/console_dist/assets/SchemaBrowser-mmFZRlm_.js → queryapigate-0.30.0/queryapigate/console_dist/assets/SchemaBrowser-DQ0mi4Vm.js +1 -1
  31. queryapigate-0.30.0/queryapigate/console_dist/assets/SettingsPage-4T0vN6dU.js +3 -0
  32. queryapigate-0.28.0/queryapigate/console_dist/assets/StatTile-DnMgkKp4.js → queryapigate-0.30.0/queryapigate/console_dist/assets/StatTile-DHKvOsXg.js +1 -1
  33. queryapigate-0.28.0/queryapigate/console_dist/assets/Time-CZQinVk_.js → queryapigate-0.30.0/queryapigate/console_dist/assets/Time-KeJDv2te.js +1 -1
  34. queryapigate-0.30.0/queryapigate/console_dist/assets/forms-StJt-xsd.js +1 -0
  35. queryapigate-0.28.0/queryapigate/console_dist/assets/forms-BlZHx8-q.js → queryapigate-0.30.0/queryapigate/console_dist/assets/forms-bSOovYin.js +1 -1
  36. queryapigate-0.28.0/queryapigate/console_dist/assets/icons-CDledy67.js → queryapigate-0.30.0/queryapigate/console_dist/assets/icons-CeIi0TUG.js +1 -1
  37. queryapigate-0.28.0/queryapigate/console_dist/assets/index-C7ega31V.js → queryapigate-0.30.0/queryapigate/console_dist/assets/index-DvCopLMA.js +2 -2
  38. queryapigate-0.28.0/queryapigate/console_dist/assets/maskRules-D7QV7oGO.js → queryapigate-0.30.0/queryapigate/console_dist/assets/maskRules-9C_A2JIu.js +1 -1
  39. queryapigate-0.28.0/queryapigate/console_dist/assets/reach-C1r9_MqV.js → queryapigate-0.30.0/queryapigate/console_dist/assets/reach-DWK2PD5I.js +1 -1
  40. queryapigate-0.28.0/queryapigate/console_dist/assets/shared-DLUzWeB2.js → queryapigate-0.30.0/queryapigate/console_dist/assets/shared-rt98BmTW.js +1 -1
  41. queryapigate-0.28.0/queryapigate/console_dist/assets/useMutation-Df0q777p.js → queryapigate-0.30.0/queryapigate/console_dist/assets/useMutation-BcyO6Hjg.js +1 -1
  42. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/console_dist/index.html +1 -1
  43. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/engine.py +13 -4
  44. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/experimental.py +3 -57
  45. queryapigate-0.30.0/queryapigate/gunicorn_conf.py +6 -0
  46. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/history.py +25 -0
  47. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/mcp_server.py +5 -3
  48. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/metrics.py +32 -0
  49. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/quotas.py +29 -1
  50. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/secretrefs.py +36 -6
  51. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/sso.py +4 -2
  52. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/v1.py +8 -0
  53. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/v1_spec.py +16 -3
  54. {queryapigate-0.28.0 → queryapigate-0.30.0/queryapigate.egg-info}/PKG-INFO +7 -5
  55. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate.egg-info/SOURCES.txt +37 -33
  56. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate.egg-info/requires.txt +4 -3
  57. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_agent_writes.py +3 -1
  58. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_configcode.py +85 -0
  59. queryapigate-0.30.0/tests/test_data_protection.py +191 -0
  60. queryapigate-0.30.0/tests/test_dependency_floors.py +67 -0
  61. queryapigate-0.30.0/tests/test_docs_site.py +36 -0
  62. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_experimental.py +17 -1
  63. queryapigate-0.30.0/tests/test_fips.py +90 -0
  64. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_helm.py +29 -4
  65. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_mcp_oauth.py +45 -0
  66. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_observability.py +39 -1
  67. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_publishing.py +2 -1
  68. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_secretrefs.py +70 -0
  69. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_settings.py +1 -1
  70. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_sso.py +1 -0
  71. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_upgrades.py +2 -0
  72. queryapigate-0.28.0/queryapigate/console_dist/assets/AgentsPage-toV9AXCc.js +0 -1
  73. queryapigate-0.28.0/queryapigate/console_dist/assets/ApprovalsPage-eyGXo0ky.js +0 -1
  74. queryapigate-0.28.0/queryapigate/console_dist/assets/Experimental-BD3aGuwW.js +0 -1
  75. queryapigate-0.28.0/queryapigate/console_dist/assets/SettingsPage-Bi6cE77i.js +0 -3
  76. queryapigate-0.28.0/queryapigate/console_dist/assets/forms-qgM6Rab7.js +0 -1
  77. queryapigate-0.28.0/tests/test_dependency_floors.py +0 -41
  78. {queryapigate-0.28.0 → queryapigate-0.30.0}/LICENSE +0 -0
  79. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/__main__.py +0 -0
  80. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/admins.py +0 -0
  81. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/agentwrites.py +0 -0
  82. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/alerts.py +0 -0
  83. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/apikeys.py +0 -0
  84. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/auditchain.py +0 -0
  85. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/auditstream.py +0 -0
  86. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/aws.py +0 -0
  87. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/broadcast.py +0 -0
  88. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/bundle.py +0 -0
  89. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/cache.py +0 -0
  90. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/collection_admin.py +0 -0
  91. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/configcode.py +0 -0
  92. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/console.py +0 -0
  93. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/console_dist/assets/index-BLd0t6vZ.css +0 -0
  94. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/console_dist/assets/state-O13cl7cO.js +0 -0
  95. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/console_dist/favicon.svg +0 -0
  96. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/cors.py +0 -0
  97. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/databases.py +0 -0
  98. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/db.py +0 -0
  99. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/definitions.py +0 -0
  100. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/deprecations.py +0 -0
  101. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/descriptions.py +0 -0
  102. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/destinations.py +0 -0
  103. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/duckfiles.py +0 -0
  104. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/errors.py +0 -0
  105. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/events.py +0 -0
  106. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/examples.py +0 -0
  107. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/exporting.py +0 -0
  108. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/exports.py +0 -0
  109. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/formats.py +0 -0
  110. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/governance.py +0 -0
  111. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/instances.py +0 -0
  112. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/jwtauth.py +0 -0
  113. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/lib/h2-2.2.224.jar +0 -0
  114. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/limits.py +0 -0
  115. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/logging_setup.py +0 -0
  116. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/masking.py +0 -0
  117. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/mongotools.py +0 -0
  118. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/oidc.py +0 -0
  119. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/openapi.py +0 -0
  120. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/params.py +0 -0
  121. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/parquet.py +0 -0
  122. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/pool.py +0 -0
  123. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/postman.py +0 -0
  124. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/ratelimit.py +0 -0
  125. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/rediscache.py +0 -0
  126. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/rekey.py +0 -0
  127. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/runners.py +0 -0
  128. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/schema.py +0 -0
  129. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/secheaders.py +0 -0
  130. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/__init__.py +0 -0
  131. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/access.py +0 -0
  132. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/administrators.py +0 -0
  133. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/audit.py +0 -0
  134. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/collections.py +0 -0
  135. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/connections.py +0 -0
  136. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/descriptions.py +0 -0
  137. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/destinations.py +0 -0
  138. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/exports.py +0 -0
  139. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/mcp.py +0 -0
  140. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/queries.py +0 -0
  141. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/sqlflow.py +0 -0
  142. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/sqltools.py +0 -0
  143. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/store.py +0 -0
  144. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/tableguard.py +0 -0
  145. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/tracing.py +0 -0
  146. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/usage.py +0 -0
  147. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/vault.py +0 -0
  148. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate.egg-info/dependency_links.txt +0 -0
  149. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate.egg-info/entry_points.txt +0 -0
  150. {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate.egg-info/top_level.txt +0 -0
  151. {queryapigate-0.28.0 → queryapigate-0.30.0}/setup.cfg +0 -0
  152. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_adhoc_history.py +0 -0
  153. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_adminroles.py +0 -0
  154. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_admins.py +0 -0
  155. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_alerts.py +0 -0
  156. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_api_administrators.py +0 -0
  157. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_api_keys.py +0 -0
  158. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_api_v1.py +0 -0
  159. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_audit_chain.py +0 -0
  160. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_audit_log.py +0 -0
  161. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_audit_stream.py +0 -0
  162. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_aws.py +0 -0
  163. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_backup_restore.py +0 -0
  164. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_broadcast.py +0 -0
  165. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_cli.py +0 -0
  166. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_collection_bundle.py +0 -0
  167. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_collection_postman.py +0 -0
  168. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_collections.py +0 -0
  169. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_connection_encryption.py +0 -0
  170. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_console.py +0 -0
  171. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_cors_and_rate_limit.py +0 -0
  172. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_databases.py +0 -0
  173. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_db.py +0 -0
  174. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_deprecations.py +0 -0
  175. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_descriptions.py +0 -0
  176. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_destinations.py +0 -0
  177. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_duckdb.py +0 -0
  178. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_duckdb_files.py +0 -0
  179. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_errors.py +0 -0
  180. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_events.py +0 -0
  181. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_events_server.py +0 -0
  182. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_examples.py +0 -0
  183. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_export_security.py +0 -0
  184. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_exporting.py +0 -0
  185. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_exports.py +0 -0
  186. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_history.py +0 -0
  187. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_howto.py +0 -0
  188. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_instances.py +0 -0
  189. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_integration.py +0 -0
  190. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_jdbc.py +0 -0
  191. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_jwt.py +0 -0
  192. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_key_rotation.py +0 -0
  193. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_limits.py +0 -0
  194. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_masking.py +0 -0
  195. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_masking_requests.py +0 -0
  196. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_mcp.py +0 -0
  197. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_mcp_keycloak.py +0 -0
  198. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_mongotools.py +0 -0
  199. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_multinode.py +0 -0
  200. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_oidc.py +0 -0
  201. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_params.py +0 -0
  202. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_parquet.py +0 -0
  203. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_pool.py +0 -0
  204. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_query_window.py +0 -0
  205. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_queryapigate.py +0 -0
  206. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_quotas.py +0 -0
  207. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_rekey.py +0 -0
  208. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_release_check.py +0 -0
  209. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_response_cache.py +0 -0
  210. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_roles.py +0 -0
  211. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_rolling_upgrade.py +0 -0
  212. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_security_headers.py +0 -0
  213. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_shared_rate_limit.py +0 -0
  214. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_sql_guard_fuzz.py +0 -0
  215. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_sql_guard_writes.py +0 -0
  216. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_sqlflow.py +0 -0
  217. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_sso_keycloak.py +0 -0
  218. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_tableguard.py +0 -0
  219. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_tracing.py +0 -0
  220. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_usage.py +0 -0
  221. {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_vault.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: queryapigate
3
- Version: 0.28.0
3
+ Version: 0.30.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: FSL-1.1-MIT
@@ -25,8 +25,9 @@ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
25
25
  Requires-Python: >=3.11
26
26
  Description-Content-Type: text/markdown
27
27
  License-File: LICENSE
28
- Requires-Dist: Flask>=2.2
29
- Requires-Dist: PyYAML>=6.0
28
+ Requires-Dist: Flask>=3.0
29
+ Requires-Dist: Werkzeug>=3.0
30
+ Requires-Dist: PyYAML>=6.0.1
30
31
  Requires-Dist: openpyxl>=3.1
31
32
  Provides-Extra: mysql
32
33
  Requires-Dist: mysql-connector-python>=8.0; extra == "mysql"
@@ -48,7 +49,7 @@ Requires-Dist: cryptography>=41; extra == "encryption"
48
49
  Provides-Extra: redis
49
50
  Requires-Dist: redis>=5; extra == "redis"
50
51
  Provides-Extra: mcp
51
- Requires-Dist: mcp<2,>=1.10; extra == "mcp"
52
+ Requires-Dist: mcp<2,>=1.21.1; extra == "mcp"
52
53
  Provides-Extra: jwt
53
54
  Requires-Dist: PyJWT[crypto]>=2.14; extra == "jwt"
54
55
  Provides-Extra: all
@@ -724,7 +725,8 @@ QueryAPIGate runs whatever SQL it is given against your databases, so it ships l
724
725
  normal reverse-proxy/TLS-termination setup rather than exposing it directly.
725
726
 
726
727
  See [documentation/THREAT_MODEL.md](documentation/THREAT_MODEL.md) for the reasoning behind these controls -
727
- what's actually defended against and where it stops - and [SECURITY.md](SECURITY.md) to report a vulnerability.
728
+ what's actually defended against and where it stops - [documentation/DATA_PROTECTION.md](documentation/DATA_PROTECTION.md)
729
+ for what is kept, where and for how long, and [SECURITY.md](SECURITY.md) to report a vulnerability.
728
730
 
729
731
  ### Calling the API from a browser
730
732
 
@@ -647,7 +647,8 @@ QueryAPIGate runs whatever SQL it is given against your databases, so it ships l
647
647
  normal reverse-proxy/TLS-termination setup rather than exposing it directly.
648
648
 
649
649
  See [documentation/THREAT_MODEL.md](documentation/THREAT_MODEL.md) for the reasoning behind these controls -
650
- what's actually defended against and where it stops - and [SECURITY.md](SECURITY.md) to report a vulnerability.
650
+ what's actually defended against and where it stops - [documentation/DATA_PROTECTION.md](documentation/DATA_PROTECTION.md)
651
+ for what is kept, where and for how long, and [SECURITY.md](SECURITY.md) to report a vulnerability.
651
652
 
652
653
  ### Calling the API from a browser
653
654
 
@@ -26,7 +26,9 @@ classifiers = [
26
26
  "Topic :: Database",
27
27
  "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
28
28
  ]
29
- dependencies = ["Flask>=2.2", "PyYAML>=6.0", "openpyxl>=3.1"]
29
+ # Werkzeug is imported directly (ProxyFix, ClosingIterator), so it has its own floor. Flask 2.x can't be imported beside
30
+ # Werkzeug 3, which pip installs with it; PyYAML 6.0 has no wheel for Python 3.12 and later, and doesn't build there.
31
+ dependencies = ["Flask>=3.0", "Werkzeug>=3.0", "PyYAML>=6.0.1", "openpyxl>=3.1"]
30
32
 
31
33
  [project.optional-dependencies]
32
34
  mysql = ["mysql-connector-python>=8.0"]
@@ -38,9 +40,9 @@ mongo = ["pymongo>=4.0"]
38
40
  flow = ["sqlglot>=25.2,<26"] # the Access tab's tables and joins (sqlflow.py) and column masking's tracing (masking.py)
39
41
  encryption = ["cryptography>=41"] # only needed when QUERYAPIGATE_SECRET_KEY is set - see store.py's _get_fernet()
40
42
  redis = ["redis>=5"] # only needed when QUERYAPIGATE_REDIS_URL is set - see rediscache.py
41
- mcp = ["mcp>=1.10,<2"] # only needed for `queryapigate mcp` - see mcp_server.py. 1.10 is the floor because
42
- # that's where outputSchema/structuredContent support landed in the SDK (1.9 doesn't
43
- # have it).
43
+ mcp = ["mcp>=1.21.1,<2"] # only needed for `queryapigate mcp` - see mcp_server.py. Structured tool output arrived
44
+ # in 1.10, but nothing before 1.21.1 can be imported beside the pydantic pip installs
45
+ # with it (a private pydantic function they used is gone).
44
46
  jwt = ["PyJWT[crypto]>=2.14"] # only needed when QUERYAPIGATE_JWT_SECRET or QUERYAPIGATE_JWT_JWKS_URL is set - see jwtauth.py.
45
47
  # 2.14 is the floor because that is where PyJWKClient stops refetching the JWKS for
46
48
  # every unknown key id: before it, forged tokens could make the server hammer the provider.
@@ -1,5 +1,5 @@
1
1
  """QueryAPIGate - expose SQL databases as a REST API."""
2
- __version__ = "0.28.0"
2
+ __version__ = "0.30.0"
3
3
 
4
4
  from .app import create_app # noqa: E402 (app imports __version__)
5
5
 
@@ -0,0 +1,29 @@
1
+ """The access log (ADR 0016): one line per request - who asked, for what path, with what result - and never the query
2
+ string's values unless the deployer asks for them.
3
+
4
+ A saved query's parameters can arrive in the URL (`GET /q/orders?customer_email=alice@example.com`), and a web
5
+ server's usual access log writes the whole request line - so every value would be in the log platform for as long as
6
+ it keeps logs. QueryAPIGate's own log, run history and traces all leave values out on purpose; this keeps the access
7
+ log in step. QUERYAPIGATE_ACCESS_LOG_QUERY=1 puts the query string back. The referrer, which can carry another URL's
8
+ query string, is never logged.
9
+
10
+ Used by the image's gunicorn (`gunicorn -c python:queryapigate.gunicorn_conf`) and by `queryapigate serve`.
11
+ """
12
+ from . import config
13
+
14
+ # gunicorn's atoms: the client, the time, method, path (U: without its query) and protocol, status, size, seconds, the
15
+ # request id QueryAPIGate answered with, and the user agent.
16
+ _WITHOUT_QUERY = '%(h)s - %(t)s "%(m)s %(U)s %(H)s" %(s)s %(b)s %(L)ss rid=%({x-request-id}o)s "%(a)s"'
17
+ _WITH_QUERY = '%(h)s - %(t)s "%(r)s" %(s)s %(b)s %(L)ss rid=%({x-request-id}o)s "%(a)s"'
18
+
19
+
20
+ def gunicorn_format():
21
+ """gunicorn's access_log_format, as QUERYAPIGATE_ACCESS_LOG_QUERY says."""
22
+ return _WITH_QUERY if config.access_log_query() else _WITHOUT_QUERY
23
+
24
+
25
+ def request_line(method, path, protocol):
26
+ """A request line as the access log shows it: `path` with its query string removed, unless asked to keep it."""
27
+ if not config.access_log_query():
28
+ path = path.split('?', 1)[0]
29
+ return f'{method} {path} {protocol}'
@@ -34,12 +34,13 @@ CAPABILITIES = {
34
34
  'exports.run': 'run an export',
35
35
  'destinations.write': 'create, change, test and delete destinations',
36
36
  'config.apply': 'see what a configuration file would change, and apply it - any of the above (ADR 0014)',
37
+ 'history.forget': "delete a person's runs and usage counts, on request (ADR 0016)",
37
38
  }
38
39
 
39
40
  _EVERYONE = {'queries.read', 'connections.read', 'observe', 'self'}
40
41
  ROLES = {
41
42
  'owner': set(CAPABILITIES),
42
- 'admin': set(CAPABILITIES) - {'admins.read', 'admins.write', 'secrets.rotate', 'config.apply'},
43
+ 'admin': set(CAPABILITIES) - {'admins.read', 'admins.write', 'secrets.rotate', 'config.apply', 'history.forget'},
43
44
  'developer': _EVERYONE | {'queries.write', 'exports.read', 'exports.run'},
44
45
  'auditor': _EVERYONE | {'access.read', 'settings.read', 'exports.read'},
45
46
  }
@@ -92,6 +93,7 @@ OPERATIONS = {
92
93
  'v1.create_export': 'exports.write', 'v1.update_export': 'exports.write', 'v1.delete_export': 'exports.write',
93
94
  'v1.run_export': 'exports.run',
94
95
  'v1.config_diff': 'config.apply', 'v1.config_apply': 'config.apply', 'v1.config_managed': 'observe',
96
+ 'v1.forget_history': 'history.forget',
95
97
  }
96
98
 
97
99
 
@@ -146,6 +146,20 @@ def load_examples_at_startup():
146
146
  ', '.join(f'{name}={secret}' for name, secret in sorted(added['key_secrets'].items())))
147
147
 
148
148
 
149
+ def warn_env_references():
150
+ """Once at startup: ${VAR} references in use with no QUERYAPIGATE_SECRET_ENV_VARS - so an administrator may
151
+ reference any other variable too, but the few secretrefs.py always refuses (ADR 0017)."""
152
+ from . import destinations, secretrefs
153
+ if config.secret_env_vars():
154
+ return
155
+ used = secretrefs.env_variables([*store.read_connections().values(), *destinations.list_all()])
156
+ if used:
157
+ log.warning('Connections and destinations reference environment variables (%s), and '
158
+ 'QUERYAPIGATE_SECRET_ENV_VARS is unset: an administrator could reference any other variable too, '
159
+ "and have the server send it to a host of theirs as a password. Set it to the ones in use.",
160
+ ', '.join(used))
161
+
162
+
149
163
  def create_app():
150
164
  from . import __version__
151
165
  config.check_settings()
@@ -153,6 +167,7 @@ def create_app():
153
167
  db.init_schema()
154
168
  store.refuse_legacy_home()
155
169
  experimental.warn_in_use(log, os.environ, store.read_connections())
170
+ warn_env_references()
156
171
  instances.register('serve')
157
172
  instances.warn_if_unshared(log)
158
173
  app = Flask(__name__)
@@ -321,7 +336,7 @@ def create_app():
321
336
  deprecations.warn(rule)
322
337
  elapsed = time.monotonic() - g.get('request_started', time.monotonic())
323
338
  endpoint = request.endpoint or 'unmatched'
324
- governance.observe(request.method, endpoint, response.status_code, elapsed, caller_key_name())
339
+ governance.observe(request.method, endpoint, response.status_code, elapsed, metrics_key_name())
325
340
  cache_status = response.headers.get('X-Cache')
326
341
  if cache_status == 'HIT':
327
342
  metrics.inc_cache_hit()
@@ -440,6 +455,11 @@ def caller_key_name():
440
455
  return (permission.name if permission else None) or '-'
441
456
 
442
457
 
458
+ def metrics_key_name():
459
+ """The caller as /metrics counts it (metrics.label_for()): an API key's name, `admin` for any administrator."""
460
+ return metrics.label_for(g.get('permission'))
461
+
462
+
443
463
  def get_json_body(required=True):
444
464
  data = request.get_json(silent=True)
445
465
  if data is None and not required:
@@ -537,7 +557,7 @@ def stream_sql_response(sql, connection_name, params, timeout, output_format, fi
537
557
  code='invalid_stream')
538
558
  if request.args.get('page') or request.args.get('page_size'):
539
559
  raise ApiError('stream=true exports the whole result and does not accept page/page_size', code='invalid_stream')
540
- key_name = caller_key_name()
560
+ key_name = metrics_key_name()
541
561
  columns, rows = engine.stream_sql(sql, connection_name, params, timeout, key_name=key_name,
542
562
  allowed_tables=allowed_tables, mask=mask)
543
563
  # Captured here, not inside _record_stream_history(): that generator's body runs lazily, as the response
@@ -664,7 +684,7 @@ def execute_sql_endpoint():
664
684
  try:
665
685
  result, elapsed_ms = engine.timed(engine.execute_sql, data['sql'], data['connection_name'], limit, offset,
666
686
  params, timeout, allow_writes=g.permission.allow_writes,
667
- key_name=caller_key_name(), allowed_write_ops=g.permission.allowed_write_ops,
687
+ key_name=metrics_key_name(), allowed_write_ops=g.permission.allowed_write_ops,
668
688
  database=database, allowed_tables=g.permission.allowed_tables,
669
689
  mask=g.permission.mask)
670
690
  except ApiError as error:
@@ -699,7 +719,7 @@ def execute_mongo_endpoint():
699
719
  result, elapsed_ms = engine.timed(engine.execute_mongo, data['collection'], filter_doc,
700
720
  data['connection_name'], limit, offset, params, timeout,
701
721
  projection=data.get('projection'), sort=data.get('sort'),
702
- key_name=caller_key_name(), mask=g.permission.mask)
722
+ key_name=metrics_key_name(), mask=g.permission.mask)
703
723
  except ApiError as error:
704
724
  _record_adhoc_and_broadcast(statement, params, data['connection_name'],
705
725
  {**entry, 'status': 'error', 'error': error.message, 'code': error.code})
@@ -891,7 +911,7 @@ def _run_saved(ref, body, url_params):
891
911
  entry = run_entry(connection_name)
892
912
  try:
893
913
  result, elapsed_ms = engine.timed(engine.execute_sql, sql, connection_name, limit, offset, values, timeout,
894
- allow_writes=effective_allow_writes, key_name=caller_key_name(),
914
+ allow_writes=effective_allow_writes, key_name=metrics_key_name(),
895
915
  allowed_write_ops=g.permission.allowed_write_ops,
896
916
  allowed_tables=g.permission.allowed_tables, mask=g.permission.mask)
897
917
  except ApiError as error:
@@ -948,7 +968,7 @@ def _execute_agent_write(request, saved, path, number, connection_name, values,
948
968
  **({'approved_by': request['decided_by']} if request['decided_by'] else {})}
949
969
  try:
950
970
  result, affected = engine.execute_agent_write(sql, connection_name, values, saved.get('max_affected_rows') or 1,
951
- timeout, key_name=caller_key_name(),
971
+ timeout, key_name=metrics_key_name(),
952
972
  allowed_write_ops=g.permission.allowed_write_ops,
953
973
  allowed_tables=g.permission.allowed_tables,
954
974
  mask=g.permission.mask)
@@ -999,7 +1019,7 @@ def run_saved_mongo(saved, path, number, ref, connection_name, raw, output_forma
999
1019
  result, elapsed_ms = engine.timed(engine.execute_mongo, saved['mongo_collection'], filter_doc,
1000
1020
  connection_name, limit, offset, timeout=timeout,
1001
1021
  projection=saved.get('mongo_projection'), sort=saved.get('mongo_sort'),
1002
- key_name=caller_key_name(), mask=g.permission.mask)
1022
+ key_name=metrics_key_name(), mask=g.permission.mask)
1003
1023
  except ApiError as error:
1004
1024
  _record_and_broadcast(path, number, connection_name,
1005
1025
  {**entry, 'status': 'error', 'error': error.message, 'code': error.code})
@@ -1142,6 +1162,18 @@ def health():
1142
1162
 
1143
1163
  @bp.route('/metrics', methods=['GET'])
1144
1164
  def metrics_endpoint():
1165
+ """Open, unless QUERYAPIGATE_METRICS_TOKEN is set: then for `Authorization: Bearer <that token>` - a scraper's -
1166
+ or an administrator whose role may observe, as the Console's Metrics screen reads it. What the Helm chart sets,
1167
+ since its ingress would otherwise publish the counts and the key, query and connection names in them (ADR 0017's
1168
+ review)."""
1169
+ from . import adminroles
1170
+ role = getattr(g.get('permission'), 'role', None)
1171
+ observer = role in adminroles.ROLES and 'observe' in adminroles.ROLES[role]
1172
+ if not (observer or metrics.authorized(request.headers.get('Authorization'))):
1173
+ response = jsonify(error_body(metrics.UNAUTHORIZED, 401))
1174
+ response.status_code = 401
1175
+ response.headers['WWW-Authenticate'] = 'Bearer realm="metrics"'
1176
+ return response
1145
1177
  return Response(metrics.render(current_app.extensions.get('queryapigate_cache')),
1146
1178
  mimetype='text/plain; version=0.0.4; charset=utf-8')
1147
1179
 
@@ -663,13 +663,37 @@ def _handler():
663
663
  """Werkzeug's request handler, without its version banner (`Server: Werkzeug/3.x Python/3.12`, ADR 0010)."""
664
664
  from werkzeug.serving import WSGIRequestHandler
665
665
 
666
+ from . import accesslog
667
+
666
668
  class Handler(WSGIRequestHandler):
667
669
  def version_string(self):
668
670
  return 'QueryAPIGate'
669
671
 
672
+ def log_request(self, code='-', size='-'):
673
+ """The access log line without the query string's values (accesslog.py, ADR 0016)."""
674
+ line = accesslog.request_line(self.command, self.path, self.request_version)
675
+ self.log('info', '"%s" %s %s', line, getattr(code, 'value', code), size)
676
+
670
677
  return Handler
671
678
 
672
679
 
680
+ def _history_forget(args):
681
+ """`queryapigate history forget CALLER`: delete a person's runs and usage counts (ADR 0016)."""
682
+ from . import history
683
+ try:
684
+ config.check_settings()
685
+ found = history.forget(args.caller, 'cli', dry_run=args.dry_run)
686
+ except (ApiError, ValueError) as error:
687
+ print(f'queryapigate history forget: {getattr(error, "message", error)}', file=sys.stderr)
688
+ return 1
689
+ if args.dry_run:
690
+ print(f"Would delete {found['runs']} run(s) and {found['usage_periods']} usage period(s) of {args.caller}.")
691
+ else:
692
+ print(f"Deleted {found['runs']} run(s) and {found['usage_periods']} usage period(s) of {args.caller}; recorded "
693
+ 'in the audit log by its hash, not its name.')
694
+ return 0
695
+
696
+
673
697
  def _audit_verify(args):
674
698
  """`queryapigate audit verify [--file PATH]`: walk the audit log's hash chain (ADR 0012) - the store's, or an export
675
699
  file's - and say whether it's intact, or where it first breaks. Exit 0 intact, 1 broken, 2 can't be checked
@@ -894,6 +918,16 @@ def build_parser():
894
918
  audit_verify.add_argument('--file', help='an export file (QUERYAPIGATE_AUDIT_LOG_EXPORT_FILE) instead of the store')
895
919
  audit_verify.set_defaults(func=_audit_verify)
896
920
 
921
+ history_parser = commands.add_parser('history', help='run history: forget a person (ADR 0016)')
922
+ history_parser.set_defaults(func=lambda a: history_parser.print_help() or 2)
923
+ history_actions = history_parser.add_subparsers(dest='action')
924
+ history_forget = history_actions.add_parser(
925
+ 'forget', help="delete every run by CALLER and its usage counts - a person's data, on request; recorded in the "
926
+ 'audit log by its hash, not its name')
927
+ history_forget.add_argument('caller', help='a key name, or jwt:<subject> for a signed-in person')
928
+ history_forget.add_argument('--dry-run', action='store_true', help='say how much would go; delete nothing')
929
+ history_forget.set_defaults(func=_history_forget)
930
+
897
931
  rekey_parser = commands.add_parser('rekey', help='re-encrypt every stored password under the first key in '
898
932
  'QUERYAPIGATE_SECRET_KEY, so the others can be removed (rotating '
899
933
  'the key)')
@@ -64,6 +64,12 @@ def load_examples():
64
64
  return env_flag('QUERYAPIGATE_LOAD_EXAMPLES')
65
65
 
66
66
 
67
+ def access_log_query():
68
+ """QUERYAPIGATE_ACCESS_LOG_QUERY: the access log keeps each request's query string - its parameter values. Off by
69
+ default (ADR 0016): the path alone, as QueryAPIGate's own log, run history and traces keep it."""
70
+ return env_flag('QUERYAPIGATE_ACCESS_LOG_QUERY')
71
+
72
+
67
73
  def config_file():
68
74
  """QUERYAPIGATE_CONFIG_FILE: a configuration file (ADR 0014) applied at startup, before the server serves - once per
69
75
  file: an instance that finds it already applied leaves the store alone. None when unset."""
@@ -351,6 +357,13 @@ def check_settings():
351
357
  for folder in os.environ.get('QUERYAPIGATE_SECRET_FILE_DIRS', '').split(','):
352
358
  if folder.strip() and not os.path.isabs(folder.strip()):
353
359
  raise ValueError(f"QUERYAPIGATE_SECRET_FILE_DIRS: '{folder.strip()}' must be an absolute path")
360
+ if metrics_token() is not None and len(metrics_token()) < METRICS_TOKEN_MIN:
361
+ raise ValueError(f'QUERYAPIGATE_METRICS_TOKEN must be at least {METRICS_TOKEN_MIN} characters - '
362
+ '`openssl rand -hex 32` makes one')
363
+ for name in secret_env_vars():
364
+ if not SECRET_ENV_VAR_RE.fullmatch(name):
365
+ raise ValueError(f"QUERYAPIGATE_SECRET_ENV_VARS: '{name}' must be a variable's name, or a prefix "
366
+ 'ending in *')
354
367
  # This project was named SQL2API before it was QueryAPIGate, and its settings were SQL2API_*. They are
355
368
  # deliberately not read any more - but silently ignoring a leftover SQL2API_API_KEY would start the server
356
369
  # with no admin key configured, which means open access. So a leftover old name is a startup error.
@@ -362,7 +375,7 @@ def check_settings():
362
375
  raw = os.environ.get('QUERYAPIGATE_LOAD_EXAMPLES', '').strip().lower()
363
376
  if raw not in _FLAG_WORDS:
364
377
  raise ValueError(f"QUERYAPIGATE_LOAD_EXAMPLES must be yes or no (or 1/0, true/false, on/off), not '{raw}'")
365
- for name in ('QUERYAPIGATE_CONFIG_PRUNE', 'QUERYAPIGATE_CONFIG_LOCKED'):
378
+ for name in ('QUERYAPIGATE_CONFIG_PRUNE', 'QUERYAPIGATE_CONFIG_LOCKED', 'QUERYAPIGATE_ACCESS_LOG_QUERY'):
366
379
  raw = os.environ.get(name, '').strip().lower()
367
380
  if raw not in _FLAG_WORDS:
368
381
  raise ValueError(f"{name} must be yes or no (or 1/0, true/false, on/off), not '{raw}'")
@@ -469,6 +482,18 @@ def secret_file_dirs():
469
482
  return [os.path.realpath(p.strip()) for p in raw.split(',') if p.strip()]
470
483
 
471
484
 
485
+ SECRET_ENV_VAR_RE = re.compile(r'[A-Za-z_][A-Za-z0-9_]*\*?')
486
+
487
+
488
+ def secret_env_vars():
489
+ """QUERYAPIGATE_SECRET_ENV_VARS: the environment variables `${VAR}` references may read (ADR 0017) - names, or
490
+ prefixes ending in `*` (`WAREHOUSE_*`), comma-separated. Unset, any may be read but those secretrefs.py always
491
+ refuses: the server's own settings, and the cloud, Vault and Kubernetes credentials it may run with. Validated by
492
+ check_settings()."""
493
+ raw = os.environ.get('QUERYAPIGATE_SECRET_ENV_VARS', '')
494
+ return [p.strip() for p in raw.split(',') if p.strip()]
495
+
496
+
472
497
  def secret_cache_seconds():
473
498
  """QUERYAPIGATE_SECRET_CACHE_SECONDS: how long a secret resolved from a provider is kept in memory at most (default
474
499
  300) - fetched again a little before. Validated by check_settings()."""
@@ -595,7 +620,8 @@ def _check_vault():
595
620
  # convention - read at startup, as the environment variable would be.
596
621
  SECRET_SETTINGS = ('QUERYAPIGATE_API_KEY', 'QUERYAPIGATE_SECRET_KEY', 'QUERYAPIGATE_DATABASE_URL',
597
622
  'QUERYAPIGATE_REDIS_URL', 'QUERYAPIGATE_AUDIT_KEY', 'QUERYAPIGATE_MASK_KEY',
598
- 'QUERYAPIGATE_JWT_SECRET', 'QUERYAPIGATE_SSO_CLIENT_SECRET', 'QUERYAPIGATE_VAULT_SECRET_ID')
623
+ 'QUERYAPIGATE_JWT_SECRET', 'QUERYAPIGATE_SSO_CLIENT_SECRET', 'QUERYAPIGATE_VAULT_SECRET_ID',
624
+ 'QUERYAPIGATE_METRICS_TOKEN')
599
625
  # QUERYAPIGATE_VAULT_TOKEN_FILE isn't one of them: vault.py reads it again at each sign-in, as Vault Agent rotates it.
600
626
  _from_files: dict[str, str] = {} # setting -> the file it was read from
601
627
 
@@ -626,6 +652,15 @@ def secret_source(name):
626
652
  return _from_files.get(name)
627
653
 
628
654
 
655
+ METRICS_TOKEN_MIN = 16
656
+
657
+
658
+ def metrics_token():
659
+ """QUERYAPIGATE_METRICS_TOKEN: the bearer token /metrics needs, or None - then /metrics is open, as it was before
660
+ 0.30 (metrics.authorized()). Validated by check_settings()."""
661
+ return os.environ.get('QUERYAPIGATE_METRICS_TOKEN', '').strip() or None
662
+
663
+
629
664
  def mask_key():
630
665
  """QUERYAPIGATE_MASK_KEY: the key column masking's `hash` action is keyed with (masking.py), or None - then the
631
666
  store makes one once and every instance shares it."""
@@ -1237,6 +1272,10 @@ def describe_settings():
1237
1272
  row('Secret files', 'Folders ${file:...} references may read - a connection\'s or destination\'s '
1238
1273
  'password from a file the CSI driver, Docker or Vault Agent keeps current (ADR 0015).',
1239
1274
  'QUERYAPIGATE_SECRET_FILE_DIRS', ', '.join(secret_file_dirs()) or 'none - ${file:...} refused'),
1275
+ row('Environment secrets', 'Variables ${VAR} references may read. Never the server\'s own settings, nor '
1276
+ 'the cloud, Vault and Kubernetes credentials it may run with (AWS_*, AZURE_*, ARM_*, GOOGLE_*, GCP_*, '
1277
+ 'VAULT_*, KUBERNETES_*).', 'QUERYAPIGATE_SECRET_ENV_VARS',
1278
+ ', '.join(secret_env_vars()) or 'any - set it to the ones your connections use'),
1240
1279
  row('Secret cache', 'How long a secret from a provider is kept in memory at most.',
1241
1280
  'QUERYAPIGATE_SECRET_CACHE_SECONDS', f'{secret_cache_seconds()} s'),
1242
1281
  row('Secret providers', 'How the ones this process has used answered, last.', '',
@@ -1262,6 +1301,9 @@ def describe_settings():
1262
1301
  row('Encryption at rest', 'Fernet key used to encrypt connection and destination passwords on disk; '
1263
1302
  'several, comma-separated, while rotating it - the first encrypts.',
1264
1303
  'QUERYAPIGATE_SECRET_KEY', _encryption_state(), secret=True),
1304
+ row('Metrics token', 'The bearer token /metrics needs. Unset: /metrics is open to whoever can reach it.',
1305
+ 'QUERYAPIGATE_METRICS_TOKEN', 'configured' if metrics_token() else 'not set - /metrics is open',
1306
+ secret=True),
1265
1307
  row('Masking hash key', "Key for column masking's hash action. Unset: one made once in the store.",
1266
1308
  'QUERYAPIGATE_MASK_KEY', 'configured' if mask_key() else 'from the store', secret=True),
1267
1309
  row('Export folders', 'Local folders a destination may write under. Unset: object storage only.',
@@ -1359,8 +1401,12 @@ def describe_settings():
1359
1401
  row('Ad-hoc history limit', 'Ad-hoc SQL runs kept in all, unless a retention period is set.',
1360
1402
  'QUERYAPIGATE_HISTORY_ADHOC_LIMIT', f'{history_adhoc_limit()} runs'),
1361
1403
  row('Ad-hoc SQL kept', 'What an ad-hoc run\'s history keeps of its SQL: the text (first '
1362
- f'{HISTORY_ADHOC_SQL_MAX} characters), a hash, or none. Parameter values are never kept.',
1404
+ f'{HISTORY_ADHOC_SQL_MAX} characters), a hash, or none - and with hash or none, the log shows every '
1405
+ 'statement by its hash too. Parameter values are never kept.',
1363
1406
  'QUERYAPIGATE_HISTORY_ADHOC_SQL', history_adhoc_sql()),
1407
+ row('Access log query strings', 'The access log keeps each request\'s query string - parameter values '
1408
+ 'sent in the URL. Off: the path alone (ADR 0016).', 'QUERYAPIGATE_ACCESS_LOG_QUERY',
1409
+ on_off(access_log_query())),
1364
1410
  row('History retention', 'Keep every run for this many days instead of a per-version count. Best '
1365
1411
  'with a PostgreSQL metadata store.', 'QUERYAPIGATE_HISTORY_RETENTION_DAYS',
1366
1412
  'per-version limit' if history_retention_days() is None else f'{history_retention_days()} days'),
@@ -1,3 +1,3 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/forms-BlZHx8-q.js","assets/index-C7ega31V.js","assets/index-BLd0t6vZ.css","assets/SchemaBrowser-mmFZRlm_.js","assets/useMutation-Df0q777p.js","assets/reach-C1r9_MqV.js","assets/CodeBox-DRfv6yXf.js"])))=>i.map(i=>d[i]);
2
- import{$ as e,C as t,D as n,F as r,H as i,I as a,L as o,Q as s,V as c,Z as l,h as u,o as d,s as f,u as p,z as m}from"./index-C7ega31V.js";import{a as h}from"./reach-C1r9_MqV.js";import{t as g}from"./Time-CZQinVk_.js";import{f as _,i as v,n as y,r as b,u as x}from"./SchemaBrowser-mmFZRlm_.js";import{t as S}from"./CodeBox-DRfv6yXf.js";var C=e(s(),1),w=l();function T({term:e,children:t}){return(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`dt`,{children:e}),(0,w.jsx)(`dd`,{title:typeof t==`string`?t:void 0,children:t})]})}function E({name:e,dbType:s}){let c=m(),{closeDrawer:l,toast:u}=o(),d=i({queryKey:[`queries`,`detail`,e,`info`],queryFn:async()=>n(await t.GET(`/api/v1/queries/{name}`,{params:{path:{name:e}}})),retry:!1}),f=d.data,p=f?.versions[f.versions.length-1],h=p?.query_type===`mongo`?JSON.stringify(p.mongo??{},null,2):p?.sql??``,_=i({queryKey:[`queries`,`flow`,e,p?.version],queryFn:()=>v(e,p.version),enabled:!(!p||p.query_type!==`sql`||h.includes(`
3
- `)),staleTime:1/0});if(d.isPending)return(0,w.jsx)(r,{});if(!f||!p)return(0,w.jsx)(`div`,{className:`hint`,children:`Could not load the SQL.`});let y=_.data?.formatted||h,b=f.created_at,x=f.last_used_at;return(0,w.jsxs)(`div`,{className:`form`,children:[p.description?(0,w.jsx)(`p`,{className:`d-desc`,children:p.description}):null,(0,w.jsxs)(`dl`,{className:`meta`,children:[(0,w.jsx)(T,{term:`connection`,children:p.connection_name||`—`}),(0,w.jsx)(T,{term:`database`,children:s?(0,w.jsx)(`span`,{className:`tag`,children:s}):(0,w.jsx)(`span`,{className:`dim`,children:`—`})}),(0,w.jsx)(T,{term:`collection`,children:f.collection||`—`}),(0,w.jsx)(T,{term:`status`,children:p.status||`—`}),(0,w.jsx)(T,{term:`author`,children:p.author||`—`}),(0,w.jsx)(T,{term:`created`,children:(0,w.jsx)(g,{value:b})}),(0,w.jsx)(T,{term:`last modified`,children:(0,w.jsx)(g,{value:p.last_modified_at||p.created_at})}),(0,w.jsx)(T,{term:`last used`,children:(0,w.jsx)(g,{value:x,fallback:`never`})}),p.tags.length?(0,w.jsx)(T,{term:`tags`,children:p.tags.join(`, `)}):null]}),(0,w.jsx)(`p`,{className:`sub-h`,style:{marginTop:6},children:`SQL · v${p.version}${f.versions.length>1?` (latest)`:``}`}),(0,w.jsx)(`div`,{children:_.isFetching?(0,w.jsx)(r,{text:`Loading SQL…`}):(0,w.jsxs)(w.Fragment,{children:[(0,w.jsx)(S,{sql:y}),(0,w.jsx)(`div`,{style:{marginTop:6},children:(0,w.jsx)(`button`,{type:`button`,className:`btn sm ghost`,onClick:()=>a(y,u),children:`Copy SQL`})})]})}),(0,w.jsx)(`p`,{className:`sub-h`,style:{marginTop:6},children:`Versions`}),(0,w.jsx)(`div`,{className:`panel`,style:{overflow:`auto`},children:(0,w.jsxs)(`table`,{className:`grid qi-versions`,children:[(0,w.jsx)(`thead`,{children:(0,w.jsx)(`tr`,{children:[`Version`,`Created`,`Last modified`,`Runs`].map(e=>(0,w.jsx)(`th`,{children:e},e))})}),(0,w.jsx)(`tbody`,{children:[...f.versions].reverse().map(e=>(0,w.jsxs)(`tr`,{children:[(0,w.jsx)(`td`,{className:`mono`,children:`v`+e.version}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(g,{value:e.created_at})}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(g,{value:e.last_modified_at})}),(0,w.jsx)(`td`,{className:`mono`,style:{textAlign:`right`},children:String(e.run_count??0)})]},e.version))})]})}),(0,w.jsxs)(`div`,{className:`form-actions`,children:[(0,w.jsx)(`button`,{type:`button`,className:`btn`,onClick:l,children:`Close`}),(0,w.jsx)(`button`,{type:`button`,className:`btn primary`,onClick:()=>{l(),c(`/queries/${encodeURIComponent(e)}`)},children:`Open in API Repository`})]})]})}var D=(0,C.lazy)(()=>c(()=>import(`./forms-BlZHx8-q.js`).then(e=>({default:e.QueryForm})),__vite__mapDeps([0,1,2,3,4,5,6]))),O={query:`Q`,collection:`C`,connection:`W`},k=[`Database`,`Created`,`Last modified`,`Last used`,`Version`],A=[`database`,`created`,`modified`,`lastUsed`,`version`],j=()=>new Date().toISOString().slice(0,10);function M(){let e=m(),{openDrawer:t}=o(),n=d(),i=f(),a=u(),s=p(),[c,l]=(0,C.useState)(x),g=e=>{let t={...c,...e};_(t),l(t)},v=s.data??[],S=e=>v.find(t=>t.name===e)?.db??null,T=y(c.conn||void 0),O=T.data?.tables??[],k=b(c.conn||void 0,S(c.conn),c.table&&!c.tableMatches?[c.table]:[]),A=c.table?c.tableMatches??(k.data?k.data[c.table]??[]:null):null,j=n.data??[],M=[...new Set(j.map(e=>S(e.connection_name)).filter(e=>!!e))].sort(),P=i.data??{},F=a.data??{},I=Object.keys(P).sort(),L=Object.keys(F).sort(),R,z=!0;c.conn?T.isPending?R=(0,w.jsx)(`option`,{value:``,children:`Loading tables…`}):T.isError?R=(0,w.jsx)(`option`,{value:``,children:`Schema unavailable`}):(z=!1,R=(0,w.jsxs)(w.Fragment,{children:[(0,w.jsx)(`option`,{value:``,children:`All tables (${O.length})`}),O.map(e=>(0,w.jsx)(`option`,{value:e.name,children:`${e.name} · ${e.columns.length}${e.columns.length===1?` col`:` cols`}`},e.name))]})):R=(0,w.jsx)(`option`,{value:``,children:`Select a connection first`});let B=(e,t)=>h(e,t.name,t.collection,t.connection_name),V;if(n.isPending)V=(0,w.jsx)(r,{});else if(!j.length)V=(0,w.jsx)(`div`,{className:`empty`,children:(0,w.jsx)(`strong`,{children:`No saved queries yet`})});else if(!I.length&&!L.length)V=(0,w.jsxs)(`div`,{className:`empty`,children:[(0,w.jsx)(`strong`,{children:`No scoped API keys or roles yet`}),(0,w.jsx)(`span`,{children:`Every query is reachable by the admin key only until a scoped key is created.`})]});else{let n=c.q.trim().toLowerCase(),i=[...j].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,l=n?L.filter(a):L,u=o.length||s.length||l.length,d=o.length?o:u?i:[],f=s.length?s:u?I:[],p=l.length?l:u?L:[];if(c.conn&&(d=d.filter(e=>e.connection_name===c.conn)),c.table&&A===null)V=(0,w.jsx)(r,{text:`Checking which queries reference ${c.table}…`});else if(A&&(d=d.filter(e=>A.includes(e.name))),d.length){let n=d.map(e=>({q:e,dbType:S(e.connection_name),reach:f.filter(t=>B(P[t],e).length).length}));c.db&&(n=n.filter(e=>e.dbType===c.db)),c.reach===`reachable`?n=n.filter(e=>e.reach>0):c.reach===`unreachable`&&(n=n.filter(e=>e.reach===0));let r=c.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,w.jsx)(N,{rows:n,keyCols:f,roleCols:p,keyMap:P,roleMap:F,st:c,onSort:e=>g({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,w.jsx)(E,{name:e.q.name,dbType:e.dbType})})}):(0,w.jsx)(`div`,{className:`empty`,children:(0,w.jsx)(`span`,{children:`No queries match these filters.`})})}else{let i=n?`Nothing matches “${n}”.`:c.table?`“${c.table}” on ${c.conn} is not exposed by any saved query yet.`:c.conn?`No saved query uses “${c.conn}” yet.`:`Nothing matches these filters.`;V=(0,w.jsxs)(`div`,{className:`empty`,children:[(0,w.jsx)(`span`,{children:i}),!n&&c.conn?(0,w.jsx)(`button`,{type:`button`,className:`btn primary`,onClick:()=>{let n=S(c.conn)===`mongo`,i=c.table?n?JSON.stringify({collection:c.table,filter:{}},null,2):`SELECT * FROM `+c.table:void 0;e(`/queries`),t({title:`New API`,kicker:`POST /api/v1/queries`,content:(0,w.jsx)(C.Suspense,{fallback:(0,w.jsx)(r,{}),children:(0,w.jsx)(D,{starter:{connection:c.conn,sql:i}})})})},children:`New API`+(c.table?` on `+c.table:``)}):null]})}}return(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:`Access map`}),(0,w.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,w.jsxs)(`span`,{className:`legend`,children:[(0,w.jsx)(`span`,{className:`amap-dot`,children:`Q`}),` named query \xA0`,(0,w.jsx)(`span`,{className:`amap-dot`,children:`C`}),` `,`collection \xA0`,(0,w.jsx)(`span`,{className:`amap-dot conn`,children:`W`}),` whole connection`]})]})]}),(0,w.jsx)(`span`,{className:`spacer`}),(0,w.jsxs)(`select`,{id:`accessmap-conn-filter`,"aria-label":`Connection`,value:v.some(e=>e.name===c.conn)?c.conn:``,onChange:e=>g({conn:e.target.value,table:``,tableMatches:null}),children:[(0,w.jsx)(`option`,{value:``,children:`All connections`}),[...v].sort((e,t)=>e.name<t.name?-1:1).map(e=>(0,w.jsx)(`option`,{value:e.name,children:e.name},e.name))]}),(0,w.jsx)(`select`,{id:`accessmap-table-filter`,"aria-label":`Table`,disabled:z,value:O.some(e=>e.name===c.table)?c.table:``,onChange:e=>g({table:e.target.value,tableMatches:null}),children:R}),(0,w.jsxs)(`select`,{id:`accessmap-db-filter`,"aria-label":`Database`,value:M.includes(c.db)?c.db:``,onChange:e=>g({db:e.target.value}),children:[(0,w.jsx)(`option`,{value:``,children:`All databases`}),M.map(e=>(0,w.jsx)(`option`,{value:e,children:e},e))]}),(0,w.jsxs)(`select`,{id:`accessmap-reach-filter`,"aria-label":`Reach`,value:c.reach,onChange:e=>g({reach:e.target.value}),children:[(0,w.jsx)(`option`,{value:``,children:`Any reach`}),(0,w.jsx)(`option`,{value:`reachable`,children:`Reachable by a key`}),(0,w.jsx)(`option`,{value:`unreachable`,children:`Reachable by no key`})]}),(0,w.jsx)(`input`,{id:`accessmap-filter`,className:`search`,type:`search`,placeholder:`Filter by query or key…`,value:c.q,onChange:e=>g({q:e.target.value})})]}),(0,w.jsx)(`div`,{id:`accessmap-body`,className:`panel`,children:V})]})}function N({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,d=(e,t=null,n=null)=>u(e,t,n)?l.dir===1?` ▲`:` ▼`:``,f=(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})},p=(e,t,n)=>(0,w.jsx)(`th`,{className:t,onClick:()=>f(n),children:e+d(n)},n),m=(e,t,n,r)=>(0,w.jsxs)(`th`,{className:`amap-key-head amap-sortable`+(n?` revoked`:``),title:n?e+n:e,onClick:()=>f(`col`,r,e),children:[(0,w.jsxs)(`span`,{className:`amap-key-name`,children:[e,d(`col`,r,e),n?(0,w.jsx)(`span`,{className:`dim`,children:n}):null]}),(0,w.jsxs)(`span`,{className:`amap-key-sub`,children:[(t.allow_writes?`Read/write`:`Read-only`)+` · `+(t.rate_limit||`server default`),t.mask?.length?(0,w.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=h(e,t.name,t.collection,t.connection_name);if(!i.length)return(0,w.jsx)(`td`,{className:`amap-cell`},r);let a=[...new Set(i.map(e=>O[e.kind]))],o=a.length===1&&a[0]===`W`;return(0,w.jsx)(`td`,{className:`amap-cell`,title:i.map(e=>e.label).join(`, `),children:(0,w.jsx)(`span`,{className:`amap-dot`+(n?` role`:``)+(o?` conn`:``),children:a.join(`·`)})},r)},v=j();return(0,w.jsx)(`div`,{style:{overflow:`auto`},children:(0,w.jsxs)(`table`,{className:`amap`,children:[(0,w.jsxs)(`thead`,{children:[(0,w.jsxs)(`tr`,{children:[(0,w.jsx)(`th`,{colSpan:3+k.length}),t.length?(0,w.jsx)(`th`,{colSpan:t.length,className:`amap-group`,children:`API KEYS`}):null,n.length?(0,w.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,w.jsxs)(`tr`,{children:[p(`Query`,`amap-query amap-sortable`,`query`),A.map((e,t)=>p(k[t],`amap-sortable`,e)),p(`Connection`,`amap-sortable`,`connection`),p(`Reach`,`num amap-sortable`,`reach`),t.map(e=>{let t=r[e],n=!!(t.expires_at&&t.expires_at<v);return m(e,t,t.active?n?` (expired ${t.expires_at})`:``:` (revoked)`,`key`)}),n.map(e=>m(e,i[e],``,`role`))]})]}),(0,w.jsx)(`tbody`,{children:e.map(e=>{let a=e.q;return(0,w.jsxs)(`tr`,{children:[(0,w.jsxs)(`td`,{className:`amap-query`,children:[(0,w.jsxs)(`div`,{className:`amap-q-row`,children:[(0,w.jsx)(`button`,{type:`button`,className:`amap-q-name`,title:`Open ${a.name} in API Repository`,onClick:()=>s(a.name),children:a.name}),(0,w.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,w.jsx)(`span`,{className:`amap-coll`,children:a.collection}):null]}),(0,w.jsx)(`td`,{children:e.dbType?(0,w.jsx)(`span`,{className:`tag`,children:e.dbType}):(0,w.jsx)(`span`,{className:`dim`,children:`—`})}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(g,{value:a.created_at})}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(g,{value:a.updated_at})}),(0,w.jsx)(`td`,{className:a.last_used_at?`mono`:`mono dim`,children:(0,w.jsx)(g,{value:a.last_used_at,fallback:`never`})}),(0,w.jsx)(`td`,{className:`mono`,children:`v`+a.latest_version}),(0,w.jsx)(`td`,{className:`mono dim`,children:a.connection_name||`—`}),(0,w.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,w.jsx)(`tfoot`,{children:(0,w.jsxs)(`tr`,{children:[(0,w.jsx)(`td`,{className:`amap-query`,children:`Reaches`}),k.map(e=>(0,w.jsx)(`td`,{},e)),(0,w.jsx)(`td`,{}),(0,w.jsx)(`td`,{}),t.map(t=>(0,w.jsx)(`td`,{className:`num`,children:`${e.filter(e=>h(r[t],e.q.name,e.q.collection,e.q.connection_name).length).length} / ${e.length}`},t)),n.map(t=>(0,w.jsx)(`td`,{className:`num`,children:`${e.filter(e=>h(i[t],e.q.name,e.q.collection,e.q.connection_name).length).length} / ${e.length}`},t))]})})]})})}export{M as AccessMapPage};
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/forms-bSOovYin.js","assets/index-DvCopLMA.js","assets/index-BLd0t6vZ.css","assets/SchemaBrowser-DQ0mi4Vm.js","assets/useMutation-BcyO6Hjg.js","assets/reach-DWK2PD5I.js","assets/CodeBox-DuaKuoNl.js"])))=>i.map(i=>d[i]);
2
+ import{$ as e,C as t,D as n,F as r,H as i,I as a,L as o,Q as s,V as c,Z as l,h as u,o as d,s as f,u as p,z as m}from"./index-DvCopLMA.js";import{a as h}from"./reach-DWK2PD5I.js";import{t as g}from"./Time-KeJDv2te.js";import{f as _,i as v,n as y,r as b,u as x}from"./SchemaBrowser-DQ0mi4Vm.js";import{t as S}from"./CodeBox-DuaKuoNl.js";var C=e(s(),1),w=l();function T({term:e,children:t}){return(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`dt`,{children:e}),(0,w.jsx)(`dd`,{title:typeof t==`string`?t:void 0,children:t})]})}function E({name:e,dbType:s}){let c=m(),{closeDrawer:l,toast:u}=o(),d=i({queryKey:[`queries`,`detail`,e,`info`],queryFn:async()=>n(await t.GET(`/api/v1/queries/{name}`,{params:{path:{name:e}}})),retry:!1}),f=d.data,p=f?.versions[f.versions.length-1],h=p?.query_type===`mongo`?JSON.stringify(p.mongo??{},null,2):p?.sql??``,_=i({queryKey:[`queries`,`flow`,e,p?.version],queryFn:()=>v(e,p.version),enabled:!(!p||p.query_type!==`sql`||h.includes(`
3
+ `)),staleTime:1/0});if(d.isPending)return(0,w.jsx)(r,{});if(!f||!p)return(0,w.jsx)(`div`,{className:`hint`,children:`Could not load the SQL.`});let y=_.data?.formatted||h,b=f.created_at,x=f.last_used_at;return(0,w.jsxs)(`div`,{className:`form`,children:[p.description?(0,w.jsx)(`p`,{className:`d-desc`,children:p.description}):null,(0,w.jsxs)(`dl`,{className:`meta`,children:[(0,w.jsx)(T,{term:`connection`,children:p.connection_name||`—`}),(0,w.jsx)(T,{term:`database`,children:s?(0,w.jsx)(`span`,{className:`tag`,children:s}):(0,w.jsx)(`span`,{className:`dim`,children:`—`})}),(0,w.jsx)(T,{term:`collection`,children:f.collection||`—`}),(0,w.jsx)(T,{term:`status`,children:p.status||`—`}),(0,w.jsx)(T,{term:`author`,children:p.author||`—`}),(0,w.jsx)(T,{term:`created`,children:(0,w.jsx)(g,{value:b})}),(0,w.jsx)(T,{term:`last modified`,children:(0,w.jsx)(g,{value:p.last_modified_at||p.created_at})}),(0,w.jsx)(T,{term:`last used`,children:(0,w.jsx)(g,{value:x,fallback:`never`})}),p.tags.length?(0,w.jsx)(T,{term:`tags`,children:p.tags.join(`, `)}):null]}),(0,w.jsx)(`p`,{className:`sub-h`,style:{marginTop:6},children:`SQL · v${p.version}${f.versions.length>1?` (latest)`:``}`}),(0,w.jsx)(`div`,{children:_.isFetching?(0,w.jsx)(r,{text:`Loading SQL…`}):(0,w.jsxs)(w.Fragment,{children:[(0,w.jsx)(S,{sql:y}),(0,w.jsx)(`div`,{style:{marginTop:6},children:(0,w.jsx)(`button`,{type:`button`,className:`btn sm ghost`,onClick:()=>a(y,u),children:`Copy SQL`})})]})}),(0,w.jsx)(`p`,{className:`sub-h`,style:{marginTop:6},children:`Versions`}),(0,w.jsx)(`div`,{className:`panel`,style:{overflow:`auto`},children:(0,w.jsxs)(`table`,{className:`grid qi-versions`,children:[(0,w.jsx)(`thead`,{children:(0,w.jsx)(`tr`,{children:[`Version`,`Created`,`Last modified`,`Runs`].map(e=>(0,w.jsx)(`th`,{children:e},e))})}),(0,w.jsx)(`tbody`,{children:[...f.versions].reverse().map(e=>(0,w.jsxs)(`tr`,{children:[(0,w.jsx)(`td`,{className:`mono`,children:`v`+e.version}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(g,{value:e.created_at})}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(g,{value:e.last_modified_at})}),(0,w.jsx)(`td`,{className:`mono`,style:{textAlign:`right`},children:String(e.run_count??0)})]},e.version))})]})}),(0,w.jsxs)(`div`,{className:`form-actions`,children:[(0,w.jsx)(`button`,{type:`button`,className:`btn`,onClick:l,children:`Close`}),(0,w.jsx)(`button`,{type:`button`,className:`btn primary`,onClick:()=>{l(),c(`/queries/${encodeURIComponent(e)}`)},children:`Open in API Repository`})]})]})}var D=(0,C.lazy)(()=>c(()=>import(`./forms-bSOovYin.js`).then(e=>({default:e.QueryForm})),__vite__mapDeps([0,1,2,3,4,5,6]))),O={query:`Q`,collection:`C`,connection:`W`},k=[`Database`,`Created`,`Last modified`,`Last used`,`Version`],A=[`database`,`created`,`modified`,`lastUsed`,`version`],j=()=>new Date().toISOString().slice(0,10);function M(){let e=m(),{openDrawer:t}=o(),n=d(),i=f(),a=u(),s=p(),[c,l]=(0,C.useState)(x),g=e=>{let t={...c,...e};_(t),l(t)},v=s.data??[],S=e=>v.find(t=>t.name===e)?.db??null,T=y(c.conn||void 0),O=T.data?.tables??[],k=b(c.conn||void 0,S(c.conn),c.table&&!c.tableMatches?[c.table]:[]),A=c.table?c.tableMatches??(k.data?k.data[c.table]??[]:null):null,j=n.data??[],M=[...new Set(j.map(e=>S(e.connection_name)).filter(e=>!!e))].sort(),P=i.data??{},F=a.data??{},I=Object.keys(P).sort(),L=Object.keys(F).sort(),R,z=!0;c.conn?T.isPending?R=(0,w.jsx)(`option`,{value:``,children:`Loading tables…`}):T.isError?R=(0,w.jsx)(`option`,{value:``,children:`Schema unavailable`}):(z=!1,R=(0,w.jsxs)(w.Fragment,{children:[(0,w.jsx)(`option`,{value:``,children:`All tables (${O.length})`}),O.map(e=>(0,w.jsx)(`option`,{value:e.name,children:`${e.name} · ${e.columns.length}${e.columns.length===1?` col`:` cols`}`},e.name))]})):R=(0,w.jsx)(`option`,{value:``,children:`Select a connection first`});let B=(e,t)=>h(e,t.name,t.collection,t.connection_name),V;if(n.isPending)V=(0,w.jsx)(r,{});else if(!j.length)V=(0,w.jsx)(`div`,{className:`empty`,children:(0,w.jsx)(`strong`,{children:`No saved queries yet`})});else if(!I.length&&!L.length)V=(0,w.jsxs)(`div`,{className:`empty`,children:[(0,w.jsx)(`strong`,{children:`No scoped API keys or roles yet`}),(0,w.jsx)(`span`,{children:`Every query is reachable by the admin key only until a scoped key is created.`})]});else{let n=c.q.trim().toLowerCase(),i=[...j].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,l=n?L.filter(a):L,u=o.length||s.length||l.length,d=o.length?o:u?i:[],f=s.length?s:u?I:[],p=l.length?l:u?L:[];if(c.conn&&(d=d.filter(e=>e.connection_name===c.conn)),c.table&&A===null)V=(0,w.jsx)(r,{text:`Checking which queries reference ${c.table}…`});else if(A&&(d=d.filter(e=>A.includes(e.name))),d.length){let n=d.map(e=>({q:e,dbType:S(e.connection_name),reach:f.filter(t=>B(P[t],e).length).length}));c.db&&(n=n.filter(e=>e.dbType===c.db)),c.reach===`reachable`?n=n.filter(e=>e.reach>0):c.reach===`unreachable`&&(n=n.filter(e=>e.reach===0));let r=c.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,w.jsx)(N,{rows:n,keyCols:f,roleCols:p,keyMap:P,roleMap:F,st:c,onSort:e=>g({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,w.jsx)(E,{name:e.q.name,dbType:e.dbType})})}):(0,w.jsx)(`div`,{className:`empty`,children:(0,w.jsx)(`span`,{children:`No queries match these filters.`})})}else{let i=n?`Nothing matches “${n}”.`:c.table?`“${c.table}” on ${c.conn} is not exposed by any saved query yet.`:c.conn?`No saved query uses “${c.conn}” yet.`:`Nothing matches these filters.`;V=(0,w.jsxs)(`div`,{className:`empty`,children:[(0,w.jsx)(`span`,{children:i}),!n&&c.conn?(0,w.jsx)(`button`,{type:`button`,className:`btn primary`,onClick:()=>{let n=S(c.conn)===`mongo`,i=c.table?n?JSON.stringify({collection:c.table,filter:{}},null,2):`SELECT * FROM `+c.table:void 0;e(`/queries`),t({title:`New API`,kicker:`POST /api/v1/queries`,content:(0,w.jsx)(C.Suspense,{fallback:(0,w.jsx)(r,{}),children:(0,w.jsx)(D,{starter:{connection:c.conn,sql:i}})})})},children:`New API`+(c.table?` on `+c.table:``)}):null]})}}return(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:`Access map`}),(0,w.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,w.jsxs)(`span`,{className:`legend`,children:[(0,w.jsx)(`span`,{className:`amap-dot`,children:`Q`}),` named query \xA0`,(0,w.jsx)(`span`,{className:`amap-dot`,children:`C`}),` `,`collection \xA0`,(0,w.jsx)(`span`,{className:`amap-dot conn`,children:`W`}),` whole connection`]})]})]}),(0,w.jsx)(`span`,{className:`spacer`}),(0,w.jsxs)(`select`,{id:`accessmap-conn-filter`,"aria-label":`Connection`,value:v.some(e=>e.name===c.conn)?c.conn:``,onChange:e=>g({conn:e.target.value,table:``,tableMatches:null}),children:[(0,w.jsx)(`option`,{value:``,children:`All connections`}),[...v].sort((e,t)=>e.name<t.name?-1:1).map(e=>(0,w.jsx)(`option`,{value:e.name,children:e.name},e.name))]}),(0,w.jsx)(`select`,{id:`accessmap-table-filter`,"aria-label":`Table`,disabled:z,value:O.some(e=>e.name===c.table)?c.table:``,onChange:e=>g({table:e.target.value,tableMatches:null}),children:R}),(0,w.jsxs)(`select`,{id:`accessmap-db-filter`,"aria-label":`Database`,value:M.includes(c.db)?c.db:``,onChange:e=>g({db:e.target.value}),children:[(0,w.jsx)(`option`,{value:``,children:`All databases`}),M.map(e=>(0,w.jsx)(`option`,{value:e,children:e},e))]}),(0,w.jsxs)(`select`,{id:`accessmap-reach-filter`,"aria-label":`Reach`,value:c.reach,onChange:e=>g({reach:e.target.value}),children:[(0,w.jsx)(`option`,{value:``,children:`Any reach`}),(0,w.jsx)(`option`,{value:`reachable`,children:`Reachable by a key`}),(0,w.jsx)(`option`,{value:`unreachable`,children:`Reachable by no key`})]}),(0,w.jsx)(`input`,{id:`accessmap-filter`,className:`search`,type:`search`,placeholder:`Filter by query or key…`,value:c.q,onChange:e=>g({q:e.target.value})})]}),(0,w.jsx)(`div`,{id:`accessmap-body`,className:`panel`,children:V})]})}function N({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,d=(e,t=null,n=null)=>u(e,t,n)?l.dir===1?` ▲`:` ▼`:``,f=(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})},p=(e,t,n)=>(0,w.jsx)(`th`,{className:t,onClick:()=>f(n),children:e+d(n)},n),m=(e,t,n,r)=>(0,w.jsxs)(`th`,{className:`amap-key-head amap-sortable`+(n?` revoked`:``),title:n?e+n:e,onClick:()=>f(`col`,r,e),children:[(0,w.jsxs)(`span`,{className:`amap-key-name`,children:[e,d(`col`,r,e),n?(0,w.jsx)(`span`,{className:`dim`,children:n}):null]}),(0,w.jsxs)(`span`,{className:`amap-key-sub`,children:[(t.allow_writes?`Read/write`:`Read-only`)+` · `+(t.rate_limit||`server default`),t.mask?.length?(0,w.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=h(e,t.name,t.collection,t.connection_name);if(!i.length)return(0,w.jsx)(`td`,{className:`amap-cell`},r);let a=[...new Set(i.map(e=>O[e.kind]))],o=a.length===1&&a[0]===`W`;return(0,w.jsx)(`td`,{className:`amap-cell`,title:i.map(e=>e.label).join(`, `),children:(0,w.jsx)(`span`,{className:`amap-dot`+(n?` role`:``)+(o?` conn`:``),children:a.join(`·`)})},r)},v=j();return(0,w.jsx)(`div`,{style:{overflow:`auto`},children:(0,w.jsxs)(`table`,{className:`amap`,children:[(0,w.jsxs)(`thead`,{children:[(0,w.jsxs)(`tr`,{children:[(0,w.jsx)(`th`,{colSpan:3+k.length}),t.length?(0,w.jsx)(`th`,{colSpan:t.length,className:`amap-group`,children:`API KEYS`}):null,n.length?(0,w.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,w.jsxs)(`tr`,{children:[p(`Query`,`amap-query amap-sortable`,`query`),A.map((e,t)=>p(k[t],`amap-sortable`,e)),p(`Connection`,`amap-sortable`,`connection`),p(`Reach`,`num amap-sortable`,`reach`),t.map(e=>{let t=r[e],n=!!(t.expires_at&&t.expires_at<v);return m(e,t,t.active?n?` (expired ${t.expires_at})`:``:` (revoked)`,`key`)}),n.map(e=>m(e,i[e],``,`role`))]})]}),(0,w.jsx)(`tbody`,{children:e.map(e=>{let a=e.q;return(0,w.jsxs)(`tr`,{children:[(0,w.jsxs)(`td`,{className:`amap-query`,children:[(0,w.jsxs)(`div`,{className:`amap-q-row`,children:[(0,w.jsx)(`button`,{type:`button`,className:`amap-q-name`,title:`Open ${a.name} in API Repository`,onClick:()=>s(a.name),children:a.name}),(0,w.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,w.jsx)(`span`,{className:`amap-coll`,children:a.collection}):null]}),(0,w.jsx)(`td`,{children:e.dbType?(0,w.jsx)(`span`,{className:`tag`,children:e.dbType}):(0,w.jsx)(`span`,{className:`dim`,children:`—`})}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(g,{value:a.created_at})}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(g,{value:a.updated_at})}),(0,w.jsx)(`td`,{className:a.last_used_at?`mono`:`mono dim`,children:(0,w.jsx)(g,{value:a.last_used_at,fallback:`never`})}),(0,w.jsx)(`td`,{className:`mono`,children:`v`+a.latest_version}),(0,w.jsx)(`td`,{className:`mono dim`,children:a.connection_name||`—`}),(0,w.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,w.jsx)(`tfoot`,{children:(0,w.jsxs)(`tr`,{children:[(0,w.jsx)(`td`,{className:`amap-query`,children:`Reaches`}),k.map(e=>(0,w.jsx)(`td`,{},e)),(0,w.jsx)(`td`,{}),(0,w.jsx)(`td`,{}),t.map(t=>(0,w.jsx)(`td`,{className:`num`,children:`${e.filter(e=>h(r[t],e.q.name,e.q.collection,e.q.connection_name).length).length} / ${e.length}`},t)),n.map(t=>(0,w.jsx)(`td`,{className:`num`,children:`${e.filter(e=>h(i[t],e.q.name,e.q.collection,e.q.connection_name).length).length} / ${e.length}`},t))]})})]})})}export{M as AccessMapPage};
@@ -1 +1 @@
1
- import{t as e}from"./useMutation-Df0q777p.js";import{$ as t,A as n,C as r,D as i,F as a,H as o,L as s,N as c,O as l,P as u,Q as d,S as f,X as p,Z as m,c as h,d as g,k as _,m as v}from"./index-C7ega31V.js";import{t as y}from"./Time-CZQinVk_.js";import{t as b}from"./forms-qgM6Rab7.js";import{n as x,t as S}from"./icons-CDledy67.js";var C=t(d(),1),w=m(),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=>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}=s();return t=>t instanceof f?e(t.message,{status:t.status}):e(t.message)}function j(e){let t=g(`admins.read`,`other administrators`);return o({queryKey:[`administrators`],queryFn:async()=>(await t.check(),i(await r.GET(`/api/v1/administrators`)).items),enabled:e,retry:!1})}function M(e){return o({queryKey:[`administrators`,e,`tokens`],queryFn:async()=>i(await r.GET(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:e}}})).items,retry:!1})}function N(){let e=v(),t=h(),n=!!e.data&&t(`admins.read`),r=j(n),{openDrawer:i}=s(),o=()=>i({title:`New administrator`,content:(0,w.jsx)(I,{})}),c;return c=e.isPending?(0,w.jsx)(a,{}):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)(a,{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:o,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:o,children:`New administrator`})]}),(0,w.jsx)(`div`,{id:`administrators-table`,className:`panel`,children:c})]})}function P({items:e,self:t}){let{openDrawer:n,toast:i}=s(),a=A(),o=p(),c=async e=>{if(window.confirm(`Remove administrator '${e}'? Every token of theirs stops working immediately.`))try{l(await r.DELETE(`/api/v1/administrators/{name}`,{params:{path:{name:e}}})),i(`Removed `+e),o.invalidateQueries({queryKey:[`administrators`]})}catch(e){a(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:()=>n({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:()=>n({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 c(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=o({queryKey:[`administrators`,`detail`,e],queryFn:async()=>_(await r.GET(`/api/v1/administrators/{name}`,{params:{path:{name:e}}})),enabled:!!e,retry:!1,gcTime:0});return e&&t.isPending?(0,w.jsx)(a,{}):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:t,existing:a,etag:o}){let l=!!t,{closeDrawer:d,openDrawer:f,showError:m,toast:h}=s(),g=A(),_=p(),[v,y]=(0,C.useState)(t??``),[x,S]=(0,C.useState)(a?.role??`developer`),[E,j]=(0,C.useState)(a?.email??``),[M,N]=(0,C.useState)(!a||a.active),P=n(),F=a?.sso?.status===`linked`,[I,L]=(0,C.useState)(a?.sso?.status??`none`),R=e({mutationFn:async()=>{let e={role:x,email:E.trim()||null};if(l)return i(await r.PATCH(`/api/v1/administrators/{name}`,{params:{path:{name:t},header:o?{"If-Match":o}:{}},body:{...F?{email:e.email}:e,active:M,...I===(a?.sso?.status??`none`)?{}:{sso:I===`pending`?`pending`:null}}})),null;let n=i(await r.POST(`/api/v1/administrators`,{body:{name:v.trim(),...e}}));return i(await r.POST(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:n.name}},body:{label:`first token`,expires_at:O(D)}}))},onSuccess:e=>{if(m(``),_.invalidateQueries({queryKey:[`administrators`]}),!e){d(),h(`Updated `+t);return}f({title:`Administrator created`,kicker:e.admin,content:(0,w.jsx)(b,{secret:e.secret,label:`Admin token for ${e.admin} (expires ${e.expires_at})`,hint:k})})},onError:g});return(0,w.jsxs)(`form`,{className:`form`,noValidate:!0,onSubmit:e=>{if(e.preventDefault(),!l&&!v.trim()){m(`A name is required.`,{errors:{name:`This field is required`}});return}R.mutate()},children:[(0,w.jsx)(c,{id:`a-name`,label:`Name`,hint:l?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:l,autoFocus:!l,onChange:e=>y(e.target.value)})}),(0,w.jsx)(c,{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)(c,{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)})}),l&&(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`})]}),l&&P.data?.sso&&(0,w.jsx)(c,{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`})]})}),!l&&(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)(u,{onCancel:d,children:(0,w.jsx)(`button`,{type:`submit`,className:`btn primary`,disabled:R.isPending,children:l?`Save`:`Create`})})]})}function R({name:t}){let n=M(t),o=p(),{toast:u}=s(),d=A(),[f,m]=(0,C.useState)(``),[h,g]=(0,C.useState)(O(D)),[_,v]=(0,C.useState)(null),x=()=>void o.invalidateQueries({queryKey:[`administrators`]}),S=e({mutationFn:async()=>i(await r.POST(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:t}},body:{label:f.trim()||null,expires_at:h||null}})),onSuccess:e=>{v(e),m(``),x()},onError:d}),T=async e=>{if(window.confirm(`Revoke ${e.label?`'${e.label}'`:e.id}? Anything using it stops working now.`))try{l(await r.DELETE(`/api/v1/administrators/{name}/tokens/{token_id}`,{params:{path:{name:t,token_id:e.id}}})),u(`Revoked `+(e.label??e.id)),x()}catch(e){d(e)}},E=new Date().toISOString().slice(0,10);return(0,w.jsxs)(`div`,{className:`form`,id:`admin-tokens`,children:[n.isPending?(0,w.jsx)(a,{text:`Loading tokens…`}):n.isError?(0,w.jsx)(`div`,{className:`hint`,children:n.error.message}):n.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:n.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.`}),_?(0,w.jsx)(b,{secret:_.secret,label:`New token${_.label?` '${_.label}'`:``} (${_.expires_at?`expires ${_.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)(c,{id:`t-label`,label:`New token: label`,hint:`What it's for - laptop, ci.`,children:(0,w.jsx)(`input`,{id:`t-label`,value:f,autoComplete:`off`,onChange:e=>m(e.target.value)})}),(0,w.jsx)(c,{id:`t-expires`,label:`Expires`,hint:`Leave blank for no expiry.`,children:(0,w.jsx)(`input`,{id:`t-expires`,type:`date`,value:h,onChange:e=>g(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{t as e}from"./useMutation-BcyO6Hjg.js";import{$ as t,A as n,C as r,D as i,F as a,H as o,L as s,N as c,O as l,P as u,Q as d,S as f,X as p,Z as m,c as h,d as g,k as _,m as v}from"./index-DvCopLMA.js";import{t as y}from"./Time-KeJDv2te.js";import{t as b}from"./forms-StJt-xsd.js";import{n as x,t as S}from"./icons-CeIi0TUG.js";var C=t(d(),1),w=m(),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=>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}=s();return t=>t instanceof f?e(t.message,{status:t.status}):e(t.message)}function j(e){let t=g(`admins.read`,`other administrators`);return o({queryKey:[`administrators`],queryFn:async()=>(await t.check(),i(await r.GET(`/api/v1/administrators`)).items),enabled:e,retry:!1})}function M(e){return o({queryKey:[`administrators`,e,`tokens`],queryFn:async()=>i(await r.GET(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:e}}})).items,retry:!1})}function N(){let e=v(),t=h(),n=!!e.data&&t(`admins.read`),r=j(n),{openDrawer:i}=s(),o=()=>i({title:`New administrator`,content:(0,w.jsx)(I,{})}),c;return c=e.isPending?(0,w.jsx)(a,{}):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)(a,{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:o,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:o,children:`New administrator`})]}),(0,w.jsx)(`div`,{id:`administrators-table`,className:`panel`,children:c})]})}function P({items:e,self:t}){let{openDrawer:n,toast:i}=s(),a=A(),o=p(),c=async e=>{if(window.confirm(`Remove administrator '${e}'? Every token of theirs stops working immediately.`))try{l(await r.DELETE(`/api/v1/administrators/{name}`,{params:{path:{name:e}}})),i(`Removed `+e),o.invalidateQueries({queryKey:[`administrators`]})}catch(e){a(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:()=>n({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:()=>n({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 c(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=o({queryKey:[`administrators`,`detail`,e],queryFn:async()=>_(await r.GET(`/api/v1/administrators/{name}`,{params:{path:{name:e}}})),enabled:!!e,retry:!1,gcTime:0});return e&&t.isPending?(0,w.jsx)(a,{}):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:t,existing:a,etag:o}){let l=!!t,{closeDrawer:d,openDrawer:f,showError:m,toast:h}=s(),g=A(),_=p(),[v,y]=(0,C.useState)(t??``),[x,S]=(0,C.useState)(a?.role??`developer`),[E,j]=(0,C.useState)(a?.email??``),[M,N]=(0,C.useState)(!a||a.active),P=n(),F=a?.sso?.status===`linked`,[I,L]=(0,C.useState)(a?.sso?.status??`none`),R=e({mutationFn:async()=>{let e={role:x,email:E.trim()||null};if(l)return i(await r.PATCH(`/api/v1/administrators/{name}`,{params:{path:{name:t},header:o?{"If-Match":o}:{}},body:{...F?{email:e.email}:e,active:M,...I===(a?.sso?.status??`none`)?{}:{sso:I===`pending`?`pending`:null}}})),null;let n=i(await r.POST(`/api/v1/administrators`,{body:{name:v.trim(),...e}}));return i(await r.POST(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:n.name}},body:{label:`first token`,expires_at:O(D)}}))},onSuccess:e=>{if(m(``),_.invalidateQueries({queryKey:[`administrators`]}),!e){d(),h(`Updated `+t);return}f({title:`Administrator created`,kicker:e.admin,content:(0,w.jsx)(b,{secret:e.secret,label:`Admin token for ${e.admin} (expires ${e.expires_at})`,hint:k})})},onError:g});return(0,w.jsxs)(`form`,{className:`form`,noValidate:!0,onSubmit:e=>{if(e.preventDefault(),!l&&!v.trim()){m(`A name is required.`,{errors:{name:`This field is required`}});return}R.mutate()},children:[(0,w.jsx)(c,{id:`a-name`,label:`Name`,hint:l?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:l,autoFocus:!l,onChange:e=>y(e.target.value)})}),(0,w.jsx)(c,{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)(c,{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)})}),l&&(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`})]}),l&&P.data?.sso&&(0,w.jsx)(c,{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`})]})}),!l&&(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)(u,{onCancel:d,children:(0,w.jsx)(`button`,{type:`submit`,className:`btn primary`,disabled:R.isPending,children:l?`Save`:`Create`})})]})}function R({name:t}){let n=M(t),o=p(),{toast:u}=s(),d=A(),[f,m]=(0,C.useState)(``),[h,g]=(0,C.useState)(O(D)),[_,v]=(0,C.useState)(null),x=()=>void o.invalidateQueries({queryKey:[`administrators`]}),S=e({mutationFn:async()=>i(await r.POST(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:t}},body:{label:f.trim()||null,expires_at:h||null}})),onSuccess:e=>{v(e),m(``),x()},onError:d}),T=async e=>{if(window.confirm(`Revoke ${e.label?`'${e.label}'`:e.id}? Anything using it stops working now.`))try{l(await r.DELETE(`/api/v1/administrators/{name}/tokens/{token_id}`,{params:{path:{name:t,token_id:e.id}}})),u(`Revoked `+(e.label??e.id)),x()}catch(e){d(e)}},E=new Date().toISOString().slice(0,10);return(0,w.jsxs)(`div`,{className:`form`,id:`admin-tokens`,children:[n.isPending?(0,w.jsx)(a,{text:`Loading tokens…`}):n.isError?(0,w.jsx)(`div`,{className:`hint`,children:n.error.message}):n.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:n.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.`}),_?(0,w.jsx)(b,{secret:_.secret,label:`New token${_.label?` '${_.label}'`:``} (${_.expires_at?`expires ${_.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)(c,{id:`t-label`,label:`New token: label`,hint:`What it's for - laptop, ci.`,children:(0,w.jsx)(`input`,{id:`t-label`,value:f,autoComplete:`off`,onChange:e=>m(e.target.value)})}),(0,w.jsx)(c,{id:`t-expires`,label:`Expires`,hint:`Leave blank for no expiry.`,children:(0,w.jsx)(`input`,{id:`t-expires`,type:`date`,value:h,onChange:e=>g(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};
@@ -0,0 +1 @@
1
+ import{$ as e,C as t,D as n,F as r,H as i,Q as a,Z as o}from"./index-DvCopLMA.js";import{t as s}from"./Time-KeJDv2te.js";import{t as c}from"./CodeBox-DuaKuoNl.js";var l=e(a(),1),u=o(),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,a]=(0,l.useState)(`24h`),[o,c]=(0,l.useState)(null),m=d.find(t=>t.id===e).hours,g=i({queryKey:[`usage`,`agents`,e],queryFn:async()=>n(await t.GET(`/api/v1/usage/agents`,{params:{query:{hours:m}}})),retry:!1}),_=g.data?.items??[],v;v=g.isPending?(0,u.jsx)(r,{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":o===e.agent,onClick:()=>c(o===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:``})})]}),o===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=>a(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{$ as e,B as t,F as n,Q as r,Z as i,_ as a,b as o,x as s,y as c,z as l}from"./index-C7ega31V.js";import{t as u}from"./Time-CZQinVk_.js";import{n as d}from"./state-O13cl7cO.js";var f=e(r(),1),p=i(),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=l();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:()=>o(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=l(),r=t(),i=a.find(e=>e.id===r.tab),{query:o,active:c,dismissed:d}=s(),[m,h]=(0,f.useState)(!1),g=e=>!i||i.kinds.includes(e.kind),y=c.filter(g),b=d.filter(g),x=e=>c.filter(t=>!e||e.includes(t.kind)),S;S=o.isPending?(0,p.jsx)(n,{text:`Checking…`}):o.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:o.error.message}),(0,p.jsx)(`button`,{type:`button`,className:`btn sm`,onClick:()=>void o.refetch(),children:`Retry`})]})}):(0,p.jsxs)(p.Fragment,{children:[i&&(0,p.jsx)(`p`,{className:`alerts-about`,children:i.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:i?`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,n,r)=>(0,p.jsxs)(`button`,{type:`button`,role:`tab`,"aria-selected":(i?.id??`all`)===t,className:`minitab`+((i?.id??`all`)===t?` active`:``),onClick:()=>e(t===`all`?`/alerts`:`/alerts/`+t),children:[n,o.data?(0,p.jsx)(`span`,{className:`n`+(r.length?` on`:``)+(r.some(e=>e.severity===`critical`)?` critical`:``),children:r.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.`,o.data?(0,p.jsxs)(p.Fragment,{children:[` Last checked `,(0,p.jsx)(u,{value:o.data.checked_at}),`.`]}):null]})]}),(0,p.jsx)(`span`,{className:`spacer`}),(0,p.jsx)(`button`,{id:`refresh-alerts`,type:`button`,className:`btn`,onClick:()=>void o.refetch(),children:`Check now`})]}),(0,p.jsxs)(`div`,{className:`minitabs alerts-tabs`,role:`tablist`,"aria-label":`Checks`,children:[C(`all`,`All`,x()),a.map(e=>C(e.id,e.label,x(e.kinds)))]}),S]})}export{y as AlertsPage};
1
+ import{$ as e,B as t,F as n,Q as r,Z as i,_ as a,b as o,x as s,y as c,z as l}from"./index-DvCopLMA.js";import{t as u}from"./Time-KeJDv2te.js";import{n as d}from"./state-O13cl7cO.js";var f=e(r(),1),p=i(),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=l();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:()=>o(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=l(),r=t(),i=a.find(e=>e.id===r.tab),{query:o,active:c,dismissed:d}=s(),[m,h]=(0,f.useState)(!1),g=e=>!i||i.kinds.includes(e.kind),y=c.filter(g),b=d.filter(g),x=e=>c.filter(t=>!e||e.includes(t.kind)),S;S=o.isPending?(0,p.jsx)(n,{text:`Checking…`}):o.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:o.error.message}),(0,p.jsx)(`button`,{type:`button`,className:`btn sm`,onClick:()=>void o.refetch(),children:`Retry`})]})}):(0,p.jsxs)(p.Fragment,{children:[i&&(0,p.jsx)(`p`,{className:`alerts-about`,children:i.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:i?`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,n,r)=>(0,p.jsxs)(`button`,{type:`button`,role:`tab`,"aria-selected":(i?.id??`all`)===t,className:`minitab`+((i?.id??`all`)===t?` active`:``),onClick:()=>e(t===`all`?`/alerts`:`/alerts/`+t),children:[n,o.data?(0,p.jsx)(`span`,{className:`n`+(r.length?` on`:``)+(r.some(e=>e.severity===`critical`)?` critical`:``),children:r.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.`,o.data?(0,p.jsxs)(p.Fragment,{children:[` Last checked `,(0,p.jsx)(u,{value:o.data.checked_at}),`.`]}):null]})]}),(0,p.jsx)(`span`,{className:`spacer`}),(0,p.jsx)(`button`,{id:`refresh-alerts`,type:`button`,className:`btn`,onClick:()=>void o.refetch(),children:`Check now`})]}),(0,p.jsxs)(`div`,{className:`minitabs alerts-tabs`,role:`tablist`,"aria-label":`Checks`,children:[C(`all`,`All`,x()),a.map(e=>C(e.id,e.label,x(e.kinds)))]}),S]})}export{y as AlertsPage};