@pyric/cli 0.1.0-alpha.11 → 0.1.0-alpha.13
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/dist/cli/serve.js +4 -4
- package/dist/cli/serve.js.map +1 -1
- package/dist/conformance/.generated/can-i-use-browser.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use-browser.js +1 -1
- package/dist/conformance/.generated/can-i-use-browser.js.map +1 -1
- package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use.js +2 -2
- package/dist/conformance/.generated/can-i-use.js.map +1 -1
- package/dist/serve/bundler.d.ts +13 -8
- package/dist/serve/bundler.d.ts.map +1 -1
- package/dist/serve/bundler.js +31 -36
- package/dist/serve/bundler.js.map +1 -1
- package/dist/serve/docs-ui/docs/_rhythm/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/set-up-your-agent/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/watch-and-review/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/work-with-an-agent/index.html +1 -1
- package/dist/serve/docs-ui/docs/ai-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/api-reference/index.html +2 -2
- package/dist/serve/docs-ui/docs/api-reference.md +1 -1
- package/dist/serve/docs-ui/docs/app-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/auth-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/ai-logic/index.html +44 -22
- package/dist/serve/docs-ui/docs/build/ai-logic.md +69 -21
- package/dist/serve/docs-ui/docs/build/authentication/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-firestore/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-messaging/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-storage/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/realtime-database/index.html +1 -1
- package/dist/serve/docs-ui/docs/conformance-scores/index.html +1 -1
- package/dist/serve/docs-ui/docs/create-pyric-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/database-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/firestore-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/functions-rtdb-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/get-started/how-the-swap-works/index.html +2 -1
- package/dist/serve/docs-ui/docs/get-started/how-the-swap-works.md +2 -0
- package/dist/serve/docs-ui/docs/get-started/start-building/index.html +3 -2
- package/dist/serve/docs-ui/docs/get-started/start-building.md +3 -1
- package/dist/serve/docs-ui/docs/index.json +349 -29
- package/dist/serve/docs-ui/docs/messaging-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/observe/resolve-runtime-status/index.html +28 -0
- package/dist/serve/docs-ui/docs/observe/resolve-runtime-status.md +47 -0
- package/dist/serve/docs-ui/docs/observe/see-whats-happening/index.html +1 -1
- package/dist/serve/docs-ui/docs/observe/shape-your-data/index.html +1 -1
- package/dist/serve/docs-ui/docs/overview/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api/index.html +916 -0
- package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api.md +706 -0
- package/dist/serve/docs-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ai-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-app-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-assurance-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api/index.html +168 -76
- package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api.md +156 -73
- package/dist/serve/docs-ui/docs/pyric-cli-verify-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api/index.html +11 -8
- package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api.md +17 -4
- package/dist/serve/docs-ui/docs/pyric-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-messaging-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-rules-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api/index.html +37 -14
- package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api.md +28 -8
- package/dist/serve/docs-ui/docs/pyric-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/rules-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/audit-your-rules/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/firestore-rules-limits/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/read-a-denial/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/rules-standard-library/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/secure-it-with-rules/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/simulate-and-lint/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
- package/dist/serve/docs-ui/docs/ship/ship-to-production/index.html +1 -1
- package/dist/serve/docs-ui/docs/ship/test-in-node/index.html +1 -1
- package/dist/serve/docs-ui/docs/storage-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
- package/dist/serve/docs-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
- package/dist/serve/docs-ui/docs/tutorial/index.html +429 -0
- package/dist/serve/docs-ui/docs/tutorial.md +701 -0
- package/dist/serve/docs-ui/llms.txt +3 -1
- package/dist/serve/entries/init.d.ts.map +1 -1
- package/dist/serve/entries/init.js +3 -0
- package/dist/serve/entries/init.js.map +1 -1
- package/dist/serve/entries/runtime.d.ts.map +1 -1
- package/dist/serve/entries/runtime.js +20 -5
- package/dist/serve/entries/runtime.js.map +1 -1
- package/dist/serve/entries/worker-runtime.d.ts +2 -2
- package/dist/serve/entries/worker-runtime.d.ts.map +1 -1
- package/dist/serve/entries/worker-runtime.js +45 -10
- package/dist/serve/entries/worker-runtime.js.map +1 -1
- package/dist/serve/runtime/chip-config.d.ts +19 -0
- package/dist/serve/runtime/chip-config.d.ts.map +1 -0
- package/dist/serve/runtime/chip-config.js +19 -0
- package/dist/serve/runtime/chip-config.js.map +1 -0
- package/dist/serve/runtime/chip-install.d.ts +10 -0
- package/dist/serve/runtime/chip-install.d.ts.map +1 -0
- package/dist/serve/runtime/chip-install.js +15 -0
- package/dist/serve/runtime/chip-install.js.map +1 -0
- package/dist/serve/runtime/chip.d.ts +17 -0
- package/dist/serve/runtime/chip.d.ts.map +1 -0
- package/dist/serve/runtime/chip.js +257 -0
- package/dist/serve/runtime/chip.js.map +1 -0
- package/dist/serve/runtime/manifest.d.ts +22 -0
- package/dist/serve/runtime/manifest.d.ts.map +1 -0
- package/dist/serve/runtime/manifest.js +20 -0
- package/dist/serve/runtime/manifest.js.map +1 -0
- package/dist/serve/runtime/status.d.ts +49 -0
- package/dist/serve/runtime/status.d.ts.map +1 -0
- package/dist/serve/runtime/status.js +164 -0
- package/dist/serve/runtime/status.js.map +1 -0
- package/dist/serve/runtime/worker-connection.d.ts +4 -0
- package/dist/serve/runtime/worker-connection.d.ts.map +1 -0
- package/dist/serve/runtime/worker-connection.js +11 -0
- package/dist/serve/runtime/worker-connection.js.map +1 -0
- package/dist/serve/runtime/worker-generation.d.ts +14 -0
- package/dist/serve/runtime/worker-generation.d.ts.map +1 -0
- package/dist/serve/runtime/worker-generation.js +56 -0
- package/dist/serve/runtime/worker-generation.js.map +1 -0
- package/dist/serve/runtime/worker-replacement.d.ts +18 -0
- package/dist/serve/runtime/worker-replacement.d.ts.map +1 -0
- package/dist/serve/runtime/worker-replacement.js +29 -0
- package/dist/serve/runtime/worker-replacement.js.map +1 -0
- package/dist/serve/standalone-assets.d.ts +2 -2
- package/dist/serve/standalone-assets.d.ts.map +1 -1
- package/dist/serve/standalone-assets.js +1 -1
- package/dist/serve/standalone-assets.js.map +1 -1
- package/dist/serve/studio-ui/assets/{RulesCodeEditor-CxVYcZXw.js → RulesCodeEditor-BDlH-5lp.js} +1 -1
- package/dist/serve/studio-ui/assets/{index-cpSfgQ7H.js → index-Bf9SSE8L.js} +5 -5
- package/dist/serve/studio-ui/assets/{seed-DeuFKi4J.js → seed-D_UjxfOU.js} +1 -1
- package/dist/serve/studio-ui/index.html +1 -1
- package/dist/serve/vite-ai-config.d.ts +25 -0
- package/dist/serve/vite-ai-config.d.ts.map +1 -0
- package/dist/serve/vite-ai-config.js +45 -0
- package/dist/serve/vite-ai-config.js.map +1 -0
- package/dist/serve/vite-plugin.d.ts +17 -36
- package/dist/serve/vite-plugin.d.ts.map +1 -1
- package/dist/serve/vite-plugin.js +38 -57
- package/dist/serve/vite-plugin.js.map +1 -1
- package/dist/serve/vite-rules-source.d.ts +8 -0
- package/dist/serve/vite-rules-source.d.ts.map +1 -0
- package/dist/serve/vite-rules-source.js +23 -0
- package/dist/serve/vite-rules-source.js.map +1 -0
- package/dist/serve/vite-worker-runtime.d.ts +20 -0
- package/dist/serve/vite-worker-runtime.d.ts.map +1 -0
- package/dist/serve/vite-worker-runtime.js +25 -0
- package/dist/serve/vite-worker-runtime.js.map +1 -0
- package/dist/serve/worker/client/connection.d.ts +7 -2
- package/dist/serve/worker/client/connection.d.ts.map +1 -1
- package/dist/serve/worker/client/connection.js +13 -16
- package/dist/serve/worker/client/connection.js.map +1 -1
- package/dist/serve/worker/client/core.d.ts +7 -0
- package/dist/serve/worker/client/core.d.ts.map +1 -1
- package/dist/serve/worker/client/core.js +28 -0
- package/dist/serve/worker/client/core.js.map +1 -1
- package/dist/serve/worker/client/runtime-control.d.ts +8 -0
- package/dist/serve/worker/client/runtime-control.d.ts.map +1 -0
- package/dist/serve/worker/client/runtime-control.js +11 -0
- package/dist/serve/worker/client/runtime-control.js.map +1 -0
- package/dist/serve/worker/entry.js +44 -2
- package/dist/serve/worker/entry.js.map +1 -1
- package/dist/serve/worker/host/core.js +1 -1
- package/dist/serve/worker/host/core.js.map +1 -1
- package/dist/serve/worker/host/firestore-writes.d.ts.map +1 -1
- package/dist/serve/worker/host/firestore-writes.js +3 -2
- package/dist/serve/worker/host/firestore-writes.js.map +1 -1
- package/dist/serve/worker/host/studio.js +1 -1
- package/dist/serve/worker/host/studio.js.map +1 -1
- package/dist/serve/worker/host-context.d.ts +1 -1
- package/dist/serve/worker/host-context.d.ts.map +1 -1
- package/dist/serve/worker/index.d.ts +2 -1
- package/dist/serve/worker/index.d.ts.map +1 -1
- package/dist/serve/worker/index.js +2 -1
- package/dist/serve/worker/index.js.map +1 -1
- package/dist/serve/worker/protocol.d.ts +15 -1
- package/dist/serve/worker/protocol.d.ts.map +1 -1
- package/dist/serve/worker/protocol.js +2 -2
- package/dist/serve/worker/protocol.js.map +1 -1
- package/dist/serve/worker/retirement.d.ts +21 -0
- package/dist/serve/worker/retirement.d.ts.map +1 -0
- package/dist/serve/worker/retirement.js +97 -0
- package/dist/serve/worker/retirement.js.map +1 -0
- package/dist/serve/worker/serve-init.d.ts.map +1 -1
- package/dist/serve/worker/serve-init.js +5 -6
- package/dist/serve/worker/serve-init.js.map +1 -1
- package/dist/vite.d.ts +1 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js.map +1 -1
- package/package.json +6 -4
- package/src/assurance/.generated/conformance-verdicts.ts +1090 -0
- package/src/assurance/attachment.ts +211 -0
- package/src/assurance/browser.ts +80 -0
- package/src/assurance/campaign.ts +496 -0
- package/src/assurance/capabilities.ts +446 -0
- package/src/assurance/cases.ts +43 -0
- package/src/assurance/index.ts +75 -0
- package/src/assurance/runner.ts +885 -0
- package/src/assurance/tool-names.ts +15 -0
- package/src/assurance/tools.ts +830 -0
- package/src/assurance/types.ts +352 -0
- package/src/assurance/validation.ts +434 -0
- package/src/bridge/client/bridge.ts +550 -0
- package/src/bridge/client/dispatch.ts +137 -0
- package/src/bridge/client.ts +45 -0
- package/src/bridge/protocol.ts +351 -0
- package/src/bridge/server/audit.ts +53 -0
- package/src/bridge/server/bridge.ts +597 -0
- package/src/bridge/server/headless.ts +160 -0
- package/src/bridge/server/json-schema-to-zod.ts +108 -0
- package/src/bridge/server/local-bridge.ts +66 -0
- package/src/bridge/server/logger.ts +50 -0
- package/src/bridge/server/mcp-contract.ts +93 -0
- package/src/bridge/server/mcp.ts +129 -0
- package/src/bridge/server/peer.ts +232 -0
- package/src/bridge/server/standalone.ts +347 -0
- package/src/bridge/server/tool-metadata.ts +99 -0
- package/src/bridge/server.ts +28 -0
- package/src/cli/can-i-use.ts +50 -0
- package/src/cli/cli.test.ts +493 -0
- package/src/cli/database-rules.ts +329 -0
- package/src/cli/dev-runner.ts +282 -0
- package/src/cli/firebase-json.ts +100 -0
- package/src/cli/firestore-indexes.ts +70 -0
- package/src/cli/index.ts +425 -0
- package/src/cli/init.ts +260 -0
- package/src/cli/mcp-proxy.ts +196 -0
- package/src/cli/parse-args.ts +80 -0
- package/src/cli/rules.ts +259 -0
- package/src/cli/scope.ts +67 -0
- package/src/cli/serve.ts +996 -0
- package/src/cli/service-commands.ts +73 -0
- package/src/cli/snapshot.ts +164 -0
- package/src/cli/storage-rules.ts +172 -0
- package/src/cli/verify.ts +416 -0
- package/src/conformance/.generated/can-i-use-browser.ts +113 -0
- package/src/conformance/.generated/can-i-use.ts +117 -0
- package/src/conformance/.generated/conformance-docs.ts +14 -0
- package/src/conformance/browser.ts +24 -0
- package/src/conformance/can-i-use-tool.ts +51 -0
- package/src/conformance/can-i-use.ts +18 -0
- package/src/conformance/docs.ts +8 -0
- package/src/conformance/index.ts +20 -0
- package/src/conformance/tools.ts +16 -0
- package/src/credentials/core/memoize-ttl.ts +142 -0
- package/src/credentials/core/types.ts +10 -0
- package/src/credentials/node/from-adc.ts +96 -0
- package/src/credentials/node/from-service-account.ts +133 -0
- package/src/credentials/node/index.ts +6 -0
- package/src/discover/concurrency.ts +127 -0
- package/src/discover/crawler-adapter.ts +142 -0
- package/src/discover/crawler.ts +1127 -0
- package/src/discover/credential-free.ts +27 -0
- package/src/discover/findCollectionGroup.ts +131 -0
- package/src/discover/firestore-source.ts +59 -0
- package/src/discover/index.ts +5 -0
- package/src/discover/merge.ts +523 -0
- package/src/discover/session.ts +402 -0
- package/src/discover/tools.ts +201 -0
- package/src/discover/types.ts +187 -0
- package/src/discover/wire.ts +324 -0
- package/src/functions-rtdb/child.ts +382 -0
- package/src/functions-rtdb/delivery.ts +7 -0
- package/src/functions-rtdb/discovery.ts +136 -0
- package/src/functions-rtdb/event.ts +64 -0
- package/src/functions-rtdb/execution.ts +111 -0
- package/src/functions-rtdb/in-memory-delivery.ts +40 -0
- package/src/functions-rtdb/project.ts +74 -0
- package/src/functions-rtdb/projection.ts +104 -0
- package/src/functions-rtdb/reference-pattern.ts +20 -0
- package/src/functions-rtdb/remote-delivery.ts +23 -0
- package/src/pkg-version.ts +49 -0
- package/src/register/esm-exports.ts +63 -0
- package/src/register/hooks.ts +30 -0
- package/src/register/index.ts +150 -0
- package/src/register/mapping.ts +33 -0
- package/src/remote/index.ts +1050 -0
- package/src/rtdb/crawl-snapshot.ts +118 -0
- package/src/rtdb/inspection.ts +137 -0
- package/src/rtdb/load-rules-document.ts +56 -0
- package/src/rtdb/rules-generation-tool.ts +38 -0
- package/src/rtdb/rules-json.ts +29 -0
- package/src/serve/activity-guard.ts +29 -0
- package/src/serve/activity-route.ts +164 -0
- package/src/serve/activity-warning.ts +26 -0
- package/src/serve/bridge-mount.ts +200 -0
- package/src/serve/bundler.ts +519 -0
- package/src/serve/capture-store.ts +65 -0
- package/src/serve/discovery.ts +199 -0
- package/src/serve/entries/ai.ts +292 -0
- package/src/serve/entries/app-backend.ts +4 -0
- package/src/serve/entries/app-client.ts +26 -0
- package/src/serve/entries/app-session-store.ts +47 -0
- package/src/serve/entries/app.ts +11 -0
- package/src/serve/entries/auth-helper-core.ts +181 -0
- package/src/serve/entries/auth-helper-dom.ts +134 -0
- package/src/serve/entries/auth-helper-runtime.ts +20 -0
- package/src/serve/entries/auth.ts +255 -0
- package/src/serve/entries/bridge-url.ts +31 -0
- package/src/serve/entries/database.ts +129 -0
- package/src/serve/entries/firestore.ts +232 -0
- package/src/serve/entries/init.ts +49 -0
- package/src/serve/entries/keepalive.ts +46 -0
- package/src/serve/entries/messaging-sw.ts +75 -0
- package/src/serve/entries/messaging.ts +96 -0
- package/src/serve/entries/runtime.ts +556 -0
- package/src/serve/entries/session-store.ts +97 -0
- package/src/serve/entries/storage.ts +92 -0
- package/src/serve/entries/tab-sync-wiring.ts +274 -0
- package/src/serve/entries/worker-runtime.ts +135 -0
- package/src/serve/init-payload.ts +42 -0
- package/src/serve/namespace.ts +675 -0
- package/src/serve/open-browser.ts +68 -0
- package/src/serve/rules.ts +272 -0
- package/src/serve/runtime/chip-config.ts +30 -0
- package/src/serve/runtime/chip-install.ts +27 -0
- package/src/serve/runtime/chip.ts +282 -0
- package/src/serve/runtime/manifest.ts +37 -0
- package/src/serve/runtime/status.ts +216 -0
- package/src/serve/runtime/worker-connection.ts +14 -0
- package/src/serve/runtime/worker-generation.ts +75 -0
- package/src/serve/runtime/worker-replacement.ts +42 -0
- package/src/serve/sandbox-marker.ts +31 -0
- package/src/serve/server.ts +428 -0
- package/src/serve/standalone-assets.ts +180 -0
- package/src/serve/state-store.ts +166 -0
- package/src/serve/studio/disk-project-store.ts +185 -0
- package/src/serve/studio/disk-workspace.ts +162 -0
- package/src/serve/studio/index.ts +20 -0
- package/src/serve/studio/routes.ts +237 -0
- package/src/serve/studio/store-types.ts +48 -0
- package/src/serve/studio/studio-storage.test.ts +289 -0
- package/src/serve/vite-ai-config.ts +77 -0
- package/src/serve/vite-plugin.ts +1075 -0
- package/src/serve/vite-rules-source.ts +28 -0
- package/src/serve/vite-worker-runtime.ts +51 -0
- package/src/serve/worker/activity-bootstrap.ts +24 -0
- package/src/serve/worker/client/admin-firestore.ts +38 -0
- package/src/serve/worker/client/ai.ts +124 -0
- package/src/serve/worker/client/auth.ts +489 -0
- package/src/serve/worker/client/connection.ts +227 -0
- package/src/serve/worker/client/core.ts +384 -0
- package/src/serve/worker/client/disconnect.ts +45 -0
- package/src/serve/worker/client/firestore-reads.ts +181 -0
- package/src/serve/worker/client/firestore-refs.ts +226 -0
- package/src/serve/worker/client/firestore-writes.ts +232 -0
- package/src/serve/worker/client/handles.ts +91 -0
- package/src/serve/worker/client/messaging.ts +119 -0
- package/src/serve/worker/client/presence.ts +174 -0
- package/src/serve/worker/client/rtdb.ts +311 -0
- package/src/serve/worker/client/rules.ts +63 -0
- package/src/serve/worker/client/runtime-control.ts +22 -0
- package/src/serve/worker/client/service-worker-connection.ts +59 -0
- package/src/serve/worker/client/snapshots.ts +86 -0
- package/src/serve/worker/client/storage.ts +195 -0
- package/src/serve/worker/client/studio.ts +88 -0
- package/src/serve/worker/client.ts +57 -0
- package/src/serve/worker/durable-persistence.ts +137 -0
- package/src/serve/worker/entry.ts +223 -0
- package/src/serve/worker/host/admin-firestore.ts +88 -0
- package/src/serve/worker/host/connection.ts +179 -0
- package/src/serve/worker/host/core.ts +387 -0
- package/src/serve/worker/host/dispatch.ts +316 -0
- package/src/serve/worker/host/firestore-reads.ts +117 -0
- package/src/serve/worker/host/firestore-writes.ts +456 -0
- package/src/serve/worker/host/presence.ts +312 -0
- package/src/serve/worker/host/rtdb.ts +136 -0
- package/src/serve/worker/host/rules.ts +128 -0
- package/src/serve/worker/host/storage.ts +312 -0
- package/src/serve/worker/host/studio.ts +75 -0
- package/src/serve/worker/host/subscriptions.ts +238 -0
- package/src/serve/worker/host-ai.ts +165 -0
- package/src/serve/worker/host-auth.ts +469 -0
- package/src/serve/worker/host-context.ts +273 -0
- package/src/serve/worker/host-events.ts +94 -0
- package/src/serve/worker/host-messaging.ts +240 -0
- package/src/serve/worker/host.ts +47 -0
- package/src/serve/worker/index.ts +196 -0
- package/src/serve/worker/presence-timing.ts +15 -0
- package/src/serve/worker/protocol.ts +1124 -0
- package/src/serve/worker/retirement.ts +125 -0
- package/src/serve/worker/serve-init.ts +605 -0
- package/src/serve/worker/service-worker-channel.ts +33 -0
- package/src/serve/worker/service-worker-relay.ts +86 -0
- package/src/serve/writer-lock.ts +53 -0
- package/src/verify/cases.ts +233 -0
- package/src/verify/fixture.ts +258 -0
- package/src/verify/index.ts +519 -0
- package/src/verify/tools.ts +108 -0
- package/src/version/compat-target.ts +17 -0
- package/src/vite.ts +18 -0
- package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api/index.html +0 -31
- package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api.md +0 -29
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/__pyric/` reserved namespace — pyric's analog of firebase serve's
|
|
3
|
+
* `/__/firebase/` namespace:
|
|
4
|
+
*
|
|
5
|
+
* /__pyric/sdk/<file>.js the bundled SDK files (import-map targets +
|
|
6
|
+
* the injected init script + shared chunks)
|
|
7
|
+
* /__pyric/init.json the page init payload (rules, bridge URL) —
|
|
8
|
+
* fetched by the runtime chunk at module init
|
|
9
|
+
*
|
|
10
|
+
* Bridge routes (`/__pyric/mcp`, `/__pyric/sandbox`) mount here in P2.
|
|
11
|
+
*/
|
|
12
|
+
import { randomBytes } from 'node:crypto';
|
|
13
|
+
import { existsSync, statSync } from 'node:fs';
|
|
14
|
+
import { basename, extname, join } from 'node:path';
|
|
15
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
16
|
+
import type { ActivityIncident } from 'pyric/firestore/internal';
|
|
17
|
+
import { collectBody } from '../bridge/server/peer.js';
|
|
18
|
+
import { StateFileError, type StateSection, type StateStore } from './state-store.js';
|
|
19
|
+
import { createWriterLock, type WriterLock } from './writer-lock.js';
|
|
20
|
+
import { createStudioRoutes, type StudioRouteOptions } from './studio/index.js';
|
|
21
|
+
import { contentTypeFor, pipeFileToResponse, resolveStaticFile, type ServeLogger } from './server.js';
|
|
22
|
+
import { SANDBOX_BUILD_MARKER } from './sandbox-marker.js';
|
|
23
|
+
import type { InitPayload } from './init-payload.js';
|
|
24
|
+
import { handleActivity } from './activity-route.js';
|
|
25
|
+
export type { InitPayload } from './init-payload.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Server-sent-events hub for `/__pyric/events` — the hot-reload channel
|
|
29
|
+
* (P3). The runtime opens an EventSource; the rules watcher broadcasts
|
|
30
|
+
* `rules-changed` and connected pages re-deploy without a refresh.
|
|
31
|
+
*/
|
|
32
|
+
export interface ServeEventHub {
|
|
33
|
+
/** Mounted inside the namespace handler. */
|
|
34
|
+
handle(req: IncomingMessage, res: ServerResponse): void;
|
|
35
|
+
broadcast(event: string, data: unknown): void;
|
|
36
|
+
clientCount(): number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createEventHub(): ServeEventHub {
|
|
40
|
+
const clients = new Set<ServerResponse>();
|
|
41
|
+
return {
|
|
42
|
+
handle(req, res) {
|
|
43
|
+
res.writeHead(200, {
|
|
44
|
+
'content-type': 'text/event-stream',
|
|
45
|
+
'cache-control': 'no-store',
|
|
46
|
+
connection: 'keep-alive',
|
|
47
|
+
});
|
|
48
|
+
res.write(': connected\n\n');
|
|
49
|
+
clients.add(res);
|
|
50
|
+
req.on('close', () => clients.delete(res));
|
|
51
|
+
// A dying SSE socket must never surface an unhandled 'error' event
|
|
52
|
+
// (which would kill the whole serve process) — drop the client instead.
|
|
53
|
+
res.on('error', () => clients.delete(res));
|
|
54
|
+
},
|
|
55
|
+
broadcast(event, data) {
|
|
56
|
+
const frame = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
57
|
+
for (const res of clients) {
|
|
58
|
+
try {
|
|
59
|
+
res.write(frame);
|
|
60
|
+
} catch {
|
|
61
|
+
clients.delete(res); // half-closed socket in the close race window
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
clientCount: () => clients.size,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface NamespaceOptions {
|
|
70
|
+
/** The bundle output dir (`BundleResult.outDir`). */
|
|
71
|
+
sdkDir: string;
|
|
72
|
+
/** Producer for `/__pyric/init.json` — a function so hot-reload serves
|
|
73
|
+
* the latest rules without server restart. */
|
|
74
|
+
initPayload: () => InitPayload;
|
|
75
|
+
/** Hot-reload channel; serves `/__pyric/events` when present. */
|
|
76
|
+
events?: ServeEventHub;
|
|
77
|
+
/** Default-on Firebase Activity Guard sink. The served worker posts bounded,
|
|
78
|
+
* deduplicated incidents here so the host can surface them in its terminal. */
|
|
79
|
+
activity?: (incident: ActivityIncident) => void;
|
|
80
|
+
/** `--persist`: mounts GET/POST /__pyric/state (the state channel). */
|
|
81
|
+
state?: StateStore;
|
|
82
|
+
/** `--capture`: mounts GET/POST /__pyric/capture. POST — the page/worker
|
|
83
|
+
* pushes its session fixture here; the handler writes it verbatim to
|
|
84
|
+
* `.pyric/last-session.json` for `pyric verify` to replay. GET — returns the
|
|
85
|
+
* current fixture JSON (200) or 404 when nothing is captured yet, so the
|
|
86
|
+
* served worker can re-hydrate its event history on boot after a death. */
|
|
87
|
+
capture?: { write(json: string): void; read(): string | null };
|
|
88
|
+
/** `--ui` (Pyric Studio): mounts `/__pyric/workspace` + `/__pyric/projects`
|
|
89
|
+
* (disk-backed `WorkspaceStore`/`ProjectStore`, plus the SSE watch stream)
|
|
90
|
+
* that `@pyric/studio`'s `local` mode talks to. */
|
|
91
|
+
studio?: StudioRouteOptions;
|
|
92
|
+
/** `--ui` (Pyric Studio): the dir of the built Studio app, served under
|
|
93
|
+
* `/__pyric/ui/`. Resolved by file path in the CLI (@pyric/cli never
|
|
94
|
+
* imports `@pyric/studio`). Absent when `--ui` is off or the build is
|
|
95
|
+
* missing. */
|
|
96
|
+
studioUiDir?: string;
|
|
97
|
+
/** `--ui` (Pyric Studio): the built docs site (site-docs), served so the
|
|
98
|
+
* Studio Docs tab has local docs without the hosted site. Built with base
|
|
99
|
+
* `/__pyric/ui/`, so its output straddles two subtrees under the mount:
|
|
100
|
+
* the pages/twins/index.json live under `/__pyric/ui/docs/`, but the shared
|
|
101
|
+
* assets live at `/__pyric/ui/_astro/` (Astro's asset dir, at the base
|
|
102
|
+
* root — NOT under `docs/`). Both are served from this one dir. */
|
|
103
|
+
docsUiDir?: string;
|
|
104
|
+
/** The OpenAI-compatible upstream `/__pyric/ai-proxy` forwards to
|
|
105
|
+
* (pyric/ai — cdd-deltas #98.2). Falls back to the
|
|
106
|
+
* `PYRIC_AI_PROXY_UPSTREAM` env var, then `http://localhost:11434/v1`
|
|
107
|
+
* (local Ollama). Always mounted — the route only touches the network
|
|
108
|
+
* when a request arrives. */
|
|
109
|
+
aiProxyUpstream?: string;
|
|
110
|
+
/** Where hot-reload/diagnostic lines print (rules reload, denial relay).
|
|
111
|
+
* Absent ⇒ diagnostics are dropped (a caller that wires no logger opts out
|
|
112
|
+
* silently rather than falling back to raw `console`). */
|
|
113
|
+
logger?: ServeLogger;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── AI proxy (`/__pyric/ai-proxy` — pyric/ai, cdd-deltas #98.2) ───────────
|
|
117
|
+
|
|
118
|
+
/** Default upstream: local Ollama's OpenAI-compatible endpoint. */
|
|
119
|
+
export const AI_PROXY_DEFAULT_UPSTREAM = 'http://localhost:11434/v1';
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Request headers that must NOT be forwarded upstream: origin-sensitive
|
|
123
|
+
* browser context (`origin`/`referer`/`cookie` — the upstream is a different
|
|
124
|
+
* origin and must never see the page's), hop-by-hop headers, and the two the
|
|
125
|
+
* proxy re-derives (`host`, `content-length`). `accept-encoding` is dropped
|
|
126
|
+
* so fetch negotiates its own compression and the streamed body needs no
|
|
127
|
+
* length fixups.
|
|
128
|
+
*/
|
|
129
|
+
const AI_PROXY_STRIPPED_HEADERS = new Set([
|
|
130
|
+
'host',
|
|
131
|
+
'origin',
|
|
132
|
+
'referer',
|
|
133
|
+
'cookie',
|
|
134
|
+
'connection',
|
|
135
|
+
'content-length',
|
|
136
|
+
'accept-encoding',
|
|
137
|
+
'keep-alive',
|
|
138
|
+
'proxy-authorization',
|
|
139
|
+
'te',
|
|
140
|
+
'trailer',
|
|
141
|
+
'transfer-encoding',
|
|
142
|
+
'upgrade',
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Handle `POST /__pyric/ai-proxy/<suffix>` — a same-origin passthrough to the
|
|
147
|
+
* configured OpenAI-compatible upstream, so the browser openai engine
|
|
148
|
+
* (running in the served page or the SharedWorker host) reaches a localhost
|
|
149
|
+
* upstream like Ollama with ZERO CORS setup (no `OLLAMA_ORIGINS`, ever).
|
|
150
|
+
*
|
|
151
|
+
* Behavior:
|
|
152
|
+
* - POST only (the OpenAI chat-completions surface is POST); 405 otherwise.
|
|
153
|
+
* - the path suffix + query ride through verbatim
|
|
154
|
+
* (`/__pyric/ai-proxy/chat/completions` → `<upstream>/chat/completions`).
|
|
155
|
+
* - the request body is forwarded verbatim; headers minus the
|
|
156
|
+
* origin-sensitive/hop-by-hop set above (`authorization` DOES forward —
|
|
157
|
+
* upstreams may require a key).
|
|
158
|
+
* - the response body is STREAMED through chunk-by-chunk — SSE passthrough
|
|
159
|
+
* must never buffer, or `stream: true` completions would arrive all at
|
|
160
|
+
* once at the end.
|
|
161
|
+
* - an unreachable upstream answers 502 with a plain-text explanation.
|
|
162
|
+
*/
|
|
163
|
+
async function handleAiProxy(
|
|
164
|
+
configuredUpstream: string | undefined,
|
|
165
|
+
req: IncomingMessage,
|
|
166
|
+
res: ServerResponse,
|
|
167
|
+
url: URL,
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
if (req.method !== 'POST') {
|
|
170
|
+
res.writeHead(405, { allow: 'POST' }).end('method not allowed');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const upstreamBase = (
|
|
174
|
+
configuredUpstream ??
|
|
175
|
+
process.env.PYRIC_AI_PROXY_UPSTREAM ??
|
|
176
|
+
AI_PROXY_DEFAULT_UPSTREAM
|
|
177
|
+
).replace(/\/$/, '');
|
|
178
|
+
const suffix = url.pathname.slice('/__pyric/ai-proxy'.length);
|
|
179
|
+
const target = `${upstreamBase}${suffix}${url.search}`;
|
|
180
|
+
|
|
181
|
+
// Buffer the REQUEST body (small JSON payloads); the RESPONSE streams.
|
|
182
|
+
const raw = await new Promise<Buffer>((resolve, reject) => {
|
|
183
|
+
const chunks: Buffer[] = [];
|
|
184
|
+
req.on('data', (c: Buffer) => chunks.push(c));
|
|
185
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
186
|
+
req.on('error', reject);
|
|
187
|
+
});
|
|
188
|
+
// Copy into a plain-ArrayBuffer view — fetch's BodyInit typing rejects
|
|
189
|
+
// Buffer's ArrayBufferLike backing.
|
|
190
|
+
const body = new Uint8Array(raw.byteLength);
|
|
191
|
+
body.set(raw);
|
|
192
|
+
|
|
193
|
+
const headers: Record<string, string> = {};
|
|
194
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
195
|
+
if (value === undefined) continue;
|
|
196
|
+
if (AI_PROXY_STRIPPED_HEADERS.has(key.toLowerCase())) continue;
|
|
197
|
+
headers[key] = Array.isArray(value) ? value.join(', ') : value;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let upstream: Response;
|
|
201
|
+
try {
|
|
202
|
+
upstream = await fetch(target, { method: 'POST', headers, body });
|
|
203
|
+
} catch (e) {
|
|
204
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
205
|
+
res.end(
|
|
206
|
+
`pyric dev ai-proxy: upstream ${target} unreachable: ` +
|
|
207
|
+
`${e instanceof Error ? e.message : String(e)}\n` +
|
|
208
|
+
'Set PYRIC_AI_PROXY_UPSTREAM to an OpenAI-compatible base URL ' +
|
|
209
|
+
`(default ${AI_PROXY_DEFAULT_UPSTREAM}).`,
|
|
210
|
+
);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const responseHeaders: Record<string, string> = { 'cache-control': 'no-store' };
|
|
215
|
+
const contentType = upstream.headers.get('content-type');
|
|
216
|
+
if (contentType) responseHeaders['content-type'] = contentType;
|
|
217
|
+
res.writeHead(upstream.status, responseHeaders);
|
|
218
|
+
if (!upstream.body) {
|
|
219
|
+
res.end();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
(res as ServerResponse & { flushHeaders?: () => void }).flushHeaders?.();
|
|
223
|
+
|
|
224
|
+
// Chunk-by-chunk passthrough. A dropped client cancels the upstream read
|
|
225
|
+
// so an abandoned SSE stream doesn't keep the upstream generating.
|
|
226
|
+
const reader = upstream.body.getReader();
|
|
227
|
+
res.on('close', () => {
|
|
228
|
+
void reader.cancel().catch(() => {});
|
|
229
|
+
});
|
|
230
|
+
try {
|
|
231
|
+
for (;;) {
|
|
232
|
+
const { done, value } = await reader.read();
|
|
233
|
+
if (done) break;
|
|
234
|
+
res.write(value);
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// Upstream or client dropped mid-stream — nothing to salvage.
|
|
238
|
+
}
|
|
239
|
+
res.end();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The page's persistence backend speaks this route:
|
|
243
|
+
* GET /__pyric/state → whole envelope (404 when absent)
|
|
244
|
+
* GET /__pyric/state?section=firestore → that section (404 when null)
|
|
245
|
+
* POST /__pyric/state?section=... → write the section (204; 423 if
|
|
246
|
+
* another tab holds the writer lock)
|
|
247
|
+
* DELETE /__pyric/state → release the writer lock (beacon)
|
|
248
|
+
* Writes carry `x-pyric-writer: <id>`; the first writer wins, others get 423
|
|
249
|
+
* and drop to read-only (pre-mortem #3). The firestore body is the sandbox
|
|
250
|
+
* persistence controller's own blob — stored verbatim, never interpreted. */
|
|
251
|
+
async function handleState(
|
|
252
|
+
state: StateStore,
|
|
253
|
+
lock: WriterLock,
|
|
254
|
+
req: IncomingMessage,
|
|
255
|
+
res: ServerResponse,
|
|
256
|
+
url: URL,
|
|
257
|
+
): Promise<void> {
|
|
258
|
+
const section = url.searchParams.get('section');
|
|
259
|
+
const writerId = (req.headers['x-pyric-writer'] as string | undefined) ?? '';
|
|
260
|
+
try {
|
|
261
|
+
if (req.method === 'GET') {
|
|
262
|
+
const value = section ? state.readSection(section as StateSection) : state.load();
|
|
263
|
+
if (value === null) {
|
|
264
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end('null');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
res.writeHead(200, {
|
|
268
|
+
'content-type': 'application/json; charset=utf-8',
|
|
269
|
+
'cache-control': 'no-store',
|
|
270
|
+
// Lets `pyric snapshot` detect it scanned into a NEIGHBOR project's
|
|
271
|
+
// serve (pre-mortem #4) and warn instead of promoting the wrong DB.
|
|
272
|
+
'x-pyric-project-dir': state.projectDir,
|
|
273
|
+
});
|
|
274
|
+
res.end(JSON.stringify(value));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (req.method === 'DELETE') {
|
|
278
|
+
if (writerId) lock.release(writerId);
|
|
279
|
+
res.writeHead(204).end();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (req.method === 'PUT') {
|
|
283
|
+
// Lock heartbeat — refresh/claim WITHOUT writing state. 423 if another
|
|
284
|
+
// live tab holds it.
|
|
285
|
+
res.writeHead(lock.claim(writerId || 'anon', Date.now()) ? 204 : 423).end();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (req.method === 'POST') {
|
|
289
|
+
if (section !== 'firestore' && section !== 'auth') {
|
|
290
|
+
res.writeHead(400).end('section must be firestore|auth');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// Single-writer: the first page to flush claims the lock; a different
|
|
294
|
+
// live page is refused (423) so it can't erase the writer's world.
|
|
295
|
+
if (!lock.claim(writerId || 'anon', Date.now())) {
|
|
296
|
+
res.writeHead(423, { 'content-type': 'text/plain' }).end(
|
|
297
|
+
`another tab holds the persist writer lock (held by ${lock.holder()})`,
|
|
298
|
+
);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const body = await collectBody(req);
|
|
302
|
+
state.writeSection(section, body);
|
|
303
|
+
res.writeHead(204).end();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
res.writeHead(405, { allow: 'GET, POST, DELETE' }).end('method not allowed');
|
|
307
|
+
} catch (e) {
|
|
308
|
+
// StateFileError (corrupt/version) or bad body — surface, don't clobber.
|
|
309
|
+
res.writeHead(e instanceof StateFileError ? 409 : 400, { 'content-type': 'text/plain' });
|
|
310
|
+
res.end(e instanceof Error ? e.message : String(e));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Handle POST /__pyric/capture — the page pushes its full session fixture
|
|
316
|
+
* (JSON-serialized history + snapshot + rules) whenever the sandbox changes.
|
|
317
|
+
* The server writes it verbatim to `.pyric/last-session.json` so
|
|
318
|
+
* `pyric verify` can pick it up without any extra arguments.
|
|
319
|
+
*
|
|
320
|
+
* GET returns the current fixture JSON verbatim (200), or 404 when nothing
|
|
321
|
+
* has been captured yet. The served SharedWorker fetches this on boot to
|
|
322
|
+
* re-hydrate its in-memory event history after a worker death (Traffic /
|
|
323
|
+
* activity / metrics survive the refresh even though the worker restarted).
|
|
324
|
+
* Cheap + read-only.
|
|
325
|
+
*
|
|
326
|
+
* POST fails fast rather than silently swallowing write errors so the
|
|
327
|
+
* developer knows the capture is broken.
|
|
328
|
+
*
|
|
329
|
+
* We collect the RAW request body as a string rather than using
|
|
330
|
+
* `collectBody` (which parses JSON). The capture must be stored verbatim
|
|
331
|
+
* — re-serializing a parsed object would round-trip through JS values and
|
|
332
|
+
* could alter key order or lose whitespace, making the file differ from
|
|
333
|
+
* what the page intended to write.
|
|
334
|
+
*/
|
|
335
|
+
async function handleCapture(
|
|
336
|
+
capture: { write(json: string): void; read(): string | null },
|
|
337
|
+
req: IncomingMessage,
|
|
338
|
+
res: ServerResponse,
|
|
339
|
+
): Promise<void> {
|
|
340
|
+
if (req.method === 'GET') {
|
|
341
|
+
const body = capture.read();
|
|
342
|
+
if (body === null) {
|
|
343
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end('null');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
res.writeHead(200, {
|
|
347
|
+
'content-type': 'application/json; charset=utf-8',
|
|
348
|
+
'cache-control': 'no-store',
|
|
349
|
+
});
|
|
350
|
+
res.end(body);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (req.method !== 'POST') {
|
|
354
|
+
res.writeHead(405, { allow: 'GET, POST' }).end('method not allowed');
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
const body = await new Promise<string>((resolve, reject) => {
|
|
359
|
+
let raw = '';
|
|
360
|
+
req.setEncoding('utf8');
|
|
361
|
+
req.on('data', (chunk: string) => { raw += chunk; });
|
|
362
|
+
req.on('end', () => resolve(raw));
|
|
363
|
+
req.on('error', reject);
|
|
364
|
+
});
|
|
365
|
+
capture.write(body);
|
|
366
|
+
res.writeHead(204).end();
|
|
367
|
+
} catch (e) {
|
|
368
|
+
res.writeHead(400, { 'content-type': 'text/plain' });
|
|
369
|
+
res.end(e instanceof Error ? e.message : String(e));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ─── Denial relay (`POST /__pyric/denials` — headless dev visibility) ─────
|
|
374
|
+
//
|
|
375
|
+
// The worker client (`serve/worker/client/core.ts`) fire-and-forget POSTs
|
|
376
|
+
// here whenever it reconstructs an error carrying `denialContext` (a rules
|
|
377
|
+
// denial) — from a one-shot rejection or an unobserved listener error alike.
|
|
378
|
+
// An agent driving `pyric dev` headlessly has no browser console to watch;
|
|
379
|
+
// this is that visibility, printed to the terminal instead.
|
|
380
|
+
//
|
|
381
|
+
// A denied listener re-fires on every auth/rules change, so printing is
|
|
382
|
+
// throttled per (path, message) pair — at most one line per window, and
|
|
383
|
+
// silent (no output at all) on suppression rather than a "suppressed N more"
|
|
384
|
+
// line, to keep this a diagnostics side channel, not its own log noise.
|
|
385
|
+
|
|
386
|
+
const DENIAL_THROTTLE_MS = 5_000;
|
|
387
|
+
|
|
388
|
+
/** Loosely-typed mirror of the client's relay payload — JSON from the served
|
|
389
|
+
* page, read defensively since nothing here enforces the wire shape. */
|
|
390
|
+
interface DenialRelayPayload {
|
|
391
|
+
kind?: unknown;
|
|
392
|
+
code?: unknown;
|
|
393
|
+
message?: unknown;
|
|
394
|
+
/** Sibling of `denialContext` on the sandbox error — query-proof denials
|
|
395
|
+
* attach the suggested `where()` fix there; the client forwards it. */
|
|
396
|
+
remediation?: unknown;
|
|
397
|
+
denialContext?: {
|
|
398
|
+
auth?: { uid?: string } | null;
|
|
399
|
+
request?: { method?: string; path?: string };
|
|
400
|
+
/** Fallback position, in case a denial path nests it in the context. */
|
|
401
|
+
remediation?: string;
|
|
402
|
+
} | null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Per-dev-server-instance throttle: at most one printed line per (path,
|
|
406
|
+
* message) pair within {@link DENIAL_THROTTLE_MS}. Exported for unit tests. */
|
|
407
|
+
export function createDenialThrottle(windowMs: number = DENIAL_THROTTLE_MS): {
|
|
408
|
+
shouldPrint(key: string, now: number): boolean;
|
|
409
|
+
} {
|
|
410
|
+
const lastPrinted = new Map<string, number>();
|
|
411
|
+
return {
|
|
412
|
+
shouldPrint(key, now) {
|
|
413
|
+
const last = lastPrinted.get(key);
|
|
414
|
+
if (last !== undefined && now - last < windowMs) return false;
|
|
415
|
+
lastPrinted.set(key, now);
|
|
416
|
+
return true;
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
type DenialThrottle = ReturnType<typeof createDenialThrottle>;
|
|
421
|
+
|
|
422
|
+
/** Format a relayed denial into the compact terminal block: the message,
|
|
423
|
+
* then (when present) the request method/path, the auth uid, and any
|
|
424
|
+
* remediation guidance. A few lines, never a JSON dump. */
|
|
425
|
+
export function formatDenialBlock(payload: DenialRelayPayload): string {
|
|
426
|
+
const message = typeof payload.message === 'string' ? payload.message : 'permission denied';
|
|
427
|
+
const lines = [` ⚠ [pyric] denied: ${message}`];
|
|
428
|
+
const ctx = payload.denialContext;
|
|
429
|
+
const request = ctx?.request;
|
|
430
|
+
if (request?.method && request?.path) {
|
|
431
|
+
lines.push(` ${request.method} ${request.path}`);
|
|
432
|
+
}
|
|
433
|
+
const uid = ctx?.auth?.uid;
|
|
434
|
+
lines.push(` auth: ${uid ?? 'anonymous'}`);
|
|
435
|
+
const remediation = typeof payload.remediation === 'string' ? payload.remediation : ctx?.remediation;
|
|
436
|
+
if (remediation) lines.push(` ${remediation}`);
|
|
437
|
+
return lines.join('\n');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Handle `POST /__pyric/denials`. Always 204s (best-effort diagnostics —
|
|
441
|
+
* a malformed body or a caller with no logger wired must never fail the
|
|
442
|
+
* page that reported the denial). */
|
|
443
|
+
async function handleDenials(
|
|
444
|
+
throttle: DenialThrottle,
|
|
445
|
+
logger: ServeLogger | undefined,
|
|
446
|
+
req: IncomingMessage,
|
|
447
|
+
res: ServerResponse,
|
|
448
|
+
): Promise<void> {
|
|
449
|
+
if (req.method !== 'POST') {
|
|
450
|
+
res.writeHead(405, { allow: 'POST' }).end('method not allowed');
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
const payload = (await collectBody(req)) as DenialRelayPayload;
|
|
455
|
+
const message = typeof payload.message === 'string' ? payload.message : 'permission denied';
|
|
456
|
+
const path = payload.denialContext?.request?.path ?? '';
|
|
457
|
+
const key = `${path}${message}`;
|
|
458
|
+
if (logger && throttle.shouldPrint(key, Date.now())) {
|
|
459
|
+
logger.note(formatDenialBlock(payload));
|
|
460
|
+
}
|
|
461
|
+
} catch {
|
|
462
|
+
/* malformed body — drop it; this is a diagnostics side channel */
|
|
463
|
+
}
|
|
464
|
+
res.writeHead(204).end();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function createPyricNamespace(opts: NamespaceOptions) {
|
|
468
|
+
const writerLock = createWriterLock();
|
|
469
|
+
const studioRoutes = opts.studio ? createStudioRoutes(opts.studio) : null;
|
|
470
|
+
const denialThrottle = createDenialThrottle();
|
|
471
|
+
// Issued once per server boot. The outer static/Vite host guard protects
|
|
472
|
+
// init.json before this capability is disclosed to the served runtime.
|
|
473
|
+
const activityToken = opts.activity ? randomBytes(24).toString('base64url') : undefined;
|
|
474
|
+
return (req: IncomingMessage, res: ServerResponse, url: URL): boolean | Promise<boolean> => {
|
|
475
|
+
if (
|
|
476
|
+
studioRoutes &&
|
|
477
|
+
(url.pathname.startsWith('/__pyric/workspace') ||
|
|
478
|
+
url.pathname.startsWith('/__pyric/projects'))
|
|
479
|
+
) {
|
|
480
|
+
return studioRoutes(req, res, url);
|
|
481
|
+
}
|
|
482
|
+
if (opts.state && url.pathname === '/__pyric/state') {
|
|
483
|
+
return handleState(opts.state!, writerLock, req, res, url).then(() => true);
|
|
484
|
+
}
|
|
485
|
+
if (opts.capture && url.pathname === '/__pyric/capture') {
|
|
486
|
+
return handleCapture(opts.capture, req, res).then(() => true);
|
|
487
|
+
}
|
|
488
|
+
if (opts.activity && url.pathname === '/__pyric/activity') {
|
|
489
|
+
return handleActivity(opts.activity, req, res, activityToken!).then(() => true);
|
|
490
|
+
}
|
|
491
|
+
if (opts.events && url.pathname === '/__pyric/events') {
|
|
492
|
+
opts.events.handle(req, res);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
if (url.pathname === '/__pyric/ai-proxy' || url.pathname.startsWith('/__pyric/ai-proxy/')) {
|
|
496
|
+
return handleAiProxy(opts.aiProxyUpstream, req, res, url).then(() => true);
|
|
497
|
+
}
|
|
498
|
+
if (url.pathname === '/__pyric/denials') {
|
|
499
|
+
return handleDenials(denialThrottle, opts.logger, req, res).then(() => true);
|
|
500
|
+
}
|
|
501
|
+
if (url.pathname === '/__pyric/init.json') {
|
|
502
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
503
|
+
res.end(JSON.stringify({ ...opts.initPayload(), ...(activityToken ? { activityToken } : {}) }));
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
if (url.pathname.startsWith('/__pyric/sdk/')) {
|
|
507
|
+
// basename() flattens any traversal attempt — the sdk dir is flat.
|
|
508
|
+
const file = join(opts.sdkDir, basename(url.pathname));
|
|
509
|
+
if (!existsSync(file)) {
|
|
510
|
+
res.writeHead(404).end('not found');
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
const type = file.endsWith('.map') ? 'application/json' : 'text/javascript; charset=utf-8';
|
|
514
|
+
// Immutable-friendly: bundle filenames are content-hashed chunks or
|
|
515
|
+
// cache-keyed outputs; still no-store in dev for simplicity.
|
|
516
|
+
res.writeHead(200, { 'content-type': type, 'cache-control': 'no-store' });
|
|
517
|
+
pipeFileToResponse(file, res);
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
// Embedded docs site (site-docs). MUST run BEFORE the general
|
|
521
|
+
// `/__pyric/ui/` studio handler below: that handler's SPA fallback answers
|
|
522
|
+
// every miss with Studio's index.html, which would swallow docs pages and
|
|
523
|
+
// (worse) return HTML for a missing docs asset. Built with base
|
|
524
|
+
// `/__pyric/ui/`, the docs output lives in TWO subtrees under the mount —
|
|
525
|
+
// pages/twins/index.json under `/__pyric/ui/docs/`, and shared assets at
|
|
526
|
+
// `/__pyric/ui/_astro/` (Astro's asset dir sits at the base root, not under
|
|
527
|
+
// `docs/`). We claim both; `_astro` is Astro-specific and never collides
|
|
528
|
+
// with Studio (Vite emits `/__pyric/ui/assets/`). No SPA fallback here — a
|
|
529
|
+
// genuinely missing docs page 404s (a broken doc link must fail loudly, not
|
|
530
|
+
// masquerade as another page). Directory-format pages (`<slug>/index.html`)
|
|
531
|
+
// and `bunx serve`-style extensionless→trailing-slash redirects are handled
|
|
532
|
+
// by resolveStaticFile + the directory redirect below.
|
|
533
|
+
if (
|
|
534
|
+
opts.docsUiDir &&
|
|
535
|
+
(url.pathname === '/__pyric/ui/docs' ||
|
|
536
|
+
url.pathname.startsWith('/__pyric/ui/docs/') ||
|
|
537
|
+
url.pathname.startsWith('/__pyric/ui/_astro/'))
|
|
538
|
+
) {
|
|
539
|
+
const rel = url.pathname.slice('/__pyric/ui'.length) || '/';
|
|
540
|
+
// `bunx serve` parity: an extensionless path that names a real directory
|
|
541
|
+
// (e.g. `/__pyric/ui/docs` or `/__pyric/ui/docs/<slug>`) redirects to the
|
|
542
|
+
// trailing-slash form so the directory's index.html loads with correct
|
|
543
|
+
// relative-URL resolution.
|
|
544
|
+
if (!rel.endsWith('/') && !extname(rel)) {
|
|
545
|
+
const dir = join(opts.docsUiDir, decodeURIComponent(rel));
|
|
546
|
+
if (existsSync(dir) && statSync(dir).isDirectory()) {
|
|
547
|
+
res.writeHead(301, { location: `${url.pathname}/` }).end();
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const file = resolveStaticFile(opts.docsUiDir, rel);
|
|
552
|
+
if (!file) {
|
|
553
|
+
res.writeHead(404).end('not found');
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
res.writeHead(200, { 'content-type': contentTypeFor(file), 'cache-control': 'no-store' });
|
|
557
|
+
pipeFileToResponse(file, res);
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
if (
|
|
561
|
+
opts.studioUiDir &&
|
|
562
|
+
(url.pathname === '/__pyric/ui' || url.pathname.startsWith('/__pyric/ui/'))
|
|
563
|
+
) {
|
|
564
|
+
// The built Pyric Studio app. Served verbatim (NOT through
|
|
565
|
+
// injectServeTags: that import-map/init injection is for sandbox pages,
|
|
566
|
+
// not Studio). Studio uses History-API routing under this mount, so any
|
|
567
|
+
// path that doesn't resolve to a real file falls back to index.html —
|
|
568
|
+
// INCLUDING paths with dots (deep links like /storage/uploads/logo.png).
|
|
569
|
+
// Only misses under Vite's content-hashed asset dir stay hard 404s, so a
|
|
570
|
+
// broken script/style URL fails loudly instead of returning HTML.
|
|
571
|
+
if (url.pathname === '/__pyric/ui') {
|
|
572
|
+
res.writeHead(301, { location: '/__pyric/ui/' }).end();
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
const rel = url.pathname.slice('/__pyric/ui'.length) || '/';
|
|
576
|
+
let file = resolveStaticFile(opts.studioUiDir, rel);
|
|
577
|
+
if (!file && !rel.startsWith('/assets/')) {
|
|
578
|
+
file = resolveStaticFile(opts.studioUiDir, '/index.html');
|
|
579
|
+
}
|
|
580
|
+
if (!file) {
|
|
581
|
+
res.writeHead(404).end('not found');
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
res.writeHead(200, { 'content-type': contentTypeFor(file), 'cache-control': 'no-store' });
|
|
585
|
+
pipeFileToResponse(file, res);
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
return false; // unknown /__pyric/* → caller 404s
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ─── HTML injection ───────────────────────────────────────────────────
|
|
593
|
+
|
|
594
|
+
/** The import-map targets. Spec → served URL. */
|
|
595
|
+
export function sdkImportMap(): Record<string, string> {
|
|
596
|
+
return {
|
|
597
|
+
'firebase/ai': '/__pyric/sdk/ai.js',
|
|
598
|
+
'firebase/app': '/__pyric/sdk/app.js',
|
|
599
|
+
'firebase/auth': '/__pyric/sdk/auth.js',
|
|
600
|
+
'firebase/database': '/__pyric/sdk/database.js',
|
|
601
|
+
'firebase/firestore': '/__pyric/sdk/firestore.js',
|
|
602
|
+
'firebase/messaging': '/__pyric/sdk/messaging.js',
|
|
603
|
+
'firebase/messaging/sw': '/__pyric/sdk/messaging-sw.js',
|
|
604
|
+
'firebase/storage': '/__pyric/sdk/storage.js',
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Inject the import map + init script into an HTML document. The import map
|
|
610
|
+
* MUST precede any module script that imports a mapped specifier, so both
|
|
611
|
+
* tags go at the very start of `<head>` (fallbacks: after `<html>`, else
|
|
612
|
+
* prepended). Idempotent — a page already carrying the marker is untouched
|
|
613
|
+
* (matters when transformHtml runs over an SPA fallback repeatedly).
|
|
614
|
+
*/
|
|
615
|
+
export function injectServeTags(
|
|
616
|
+
html: string,
|
|
617
|
+
importMap: Record<string, string> = sdkImportMap(),
|
|
618
|
+
workerVersion?: string,
|
|
619
|
+
forceInPage = false,
|
|
620
|
+
): string {
|
|
621
|
+
const MARKER = 'data-pyric-serve';
|
|
622
|
+
if (html.includes(MARKER)) return html;
|
|
623
|
+
// A pyric SANDBOX BUILD (`vite build` under the pyricSandbox plugin's sandbox
|
|
624
|
+
// mode) already BUNDLES its own runtime + init chunk — injecting the import
|
|
625
|
+
// map + /__pyric/sdk/init.js on top would boot a SECOND runtime instance on
|
|
626
|
+
// the page (two banners, two bridge peer registrations, races between them).
|
|
627
|
+
// The bundle owns the sandbox for a marked page: it fetches
|
|
628
|
+
// /__pyric/init.json itself (worker path: the SharedWorker does; in-page
|
|
629
|
+
// path: the runtime does) and owns rules hot-reload the same way. The ONLY
|
|
630
|
+
// serve-time contribution left is the worker-version staleness stamp, which
|
|
631
|
+
// the bundled runtime reads from this meta to warn about a stale
|
|
632
|
+
// still-running SharedWorker.
|
|
633
|
+
if (html.includes(SANDBOX_BUILD_MARKER)) {
|
|
634
|
+
if (!workerVersion || html.includes('pyric-worker-v')) return html;
|
|
635
|
+
const meta = `<meta name="pyric-worker-v" content="${workerVersion}" ${MARKER}>`;
|
|
636
|
+
const headTag = html.match(/<head[^>]*>/i);
|
|
637
|
+
if (headTag && headTag.index !== undefined) {
|
|
638
|
+
const at = headTag.index + headTag[0].length;
|
|
639
|
+
return html.slice(0, at) + meta + html.slice(at);
|
|
640
|
+
}
|
|
641
|
+
return meta + html;
|
|
642
|
+
}
|
|
643
|
+
// Stamp the worker's content hash so the page can DETECT staleness: a live
|
|
644
|
+
// SharedWorker survives serve restarts + reloads and can't hot-update, so it
|
|
645
|
+
// keeps running old code until every tab of the origin closes. The worker
|
|
646
|
+
// name is intentionally STABLE (one backend shared by all tabs), so rather
|
|
647
|
+
// than split tabs across versions, `runtime.ts` compares this served hash to
|
|
648
|
+
// the running worker's baked hash and warns the user to close all tabs.
|
|
649
|
+
const versionMeta = workerVersion
|
|
650
|
+
? `<meta name="pyric-worker-v" content="${workerVersion}" ${MARKER}>`
|
|
651
|
+
: '';
|
|
652
|
+
// `--bridge`: the MCP bridge peers with the IN-PAGE sandbox, so force the page
|
|
653
|
+
// off the default SharedWorker path. Otherwise the agent drives an empty
|
|
654
|
+
// in-page sandbox while the app's data lives in the worker. Set before any app
|
|
655
|
+
// code runs; mirrors the Vite plugin's transformIndexHtml.
|
|
656
|
+
const forceTag = forceInPage
|
|
657
|
+
? `<script ${MARKER}>globalThis.__PYRIC_FORCE_INPAGE__=true;</script>`
|
|
658
|
+
: '';
|
|
659
|
+
const tags =
|
|
660
|
+
versionMeta +
|
|
661
|
+
forceTag +
|
|
662
|
+
`<script type="importmap" ${MARKER}>${JSON.stringify({ imports: importMap })}</script>` +
|
|
663
|
+
`<script type="module" src="/__pyric/sdk/init.js" ${MARKER}></script>`;
|
|
664
|
+
const head = html.match(/<head[^>]*>/i);
|
|
665
|
+
if (head && head.index !== undefined) {
|
|
666
|
+
const at = head.index + head[0].length;
|
|
667
|
+
return html.slice(0, at) + tags + html.slice(at);
|
|
668
|
+
}
|
|
669
|
+
const htmlTag = html.match(/<html[^>]*>/i);
|
|
670
|
+
if (htmlTag && htmlTag.index !== undefined) {
|
|
671
|
+
const at = htmlTag.index + htmlTag[0].length;
|
|
672
|
+
return html.slice(0, at) + tags + html.slice(at);
|
|
673
|
+
}
|
|
674
|
+
return tags + html;
|
|
675
|
+
}
|