@pyric/cli 0.1.0-alpha.11 → 0.1.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/dist/conformance/.generated/can-i-use-browser.d.ts.map +1 -1
  2. package/dist/conformance/.generated/can-i-use-browser.js +1 -1
  3. package/dist/conformance/.generated/can-i-use-browser.js.map +1 -1
  4. package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
  5. package/dist/conformance/.generated/can-i-use.js +2 -2
  6. package/dist/conformance/.generated/can-i-use.js.map +1 -1
  7. package/dist/serve/docs-ui/docs/_rhythm/index.html +1 -1
  8. package/dist/serve/docs-ui/docs/agent/set-up-your-agent/index.html +1 -1
  9. package/dist/serve/docs-ui/docs/agent/watch-and-review/index.html +1 -1
  10. package/dist/serve/docs-ui/docs/agent/work-with-an-agent/index.html +1 -1
  11. package/dist/serve/docs-ui/docs/ai-compat/index.html +1 -1
  12. package/dist/serve/docs-ui/docs/api-reference/index.html +2 -2
  13. package/dist/serve/docs-ui/docs/api-reference.md +1 -1
  14. package/dist/serve/docs-ui/docs/app-compat/index.html +1 -1
  15. package/dist/serve/docs-ui/docs/auth-compat/index.html +1 -1
  16. package/dist/serve/docs-ui/docs/build/ai-logic/index.html +1 -1
  17. package/dist/serve/docs-ui/docs/build/authentication/index.html +1 -1
  18. package/dist/serve/docs-ui/docs/build/cloud-firestore/index.html +1 -1
  19. package/dist/serve/docs-ui/docs/build/cloud-messaging/index.html +1 -1
  20. package/dist/serve/docs-ui/docs/build/cloud-storage/index.html +1 -1
  21. package/dist/serve/docs-ui/docs/build/realtime-database/index.html +1 -1
  22. package/dist/serve/docs-ui/docs/conformance-scores/index.html +1 -1
  23. package/dist/serve/docs-ui/docs/create-pyric-reference-api/index.html +1 -1
  24. package/dist/serve/docs-ui/docs/database-compat/index.html +1 -1
  25. package/dist/serve/docs-ui/docs/firestore-compat/index.html +1 -1
  26. package/dist/serve/docs-ui/docs/functions-rtdb-compat/index.html +1 -1
  27. package/dist/serve/docs-ui/docs/get-started/how-the-swap-works/index.html +1 -1
  28. package/dist/serve/docs-ui/docs/get-started/start-building/index.html +1 -1
  29. package/dist/serve/docs-ui/docs/index.json +200 -25
  30. package/dist/serve/docs-ui/docs/messaging-compat/index.html +1 -1
  31. package/dist/serve/docs-ui/docs/observe/see-whats-happening/index.html +1 -1
  32. package/dist/serve/docs-ui/docs/observe/shape-your-data/index.html +1 -1
  33. package/dist/serve/docs-ui/docs/overview/index.html +1 -1
  34. package/dist/serve/docs-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
  35. package/dist/serve/docs-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
  36. package/dist/serve/docs-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
  37. package/dist/serve/docs-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
  38. package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api/index.html +916 -0
  39. package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api.md +706 -0
  40. package/dist/serve/docs-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
  41. package/dist/serve/docs-ui/docs/pyric-ai-reference-api/index.html +1 -1
  42. package/dist/serve/docs-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
  43. package/dist/serve/docs-ui/docs/pyric-app-reference-api/index.html +1 -1
  44. package/dist/serve/docs-ui/docs/pyric-auth-reference-api/index.html +1 -1
  45. package/dist/serve/docs-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
  46. package/dist/serve/docs-ui/docs/pyric-cli-assurance-reference-api/index.html +1 -1
  47. package/dist/serve/docs-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
  48. package/dist/serve/docs-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
  49. package/dist/serve/docs-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
  50. package/dist/serve/docs-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
  51. package/dist/serve/docs-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
  52. package/dist/serve/docs-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
  53. package/dist/serve/docs-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
  54. package/dist/serve/docs-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
  55. package/dist/serve/docs-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
  56. package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api/index.html +131 -66
  57. package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api.md +130 -65
  58. package/dist/serve/docs-ui/docs/pyric-cli-verify-reference-api/index.html +1 -1
  59. package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api/index.html +1 -1
  60. package/dist/serve/docs-ui/docs/pyric-database-reference-api/index.html +1 -1
  61. package/dist/serve/docs-ui/docs/pyric-firestore-reference-api/index.html +1 -1
  62. package/dist/serve/docs-ui/docs/pyric-messaging-reference-api/index.html +1 -1
  63. package/dist/serve/docs-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
  64. package/dist/serve/docs-ui/docs/pyric-rules-reference-api/index.html +1 -1
  65. package/dist/serve/docs-ui/docs/pyric-sandbox-database-reference-api/index.html +1 -1
  66. package/dist/serve/docs-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
  67. package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api/index.html +37 -14
  68. package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api.md +28 -8
  69. package/dist/serve/docs-ui/docs/pyric-storage-reference-api/index.html +1 -1
  70. package/dist/serve/docs-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
  71. package/dist/serve/docs-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
  72. package/dist/serve/docs-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
  73. package/dist/serve/docs-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
  74. package/dist/serve/docs-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
  75. package/dist/serve/docs-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
  76. package/dist/serve/docs-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
  77. package/dist/serve/docs-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
  78. package/dist/serve/docs-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
  79. package/dist/serve/docs-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
  80. package/dist/serve/docs-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
  81. package/dist/serve/docs-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
  82. package/dist/serve/docs-ui/docs/pyric-ui-storage-reference-api/index.html +1 -1
  83. package/dist/serve/docs-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
  84. package/dist/serve/docs-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
  85. package/dist/serve/docs-ui/docs/rules-compat/index.html +1 -1
  86. package/dist/serve/docs-ui/docs/secure/audit-your-rules/index.html +1 -1
  87. package/dist/serve/docs-ui/docs/secure/firestore-rules-limits/index.html +1 -1
  88. package/dist/serve/docs-ui/docs/secure/read-a-denial/index.html +1 -1
  89. package/dist/serve/docs-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
  90. package/dist/serve/docs-ui/docs/secure/rules-standard-library/index.html +1 -1
  91. package/dist/serve/docs-ui/docs/secure/secure-it-with-rules/index.html +1 -1
  92. package/dist/serve/docs-ui/docs/secure/simulate-and-lint/index.html +1 -1
  93. package/dist/serve/docs-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
  94. package/dist/serve/docs-ui/docs/ship/ship-to-production/index.html +1 -1
  95. package/dist/serve/docs-ui/docs/ship/test-in-node/index.html +1 -1
  96. package/dist/serve/docs-ui/docs/storage-compat/index.html +1 -1
  97. package/dist/serve/docs-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
  98. package/dist/serve/docs-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
  99. package/dist/serve/docs-ui/llms.txt +1 -1
  100. package/dist/serve/worker/host/core.js +1 -1
  101. package/dist/serve/worker/host/core.js.map +1 -1
  102. package/dist/serve/worker/host/firestore-writes.d.ts.map +1 -1
  103. package/dist/serve/worker/host/firestore-writes.js +3 -2
  104. package/dist/serve/worker/host/firestore-writes.js.map +1 -1
  105. package/dist/serve/worker/index.d.ts +2 -1
  106. package/dist/serve/worker/index.d.ts.map +1 -1
  107. package/dist/serve/worker/index.js +2 -1
  108. package/dist/serve/worker/index.js.map +1 -1
  109. package/dist/serve/worker/protocol.js +2 -2
  110. package/dist/serve/worker/protocol.js.map +1 -1
  111. package/package.json +5 -4
  112. package/src/assurance/.generated/conformance-verdicts.ts +1090 -0
  113. package/src/assurance/attachment.ts +211 -0
  114. package/src/assurance/browser.ts +80 -0
  115. package/src/assurance/campaign.ts +496 -0
  116. package/src/assurance/capabilities.ts +446 -0
  117. package/src/assurance/cases.ts +43 -0
  118. package/src/assurance/index.ts +75 -0
  119. package/src/assurance/runner.ts +885 -0
  120. package/src/assurance/tool-names.ts +15 -0
  121. package/src/assurance/tools.ts +830 -0
  122. package/src/assurance/types.ts +352 -0
  123. package/src/assurance/validation.ts +434 -0
  124. package/src/bridge/client/bridge.ts +550 -0
  125. package/src/bridge/client/dispatch.ts +137 -0
  126. package/src/bridge/client.ts +45 -0
  127. package/src/bridge/protocol.ts +351 -0
  128. package/src/bridge/server/audit.ts +53 -0
  129. package/src/bridge/server/bridge.ts +597 -0
  130. package/src/bridge/server/headless.ts +160 -0
  131. package/src/bridge/server/json-schema-to-zod.ts +108 -0
  132. package/src/bridge/server/local-bridge.ts +66 -0
  133. package/src/bridge/server/logger.ts +50 -0
  134. package/src/bridge/server/mcp-contract.ts +93 -0
  135. package/src/bridge/server/mcp.ts +129 -0
  136. package/src/bridge/server/peer.ts +232 -0
  137. package/src/bridge/server/standalone.ts +347 -0
  138. package/src/bridge/server/tool-metadata.ts +99 -0
  139. package/src/bridge/server.ts +28 -0
  140. package/src/cli/can-i-use.ts +50 -0
  141. package/src/cli/cli.test.ts +493 -0
  142. package/src/cli/database-rules.ts +329 -0
  143. package/src/cli/dev-runner.ts +282 -0
  144. package/src/cli/firebase-json.ts +100 -0
  145. package/src/cli/firestore-indexes.ts +70 -0
  146. package/src/cli/index.ts +425 -0
  147. package/src/cli/init.ts +260 -0
  148. package/src/cli/mcp-proxy.ts +196 -0
  149. package/src/cli/parse-args.ts +80 -0
  150. package/src/cli/rules.ts +259 -0
  151. package/src/cli/scope.ts +67 -0
  152. package/src/cli/serve.ts +996 -0
  153. package/src/cli/service-commands.ts +73 -0
  154. package/src/cli/snapshot.ts +164 -0
  155. package/src/cli/storage-rules.ts +172 -0
  156. package/src/cli/verify.ts +416 -0
  157. package/src/conformance/.generated/can-i-use-browser.ts +113 -0
  158. package/src/conformance/.generated/can-i-use.ts +117 -0
  159. package/src/conformance/.generated/conformance-docs.ts +14 -0
  160. package/src/conformance/browser.ts +24 -0
  161. package/src/conformance/can-i-use-tool.ts +51 -0
  162. package/src/conformance/can-i-use.ts +18 -0
  163. package/src/conformance/docs.ts +8 -0
  164. package/src/conformance/index.ts +20 -0
  165. package/src/conformance/tools.ts +16 -0
  166. package/src/credentials/core/memoize-ttl.ts +142 -0
  167. package/src/credentials/core/types.ts +10 -0
  168. package/src/credentials/node/from-adc.ts +96 -0
  169. package/src/credentials/node/from-service-account.ts +133 -0
  170. package/src/credentials/node/index.ts +6 -0
  171. package/src/discover/concurrency.ts +127 -0
  172. package/src/discover/crawler-adapter.ts +142 -0
  173. package/src/discover/crawler.ts +1127 -0
  174. package/src/discover/credential-free.ts +27 -0
  175. package/src/discover/findCollectionGroup.ts +131 -0
  176. package/src/discover/firestore-source.ts +59 -0
  177. package/src/discover/index.ts +5 -0
  178. package/src/discover/merge.ts +523 -0
  179. package/src/discover/session.ts +402 -0
  180. package/src/discover/tools.ts +201 -0
  181. package/src/discover/types.ts +187 -0
  182. package/src/discover/wire.ts +324 -0
  183. package/src/functions-rtdb/child.ts +382 -0
  184. package/src/functions-rtdb/delivery.ts +7 -0
  185. package/src/functions-rtdb/discovery.ts +136 -0
  186. package/src/functions-rtdb/event.ts +64 -0
  187. package/src/functions-rtdb/execution.ts +111 -0
  188. package/src/functions-rtdb/in-memory-delivery.ts +40 -0
  189. package/src/functions-rtdb/project.ts +74 -0
  190. package/src/functions-rtdb/projection.ts +104 -0
  191. package/src/functions-rtdb/reference-pattern.ts +20 -0
  192. package/src/functions-rtdb/remote-delivery.ts +23 -0
  193. package/src/pkg-version.ts +49 -0
  194. package/src/register/esm-exports.ts +63 -0
  195. package/src/register/hooks.ts +30 -0
  196. package/src/register/index.ts +150 -0
  197. package/src/register/mapping.ts +33 -0
  198. package/src/remote/index.ts +1050 -0
  199. package/src/rtdb/crawl-snapshot.ts +118 -0
  200. package/src/rtdb/inspection.ts +137 -0
  201. package/src/rtdb/load-rules-document.ts +56 -0
  202. package/src/rtdb/rules-generation-tool.ts +38 -0
  203. package/src/rtdb/rules-json.ts +29 -0
  204. package/src/serve/activity-guard.ts +29 -0
  205. package/src/serve/activity-route.ts +164 -0
  206. package/src/serve/activity-warning.ts +26 -0
  207. package/src/serve/bridge-mount.ts +200 -0
  208. package/src/serve/bundler.ts +493 -0
  209. package/src/serve/capture-store.ts +65 -0
  210. package/src/serve/discovery.ts +199 -0
  211. package/src/serve/entries/ai.ts +292 -0
  212. package/src/serve/entries/app-backend.ts +4 -0
  213. package/src/serve/entries/app-client.ts +26 -0
  214. package/src/serve/entries/app-session-store.ts +47 -0
  215. package/src/serve/entries/app.ts +11 -0
  216. package/src/serve/entries/auth-helper-core.ts +181 -0
  217. package/src/serve/entries/auth-helper-dom.ts +134 -0
  218. package/src/serve/entries/auth-helper-runtime.ts +20 -0
  219. package/src/serve/entries/auth.ts +255 -0
  220. package/src/serve/entries/bridge-url.ts +31 -0
  221. package/src/serve/entries/database.ts +129 -0
  222. package/src/serve/entries/firestore.ts +232 -0
  223. package/src/serve/entries/init.ts +46 -0
  224. package/src/serve/entries/keepalive.ts +46 -0
  225. package/src/serve/entries/messaging-sw.ts +75 -0
  226. package/src/serve/entries/messaging.ts +96 -0
  227. package/src/serve/entries/runtime.ts +540 -0
  228. package/src/serve/entries/session-store.ts +97 -0
  229. package/src/serve/entries/storage.ts +92 -0
  230. package/src/serve/entries/tab-sync-wiring.ts +274 -0
  231. package/src/serve/entries/worker-runtime.ts +75 -0
  232. package/src/serve/init-payload.ts +42 -0
  233. package/src/serve/namespace.ts +675 -0
  234. package/src/serve/open-browser.ts +68 -0
  235. package/src/serve/rules.ts +272 -0
  236. package/src/serve/sandbox-marker.ts +31 -0
  237. package/src/serve/server.ts +428 -0
  238. package/src/serve/standalone-assets.ts +180 -0
  239. package/src/serve/state-store.ts +166 -0
  240. package/src/serve/studio/disk-project-store.ts +185 -0
  241. package/src/serve/studio/disk-workspace.ts +162 -0
  242. package/src/serve/studio/index.ts +20 -0
  243. package/src/serve/studio/routes.ts +237 -0
  244. package/src/serve/studio/store-types.ts +48 -0
  245. package/src/serve/studio/studio-storage.test.ts +289 -0
  246. package/src/serve/vite-plugin.ts +1102 -0
  247. package/src/serve/worker/activity-bootstrap.ts +24 -0
  248. package/src/serve/worker/client/admin-firestore.ts +38 -0
  249. package/src/serve/worker/client/ai.ts +124 -0
  250. package/src/serve/worker/client/auth.ts +489 -0
  251. package/src/serve/worker/client/connection.ts +202 -0
  252. package/src/serve/worker/client/core.ts +350 -0
  253. package/src/serve/worker/client/disconnect.ts +45 -0
  254. package/src/serve/worker/client/firestore-reads.ts +181 -0
  255. package/src/serve/worker/client/firestore-refs.ts +226 -0
  256. package/src/serve/worker/client/firestore-writes.ts +232 -0
  257. package/src/serve/worker/client/handles.ts +91 -0
  258. package/src/serve/worker/client/messaging.ts +119 -0
  259. package/src/serve/worker/client/presence.ts +174 -0
  260. package/src/serve/worker/client/rtdb.ts +311 -0
  261. package/src/serve/worker/client/rules.ts +63 -0
  262. package/src/serve/worker/client/service-worker-connection.ts +59 -0
  263. package/src/serve/worker/client/snapshots.ts +86 -0
  264. package/src/serve/worker/client/storage.ts +195 -0
  265. package/src/serve/worker/client/studio.ts +88 -0
  266. package/src/serve/worker/client.ts +57 -0
  267. package/src/serve/worker/durable-persistence.ts +137 -0
  268. package/src/serve/worker/entry.ts +179 -0
  269. package/src/serve/worker/host/admin-firestore.ts +88 -0
  270. package/src/serve/worker/host/connection.ts +179 -0
  271. package/src/serve/worker/host/core.ts +387 -0
  272. package/src/serve/worker/host/dispatch.ts +316 -0
  273. package/src/serve/worker/host/firestore-reads.ts +117 -0
  274. package/src/serve/worker/host/firestore-writes.ts +456 -0
  275. package/src/serve/worker/host/presence.ts +312 -0
  276. package/src/serve/worker/host/rtdb.ts +136 -0
  277. package/src/serve/worker/host/rules.ts +128 -0
  278. package/src/serve/worker/host/storage.ts +312 -0
  279. package/src/serve/worker/host/studio.ts +75 -0
  280. package/src/serve/worker/host/subscriptions.ts +238 -0
  281. package/src/serve/worker/host-ai.ts +165 -0
  282. package/src/serve/worker/host-auth.ts +469 -0
  283. package/src/serve/worker/host-context.ts +273 -0
  284. package/src/serve/worker/host-events.ts +94 -0
  285. package/src/serve/worker/host-messaging.ts +240 -0
  286. package/src/serve/worker/host.ts +47 -0
  287. package/src/serve/worker/index.ts +196 -0
  288. package/src/serve/worker/presence-timing.ts +15 -0
  289. package/src/serve/worker/protocol.ts +1112 -0
  290. package/src/serve/worker/serve-init.ts +606 -0
  291. package/src/serve/worker/service-worker-channel.ts +33 -0
  292. package/src/serve/worker/service-worker-relay.ts +86 -0
  293. package/src/serve/writer-lock.ts +53 -0
  294. package/src/verify/cases.ts +233 -0
  295. package/src/verify/fixture.ts +258 -0
  296. package/src/verify/index.ts +519 -0
  297. package/src/verify/tools.ts +108 -0
  298. package/src/version/compat-target.ts +17 -0
  299. package/src/vite.ts +17 -0
  300. package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api/index.html +0 -31
  301. package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api.md +0 -29
@@ -0,0 +1,86 @@
1
+ /** Session-aware Service Worker relay over the authoritative SharedWorker host. */
2
+ import {
3
+ cleanupPort,
4
+ handleMessage,
5
+ type HostCtx,
6
+ type PortLike,
7
+ } from './host.js';
8
+ import type { ServiceWorkerChannelMessage } from './service-worker-channel.js';
9
+
10
+ type HostEnvelope = Extract<ServiceWorkerChannelMessage, { direction: 'host' }>;
11
+
12
+ interface RelayState {
13
+ readonly sessionId: string;
14
+ readonly port: PortLike;
15
+ queue: Promise<void>;
16
+ }
17
+
18
+ export interface ServiceWorkerRelay {
19
+ handle(envelope: HostEnvelope): Promise<void>;
20
+ }
21
+
22
+ /**
23
+ * Keep one virtual host port per logical app while treating each Service Worker
24
+ * realm as a new session. An attach from a replacement realm first tears down
25
+ * the previous port's subscriptions; frames from the superseded realm are then
26
+ * ignored instead of being attributed to the replacement.
27
+ */
28
+ export function createServiceWorkerRelay(options: {
29
+ getCtx(): Promise<HostCtx>;
30
+ send(message: ServiceWorkerChannelMessage): void;
31
+ onError?(error: unknown, envelope: HostEnvelope): void;
32
+ }): ServiceWorkerRelay {
33
+ const ports = new Map<string, RelayState>();
34
+
35
+ const makePort = (clientId: string, sessionId: string): PortLike => ({
36
+ postMessage(message) {
37
+ options.send({ direction: 'client', clientId, sessionId, message });
38
+ },
39
+ });
40
+
41
+ return {
42
+ async handle(envelope) {
43
+ if (envelope.phase === 'attach') {
44
+ const previous = ports.get(envelope.clientId);
45
+ if (previous?.sessionId === envelope.sessionId) {
46
+ await previous.queue;
47
+ return;
48
+ }
49
+ const state: RelayState = {
50
+ sessionId: envelope.sessionId,
51
+ port: makePort(envelope.clientId, envelope.sessionId),
52
+ queue: (previous?.queue ?? Promise.resolve()).then(async () => {
53
+ if (!previous) return;
54
+ try {
55
+ const ctx = await options.getCtx();
56
+ cleanupPort(ctx, previous.port);
57
+ } catch (error) {
58
+ options.onError?.(error, envelope);
59
+ }
60
+ }),
61
+ };
62
+ ports.set(envelope.clientId, state);
63
+ await state.queue;
64
+ return;
65
+ }
66
+
67
+ const state = ports.get(envelope.clientId);
68
+ if (!state || state.sessionId !== envelope.sessionId) return;
69
+ state.queue = state.queue.then(async () => {
70
+ try {
71
+ const ctx = await options.getCtx();
72
+ await handleMessage(ctx, state.port, envelope.message);
73
+ if (
74
+ envelope.message.t === 'disconnect'
75
+ && ports.get(envelope.clientId) === state
76
+ ) {
77
+ ports.delete(envelope.clientId);
78
+ }
79
+ } catch (error) {
80
+ options.onError?.(error, envelope);
81
+ }
82
+ });
83
+ await state.queue;
84
+ },
85
+ };
86
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Single-writer lock for `--persist` (pre-mortem #3).
3
+ *
4
+ * Two tabs = two sandboxes, each flushing its WHOLE world view — so the
5
+ * second tab's flush doesn't merge, it ERASES everything the first wrote.
6
+ * "last-writer-wins" undersold it: last-tab's-universe wins, visibly, on the
7
+ * loser's next reload.
8
+ *
9
+ * The conflict only exists at WRITE time, so the lock is resolved there: the
10
+ * first page to flush claims the writer role (carried in the
11
+ * `x-pyric-writer` header); a different page's flush is refused (409) and
12
+ * that page drops to read-only persistence + a loud console line. A reader
13
+ * that never mutates never conflicts.
14
+ *
15
+ * Liveness: the holder refreshes `lastSeen` on every accepted write and via
16
+ * a periodic heartbeat. A holder that goes quiet for `staleMs` (crash, or an
17
+ * idle tab whose heartbeat also stopped) is considered gone, so a fresh page
18
+ * can claim the role — no permanent wedge. The small overlap window where a
19
+ * stale-but-alive holder and a new claimer both think they're the writer is
20
+ * an accepted dev-tool race (documented), kept rare by a generous staleMs.
21
+ */
22
+
23
+ export interface WriterLock {
24
+ /** Claim/refresh for `id`. Granted when free, already held by `id`, or the
25
+ * current holder is stale. Returns whether `id` now holds the lock. */
26
+ claim(id: string, now: number): boolean;
27
+ /** Release if `id` is the holder (no-op otherwise). */
28
+ release(id: string): void;
29
+ /** Current holder, or null. */
30
+ holder(): string | null;
31
+ }
32
+
33
+ export function createWriterLock(staleMs = 60_000): WriterLock {
34
+ let holder: string | null = null;
35
+ let lastSeen = 0;
36
+ return {
37
+ claim(id, now) {
38
+ if (holder === null || holder === id || now - lastSeen > staleMs) {
39
+ holder = id;
40
+ lastSeen = now;
41
+ return true;
42
+ }
43
+ return false;
44
+ },
45
+ release(id) {
46
+ if (holder === id) {
47
+ holder = null;
48
+ lastSeen = 0;
49
+ }
50
+ },
51
+ holder: () => holder,
52
+ };
53
+ }
@@ -0,0 +1,233 @@
1
+ import type { RequestEvent } from 'pyric/sandbox';
2
+ import type { FirestoreMethod, FunctionMock, TestCase } from 'pyric/rules/internal';
3
+ import {
4
+ parseVerifyFixture,
5
+ type PyricVerifyFixture,
6
+ } from './fixture.js';
7
+
8
+ export type VerifyFixtureWarning = {
9
+ service: 'firestore';
10
+ eventId?: string;
11
+ path?: string;
12
+ method?: string;
13
+ code: string;
14
+ message: string;
15
+ };
16
+
17
+ export type VerifyUnsupportedEvent = {
18
+ service: 'firestore';
19
+ eventId?: string;
20
+ path?: string;
21
+ method?: string;
22
+ reason: string;
23
+ };
24
+
25
+ export interface DeriveRulesTestCasesOptions {
26
+ service?: 'firestore';
27
+ includeAllowed?: boolean;
28
+ includeDenied?: boolean;
29
+ mockReads?: 'strict' | 'omit';
30
+ }
31
+
32
+ export interface DerivedRulesTestCase {
33
+ eventId: string;
34
+ testCase: TestCase;
35
+ }
36
+
37
+ export interface DeriveRulesTestCasesResult {
38
+ ok: boolean;
39
+ service: 'firestore';
40
+ testCases: TestCase[];
41
+ derived: DerivedRulesTestCase[];
42
+ warnings: VerifyFixtureWarning[];
43
+ unsupportedEvents: VerifyUnsupportedEvent[];
44
+ }
45
+
46
+ export function deriveRulesTestCases(
47
+ fixtureInput: PyricVerifyFixture | unknown,
48
+ opts: DeriveRulesTestCasesOptions = {},
49
+ ): DeriveRulesTestCasesResult {
50
+ const fixture = parseVerifyFixture(fixtureInput);
51
+ const includeAllowed = opts.includeAllowed ?? true;
52
+ const includeDenied = opts.includeDenied ?? true;
53
+ const mockReads = opts.mockReads ?? 'strict';
54
+ const derived: DerivedRulesTestCase[] = [];
55
+ const warnings: VerifyFixtureWarning[] = [];
56
+ const unsupportedEvents: VerifyUnsupportedEvent[] = [];
57
+
58
+ for (const event of fixture.events) {
59
+ if (!isFirestoreRequestEvent(event)) continue;
60
+ if (event.origin === 'listener') continue;
61
+ if (event.detail?.admin === true) continue;
62
+ if (event.result === 'allow' && !includeAllowed) continue;
63
+ if (event.result === 'deny' && !includeDenied) continue;
64
+
65
+ if (event.result === 'unsupported') {
66
+ unsupportedEvents.push({
67
+ service: 'firestore',
68
+ eventId: event.id,
69
+ path: event.path,
70
+ method: event.method,
71
+ reason: event.reasons.join('\n') || 'captured Firestore request was unsupported locally.',
72
+ });
73
+ continue;
74
+ }
75
+
76
+ const method = lowerFirestoreMethod(event);
77
+ if (!method) {
78
+ unsupportedEvents.push({
79
+ service: 'firestore',
80
+ eventId: event.id,
81
+ path: event.path,
82
+ method: event.method,
83
+ reason: `cannot derive a Rules Test API case for Firestore method '${event.method}'.`,
84
+ });
85
+ continue;
86
+ }
87
+
88
+ const functionMocks = extractFunctionMocks(event);
89
+ if (mockReads === 'strict' && functionMocks === null) {
90
+ unsupportedEvents.push({
91
+ service: 'firestore',
92
+ eventId: event.id,
93
+ path: event.path,
94
+ method: event.method,
95
+ reason:
96
+ 'captured request appears to depend on get()/exists(), but the fixture does not contain exact rule-read mocks.',
97
+ });
98
+ continue;
99
+ }
100
+ if (mockReads === 'omit' && eventMayNeedFunctionMocks(event)) {
101
+ warnings.push({
102
+ service: 'firestore',
103
+ eventId: event.id,
104
+ path: event.path,
105
+ method: event.method,
106
+ code: 'READ_MOCKS_OMITTED',
107
+ message:
108
+ 'captured request may depend on get()/exists(); deriving the case without functionMocks may change hosted Rules Test API behavior.',
109
+ });
110
+ }
111
+
112
+ derived.push({
113
+ eventId: event.id,
114
+ testCase: buildTestCase(event, method, functionMocks ?? []),
115
+ });
116
+ }
117
+
118
+ return {
119
+ ok: unsupportedEvents.length === 0,
120
+ service: 'firestore',
121
+ testCases: derived.map((item) => item.testCase),
122
+ derived,
123
+ warnings,
124
+ unsupportedEvents,
125
+ };
126
+ }
127
+
128
+ function isFirestoreRequestEvent(event: unknown): event is RequestEvent {
129
+ if (!isCapturedRequestEvent(event) || event.kind !== 'request') return false;
130
+ const service = event.service;
131
+ return service === undefined || service === 'firestore';
132
+ }
133
+
134
+ function lowerFirestoreMethod(event: RequestEvent): FirestoreMethod | null {
135
+ if (event.method === 'get' || event.method === 'list' || event.method === 'create' || event.method === 'update' || event.method === 'delete') {
136
+ return event.method;
137
+ }
138
+ if (event.method === 'set') {
139
+ return event.resourceBefore?.exists ? 'update' : 'create';
140
+ }
141
+ return null;
142
+ }
143
+
144
+ function buildTestCase(
145
+ event: RequestEvent,
146
+ method: FirestoreMethod,
147
+ functionMocks: FunctionMock[],
148
+ ): TestCase {
149
+ const testCase: TestCase = {
150
+ description: `${event.result === 'allow' ? 'allow' : 'deny'} ${method} ${event.path}`,
151
+ expectation: event.result === 'allow' ? 'ALLOW' : 'DENY',
152
+ method,
153
+ path: event.path,
154
+ auth: event.auth,
155
+ requestTime: new Date(event.at).toISOString(),
156
+ };
157
+
158
+ if (event.resourceBefore?.exists && event.resourceBefore.data !== null) {
159
+ testCase.resource = event.resourceBefore.data;
160
+ }
161
+
162
+ if (isWriteMethod(method)) {
163
+ if (method !== 'delete') {
164
+ const data = event.resourceAfter?.data ?? event.request?.resourceData;
165
+ if (isFirestoreWriteData(data)) testCase.data = data;
166
+ }
167
+ testCase.writeMode = writeModeFor(method);
168
+ }
169
+
170
+ if (functionMocks.length > 0) {
171
+ testCase.functionMocks = functionMocks;
172
+ }
173
+
174
+ const query = (event.detail as { query?: unknown } | undefined)?.query;
175
+ if (isListQuery(query)) {
176
+ testCase.query = query;
177
+ }
178
+
179
+ return testCase;
180
+ }
181
+
182
+ function writeModeFor(method: FirestoreMethod): TestCase['writeMode'] | undefined {
183
+ if (method === 'create') return { kind: 'create' };
184
+ if (method === 'update') return { kind: 'update' };
185
+ if (method === 'delete') return { kind: 'delete' };
186
+ return undefined;
187
+ }
188
+
189
+ function isWriteMethod(method: FirestoreMethod): boolean {
190
+ return method === 'create' || method === 'update' || method === 'delete';
191
+ }
192
+
193
+ function extractFunctionMocks(event: RequestEvent): FunctionMock[] | null {
194
+ const detail = event.detail as { functionMocks?: unknown } | undefined;
195
+ if (Array.isArray(detail?.functionMocks)) {
196
+ return detail.functionMocks.filter(isFunctionMock);
197
+ }
198
+ return eventMayNeedFunctionMocks(event) ? null : [];
199
+ }
200
+
201
+ function isFunctionMock(value: unknown): value is FunctionMock {
202
+ if (!isFunctionMockCandidate(value)) return false;
203
+ if (value.function !== 'get' && value.function !== 'exists') return false;
204
+ return typeof value.path === 'string' && 'result' in value;
205
+ }
206
+
207
+ function isListQuery(value: unknown): value is NonNullable<TestCase['query']> {
208
+ if (!isListQueryCandidate(value)) return false;
209
+ if ('limit' in value && value.limit !== undefined && typeof value.limit !== 'number') return false;
210
+ if ('offset' in value && value.offset !== undefined && typeof value.offset !== 'number') return false;
211
+ if ('orderBy' in value && value.orderBy !== undefined && typeof value.orderBy !== 'string') return false;
212
+ return true;
213
+ }
214
+
215
+ function eventMayNeedFunctionMocks(event: RequestEvent): boolean {
216
+ return event.reasons.some((reason) => /\b(get|exists)\s*\(/.test(reason));
217
+ }
218
+
219
+ function isCapturedRequestEvent(value: unknown): value is Record<string, unknown> {
220
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
221
+ }
222
+
223
+ function isFirestoreWriteData(value: unknown): value is Record<string, unknown> {
224
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
225
+ }
226
+
227
+ function isFunctionMockCandidate(value: unknown): value is Record<string, unknown> {
228
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
229
+ }
230
+
231
+ function isListQueryCandidate(value: unknown): value is Record<string, unknown> {
232
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
233
+ }
@@ -0,0 +1,258 @@
1
+ import type { EventService, Sandbox, SandboxEvent } from 'pyric/sandbox';
2
+ import { getStorageSandbox } from 'pyric/storage';
3
+ import { isRtdbRulesJson } from '../rtdb/rules-json.js';
4
+
5
+ export const VERIFY_FIXTURE_SCHEMA = 'pyric.verify.fixture.v1' as const;
6
+
7
+ export interface VerifyFirestoreRulesBlock {
8
+ format: 'firestore.rules';
9
+ source: string;
10
+ }
11
+
12
+ export interface VerifyRtdbRulesBlock {
13
+ format: 'rtdb.rules.json';
14
+ json: { rules: Record<string, unknown> };
15
+ }
16
+
17
+ export interface VerifyStorageRulesBlock {
18
+ format: 'storage.rules';
19
+ source: string;
20
+ }
21
+
22
+ export interface PyricVerifyFixture {
23
+ schema: typeof VERIFY_FIXTURE_SCHEMA;
24
+ description?: string;
25
+ createdAt?: string;
26
+ /** Opaque id of the sandbox instance that produced this capture (the served
27
+ * SharedWorker's `instanceId`). Purely additive: `pyric verify` ignores it.
28
+ * Present only on captures written by the worker's capture flush; used by
29
+ * boot-time event hydration to SKIP priming a capture that belongs to a
30
+ * DIFFERENT instance (e.g. another browser profile sharing one `pyric dev`),
31
+ * so someone else's session never shows up as yours. Absent on older /
32
+ * standalone captures → hydration primes best-effort. */
33
+ capturedBy?: string;
34
+ events: SandboxEvent[];
35
+ services: {
36
+ firestore?: {
37
+ rules: VerifyFirestoreRulesBlock;
38
+ state: { documents: Record<string, Record<string, unknown>> };
39
+ };
40
+ rtdb?: {
41
+ rules: VerifyRtdbRulesBlock;
42
+ state: { tree: unknown };
43
+ databaseUrl?: string;
44
+ };
45
+ auth?: {
46
+ state: {
47
+ users?: unknown[];
48
+ currentUser?: unknown;
49
+ };
50
+ };
51
+ storage?: {
52
+ /** RULES TEXT ONLY — captured object state is a separate, larger
53
+ * redesign (persistence.ts's IDB blob store) and is deliberately
54
+ * left untouched here. `state` stays `null` until that lands. */
55
+ rules: VerifyStorageRulesBlock;
56
+ state: unknown;
57
+ };
58
+ [service: string]: unknown;
59
+ };
60
+ }
61
+
62
+ export interface BuildVerifyFixtureInput {
63
+ sandbox: Pick<Sandbox, 'history' | 'snapshot'> & { currentUser?: unknown };
64
+ description?: string;
65
+ firestoreRules?: string | null;
66
+ rtdbRules?: { rules: Record<string, unknown> } | null;
67
+ rtdbState?: unknown;
68
+ rtdbDatabaseUrl?: string | null;
69
+ /** Currently-deployed storage rules text. RULES ONLY — there is no
70
+ * `storageState` input; captured storage OBJECTS are a separate,
71
+ * larger redesign left untouched by this fixture. */
72
+ storageRules?: string | null;
73
+ authState?: {
74
+ users?: unknown[];
75
+ currentUser?: unknown;
76
+ } | null;
77
+ createdAt?: string;
78
+ /** Stamped into the fixture as `capturedBy` (identity for boot-time event
79
+ * hydration). Omit for `pyric verify` builds — they have no instance. */
80
+ capturedBy?: string;
81
+ }
82
+
83
+ export function buildVerifyFixture(input: BuildVerifyFixtureInput): PyricVerifyFixture {
84
+ const events = input.sandbox.history();
85
+ const snapshot = input.sandbox.snapshot();
86
+ const firestoreDocuments = snapshot.firestore;
87
+ const services: PyricVerifyFixture['services'] = {};
88
+
89
+ if (
90
+ input.firestoreRules != null ||
91
+ Object.keys(firestoreDocuments).length > 0 ||
92
+ events.some((event) => eventService(event) === 'firestore')
93
+ ) {
94
+ services.firestore = {
95
+ rules: {
96
+ format: 'firestore.rules',
97
+ source: input.firestoreRules ?? '',
98
+ },
99
+ state: { documents: firestoreDocuments },
100
+ };
101
+ }
102
+
103
+ if (
104
+ input.rtdbRules != null ||
105
+ input.rtdbState !== undefined ||
106
+ events.some((event) => eventService(event) === 'rtdb')
107
+ ) {
108
+ services.rtdb = {
109
+ rules: {
110
+ format: 'rtdb.rules.json',
111
+ json: input.rtdbRules ?? { rules: {} },
112
+ },
113
+ state: { tree: input.rtdbState ?? null },
114
+ ...(input.rtdbDatabaseUrl ? { databaseUrl: input.rtdbDatabaseUrl } : {}),
115
+ };
116
+ }
117
+
118
+ if (
119
+ input.storageRules != null ||
120
+ events.some((event) => eventService(event) === 'storage')
121
+ ) {
122
+ services.storage = {
123
+ rules: {
124
+ format: 'storage.rules',
125
+ source: input.storageRules ?? '',
126
+ },
127
+ // RULES TEXT ONLY (scope note above) — objects are never captured.
128
+ state: null,
129
+ };
130
+ }
131
+
132
+ const authUsers = input.authState?.users;
133
+ const currentUser = input.authState?.currentUser ?? input.sandbox.currentUser;
134
+ if ((authUsers && authUsers.length > 0) || currentUser != null) {
135
+ services.auth = {
136
+ state: {
137
+ ...(authUsers ? { users: authUsers } : {}),
138
+ ...(currentUser != null ? { currentUser } : {}),
139
+ },
140
+ };
141
+ }
142
+
143
+ return {
144
+ schema: VERIFY_FIXTURE_SCHEMA,
145
+ ...(input.description ? { description: input.description } : {}),
146
+ createdAt: input.createdAt ?? new Date().toISOString(),
147
+ ...(input.capturedBy ? { capturedBy: input.capturedBy } : {}),
148
+ events,
149
+ services,
150
+ };
151
+ }
152
+
153
+ export function parseVerifyFixture(value: unknown): PyricVerifyFixture {
154
+ if (!isVerifyFixtureObject(value)) {
155
+ throw new Error('fixture must be a JSON object.');
156
+ }
157
+ if (value.schema !== VERIFY_FIXTURE_SCHEMA) {
158
+ throw new Error(`fixture schema must be '${VERIFY_FIXTURE_SCHEMA}'.`);
159
+ }
160
+ if (!Array.isArray(value.events)) {
161
+ throw new Error('fixture.events must be an array.');
162
+ }
163
+ if (!isVerifyFixtureObject(value.services)) {
164
+ throw new Error('fixture.services must be an object.');
165
+ }
166
+
167
+ const services = value.services;
168
+ if (services.firestore !== undefined) {
169
+ assertFirestoreService(services.firestore);
170
+ }
171
+ if (services.rtdb !== undefined) {
172
+ assertRtdbService(services.rtdb);
173
+ }
174
+ if (services.storage !== undefined) {
175
+ assertStorageService(services.storage);
176
+ }
177
+ if (services.auth !== undefined && !isVerifyFixtureObject(services.auth)) {
178
+ throw new Error('fixture.services.auth must be an object.');
179
+ }
180
+
181
+ return value as unknown as PyricVerifyFixture;
182
+ }
183
+
184
+ export function fixtureVerifiableServices(
185
+ fixture: PyricVerifyFixture,
186
+ ): Array<'firestore' | 'rtdb'> {
187
+ const services: Array<'firestore' | 'rtdb'> = [];
188
+ if (fixture.services.firestore) services.push('firestore');
189
+ if (fixture.services.rtdb) services.push('rtdb');
190
+ return services;
191
+ }
192
+
193
+ function assertFirestoreService(value: unknown): void {
194
+ if (!isVerifyFixtureObject(value)) throw new Error('fixture.services.firestore must be an object.');
195
+ if (!isVerifyFixtureObject(value.rules) || value.rules.format !== 'firestore.rules' || typeof value.rules.source !== 'string') {
196
+ throw new Error('fixture.services.firestore.rules must contain firestore.rules source.');
197
+ }
198
+ if (!isVerifyFixtureObject(value.state) || !isVerifyFixtureObject(value.state.documents)) {
199
+ throw new Error('fixture.services.firestore.state.documents must be an object.');
200
+ }
201
+ }
202
+
203
+ function assertRtdbService(value: unknown): void {
204
+ if (!isVerifyFixtureObject(value)) throw new Error('fixture.services.rtdb must be an object.');
205
+ if (
206
+ !isVerifyFixtureObject(value.rules) ||
207
+ value.rules.format !== 'rtdb.rules.json' ||
208
+ !isRtdbRulesJson(value.rules.json)
209
+ ) {
210
+ throw new Error('fixture.services.rtdb.rules must contain RTDB rules JSON.');
211
+ }
212
+ if (!isVerifyFixtureObject(value.state) || !('tree' in value.state)) {
213
+ throw new Error('fixture.services.rtdb.state.tree is required.');
214
+ }
215
+ }
216
+
217
+ function assertStorageService(value: unknown): void {
218
+ if (!isVerifyFixtureObject(value)) throw new Error('fixture.services.storage must be an object.');
219
+ if (
220
+ !isVerifyFixtureObject(value.rules) ||
221
+ value.rules.format !== 'storage.rules' ||
222
+ typeof value.rules.source !== 'string'
223
+ ) {
224
+ throw new Error('fixture.services.storage.rules must contain storage.rules source.');
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Re-deploy a fixture's captured storage rules into a sandbox's storage
230
+ * evaluator. RULES TEXT ONLY, mirroring the capture-side scope note: this
231
+ * never touches storage OBJECTS (persistence.ts's IDB blob store) — only
232
+ * `fixture.services.storage.rules.source` is applied.
233
+ *
234
+ * Storage rules are honored only on the FIRST `getStorageSandbox` call per
235
+ * `Sandbox` (see `storage/service.ts`), so this must run before any other
236
+ * code opens the storage service on `sandbox` — exactly the same ordering
237
+ * constraint firestore/rtdb rules already have at restore time. A no-op
238
+ * when the fixture carries no storage block.
239
+ */
240
+ export function restoreStorageRulesFromFixture(
241
+ fixture: PyricVerifyFixture,
242
+ sandbox: Sandbox,
243
+ ): void {
244
+ const source = fixture.services.storage?.rules?.source;
245
+ if (source === undefined) return;
246
+ getStorageSandbox(sandbox, { rules: source });
247
+ }
248
+
249
+ function eventService(event: SandboxEvent): EventService {
250
+ if ('service' in event && typeof event.service === 'string') {
251
+ return event.service as EventService;
252
+ }
253
+ return 'firestore';
254
+ }
255
+
256
+ function isVerifyFixtureObject(value: unknown): value is Record<string, unknown> {
257
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
258
+ }