@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
@@ -39,6 +39,8 @@ import {
39
39
  VERIFY_FIXTURE_SCHEMA,
40
40
  type PyricVerifyFixture,
41
41
  } from './fixture.js';
42
+ import { runStagingInstance, StagingDeployRefusedError, withStagingInstance, type StagingRunHooks } from './staging/engine.js';
43
+ import type { RtdbRestClient } from './staging/rest.js';
42
44
  import {
43
45
  recordedRtdbInstances,
44
46
  recordsDefaultRtdbInstance,
@@ -94,7 +96,7 @@ export {
94
96
  } from './hosted-rules-test.js';
95
97
 
96
98
  export type VerifiableService = 'firestore' | 'rtdb' | 'storage';
97
- export type VerifyEngine = 'sandbox' | 'rulesTestApi';
99
+ export type VerifyEngine = 'sandbox' | 'rulesTestApi' | 'staging';
98
100
 
99
101
  type RtdbCandidateRules = { rules: Record<string, unknown> } | RtdbRulesDocument;
100
102
 
@@ -120,6 +122,12 @@ export interface VerifyFixtureOptions {
120
122
  expressionReportLevel?: ExpressionReportLevel;
121
123
  };
122
124
  caseDerivation?: Omit<DeriveRulesTestCasesOptions, 'service'>;
125
+ /**
126
+ * The staging engine's instance: a client for a dedicated RTDB instance
127
+ * enrolled with `pyric verify staging init`, and the project it is
128
+ * enrolled for. Required when `engines` includes `staging`.
129
+ */
130
+ staging?: { client: RtdbRestClient; projectId: string } & StagingRunHooks;
123
131
  }
124
132
 
125
133
  export interface VerifyResult {
@@ -198,7 +206,10 @@ type VerifyDivergenceKind =
198
206
  path?: string;
199
207
  method?: string;
200
208
  sandbox: string;
201
- rulesTestApi: string;
209
+ /** Firestore: the hosted Rules Test API's decision. */
210
+ rulesTestApi?: string;
211
+ /** RTDB: what production did on the staging instance. */
212
+ staging?: string;
202
213
  reason?: string;
203
214
  };
204
215
 
@@ -224,10 +235,15 @@ export async function verifyFixture(
224
235
 
225
236
  const services: Partial<Record<VerifiableService, VerifyServiceResult>> = {};
226
237
  for (const service of selected) {
238
+ if (engines.includes('staging') && service !== 'rtdb') {
239
+ throw new VerifyInputError(
240
+ `the staging engine checks RTDB only; ${service} is not supported by it yet. Select --service rtdb, or the sandbox engine for ${service}.`,
241
+ );
242
+ }
227
243
  if (service === 'firestore') {
228
244
  services.firestore = await verifyFirestore(fixture, requireFirestoreRules(opts.rules), engines, opts);
229
245
  } else if (service === 'rtdb') {
230
- services.rtdb = await verifyRtdb(fixture, opts.rules, engines);
246
+ services.rtdb = await verifyRtdb(fixture, opts.rules, engines, opts.staging);
231
247
  } else if (service === 'storage') {
232
248
  services.storage = await verifyStorage(fixture, opts.rules, engines);
233
249
  } else {
@@ -578,19 +594,24 @@ async function verifyRtdb(
578
594
  fixture: PyricVerifyFixture,
579
595
  rules: VerifyRulesInput,
580
596
  engines: VerifyEngine[],
597
+ staging: VerifyFixtureOptions['staging'],
581
598
  ): Promise<VerifyServiceResult> {
582
599
  const rtdb = fixture.services.rtdb;
583
600
  if (!rtdb) {
584
601
  throw new VerifyInputError('fixture does not contain services.rtdb.');
585
602
  }
586
603
  if (engines.includes('rulesTestApi')) {
587
- throw new VerifyInputError('rulesTestApi verification is Firestore-only; select --service firestore or use the sandbox engine for RTDB.');
604
+ throw new VerifyInputError('rulesTestApi verification is Firestore-only; select --service firestore or use the sandbox or staging engine for RTDB.');
605
+ }
606
+ if (engines.includes('staging') && staging === undefined) {
607
+ throw new VerifyInputError('the staging engine needs a staging instance (opts.staging).');
588
608
  }
589
609
 
590
- const replays = [];
610
+ // The instances to check, each with its candidate rules.
611
+ const checks: Array<{ instance?: string; rules: RtdbRulesJson; capturedState: unknown }> = [];
591
612
  const checksDefault = rules.rtdb !== undefined || recordsDefaultRtdbInstance(fixture);
592
613
  if (checksDefault) {
593
- replays.push(await replayRtdbInstance(fixture, requireRtdbRules(rules.rtdb, 'missing candidate RTDB rules. Pass rules.rtdb.')));
614
+ checks.push({ rules: requireRtdbRules(rules.rtdb, 'missing candidate RTDB rules. Pass rules.rtdb.'), capturedState: rtdb.state.tree });
594
615
  }
595
616
  const skippedInstances: string[] = [];
596
617
  for (const instance of recordedRtdbInstances(fixture)) {
@@ -600,28 +621,107 @@ async function verifyRtdb(
600
621
  continue;
601
622
  }
602
623
  const parsed = requireRtdbRules(candidate, `missing candidate RTDB rules for instance "${instance}".`, instance);
603
- replays.push(await replayRtdbInstance(fixture, parsed, instance));
624
+ checks.push({ instance, rules: parsed, capturedState: rtdb.instances?.[instance]?.state.tree });
604
625
  }
605
626
 
606
- const divergences = replays.flatMap((replayed) => replayed.divergences.map(mapRtdbReplayDivergence));
607
- const checkedEvents = replays.reduce((total, replayed) => total + replayed.checkedEvents, 0);
608
- const sandbox: VerifyEngineResult = {
609
- engine: 'sandbox',
610
- ok: divergences.every(isInformational),
611
- checkedEvents,
612
- divergences,
613
- };
627
+ const engineResults: Partial<Record<VerifyEngine, VerifyEngineResult>> = {};
628
+ if (engines.includes('sandbox')) {
629
+ const replays = [];
630
+ for (const check of checks) replays.push(await replayRtdbInstance(fixture, check.rules, check.instance));
631
+ const divergences = replays.flatMap((replayed) => replayed.divergences.map(mapRtdbReplayDivergence));
632
+ engineResults.sandbox = {
633
+ engine: 'sandbox',
634
+ ok: divergences.every(isInformational),
635
+ checkedEvents: replays.reduce((total, replayed) => total + replayed.checkedEvents, 0),
636
+ divergences,
637
+ };
638
+ }
639
+ if (engines.includes('staging') && staging !== undefined) {
640
+ engineResults.staging = await verifyRtdbOnStaging(fixture, checks, staging);
641
+ }
642
+ if (engineResults.sandbox && engineResults.staging) {
643
+ addRtdbEngineDrift(engineResults.sandbox, engineResults.staging);
644
+ }
614
645
 
646
+ const divergences = collectEngineDivergences(engineResults);
615
647
  return {
616
648
  service: 'rtdb',
617
649
  ok: divergences.every(isInformational),
618
- checkedEvents,
650
+ checkedEvents: engineResults.sandbox?.checkedEvents ?? engineResults.staging?.checkedEvents ?? 0,
619
651
  divergences,
620
- engines: { sandbox },
652
+ engines: engineResults,
621
653
  ...(skippedInstances.length > 0 ? { skippedInstances } : {}),
622
654
  };
623
655
  }
624
656
 
657
+ /**
658
+ * Replay each instance on the staging instance in turn, each run guarded and
659
+ * the instance put back before the next. Production refusing to deploy an
660
+ * instance's candidate rules is reported for that instance; the others run.
661
+ */
662
+ async function verifyRtdbOnStaging(
663
+ fixture: PyricVerifyFixture,
664
+ checks: Array<{ instance?: string; rules: RtdbRulesJson; capturedState: unknown }>,
665
+ staging: NonNullable<VerifyFixtureOptions['staging']>,
666
+ ): Promise<VerifyEngineResult> {
667
+ const divergences: VerifyDivergence[] = [];
668
+ let checkedEvents = 0;
669
+ for (const check of checks) {
670
+ const tag = check.instance === undefined ? {} : { instance: check.instance };
671
+ try {
672
+ const { result } = await withStagingInstance(staging.client, staging.projectId, () =>
673
+ runStagingInstance({
674
+ client: staging.client,
675
+ events: fixture.events,
676
+ rules: check.rules,
677
+ capturedState: check.capturedState,
678
+ ...(check.instance === undefined ? {} : { instance: check.instance }),
679
+ ...(staging.signal ? { signal: staging.signal } : {}),
680
+ }), staging);
681
+ checkedEvents += result.checkedEvents;
682
+ divergences.push(...result.divergences.map((divergence) => ({ service: 'rtdb', ...divergence, ...tag })));
683
+ } catch (error) {
684
+ if (!(error instanceof StagingDeployRefusedError)) throw error;
685
+ divergences.push({ service: 'rtdb', kind: 'unsupported', reason: error.message, ...tag });
686
+ }
687
+ }
688
+ return { engine: 'staging', ok: divergences.every(isInformational), checkedEvents, divergences };
689
+ }
690
+
691
+ /**
692
+ * Where the sandbox and production decided a captured step differently: one
693
+ * engine reports `now-denied`, `now-allowed` or `unsupported` for a step and
694
+ * the other reports something else, or nothing. Steps are matched by their
695
+ * index in the replay both engines walk.
696
+ */
697
+ function addRtdbEngineDrift(sandbox: VerifyEngineResult, staging: VerifyEngineResult): void {
698
+ const decisive = new Set(['now-denied', 'now-allowed', 'unsupported']);
699
+ const keyOf = (divergence: VerifyDivergence) =>
700
+ `${divergence.instance ?? ''}|${(divergence as { step?: number }).step ?? `${(divergence as { method?: string }).method}|${divergence.path}`}`;
701
+ const byKey = (divergences: VerifyDivergence[]) =>
702
+ new Map(divergences.filter((divergence) => decisive.has(divergence.kind)).map((divergence) => [keyOf(divergence), divergence]));
703
+ const local = byKey(sandbox.divergences);
704
+ const remote = byKey(staging.divergences);
705
+ for (const key of new Set([...local.keys(), ...remote.keys()])) {
706
+ const sandboxKind = local.get(key)?.kind ?? 'unchanged';
707
+ const stagingKind = remote.get(key)?.kind ?? 'unchanged';
708
+ if (sandboxKind === stagingKind) continue;
709
+ const sample = (local.get(key) ?? remote.get(key))!;
710
+ staging.divergences.push({
711
+ service: 'rtdb',
712
+ kind: 'engine-drift',
713
+ path: sample.path,
714
+ method: (sample as { method?: string }).method,
715
+ sandbox: sandboxKind,
716
+ staging: stagingKind,
717
+ reason: `the sandbox reported ${sandboxKind}; production reported ${stagingKind}.`,
718
+ ...(sample.instance === undefined ? {} : { instance: sample.instance }),
719
+ ...((sample as { step?: number }).step === undefined ? {} : { step: (sample as { step?: number }).step }),
720
+ });
721
+ }
722
+ staging.ok = staging.divergences.every(isInformational);
723
+ }
724
+
625
725
  async function verifyStorage(
626
726
  fixture: PyricVerifyFixture,
627
727
  rules: VerifyRulesInput,
@@ -864,7 +964,7 @@ function normalizeEngines(input: VerifyEngine[] | undefined): VerifyEngine[] {
864
964
  const engines = input ?? ['sandbox'];
865
965
  const out: VerifyEngine[] = [];
866
966
  for (const engine of engines) {
867
- if (engine !== 'sandbox' && engine !== 'rulesTestApi') {
967
+ if (engine !== 'sandbox' && engine !== 'rulesTestApi' && engine !== 'staging') {
868
968
  throw new VerifyInputError(`unsupported verify engine: ${String(engine)}`);
869
969
  }
870
970
  if (!out.includes(engine)) out.push(engine);
@@ -881,7 +981,7 @@ function collectEngineDivergences(
881
981
  function serviceCheckedEvents(
882
982
  engines: Partial<Record<VerifyEngine, VerifyEngineResult>>,
883
983
  ): number {
884
- return engines.sandbox?.checkedEvents ?? engines.rulesTestApi?.checkedEvents ?? 0;
984
+ return engines.sandbox?.checkedEvents ?? engines.rulesTestApi?.checkedEvents ?? engines.staging?.checkedEvents ?? 0;
885
985
  }
886
986
 
887
987
  function mapUnsupportedEvent(event: VerifyUnsupportedEvent): VerifyDivergence {
@@ -0,0 +1,455 @@
1
+ /**
2
+ * The staging engine: `pyric verify --engine staging` replays a captured RTDB
3
+ * session against a real Realtime Database instance the user dedicates to it,
4
+ * so production's rules engine decides every operation.
5
+ *
6
+ * It walks the same steps as the sandbox replay (`rtdbReplaySteps`), in order,
7
+ * performing each against the instance through the REST API as the captured
8
+ * user (`auth_variable_override`) or as admin:
9
+ *
10
+ * - `admin-commit`: written as admin, as context.
11
+ * - `app-commit`: written as the user; a denial is `now-denied`.
12
+ * - `allowed-read`: read as the user, with its query; a denial is `now-denied`.
13
+ * - `denial`: attempted as the user; success is `now-allowed`, and a write
14
+ * that landed is put back as admin, so the tree stays the session's.
15
+ * - `allowed-registration`: the `onDisconnect` write it registers, attempted
16
+ * as the user and put back (production decides a registration as that
17
+ * write when it is made, row M81); a denial is `now-denied`.
18
+ *
19
+ * Then the tree is compared with the one the session left (`state-drift`).
20
+ *
21
+ * The instance is the user's, so every run is guarded (`withStagingInstance`):
22
+ * it must carry the enrollment marker `pyric verify staging init` wrote and
23
+ * hold no data; the run takes a lease (a conditional write of
24
+ * `/__pyric_verify_lease__`, so a second run is refused); on every exit,
25
+ * including an aborted walk, its rules are restored and its data deleted,
26
+ * each step on its own and retried, then both are read back, and a run that
27
+ * cannot prove that fails loudly, naming `pyric verify staging restore`.
28
+ * A process killed outright (SIGKILL, power loss) cannot clean up; the CLI
29
+ * keeps the prior rules in `.pyric/staging-restore.json` for that command.
30
+ */
31
+ import type { SandboxCommitEvent, SandboxEvent, SandboxOperationEvent } from 'pyric/sandbox';
32
+ import {
33
+ rewoundRtdbTree,
34
+ rtdbAuthVariable,
35
+ rtdbReplaySteps,
36
+ rtdbStateDrift,
37
+ type RtdbReplayDivergence,
38
+ } from 'pyric/rules/internal/rtdb';
39
+
40
+ import { isRestDenial, type RtdbAuthOverride, type RtdbRestClient, type RtdbRestResponse } from './rest.js';
41
+
42
+ /** The comment `pyric verify staging init` puts at the top of the instance's rules. */
43
+ export const STAGING_MARKER_PREFIX = '/* pyric verify staging:';
44
+
45
+ export function stagingMarker(projectId: string, at = new Date()): string {
46
+ return `${STAGING_MARKER_PREFIX} this instance is dedicated to \`pyric verify --engine staging\` for project ${projectId}, enrolled ${at.toISOString()}. Its rules and data are replaced while a verification runs. */`;
47
+ }
48
+
49
+ /** The project an enrollment marker names, or undefined when the rules carry no marker. */
50
+ export function markedProject(rulesText: string): string | undefined {
51
+ const trimmed = rulesText.trimStart();
52
+ if (!trimmed.startsWith(STAGING_MARKER_PREFIX)) return undefined;
53
+ return /for project ([^\s,]+),/.exec(trimmed.slice(0, trimmed.indexOf('*/') + 2))?.[1];
54
+ }
55
+
56
+ export class StagingSafetyError extends Error {
57
+ constructor(message: string) {
58
+ super(message);
59
+ this.name = 'StagingSafetyError';
60
+ }
61
+ }
62
+
63
+ /** Production refused to deploy the candidate rules; its message is the reason. */
64
+ export class StagingDeployRefusedError extends Error {
65
+ constructor(readonly instanceLabel: string, readonly production: string) {
66
+ super(`production refused to deploy the candidate rules for ${instanceLabel}: ${production}`);
67
+ this.name = 'StagingDeployRefusedError';
68
+ }
69
+ }
70
+
71
+ /** What a guarded run found when it put the instance back. */
72
+ export interface StagingInvariants {
73
+ rulesRestored: boolean;
74
+ dataRemoved: boolean;
75
+ }
76
+
77
+ /** The data node a run holds while it runs; written with a conditional request, so only one run gets it. */
78
+ export const STAGING_LEASE_KEY = '__pyric_verify_lease__';
79
+
80
+ /** Hooks a caller can attach to a guarded run. */
81
+ export interface StagingRunHooks {
82
+ /** Called with the prior rules text before anything is changed, so the caller can keep it for recovery. */
83
+ onBeforeChange?: (priorRules: string) => void;
84
+ /** Called once the instance reads back as it was found. */
85
+ onRestored?: () => void;
86
+ /** Abort the walk between steps; cleanup still runs. */
87
+ signal?: AbortSignal;
88
+ }
89
+
90
+ /**
91
+ * Run `fn` against the staging instance behind `client`, guarded: refuse an
92
+ * instance without the marker for `projectId`, with data, or with another
93
+ * run's lease; on every exit restore its rules and delete its data, each step
94
+ * attempted on its own and retried, then read both back.
95
+ */
96
+ export async function withStagingInstance<T>(
97
+ client: RtdbRestClient,
98
+ projectId: string,
99
+ fn: () => Promise<T>,
100
+ hooks: StagingRunHooks = {},
101
+ ): Promise<{ result: T; invariants: StagingInvariants }> {
102
+ const priorRules = await client.getRulesText();
103
+ const marked = markedProject(priorRules);
104
+ if (marked === undefined) {
105
+ throw new StagingSafetyError(
106
+ `${client.databaseUrl} is not enrolled for staging verification (its rules do not start with the enrollment marker). ` +
107
+ 'Run `pyric verify staging init` once to enroll a dedicated, empty instance. If a verification was interrupted, run `pyric verify staging restore`.',
108
+ );
109
+ }
110
+ if (marked !== projectId) {
111
+ throw new StagingSafetyError(`${client.databaseUrl} is enrolled for project ${marked}, not ${projectId}.`);
112
+ }
113
+ const root = await client.get('/', { shallow: true });
114
+ if (root.status !== 200) throw new StagingSafetyError(`reading ${client.databaseUrl} failed: HTTP ${root.status} ${root.text.slice(0, 200)}`);
115
+ if (root.body !== null) {
116
+ const leased = typeof root.body === 'object' && root.body !== null && STAGING_LEASE_KEY in root.body;
117
+ throw new StagingSafetyError(leased
118
+ ? `another verification holds ${client.databaseUrl} (its lease is set). Wait for it to finish; if none is running, run \`pyric verify staging restore\`.`
119
+ : `${client.databaseUrl} holds data, so it is not a dedicated staging instance. Refusing to replace it.`);
120
+ }
121
+ const lease = { run: `${process.pid}-${Date.now().toString(36)}`, at: new Date().toISOString() };
122
+ const empty = await client.get(STAGING_LEASE_KEY, { etag: true });
123
+ const taken = empty.etag === undefined
124
+ ? { status: 0, text: 'no ETag', body: null }
125
+ : await client.put(STAGING_LEASE_KEY, lease, { ifMatch: empty.etag });
126
+ if (taken.status !== 200) {
127
+ throw new StagingSafetyError(
128
+ taken.status === 412
129
+ ? `another verification took ${client.databaseUrl} first. Run one verification at a time per staging instance.`
130
+ : `taking the lease on ${client.databaseUrl} failed: HTTP ${taken.status} ${String(taken.text).slice(0, 200)}`,
131
+ );
132
+ }
133
+
134
+ hooks.onBeforeChange?.(priorRules);
135
+ let outcome: { ok: true; value: T } | { ok: false; error: unknown };
136
+ try {
137
+ outcome = { ok: true, value: await fn() };
138
+ } catch (error) {
139
+ outcome = { ok: false, error };
140
+ }
141
+
142
+ const invariants = await restoreStagingInstance(client, priorRules);
143
+ if (!invariants.rulesRestored || !invariants.dataRemoved) {
144
+ throw new StagingSafetyError(
145
+ `${client.databaseUrl} was not left as it was found (rules restored: ${invariants.rulesRestored}, data removed: ${invariants.dataRemoved}${invariants.errors.length > 0 ? `; ${invariants.errors.join('; ')}` : ''}). ` +
146
+ 'Run `pyric verify staging restore` to put it back.',
147
+ );
148
+ }
149
+ hooks.onRestored?.();
150
+ if (!outcome.ok) throw outcome.error;
151
+ return { result: outcome.value, invariants: { rulesRestored: true, dataRemoved: true } };
152
+ }
153
+
154
+ /**
155
+ * Put a staging instance back: deploy `rulesText` first, since candidate
156
+ * rules may be open, then delete the data; each step on its own, retried,
157
+ * and read back. Never throws: the result says what holds.
158
+ */
159
+ export async function restoreStagingInstance(
160
+ client: RtdbRestClient,
161
+ rulesText: string,
162
+ ): Promise<StagingInvariants & { errors: string[] }> {
163
+ const errors: string[] = [];
164
+ const attempt = async (what: string, step: () => Promise<boolean>): Promise<boolean> => {
165
+ let last = '';
166
+ for (let tries = 0; tries < 3; tries += 1) {
167
+ try {
168
+ if (await step()) return true;
169
+ last = 'did not read back';
170
+ } catch (error) {
171
+ last = error instanceof Error ? error.message : String(error);
172
+ }
173
+ await new Promise((resolve) => setTimeout(resolve, 250 * (tries + 1)));
174
+ }
175
+ errors.push(`${what}: ${last}`);
176
+ return false;
177
+ };
178
+ const rulesRestored = await attempt('restoring the rules', async () => {
179
+ const put = await client.putRules(rulesText);
180
+ return put.status === 200 && (await client.getRulesText()) === rulesText;
181
+ });
182
+ const dataRemoved = await attempt('deleting the data', async () => {
183
+ const removed = await client.delete('/');
184
+ const back = await client.get('/', { shallow: true });
185
+ return removed.status === 200 && back.status === 200 && back.body === null;
186
+ });
187
+ return { rulesRestored, dataRemoved, errors };
188
+ }
189
+
190
+ export interface StagingInstanceRun {
191
+ checkedEvents: number;
192
+ divergences: RtdbReplayDivergence[];
193
+ }
194
+
195
+ /**
196
+ * Replay one captured instance's steps against the staging instance under
197
+ * `rules`. Call inside `withStagingInstance`; the instance must be empty.
198
+ */
199
+ export async function runStagingInstance(input: {
200
+ client: RtdbRestClient;
201
+ events: readonly SandboxEvent[];
202
+ rules: { rules: Record<string, unknown> };
203
+ capturedState: unknown;
204
+ /** A named instance; absent is the default instance. */
205
+ instance?: string;
206
+ /** Abort between steps; the guard still cleans up. */
207
+ signal?: AbortSignal;
208
+ }): Promise<StagingInstanceRun> {
209
+ const { client, events } = input;
210
+ const label = input.instance === undefined ? 'the default instance' : `instance "${input.instance}"`;
211
+ const source = JSON.stringify(input.rules, null, 2);
212
+ const dry = await client.putRules(source, { dryRun: true });
213
+ if (dry.status !== 200) throw new StagingDeployRefusedError(label, productionMessage(dry));
214
+ const deployed = await client.putRules(source);
215
+ if (deployed.status !== 200) throw new StagingDeployRefusedError(label, productionMessage(deployed));
216
+
217
+ const lease = (await expectOk(client.get(STAGING_LEASE_KEY), 'reading the lease')).body;
218
+ if (input.capturedState !== undefined) {
219
+ const initial = await rewoundRtdbTree(events, input.capturedState, input.instance);
220
+ if (initial !== null) await expectOk(client.put('/', withLease(initial, lease)), 'seeding the session\'s starting tree');
221
+ }
222
+
223
+ const divergences: RtdbReplayDivergence[] = [];
224
+ let checkedEvents = 0;
225
+ const steps = rtdbReplaySteps(events, input.instance);
226
+ for (const [index, step] of steps.entries()) {
227
+ input.signal?.throwIfAborted();
228
+ const before = divergences.length;
229
+ if (step.kind === 'admin-commit') {
230
+ const res = await writeCommit(client, step.event, undefined, lease);
231
+ if (typeof res === 'string') divergences.push({ kind: 'unsupported', path: pathOf(step.event), method: step.event.method, reason: res });
232
+ else await expectOk(Promise.resolve(res), `the admin ${step.event.method} at ${step.event.path}`);
233
+ } else if (step.kind === 'app-commit') {
234
+ checkedEvents += 1;
235
+ const res = await writeCommit(client, step.event, authOverride(step.event), lease);
236
+ if (typeof res === 'string') divergences.push({ kind: 'unsupported', path: pathOf(step.event), method: step.event.method, reason: res });
237
+ else if (isRestDenial(res.status)) divergences.push(denied(step.event, res));
238
+ else if (res.status !== 200) divergences.push(unsupported(step.event, res));
239
+ } else if (step.kind === 'allowed-read') {
240
+ checkedEvents += 1;
241
+ const query = restQuery(step.event);
242
+ if (typeof query === 'string') {
243
+ divergences.push({ kind: 'unsupported', path: pathOf(step.event), method: step.event.method, reason: query });
244
+ } else {
245
+ const res = await client.get(pathOf(step.event), { auth: authOverride(step.event), query });
246
+ if (isRestDenial(res.status)) divergences.push(denied(step.event, res));
247
+ else if (res.status !== 200) divergences.push(unsupported(step.event, res));
248
+ }
249
+ } else {
250
+ checkedEvents += 1;
251
+ await decide(client, step.event, step.kind === 'denial' ? 'deny' : 'allow', divergences, lease);
252
+ }
253
+ // Each divergence names the replay step it came from, so engines compare step for step.
254
+ for (const divergence of divergences.slice(before)) (divergence as { step?: number }).step = index;
255
+ }
256
+
257
+ if (input.capturedState !== undefined) {
258
+ const finalTree = await expectOk(client.get('/'), 'reading the replayed tree');
259
+ divergences.push(...rtdbStateDrift(input.capturedState, withoutLease(finalTree.body)));
260
+ }
261
+ return { checkedEvents, divergences };
262
+ }
263
+
264
+ /** A tree with the run's lease kept at the root, so a root write does not release it. */
265
+ function withLease(tree: unknown, lease: unknown): unknown {
266
+ if (lease === null || lease === undefined) return tree;
267
+ if (tree === null || tree === undefined) return { [STAGING_LEASE_KEY]: lease };
268
+ if (!isObject(tree)) return tree;
269
+ return { ...tree, [STAGING_LEASE_KEY]: lease };
270
+ }
271
+
272
+ function withoutLease(tree: unknown): unknown {
273
+ if (!isObject(tree) || !(STAGING_LEASE_KEY in tree)) return tree;
274
+ const { [STAGING_LEASE_KEY]: _lease, ...rest } = tree;
275
+ return Object.keys(rest).length === 0 ? null : rest;
276
+ }
277
+
278
+ /**
279
+ * Attempt a captured denial or registration as its user. A write that lands
280
+ * is put back as admin from the subtree read before it.
281
+ */
282
+ async function decide(
283
+ client: RtdbRestClient,
284
+ event: SandboxOperationEvent,
285
+ captured: 'allow' | 'deny',
286
+ divergences: RtdbReplayDivergence[],
287
+ lease: unknown,
288
+ ): Promise<void> {
289
+ const path = pathOf(event);
290
+ const auth = authOverride(event);
291
+ let res: RtdbRestResponse;
292
+ if (event.method === 'get' || event.method === 'listen') {
293
+ const query = restQuery(event);
294
+ if (typeof query === 'string') {
295
+ divergences.push({ kind: 'unsupported', path, method: event.method, reason: query });
296
+ return;
297
+ }
298
+ res = await client.get(path, { auth, query });
299
+ } else {
300
+ const write = attemptedWrite(event);
301
+ if (typeof write === 'string') {
302
+ divergences.push({ kind: 'unsupported', path, method: event.method, reason: write });
303
+ return;
304
+ }
305
+ const before = await expectOk(client.get(write.restorePath, { format: 'export' }), `reading ${write.restorePath} before the attempt`);
306
+ res = await write.send(client, auth);
307
+ if (res.status === 200) {
308
+ const restored = write.restorePath === '/' ? withLease(before.body, lease) : before.body;
309
+ await expectOk(client.put(write.restorePath, restored), `putting ${write.restorePath} back after the attempt`);
310
+ }
311
+ }
312
+ if (res.status === 200) {
313
+ if (captured === 'deny') {
314
+ divergences.push({
315
+ kind: 'now-allowed',
316
+ path,
317
+ method: event.method,
318
+ reason: 'the captured rules denied this operation; production allows it under the candidate rules.',
319
+ });
320
+ }
321
+ } else if (isRestDenial(res.status)) {
322
+ if (captured === 'allow') divergences.push(denied(event, res));
323
+ } else {
324
+ divergences.push(unsupported(event, res));
325
+ }
326
+ }
327
+
328
+ interface AttemptedWrite {
329
+ /** The subtree read before the attempt and put back after a write that lands. */
330
+ restorePath: string;
331
+ send(client: RtdbRestClient, auth: RtdbAuthOverride): Promise<RtdbRestResponse>;
332
+ }
333
+
334
+ function attemptedWrite(event: SandboxOperationEvent): AttemptedWrite | string {
335
+ const path = pathOf(event);
336
+ const data = event.request?.data;
337
+ const detail = (event.detail ?? {}) as { patch?: unknown; rootPath?: unknown; priority?: unknown };
338
+ switch (event.method) {
339
+ case 'set':
340
+ case 'transaction':
341
+ return { restorePath: path, send: (client, auth) => client.put(path, withPriority(data ?? null, detail.priority), { auth }) };
342
+ case 'onDisconnect.set':
343
+ return { restorePath: path, send: (client, auth) => client.put(path, withPriority(data ?? null, detail.priority), { auth }) };
344
+ case 'remove':
345
+ case 'onDisconnect.remove':
346
+ return { restorePath: path, send: (client, auth) => client.delete(path, { auth }) };
347
+ case 'update':
348
+ if (!isObject(detail.patch) || typeof detail.rootPath !== 'string') {
349
+ return 'the capture records only the denied path of this update, not the paths written with it, so the update cannot be rebuilt.';
350
+ }
351
+ return { restorePath: detail.rootPath, send: (client, auth) => client.patch(detail.rootPath as string, detail.patch, { auth }) };
352
+ case 'onDisconnect.update':
353
+ if (!isObject(data)) return 'the captured onDisconnect update did not contain an object patch.';
354
+ return { restorePath: path, send: (client, auth) => client.patch(path, data, { auth }) };
355
+ case 'setPriority':
356
+ return { restorePath: path, send: (client, auth) => client.put(`${path}/.priority`, data ?? null, { auth }) };
357
+ default:
358
+ return `the staging engine does not check '${event.method}' operations.`;
359
+ }
360
+ }
361
+
362
+ /** Perform a captured commit; the reason it cannot be performed when its method is not one replay handles. */
363
+ async function writeCommit(
364
+ client: RtdbRestClient,
365
+ commit: SandboxCommitEvent,
366
+ auth: RtdbAuthOverride | undefined,
367
+ lease: unknown,
368
+ ): Promise<RtdbRestResponse | string> {
369
+ const path = commit.path ?? '/';
370
+ const atRoot = path === '/';
371
+ const request = auth === undefined ? {} : { auth };
372
+ const detail = commit.detail ?? {};
373
+ switch (commit.method) {
374
+ case 'update':
375
+ return client.patch(path, commit.data, request);
376
+ case 'remove':
377
+ // Removing the root as admin keeps the lease; a user's removal is decided as a write of nothing.
378
+ return atRoot ? client.put(path, withLease(null, lease), request) : client.delete(path, request);
379
+ case 'setPriority':
380
+ return client.put(`${path}/.priority`, commit.data ?? null, request);
381
+ case 'transaction':
382
+ return client.put(path, atRoot ? withLease(commit.data ?? null, lease) : commit.data ?? null, request);
383
+ case 'set': {
384
+ const value = withPriority(commit.data ?? null, Object.hasOwn(detail, 'priority') ? detail.priority : undefined);
385
+ return client.put(path, atRoot ? withLease(value, lease) : value, request);
386
+ }
387
+ default:
388
+ return `the staging engine does not replay '${commit.method}' commits.`;
389
+ }
390
+ }
391
+
392
+ /** A value with a `.priority`, in the shape the REST API reads it; the value itself when there is none. */
393
+ function withPriority(value: unknown, priority: unknown): unknown {
394
+ if (priority === undefined || priority === null) return value;
395
+ if (isObject(value)) return { ...value, '.priority': priority };
396
+ return { '.value': value, '.priority': priority };
397
+ }
398
+
399
+ /** The REST query parameters for a captured query, or the reason REST cannot express it. */
400
+ function restQuery(event: SandboxOperationEvent): Record<string, string> | string | undefined {
401
+ const spec = event.request?.query as
402
+ | { orderBy: { kind: string; path?: string } | null; bounds: Array<{ kind: string; value: unknown; key?: string }>; limit: { kind: string; n: number } | null }
403
+ | undefined;
404
+ if (spec === undefined) return undefined;
405
+ const query: Record<string, string> = {};
406
+ const order = spec.orderBy;
407
+ if (order === null) {
408
+ if (spec.bounds.length > 0 || spec.limit !== null) query.orderBy = JSON.stringify('$priority');
409
+ } else if (order.kind === 'child') {
410
+ query.orderBy = JSON.stringify(order.path);
411
+ } else {
412
+ query.orderBy = JSON.stringify(`$${order.kind}`);
413
+ }
414
+ for (const bound of spec.bounds) {
415
+ if (bound.key !== undefined) return `the REST API cannot express a ${bound.kind} bound with a key.`;
416
+ if (bound.kind !== 'startAt' && bound.kind !== 'endAt' && bound.kind !== 'equalTo') {
417
+ return `the REST API has no ${bound.kind} parameter.`;
418
+ }
419
+ query[bound.kind] = JSON.stringify(bound.value);
420
+ }
421
+ if (spec.limit !== null) query[spec.limit.kind] = String(spec.limit.n);
422
+ return query;
423
+ }
424
+
425
+ function authOverride(event: SandboxCommitEvent | SandboxOperationEvent): RtdbAuthOverride {
426
+ if (event.auth === null) return null;
427
+ return rtdbAuthVariable(event.auth, typeof event.at === 'number' ? event.at : Date.now()) as unknown as Record<string, unknown>;
428
+ }
429
+
430
+ function pathOf(event: { path?: string }): string {
431
+ return event.path ?? '/';
432
+ }
433
+
434
+ function denied(event: { path?: string; method: string }, res: RtdbRestResponse): RtdbReplayDivergence {
435
+ return { kind: 'now-denied', path: pathOf(event), method: event.method, reason: `production denied it under the candidate rules (HTTP ${res.status}).` };
436
+ }
437
+
438
+ function unsupported(event: { path?: string; method: string }, res: RtdbRestResponse): RtdbReplayDivergence {
439
+ return { kind: 'unsupported', path: pathOf(event), method: event.method, reason: `production answered HTTP ${res.status}: ${productionMessage(res)}` };
440
+ }
441
+
442
+ function productionMessage(res: RtdbRestResponse): string {
443
+ const error = isObject(res.body) ? res.body.error : undefined;
444
+ return (typeof error === 'string' ? error : res.text).slice(0, 500);
445
+ }
446
+
447
+ async function expectOk(pending: Promise<RtdbRestResponse>, what: string): Promise<RtdbRestResponse> {
448
+ const res = await pending;
449
+ if (res.status !== 200) throw new Error(`staging engine: ${what} failed: HTTP ${res.status} ${productionMessage(res)}`);
450
+ return res;
451
+ }
452
+
453
+ function isObject(value: unknown): value is Record<string, unknown> {
454
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
455
+ }