@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.
- package/README.md +1 -1
- package/dist/assurance/.generated/conformance-verdicts.d.ts +5 -0
- package/dist/assurance/.generated/conformance-verdicts.d.ts.map +1 -1
- package/dist/assurance/.generated/conformance-verdicts.js +5 -0
- package/dist/assurance/.generated/conformance-verdicts.js.map +1 -1
- package/dist/assurance/capabilities.d.ts.map +1 -1
- package/dist/assurance/capabilities.js +24 -5
- package/dist/assurance/capabilities.js.map +1 -1
- package/dist/assurance/runner.d.ts.map +1 -1
- package/dist/assurance/runner.js +37 -50
- package/dist/assurance/runner.js.map +1 -1
- package/dist/assurance/types.d.ts +5 -0
- package/dist/assurance/types.d.ts.map +1 -1
- package/dist/assurance/types.js.map +1 -1
- package/dist/assurance/validation.d.ts +2 -16
- package/dist/assurance/validation.d.ts.map +1 -1
- package/dist/assurance/validation.js +121 -1
- package/dist/assurance/validation.js.map +1 -1
- package/dist/bridge/surface/arguments/assurance.d.ts +31 -6
- package/dist/bridge/surface/arguments/assurance.d.ts.map +1 -1
- package/dist/bridge/surface/arguments/assurance.js +1 -0
- package/dist/bridge/surface/arguments/assurance.js.map +1 -1
- package/dist/bridge/surface/arguments/messaging.d.ts +2 -2
- package/dist/bridge/surface/methods/assurance/define.d.ts +2 -2
- package/dist/bridge/surface/methods/assurance/map.d.ts +29 -6
- package/dist/bridge/surface/methods/assurance/map.d.ts.map +1 -1
- package/dist/bridge/surface/methods/assurance/propose.d.ts +7 -0
- package/dist/bridge/surface/methods/assurance/propose.d.ts.map +1 -1
- package/dist/bridge/surface/methods/messaging/send.d.ts +4 -4
- package/dist/bridge/surface/render/discriminator-schemas.d.ts +6 -6
- package/dist/cli/database-rules.js +1 -1
- package/dist/cli/database-rules.js.map +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +28 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/pyric-config.d.ts +8 -0
- package/dist/cli/pyric-config.d.ts.map +1 -1
- package/dist/cli/pyric-config.js.map +1 -1
- package/dist/cli/run.d.ts +42 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +115 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/serve.d.ts.map +1 -1
- package/dist/cli/serve.js +5 -3
- package/dist/cli/serve.js.map +1 -1
- package/dist/cli/verify.d.ts +6 -0
- package/dist/cli/verify.d.ts.map +1 -1
- package/dist/cli/verify.js +240 -15
- package/dist/cli/verify.js.map +1 -1
- package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use.js +1 -1
- package/dist/conformance/.generated/can-i-use.js.map +1 -1
- package/dist/conformance/.generated/conformance-docs.d.ts.map +1 -1
- package/dist/conformance/.generated/conformance-docs.js +1 -1
- package/dist/conformance/.generated/conformance-docs.js.map +1 -1
- package/dist/remote/local-storage-url.d.ts +2 -15
- package/dist/remote/local-storage-url.d.ts.map +1 -1
- package/dist/remote/local-storage-url.js +1 -60
- package/dist/remote/local-storage-url.js.map +1 -1
- package/dist/serve/assets/avatar-route.d.ts.map +1 -1
- package/dist/serve/assets/avatar-route.js +6 -6
- package/dist/serve/assets/avatar-route.js.map +1 -1
- package/dist/serve/assets/resolver.d.ts +3 -0
- package/dist/serve/assets/resolver.d.ts.map +1 -1
- package/dist/serve/assets/resolver.js +1 -1
- package/dist/serve/assets/resolver.js.map +1 -1
- package/dist/serve/entries/app-client.d.ts.map +1 -1
- package/dist/serve/entries/app-client.js +6 -0
- package/dist/serve/entries/app-client.js.map +1 -1
- package/dist/serve/repro/denials.d.ts.map +1 -1
- package/dist/serve/repro/denials.js +2 -1
- package/dist/serve/repro/denials.js.map +1 -1
- package/dist/serve/site-ui/_astro/{app.DFdWhpVY.js → app.BCNb6gMq.js} +1 -1
- package/dist/serve/site-ui/_astro/app.DhoRPDdN.js +147 -0
- package/dist/serve/site-ui/_astro/{seed.vvXV1ioI.js → seed.BO5CXcqE.js} +1 -1
- package/dist/serve/site-ui/auth/index.html +1 -1
- package/dist/serve/site-ui/docs/_rhythm/index.html +1 -1
- package/dist/serve/site-ui/docs/agent/set-up-your-agent/index.html +1 -1
- package/dist/serve/site-ui/docs/agent/watch-and-review/index.html +1 -1
- package/dist/serve/site-ui/docs/agent/work-with-an-agent/index.html +1 -1
- package/dist/serve/site-ui/docs/ai-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/api-reference/index.html +2 -1
- package/dist/serve/site-ui/docs/api-reference.md +1 -0
- package/dist/serve/site-ui/docs/app-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/auth-compat/index.html +2 -2
- package/dist/serve/site-ui/docs/auth-compat.md +1 -1
- package/dist/serve/site-ui/docs/auth-flutter-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/auth-kotlin-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/auth-swift-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/build/ai-logic/index.html +1 -1
- package/dist/serve/site-ui/docs/build/authentication/index.html +1 -1
- package/dist/serve/site-ui/docs/build/cloud-firestore/index.html +1 -1
- package/dist/serve/site-ui/docs/build/cloud-messaging/index.html +1 -1
- package/dist/serve/site-ui/docs/build/cloud-storage/index.html +13 -2
- package/dist/serve/site-ui/docs/build/cloud-storage.md +23 -0
- package/dist/serve/site-ui/docs/build/default-avatars/index.html +1 -1
- package/dist/serve/site-ui/docs/build/inspect-activity/index.html +1 -1
- package/dist/serve/site-ui/docs/build/realtime-database/index.html +1 -1
- package/dist/serve/site-ui/docs/conformance-scores/index.html +1 -1
- package/dist/serve/site-ui/docs/create-pyric-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/database-compat/index.html +5 -1
- package/dist/serve/site-ui/docs/database-compat.md +4 -0
- package/dist/serve/site-ui/docs/examples/chess/index.html +1 -1
- package/dist/serve/site-ui/docs/examples/index.html +1 -1
- package/dist/serve/site-ui/docs/firestore-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/firestore-flutter-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/firestore-kotlin-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/firestore-swift-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/functions-rtdb-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/get-started/backend-bundlers/index.html +1 -1
- package/dist/serve/site-ui/docs/get-started/flutter/index.html +1 -1
- package/dist/serve/site-ui/docs/get-started/how-the-swap-works/index.html +1 -1
- package/dist/serve/site-ui/docs/get-started/nextjs/index.html +1 -1
- package/dist/serve/site-ui/docs/get-started/node-host/index.html +1 -1
- package/dist/serve/site-ui/docs/get-started/start-building/index.html +2 -1
- package/dist/serve/site-ui/docs/get-started/start-building.md +2 -0
- package/dist/serve/site-ui/docs/get-started/vite/index.html +4 -2
- package/dist/serve/site-ui/docs/get-started/vite.md +4 -0
- package/dist/serve/site-ui/docs/index.json +50 -0
- package/dist/serve/site-ui/docs/messaging-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/observe/resolve-runtime-status/index.html +1 -1
- package/dist/serve/site-ui/docs/observe/see-whats-happening/index.html +1 -1
- package/dist/serve/site-ui/docs/observe/shape-your-data/index.html +1 -1
- package/dist/serve/site-ui/docs/observe/switch-and-impersonate-identities/index.html +1 -1
- package/dist/serve/site-ui/docs/overview/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-app-check-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-data-connect-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-eventarc-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-extensions-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-functions-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-installations-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-instance-id-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-machine-learning-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-messaging-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-phone-number-verification-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-project-management-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-remote-config-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-security-rules-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ai-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-analytics-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-app-check-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-app-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-auth-reference-api/index.html +2 -2
- package/dist/serve/site-ui/docs/pyric-auth-reference-api.md +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-assurance-reference-api/index.html +24 -5
- package/dist/serve/site-ui/docs/pyric-cli-assurance-reference-api.md +11 -4
- package/dist/serve/site-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-bundler-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-flow-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-next-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-register-app-bridge-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-register-client-app-bridge-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-register-client-app-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-register-client-auth-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-register-client-database-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-register-client-firestore-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-register-client-storage-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-repro-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-serve-worker-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-verify-reference-api/index.html +16 -3
- package/dist/serve/site-ui/docs/pyric-cli-verify-reference-api.md +11 -10
- package/dist/serve/site-ui/docs/pyric-cli-vite-reference-api/index.html +17 -3
- package/dist/serve/site-ui/docs/pyric-cli-vite-reference-api.md +7 -5
- package/dist/serve/site-ui/docs/pyric-cli-vite-ssr-loader-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-database-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-firestore-lite-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-firestore-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-functions-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-messaging-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-performance-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-remote-config-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-rules-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-sandbox-branches-store-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-sandbox-checkpoints-directory-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-sandbox-checkpoints-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-sandbox-database-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-sandbox-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-storage-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-storage-sw-reference-api/index.html +102 -0
- package/dist/serve/site-ui/docs/pyric-storage-sw-reference-api.md +76 -0
- package/dist/serve/site-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-listener-owner-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-storage-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/reference/cli/index.html +12 -2
- package/dist/serve/site-ui/docs/reference/cli.md +24 -0
- package/dist/serve/site-ui/docs/rtdb-flutter-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/rtdb-kotlin-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/rtdb-swift-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/rules-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/audit-your-rules/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/firestore-rules-limits/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/read-a-denial/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/rtdb-rules-standard-library/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/rules-standard-library/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/secure-it-with-rules/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/simulate-and-lint/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/write-a-rules-test-suite/index.html +2 -2
- package/dist/serve/site-ui/docs/secure/write-a-rules-test-suite.md +1 -1
- package/dist/serve/site-ui/docs/ship/ship-to-production/index.html +5 -2
- package/dist/serve/site-ui/docs/ship/ship-to-production.md +7 -1
- package/dist/serve/site-ui/docs/ship/test-in-node/index.html +1 -1
- package/dist/serve/site-ui/docs/storage-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
- package/dist/serve/site-ui/docs/trust/rules-standard-library/index.html +1 -1
- package/dist/serve/site-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
- package/dist/serve/site-ui/docs/tutorial/index.html +1 -1
- package/dist/serve/site-ui/examples/chess/index.html +1 -1
- package/dist/serve/site-ui/firestore/index.html +1 -1
- package/dist/serve/site-ui/llms.txt +1 -0
- package/dist/serve/site-ui/rtdb/index.html +1 -1
- package/dist/serve/site-ui/settings/index.html +1 -1
- package/dist/serve/site-ui/storage/index.html +1 -1
- package/dist/serve/site-ui/studio/index.html +1 -1
- package/dist/serve/site-ui/traffic/index.html +1 -1
- package/dist/serve/verify-hint.d.ts +2 -0
- package/dist/serve/verify-hint.d.ts.map +1 -0
- package/dist/serve/verify-hint.js +10 -0
- package/dist/serve/verify-hint.js.map +1 -0
- package/dist/serve/vite-generation-rules-watch.d.ts.map +1 -1
- package/dist/serve/vite-generation-rules-watch.js +5 -3
- package/dist/serve/vite-generation-rules-watch.js.map +1 -1
- package/dist/serve/vite-plugin.d.ts +6 -0
- package/dist/serve/vite-plugin.d.ts.map +1 -1
- package/dist/serve/vite-plugin.js +11 -1
- package/dist/serve/vite-plugin.js.map +1 -1
- package/dist/serve/vite-service-worker.d.ts +12 -0
- package/dist/serve/vite-service-worker.d.ts.map +1 -0
- package/dist/serve/vite-service-worker.js +33 -0
- package/dist/serve/vite-service-worker.js.map +1 -0
- package/dist/serve/worker/client/auth.d.ts.map +1 -1
- package/dist/serve/worker/client/auth.js +4 -3
- package/dist/serve/worker/client/auth.js.map +1 -1
- package/dist/serve/worker/host/auth-action-codes.js +2 -2
- package/dist/serve/worker/host/auth-action-codes.js.map +1 -1
- package/dist/serve/worker/host/auth-custom-token.d.ts.map +1 -1
- package/dist/serve/worker/host/auth-custom-token.js +4 -2
- package/dist/serve/worker/host/auth-custom-token.js.map +1 -1
- package/dist/serve/worker/host/dispatch.d.ts.map +1 -1
- package/dist/serve/worker/host/dispatch.js +3 -1
- package/dist/serve/worker/host/dispatch.js.map +1 -1
- package/dist/serve/worker/host-auth.d.ts +6 -0
- package/dist/serve/worker/host-auth.d.ts.map +1 -1
- package/dist/serve/worker/host-auth.js +16 -5
- package/dist/serve/worker/host-auth.js.map +1 -1
- package/dist/serve/worker/inbound-validation.d.ts.map +1 -1
- package/dist/serve/worker/inbound-validation.js +1 -0
- package/dist/serve/worker/inbound-validation.js.map +1 -1
- package/dist/serve/worker/protocol/storage.d.ts +1 -8
- package/dist/serve/worker/protocol/storage.d.ts.map +1 -1
- package/dist/serve/worker/protocol/storage.js +2 -14
- package/dist/serve/worker/protocol/storage.js.map +1 -1
- package/dist/serve/worker/protocol.d.ts +2 -0
- package/dist/serve/worker/protocol.d.ts.map +1 -1
- package/dist/serve/worker/protocol.js.map +1 -1
- package/dist/verify/index.d.ts +16 -2
- package/dist/verify/index.d.ts.map +1 -1
- package/dist/verify/index.js +100 -18
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/staging/engine.d.ts +102 -0
- package/dist/verify/staging/engine.d.ts.map +1 -0
- package/dist/verify/staging/engine.js +371 -0
- package/dist/verify/staging/engine.js.map +1 -0
- package/dist/verify/staging/rest.d.ts +68 -0
- package/dist/verify/staging/rest.d.ts.map +1 -0
- package/dist/verify/staging/rest.js +93 -0
- package/dist/verify/staging/rest.js.map +1 -0
- package/dist/verify/staging/target.d.ts +39 -0
- package/dist/verify/staging/target.d.ts.map +1 -0
- package/dist/verify/staging/target.js +117 -0
- package/dist/verify/staging/target.js.map +1 -0
- package/package.json +12 -8
- package/src/assurance/.generated/conformance-verdicts.ts +5 -0
- package/src/assurance/capabilities.ts +22 -4
- package/src/assurance/runner.ts +40 -58
- package/src/assurance/types.ts +3 -0
- package/src/assurance/validation.ts +137 -0
- package/src/bridge/surface/arguments/assurance.ts +1 -0
- package/src/cli/database-rules.ts +1 -1
- package/src/cli/index.ts +28 -3
- package/src/cli/pyric-config.ts +5 -0
- package/src/cli/run.ts +139 -0
- package/src/cli/serve.ts +6 -3
- package/src/cli/verify.ts +244 -18
- package/src/conformance/.generated/can-i-use.ts +1 -1
- package/src/conformance/.generated/conformance-docs.ts +1 -1
- package/src/remote/local-storage-url.ts +1 -58
- package/src/serve/assets/avatar-route.ts +6 -7
- package/src/serve/assets/resolver.ts +4 -1
- package/src/serve/entries/app-client.ts +7 -0
- package/src/serve/repro/denials.ts +2 -1
- package/src/serve/verify-hint.ts +8 -0
- package/src/serve/vite-generation-rules-watch.ts +6 -3
- package/src/serve/vite-plugin.ts +15 -1
- package/src/serve/vite-service-worker.ts +44 -0
- package/src/serve/worker/client/auth.ts +4 -3
- package/src/serve/worker/host/auth-action-codes.ts +2 -2
- package/src/serve/worker/host/auth-custom-token.ts +4 -2
- package/src/serve/worker/host/dispatch.ts +2 -0
- package/src/serve/worker/host-auth.ts +18 -5
- package/src/serve/worker/inbound-validation.ts +1 -0
- package/src/serve/worker/protocol/storage.ts +2 -14
- package/src/serve/worker/protocol.ts +2 -0
- package/src/verify/index.ts +119 -19
- package/src/verify/staging/engine.ts +455 -0
- package/src/verify/staging/rest.ts +129 -0
- package/src/verify/staging/target.ts +137 -0
- 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
|
|
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':
|
package/src/cli/pyric-config.ts
CHANGED
|
@@ -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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|