@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,605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-side serve init (Phase 3c.B) — apply `pyric dev`'s init payload
|
|
3
|
+
* INSIDE the SharedWorker.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS
|
|
6
|
+
* ---------------
|
|
7
|
+
* On the in-page path, `entries/runtime.ts` is the whole backend: it fetches
|
|
8
|
+
* `/__pyric/init.json` and deploys rules, seeds users + docs, and (when
|
|
9
|
+
* `--capture` is on, which is the default) pushes the session fixture to
|
|
10
|
+
* `/__pyric/capture` so `pyric verify` can replay. When `pyric dev` routes
|
|
11
|
+
* `firebase/*` to the SharedWorker instead (the default-on flip, 3c.E), the
|
|
12
|
+
* sandbox lives in the worker — so the worker must subsume that same init, or
|
|
13
|
+
* those features silently no-op on every modern browser.
|
|
14
|
+
*
|
|
15
|
+
* This module is the worker's mirror of `runtime.ts`'s init block, factored so
|
|
16
|
+
* it is unit-testable WITHOUT a browser: it takes a real `HostCtx` (sandbox +
|
|
17
|
+
* db) and an injected `fetch`, exactly like `host.ts`. `entry.ts` wires the
|
|
18
|
+
* real `fetch` on first connect.
|
|
19
|
+
*
|
|
20
|
+
* SCOPE (3c.B): rules → authUsers → seed docs → capture. The `--persist`
|
|
21
|
+
* server-file tier and the `seedState` ephemeral-fixture restore are state-blob
|
|
22
|
+
* concerns handled in 3c.D (the worker already attaches its own IDB durable
|
|
23
|
+
* store; persist adds the committable server-file tier on top).
|
|
24
|
+
*
|
|
25
|
+
* CAPTURE FIDELITY: the worker's sandbox accumulates EVERY tab's events, so a
|
|
26
|
+
* single capture reflects the unified backend — strictly better for the verify
|
|
27
|
+
* loop than the per-tab in-page capture it replaces.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { getFirestore } from 'pyric/firestore';
|
|
31
|
+
import { seedDocuments, setRules, snapshotDocuments } from 'pyric/sandbox/firestore';
|
|
32
|
+
import { getDatabase, sandbox as rtdbSandbox } from 'pyric/database';
|
|
33
|
+
import { getAuth, sandbox as authOps, type SeedUser } from 'pyric/auth';
|
|
34
|
+
import { getStorageSandbox } from 'pyric/storage';
|
|
35
|
+
import type { PersistenceBackend } from 'pyric/sandbox';
|
|
36
|
+
import { initializeSandbox } from 'pyric/sandbox';
|
|
37
|
+
import {
|
|
38
|
+
primeEventHistory,
|
|
39
|
+
} from 'pyric/sandbox/internal';
|
|
40
|
+
import type { InitPayload } from '../namespace.js';
|
|
41
|
+
import { setupFirebaseActivityGuard } from './activity-bootstrap.js';
|
|
42
|
+
import { createWorkerDurableBackend, setupServerAuthFlush } from './durable-persistence.js';
|
|
43
|
+
import { ensureAuth, getOrCreateInstanceId, type HostCtx } from './host.js';
|
|
44
|
+
import { buildVerifyFixture, type PyricVerifyFixture } from '../../verify/fixture.js';
|
|
45
|
+
|
|
46
|
+
/** Injected environment — `fetch` is the only ambient the worker init needs
|
|
47
|
+
* (capture POSTs through it). Injectable so tests drive it with a stub. */
|
|
48
|
+
export interface ServeInitEnv {
|
|
49
|
+
fetch: typeof fetch;
|
|
50
|
+
/** Capture debounce window (ms). Default 400 — matches `runtime.ts`. Tests
|
|
51
|
+
* pass a small value to keep the round-trip fast. */
|
|
52
|
+
captureDebounceMs?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Minimal EventSource surface the worker hot-reload needs. The real browser
|
|
56
|
+
* `EventSource` satisfies it; tests pass a fake. */
|
|
57
|
+
export interface EventSourceLike {
|
|
58
|
+
addEventListener(type: string, listener: (ev: { data: string }) => void): void;
|
|
59
|
+
close(): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Re-deploy rules to the worker's sandbox when `pyric dev` hot-reloads
|
|
64
|
+
* `firestore.rules`. On the worker path the SharedWorker owns ONE
|
|
65
|
+
* `/__pyric/events` connection for the whole origin — so tabs open ZERO SSE
|
|
66
|
+
* connections (avoiding Chrome's ~6-per-origin HTTP/1.1 cap, which multi-tab
|
|
67
|
+
* pages otherwise hit) AND a single rules-change deploys once, not once per
|
|
68
|
+
* tab. The `EventSource` is injected so this is testable without a browser.
|
|
69
|
+
*
|
|
70
|
+
* Returns a teardown that closes the stream.
|
|
71
|
+
*/
|
|
72
|
+
export function setupWorkerHotReload(
|
|
73
|
+
ctx: HostCtx,
|
|
74
|
+
makeEventSource: (url: string) => EventSourceLike,
|
|
75
|
+
): () => void {
|
|
76
|
+
const events = makeEventSource('/__pyric/events');
|
|
77
|
+
events.addEventListener('rules-changed', (ev) => {
|
|
78
|
+
try {
|
|
79
|
+
const { rules } = JSON.parse(ev.data) as { rules: string; rulesHash?: string };
|
|
80
|
+
const lint = setRules(ctx.sandbox, rules);
|
|
81
|
+
if (lint.parseError) throw new Error(JSON.stringify(lint.parseError));
|
|
82
|
+
} catch (err) {
|
|
83
|
+
// eslint-disable-next-line no-console
|
|
84
|
+
console.error('[pyric worker] rules hot-reload failed:', err instanceof Error ? err.message : String(err));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return () => events.close();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** What `applyServeInit` did — surfaced for diagnostics + tests. `dispose`
|
|
91
|
+
* stops the capture event subscription (worker teardown / re-init). */
|
|
92
|
+
export interface ServeInitResult {
|
|
93
|
+
rulesDeployed: boolean;
|
|
94
|
+
/** Whether storage rules were deployed at boot. Storage rules are
|
|
95
|
+
* honored only on the FIRST `pyric/storage` call per Sandbox
|
|
96
|
+
* (see `getStorageSandbox`'s `StorageOptions.rules` doc) — so unlike
|
|
97
|
+
* `rulesDeployed`/database rules this never flips true→false→true
|
|
98
|
+
* across a session; it is decided once, here, before any storage op
|
|
99
|
+
* runs. */
|
|
100
|
+
storageRulesDeployed: boolean;
|
|
101
|
+
seededDocs: number;
|
|
102
|
+
seededUsers: number;
|
|
103
|
+
captureEnabled: boolean;
|
|
104
|
+
/** Whether the init payload enabled the Messaging host capability. */
|
|
105
|
+
messagingEnabled: boolean;
|
|
106
|
+
/** Parse error from a malformed ruleset (defensive — the server lints first);
|
|
107
|
+
* the sandbox keeps its default rules rather than bricking. */
|
|
108
|
+
rulesParseError: string | null;
|
|
109
|
+
/** Set when a `--seed` fixture (docs and/or authUsers) was withheld because
|
|
110
|
+
* the sandbox already held restored/lived data — see the "seed applies
|
|
111
|
+
* only into an empty home" guard below. */
|
|
112
|
+
seedSkipped: 'existing-data' | null;
|
|
113
|
+
dispose(): void;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A seed fixture applies ONLY into an empty home. `buildWorkerCtx` calls
|
|
118
|
+
* `sandbox.enablePersistence` (restoring from IDB — and, with `--persist`,
|
|
119
|
+
* priming from the committable server file — see `createWorkerDurableBackend`)
|
|
120
|
+
* BEFORE `applyServeInit` runs, so by the time this check runs any restored
|
|
121
|
+
* data is already visible on `ctx`. If the sandbox holds ANY Firestore
|
|
122
|
+
* document or auth user at this point, that's lived/restored state, not a
|
|
123
|
+
* blank slate — a `--seed` fixture must never stomp it. This is the fix for
|
|
124
|
+
* the worst persistence bug: without it, `--seed` (map form) re-applies on
|
|
125
|
+
* EVERY boot because default (no `--persist`) mode never has a state.json
|
|
126
|
+
* file to gate on, silently reverting whatever IndexedDB persistence just
|
|
127
|
+
* restored.
|
|
128
|
+
*/
|
|
129
|
+
function sandboxHasExistingData(ctx: HostCtx): boolean {
|
|
130
|
+
const docs = snapshotDocuments(ctx.sandbox);
|
|
131
|
+
if (Object.keys(docs).length > 0) return true;
|
|
132
|
+
const auth = ensureAuth(ctx);
|
|
133
|
+
return authOps.exportUsers(auth).length > 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Apply the init payload to the worker's sandbox.
|
|
138
|
+
*
|
|
139
|
+
* Ordering mirrors `runtime.ts`: rules first (so the first write is governed),
|
|
140
|
+
* then authUsers (so restored docs' owner uids resolve in rules), then seed
|
|
141
|
+
* docs, then capture wiring. Synchronous except for capture's fire-and-forget
|
|
142
|
+
* POSTs — the caller (`entry.ts`) does not need to await anything here.
|
|
143
|
+
*/
|
|
144
|
+
export function applyServeInit(
|
|
145
|
+
ctx: HostCtx,
|
|
146
|
+
payload: InitPayload,
|
|
147
|
+
env: ServeInitEnv,
|
|
148
|
+
): ServeInitResult {
|
|
149
|
+
const result: ServeInitResult = {
|
|
150
|
+
rulesDeployed: false,
|
|
151
|
+
storageRulesDeployed: false,
|
|
152
|
+
seededDocs: 0,
|
|
153
|
+
seededUsers: 0,
|
|
154
|
+
captureEnabled: false,
|
|
155
|
+
messagingEnabled: false,
|
|
156
|
+
rulesParseError: null,
|
|
157
|
+
seedSkipped: null,
|
|
158
|
+
dispose: () => {},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// 0. Messaging host capability. Serve producers enable it as part of the
|
|
162
|
+
// canonical SDK swap; a worker without an init payload stays disabled.
|
|
163
|
+
if (payload.messaging === true) {
|
|
164
|
+
ctx.messagingEnabled = true;
|
|
165
|
+
result.messagingEnabled = true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 1. Rules — deploy the project's ruleset, replacing the permissive starter
|
|
169
|
+
// `entry.ts` deployed at bootstrap. A parse error is defensive (the server
|
|
170
|
+
// lints before serving): keep the default rules, surface loudly.
|
|
171
|
+
if (payload.rules) {
|
|
172
|
+
const lint = setRules(ctx.sandbox, payload.rules);
|
|
173
|
+
if (lint.parseError) {
|
|
174
|
+
result.rulesParseError = JSON.stringify(lint.parseError);
|
|
175
|
+
console.error(
|
|
176
|
+
'[pyric worker] firestore.rules failed to parse — running WITHOUT your rules:',
|
|
177
|
+
result.rulesParseError,
|
|
178
|
+
);
|
|
179
|
+
} else {
|
|
180
|
+
result.rulesDeployed = true;
|
|
181
|
+
// Record the deployed source on ctx (mirrors the database branch below)
|
|
182
|
+
// so diagnostics report it AND the `resetAll` op can re-deploy it —
|
|
183
|
+
// `sandbox.resetAll()` swaps the env, which wipes env-owned Firestore
|
|
184
|
+
// rules; a data reset must not silently de-govern writes.
|
|
185
|
+
ctx.activeRules ??= {};
|
|
186
|
+
ctx.activeRules.firestore = {
|
|
187
|
+
source: payload.rules,
|
|
188
|
+
updatedAt: Date.now(),
|
|
189
|
+
status: 'active',
|
|
190
|
+
messages: [],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (payload.databaseRules) {
|
|
195
|
+
const rtdb = ctx.rtdb ??= getDatabase(ctx.sandbox);
|
|
196
|
+
rtdbSandbox.setRules(rtdb, payload.databaseRules);
|
|
197
|
+
ctx.activeRules ??= {};
|
|
198
|
+
ctx.activeRules.database = {
|
|
199
|
+
source: payload.databaseRules,
|
|
200
|
+
updatedAt: Date.now(),
|
|
201
|
+
status: 'active',
|
|
202
|
+
messages: [],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 1b. Storage rules — deployed ONCE, here, before any storage op can run.
|
|
207
|
+
// `pyric/storage`'s `getStorageSandbox` only honors a `rules` option on
|
|
208
|
+
// the FIRST call per Sandbox (later differing rules throw — a
|
|
209
|
+
// deliberate silent-rules-wipe guard, see StorageOptions.rules). This
|
|
210
|
+
// call IS that first call: `ensureStorage`/`lensStorage` in host.ts
|
|
211
|
+
// always open storage with no `rules` option, so whichever call opens
|
|
212
|
+
// the service first wins — making this the sanctioned place to
|
|
213
|
+
// configure it. `payload.storageRules` is null when the project has no
|
|
214
|
+
// storage.rules, matching the same open-by-default posture as no
|
|
215
|
+
// firestore.rules / no database.rules.
|
|
216
|
+
// The open ALSO claims the project-scoped IDB name
|
|
217
|
+
// (`pyric-storage:<projectKey>`, issue #359) — which is why it now runs
|
|
218
|
+
// unconditionally: a lazy first open from `ensureStorage`/`lensStorage`
|
|
219
|
+
// would land on the legacy shared database.
|
|
220
|
+
getStorageSandbox(ctx.sandbox, {
|
|
221
|
+
...(payload.storageRules ? { rules: payload.storageRules } : {}),
|
|
222
|
+
...(payload.projectKey ? { projectId: payload.projectKey } : {}),
|
|
223
|
+
});
|
|
224
|
+
if (payload.storageRules) {
|
|
225
|
+
result.storageRulesDeployed = true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// A seed fixture applies only into an empty home — checked ONCE, before
|
|
229
|
+
// either seed step, so step 2's own writes can't make step 3's check look
|
|
230
|
+
// non-empty (see sandboxHasExistingData).
|
|
231
|
+
const hasExistingData =
|
|
232
|
+
((payload.seed && Object.keys(payload.seed).length > 0) ||
|
|
233
|
+
(payload.authUsers && payload.authUsers.length > 0)) &&
|
|
234
|
+
sandboxHasExistingData(ctx);
|
|
235
|
+
if (hasExistingData) {
|
|
236
|
+
result.seedSkipped = 'existing-data';
|
|
237
|
+
console.info(
|
|
238
|
+
'[pyric worker] --seed skipped: the sandbox already has restored data (persisted state or ' +
|
|
239
|
+
'an earlier session in this browser) — the fixture would have overwritten it. Use ' +
|
|
240
|
+
'`--persist --fresh` to discard existing state and re-seed from scratch.',
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 2. Auth users — seed BEFORE docs so any owner-uid the rules reference
|
|
245
|
+
// resolves, and before session restore so there is a DB to restore into.
|
|
246
|
+
if (!hasExistingData && payload.authUsers && payload.authUsers.length > 0) {
|
|
247
|
+
const auth = ensureAuth(ctx);
|
|
248
|
+
authOps.seedUsers(auth, payload.authUsers as unknown as ReadonlyArray<SeedUser>);
|
|
249
|
+
result.seededUsers = payload.authUsers.length;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 3. Seed docs — admin-style fixture load (bypasses rules).
|
|
253
|
+
if (!hasExistingData && payload.seed && Object.keys(payload.seed).length > 0) {
|
|
254
|
+
seedDocuments(ctx.sandbox, payload.seed);
|
|
255
|
+
result.seededDocs = Object.keys(payload.seed).length;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 4. Capture — the write side of the `pyric verify` loop. On every sandbox
|
|
259
|
+
// event, debounce-POST the full session fixture (rules + history + state)
|
|
260
|
+
// to `/__pyric/capture`. The server writes it verbatim to
|
|
261
|
+
// `.pyric/last-session.json`. Independent of --persist.
|
|
262
|
+
if (payload.capture) {
|
|
263
|
+
const debounceMs = payload.capture ? (env.captureDebounceMs ?? 400) : 0;
|
|
264
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
265
|
+
|
|
266
|
+
const flush = async (): Promise<void> => {
|
|
267
|
+
const rtdb = ctx.rtdb ??= getDatabase(ctx.sandbox);
|
|
268
|
+
const rtdbState =
|
|
269
|
+
payload.databaseRules || ctx.sandbox.history().some((event) => event.service === 'rtdb')
|
|
270
|
+
? rtdbSandbox.snapshotState(rtdb)
|
|
271
|
+
: undefined;
|
|
272
|
+
const auth = ensureAuth(ctx);
|
|
273
|
+
const body = JSON.stringify(buildVerifyFixture({
|
|
274
|
+
sandbox: ctx.sandbox,
|
|
275
|
+
firestoreRules: payload.rules,
|
|
276
|
+
rtdbRules: payload.databaseRules ?? null,
|
|
277
|
+
rtdbState,
|
|
278
|
+
rtdbDatabaseUrl: payload.databaseUrl ?? null,
|
|
279
|
+
authState: {
|
|
280
|
+
users: authOps.exportUsers(auth),
|
|
281
|
+
currentUser: ctx.sandbox.currentUser,
|
|
282
|
+
},
|
|
283
|
+
// Stamp WHO produced this capture so a booting worker only re-hydrates
|
|
284
|
+
// its OWN session (see hydrateEventHistory's identity guard).
|
|
285
|
+
capturedBy: ctx.instanceId,
|
|
286
|
+
}));
|
|
287
|
+
// Relative URL resolves against the worker script's origin (same origin
|
|
288
|
+
// as the page). Fire-and-forget — capture failures never break ops.
|
|
289
|
+
await env.fetch('/__pyric/capture', {
|
|
290
|
+
method: 'POST',
|
|
291
|
+
headers: { 'content-type': 'application/json' },
|
|
292
|
+
body,
|
|
293
|
+
})
|
|
294
|
+
.catch(() => {});
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const unsub = ctx.sandbox.onEvent(() => {
|
|
298
|
+
if (timer) clearTimeout(timer);
|
|
299
|
+
timer = setTimeout(() => { void flush(); }, debounceMs);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
result.captureEnabled = true;
|
|
303
|
+
// Immediate-flush seam for the `resetAll` op (issue #359 extension):
|
|
304
|
+
// reset clears `sandbox.history()`, and the SERVER-persisted capture
|
|
305
|
+
// (`.pyric/last-session.json`) must follow NOW — inside the debounce
|
|
306
|
+
// window a dying worker leaves the wiped session's events on disk, and
|
|
307
|
+
// the next boot's `hydrateEventHistory` would prime them straight back
|
|
308
|
+
// into Traffic. Bypasses the debounce; cancels any pending flush (it
|
|
309
|
+
// would only re-write the same post-reset history).
|
|
310
|
+
ctx.captureFlush = async (): Promise<void> => {
|
|
311
|
+
if (timer) {
|
|
312
|
+
clearTimeout(timer);
|
|
313
|
+
timer = null;
|
|
314
|
+
}
|
|
315
|
+
await flush();
|
|
316
|
+
};
|
|
317
|
+
result.dispose = (): void => {
|
|
318
|
+
if (timer) clearTimeout(timer);
|
|
319
|
+
unsub();
|
|
320
|
+
ctx.captureFlush = undefined;
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ─── Event-history hydration: survive worker death (served mode) ─────────────
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Cap on the number of events primed on boot. A long-lived session can
|
|
331
|
+
* accumulate a large capture; a fresh worker only needs enough recent history
|
|
332
|
+
* for Traffic / activity / metrics to feel continuous, so we prime the most
|
|
333
|
+
* recent N and drop older events rather than balloon worker memory with the
|
|
334
|
+
* whole log. The full log still lives on disk for `pyric verify`.
|
|
335
|
+
*/
|
|
336
|
+
export const MAX_PRIMED_EVENTS = 2000;
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Re-hydrate the worker's in-memory event history from the served capture
|
|
340
|
+
* file on boot — the fix for "worker death zeroes Traffic / activity / metrics
|
|
341
|
+
* even though the data restored from IDB".
|
|
342
|
+
*
|
|
343
|
+
* The SERVER already holds the complete event log (`.pyric/last-session.json`,
|
|
344
|
+
* written continuously by the capture flush). On a fresh worker we GET it back
|
|
345
|
+
* and REPLAY `events[]` into the sandbox's `history()` via the
|
|
346
|
+
* {@link primeEventHistory} seam — append-only, NO re-execution of ops and NO
|
|
347
|
+
* dispatch to live `onEvent` subscribers. Studio subscribes AFTER boot and
|
|
348
|
+
* reads history-first, so append-only-no-dispatch gives every consumer
|
|
349
|
+
* continuity for free with zero Studio changes.
|
|
350
|
+
*
|
|
351
|
+
* GUARD RAILS:
|
|
352
|
+
* - Only primes when history is EMPTY (the {@link primeEventHistory} seam
|
|
353
|
+
* enforces this) — a warm sandbox is never disturbed and primed events can
|
|
354
|
+
* never interleave after live boot events. Call this BEFORE applyServeInit.
|
|
355
|
+
* - Caps at {@link MAX_PRIMED_EVENTS} (most recent) so a huge capture can't
|
|
356
|
+
* balloon worker memory.
|
|
357
|
+
* - IDENTITY: skips when the capture was produced by a DIFFERENT instance
|
|
358
|
+
* (`capturedBy !== ctx.instanceId`) — another browser profile sharing one
|
|
359
|
+
* `pyric dev` shouldn't see someone else's session as its own. A capture
|
|
360
|
+
* with no `capturedBy` (older / standalone) primes best-effort.
|
|
361
|
+
* - Skips cleanly when the endpoint 404s (capture off / nothing captured) or
|
|
362
|
+
* the fetch throws (standalone worker, no `pyric dev` behind it).
|
|
363
|
+
*
|
|
364
|
+
* FRESHNESS: the capture lags the last pre-death moments by up to the debounce
|
|
365
|
+
* window (~400ms), so the final events before a worker death may be missing.
|
|
366
|
+
* That is acceptable — the data itself is durable via IDB; this only restores
|
|
367
|
+
* the activity RECORD, and near-perfect is enough for Traffic/feed continuity.
|
|
368
|
+
*
|
|
369
|
+
* Returns the number of events primed (0 when skipped).
|
|
370
|
+
*/
|
|
371
|
+
export async function hydrateEventHistory(
|
|
372
|
+
ctx: HostCtx,
|
|
373
|
+
env: ServeInitEnv,
|
|
374
|
+
): Promise<number> {
|
|
375
|
+
let res: Response;
|
|
376
|
+
try {
|
|
377
|
+
res = await env.fetch('/__pyric/capture');
|
|
378
|
+
} catch {
|
|
379
|
+
return 0; // standalone / no capture endpoint.
|
|
380
|
+
}
|
|
381
|
+
if (res.status !== 200) return 0; // 404 → capture off or nothing captured.
|
|
382
|
+
|
|
383
|
+
let fixture: PyricVerifyFixture;
|
|
384
|
+
try {
|
|
385
|
+
fixture = JSON.parse(await res.text()) as PyricVerifyFixture;
|
|
386
|
+
} catch {
|
|
387
|
+
return 0; // corrupt capture — never brick boot over the activity log.
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const events = fixture.events;
|
|
391
|
+
if (!Array.isArray(events) || events.length === 0) return 0;
|
|
392
|
+
|
|
393
|
+
// Identity: don't show a neighbor profile's session as ours.
|
|
394
|
+
if (fixture.capturedBy && fixture.capturedBy !== ctx.instanceId) return 0;
|
|
395
|
+
|
|
396
|
+
const capped =
|
|
397
|
+
events.length > MAX_PRIMED_EVENTS ? events.slice(-MAX_PRIMED_EVENTS) : events;
|
|
398
|
+
return primeEventHistory(ctx.sandbox, capped);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ─── Worker boot: build the ONE shared HostCtx ──────────────────────────────
|
|
402
|
+
|
|
403
|
+
/** Legacy shared persistence key — also the IndexedDB database name. Stable
|
|
404
|
+
* across reloads (never derived from the page path). Used ONLY when no
|
|
405
|
+
* project identity exists (older servers without `InitPayload.projectKey`,
|
|
406
|
+
* standalone workers) — see {@link workerPersistenceKey}. */
|
|
407
|
+
export const WORKER_PERSISTENCE_KEY = 'pyric-shared-worker';
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Resolve the worker's persistence key (= IndexedDB database name) for a
|
|
411
|
+
* project identity: `pyric-shared-worker:<projectKey>`, or the legacy shared
|
|
412
|
+
* {@link WORKER_PERSISTENCE_KEY} when none is available.
|
|
413
|
+
*
|
|
414
|
+
* IndexedDB is origin-scoped, so the FIXED key made every project served on
|
|
415
|
+
* one localhost port share ONE sandbox snapshot database — another app's auth
|
|
416
|
+
* users, Firestore docs, and RTDB tree appeared in an unrelated project
|
|
417
|
+
* (issue #359's storage defect, same family; identity flows from the same
|
|
418
|
+
* `InitPayload.projectKey` the storage fix uses).
|
|
419
|
+
*
|
|
420
|
+
* Migration decision — mirrors `storageDbName` in pyric's
|
|
421
|
+
* `storage/persistence.ts`, recorded here deliberately: the legacy shared
|
|
422
|
+
* `pyric-shared-worker` database is ORPHANED, not migrated (cross-project
|
|
423
|
+
* state disappearing from the sandbox is the point of the fix) and not
|
|
424
|
+
* deleted (older pyric versions on the same origin may still read it).
|
|
425
|
+
*/
|
|
426
|
+
export function workerPersistenceKey(projectKey?: string | null): string {
|
|
427
|
+
return projectKey ? `${WORKER_PERSISTENCE_KEY}:${projectKey}` : WORKER_PERSISTENCE_KEY;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const PERMISSIVE_RULES = `
|
|
431
|
+
rules_version = '2';
|
|
432
|
+
service cloud.firestore {
|
|
433
|
+
match /databases/{database}/documents {
|
|
434
|
+
match /{document=**} {
|
|
435
|
+
allow read, write: if true;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
`;
|
|
440
|
+
|
|
441
|
+
/** Environment for {@link buildWorkerCtx}. Everything ambient is injected so
|
|
442
|
+
* the REAL boot path is testable headlessly (fake-indexeddb + stub fetch). */
|
|
443
|
+
export interface WorkerBootEnv extends ServeInitEnv {
|
|
444
|
+
/** The raw local backend (IndexedDB in the real worker). Also used for the
|
|
445
|
+
* instance id + branches + (via the durable wrapper) the controller blob. */
|
|
446
|
+
idb: PersistenceBackend;
|
|
447
|
+
/** Persistence key / IDB database name. An explicit key always wins (tests
|
|
448
|
+
* isolate per-case state with it); the default is project-scoped via
|
|
449
|
+
* {@link workerPersistenceKey} from the init payload's `projectKey`. */
|
|
450
|
+
persistenceKey?: string;
|
|
451
|
+
/** Hot-reload EventSource factory. Omit/null when unavailable (tests, hosts
|
|
452
|
+
* without EventSource). */
|
|
453
|
+
makeEventSource?: ((url: string) => EventSourceLike) | null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Build the ONE shared sandbox + Firestore handle — the SharedWorker's boot
|
|
458
|
+
* path, extracted from `entry.ts` so it is testable without a SharedWorker
|
|
459
|
+
* runtime (the reload-durability regression tests boot it twice over one
|
|
460
|
+
* fake-IDB factory).
|
|
461
|
+
*
|
|
462
|
+
* DURABILITY CONTRACT: persistence is wired via `sandbox.enablePersistence`
|
|
463
|
+
* (NOT the raw `attachPersistence` helper) so the controller registers on the
|
|
464
|
+
* sandbox and `sandbox.flush()` works. The worker host awaits that flush —
|
|
465
|
+
* which awaits the IndexedDB transaction's `oncomplete` — before acking every
|
|
466
|
+
* mutating op. That is the worker's ONLY durability mechanism: a SharedWorker
|
|
467
|
+
* gets no beforeunload/beforeterminate, so anything not committed to IDB when
|
|
468
|
+
* the last tab closes is lost.
|
|
469
|
+
*/
|
|
470
|
+
export async function buildWorkerCtx(bootEnv: WorkerBootEnv): Promise<HostCtx> {
|
|
471
|
+
// Detach `fetch` from the env record before ANY use (issue #364). Every call
|
|
472
|
+
// site in this module invokes it as `env.fetch(...)`, which binds `this` to
|
|
473
|
+
// the env object — and the real browser/worker `fetch` is this-sensitive:
|
|
474
|
+
// it throws "Illegal invocation" unless `this` is undefined or the global.
|
|
475
|
+
// That silent throw meant the capture flush never POSTed and
|
|
476
|
+
// `hydrateEventHistory`'s GET failed into its catch, so a REBOOTED worker
|
|
477
|
+
// always answered Studio's first event subscription with an EMPTY history —
|
|
478
|
+
// the Activity/Traffic first open showed nothing until new live events
|
|
479
|
+
// arrived. The wrapper restores a plain call (`this` undefined ⇒ the
|
|
480
|
+
// global); injected test stubs are unaffected.
|
|
481
|
+
const ambientFetch = bootEnv.fetch;
|
|
482
|
+
const env: WorkerBootEnv = {
|
|
483
|
+
...bootEnv,
|
|
484
|
+
// Cast: Bun's `typeof fetch` also declares a `preconnect` member the
|
|
485
|
+
// plain-call wrapper doesn't need (nothing in this module uses it).
|
|
486
|
+
fetch: ((...args: Parameters<typeof fetch>) => ambientFetch(...args)) as typeof fetch,
|
|
487
|
+
};
|
|
488
|
+
const sandbox = initializeSandbox();
|
|
489
|
+
|
|
490
|
+
// Deploy permissive starter rules via admin-firestore.
|
|
491
|
+
// Callers override at runtime via the setRules op.
|
|
492
|
+
const { getFirestore: getAdminFirestore } = await import('pyric/sandbox/admin-firestore');
|
|
493
|
+
const adminDb = getAdminFirestore(sandbox.withAuth(null));
|
|
494
|
+
adminDb.setRules(PERMISSIVE_RULES);
|
|
495
|
+
|
|
496
|
+
// Fetch serve's init payload FIRST — it decides the DURABLE strategy before
|
|
497
|
+
// we attach (`--persist` mirrors the controller blob to the committable
|
|
498
|
+
// server file and primes from it on a fresh worker; a `seedState` fixture
|
|
499
|
+
// primes IDB once) AND it carries `projectKey`, which must exist BEFORE the
|
|
500
|
+
// first restore/save below: the persistence key is chosen exactly once at
|
|
501
|
+
// enablePersistence, so a lazy identity here would claim the legacy shared
|
|
502
|
+
// database — the same eager-vs-lazy trap the storage DB scoping hit (see
|
|
503
|
+
// applyServeInit's unconditional storage open). Null when standalone (no
|
|
504
|
+
// `pyric dev` behind us) — the legacy shared key then applies.
|
|
505
|
+
const payload = await fetchInitPayload(env.fetch);
|
|
506
|
+
|
|
507
|
+
// The persistence-controller backend. `--persist`/`seedState` wrap IDB (see
|
|
508
|
+
// createWorkerDurableBackend); plain IDB otherwise. The session record stays
|
|
509
|
+
// on the RAW idb (local-only — it must NEVER reach the committable server
|
|
510
|
+
// file), so that is what we hand the host as `sessionBackend`.
|
|
511
|
+
const durable = payload ? createWorkerDurableBackend(env.idb, payload, env) : env.idb;
|
|
512
|
+
await sandbox.enablePersistence({
|
|
513
|
+
key: env.persistenceKey ?? workerPersistenceKey(payload?.projectKey),
|
|
514
|
+
injectedBackend: durable,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Register the auth service with the persistence registry BEFORE we read
|
|
518
|
+
// back any session: `getAuth(sandbox)` makes the user DB ride the snapshot
|
|
519
|
+
// (the #629 mechanism). One auth per worker — one shared user pool across
|
|
520
|
+
// all tabs (sessions are per-port; see host-auth.ts).
|
|
521
|
+
getAuth(sandbox);
|
|
522
|
+
|
|
523
|
+
// Register RTDB with the persistence registry EAGERLY, same reasoning as
|
|
524
|
+
// auth above: `getDatabase(sandbox)` calls `registerPersistableService`,
|
|
525
|
+
// which makes the persisted tree ride the controller blob AND (via the
|
|
526
|
+
// controller's late-registration hook) applies the restored tree NOW. The
|
|
527
|
+
// worker otherwise creates RTDB handles lazily (`ctx.rtdb ??= ...`), so a
|
|
528
|
+
// Studio RTDB-tab read that arrives before any RTDB op would see an empty
|
|
529
|
+
// tree even though a prior session's data was persisted. Eager registration
|
|
530
|
+
// makes the restored tree queryable immediately.
|
|
531
|
+
getDatabase(sandbox);
|
|
532
|
+
|
|
533
|
+
// Sandbox-live Firestore: `getFirestore(sandbox)` reads
|
|
534
|
+
// `sandbox.currentUser` per operation, so auth changes propagate
|
|
535
|
+
// to subsequent Firestore ops without rebuilding the handle.
|
|
536
|
+
const db = getFirestore(sandbox);
|
|
537
|
+
|
|
538
|
+
const ctx: HostCtx = {
|
|
539
|
+
db,
|
|
540
|
+
sandbox,
|
|
541
|
+
// Stable per-SharedWorker identity (raw idb, local-only). Two browser
|
|
542
|
+
// profiles on the same port get two ids — how the UI tells them apart.
|
|
543
|
+
instanceId: await getOrCreateInstanceId(env.idb),
|
|
544
|
+
subs: new Map(),
|
|
545
|
+
sessionBackend: env.idb,
|
|
546
|
+
sessionMode: 'LOCAL',
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// Re-hydrate the event history from the served capture BEFORE applyServeInit
|
|
550
|
+
// runs — history is empty here (fresh sandbox, persistence restore emits no
|
|
551
|
+
// sandbox events), so primed events land first and any live boot events
|
|
552
|
+
// (seed, first ops) follow. This is what makes Traffic / activity / metrics
|
|
553
|
+
// survive a worker death: the DATA came back from IDB above, this restores
|
|
554
|
+
// the RECORD of it. Best-effort — a failure never blocks boot.
|
|
555
|
+
if (payload?.capture) {
|
|
556
|
+
try {
|
|
557
|
+
await hydrateEventHistory(ctx, env);
|
|
558
|
+
} catch {
|
|
559
|
+
/* activity-log hydration is non-essential; never brick boot over it. */
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Plugin-level engine config (`@pyric/cli/vite`'s `ai.engine`) → the host
|
|
564
|
+
// engine slot. It wins over any op-carried `engine` field (see ensureAiBroker
|
|
565
|
+
// in host-ai.ts) and is honored on the first ai op — mirroring getAI's
|
|
566
|
+
// first-call-wins idempotence. Absent under `pyric dev` (no CLI surface).
|
|
567
|
+
if (payload?.ai?.engine) ctx.aiEngine = payload.ai.engine;
|
|
568
|
+
|
|
569
|
+
// Default-on, warning-only. Start after hydration so a restored capture can
|
|
570
|
+
// populate a report without replaying an old warning into a fresh terminal.
|
|
571
|
+
setupFirebaseActivityGuard(ctx, env, payload?.activityToken);
|
|
572
|
+
|
|
573
|
+
// Apply rules / seed / authUsers / capture BEFORE any port op runs (so
|
|
574
|
+
// seeded users exist and project rules govern the first write), then mirror
|
|
575
|
+
// auth to the committable server file (`--persist` only).
|
|
576
|
+
if (payload) {
|
|
577
|
+
applyServeInit(ctx, payload, env);
|
|
578
|
+
setupServerAuthFlush(ctx, payload, env);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// The worker owns the SINGLE hot-reload stream for the origin (tabs open
|
|
582
|
+
// none) — so a rules change deploys once and multi-tab pages never exhaust
|
|
583
|
+
// the per-origin connection cap.
|
|
584
|
+
if (env.makeEventSource) {
|
|
585
|
+
setupWorkerHotReload(ctx, env.makeEventSource);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return ctx;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Fetch + parse `/__pyric/init.json`. The URL is relative to the worker
|
|
593
|
+
* script's origin (same origin as the page). Returns null on any failure: a
|
|
594
|
+
* worker opened outside `pyric dev` has no init endpoint and simply keeps
|
|
595
|
+
* the permissive starter rules + a plain-IDB durable store.
|
|
596
|
+
*/
|
|
597
|
+
async function fetchInitPayload(fetchFn: typeof fetch): Promise<InitPayload | null> {
|
|
598
|
+
try {
|
|
599
|
+
const res = await fetchFn('/__pyric/init.json');
|
|
600
|
+
if (!res.ok) return null;
|
|
601
|
+
return (await res.json()) as InitPayload;
|
|
602
|
+
} catch {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Same-origin relay between a real Service Worker and the SharedWorker host. */
|
|
2
|
+
import type { InboundMessage, OutboundMessage } from './protocol.js';
|
|
3
|
+
|
|
4
|
+
export const SERVICE_WORKER_CHANNEL = 'pyric-shared-worker:service-worker';
|
|
5
|
+
|
|
6
|
+
export type ServiceWorkerChannelMessage =
|
|
7
|
+
| {
|
|
8
|
+
readonly direction: 'host';
|
|
9
|
+
readonly phase: 'attach';
|
|
10
|
+
readonly clientId: string;
|
|
11
|
+
readonly sessionId: string;
|
|
12
|
+
}
|
|
13
|
+
| {
|
|
14
|
+
readonly direction: 'host';
|
|
15
|
+
readonly phase: 'message';
|
|
16
|
+
readonly clientId: string;
|
|
17
|
+
readonly sessionId: string;
|
|
18
|
+
readonly message: InboundMessage;
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
readonly direction: 'client';
|
|
22
|
+
readonly clientId: string;
|
|
23
|
+
readonly sessionId: string;
|
|
24
|
+
readonly message: OutboundMessage;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** `SharedWorker` is deliberately absent in ServiceWorkerGlobalScope. */
|
|
28
|
+
export function isServiceWorkerRealm(): boolean {
|
|
29
|
+
const scope = globalThis as { registration?: unknown; clients?: unknown };
|
|
30
|
+
return typeof window === 'undefined'
|
|
31
|
+
&& scope.registration !== undefined
|
|
32
|
+
&& scope.clients !== undefined;
|
|
33
|
+
}
|