@pyric/cli 0.1.0-alpha.18 → 0.1.0-alpha.20
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 +14 -9
- package/dist/bridge/server/mcp-contract.d.ts +2 -2
- package/dist/bridge/server/mcp-contract.d.ts.map +1 -1
- package/dist/bridge/server/mcp-contract.js +3 -0
- package/dist/bridge/server/mcp-contract.js.map +1 -1
- package/dist/cli/index.d.ts +3 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +51 -42
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-proxy.d.ts.map +1 -1
- package/dist/cli/mcp-proxy.js +3 -3
- package/dist/cli/mcp-proxy.js.map +1 -1
- package/dist/cli/parse-args.d.ts +1 -1
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +51 -15
- package/dist/cli/parse-args.js.map +1 -1
- package/dist/cli/pyric-config.d.ts +24 -0
- package/dist/cli/pyric-config.d.ts.map +1 -0
- package/dist/cli/pyric-config.js +28 -0
- package/dist/cli/pyric-config.js.map +1 -0
- package/dist/cli/rules.d.ts +1 -0
- package/dist/cli/rules.d.ts.map +1 -1
- package/dist/cli/rules.js +18 -4
- package/dist/cli/rules.js.map +1 -1
- package/dist/cli/{dev-runner.d.ts → sandbox-runner.d.ts} +33 -36
- package/dist/cli/sandbox-runner.d.ts.map +1 -0
- package/dist/cli/{dev-runner.js → sandbox-runner.js} +90 -55
- package/dist/cli/sandbox-runner.js.map +1 -0
- package/dist/cli/serve.d.ts +6 -4
- package/dist/cli/serve.d.ts.map +1 -1
- package/dist/cli/serve.js +99 -40
- package/dist/cli/serve.js.map +1 -1
- package/dist/cli/service-command-records/storage-rules-resolve.d.ts +4 -0
- package/dist/cli/service-command-records/storage-rules-resolve.d.ts.map +1 -0
- package/dist/cli/service-command-records/storage-rules-resolve.js +3 -0
- package/dist/cli/service-command-records/storage-rules-resolve.js.map +1 -0
- package/dist/cli/service-commands.generated.d.ts +3 -0
- package/dist/cli/service-commands.generated.d.ts.map +1 -1
- package/dist/cli/service-commands.generated.js +2 -0
- package/dist/cli/service-commands.generated.js.map +1 -1
- package/dist/cli/snapshot.js +5 -5
- package/dist/cli/snapshot.js.map +1 -1
- package/dist/cli/verify.js +1 -1
- package/dist/cli/verify.js.map +1 -1
- package/dist/functions-rtdb/development-runtime.js +1 -1
- package/dist/functions-rtdb/development-runtime.js.map +1 -1
- package/dist/functions-rtdb/project.js +8 -8
- package/dist/functions-rtdb/project.js.map +1 -1
- package/dist/next/guard.js +1 -1
- package/dist/next/guard.js.map +1 -1
- package/dist/remote/index.d.ts +1 -1
- package/dist/remote/index.js +9 -9
- package/dist/remote/index.js.map +1 -1
- package/dist/serve/bundler.js +10 -10
- package/dist/serve/bundler.js.map +1 -1
- package/dist/serve/entries/ai.d.ts.map +1 -1
- package/dist/serve/entries/ai.js +27 -1
- package/dist/serve/entries/ai.js.map +1 -1
- package/dist/serve/entries/auth-helper-dom.js +1 -1
- package/dist/serve/entries/auth-helper-dom.js.map +1 -1
- package/dist/serve/entries/auth-helper-runtime.js +1 -1
- package/dist/serve/entries/auth-helper-runtime.js.map +1 -1
- package/dist/serve/entries/firestore.js +3 -3
- package/dist/serve/entries/firestore.js.map +1 -1
- package/dist/serve/entries/keepalive.js +1 -1
- package/dist/serve/entries/keepalive.js.map +1 -1
- package/dist/serve/entries/runtime.js +21 -14
- package/dist/serve/entries/runtime.js.map +1 -1
- package/dist/serve/entries/storage.js +1 -1
- package/dist/serve/entries/storage.js.map +1 -1
- package/dist/serve/entries/worker-runtime.js +1 -1
- package/dist/serve/entries/worker-runtime.js.map +1 -1
- package/dist/serve/init-payload.d.ts +4 -0
- package/dist/serve/init-payload.d.ts.map +1 -1
- package/dist/serve/namespace.d.ts +6 -0
- package/dist/serve/namespace.d.ts.map +1 -1
- package/dist/serve/namespace.js +36 -5
- package/dist/serve/namespace.js.map +1 -1
- package/dist/serve/rules.d.ts +3 -3
- package/dist/serve/rules.d.ts.map +1 -1
- package/dist/serve/rules.js +56 -25
- package/dist/serve/rules.js.map +1 -1
- package/dist/serve/runtime/chip.d.ts.map +1 -1
- package/dist/serve/runtime/chip.js +32 -1
- package/dist/serve/runtime/chip.js.map +1 -1
- package/dist/serve/runtime/manifest.d.ts +1 -1
- package/dist/serve/runtime/manifest.d.ts.map +1 -1
- package/dist/serve/runtime/manifest.js +1 -1
- package/dist/serve/runtime/manifest.js.map +1 -1
- package/dist/serve/runtime/status.d.ts.map +1 -1
- package/dist/serve/runtime/status.js +23 -0
- package/dist/serve/runtime/status.js.map +1 -1
- package/dist/serve/sandbox-session.d.ts +1 -0
- package/dist/serve/sandbox-session.d.ts.map +1 -1
- package/dist/serve/sandbox-session.js +10 -4
- package/dist/serve/sandbox-session.js.map +1 -1
- package/dist/serve/server.d.ts +7 -0
- package/dist/serve/server.d.ts.map +1 -1
- package/dist/serve/server.js +15 -1
- package/dist/serve/server.js.map +1 -1
- package/dist/serve/site-ui/404.html +1 -1
- package/dist/serve/site-ui/_astro/{RulesCodeEditor.CzwHdCi9.js → RulesCodeEditor.XtDmHOlN.js} +1 -1
- package/dist/serve/site-ui/_astro/_example_.B9DG9HLJ.css +1 -0
- package/dist/serve/site-ui/_astro/_slug_.Da5VLhW1.css +1 -0
- package/dist/serve/site-ui/_astro/_studio_.BnfYCKoI.css +1 -0
- package/dist/serve/site-ui/_astro/app.B52xr-DK.js +1 -0
- package/dist/serve/site-ui/_astro/app.DWBraPUw.js +12 -0
- package/dist/serve/site-ui/_astro/app.pMSnN6Qw.css +1 -0
- package/dist/serve/site-ui/_astro/chess-showcase.B72XYeoh.js +354 -0
- package/dist/serve/site-ui/_astro/{example-runtime.B0byNQOl.js → example-runtime.C25_lx6Z.js} +2 -26
- package/dist/serve/site-ui/_astro/index.Tb-31efc.css +1 -0
- package/dist/serve/site-ui/_astro/index.UkCBr-_0.css +1 -0
- package/dist/serve/site-ui/_astro/sandbox-controls.DEpnKg-O.js +1169 -0
- package/dist/serve/site-ui/_astro/seed.Co0yUJY5.js +20 -0
- package/dist/serve/site-ui/_astro/writes.rpjrFzmu.js +102 -0
- package/dist/serve/site-ui/auth/index.html +5 -3
- package/dist/serve/site-ui/david-east.jpg +0 -0
- package/dist/serve/site-ui/docs/_rhythm/index.html +6 -3
- package/dist/serve/site-ui/docs/_rhythm.md +1 -1
- package/dist/serve/site-ui/docs/agent/set-up-your-agent/index.html +45 -17
- package/dist/serve/site-ui/docs/agent/set-up-your-agent.md +20 -17
- package/dist/serve/site-ui/docs/agent/watch-and-review/index.html +6 -5
- package/dist/serve/site-ui/docs/agent/watch-and-review.md +1 -5
- package/dist/serve/site-ui/docs/agent/work-with-an-agent/index.html +8 -5
- package/dist/serve/site-ui/docs/agent/work-with-an-agent.md +4 -4
- package/dist/serve/site-ui/docs/ai-compat/index.html +5 -2
- package/dist/serve/site-ui/docs/api-reference/index.html +6 -2
- package/dist/serve/site-ui/docs/api-reference.md +1 -0
- package/dist/serve/site-ui/docs/app-compat/index.html +5 -2
- package/dist/serve/site-ui/docs/auth-compat/index.html +5 -2
- package/dist/serve/site-ui/docs/build/ai-logic/index.html +5 -2
- package/dist/serve/site-ui/docs/build/authentication/index.html +7 -4
- package/dist/serve/site-ui/docs/build/authentication.md +2 -2
- package/dist/serve/site-ui/docs/build/cloud-firestore/index.html +34 -53
- package/dist/serve/site-ui/docs/build/cloud-firestore.md +40 -36
- package/dist/serve/site-ui/docs/build/cloud-messaging/index.html +5 -2
- package/dist/serve/site-ui/docs/build/cloud-storage/index.html +6 -3
- package/dist/serve/site-ui/docs/build/cloud-storage.md +1 -1
- package/dist/serve/site-ui/docs/build/realtime-database/index.html +9 -6
- package/dist/serve/site-ui/docs/build/realtime-database.md +4 -4
- package/dist/serve/site-ui/docs/conformance-scores/index.html +5 -2
- package/dist/serve/site-ui/docs/create-pyric-reference-api/index.html +6 -3
- package/dist/serve/site-ui/docs/create-pyric-reference-api.md +1 -1
- package/dist/serve/site-ui/docs/database-compat/index.html +5 -2
- package/dist/serve/site-ui/docs/examples/chess/index.html +649 -250
- package/dist/serve/site-ui/docs/examples/chess.md +45 -16
- package/dist/serve/site-ui/docs/examples/index.html +6 -3
- package/dist/serve/site-ui/docs/firestore-compat/index.html +5 -2
- package/dist/serve/site-ui/docs/functions-rtdb-compat/index.html +5 -2
- package/dist/serve/site-ui/docs/get-started/how-the-swap-works/index.html +9 -6
- package/dist/serve/site-ui/docs/get-started/how-the-swap-works.md +5 -5
- package/dist/serve/site-ui/docs/get-started/nextjs/index.html +9 -6
- package/dist/serve/site-ui/docs/get-started/nextjs.md +4 -4
- package/dist/serve/site-ui/docs/get-started/start-building/index.html +41 -10
- package/dist/serve/site-ui/docs/get-started/start-building.md +21 -10
- package/dist/serve/site-ui/docs/get-started/vite/index.html +6 -3
- package/dist/serve/site-ui/docs/get-started/vite.md +1 -1
- package/dist/serve/site-ui/docs/index.json +203 -168
- package/dist/serve/site-ui/docs/messaging-compat/index.html +5 -2
- package/dist/serve/site-ui/docs/observe/resolve-runtime-status/index.html +5 -2
- package/dist/serve/site-ui/docs/observe/see-whats-happening/index.html +6 -4
- package/dist/serve/site-ui/docs/observe/see-whats-happening.md +2 -4
- package/dist/serve/site-ui/docs/observe/shape-your-data/index.html +9 -6
- package/dist/serve/site-ui/docs/observe/shape-your-data.md +4 -4
- package/dist/serve/site-ui/docs/overview/index.html +16 -10
- package/dist/serve/site-ui/docs/overview.md +19 -11
- package/dist/serve/site-ui/docs/pyric-admin-app-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-admin-auth-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-admin-database-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-admin-firestore-reference-api/index.html +8 -5
- package/dist/serve/site-ui/docs/pyric-admin-firestore-reference-api.md +3 -3
- package/dist/serve/site-ui/docs/pyric-admin-messaging-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-admin-reference-api/index.html +501 -0
- package/dist/serve/site-ui/docs/pyric-admin-reference-api.md +587 -0
- package/dist/serve/site-ui/docs/pyric-admin-storage-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ai-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ai-scripting-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-app-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-auth-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-cli-assurance-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-cli-bridge-client-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-cli-bridge-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-cli-conformance-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-cli-credentials-node-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-cli-discover-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-cli-next-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-cli-register-app-bridge-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-cli-register-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-cli-remote-reference-api/index.html +6 -3
- package/dist/serve/site-ui/docs/pyric-cli-remote-reference-api.md +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-serve-worker-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-cli-verify-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-cli-vite-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-database-reference-api/index.html +166 -95
- package/dist/serve/site-ui/docs/pyric-database-reference-api.md +107 -1
- package/dist/serve/site-ui/docs/pyric-firestore-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-messaging-reference-api/index.html +10 -4
- package/dist/serve/site-ui/docs/pyric-messaging-reference-api.md +5 -2
- package/dist/serve/site-ui/docs/pyric-messaging-sw-reference-api/index.html +10 -4
- package/dist/serve/site-ui/docs/pyric-messaging-sw-reference-api.md +5 -2
- package/dist/serve/site-ui/docs/pyric-rules-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-sandbox-database-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-sandbox-firestore-reference-api/index.html +6 -3
- package/dist/serve/site-ui/docs/pyric-sandbox-firestore-reference-api.md +2 -2
- package/dist/serve/site-ui/docs/pyric-sandbox-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-storage-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ui-agents-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ui-auth-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ui-events-hooks-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ui-events-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ui-firestore-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ui-primitives-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ui-rtdb-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ui-rules-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ui-storage-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/pyric-ui-traffic-reference-api/index.html +5 -2
- package/dist/serve/site-ui/docs/reference/cli/index.html +153 -0
- package/dist/serve/site-ui/docs/reference/cli.md +114 -0
- package/dist/serve/site-ui/docs/rules-compat/index.html +5 -2
- package/dist/serve/site-ui/docs/secure/audit-your-rules/index.html +5 -2
- package/dist/serve/site-ui/docs/secure/firestore-rules-limits/index.html +5 -2
- package/dist/serve/site-ui/docs/secure/read-a-denial/index.html +6 -3
- package/dist/serve/site-ui/docs/secure/read-a-denial.md +1 -1
- package/dist/serve/site-ui/docs/secure/rtdb-rules-in-typescript/index.html +5 -2
- package/dist/serve/site-ui/docs/secure/rules-standard-library/index.html +5 -2
- package/dist/serve/site-ui/docs/secure/secure-it-with-rules/index.html +5 -2
- package/dist/serve/site-ui/docs/secure/simulate-and-lint/index.html +5 -2
- package/dist/serve/site-ui/docs/secure/write-a-rules-test-suite/index.html +5 -2
- package/dist/serve/site-ui/docs/ship/ship-to-production/index.html +7 -4
- package/dist/serve/site-ui/docs/ship/ship-to-production.md +2 -2
- package/dist/serve/site-ui/docs/ship/test-in-node/index.html +9 -6
- package/dist/serve/site-ui/docs/ship/test-in-node.md +4 -4
- package/dist/serve/site-ui/docs/storage-compat/index.html +5 -2
- package/dist/serve/site-ui/docs/trust/how-we-know-it-matches-firebase/index.html +5 -2
- package/dist/serve/site-ui/docs/trust/rules-standard-library/index.html +5 -2
- package/dist/serve/site-ui/docs/trust/versioning-and-compatibility/index.html +5 -2
- package/dist/serve/site-ui/docs/tutorial/index.html +10 -7
- package/dist/serve/site-ui/docs/tutorial.md +4 -4
- package/dist/serve/site-ui/examples/chess/index.html +4 -2
- package/dist/serve/site-ui/firestore/index.html +5 -3
- package/dist/serve/site-ui/hero-charcoal.jpg +0 -0
- package/dist/serve/site-ui/index.html +9 -7
- package/dist/serve/site-ui/llms.txt +4 -3
- package/dist/serve/site-ui/pyric-campfire-full.png +0 -0
- package/dist/serve/site-ui/pyric-campfire.png +0 -0
- package/dist/serve/site-ui/rtdb/index.html +5 -3
- package/dist/serve/site-ui/settings/index.html +5 -3
- package/dist/serve/site-ui/storage/index.html +5 -3
- package/dist/serve/site-ui/studio/index.html +13 -0
- package/dist/serve/site-ui/studio-routes.json +1 -1
- package/dist/serve/site-ui/traffic/index.html +5 -3
- package/dist/serve/studio/routes.d.ts +9 -0
- package/dist/serve/studio/routes.d.ts.map +1 -1
- package/dist/serve/studio/routes.js +47 -7
- package/dist/serve/studio/routes.js.map +1 -1
- package/dist/serve/vite-ai-config.d.ts.map +1 -1
- package/dist/serve/vite-ai-config.js +38 -8
- package/dist/serve/vite-ai-config.js.map +1 -1
- package/dist/serve/vite-module-swap.d.ts.map +1 -1
- package/dist/serve/vite-module-swap.js +0 -28
- package/dist/serve/vite-module-swap.js.map +1 -1
- package/dist/serve/vite-rules-source.d.ts.map +1 -1
- package/dist/serve/vite-rules-source.js +28 -7
- package/dist/serve/vite-rules-source.js.map +1 -1
- package/dist/serve/vite-sandbox-generation.d.ts +1 -1
- package/dist/serve/vite-sandbox-generation.d.ts.map +1 -1
- package/dist/serve/vite-sandbox-generation.js +1 -1
- package/dist/serve/vite-sandbox-generation.js.map +1 -1
- package/dist/serve/worker/client/disconnect.js +1 -1
- package/dist/serve/worker/client/disconnect.js.map +1 -1
- package/dist/serve/worker/client/presence.d.ts.map +1 -1
- package/dist/serve/worker/client/presence.js +18 -5
- package/dist/serve/worker/client/presence.js.map +1 -1
- package/dist/serve/worker/host-ai.d.ts.map +1 -1
- package/dist/serve/worker/host-ai.js +15 -0
- package/dist/serve/worker/host-ai.js.map +1 -1
- package/dist/serve/worker/protocol.d.ts +4 -0
- package/dist/serve/worker/protocol.d.ts.map +1 -1
- package/dist/serve/worker/protocol.js.map +1 -1
- package/dist/serve/worker/serve-init.d.ts.map +1 -1
- package/dist/serve/worker/serve-init.js +7 -1
- package/dist/serve/worker/serve-init.js.map +1 -1
- package/package.json +5 -5
- package/src/bridge/server/mcp-contract.ts +3 -0
- package/src/cli/cli.test.ts +2 -2
- package/src/cli/index.ts +51 -42
- package/src/cli/mcp-proxy.ts +10 -11
- package/src/cli/parse-args.ts +61 -15
- package/src/cli/pyric-config.ts +45 -0
- package/src/cli/rules.ts +31 -4
- package/src/cli/{dev-runner.ts → sandbox-runner.ts} +117 -72
- package/src/cli/serve.ts +115 -47
- package/src/cli/service-command-records/storage-rules-resolve.ts +4 -0
- package/src/cli/service-commands.generated.ts +2 -0
- package/src/cli/snapshot.ts +5 -5
- package/src/cli/verify.ts +1 -1
- package/src/functions-rtdb/development-runtime.ts +1 -1
- package/src/functions-rtdb/project.ts +8 -8
- package/src/next/guard.ts +1 -1
- package/src/remote/index.ts +9 -9
- package/src/serve/bundler.ts +10 -10
- package/src/serve/entries/ai.ts +27 -1
- package/src/serve/entries/auth-helper-dom.ts +1 -1
- package/src/serve/entries/auth-helper-runtime.ts +1 -1
- package/src/serve/entries/firestore.ts +3 -3
- package/src/serve/entries/keepalive.ts +1 -1
- package/src/serve/entries/runtime.ts +19 -14
- package/src/serve/entries/storage.ts +1 -1
- package/src/serve/entries/worker-runtime.ts +1 -1
- package/src/serve/init-payload.ts +4 -0
- package/src/serve/namespace.ts +44 -5
- package/src/serve/rules.ts +61 -26
- package/src/serve/runtime/chip.ts +38 -1
- package/src/serve/runtime/manifest.ts +1 -1
- package/src/serve/runtime/status.ts +24 -0
- package/src/serve/sandbox-session.ts +10 -4
- package/src/serve/server.ts +18 -1
- package/src/serve/studio/routes.ts +63 -7
- package/src/serve/vite-ai-config.ts +37 -7
- package/src/serve/vite-module-swap.ts +0 -28
- package/src/serve/vite-rules-source.ts +29 -7
- package/src/serve/vite-sandbox-generation.ts +1 -1
- package/src/serve/worker/client/disconnect.ts +1 -1
- package/src/serve/worker/client/presence.ts +25 -5
- package/src/serve/worker/host-ai.ts +15 -0
- package/src/serve/worker/protocol.ts +2 -1
- package/src/serve/worker/serve-init.ts +5 -1
- package/README.md.orig +0 -141
- package/dist/cli/dev-runner.d.ts.map +0 -1
- package/dist/cli/dev-runner.js.map +0 -1
- package/dist/serve/site-ui/_astro/_example_.jevZ7OdV.css +0 -1
- package/dist/serve/site-ui/_astro/_slug_.C6QfVOxJ.css +0 -1
- package/dist/serve/site-ui/_astro/_slug_.CXh9yTP1.css +0 -1
- package/dist/serve/site-ui/_astro/_studio_.B6b_G1ev.css +0 -1
- package/dist/serve/site-ui/_astro/app.B7pBzz5N.js +0 -11
- package/dist/serve/site-ui/_astro/app.BsFsMjn0.css +0 -1
- package/dist/serve/site-ui/_astro/app.boex5SnQ.js +0 -1
- package/dist/serve/site-ui/_astro/chess-showcase.DDqE-QGe.js +0 -1
- package/dist/serve/site-ui/_astro/chess-source-explorer.C5k9c5yK.js +0 -1
- package/dist/serve/site-ui/_astro/chess-v2.CFHmd3UL.js +0 -354
- package/dist/serve/site-ui/_astro/index.uwPvjH_1.js +0 -1
- package/dist/serve/site-ui/_astro/jsx-runtime.D_zvdyIk.js +0 -9
- package/dist/serve/site-ui/_astro/reads.Dh3z0fJy.js +0 -1
- package/dist/serve/site-ui/_astro/sandbox-controls.C2IqtoSw.js +0 -1252
- package/dist/serve/site-ui/_astro/seed.CCmj00su.js +0 -20
- package/dist/serve/site-ui/docs/trust/storage-rules-stdlib-research/index.html +0 -582
- package/dist/serve/site-ui/docs/trust/storage-rules-stdlib-research.md +0 -448
- package/dist/serve/site-ui/examples/firestore-first-write/index.html +0 -2
package/src/serve/namespace.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { collectBody } from '../bridge/server/peer.js';
|
|
|
18
18
|
import { StateFileError, type StateSection, type StateStore } from './state-store.js';
|
|
19
19
|
import { createWriterLock, type WriterLock } from './writer-lock.js';
|
|
20
20
|
import { createStudioRoutes, type StudioRouteOptions } from './studio/index.js';
|
|
21
|
-
import { pipeFileToResponse, type ServeLogger } from './server.js';
|
|
21
|
+
import { pipeFileToResponse, getHeader, isAllowedHost, isAllowedOrigin, type ServeLogger } from './server.js';
|
|
22
22
|
import type { InitPayload } from './init-payload.js';
|
|
23
23
|
import { handleActivity } from './activity-route.js';
|
|
24
24
|
import { createSiteTreeHandler } from './site-tree.js';
|
|
@@ -107,6 +107,12 @@ export interface NamespaceOptions {
|
|
|
107
107
|
* (disk-backed `WorkspaceStore`/`ProjectStore`, plus the SSE watch stream)
|
|
108
108
|
* that `@pyric/studio`'s `local` mode talks to. */
|
|
109
109
|
studio?: StudioRouteOptions;
|
|
110
|
+
/** Per-boot session capability token required on workspace and project endpoints. */
|
|
111
|
+
sessionToken?: string;
|
|
112
|
+
/** Bound host for DNS rebinding guard. */
|
|
113
|
+
boundHost?: string;
|
|
114
|
+
/** Extra allowed hostnames. */
|
|
115
|
+
allowedHosts?: string[];
|
|
110
116
|
/** The OpenAI-compatible upstream `/__pyric/ai-proxy` forwards to
|
|
111
117
|
* (pyric/ai — cdd-deltas #98.2). Falls back to the
|
|
112
118
|
* `PYRIC_AI_PROXY_UPSTREAM` env var, then `http://localhost:11434/v1`
|
|
@@ -471,8 +477,20 @@ async function handleDenials(
|
|
|
471
477
|
}
|
|
472
478
|
|
|
473
479
|
export function createPyricNamespace(opts: NamespaceOptions) {
|
|
474
|
-
const
|
|
475
|
-
const
|
|
480
|
+
const stateWriterLock = createWriterLock();
|
|
481
|
+
const studioWriterLock = opts.studio?.writerLock ?? createWriterLock();
|
|
482
|
+
const sessionToken = opts.sessionToken ?? randomBytes(24).toString('base64url');
|
|
483
|
+
let studioRoutes: ((req: IncomingMessage, res: ServerResponse, url: URL) => Promise<boolean>) | null = null;
|
|
484
|
+
if (opts.studio) {
|
|
485
|
+
const studioOptions: StudioRouteOptions = {
|
|
486
|
+
...opts.studio,
|
|
487
|
+
sessionToken,
|
|
488
|
+
writerLock: studioWriterLock,
|
|
489
|
+
boundHost: opts.boundHost,
|
|
490
|
+
allowedHosts: opts.allowedHosts,
|
|
491
|
+
};
|
|
492
|
+
studioRoutes = createStudioRoutes(studioOptions);
|
|
493
|
+
}
|
|
476
494
|
const siteTree = opts.siteUiDir
|
|
477
495
|
? createSiteTreeHandler(opts.siteUiDir, opts.workerVersion)
|
|
478
496
|
: null;
|
|
@@ -489,7 +507,7 @@ export function createPyricNamespace(opts: NamespaceOptions) {
|
|
|
489
507
|
return studioRoutes(req, res, url);
|
|
490
508
|
}
|
|
491
509
|
if (opts.state && url.pathname === '/__pyric/state') {
|
|
492
|
-
return handleState(opts.state!,
|
|
510
|
+
return handleState(opts.state!, stateWriterLock, req, res, url).then(() => true);
|
|
493
511
|
}
|
|
494
512
|
if (opts.capture && url.pathname === '/__pyric/capture') {
|
|
495
513
|
return handleCapture(opts.capture, req, res).then(() => true);
|
|
@@ -498,6 +516,21 @@ export function createPyricNamespace(opts: NamespaceOptions) {
|
|
|
498
516
|
return handleActivity(opts.activity, req, res, activityToken!).then(() => true);
|
|
499
517
|
}
|
|
500
518
|
if (opts.events && url.pathname === '/__pyric/events') {
|
|
519
|
+
const hostHeader = getHeader(req, 'host');
|
|
520
|
+
const originHeader = getHeader(req, 'origin');
|
|
521
|
+
if (!isAllowedHost(hostHeader, opts.boundHost ?? 'localhost', opts.allowedHosts)) {
|
|
522
|
+
res.writeHead(403, { 'content-type': 'text/plain' }).end('Forbidden: host not allowed');
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
if (originHeader && !isAllowedOrigin(originHeader, opts.boundHost ?? 'localhost', opts.allowedHosts)) {
|
|
526
|
+
res.writeHead(403, { 'content-type': 'text/plain' }).end('Forbidden: origin mismatch');
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
const reqToken = getHeader(req, 'x-pyric-session-token') ?? url.searchParams.get('token');
|
|
530
|
+
if (sessionToken && reqToken && reqToken !== sessionToken) {
|
|
531
|
+
res.writeHead(401, { 'content-type': 'text/plain' }).end('Unauthorized: invalid session capability token');
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
501
534
|
opts.events.handle(req, res);
|
|
502
535
|
return true;
|
|
503
536
|
}
|
|
@@ -509,7 +542,13 @@ export function createPyricNamespace(opts: NamespaceOptions) {
|
|
|
509
542
|
}
|
|
510
543
|
if (url.pathname === '/__pyric/init.json') {
|
|
511
544
|
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
512
|
-
res.end(
|
|
545
|
+
res.end(
|
|
546
|
+
JSON.stringify({
|
|
547
|
+
...opts.initPayload(),
|
|
548
|
+
sessionToken,
|
|
549
|
+
...(activityToken ? { activityToken } : {}),
|
|
550
|
+
}),
|
|
551
|
+
);
|
|
513
552
|
return true;
|
|
514
553
|
}
|
|
515
554
|
if (url.pathname.startsWith('/__pyric/sdk/')) {
|
package/src/serve/rules.ts
CHANGED
|
@@ -56,7 +56,7 @@ export function prepareRulesSource(raw: string, sourcePath: string): string {
|
|
|
56
56
|
const resolved = resolveModulesBrowser(raw);
|
|
57
57
|
if (!resolved.success) {
|
|
58
58
|
throw new Error(
|
|
59
|
-
`pyric
|
|
59
|
+
`pyric sandbox: ${sourcePath} uses 2+modules but module resolution failed: ${resolved.error.message}`,
|
|
60
60
|
);
|
|
61
61
|
}
|
|
62
62
|
source = resolved.data.resolved;
|
|
@@ -65,13 +65,13 @@ export function prepareRulesSource(raw: string, sourcePath: string): string {
|
|
|
65
65
|
if (lint.parseError) {
|
|
66
66
|
const { line, column } = lint.parseError;
|
|
67
67
|
throw new Error(
|
|
68
|
-
`pyric
|
|
68
|
+
`pyric sandbox: ${sourcePath} failed to parse (line ${line}, col ${column}). Fix the rules before serving.`,
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
71
|
const errors = lint.warnings.filter((w) => w.severity === 'error');
|
|
72
72
|
if (errors.length > 0) {
|
|
73
73
|
throw new Error(
|
|
74
|
-
`pyric
|
|
74
|
+
`pyric sandbox: ${sourcePath} has ${errors.length} rules error(s):\n` +
|
|
75
75
|
errors.map((e) => ` - ${e.message}`).join('\n'),
|
|
76
76
|
);
|
|
77
77
|
}
|
|
@@ -98,7 +98,7 @@ export async function loadProjectRules(
|
|
|
98
98
|
} catch (e) {
|
|
99
99
|
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
100
100
|
if (configured) {
|
|
101
|
-
throw new Error(`pyric
|
|
101
|
+
throw new Error(`pyric sandbox: firebase.json points firestore.rules at ${path}, but it does not exist.`);
|
|
102
102
|
}
|
|
103
103
|
return { rules: null, rulesHash: null, sourcePath: null };
|
|
104
104
|
}
|
|
@@ -109,19 +109,29 @@ export async function loadProjectRules(
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
112
|
+
* Resolve and validate a raw Storage rules source. Modular sources are
|
|
113
|
+
* authored in storage.modules.rules and resolved in memory before the
|
|
114
|
+
* sandbox sees them; firebase.json can continue to point at storage.rules.
|
|
115
115
|
*/
|
|
116
116
|
export function prepareStorageRulesSource(raw: string, sourcePath: string): string {
|
|
117
|
+
let source = raw;
|
|
118
|
+
if (/^\s*rules_version\s*=\s*['"]2\+modules['"]/m.test(raw)) {
|
|
119
|
+
const resolved = resolveModulesBrowser(raw, { sourceFile: sourcePath });
|
|
120
|
+
if (!resolved.success) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`pyric sandbox: ${sourcePath} uses 2+modules but module resolution failed: ${resolved.error.message}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
source = resolved.data.resolved;
|
|
126
|
+
}
|
|
117
127
|
try {
|
|
118
|
-
parseStorageRules(
|
|
128
|
+
parseStorageRules(source);
|
|
119
129
|
} catch (e) {
|
|
120
130
|
throw new Error(
|
|
121
|
-
`pyric
|
|
131
|
+
`pyric sandbox: ${sourcePath} failed to parse: ${e instanceof Error ? e.message : String(e)}. Fix the rules before serving.`,
|
|
122
132
|
);
|
|
123
133
|
}
|
|
124
|
-
return
|
|
134
|
+
return source;
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
/**
|
|
@@ -149,7 +159,7 @@ export async function loadProjectStorageRules(
|
|
|
149
159
|
} catch (e) {
|
|
150
160
|
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
151
161
|
if (configured) {
|
|
152
|
-
throw new Error(`pyric
|
|
162
|
+
throw new Error(`pyric sandbox: firebase.json points storage.rules at ${path}, but it does not exist.`);
|
|
153
163
|
}
|
|
154
164
|
return { rules: null, rulesHash: null, sourcePath: null };
|
|
155
165
|
}
|
|
@@ -159,27 +169,52 @@ export async function loadProjectStorageRules(
|
|
|
159
169
|
return { rules, rulesHash: rulesHashOf(rules), sourcePath: path };
|
|
160
170
|
}
|
|
161
171
|
|
|
172
|
+
function normalizeDatabaseEntries(block: unknown): Array<Record<string, unknown>> {
|
|
173
|
+
if (!block) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
if (Array.isArray(block)) {
|
|
177
|
+
return block.filter((item): item is Record<string, unknown> => typeof item === 'object' && item !== null);
|
|
178
|
+
}
|
|
179
|
+
if (typeof block === 'object' && block !== null) {
|
|
180
|
+
return [block as Record<string, unknown>];
|
|
181
|
+
}
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function hasConfiguredRules(entry: Record<string, unknown>): entry is Record<string, unknown> & { rules: string } {
|
|
186
|
+
return typeof entry.rules === 'string';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function hasConfiguredUrl(entry: Record<string, unknown>): entry is Record<string, unknown> & { url: string } {
|
|
190
|
+
return typeof entry.url === 'string';
|
|
191
|
+
}
|
|
192
|
+
|
|
162
193
|
export async function loadProjectDatabaseRules(
|
|
163
194
|
cwd: string,
|
|
164
195
|
config: FirebaseJson | null,
|
|
165
196
|
): Promise<LoadedDatabaseRules> {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const path = isAbsolute(configured) ? configured : join(cwd, configured);
|
|
197
|
+
const entries = normalizeDatabaseEntries(config?.database);
|
|
198
|
+
const configuredEntry = entries.find(hasConfiguredRules);
|
|
199
|
+
const configured = configuredEntry?.rules;
|
|
200
|
+
const rel = configured ?? 'database.rules.json';
|
|
201
|
+
const path = isAbsolute(rel) ? rel : join(cwd, rel);
|
|
202
|
+
const urlEntry = entries.find(hasConfiguredUrl);
|
|
203
|
+
const databaseUrl = urlEntry?.url ?? null;
|
|
177
204
|
let raw: string;
|
|
178
205
|
try {
|
|
179
206
|
raw = await readFile(path, 'utf8');
|
|
180
207
|
} catch (e) {
|
|
181
208
|
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
182
|
-
|
|
209
|
+
if (configured) {
|
|
210
|
+
throw new Error(`pyric sandbox: firebase.json points database.rules at ${path}, but it does not exist.`);
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
rules: null,
|
|
214
|
+
rulesHash: null,
|
|
215
|
+
sourcePath: null,
|
|
216
|
+
databaseUrl,
|
|
217
|
+
};
|
|
183
218
|
}
|
|
184
219
|
throw e;
|
|
185
220
|
}
|
|
@@ -188,18 +223,18 @@ export async function loadProjectDatabaseRules(
|
|
|
188
223
|
try {
|
|
189
224
|
parsed = JSON.parse(stripJsonComments(raw));
|
|
190
225
|
} catch (e) {
|
|
191
|
-
throw new Error(`pyric
|
|
226
|
+
throw new Error(`pyric sandbox: ${path} failed to parse as RTDB rules JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
192
227
|
}
|
|
193
228
|
const rules = parseRtdbRulesJson(
|
|
194
229
|
parsed,
|
|
195
|
-
() => new Error(`pyric
|
|
230
|
+
() => new Error(`pyric sandbox: ${path} must contain a top-level "rules" object.`),
|
|
196
231
|
);
|
|
197
232
|
|
|
198
233
|
return {
|
|
199
234
|
rules,
|
|
200
235
|
rulesHash: rulesHashOf(raw),
|
|
201
236
|
sourcePath: path,
|
|
202
|
-
databaseUrl
|
|
237
|
+
databaseUrl,
|
|
203
238
|
};
|
|
204
239
|
}
|
|
205
240
|
|
|
@@ -18,6 +18,32 @@ export interface PyricRuntimeChip {
|
|
|
18
18
|
dispose(): void;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
interface AiEngineDisplay {
|
|
22
|
+
primary: string;
|
|
23
|
+
subline: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function aiEngineState(): AiEngineDisplay {
|
|
27
|
+
const engine = (globalThis as { __PYRIC_AI_ENGINE__?: { kind?: string; model?: string } }).__PYRIC_AI_ENGINE__;
|
|
28
|
+
if (engine?.kind === 'gemini') {
|
|
29
|
+
return {
|
|
30
|
+
primary: 'gemini (production)',
|
|
31
|
+
subline: 'gemini-3.5-flash-lite → gemini-flash-lite-latest',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (engine?.kind === 'openai') {
|
|
35
|
+
const modelLabel = engine.model ? ` (${engine.model})` : '';
|
|
36
|
+
return {
|
|
37
|
+
primary: `openai (proxy${modelLabel})`,
|
|
38
|
+
subline: null,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
primary: 'sandbox (scripted)',
|
|
43
|
+
subline: null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
21
47
|
const styles = `
|
|
22
48
|
:host {
|
|
23
49
|
--pyric-bg: #1e1e24;
|
|
@@ -86,8 +112,11 @@ const styles = `
|
|
|
86
112
|
.icon-button[data-copy-failed] { color: var(--pyric-error); }
|
|
87
113
|
.icon { height: 15px; width: 15px; }
|
|
88
114
|
.worker-state { border-top: 1px solid var(--pyric-border-soft); color: var(--pyric-muted); font-size: 10px; justify-content: space-between; min-height: 34px; padding: 7px 12px; }
|
|
115
|
+
.worker-state-col { border-top: 1px solid var(--pyric-border-soft); color: var(--pyric-muted); font-size: 10px; display: flex; flex-direction: column; min-height: 34px; padding: 7px 12px; }
|
|
116
|
+
.worker-state-row { align-items: center; display: flex; justify-content: space-between; width: 100%; }
|
|
117
|
+
.worker-state-subline { color: #89899f; font: 9px/1.4 ui-monospace, monospace; margin-top: 4px; overflow-wrap: anywhere; text-align: right; width: 100%; }
|
|
89
118
|
.worker-state .available { color: var(--pyric-warning); }
|
|
90
|
-
.worker-state .state-label { align-items: center; display: flex; gap: 7px; }
|
|
119
|
+
.worker-state .state-label, .worker-state-col .state-label { align-items: center; display: flex; gap: 7px; white-space: nowrap; }
|
|
91
120
|
.mini-dot { background: var(--pyric-accent); border-radius: 50%; height: 6px; width: 6px; }
|
|
92
121
|
.available .mini-dot { background: var(--pyric-warning); }
|
|
93
122
|
.epochs { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -203,6 +232,7 @@ export function mountPyricRuntimeChip(options: PyricRuntimeChipOptions): PyricRu
|
|
|
203
232
|
const epochs = snapshot.updateAvailable
|
|
204
233
|
? `${snapshot.runningEpoch?.slice(0, 8) ?? 'unknown'} → ${snapshot.servedEpoch?.slice(0, 8) ?? 'unknown'}`
|
|
205
234
|
: snapshot.runningEpoch?.slice(0, 8) ?? '';
|
|
235
|
+
const aiState = aiEngineState();
|
|
206
236
|
|
|
207
237
|
view.innerHTML = `${open ? `
|
|
208
238
|
<section class="panel" role="dialog" aria-label="Pyric runtime">
|
|
@@ -214,6 +244,13 @@ export function mountPyricRuntimeChip(options: PyricRuntimeChipOptions): PyricRu
|
|
|
214
244
|
</div>
|
|
215
245
|
</header>
|
|
216
246
|
<div class="worker-state"><span class="state-label${snapshot.updateAvailable ? ' available' : ''}"><span class="mini-dot"></span>${workerLabel}</span><span class="epochs">${epochs}</span></div>
|
|
247
|
+
<div class="worker-state-col" data-ai-status>
|
|
248
|
+
<div class="worker-state-row">
|
|
249
|
+
<span class="state-label"><span class="mini-dot"></span>AI engine</span>
|
|
250
|
+
<span class="epochs">${aiState.primary}</span>
|
|
251
|
+
</div>
|
|
252
|
+
${aiState.subline ? `<div class="worker-state-subline">${aiState.subline}</div>` : ''}
|
|
253
|
+
</div>
|
|
217
254
|
<div class="errors" data-error-viewport>${renderErrors(snapshot, Boolean(clipboard))}</div>
|
|
218
255
|
<div class="actions">
|
|
219
256
|
<button class="button update" type="button" data-update-worker ${snapshot.updateAvailable ? '' : 'disabled'} aria-disabled="${snapshot.updateAvailable && !snapshot.updatingWorker ? 'false' : 'true'}">${snapshot.updatingWorker ? 'Updating…' : 'Update worker'}</button>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Stable routes and identity used by the served runtime and its UI. */
|
|
2
2
|
export const PYRIC_WORKER_URL = '/__pyric/sdk/worker.js';
|
|
3
3
|
export const PYRIC_WORKER_NAME = 'pyric-shared-worker';
|
|
4
|
-
export const PYRIC_STUDIO_URL = '/__pyric/ui/';
|
|
4
|
+
export const PYRIC_STUDIO_URL = '/__pyric/ui/studio';
|
|
5
5
|
|
|
6
6
|
export interface PyricRuntimeManifest {
|
|
7
7
|
studioUrl: string;
|
|
@@ -108,6 +108,13 @@ function isListenerPhaseErroredEvent(event: SandboxEvent): boolean {
|
|
|
108
108
|
return isListenerKind && isErroredPhase && hasErrorPayload;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
function isAiRejectedEvent(event: SandboxEvent): boolean {
|
|
112
|
+
const isServiceMutationKind = event.kind === 'service_mutation';
|
|
113
|
+
const isAiService = event.service === 'ai';
|
|
114
|
+
const isRejectedOp = 'op' in event && event.op === 'request_rejected';
|
|
115
|
+
return isServiceMutationKind && isAiService && isRejectedOp;
|
|
116
|
+
}
|
|
117
|
+
|
|
111
118
|
function sandboxError(event: SandboxEvent): PyricRuntimeError | null {
|
|
112
119
|
if (isRuntimeErrorEvent(event)) {
|
|
113
120
|
const hasPathProperty = 'path' in event && typeof event.path === 'string';
|
|
@@ -127,6 +134,23 @@ function sandboxError(event: SandboxEvent): PyricRuntimeError | null {
|
|
|
127
134
|
message: errorPayload?.message ? errorPayload.message : 'Sandbox runtime error',
|
|
128
135
|
};
|
|
129
136
|
}
|
|
137
|
+
if (isAiRejectedEvent(event)) {
|
|
138
|
+
const detail = 'detail' in event ? (event.detail as Record<string, unknown> | undefined) : undefined;
|
|
139
|
+
const code = typeof detail?.code === 'string' ? detail.code : 'AI_ERROR';
|
|
140
|
+
const message = typeof detail?.message === 'string' ? detail.message : 'AI request rejected';
|
|
141
|
+
const hasPathProperty = 'path' in event && typeof event.path === 'string';
|
|
142
|
+
const targetPath = hasPathProperty ? event.path : '(ai)';
|
|
143
|
+
return {
|
|
144
|
+
id: event.id,
|
|
145
|
+
source: 'sandbox',
|
|
146
|
+
at: event.at,
|
|
147
|
+
service: 'ai',
|
|
148
|
+
method: 'generateContent',
|
|
149
|
+
path: targetPath,
|
|
150
|
+
code,
|
|
151
|
+
message: `AI_ERROR: ${message}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
130
154
|
if (isListenerErroredEvent(event)) {
|
|
131
155
|
const errorPayload = (event as { error: { code?: string; message: string } }).error;
|
|
132
156
|
return {
|
|
@@ -35,6 +35,7 @@ export interface SandboxSessionOptions {
|
|
|
35
35
|
bridgeUrl?: () => string | null;
|
|
36
36
|
ai?: InitPayload['ai'];
|
|
37
37
|
aiProxyUpstream?: string;
|
|
38
|
+
permissive?: boolean;
|
|
38
39
|
logger?: ServeLogger;
|
|
39
40
|
activity?: (incident: ActivityIncident) => void;
|
|
40
41
|
}
|
|
@@ -97,6 +98,13 @@ export async function createSandboxSession(
|
|
|
97
98
|
databaseRules: database.rules,
|
|
98
99
|
databaseRulesHash: database.rulesHash,
|
|
99
100
|
};
|
|
101
|
+
if (!database.sourcePath) {
|
|
102
|
+
if (options.permissive) {
|
|
103
|
+
options.logger?.note(' ⓘ RTDB permissive mode active — client reads/writes are open by default.');
|
|
104
|
+
} else {
|
|
105
|
+
options.logger?.note(' ⚠ no database.rules.json found — client RTDB reads/writes default to DENY (matching production Firebase). Use --permissive for open prototyping.');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
100
108
|
const events = createEventHub();
|
|
101
109
|
const capture: CaptureStore | undefined = (options.capture ?? true)
|
|
102
110
|
? createCaptureStore(options.projectDir)
|
|
@@ -164,6 +172,7 @@ export async function createSandboxSession(
|
|
|
164
172
|
: seedUsers,
|
|
165
173
|
messaging: true,
|
|
166
174
|
ai: options.ai ?? null,
|
|
175
|
+
permissive: Boolean(options.permissive),
|
|
167
176
|
});
|
|
168
177
|
|
|
169
178
|
const summary: SandboxSessionSummary = {
|
|
@@ -221,16 +230,13 @@ export async function createSandboxSession(
|
|
|
221
230
|
}
|
|
222
231
|
};
|
|
223
232
|
const reloadDatabaseRules = async (): Promise<RulesReloadResult> => {
|
|
224
|
-
const isMissingDatabaseRules = database.sourcePath === null;
|
|
225
|
-
if (isMissingDatabaseRules) {
|
|
226
|
-
return { kind: 'not-configured' };
|
|
227
|
-
}
|
|
228
233
|
try {
|
|
229
234
|
const updated = await loadProjectDatabaseRules(options.projectDir, options.firebaseConfig);
|
|
230
235
|
const isMissingUpdatedRules = updated.rules === null || updated.rulesHash === null;
|
|
231
236
|
if (isMissingUpdatedRules) {
|
|
232
237
|
return { kind: 'not-configured' };
|
|
233
238
|
}
|
|
239
|
+
database.sourcePath = updated.sourcePath;
|
|
234
240
|
live.databaseRules = updated.rules;
|
|
235
241
|
live.databaseRulesHash = updated.rulesHash;
|
|
236
242
|
events.broadcast('rtdb-rules-update', { rules: updated.rules, rulesHash: updated.rulesHash });
|
package/src/serve/server.ts
CHANGED
|
@@ -134,6 +134,23 @@ export function isAllowedUpgrade(
|
|
|
134
134
|
);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Extract a header value case-insensitively from either Node's IncomingMessage headers
|
|
139
|
+
* or Fetch API Headers object.
|
|
140
|
+
*/
|
|
141
|
+
export function getHeader(
|
|
142
|
+
req: { headers?: Record<string, string | string[] | undefined> | Headers },
|
|
143
|
+
name: string,
|
|
144
|
+
): string | undefined {
|
|
145
|
+
const headers = req?.headers;
|
|
146
|
+
if (!headers) return undefined;
|
|
147
|
+
if (typeof (headers as Headers).get === 'function') {
|
|
148
|
+
return (headers as Headers).get(name) ?? undefined;
|
|
149
|
+
}
|
|
150
|
+
const val = (headers as Record<string, string | string[] | undefined>)[name.toLowerCase()];
|
|
151
|
+
return Array.isArray(val) ? val.join(', ') : val;
|
|
152
|
+
}
|
|
153
|
+
|
|
137
154
|
export interface ServeHandle {
|
|
138
155
|
port: number;
|
|
139
156
|
host: string;
|
|
@@ -249,7 +266,7 @@ async function handleRequest(
|
|
|
249
266
|
if (!isAllowedHost(req.headers.host, opts.host ?? 'localhost', opts.allowedHosts)) {
|
|
250
267
|
logger.note(` ✖ 403 blocked Host '${req.headers.host}' (DNS-rebinding guard)`);
|
|
251
268
|
res.writeHead(403, { 'content-type': 'text/plain' }).end(
|
|
252
|
-
`pyric
|
|
269
|
+
`pyric sandbox: refused request for Host '${req.headers.host ?? ''}'. ` +
|
|
253
270
|
'Only localhost is allowed; use --allowed-host to add one.',
|
|
254
271
|
);
|
|
255
272
|
return;
|
|
@@ -33,12 +33,22 @@ import type {
|
|
|
33
33
|
WorkspaceChange,
|
|
34
34
|
WorkspaceStore,
|
|
35
35
|
} from './store-types.js';
|
|
36
|
+
import { isAllowedHost, isAllowedOrigin, getHeader } from '../server.js';
|
|
37
|
+
import type { WriterLock } from '../writer-lock.js';
|
|
36
38
|
|
|
37
39
|
export interface StudioRouteOptions {
|
|
38
40
|
/** The single-project file tree served at `/__pyric/workspace`. */
|
|
39
41
|
workspace?: WorkspaceStore;
|
|
40
42
|
/** The project list served at `/__pyric/projects`. */
|
|
41
43
|
projects?: ProjectStore;
|
|
44
|
+
/** Per-boot session capability token required on workspace & project endpoints. */
|
|
45
|
+
sessionToken?: string;
|
|
46
|
+
/** Single-writer lock validator for file mutation & deletion endpoints. */
|
|
47
|
+
writerLock?: WriterLock;
|
|
48
|
+
/** Bound host for DNS rebinding guard. */
|
|
49
|
+
boundHost?: string;
|
|
50
|
+
/** Extra hostnames allowed for DNS rebinding/origin guard. */
|
|
51
|
+
allowedHosts?: string[];
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
/** Collect the raw request body as a UTF-8 string. */
|
|
@@ -207,6 +217,10 @@ async function handleProjects(
|
|
|
207
217
|
.end('method not allowed');
|
|
208
218
|
}
|
|
209
219
|
|
|
220
|
+
function isWorkspaceMutation(method?: string): boolean {
|
|
221
|
+
return method === 'PUT' || method === 'POST' || method === 'PATCH' || method === 'DELETE';
|
|
222
|
+
}
|
|
223
|
+
|
|
210
224
|
/**
|
|
211
225
|
* Build a namespace fragment for the Studio storage routes. Returns a handler
|
|
212
226
|
* shaped like the other `/__pyric/*` fragments: it resolves `true` when it
|
|
@@ -219,13 +233,55 @@ export function createStudioRoutes(opts: StudioRouteOptions) {
|
|
|
219
233
|
url: URL,
|
|
220
234
|
): Promise<boolean> => {
|
|
221
235
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
236
|
+
const isWorkspaceRoute = opts.workspace && url.pathname.startsWith('/__pyric/workspace');
|
|
237
|
+
const isProjectsRoute = opts.projects && url.pathname.startsWith('/__pyric/projects');
|
|
238
|
+
|
|
239
|
+
if (isWorkspaceRoute || isProjectsRoute) {
|
|
240
|
+
const hostHeader = getHeader(req, 'host');
|
|
241
|
+
const originHeader = getHeader(req, 'origin');
|
|
242
|
+
|
|
243
|
+
// 1. Host and Origin guard (Requirement 1)
|
|
244
|
+
if (!isAllowedHost(hostHeader, opts.boundHost ?? 'localhost', opts.allowedHosts)) {
|
|
245
|
+
res.writeHead(403, { 'content-type': 'text/plain' }).end('Forbidden: host not allowed');
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
if (originHeader && !isAllowedOrigin(originHeader, opts.boundHost ?? 'localhost', opts.allowedHosts)) {
|
|
249
|
+
res.writeHead(403, { 'content-type': 'text/plain' }).end('Forbidden: origin mismatch');
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 2. Per-boot session capability token guard (Requirement 2)
|
|
254
|
+
if (opts.sessionToken) {
|
|
255
|
+
const reqToken =
|
|
256
|
+
getHeader(req, 'x-pyric-session-token') ??
|
|
257
|
+
url.searchParams.get('token');
|
|
258
|
+
if (!reqToken || reqToken !== opts.sessionToken) {
|
|
259
|
+
res.writeHead(401, { 'content-type': 'text/plain' }).end('Unauthorized: invalid session capability token');
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 3. Single-writer lock guard for mutation endpoints (Requirement 4)
|
|
265
|
+
if (opts.writerLock && isWorkspaceMutation(req.method)) {
|
|
266
|
+
const writerId = getHeader(req, 'x-pyric-writer');
|
|
267
|
+
if (!writerId) {
|
|
268
|
+
res.writeHead(423, { 'content-type': 'text/plain' }).end('Locked: missing active writer lock header');
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
if (!opts.writerLock.claim(writerId, Date.now())) {
|
|
272
|
+
res.writeHead(423, { 'content-type': 'text/plain' }).end('Locked: another tab holds the writer lock');
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (isWorkspaceRoute) {
|
|
278
|
+
await handleWorkspace(opts.workspace!, req, res, url);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
if (isProjectsRoute) {
|
|
282
|
+
await handleProjects(opts.projects!, req, res, url);
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
229
285
|
}
|
|
230
286
|
} catch (e) {
|
|
231
287
|
if (!res.headersSent) sendError(res, e);
|
|
@@ -44,7 +44,18 @@ export function loadViteAiEnv(
|
|
|
44
44
|
const resolvedEnvDir = envDir === undefined
|
|
45
45
|
? resolvedRoot
|
|
46
46
|
: path.resolve(resolvedRoot, envDir);
|
|
47
|
-
|
|
47
|
+
const loaded = loadEnv(mode, resolvedEnvDir, '');
|
|
48
|
+
for (const [key, val] of Object.entries(loaded)) {
|
|
49
|
+
const isAuthKey =
|
|
50
|
+
key === 'GEMINI_API_KEY' ||
|
|
51
|
+
key === 'GOOGLE_GENAI_API_KEY' ||
|
|
52
|
+
key === 'VITE_GEMINI_API_KEY' ||
|
|
53
|
+
key === 'PYRIC_API_KEY';
|
|
54
|
+
if (isAuthKey && process.env[key] === undefined) {
|
|
55
|
+
process.env[key] = val;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return loaded;
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
/** Convert a public declarative engine into the JSON-safe worker wire shape. */
|
|
@@ -65,6 +76,21 @@ export function engineConfigToWire(engine: PyricAiEngineConfig): AiEngineConfigW
|
|
|
65
76
|
}
|
|
66
77
|
return result as AiEngineConfigWire;
|
|
67
78
|
}
|
|
79
|
+
const isGeminiEngine = engine.kind === 'gemini';
|
|
80
|
+
if (isGeminiEngine) {
|
|
81
|
+
const result: Record<string, unknown> = {
|
|
82
|
+
kind: 'gemini',
|
|
83
|
+
};
|
|
84
|
+
const hasBaseUrl = engine.baseUrl !== undefined;
|
|
85
|
+
if (hasBaseUrl) {
|
|
86
|
+
result.baseUrl = engine.baseUrl;
|
|
87
|
+
}
|
|
88
|
+
const hasApiKey = engine.apiKey !== undefined;
|
|
89
|
+
if (hasApiKey) {
|
|
90
|
+
result.apiKey = engine.apiKey;
|
|
91
|
+
}
|
|
92
|
+
return result as AiEngineConfigWire;
|
|
93
|
+
}
|
|
68
94
|
const result: Record<string, unknown> = {
|
|
69
95
|
kind: 'scripted',
|
|
70
96
|
};
|
|
@@ -123,12 +149,16 @@ export function resolveViteAiConfig(
|
|
|
123
149
|
}
|
|
124
150
|
|
|
125
151
|
let engineWire: AiEngineConfigWire | undefined = undefined;
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
152
|
+
if (isProductionMode) {
|
|
153
|
+
const apiKey = env.GEMINI_API_KEY ?? env.GOOGLE_GENAI_API_KEY ?? env.VITE_GEMINI_API_KEY;
|
|
154
|
+
engineWire = {
|
|
155
|
+
kind: 'gemini',
|
|
156
|
+
...(apiKey !== undefined && apiKey.trim() !== '' ? { apiKey: apiKey.trim() } : {}),
|
|
157
|
+
};
|
|
158
|
+
} else if (explicitEngine !== undefined) {
|
|
159
|
+
engineWire = engineConfigToWire(explicitEngine);
|
|
160
|
+
} else if (model !== undefined) {
|
|
161
|
+
engineWire = engineConfigToWire({ kind: 'openai', baseUrl: AI_PROXY_PATH, model });
|
|
132
162
|
}
|
|
133
163
|
|
|
134
164
|
let proxyUpstream: string | undefined = undefined;
|
|
@@ -86,20 +86,6 @@ export function createViteModuleSwap(
|
|
|
86
86
|
|
|
87
87
|
const match = FIREBASE_SPECIFIER.exec(args.path);
|
|
88
88
|
const subpath = match !== null && match[1] !== undefined ? match[1] : '';
|
|
89
|
-
const isProductionAiMode = aiMode() === 'production';
|
|
90
|
-
if (isProductionAiMode) {
|
|
91
|
-
const isAiSubpath = subpath === 'ai';
|
|
92
|
-
if (isAiSubpath) {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
const isAppSubpath = subpath === 'app';
|
|
96
|
-
const passthroughEntry = entries['app-ai-passthrough'];
|
|
97
|
-
const hasPassthroughEntry = passthroughEntry !== undefined;
|
|
98
|
-
const shouldUsePassthroughBridge = isAppSubpath && hasPassthroughEntry;
|
|
99
|
-
if (shouldUsePassthroughBridge) {
|
|
100
|
-
return { path: passthroughEntry };
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
89
|
|
|
104
90
|
const isServedSubpath = SERVED_FIREBASE_SUBPATHS.has(subpath);
|
|
105
91
|
if (isServedSubpath) {
|
|
@@ -173,20 +159,6 @@ export function createViteModuleSwap(
|
|
|
173
159
|
const isFirebaseSpecifier = firebaseMatch !== null;
|
|
174
160
|
if (isFirebaseSpecifier) {
|
|
175
161
|
const subpath = firebaseMatch[1] !== undefined ? firebaseMatch[1] : '';
|
|
176
|
-
const isProductionAiMode = aiMode() === 'production';
|
|
177
|
-
if (isProductionAiMode) {
|
|
178
|
-
const isAiSubpath = subpath === 'ai';
|
|
179
|
-
if (isAiSubpath) {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
const isAppSubpath = subpath === 'app';
|
|
183
|
-
const passthroughEntry = entries['app-ai-passthrough'];
|
|
184
|
-
const hasPassthroughEntry = passthroughEntry !== undefined;
|
|
185
|
-
const shouldUsePassthroughBridge = isAppSubpath && hasPassthroughEntry;
|
|
186
|
-
if (shouldUsePassthroughBridge) {
|
|
187
|
-
return passthroughEntry;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
162
|
const isServedSubpath = SERVED_FIREBASE_SUBPATHS.has(subpath);
|
|
191
163
|
if (isServedSubpath) {
|
|
192
164
|
const key = entryKey(subpath);
|