@pyric/cli 0.1.0-alpha.26 → 0.1.0-alpha.28

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 (340) hide show
  1. package/README.md +1 -1
  2. package/dist/assurance/.generated/conformance-verdicts.d.ts +5 -0
  3. package/dist/assurance/.generated/conformance-verdicts.d.ts.map +1 -1
  4. package/dist/assurance/.generated/conformance-verdicts.js +5 -0
  5. package/dist/assurance/.generated/conformance-verdicts.js.map +1 -1
  6. package/dist/assurance/capabilities.d.ts.map +1 -1
  7. package/dist/assurance/capabilities.js +24 -5
  8. package/dist/assurance/capabilities.js.map +1 -1
  9. package/dist/assurance/runner.d.ts.map +1 -1
  10. package/dist/assurance/runner.js +37 -50
  11. package/dist/assurance/runner.js.map +1 -1
  12. package/dist/assurance/types.d.ts +5 -0
  13. package/dist/assurance/types.d.ts.map +1 -1
  14. package/dist/assurance/types.js.map +1 -1
  15. package/dist/assurance/validation.d.ts +2 -16
  16. package/dist/assurance/validation.d.ts.map +1 -1
  17. package/dist/assurance/validation.js +121 -1
  18. package/dist/assurance/validation.js.map +1 -1
  19. package/dist/bridge/surface/arguments/assurance.d.ts +31 -6
  20. package/dist/bridge/surface/arguments/assurance.d.ts.map +1 -1
  21. package/dist/bridge/surface/arguments/assurance.js +1 -0
  22. package/dist/bridge/surface/arguments/assurance.js.map +1 -1
  23. package/dist/bridge/surface/arguments/messaging.d.ts +2 -2
  24. package/dist/bridge/surface/methods/assurance/define.d.ts +2 -2
  25. package/dist/bridge/surface/methods/assurance/map.d.ts +29 -6
  26. package/dist/bridge/surface/methods/assurance/map.d.ts.map +1 -1
  27. package/dist/bridge/surface/methods/assurance/propose.d.ts +7 -0
  28. package/dist/bridge/surface/methods/assurance/propose.d.ts.map +1 -1
  29. package/dist/bridge/surface/methods/messaging/send.d.ts +4 -4
  30. package/dist/bridge/surface/render/discriminator-schemas.d.ts +6 -6
  31. package/dist/cli/database-rules.js +1 -1
  32. package/dist/cli/database-rules.js.map +1 -1
  33. package/dist/cli/index.d.ts +1 -0
  34. package/dist/cli/index.d.ts.map +1 -1
  35. package/dist/cli/index.js +28 -3
  36. package/dist/cli/index.js.map +1 -1
  37. package/dist/cli/pyric-config.d.ts +8 -0
  38. package/dist/cli/pyric-config.d.ts.map +1 -1
  39. package/dist/cli/pyric-config.js.map +1 -1
  40. package/dist/cli/run.d.ts +42 -0
  41. package/dist/cli/run.d.ts.map +1 -0
  42. package/dist/cli/run.js +115 -0
  43. package/dist/cli/run.js.map +1 -0
  44. package/dist/cli/serve.d.ts.map +1 -1
  45. package/dist/cli/serve.js +5 -3
  46. package/dist/cli/serve.js.map +1 -1
  47. package/dist/cli/verify.d.ts +6 -0
  48. package/dist/cli/verify.d.ts.map +1 -1
  49. package/dist/cli/verify.js +240 -15
  50. package/dist/cli/verify.js.map +1 -1
  51. package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
  52. package/dist/conformance/.generated/can-i-use.js +1 -1
  53. package/dist/conformance/.generated/can-i-use.js.map +1 -1
  54. package/dist/conformance/.generated/conformance-docs.d.ts.map +1 -1
  55. package/dist/conformance/.generated/conformance-docs.js +1 -1
  56. package/dist/conformance/.generated/conformance-docs.js.map +1 -1
  57. package/dist/remote/local-storage-url.d.ts +2 -15
  58. package/dist/remote/local-storage-url.d.ts.map +1 -1
  59. package/dist/remote/local-storage-url.js +1 -60
  60. package/dist/remote/local-storage-url.js.map +1 -1
  61. package/dist/serve/assets/avatar-route.d.ts.map +1 -1
  62. package/dist/serve/assets/avatar-route.js +6 -6
  63. package/dist/serve/assets/avatar-route.js.map +1 -1
  64. package/dist/serve/assets/resolver.d.ts +3 -0
  65. package/dist/serve/assets/resolver.d.ts.map +1 -1
  66. package/dist/serve/assets/resolver.js +1 -1
  67. package/dist/serve/assets/resolver.js.map +1 -1
  68. package/dist/serve/entries/app-client.d.ts.map +1 -1
  69. package/dist/serve/entries/app-client.js +6 -0
  70. package/dist/serve/entries/app-client.js.map +1 -1
  71. package/dist/serve/repro/denials.d.ts.map +1 -1
  72. package/dist/serve/repro/denials.js +2 -1
  73. package/dist/serve/repro/denials.js.map +1 -1
  74. package/dist/serve/site-ui/_astro/{app.DFdWhpVY.js → app.BCNb6gMq.js} +1 -1
  75. package/dist/serve/site-ui/_astro/app.DhoRPDdN.js +147 -0
  76. package/dist/serve/site-ui/_astro/{seed.vvXV1ioI.js → seed.BO5CXcqE.js} +1 -1
  77. package/dist/serve/site-ui/auth/index.html +1 -1
  78. package/dist/serve/site-ui/docs/_rhythm/index.html +1 -1
  79. package/dist/serve/site-ui/docs/agent/set-up-your-agent/index.html +1 -1
  80. package/dist/serve/site-ui/docs/agent/watch-and-review/index.html +1 -1
  81. package/dist/serve/site-ui/docs/agent/work-with-an-agent/index.html +1 -1
  82. package/dist/serve/site-ui/docs/ai-compat/index.html +1 -1
  83. package/dist/serve/site-ui/docs/api-reference/index.html +2 -1
  84. package/dist/serve/site-ui/docs/api-reference.md +1 -0
  85. package/dist/serve/site-ui/docs/app-compat/index.html +1 -1
  86. package/dist/serve/site-ui/docs/auth-compat/index.html +2 -2
  87. package/dist/serve/site-ui/docs/auth-compat.md +1 -1
  88. package/dist/serve/site-ui/docs/auth-flutter-compat/index.html +1 -1
  89. package/dist/serve/site-ui/docs/auth-kotlin-compat/index.html +1 -1
  90. package/dist/serve/site-ui/docs/auth-swift-compat/index.html +1 -1
  91. package/dist/serve/site-ui/docs/build/ai-logic/index.html +1 -1
  92. package/dist/serve/site-ui/docs/build/authentication/index.html +1 -1
  93. package/dist/serve/site-ui/docs/build/cloud-firestore/index.html +1 -1
  94. package/dist/serve/site-ui/docs/build/cloud-messaging/index.html +1 -1
  95. package/dist/serve/site-ui/docs/build/cloud-storage/index.html +13 -2
  96. package/dist/serve/site-ui/docs/build/cloud-storage.md +23 -0
  97. package/dist/serve/site-ui/docs/build/default-avatars/index.html +1 -1
  98. package/dist/serve/site-ui/docs/build/inspect-activity/index.html +1 -1
  99. package/dist/serve/site-ui/docs/build/realtime-database/index.html +1 -1
  100. package/dist/serve/site-ui/docs/conformance-scores/index.html +1 -1
  101. package/dist/serve/site-ui/docs/create-pyric-reference-api/index.html +1 -1
  102. package/dist/serve/site-ui/docs/database-compat/index.html +5 -1
  103. package/dist/serve/site-ui/docs/database-compat.md +4 -0
  104. package/dist/serve/site-ui/docs/examples/chess/index.html +1 -1
  105. package/dist/serve/site-ui/docs/examples/index.html +1 -1
  106. package/dist/serve/site-ui/docs/firestore-compat/index.html +1 -1
  107. package/dist/serve/site-ui/docs/firestore-flutter-compat/index.html +1 -1
  108. package/dist/serve/site-ui/docs/firestore-kotlin-compat/index.html +1 -1
  109. package/dist/serve/site-ui/docs/firestore-swift-compat/index.html +1 -1
  110. package/dist/serve/site-ui/docs/functions-rtdb-compat/index.html +1 -1
  111. package/dist/serve/site-ui/docs/get-started/backend-bundlers/index.html +1 -1
  112. package/dist/serve/site-ui/docs/get-started/flutter/index.html +1 -1
  113. package/dist/serve/site-ui/docs/get-started/how-the-swap-works/index.html +1 -1
  114. package/dist/serve/site-ui/docs/get-started/nextjs/index.html +1 -1
  115. package/dist/serve/site-ui/docs/get-started/node-host/index.html +1 -1
  116. package/dist/serve/site-ui/docs/get-started/start-building/index.html +2 -1
  117. package/dist/serve/site-ui/docs/get-started/start-building.md +2 -0
  118. package/dist/serve/site-ui/docs/get-started/vite/index.html +4 -2
  119. package/dist/serve/site-ui/docs/get-started/vite.md +4 -0
  120. package/dist/serve/site-ui/docs/index.json +50 -0
  121. package/dist/serve/site-ui/docs/messaging-compat/index.html +1 -1
  122. package/dist/serve/site-ui/docs/observe/resolve-runtime-status/index.html +1 -1
  123. package/dist/serve/site-ui/docs/observe/see-whats-happening/index.html +1 -1
  124. package/dist/serve/site-ui/docs/observe/shape-your-data/index.html +1 -1
  125. package/dist/serve/site-ui/docs/observe/switch-and-impersonate-identities/index.html +1 -1
  126. package/dist/serve/site-ui/docs/overview/index.html +1 -1
  127. package/dist/serve/site-ui/docs/pyric-admin-app-check-reference-api/index.html +1 -1
  128. package/dist/serve/site-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
  129. package/dist/serve/site-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
  130. package/dist/serve/site-ui/docs/pyric-admin-data-connect-reference-api/index.html +1 -1
  131. package/dist/serve/site-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
  132. package/dist/serve/site-ui/docs/pyric-admin-eventarc-reference-api/index.html +1 -1
  133. package/dist/serve/site-ui/docs/pyric-admin-extensions-reference-api/index.html +1 -1
  134. package/dist/serve/site-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
  135. package/dist/serve/site-ui/docs/pyric-admin-functions-reference-api/index.html +1 -1
  136. package/dist/serve/site-ui/docs/pyric-admin-installations-reference-api/index.html +1 -1
  137. package/dist/serve/site-ui/docs/pyric-admin-instance-id-reference-api/index.html +1 -1
  138. package/dist/serve/site-ui/docs/pyric-admin-machine-learning-reference-api/index.html +1 -1
  139. package/dist/serve/site-ui/docs/pyric-admin-messaging-reference-api/index.html +1 -1
  140. package/dist/serve/site-ui/docs/pyric-admin-phone-number-verification-reference-api/index.html +1 -1
  141. package/dist/serve/site-ui/docs/pyric-admin-project-management-reference-api/index.html +1 -1
  142. package/dist/serve/site-ui/docs/pyric-admin-reference-api/index.html +1 -1
  143. package/dist/serve/site-ui/docs/pyric-admin-remote-config-reference-api/index.html +1 -1
  144. package/dist/serve/site-ui/docs/pyric-admin-security-rules-reference-api/index.html +1 -1
  145. package/dist/serve/site-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
  146. package/dist/serve/site-ui/docs/pyric-ai-reference-api/index.html +1 -1
  147. package/dist/serve/site-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
  148. package/dist/serve/site-ui/docs/pyric-analytics-reference-api/index.html +1 -1
  149. package/dist/serve/site-ui/docs/pyric-app-check-reference-api/index.html +1 -1
  150. package/dist/serve/site-ui/docs/pyric-app-reference-api/index.html +1 -1
  151. package/dist/serve/site-ui/docs/pyric-auth-reference-api/index.html +2 -2
  152. package/dist/serve/site-ui/docs/pyric-auth-reference-api.md +1 -1
  153. package/dist/serve/site-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
  154. package/dist/serve/site-ui/docs/pyric-cli-assurance-reference-api/index.html +24 -5
  155. package/dist/serve/site-ui/docs/pyric-cli-assurance-reference-api.md +11 -4
  156. package/dist/serve/site-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
  157. package/dist/serve/site-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
  158. package/dist/serve/site-ui/docs/pyric-cli-bundler-reference-api/index.html +1 -1
  159. package/dist/serve/site-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
  160. package/dist/serve/site-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
  161. package/dist/serve/site-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
  162. package/dist/serve/site-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
  163. package/dist/serve/site-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
  164. package/dist/serve/site-ui/docs/pyric-cli-flow-reference-api/index.html +1 -1
  165. package/dist/serve/site-ui/docs/pyric-cli-next-reference-api/index.html +1 -1
  166. package/dist/serve/site-ui/docs/pyric-cli-register-app-bridge-reference-api/index.html +1 -1
  167. package/dist/serve/site-ui/docs/pyric-cli-register-client-app-bridge-reference-api/index.html +1 -1
  168. package/dist/serve/site-ui/docs/pyric-cli-register-client-app-reference-api/index.html +1 -1
  169. package/dist/serve/site-ui/docs/pyric-cli-register-client-auth-reference-api/index.html +1 -1
  170. package/dist/serve/site-ui/docs/pyric-cli-register-client-database-reference-api/index.html +1 -1
  171. package/dist/serve/site-ui/docs/pyric-cli-register-client-firestore-reference-api/index.html +1 -1
  172. package/dist/serve/site-ui/docs/pyric-cli-register-client-storage-reference-api/index.html +1 -1
  173. package/dist/serve/site-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
  174. package/dist/serve/site-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
  175. package/dist/serve/site-ui/docs/pyric-cli-repro-reference-api/index.html +1 -1
  176. package/dist/serve/site-ui/docs/pyric-cli-serve-worker-reference-api/index.html +1 -1
  177. package/dist/serve/site-ui/docs/pyric-cli-verify-reference-api/index.html +16 -3
  178. package/dist/serve/site-ui/docs/pyric-cli-verify-reference-api.md +11 -10
  179. package/dist/serve/site-ui/docs/pyric-cli-vite-reference-api/index.html +17 -3
  180. package/dist/serve/site-ui/docs/pyric-cli-vite-reference-api.md +7 -5
  181. package/dist/serve/site-ui/docs/pyric-cli-vite-ssr-loader-reference-api/index.html +1 -1
  182. package/dist/serve/site-ui/docs/pyric-database-reference-api/index.html +1 -1
  183. package/dist/serve/site-ui/docs/pyric-firestore-lite-reference-api/index.html +1 -1
  184. package/dist/serve/site-ui/docs/pyric-firestore-reference-api/index.html +1 -1
  185. package/dist/serve/site-ui/docs/pyric-functions-reference-api/index.html +1 -1
  186. package/dist/serve/site-ui/docs/pyric-messaging-reference-api/index.html +1 -1
  187. package/dist/serve/site-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
  188. package/dist/serve/site-ui/docs/pyric-performance-reference-api/index.html +1 -1
  189. package/dist/serve/site-ui/docs/pyric-remote-config-reference-api/index.html +1 -1
  190. package/dist/serve/site-ui/docs/pyric-rules-reference-api/index.html +1 -1
  191. package/dist/serve/site-ui/docs/pyric-sandbox-branches-store-reference-api/index.html +1 -1
  192. package/dist/serve/site-ui/docs/pyric-sandbox-checkpoints-directory-reference-api/index.html +1 -1
  193. package/dist/serve/site-ui/docs/pyric-sandbox-checkpoints-reference-api/index.html +1 -1
  194. package/dist/serve/site-ui/docs/pyric-sandbox-database-reference-api/index.html +1 -1
  195. package/dist/serve/site-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
  196. package/dist/serve/site-ui/docs/pyric-sandbox-reference-api/index.html +1 -1
  197. package/dist/serve/site-ui/docs/pyric-storage-reference-api/index.html +1 -1
  198. package/dist/serve/site-ui/docs/pyric-storage-sw-reference-api/index.html +102 -0
  199. package/dist/serve/site-ui/docs/pyric-storage-sw-reference-api.md +76 -0
  200. package/dist/serve/site-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
  201. package/dist/serve/site-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
  202. package/dist/serve/site-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
  203. package/dist/serve/site-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
  204. package/dist/serve/site-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
  205. package/dist/serve/site-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
  206. package/dist/serve/site-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
  207. package/dist/serve/site-ui/docs/pyric-ui-listener-owner-reference-api/index.html +1 -1
  208. package/dist/serve/site-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
  209. package/dist/serve/site-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
  210. package/dist/serve/site-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
  211. package/dist/serve/site-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
  212. package/dist/serve/site-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
  213. package/dist/serve/site-ui/docs/pyric-ui-storage-reference-api/index.html +1 -1
  214. package/dist/serve/site-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
  215. package/dist/serve/site-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
  216. package/dist/serve/site-ui/docs/reference/cli/index.html +12 -2
  217. package/dist/serve/site-ui/docs/reference/cli.md +24 -0
  218. package/dist/serve/site-ui/docs/rtdb-flutter-compat/index.html +1 -1
  219. package/dist/serve/site-ui/docs/rtdb-kotlin-compat/index.html +1 -1
  220. package/dist/serve/site-ui/docs/rtdb-swift-compat/index.html +1 -1
  221. package/dist/serve/site-ui/docs/rules-compat/index.html +1 -1
  222. package/dist/serve/site-ui/docs/secure/audit-your-rules/index.html +1 -1
  223. package/dist/serve/site-ui/docs/secure/firestore-rules-limits/index.html +1 -1
  224. package/dist/serve/site-ui/docs/secure/read-a-denial/index.html +1 -1
  225. package/dist/serve/site-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
  226. package/dist/serve/site-ui/docs/secure/rtdb-rules-standard-library/index.html +1 -1
  227. package/dist/serve/site-ui/docs/secure/rules-standard-library/index.html +1 -1
  228. package/dist/serve/site-ui/docs/secure/secure-it-with-rules/index.html +1 -1
  229. package/dist/serve/site-ui/docs/secure/simulate-and-lint/index.html +1 -1
  230. package/dist/serve/site-ui/docs/secure/write-a-rules-test-suite/index.html +2 -2
  231. package/dist/serve/site-ui/docs/secure/write-a-rules-test-suite.md +1 -1
  232. package/dist/serve/site-ui/docs/ship/ship-to-production/index.html +5 -2
  233. package/dist/serve/site-ui/docs/ship/ship-to-production.md +7 -1
  234. package/dist/serve/site-ui/docs/ship/test-in-node/index.html +1 -1
  235. package/dist/serve/site-ui/docs/storage-compat/index.html +1 -1
  236. package/dist/serve/site-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
  237. package/dist/serve/site-ui/docs/trust/rules-standard-library/index.html +1 -1
  238. package/dist/serve/site-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
  239. package/dist/serve/site-ui/docs/tutorial/index.html +1 -1
  240. package/dist/serve/site-ui/examples/chess/index.html +1 -1
  241. package/dist/serve/site-ui/firestore/index.html +1 -1
  242. package/dist/serve/site-ui/llms.txt +1 -0
  243. package/dist/serve/site-ui/rtdb/index.html +1 -1
  244. package/dist/serve/site-ui/settings/index.html +1 -1
  245. package/dist/serve/site-ui/storage/index.html +1 -1
  246. package/dist/serve/site-ui/studio/index.html +1 -1
  247. package/dist/serve/site-ui/traffic/index.html +1 -1
  248. package/dist/serve/verify-hint.d.ts +2 -0
  249. package/dist/serve/verify-hint.d.ts.map +1 -0
  250. package/dist/serve/verify-hint.js +10 -0
  251. package/dist/serve/verify-hint.js.map +1 -0
  252. package/dist/serve/vite-generation-rules-watch.d.ts.map +1 -1
  253. package/dist/serve/vite-generation-rules-watch.js +5 -3
  254. package/dist/serve/vite-generation-rules-watch.js.map +1 -1
  255. package/dist/serve/vite-plugin.d.ts +6 -0
  256. package/dist/serve/vite-plugin.d.ts.map +1 -1
  257. package/dist/serve/vite-plugin.js +11 -1
  258. package/dist/serve/vite-plugin.js.map +1 -1
  259. package/dist/serve/vite-service-worker.d.ts +12 -0
  260. package/dist/serve/vite-service-worker.d.ts.map +1 -0
  261. package/dist/serve/vite-service-worker.js +33 -0
  262. package/dist/serve/vite-service-worker.js.map +1 -0
  263. package/dist/serve/worker/client/auth.d.ts.map +1 -1
  264. package/dist/serve/worker/client/auth.js +4 -3
  265. package/dist/serve/worker/client/auth.js.map +1 -1
  266. package/dist/serve/worker/host/auth-action-codes.js +2 -2
  267. package/dist/serve/worker/host/auth-action-codes.js.map +1 -1
  268. package/dist/serve/worker/host/auth-custom-token.d.ts.map +1 -1
  269. package/dist/serve/worker/host/auth-custom-token.js +4 -2
  270. package/dist/serve/worker/host/auth-custom-token.js.map +1 -1
  271. package/dist/serve/worker/host/dispatch.d.ts.map +1 -1
  272. package/dist/serve/worker/host/dispatch.js +3 -1
  273. package/dist/serve/worker/host/dispatch.js.map +1 -1
  274. package/dist/serve/worker/host-auth.d.ts +6 -0
  275. package/dist/serve/worker/host-auth.d.ts.map +1 -1
  276. package/dist/serve/worker/host-auth.js +16 -5
  277. package/dist/serve/worker/host-auth.js.map +1 -1
  278. package/dist/serve/worker/inbound-validation.d.ts.map +1 -1
  279. package/dist/serve/worker/inbound-validation.js +1 -0
  280. package/dist/serve/worker/inbound-validation.js.map +1 -1
  281. package/dist/serve/worker/protocol/storage.d.ts +1 -8
  282. package/dist/serve/worker/protocol/storage.d.ts.map +1 -1
  283. package/dist/serve/worker/protocol/storage.js +2 -14
  284. package/dist/serve/worker/protocol/storage.js.map +1 -1
  285. package/dist/serve/worker/protocol.d.ts +2 -0
  286. package/dist/serve/worker/protocol.d.ts.map +1 -1
  287. package/dist/serve/worker/protocol.js.map +1 -1
  288. package/dist/verify/index.d.ts +16 -2
  289. package/dist/verify/index.d.ts.map +1 -1
  290. package/dist/verify/index.js +100 -18
  291. package/dist/verify/index.js.map +1 -1
  292. package/dist/verify/staging/engine.d.ts +102 -0
  293. package/dist/verify/staging/engine.d.ts.map +1 -0
  294. package/dist/verify/staging/engine.js +371 -0
  295. package/dist/verify/staging/engine.js.map +1 -0
  296. package/dist/verify/staging/rest.d.ts +68 -0
  297. package/dist/verify/staging/rest.d.ts.map +1 -0
  298. package/dist/verify/staging/rest.js +93 -0
  299. package/dist/verify/staging/rest.js.map +1 -0
  300. package/dist/verify/staging/target.d.ts +39 -0
  301. package/dist/verify/staging/target.d.ts.map +1 -0
  302. package/dist/verify/staging/target.js +117 -0
  303. package/dist/verify/staging/target.js.map +1 -0
  304. package/package.json +12 -8
  305. package/src/assurance/.generated/conformance-verdicts.ts +5 -0
  306. package/src/assurance/capabilities.ts +22 -4
  307. package/src/assurance/runner.ts +40 -58
  308. package/src/assurance/types.ts +3 -0
  309. package/src/assurance/validation.ts +137 -0
  310. package/src/bridge/surface/arguments/assurance.ts +1 -0
  311. package/src/cli/database-rules.ts +1 -1
  312. package/src/cli/index.ts +28 -3
  313. package/src/cli/pyric-config.ts +5 -0
  314. package/src/cli/run.ts +139 -0
  315. package/src/cli/serve.ts +6 -3
  316. package/src/cli/verify.ts +244 -18
  317. package/src/conformance/.generated/can-i-use.ts +1 -1
  318. package/src/conformance/.generated/conformance-docs.ts +1 -1
  319. package/src/remote/local-storage-url.ts +1 -58
  320. package/src/serve/assets/avatar-route.ts +6 -7
  321. package/src/serve/assets/resolver.ts +4 -1
  322. package/src/serve/entries/app-client.ts +7 -0
  323. package/src/serve/repro/denials.ts +2 -1
  324. package/src/serve/verify-hint.ts +8 -0
  325. package/src/serve/vite-generation-rules-watch.ts +6 -3
  326. package/src/serve/vite-plugin.ts +15 -1
  327. package/src/serve/vite-service-worker.ts +44 -0
  328. package/src/serve/worker/client/auth.ts +4 -3
  329. package/src/serve/worker/host/auth-action-codes.ts +2 -2
  330. package/src/serve/worker/host/auth-custom-token.ts +4 -2
  331. package/src/serve/worker/host/dispatch.ts +2 -0
  332. package/src/serve/worker/host-auth.ts +18 -5
  333. package/src/serve/worker/inbound-validation.ts +1 -0
  334. package/src/serve/worker/protocol/storage.ts +2 -14
  335. package/src/serve/worker/protocol.ts +2 -0
  336. package/src/verify/index.ts +119 -19
  337. package/src/verify/staging/engine.ts +455 -0
  338. package/src/verify/staging/rest.ts +129 -0
  339. package/src/verify/staging/target.ts +137 -0
  340. package/dist/serve/site-ui/_astro/app.BpEphDR7.js +0 -147
package/src/cli/index.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * Subcommands:
6
6
  * pyric bridge [--port N] [--project ID]
7
7
  * pyric sandbox [flags] [--] [command...]
8
+ * pyric run [--sandbox URL] [--wait[=SECONDS]] -- <command...>
8
9
  * pyric init [dir] [--template web|node] [--name N] [--force] [--json]
9
10
  * pyric vendor [dir] [--json]
10
11
  * pyric snapshot [--out DIR] [--port N] [--force] [--json] [--include-passwords]
@@ -60,12 +61,15 @@ clients (Claude Code, Cursor, ...).
60
61
  USAGE
61
62
  pyric bridge [flags]
62
63
  pyric sandbox [flags] [--] [command...]
64
+ pyric run [--sandbox URL] [--wait[=SECONDS]] -- <command...>
63
65
  pyric init [dir] [--template=web|node]
64
66
  pyric sandbox salvage --source=DIR --out=NEW_DIR
65
67
  pyric snapshot [--out=DIR]
66
- pyric verify [fixture|dir] [--engine sandbox|rules-test-api|both]
68
+ pyric verify [fixture|dir] [--engine sandbox|rules-test-api|staging|both]
67
69
  pyric can-i-use <feature> [--json]
68
70
  pyric verify cases [fixture] [--service firestore] [--out FILE]
71
+ pyric verify staging init
72
+ pyric verify staging restore [--force]
69
73
  pyric firestore rules validate <path>
70
74
  pyric firestore rules resolve <path> [--out <path>]
71
75
  pyric firestore indexes generate <path...> [--out <path>]
@@ -89,6 +93,15 @@ COMMANDS
89
93
  configured in pyric.json; if none is configured, runs the sandbox host only.
90
94
  A firebase.json Functions source is discovered automatically;
91
95
  supported RTDB onValueCreated exports run in isolated Node.
96
+ run [--sandbox URL] Run a command against a sandbox host that is already
97
+ running (a Vite dev server with the pyric plugin, or
98
+ \`pyric sandbox\`), with the environment \`pyric sandbox\`
99
+ gives its child. --sandbox URL names the host; without
100
+ it, PYRIC_SANDBOX=remote:<url>, then .pyric/serve.json.
101
+ The register module is passed by absolute file URL, so
102
+ the command can run outside pyric's install tree.
103
+ --wait[=SECONDS] polls the host's health until it
104
+ answers (default 30s) before starting the command.
92
105
  init [dir] Scaffold a pyric project. --template=web (default; Vite app
93
106
  on \`@pyric/cli/vite\`), static (\`pyric sandbox\`), or node.
94
107
  --name=NAME --force (overwrite scaffold files) --json (machine
@@ -110,9 +123,12 @@ COMMANDS
110
123
  for the Firestore/RTDB services present in the fixture.
111
124
  No arg replays the latest \`pyric sandbox\` capture
112
125
  (.pyric/last-session.json). --service filters services.
113
- --engine sandbox (default), rules-test-api, or both.
126
+ --engine sandbox (default), rules-test-api, staging, or both.
114
127
  Hosted Rules Test API verification is Firestore-only
115
128
  and uses --project plus the configured Google credentials.
129
+ --engine staging replays RTDB against your own dedicated
130
+ instance (pyric.json verify.staging) so production decides
131
+ each operation; repeat --engine to compare with sandbox.
116
132
  --rules service=path overrides firebase.json resolution
117
133
  (repeat for mixed captures). Each RTDB instance the session
118
134
  used is checked against its own rules; --rules
@@ -123,6 +139,12 @@ COMMANDS
123
139
  Exact and fuzzy queries use the same model as MCP. --json.
124
140
  verify cases [fixture] Derive Firestore Rules Test API cases from a captured
125
141
  fixture and print JSON, or write with --out FILE.
142
+ verify staging init Enroll the empty RTDB instance pyric.json verify.staging
143
+ names ({ project, rtdb }) for --engine staging. Refuses an
144
+ instance that holds data or that firebase.json deploys to.
145
+ verify staging restore Put the staging instance back after an interrupted run, from
146
+ the rules kept in .pyric/staging-restore.json; --force
147
+ deploys deny-all rules with the marker when none were kept.
126
148
  firestore rules validate Validate Firestore rules structure against a file.
127
149
  firestore rules resolve Resolve Firestore 2+modules imports to one ruleset.
128
150
  firestore indexes generate Generate firestore.indexes.json from application source.
@@ -209,7 +231,7 @@ CORE FLAGS (sandbox)
209
231
  browser store in Studio under Settings, Reset, or use an
210
232
  incognito window) for a full reset.
211
233
  [command...] Run a command once the host is up, with PYRIC_SANDBOX set and
212
- NODE_OPTIONS extended with --import @pyric/cli/register
234
+ NODE_OPTIONS extended with --import <register module URL>
213
235
  so firebase-admin/firebase resolve to the sandbox. Put
214
236
  Pyric flags before the command. A bare -- separates the
215
237
  command and its flags. When omitted, pyric.json#command
@@ -389,6 +411,9 @@ export async function dispatch(parsed: ParsedArgs): Promise<number> {
389
411
  }
390
412
  const { runServe } = await import('./serve.js');
391
413
  return runServe(parsed);
414
+ case 'run':
415
+ const { runRun } = await import('./run.js');
416
+ return runRun(parsed);
392
417
  case 'snapshot':
393
418
  return await runSnapshot(parsed);
394
419
  case 'verify':
@@ -31,6 +31,11 @@ export interface PyricConfig {
31
31
  rules?: string | PyricRulesConfig;
32
32
  /** Project ID label for emulation. */
33
33
  project?: string;
34
+ /** `pyric verify` settings. */
35
+ verify?: {
36
+ /** The dedicated RTDB instance `pyric verify --engine staging` replays against. */
37
+ staging?: { project: string; rtdb: string };
38
+ };
34
39
  }
35
40
 
36
41
  export async function readPyricConfig(cwd: string = process.cwd()): Promise<PyricConfig> {
package/src/cli/run.ts ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * `pyric run [--sandbox <url>] -- <command...>`: run one command against a
3
+ * sandbox host that is already running, such as a Vite dev server with the
4
+ * pyric plugin or a `pyric sandbox` in another terminal.
5
+ *
6
+ * The child gets the environment `pyric sandbox` gives its own child
7
+ * (`buildChildEnv`): the activator, the host's identity, and `--import` with
8
+ * the absolute `file:` URL of this install's register module. Nothing in the
9
+ * child resolves `@pyric/cli` by name, so the command works from any working
10
+ * directory, including one outside the tree pyric is installed in.
11
+ */
12
+ import type { ParsedArgs } from './parse-args.js';
13
+ import {
14
+ buildChildEnv,
15
+ registerModuleUrl,
16
+ spawnSandboxChild,
17
+ type SandboxChildPlan,
18
+ } from './sandbox-runner.js';
19
+ import { discoverServe, probeHostUrl } from '../serve/discovery.js';
20
+
21
+ export interface RunInvocation {
22
+ /** The host URL named by `--sandbox` or `PYRIC_SANDBOX=remote:<url>`; null to discover it. */
23
+ readonly hostUrl: string | null;
24
+ readonly plan: SandboxChildPlan;
25
+ /** How long to wait for the host to answer before giving up; 0 probes once. */
26
+ readonly waitMs: number;
27
+ }
28
+
29
+ export const RUN_USAGE = 'usage: pyric run [--sandbox <url>] [--wait[=<seconds>]] -- <command> [args...]\n';
30
+
31
+ /** `--wait` without a value: the bound `pyric sandbox` gives its first-run race guard. */
32
+ export const DEFAULT_WAIT_SECONDS = 30;
33
+
34
+ /** Strip the optional `remote:` prefix the activator uses. Null when nothing names a URL. */
35
+ function hostUrlOf(value: string | undefined): string | null {
36
+ if (value === undefined) return null;
37
+ const url = value.startsWith('remote:') ? value.slice('remote:'.length) : value;
38
+ const namesUrl = /^https?:\/\//.test(url);
39
+ return namesUrl ? url : null;
40
+ }
41
+
42
+ /**
43
+ * Read a `pyric run` invocation. The host comes from `--sandbox`, then from an
44
+ * inherited `PYRIC_SANDBOX=remote:<url>`; with neither, the caller discovers
45
+ * the project's running host. Returns a usage error string when the command or
46
+ * the flag is missing or malformed.
47
+ */
48
+ export function parseRunInvocation(parsed: ParsedArgs, env: NodeJS.ProcessEnv): RunInvocation | string {
49
+ const passthrough = parsed.passthrough ?? [];
50
+ const argv = passthrough.length > 0 ? passthrough : parsed.positional;
51
+ if (argv.length === 0) return `pyric run: no command to run.\n${RUN_USAGE}`;
52
+ const flag = parsed.flags.get('sandbox');
53
+ let hostUrl: string | null;
54
+ if (flag === undefined) {
55
+ hostUrl = hostUrlOf(env.PYRIC_SANDBOX);
56
+ } else {
57
+ hostUrl = typeof flag === 'string' ? hostUrlOf(flag) : null;
58
+ if (hostUrl === null) {
59
+ return `pyric run: --sandbox needs a host URL, such as http://localhost:5173.\n${RUN_USAGE}`;
60
+ }
61
+ }
62
+ const wait = parsed.flags.get('wait');
63
+ let waitMs = 0;
64
+ if (wait === true) {
65
+ waitMs = DEFAULT_WAIT_SECONDS * 1000;
66
+ } else if (wait !== undefined) {
67
+ const seconds = typeof wait === 'string' ? Number(wait) : Number.NaN;
68
+ const isDuration = Number.isFinite(seconds) && seconds > 0;
69
+ if (!isDuration) return `pyric run: --wait takes a number of seconds, such as --wait=60.\n${RUN_USAGE}`;
70
+ waitMs = seconds * 1000;
71
+ }
72
+ return { hostUrl, plan: { argv: [...argv], label: argv.join(' ') }, waitMs };
73
+ }
74
+
75
+ /**
76
+ * Call `attempt` until it returns a value or `timeoutMs` passes, `intervalMs`
77
+ * apart. A zero timeout makes one attempt. Injectable sleep for the unit suite.
78
+ */
79
+ export async function pollUntil<T>(
80
+ attempt: () => Promise<T | null>,
81
+ opts: { timeoutMs: number; intervalMs?: number; sleep?: (ms: number) => Promise<void>; now?: () => number },
82
+ ): Promise<T | null> {
83
+ const intervalMs = opts.intervalMs ?? 250;
84
+ const sleep = opts.sleep ?? ((ms) => new Promise<void>((r) => setTimeout(r, ms)));
85
+ const now = opts.now ?? Date.now;
86
+ const deadline = now() + opts.timeoutMs;
87
+ for (;;) {
88
+ const value = await attempt();
89
+ if (value !== null) return value;
90
+ if (now() >= deadline) return null;
91
+ await sleep(intervalMs);
92
+ }
93
+ }
94
+
95
+ export async function runRun(parsed: ParsedArgs): Promise<number> {
96
+ const invocation = parseRunInvocation(parsed, process.env);
97
+ if (typeof invocation === 'string') {
98
+ process.stderr.write(invocation);
99
+ return 1;
100
+ }
101
+ const cwd = process.cwd();
102
+ let serveUrl: string;
103
+ let instanceId: string | null;
104
+ if (invocation.hostUrl !== null) {
105
+ const hostUrl = invocation.hostUrl;
106
+ const host = await pollUntil(() => probeHostUrl(hostUrl), { timeoutMs: invocation.waitMs });
107
+ if (host === null) {
108
+ const waited = invocation.waitMs > 0 ? ` after waiting ${invocation.waitMs / 1000}s` : '';
109
+ process.stderr.write(`pyric run: no sandbox host answers at ${hostUrl}/__pyric/health${waited}.\n`);
110
+ return 2;
111
+ }
112
+ serveUrl = host.base;
113
+ instanceId = host.instanceId;
114
+ } else {
115
+ const log = (line: string): void => void process.stderr.write(`pyric run: ${line}\n`);
116
+ const host = await pollUntil(() => discoverServe(cwd, log), { timeoutMs: invocation.waitMs });
117
+ if (host === null) {
118
+ process.stderr.write('pyric run: no running sandbox host found; pass --sandbox <url>.\n');
119
+ return 2;
120
+ }
121
+ serveUrl = host.base;
122
+ instanceId = host.instanceId;
123
+ }
124
+ const env = buildChildEnv(process.env, {
125
+ serveUrl,
126
+ registerUrl: registerModuleUrl(),
127
+ instanceId: instanceId ?? undefined,
128
+ });
129
+ const child = spawnSandboxChild(invocation.plan, { cwd, env, json: false });
130
+ const forward = (signal: NodeJS.Signals): void => child.signal(signal);
131
+ process.on('SIGINT', forward);
132
+ process.on('SIGTERM', forward);
133
+ try {
134
+ return await child.exited;
135
+ } finally {
136
+ process.off('SIGINT', forward);
137
+ process.off('SIGTERM', forward);
138
+ }
139
+ }
package/src/cli/serve.ts CHANGED
@@ -71,6 +71,7 @@ import {
71
71
  type SandboxSession,
72
72
  } from '../serve/sandbox-session.js';
73
73
  import { resolveAvatarsConfig } from '../serve/avatars-config.js';
74
+ import { createVerifyHint } from '../serve/verify-hint.js';
74
75
 
75
76
  interface HostingConfig {
76
77
  public?: string;
@@ -552,6 +553,8 @@ async function startServeRuntime(opts: {
552
553
  // a failed reload asked for, so fixing or creating one reloads. The source
553
554
  // is the path the rules load from, watched whether or not it exists at
554
555
  // startup, so a rules file created later loads.
556
+ const getVerifyHint = createVerifyHint();
557
+
555
558
  const isWatchEnabled = opts.watch ?? true;
556
559
  if (isWatchEnabled) {
557
560
  let debounce: ReturnType<typeof setTimeout> | null = null;
@@ -567,7 +570,7 @@ async function startServeRuntime(opts: {
567
570
  const isReloaded = result.kind === 'reloaded';
568
571
  const isRejected = result.kind === 'rejected';
569
572
  if (isReloaded) {
570
- logger.note(` ↻ rules reloaded (hash ${result.rulesHash}) → ${result.clients} page(s)`);
573
+ logger.note(` ↻ rules reloaded (hash ${result.rulesHash}) → ${result.clients} page(s)${getVerifyHint()}`);
571
574
  if (result.fixtureCheck && result.fixtureCheck.ran > 0) {
572
575
  for (const line of result.fixtureCheck.lines) logger.note(line);
573
576
  }
@@ -610,7 +613,7 @@ async function startServeRuntime(opts: {
610
613
  void session.reloadDatabaseRules(changed).then((results) => {
611
614
  for (const { instance, file: source, result } of results) {
612
615
  if (result.kind === 'reloaded') {
613
- logger.note(` ↻ rtdb rules reloaded for ${instance} (hash ${result.rulesHash}) → ${result.clients} page(s)`);
616
+ logger.note(` ↻ rtdb rules reloaded for ${instance} (hash ${result.rulesHash}) → ${result.clients} page(s)${getVerifyHint()}`);
614
617
  if (result.fixtureCheck && result.fixtureCheck.ran > 0) {
615
618
  for (const line of result.fixtureCheck.lines) logger.note(line);
616
619
  }
@@ -650,7 +653,7 @@ async function startServeRuntime(opts: {
650
653
  debounceStorage = setTimeout(() => {
651
654
  void session.reloadStorageRules().then((result) => {
652
655
  if (result.kind === 'reloaded') {
653
- logger.note(` ↻ storage rules reloaded (hash ${result.rulesHash}) → ${result.clients} page(s)`);
656
+ logger.note(` ↻ storage rules reloaded (hash ${result.rulesHash}) → ${result.clients} page(s)${getVerifyHint()}`);
654
657
  } else if (result.kind === 'rejected') {
655
658
  logger.note(` ⚠ storage rules NOT reloaded (last-good stays live): ${result.error.message}`);
656
659
  } else if (result.kind === 'removed') {
package/src/cli/verify.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
- import { basename, join, resolve } from 'node:path';
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
+ import { basename, dirname, join, resolve } from 'node:path';
3
3
  import {
4
4
  deriveRulesTestCases,
5
5
  fixtureVerifiableServices,
@@ -15,7 +15,12 @@ import {
15
15
  type VerifyRulesInput,
16
16
  type VerifyServiceResult,
17
17
  } from '../verify/index.js';
18
- import { readFirebaseJson, type FirebaseJson } from './firebase-json.js';
18
+ import { readFirebaseJson, readFirebaseRcSync, type FirebaseJson } from './firebase-json.js';
19
+ import { resolveFirebaseProject } from './firebase-project.js';
20
+ import { readPyricConfig } from './pyric-config.js';
21
+ import { markedProject, restoreStagingInstance, StagingSafetyError, stagingMarker } from '../verify/staging/engine.js';
22
+ import { RtdbRestClient } from '../verify/staging/rest.js';
23
+ import { assertNotAppInstance, stagingTargetFrom, stagingToken, type StagingTarget } from '../verify/staging/target.js';
19
24
  import type { FlagValue, ParsedArgs } from './parse-args.js';
20
25
  import { resolveScope } from '../credentials/node/scope.js';
21
26
  import { CAPTURE_RELATIVE_PATH } from '../serve/capture-store.js';
@@ -37,6 +42,8 @@ export interface FixtureResult {
37
42
 
38
43
  export interface VerifyCliDeps {
39
44
  resolveScope?: typeof resolveScope;
45
+ /** The staging instance's REST client; tests pass one backed by a local sandbox. */
46
+ stagingClient?: (target: StagingTarget, token: () => Promise<string>) => RtdbRestClient;
40
47
  }
41
48
 
42
49
  export function loadFixture(path: string): Fixture {
@@ -88,9 +95,16 @@ export function formatResults(results: FixtureResult[]): string {
88
95
  lines.push(`${marker} ${result.name}${desc} - ${serviceSummary}`);
89
96
  for (const service of Object.values(result.result.services)) {
90
97
  if (!service) continue;
91
- for (const divergence of failingDivergences(service.divergences)) {
92
- const where = divergence.instance === undefined ? service.service : `${service.service} ${divergence.instance}`;
93
- lines.push(` [${where}] ${formatDivergence(divergence)}`);
98
+ const engines = Object.values(service.engines ?? {}).filter((engine) => engine !== undefined);
99
+ // With more than one engine, each line names the engine that reported it.
100
+ const groups = engines.length > 1
101
+ ? engines.map((engine) => ({ label: ` ${engine.engine}`, divergences: engine.divergences }))
102
+ : [{ label: '', divergences: service.divergences }];
103
+ for (const group of groups) {
104
+ for (const divergence of failingDivergences(group.divergences)) {
105
+ const instance = divergence.instance === undefined ? '' : ` ${divergence.instance}`;
106
+ lines.push(` [${service.service}${instance}${group.label}] ${formatDivergence(divergence)}`);
107
+ }
94
108
  }
95
109
  }
96
110
  }
@@ -104,6 +118,9 @@ export async function runVerify(parsed: ParsedArgs, deps: VerifyCliDeps = {}): P
104
118
  if (parsed.positional[0] === 'cases') {
105
119
  return runVerifyCases(parsed, cwd);
106
120
  }
121
+ if (parsed.positional[0] === 'staging') {
122
+ return runVerifyStaging(parsed, cwd, deps);
123
+ }
107
124
 
108
125
  const target = parsed.positional[0];
109
126
  const inputPath = resolve(cwd, target ?? CAPTURE_RELATIVE_PATH);
@@ -149,6 +166,23 @@ export async function runVerify(parsed: ParsedArgs, deps: VerifyCliDeps = {}): P
149
166
  return 2;
150
167
  }
151
168
 
169
+ // The staging target is checked before anything else reads the project: an
170
+ // instance the app uses is refused before a request is made.
171
+ let staging: VerifyFixtureOptions['staging'];
172
+ let stopStaging: (() => void) | undefined;
173
+ if (engines.includes('staging')) {
174
+ try {
175
+ const opened = await openStagingInstance(cwd, stringFlag(parsed.flags.get('project')), deps);
176
+ process.stderr.write(`pyric verify: staging instance ${opened.target.databaseUrl} (project ${opened.projectId}, credentials: ${opened.source})\n`);
177
+ const guard = stagingRunGuard(cwd, opened.target);
178
+ stopStaging = guard.stop;
179
+ staging = { client: opened.client, projectId: opened.projectId, ...guard.hooks };
180
+ } catch (e) {
181
+ process.stderr.write(`pyric verify: ${messageOf(e)}\n`);
182
+ return 2;
183
+ }
184
+ }
185
+
152
186
  let rulesResolution: { rules: VerifyRulesInput; paths: Record<string, string> };
153
187
  try {
154
188
  rulesResolution = await resolveCandidateRules({
@@ -177,19 +211,24 @@ export async function runVerify(parsed: ParsedArgs, deps: VerifyCliDeps = {}): P
177
211
  }
178
212
 
179
213
  let results: FixtureResult[];
214
+ const options = {
215
+ services,
216
+ engines,
217
+ ...(rulesTestApi ? { rulesTestApi } : {}),
218
+ ...(staging ? { staging } : {}),
219
+ };
180
220
  try {
181
- results = await Promise.all(
182
- loaded.map((item) =>
183
- runFixture(item.name, item.fixture, rulesResolution.rules, {
184
- services,
185
- engines,
186
- ...(rulesTestApi ? { rulesTestApi } : {}),
187
- })),
188
- );
221
+ // The staging engine holds one instance, so its fixtures run one at a time.
222
+ results = staging
223
+ ? await pipelineFixtures(loaded, (item) => runFixture(item.name, item.fixture, rulesResolution.rules, options))
224
+ : await Promise.all(loaded.map((item) => runFixture(item.name, item.fixture, rulesResolution.rules, options)));
189
225
  } catch (e) {
190
- const prefix = e instanceof VerifyInputError ? 'invalid input' : 'failed to replay';
226
+ const prefix = e instanceof VerifyInputError ? 'invalid input' : e instanceof StagingSafetyError ? 'staging instance' : 'failed to replay';
191
227
  process.stderr.write(`pyric verify: ${prefix}: ${messageOf(e)}\n`);
192
- return 2;
228
+ stopStaging?.();
229
+ return e instanceof StagingAbortedError || (e as { name?: string }).name === 'AbortError' ? 130 : 2;
230
+ } finally {
231
+ stopStaging?.();
193
232
  }
194
233
  const allOk = results.every((result) => result.ok);
195
234
 
@@ -259,6 +298,190 @@ function runVerifyCases(parsed: ParsedArgs, cwd: string): number {
259
298
  return result.ok ? 0 : 1;
260
299
  }
261
300
 
301
+ /** The staging instance from `pyric.json`, refused when the app uses it, with credentials for its project. */
302
+ async function openStagingInstance(
303
+ cwd: string,
304
+ appProjectOption: string | undefined,
305
+ deps: VerifyCliDeps,
306
+ ): Promise<{ client: RtdbRestClient; projectId: string; target: StagingTarget; source: string }> {
307
+ const target = stagingTargetFrom(await readPyricConfig(cwd));
308
+ const firebaseJson = await readFirebaseJsonOrNull(cwd);
309
+ const appProject = resolveFirebaseProject({ projectDir: cwd, ...(appProjectOption ? { option: appProjectOption } : {}) });
310
+ assertNotAppInstance(target, {
311
+ firebaseJson,
312
+ ...(appProject.projectId ? { appProjectId: appProject.projectId } : {}),
313
+ targetInstances: databaseTargetInstances(cwd, appProject.projectId),
314
+ });
315
+ const credentials = await stagingToken(target.projectId, deps.resolveScope ? { resolve: deps.resolveScope } : {});
316
+ const client = deps.stagingClient?.(target, credentials.token) ?? new RtdbRestClient(target.databaseUrl, credentials.token);
317
+ return { client, projectId: target.projectId, target, source: credentials.source };
318
+ }
319
+
320
+ /** Every instance a `.firebaserc` database target maps for the app's project. */
321
+ function databaseTargetInstances(cwd: string, projectId: string | undefined): string[] {
322
+ if (!projectId) return [];
323
+ try {
324
+ const rc = readFirebaseRcSync(cwd) as { targets?: Record<string, { database?: Record<string, string[]> }> } | null;
325
+ return Object.values(rc?.targets?.[projectId]?.database ?? {}).flat();
326
+ } catch {
327
+ return [];
328
+ }
329
+ }
330
+
331
+ async function pipelineFixtures<T>(items: T[], run: (item: T) => Promise<FixtureResult>): Promise<FixtureResult[]> {
332
+ const out: FixtureResult[] = [];
333
+ for (const item of items) out.push(await run(item));
334
+ return out;
335
+ }
336
+
337
+ class StagingAbortedError extends Error {}
338
+
339
+ /** Where a staging run keeps the prior rules while it runs, for `pyric verify staging restore`. */
340
+ export const STAGING_RESTORE_RELATIVE_PATH = join('.pyric', 'staging-restore.json');
341
+
342
+ /**
343
+ * While a staging run is active: keep the prior rules on disk, and turn
344
+ * SIGINT/SIGTERM into an abort between steps so the run still cleans up. A
345
+ * second signal exits at once, naming the recovery command.
346
+ */
347
+ function stagingRunGuard(cwd: string, target: StagingTarget): { hooks: Pick<NonNullable<VerifyFixtureOptions['staging']>, 'onBeforeChange' | 'onRestored' | 'signal'>; stop: () => void } {
348
+ const controller = new AbortController();
349
+ const restorePath = join(cwd, STAGING_RESTORE_RELATIVE_PATH);
350
+ let signals = 0;
351
+ const onSignal = (signal: NodeJS.Signals) => {
352
+ signals += 1;
353
+ if (signals === 1) {
354
+ process.stderr.write(`\npyric verify: ${signal}: stopping after the current step and restoring ${target.databaseUrl}. Press again to quit without restoring.\n`);
355
+ controller.abort(new StagingAbortedError(`interrupted by ${signal}`));
356
+ return;
357
+ }
358
+ process.stderr.write(`pyric verify: quitting without restoring ${target.databaseUrl}. Run \`pyric verify staging restore\` to put it back.\n`);
359
+ process.exit(130);
360
+ };
361
+ process.on('SIGINT', onSignal);
362
+ process.on('SIGTERM', onSignal);
363
+ let stopped = false;
364
+ return {
365
+ hooks: {
366
+ signal: controller.signal,
367
+ onBeforeChange: (priorRules) => {
368
+ mkdirSync(dirname(restorePath), { recursive: true });
369
+ writeFileSync(restorePath, `${JSON.stringify({ databaseUrl: target.databaseUrl, projectId: target.projectId, rules: priorRules, startedAt: new Date().toISOString() }, null, 2)}\n`);
370
+ },
371
+ onRestored: () => rmSync(restorePath, { force: true }),
372
+ },
373
+ stop: () => {
374
+ if (stopped) return;
375
+ stopped = true;
376
+ process.off('SIGINT', onSignal);
377
+ process.off('SIGTERM', onSignal);
378
+ },
379
+ };
380
+ }
381
+
382
+ /**
383
+ * `pyric verify staging restore`: put the configured instance back after a run
384
+ * that could not, from the prior rules the run kept in
385
+ * `.pyric/staging-restore.json`; with `--force` and no such file, enroll it
386
+ * afresh with deny-all rules. Its data is deleted either way.
387
+ */
388
+ async function runVerifyStagingRestore(parsed: ParsedArgs, cwd: string, deps: VerifyCliDeps): Promise<number> {
389
+ let opened: Awaited<ReturnType<typeof openStagingInstance>>;
390
+ try {
391
+ opened = await openStagingInstance(cwd, stringFlag(parsed.flags.get('project')), deps);
392
+ } catch (e) {
393
+ process.stderr.write(`pyric verify staging restore: ${messageOf(e)}\n`);
394
+ return 2;
395
+ }
396
+ const { client, target } = opened;
397
+ const restorePath = join(cwd, STAGING_RESTORE_RELATIVE_PATH);
398
+ let rules: string | undefined;
399
+ if (existsSync(restorePath)) {
400
+ const kept = JSON.parse(readFileSync(restorePath, 'utf8')) as { databaseUrl?: string; rules?: string };
401
+ if (kept.databaseUrl !== target.databaseUrl) {
402
+ process.stderr.write(`pyric verify staging restore: ${STAGING_RESTORE_RELATIVE_PATH} is for ${kept.databaseUrl}, not ${target.databaseUrl}.\n`);
403
+ return 2;
404
+ }
405
+ rules = kept.rules;
406
+ } else if (parsed.flags.get('force') === true) {
407
+ rules = `${stagingMarker(target.projectId)}\n{\n "rules": {\n ".read": false,\n ".write": false\n }\n}`;
408
+ } else {
409
+ const current = await client.getRulesText().catch(() => '');
410
+ if (markedProject(current) === target.projectId && (await client.get('/', { shallow: true })).body === null) {
411
+ process.stdout.write(`✓ ${target.databaseUrl} is enrolled and empty; nothing to restore.\n`);
412
+ return 0;
413
+ }
414
+ process.stderr.write(
415
+ `pyric verify staging restore: no ${STAGING_RESTORE_RELATIVE_PATH} holds the rules ${target.databaseUrl} had. ` +
416
+ 'Pass --force to deploy deny-all rules with the enrollment marker and delete its data.\n',
417
+ );
418
+ return 2;
419
+ }
420
+ if (typeof rules !== 'string' || markedProject(rules) !== target.projectId) {
421
+ process.stderr.write(`pyric verify staging restore: the kept rules do not carry the enrollment marker for ${target.projectId}; refusing to deploy them.\n`);
422
+ return 2;
423
+ }
424
+ const result = await restoreStagingInstance(client, rules);
425
+ if (!result.rulesRestored || !result.dataRemoved) {
426
+ process.stderr.write(`pyric verify staging restore: ${target.databaseUrl} is not restored (${result.errors.join('; ')}).\n`);
427
+ return 1;
428
+ }
429
+ rmSync(restorePath, { force: true });
430
+ process.stdout.write(`✓ restored ${target.databaseUrl}: enrolled rules deployed, data deleted.\n`);
431
+ return 0;
432
+ }
433
+
434
+ /** `pyric verify staging init`: enroll the configured instance, once. */
435
+ async function runVerifyStaging(parsed: ParsedArgs, cwd: string, deps: VerifyCliDeps): Promise<number> {
436
+ if (parsed.positional[1] === 'restore') return runVerifyStagingRestore(parsed, cwd, deps);
437
+ if (parsed.positional[1] !== 'init') {
438
+ process.stderr.write('pyric verify staging: use `init` to enroll the instance pyric.json names, or `restore` to put it back after an interrupted run.\n');
439
+ return 2;
440
+ }
441
+ let opened: Awaited<ReturnType<typeof openStagingInstance>>;
442
+ try {
443
+ opened = await openStagingInstance(cwd, stringFlag(parsed.flags.get('project')), deps);
444
+ } catch (e) {
445
+ process.stderr.write(`pyric verify staging init: ${messageOf(e)}\n`);
446
+ return 2;
447
+ }
448
+ const { client, target } = opened;
449
+ try {
450
+ const rules = await client.getRulesText();
451
+ const marked = markedProject(rules);
452
+ if (marked === target.projectId) {
453
+ process.stdout.write(`✓ ${target.databaseUrl} is already enrolled for project ${target.projectId}.\n`);
454
+ return 0;
455
+ }
456
+ if (marked !== undefined) {
457
+ process.stderr.write(`pyric verify staging init: ${target.databaseUrl} is enrolled for project ${marked}, not ${target.projectId}.\n`);
458
+ return 2;
459
+ }
460
+ const root = await client.get('/', { shallow: true });
461
+ if (root.status !== 200 || root.body !== null) {
462
+ process.stderr.write(
463
+ `pyric verify staging init: refusing to enroll ${target.databaseUrl}: it holds data (or could not be read, HTTP ${root.status}). ` +
464
+ 'The staging engine replaces an instance\'s rules and data, so enroll an empty instance created for it.\n',
465
+ );
466
+ return 2;
467
+ }
468
+ const enrolled = `${stagingMarker(target.projectId)}\n${rules.trimStart()}`;
469
+ const dry = await client.putRules(enrolled, { dryRun: true });
470
+ if (dry.status !== 200) throw new Error(`production refused the marked rules: HTTP ${dry.status} ${dry.text.slice(0, 200)}`);
471
+ const put = await client.putRules(enrolled);
472
+ if (put.status !== 200) throw new Error(`deploying the marked rules failed: HTTP ${put.status} ${put.text.slice(0, 200)}`);
473
+ if (markedProject(await client.getRulesText()) !== target.projectId) throw new Error('the marker did not read back.');
474
+ } catch (e) {
475
+ process.stderr.write(`pyric verify staging init: ${messageOf(e)}\n`);
476
+ return 1;
477
+ }
478
+ process.stdout.write(
479
+ `✓ enrolled ${target.databaseUrl} (project ${target.projectId}, credentials: ${opened.source}).\n` +
480
+ ' Run `pyric verify --engine staging` to replay a captured session against it; add `--engine sandbox` to compare engines.\n',
481
+ );
482
+ return 0;
483
+ }
484
+
262
485
  function loadFixturesFromDirectory(dir: string): Array<{ name: string; fixture: Fixture }> {
263
486
  return readdirSync(dir)
264
487
  .filter((f) => f.endsWith('.json'))
@@ -296,8 +519,10 @@ function parseEngines(flag: FlagValue | undefined): VerifyEngine[] {
296
519
  pushUnique(out, 'sandbox');
297
520
  } else if (item === 'rules-test-api' || item === 'rulesTestApi') {
298
521
  pushUnique(out, 'rulesTestApi');
522
+ } else if (item === 'staging') {
523
+ pushUnique(out, 'staging');
299
524
  } else {
300
- throw new Error(`unsupported verify engine '${item}'. Supported engines: sandbox, rules-test-api, both.`);
525
+ throw new Error(`unsupported verify engine '${item}'. Supported engines: sandbox, rules-test-api, staging, both.`);
301
526
  }
302
527
  }
303
528
  return out;
@@ -447,7 +672,8 @@ function formatDivergence(divergence: VerifyDivergence): string {
447
672
  }
448
673
  if (divergence.kind === 'engine-drift') {
449
674
  const method = divergence.method ? `${divergence.method} ` : '';
450
- return `engine-drift: ${method}${divergence.path ?? ''}: sandbox=${divergence.sandbox}, rulesTestApi=${divergence.rulesTestApi}`;
675
+ const other = divergence.staging !== undefined ? `staging=${divergence.staging}` : `rulesTestApi=${divergence.rulesTestApi}`;
676
+ return `engine-drift: ${method}${divergence.path ?? ''}: sandbox=${divergence.sandbox}, ${other}`;
451
677
  }
452
678
  return `${divergence.drift}: ${divergence.path ?? ''}`;
453
679
  }