@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.
Files changed (424) hide show
  1. package/dist/cli/serve.js +4 -4
  2. package/dist/cli/serve.js.map +1 -1
  3. package/dist/conformance/.generated/can-i-use-browser.d.ts.map +1 -1
  4. package/dist/conformance/.generated/can-i-use-browser.js +1 -1
  5. package/dist/conformance/.generated/can-i-use-browser.js.map +1 -1
  6. package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
  7. package/dist/conformance/.generated/can-i-use.js +2 -2
  8. package/dist/conformance/.generated/can-i-use.js.map +1 -1
  9. package/dist/serve/bundler.d.ts +13 -8
  10. package/dist/serve/bundler.d.ts.map +1 -1
  11. package/dist/serve/bundler.js +31 -36
  12. package/dist/serve/bundler.js.map +1 -1
  13. package/dist/serve/docs-ui/docs/_rhythm/index.html +1 -1
  14. package/dist/serve/docs-ui/docs/agent/set-up-your-agent/index.html +1 -1
  15. package/dist/serve/docs-ui/docs/agent/watch-and-review/index.html +1 -1
  16. package/dist/serve/docs-ui/docs/agent/work-with-an-agent/index.html +1 -1
  17. package/dist/serve/docs-ui/docs/ai-compat/index.html +1 -1
  18. package/dist/serve/docs-ui/docs/api-reference/index.html +2 -2
  19. package/dist/serve/docs-ui/docs/api-reference.md +1 -1
  20. package/dist/serve/docs-ui/docs/app-compat/index.html +1 -1
  21. package/dist/serve/docs-ui/docs/auth-compat/index.html +1 -1
  22. package/dist/serve/docs-ui/docs/build/ai-logic/index.html +44 -22
  23. package/dist/serve/docs-ui/docs/build/ai-logic.md +69 -21
  24. package/dist/serve/docs-ui/docs/build/authentication/index.html +1 -1
  25. package/dist/serve/docs-ui/docs/build/cloud-firestore/index.html +1 -1
  26. package/dist/serve/docs-ui/docs/build/cloud-messaging/index.html +1 -1
  27. package/dist/serve/docs-ui/docs/build/cloud-storage/index.html +1 -1
  28. package/dist/serve/docs-ui/docs/build/realtime-database/index.html +1 -1
  29. package/dist/serve/docs-ui/docs/conformance-scores/index.html +1 -1
  30. package/dist/serve/docs-ui/docs/create-pyric-reference-api/index.html +1 -1
  31. package/dist/serve/docs-ui/docs/database-compat/index.html +1 -1
  32. package/dist/serve/docs-ui/docs/firestore-compat/index.html +1 -1
  33. package/dist/serve/docs-ui/docs/functions-rtdb-compat/index.html +1 -1
  34. package/dist/serve/docs-ui/docs/get-started/how-the-swap-works/index.html +2 -1
  35. package/dist/serve/docs-ui/docs/get-started/how-the-swap-works.md +2 -0
  36. package/dist/serve/docs-ui/docs/get-started/start-building/index.html +3 -2
  37. package/dist/serve/docs-ui/docs/get-started/start-building.md +3 -1
  38. package/dist/serve/docs-ui/docs/index.json +349 -29
  39. package/dist/serve/docs-ui/docs/messaging-compat/index.html +1 -1
  40. package/dist/serve/docs-ui/docs/observe/resolve-runtime-status/index.html +28 -0
  41. package/dist/serve/docs-ui/docs/observe/resolve-runtime-status.md +47 -0
  42. package/dist/serve/docs-ui/docs/observe/see-whats-happening/index.html +1 -1
  43. package/dist/serve/docs-ui/docs/observe/shape-your-data/index.html +1 -1
  44. package/dist/serve/docs-ui/docs/overview/index.html +1 -1
  45. package/dist/serve/docs-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
  46. package/dist/serve/docs-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
  47. package/dist/serve/docs-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
  48. package/dist/serve/docs-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
  49. package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api/index.html +916 -0
  50. package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api.md +706 -0
  51. package/dist/serve/docs-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
  52. package/dist/serve/docs-ui/docs/pyric-ai-reference-api/index.html +1 -1
  53. package/dist/serve/docs-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
  54. package/dist/serve/docs-ui/docs/pyric-app-reference-api/index.html +1 -1
  55. package/dist/serve/docs-ui/docs/pyric-auth-reference-api/index.html +1 -1
  56. package/dist/serve/docs-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
  57. package/dist/serve/docs-ui/docs/pyric-cli-assurance-reference-api/index.html +1 -1
  58. package/dist/serve/docs-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
  59. package/dist/serve/docs-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
  60. package/dist/serve/docs-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
  61. package/dist/serve/docs-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
  62. package/dist/serve/docs-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
  63. package/dist/serve/docs-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
  64. package/dist/serve/docs-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
  65. package/dist/serve/docs-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
  66. package/dist/serve/docs-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
  67. package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api/index.html +168 -76
  68. package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api.md +156 -73
  69. package/dist/serve/docs-ui/docs/pyric-cli-verify-reference-api/index.html +1 -1
  70. package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api/index.html +11 -8
  71. package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api.md +17 -4
  72. package/dist/serve/docs-ui/docs/pyric-database-reference-api/index.html +1 -1
  73. package/dist/serve/docs-ui/docs/pyric-firestore-reference-api/index.html +1 -1
  74. package/dist/serve/docs-ui/docs/pyric-messaging-reference-api/index.html +1 -1
  75. package/dist/serve/docs-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
  76. package/dist/serve/docs-ui/docs/pyric-rules-reference-api/index.html +1 -1
  77. package/dist/serve/docs-ui/docs/pyric-sandbox-database-reference-api/index.html +1 -1
  78. package/dist/serve/docs-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
  79. package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api/index.html +37 -14
  80. package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api.md +28 -8
  81. package/dist/serve/docs-ui/docs/pyric-storage-reference-api/index.html +1 -1
  82. package/dist/serve/docs-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
  83. package/dist/serve/docs-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
  84. package/dist/serve/docs-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
  85. package/dist/serve/docs-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
  86. package/dist/serve/docs-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
  87. package/dist/serve/docs-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
  88. package/dist/serve/docs-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
  89. package/dist/serve/docs-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
  90. package/dist/serve/docs-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
  91. package/dist/serve/docs-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
  92. package/dist/serve/docs-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
  93. package/dist/serve/docs-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
  94. package/dist/serve/docs-ui/docs/pyric-ui-storage-reference-api/index.html +1 -1
  95. package/dist/serve/docs-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
  96. package/dist/serve/docs-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
  97. package/dist/serve/docs-ui/docs/rules-compat/index.html +1 -1
  98. package/dist/serve/docs-ui/docs/secure/audit-your-rules/index.html +1 -1
  99. package/dist/serve/docs-ui/docs/secure/firestore-rules-limits/index.html +1 -1
  100. package/dist/serve/docs-ui/docs/secure/read-a-denial/index.html +1 -1
  101. package/dist/serve/docs-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
  102. package/dist/serve/docs-ui/docs/secure/rules-standard-library/index.html +1 -1
  103. package/dist/serve/docs-ui/docs/secure/secure-it-with-rules/index.html +1 -1
  104. package/dist/serve/docs-ui/docs/secure/simulate-and-lint/index.html +1 -1
  105. package/dist/serve/docs-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
  106. package/dist/serve/docs-ui/docs/ship/ship-to-production/index.html +1 -1
  107. package/dist/serve/docs-ui/docs/ship/test-in-node/index.html +1 -1
  108. package/dist/serve/docs-ui/docs/storage-compat/index.html +1 -1
  109. package/dist/serve/docs-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
  110. package/dist/serve/docs-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
  111. package/dist/serve/docs-ui/docs/tutorial/index.html +429 -0
  112. package/dist/serve/docs-ui/docs/tutorial.md +701 -0
  113. package/dist/serve/docs-ui/llms.txt +3 -1
  114. package/dist/serve/entries/init.d.ts.map +1 -1
  115. package/dist/serve/entries/init.js +3 -0
  116. package/dist/serve/entries/init.js.map +1 -1
  117. package/dist/serve/entries/runtime.d.ts.map +1 -1
  118. package/dist/serve/entries/runtime.js +20 -5
  119. package/dist/serve/entries/runtime.js.map +1 -1
  120. package/dist/serve/entries/worker-runtime.d.ts +2 -2
  121. package/dist/serve/entries/worker-runtime.d.ts.map +1 -1
  122. package/dist/serve/entries/worker-runtime.js +45 -10
  123. package/dist/serve/entries/worker-runtime.js.map +1 -1
  124. package/dist/serve/runtime/chip-config.d.ts +19 -0
  125. package/dist/serve/runtime/chip-config.d.ts.map +1 -0
  126. package/dist/serve/runtime/chip-config.js +19 -0
  127. package/dist/serve/runtime/chip-config.js.map +1 -0
  128. package/dist/serve/runtime/chip-install.d.ts +10 -0
  129. package/dist/serve/runtime/chip-install.d.ts.map +1 -0
  130. package/dist/serve/runtime/chip-install.js +15 -0
  131. package/dist/serve/runtime/chip-install.js.map +1 -0
  132. package/dist/serve/runtime/chip.d.ts +17 -0
  133. package/dist/serve/runtime/chip.d.ts.map +1 -0
  134. package/dist/serve/runtime/chip.js +257 -0
  135. package/dist/serve/runtime/chip.js.map +1 -0
  136. package/dist/serve/runtime/manifest.d.ts +22 -0
  137. package/dist/serve/runtime/manifest.d.ts.map +1 -0
  138. package/dist/serve/runtime/manifest.js +20 -0
  139. package/dist/serve/runtime/manifest.js.map +1 -0
  140. package/dist/serve/runtime/status.d.ts +49 -0
  141. package/dist/serve/runtime/status.d.ts.map +1 -0
  142. package/dist/serve/runtime/status.js +164 -0
  143. package/dist/serve/runtime/status.js.map +1 -0
  144. package/dist/serve/runtime/worker-connection.d.ts +4 -0
  145. package/dist/serve/runtime/worker-connection.d.ts.map +1 -0
  146. package/dist/serve/runtime/worker-connection.js +11 -0
  147. package/dist/serve/runtime/worker-connection.js.map +1 -0
  148. package/dist/serve/runtime/worker-generation.d.ts +14 -0
  149. package/dist/serve/runtime/worker-generation.d.ts.map +1 -0
  150. package/dist/serve/runtime/worker-generation.js +56 -0
  151. package/dist/serve/runtime/worker-generation.js.map +1 -0
  152. package/dist/serve/runtime/worker-replacement.d.ts +18 -0
  153. package/dist/serve/runtime/worker-replacement.d.ts.map +1 -0
  154. package/dist/serve/runtime/worker-replacement.js +29 -0
  155. package/dist/serve/runtime/worker-replacement.js.map +1 -0
  156. package/dist/serve/standalone-assets.d.ts +2 -2
  157. package/dist/serve/standalone-assets.d.ts.map +1 -1
  158. package/dist/serve/standalone-assets.js +1 -1
  159. package/dist/serve/standalone-assets.js.map +1 -1
  160. package/dist/serve/studio-ui/assets/{RulesCodeEditor-CxVYcZXw.js → RulesCodeEditor-BDlH-5lp.js} +1 -1
  161. package/dist/serve/studio-ui/assets/{index-cpSfgQ7H.js → index-Bf9SSE8L.js} +5 -5
  162. package/dist/serve/studio-ui/assets/{seed-DeuFKi4J.js → seed-D_UjxfOU.js} +1 -1
  163. package/dist/serve/studio-ui/index.html +1 -1
  164. package/dist/serve/vite-ai-config.d.ts +25 -0
  165. package/dist/serve/vite-ai-config.d.ts.map +1 -0
  166. package/dist/serve/vite-ai-config.js +45 -0
  167. package/dist/serve/vite-ai-config.js.map +1 -0
  168. package/dist/serve/vite-plugin.d.ts +17 -36
  169. package/dist/serve/vite-plugin.d.ts.map +1 -1
  170. package/dist/serve/vite-plugin.js +38 -57
  171. package/dist/serve/vite-plugin.js.map +1 -1
  172. package/dist/serve/vite-rules-source.d.ts +8 -0
  173. package/dist/serve/vite-rules-source.d.ts.map +1 -0
  174. package/dist/serve/vite-rules-source.js +23 -0
  175. package/dist/serve/vite-rules-source.js.map +1 -0
  176. package/dist/serve/vite-worker-runtime.d.ts +20 -0
  177. package/dist/serve/vite-worker-runtime.d.ts.map +1 -0
  178. package/dist/serve/vite-worker-runtime.js +25 -0
  179. package/dist/serve/vite-worker-runtime.js.map +1 -0
  180. package/dist/serve/worker/client/connection.d.ts +7 -2
  181. package/dist/serve/worker/client/connection.d.ts.map +1 -1
  182. package/dist/serve/worker/client/connection.js +13 -16
  183. package/dist/serve/worker/client/connection.js.map +1 -1
  184. package/dist/serve/worker/client/core.d.ts +7 -0
  185. package/dist/serve/worker/client/core.d.ts.map +1 -1
  186. package/dist/serve/worker/client/core.js +28 -0
  187. package/dist/serve/worker/client/core.js.map +1 -1
  188. package/dist/serve/worker/client/runtime-control.d.ts +8 -0
  189. package/dist/serve/worker/client/runtime-control.d.ts.map +1 -0
  190. package/dist/serve/worker/client/runtime-control.js +11 -0
  191. package/dist/serve/worker/client/runtime-control.js.map +1 -0
  192. package/dist/serve/worker/entry.js +44 -2
  193. package/dist/serve/worker/entry.js.map +1 -1
  194. package/dist/serve/worker/host/core.js +1 -1
  195. package/dist/serve/worker/host/core.js.map +1 -1
  196. package/dist/serve/worker/host/firestore-writes.d.ts.map +1 -1
  197. package/dist/serve/worker/host/firestore-writes.js +3 -2
  198. package/dist/serve/worker/host/firestore-writes.js.map +1 -1
  199. package/dist/serve/worker/host/studio.js +1 -1
  200. package/dist/serve/worker/host/studio.js.map +1 -1
  201. package/dist/serve/worker/host-context.d.ts +1 -1
  202. package/dist/serve/worker/host-context.d.ts.map +1 -1
  203. package/dist/serve/worker/index.d.ts +2 -1
  204. package/dist/serve/worker/index.d.ts.map +1 -1
  205. package/dist/serve/worker/index.js +2 -1
  206. package/dist/serve/worker/index.js.map +1 -1
  207. package/dist/serve/worker/protocol.d.ts +15 -1
  208. package/dist/serve/worker/protocol.d.ts.map +1 -1
  209. package/dist/serve/worker/protocol.js +2 -2
  210. package/dist/serve/worker/protocol.js.map +1 -1
  211. package/dist/serve/worker/retirement.d.ts +21 -0
  212. package/dist/serve/worker/retirement.d.ts.map +1 -0
  213. package/dist/serve/worker/retirement.js +97 -0
  214. package/dist/serve/worker/retirement.js.map +1 -0
  215. package/dist/serve/worker/serve-init.d.ts.map +1 -1
  216. package/dist/serve/worker/serve-init.js +5 -6
  217. package/dist/serve/worker/serve-init.js.map +1 -1
  218. package/dist/vite.d.ts +1 -0
  219. package/dist/vite.d.ts.map +1 -1
  220. package/dist/vite.js.map +1 -1
  221. package/package.json +6 -4
  222. package/src/assurance/.generated/conformance-verdicts.ts +1090 -0
  223. package/src/assurance/attachment.ts +211 -0
  224. package/src/assurance/browser.ts +80 -0
  225. package/src/assurance/campaign.ts +496 -0
  226. package/src/assurance/capabilities.ts +446 -0
  227. package/src/assurance/cases.ts +43 -0
  228. package/src/assurance/index.ts +75 -0
  229. package/src/assurance/runner.ts +885 -0
  230. package/src/assurance/tool-names.ts +15 -0
  231. package/src/assurance/tools.ts +830 -0
  232. package/src/assurance/types.ts +352 -0
  233. package/src/assurance/validation.ts +434 -0
  234. package/src/bridge/client/bridge.ts +550 -0
  235. package/src/bridge/client/dispatch.ts +137 -0
  236. package/src/bridge/client.ts +45 -0
  237. package/src/bridge/protocol.ts +351 -0
  238. package/src/bridge/server/audit.ts +53 -0
  239. package/src/bridge/server/bridge.ts +597 -0
  240. package/src/bridge/server/headless.ts +160 -0
  241. package/src/bridge/server/json-schema-to-zod.ts +108 -0
  242. package/src/bridge/server/local-bridge.ts +66 -0
  243. package/src/bridge/server/logger.ts +50 -0
  244. package/src/bridge/server/mcp-contract.ts +93 -0
  245. package/src/bridge/server/mcp.ts +129 -0
  246. package/src/bridge/server/peer.ts +232 -0
  247. package/src/bridge/server/standalone.ts +347 -0
  248. package/src/bridge/server/tool-metadata.ts +99 -0
  249. package/src/bridge/server.ts +28 -0
  250. package/src/cli/can-i-use.ts +50 -0
  251. package/src/cli/cli.test.ts +493 -0
  252. package/src/cli/database-rules.ts +329 -0
  253. package/src/cli/dev-runner.ts +282 -0
  254. package/src/cli/firebase-json.ts +100 -0
  255. package/src/cli/firestore-indexes.ts +70 -0
  256. package/src/cli/index.ts +425 -0
  257. package/src/cli/init.ts +260 -0
  258. package/src/cli/mcp-proxy.ts +196 -0
  259. package/src/cli/parse-args.ts +80 -0
  260. package/src/cli/rules.ts +259 -0
  261. package/src/cli/scope.ts +67 -0
  262. package/src/cli/serve.ts +996 -0
  263. package/src/cli/service-commands.ts +73 -0
  264. package/src/cli/snapshot.ts +164 -0
  265. package/src/cli/storage-rules.ts +172 -0
  266. package/src/cli/verify.ts +416 -0
  267. package/src/conformance/.generated/can-i-use-browser.ts +113 -0
  268. package/src/conformance/.generated/can-i-use.ts +117 -0
  269. package/src/conformance/.generated/conformance-docs.ts +14 -0
  270. package/src/conformance/browser.ts +24 -0
  271. package/src/conformance/can-i-use-tool.ts +51 -0
  272. package/src/conformance/can-i-use.ts +18 -0
  273. package/src/conformance/docs.ts +8 -0
  274. package/src/conformance/index.ts +20 -0
  275. package/src/conformance/tools.ts +16 -0
  276. package/src/credentials/core/memoize-ttl.ts +142 -0
  277. package/src/credentials/core/types.ts +10 -0
  278. package/src/credentials/node/from-adc.ts +96 -0
  279. package/src/credentials/node/from-service-account.ts +133 -0
  280. package/src/credentials/node/index.ts +6 -0
  281. package/src/discover/concurrency.ts +127 -0
  282. package/src/discover/crawler-adapter.ts +142 -0
  283. package/src/discover/crawler.ts +1127 -0
  284. package/src/discover/credential-free.ts +27 -0
  285. package/src/discover/findCollectionGroup.ts +131 -0
  286. package/src/discover/firestore-source.ts +59 -0
  287. package/src/discover/index.ts +5 -0
  288. package/src/discover/merge.ts +523 -0
  289. package/src/discover/session.ts +402 -0
  290. package/src/discover/tools.ts +201 -0
  291. package/src/discover/types.ts +187 -0
  292. package/src/discover/wire.ts +324 -0
  293. package/src/functions-rtdb/child.ts +382 -0
  294. package/src/functions-rtdb/delivery.ts +7 -0
  295. package/src/functions-rtdb/discovery.ts +136 -0
  296. package/src/functions-rtdb/event.ts +64 -0
  297. package/src/functions-rtdb/execution.ts +111 -0
  298. package/src/functions-rtdb/in-memory-delivery.ts +40 -0
  299. package/src/functions-rtdb/project.ts +74 -0
  300. package/src/functions-rtdb/projection.ts +104 -0
  301. package/src/functions-rtdb/reference-pattern.ts +20 -0
  302. package/src/functions-rtdb/remote-delivery.ts +23 -0
  303. package/src/pkg-version.ts +49 -0
  304. package/src/register/esm-exports.ts +63 -0
  305. package/src/register/hooks.ts +30 -0
  306. package/src/register/index.ts +150 -0
  307. package/src/register/mapping.ts +33 -0
  308. package/src/remote/index.ts +1050 -0
  309. package/src/rtdb/crawl-snapshot.ts +118 -0
  310. package/src/rtdb/inspection.ts +137 -0
  311. package/src/rtdb/load-rules-document.ts +56 -0
  312. package/src/rtdb/rules-generation-tool.ts +38 -0
  313. package/src/rtdb/rules-json.ts +29 -0
  314. package/src/serve/activity-guard.ts +29 -0
  315. package/src/serve/activity-route.ts +164 -0
  316. package/src/serve/activity-warning.ts +26 -0
  317. package/src/serve/bridge-mount.ts +200 -0
  318. package/src/serve/bundler.ts +519 -0
  319. package/src/serve/capture-store.ts +65 -0
  320. package/src/serve/discovery.ts +199 -0
  321. package/src/serve/entries/ai.ts +292 -0
  322. package/src/serve/entries/app-backend.ts +4 -0
  323. package/src/serve/entries/app-client.ts +26 -0
  324. package/src/serve/entries/app-session-store.ts +47 -0
  325. package/src/serve/entries/app.ts +11 -0
  326. package/src/serve/entries/auth-helper-core.ts +181 -0
  327. package/src/serve/entries/auth-helper-dom.ts +134 -0
  328. package/src/serve/entries/auth-helper-runtime.ts +20 -0
  329. package/src/serve/entries/auth.ts +255 -0
  330. package/src/serve/entries/bridge-url.ts +31 -0
  331. package/src/serve/entries/database.ts +129 -0
  332. package/src/serve/entries/firestore.ts +232 -0
  333. package/src/serve/entries/init.ts +49 -0
  334. package/src/serve/entries/keepalive.ts +46 -0
  335. package/src/serve/entries/messaging-sw.ts +75 -0
  336. package/src/serve/entries/messaging.ts +96 -0
  337. package/src/serve/entries/runtime.ts +556 -0
  338. package/src/serve/entries/session-store.ts +97 -0
  339. package/src/serve/entries/storage.ts +92 -0
  340. package/src/serve/entries/tab-sync-wiring.ts +274 -0
  341. package/src/serve/entries/worker-runtime.ts +135 -0
  342. package/src/serve/init-payload.ts +42 -0
  343. package/src/serve/namespace.ts +675 -0
  344. package/src/serve/open-browser.ts +68 -0
  345. package/src/serve/rules.ts +272 -0
  346. package/src/serve/runtime/chip-config.ts +30 -0
  347. package/src/serve/runtime/chip-install.ts +27 -0
  348. package/src/serve/runtime/chip.ts +282 -0
  349. package/src/serve/runtime/manifest.ts +37 -0
  350. package/src/serve/runtime/status.ts +216 -0
  351. package/src/serve/runtime/worker-connection.ts +14 -0
  352. package/src/serve/runtime/worker-generation.ts +75 -0
  353. package/src/serve/runtime/worker-replacement.ts +42 -0
  354. package/src/serve/sandbox-marker.ts +31 -0
  355. package/src/serve/server.ts +428 -0
  356. package/src/serve/standalone-assets.ts +180 -0
  357. package/src/serve/state-store.ts +166 -0
  358. package/src/serve/studio/disk-project-store.ts +185 -0
  359. package/src/serve/studio/disk-workspace.ts +162 -0
  360. package/src/serve/studio/index.ts +20 -0
  361. package/src/serve/studio/routes.ts +237 -0
  362. package/src/serve/studio/store-types.ts +48 -0
  363. package/src/serve/studio/studio-storage.test.ts +289 -0
  364. package/src/serve/vite-ai-config.ts +77 -0
  365. package/src/serve/vite-plugin.ts +1075 -0
  366. package/src/serve/vite-rules-source.ts +28 -0
  367. package/src/serve/vite-worker-runtime.ts +51 -0
  368. package/src/serve/worker/activity-bootstrap.ts +24 -0
  369. package/src/serve/worker/client/admin-firestore.ts +38 -0
  370. package/src/serve/worker/client/ai.ts +124 -0
  371. package/src/serve/worker/client/auth.ts +489 -0
  372. package/src/serve/worker/client/connection.ts +227 -0
  373. package/src/serve/worker/client/core.ts +384 -0
  374. package/src/serve/worker/client/disconnect.ts +45 -0
  375. package/src/serve/worker/client/firestore-reads.ts +181 -0
  376. package/src/serve/worker/client/firestore-refs.ts +226 -0
  377. package/src/serve/worker/client/firestore-writes.ts +232 -0
  378. package/src/serve/worker/client/handles.ts +91 -0
  379. package/src/serve/worker/client/messaging.ts +119 -0
  380. package/src/serve/worker/client/presence.ts +174 -0
  381. package/src/serve/worker/client/rtdb.ts +311 -0
  382. package/src/serve/worker/client/rules.ts +63 -0
  383. package/src/serve/worker/client/runtime-control.ts +22 -0
  384. package/src/serve/worker/client/service-worker-connection.ts +59 -0
  385. package/src/serve/worker/client/snapshots.ts +86 -0
  386. package/src/serve/worker/client/storage.ts +195 -0
  387. package/src/serve/worker/client/studio.ts +88 -0
  388. package/src/serve/worker/client.ts +57 -0
  389. package/src/serve/worker/durable-persistence.ts +137 -0
  390. package/src/serve/worker/entry.ts +223 -0
  391. package/src/serve/worker/host/admin-firestore.ts +88 -0
  392. package/src/serve/worker/host/connection.ts +179 -0
  393. package/src/serve/worker/host/core.ts +387 -0
  394. package/src/serve/worker/host/dispatch.ts +316 -0
  395. package/src/serve/worker/host/firestore-reads.ts +117 -0
  396. package/src/serve/worker/host/firestore-writes.ts +456 -0
  397. package/src/serve/worker/host/presence.ts +312 -0
  398. package/src/serve/worker/host/rtdb.ts +136 -0
  399. package/src/serve/worker/host/rules.ts +128 -0
  400. package/src/serve/worker/host/storage.ts +312 -0
  401. package/src/serve/worker/host/studio.ts +75 -0
  402. package/src/serve/worker/host/subscriptions.ts +238 -0
  403. package/src/serve/worker/host-ai.ts +165 -0
  404. package/src/serve/worker/host-auth.ts +469 -0
  405. package/src/serve/worker/host-context.ts +273 -0
  406. package/src/serve/worker/host-events.ts +94 -0
  407. package/src/serve/worker/host-messaging.ts +240 -0
  408. package/src/serve/worker/host.ts +47 -0
  409. package/src/serve/worker/index.ts +196 -0
  410. package/src/serve/worker/presence-timing.ts +15 -0
  411. package/src/serve/worker/protocol.ts +1124 -0
  412. package/src/serve/worker/retirement.ts +125 -0
  413. package/src/serve/worker/serve-init.ts +605 -0
  414. package/src/serve/worker/service-worker-channel.ts +33 -0
  415. package/src/serve/worker/service-worker-relay.ts +86 -0
  416. package/src/serve/writer-lock.ts +53 -0
  417. package/src/verify/cases.ts +233 -0
  418. package/src/verify/fixture.ts +258 -0
  419. package/src/verify/index.ts +519 -0
  420. package/src/verify/tools.ts +108 -0
  421. package/src/version/compat-target.ts +17 -0
  422. package/src/vite.ts +18 -0
  423. package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api/index.html +0 -31
  424. package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api.md +0 -29
@@ -0,0 +1,1050 @@
1
+ /**
2
+ * `connectRemoteSandbox()` — Node-side client for the browser-hosted
3
+ * SharedWorker sandbox (remote sandbox, slice 1 / checkpoint 1).
4
+ *
5
+ * Server-side Node code (ultimately `pyric-admin`'s remote dispatch arm)
6
+ * reaches the ONE sandbox the app + Studio + agent share:
7
+ *
8
+ * Node process ──ws──> `pyric dev --bridge` ──ws──> browser tab
9
+ * (attach/consumer leg) (peer leg)
10
+ * ──MessagePort──> SharedWorker
11
+ *
12
+ * DISCOVERY mirrors `pyric mcp-proxy` (`serve/discovery.ts`): the
13
+ * `.pyric/serve.json` pointer in cwd, health-probed across both loopback
14
+ * families with the pointer's `instanceId` pinned so a cross-family
15
+ * port-squatter can't split-brain the connection.
16
+ *
17
+ * FAIL-FAST, NEVER FALL BACK: when no browser tab is connected the connect
18
+ * (and every later op that races a closing tab) fails with a clear
19
+ * "open <serve url>" error. There is deliberately NO silent headless
20
+ * fallback — a silently split backend is exactly the failure this client
21
+ * exists to avoid.
22
+ *
23
+ * SUBSCRIPTION OWNERSHIP: the bridge process owns the durable sub registry
24
+ * and re-issues it to every newly registered peer (tab refresh/replacement),
25
+ * so a subscription made here survives peer churn — RTDB value semantics
26
+ * make the replay safe (each resubscribe re-delivers a fresh snapshot;
27
+ * consumers are last-value-wins).
28
+ *
29
+ * NODE-ONLY: uses `ws` and `node:fs` — exported via a `node`-conditional
30
+ * subpath (`@pyric/cli/remote`), never bundled for the browser.
31
+ */
32
+ import { WebSocket } from 'ws';
33
+ import type { AuthUserRecord, CreateUserRequest, UpdateUserRequest } from 'pyric/auth';
34
+ import type { FullMetadata } from 'pyric/storage';
35
+ import {
36
+ REMOTE_SANDBOX,
37
+ SandboxContextImpl,
38
+ type RemoteSandbox as RemoteSandboxBase,
39
+ type AuthState,
40
+ type SandboxContext,
41
+ } from 'pyric/sandbox';
42
+ import type {
43
+ BridgeMessage,
44
+ WorkerOpPayload,
45
+ WorkerSubPayload,
46
+ } from '../bridge/protocol.js';
47
+ import { isBridgeMessage, NO_SANDBOX_ERROR_MESSAGE } from '../bridge/protocol.js';
48
+ import { cliVersion } from '../pkg-version.js';
49
+ import { MAX_STORAGE_OP_BYTES, storagePayloadTooLarge } from '../serve/worker/protocol.js';
50
+ import { discoverServe } from '../serve/discovery.js';
51
+
52
+ /** Sits just above the bridge's own 30s `callTimeoutMs` so a legitimately
53
+ * slow worker op still completes (same layering as `mcp-proxy`'s 35s). */
54
+ const DEFAULT_OP_TIMEOUT_MS = 35_000;
55
+
56
+ /** How long the attach handshake may take before connect() fails. */
57
+ const ATTACH_TIMEOUT_MS = 5_000;
58
+
59
+ function remoteError(
60
+ code: string,
61
+ message: string,
62
+ denialContext?: unknown,
63
+ ): Error & { code: string; denialContext?: unknown } {
64
+ const err = new Error(message) as Error & { code: string; denialContext?: unknown };
65
+ err.code = code;
66
+ // Structured denial context (spike gap 6): a relayed permission-denied
67
+ // carries the same "why did this deny" frame a local SandboxError has —
68
+ // plain JSON off the wire, re-attached here.
69
+ if (denialContext !== undefined) err.denialContext = denialContext;
70
+ return err;
71
+ }
72
+
73
+ function noTabError(serveUrl: string): Error & { code: string } {
74
+ return remoteError(
75
+ 'unavailable',
76
+ `no browser tab is connected to the sandbox — open ${serveUrl} in a browser and retry.`,
77
+ );
78
+ }
79
+
80
+ // ─── Public types ──────────────────────────────────────────────────────────
81
+
82
+ export interface ConnectRemoteSandboxOptions {
83
+ /** Project root for `.pyric/serve.json` discovery. Default: `process.cwd()`. */
84
+ cwd?: string;
85
+ /** Explicit serve base URL (e.g. `http://127.0.0.1:5000`) — skips discovery. */
86
+ url?: string;
87
+ /** Per-op timeout in ms on the Node side (default 35s, above the bridge's 30s). */
88
+ opTimeoutMs?: number;
89
+ }
90
+
91
+ /**
92
+ * The raw relay channel: any worker-protocol op or snap-delivering
93
+ * subscription, verbatim. The typed conveniences below are built on this;
94
+ * checkpoint 2's `pyric-admin` remote-dispatch arm consumes it directly.
95
+ */
96
+ export interface RemoteSandboxChannel {
97
+ /** Dispatch one worker op. Resolves with the worker's `res.value`;
98
+ * rejects with an Error carrying `.code`. NOTE: callers choose their own
99
+ * `actAs` lens — nothing is pinned here. */
100
+ op(op: WorkerOpPayload): Promise<unknown>;
101
+ /**
102
+ * Register a worker subscription. `onSnap` receives each snap value;
103
+ * an establishment failure (the worker host's `{ __error }` snap) routes
104
+ * to `onError` instead. Returns the unsubscribe function.
105
+ */
106
+ subscribe(
107
+ sub: WorkerSubPayload,
108
+ onSnap: (value: unknown) => void,
109
+ onError?: (err: Error & { code: string }) => void,
110
+ ): () => void;
111
+ }
112
+
113
+ /** Wire shape of an RTDB snapshot as the worker host serializes it. */
114
+ export interface RemoteRtdbSnapshot {
115
+ key: string | null;
116
+ exists: boolean;
117
+ value: unknown;
118
+ size: number;
119
+ }
120
+
121
+ /**
122
+ * Thin RTDB conveniences over the channel. Every call pins
123
+ * `actAs: { mode: 'admin' }` — firebase-admin's rules-bypass semantics,
124
+ * matching what `pyric-admin`'s database backend needs. Use the raw
125
+ * `channel` for lensed (rules-evaluated) access.
126
+ */
127
+ export interface RemoteRtdb {
128
+ /** Read the value at `path` (null when absent). */
129
+ get(path: string): Promise<unknown>;
130
+ set(path: string, value: unknown): Promise<void>;
131
+ update(path: string, values: Record<string, unknown>): Promise<void>;
132
+ remove(path: string): Promise<void>;
133
+ /** Push `value` under a CLIENT-minted 20-char push id (the worker-protocol
134
+ * contract: `rtdb.push` carries the key, so `.key` is known synchronously
135
+ * on the pyric-admin side). Resolves with the minted key + full path. */
136
+ push(path: string, value?: unknown): Promise<{ key: string; path: string }>;
137
+ /** Subscribe to the value at `path` (initial snapshot + every change). */
138
+ onValue(
139
+ path: string,
140
+ callback: (snapshot: RemoteRtdbSnapshot) => void,
141
+ onError?: (err: Error & { code: string }) => void,
142
+ ): () => void;
143
+ }
144
+
145
+ /**
146
+ * Thin Storage conveniences over the channel — the byte-carrying base64 ops
147
+ * plus browse/metadata. Every call pins `actAs: { mode: 'admin' }`
148
+ * (firebase-admin's rules-bypass semantics, matching {@link RemoteRtdb});
149
+ * use the raw `channel` for lensed (rules-evaluated) access. Bytes are
150
+ * capped at 8 MiB raw ({@link MAX_STORAGE_OP_BYTES}) on both ends —
151
+ * streaming transfers are not supported on the sandbox backend.
152
+ */
153
+ export interface RemoteStorage {
154
+ /** Upload `data` at `path` (replaces any existing object). Resolves with
155
+ * the stored object's `FullMetadata`. */
156
+ putBytes(
157
+ path: string,
158
+ data: Uint8Array,
159
+ options?: { contentType?: string; metadata?: Record<string, unknown> },
160
+ ): Promise<FullMetadata>;
161
+ /** Download the object's bytes. Rejects `storage/object-not-found` when
162
+ * absent, `payload-too-large` when over the op cap. */
163
+ getBytes(path: string): Promise<Buffer>;
164
+ /** Delete the object at `path`. Idempotent (missing = no-op). */
165
+ deleteObject(path: string): Promise<void>;
166
+ /** Read the object's `FullMetadata`. */
167
+ getMetadata(path: string): Promise<FullMetadata>;
168
+ /** Does an object exist at `path`? (`getMetadata` with not-found → false.) */
169
+ exists(path: string): Promise<boolean>;
170
+ /** Enumerate immediate child items + prefixes under `path`. */
171
+ listAll(path: string): Promise<{
172
+ items: Array<{ fullPath: string; name: string }>;
173
+ prefixes: Array<{ fullPath: string; name: string }>;
174
+ }>;
175
+ }
176
+
177
+ /** Admin auth user-CRUD passthrough (never lensed — auth ops operate the
178
+ * worker's user pool directly, mirroring `pyric/auth`'s sandbox ops). */
179
+ export interface RemoteAuthAdmin {
180
+ listUsers(): Promise<AuthUserRecord[]>;
181
+ createUser(request: CreateUserRequest): Promise<AuthUserRecord>;
182
+ updateUser(uid: string, request: UpdateUserRequest): Promise<AuthUserRecord>;
183
+ deleteUser(uid: string): Promise<void>;
184
+ clearUsers(): Promise<void>;
185
+ }
186
+
187
+ /**
188
+ * The Node-side remote sandbox handle. Extends `pyric/sandbox`'s branded
189
+ * {@link RemoteSandboxBase} — structurally a full `Sandbox`, so it can be
190
+ * passed to `pyric-admin/app`'s `initializeApp({ sandbox })`, whose RTDB and
191
+ * Auth backends dispatch on the brand and route through {@link channel}.
192
+ *
193
+ * Sandbox members that are genuinely sync-only (`admin`, `snapshot()`,
194
+ * `history()`, `onEvent`, `currentUser`, …) cannot be mirrored over the
195
+ * wire in slice 1 and throw a remediating `unimplemented` error naming
196
+ * what to do instead. Implemented members: `withAuth` (pure local pair
197
+ * construction) and `dispose` (aliases {@link close}).
198
+ */
199
+ export interface RemoteSandbox extends RemoteSandboxBase {
200
+ /** The raw worker op/sub relay channel (narrowed to the wire payload types). */
201
+ readonly channel: RemoteSandboxChannel;
202
+ /** RTDB conveniences (admin lens pinned). */
203
+ readonly rtdb: RemoteRtdb;
204
+ /** Storage conveniences (admin lens pinned; 8 MiB per-op byte cap). */
205
+ readonly storage: RemoteStorage;
206
+ /** Admin auth user CRUD. */
207
+ readonly auth: RemoteAuthAdmin;
208
+ /** Close the connection. In-flight ops reject; subscriptions stop. */
209
+ close(): void;
210
+ }
211
+
212
+ // ─── Core (transport-injected — the test seam) ─────────────────────────────
213
+
214
+ /** Minimal transport the core writes to. `connectRemoteSandbox` adapts a
215
+ * `ws` socket; tests inject an in-process pipe to a `ConsumerSession`. */
216
+ export interface RemoteTransport {
217
+ send(msg: BridgeMessage): void;
218
+ /**
219
+ * OPTIONAL event-loop hold hooks (exit-hang fix). The WS adapter unrefs
220
+ * its socket once connected so an IDLE remote client never pins the Node
221
+ * event loop (a finished script exits); the core calls `ref()` when work
222
+ * becomes outstanding (first pending op / live subscription) and
223
+ * `unref()` when the last one settles, so in-flight delivery keeps the
224
+ * process alive. Pure in-process transports (tests) may omit both.
225
+ */
226
+ ref?(): void;
227
+ unref?(): void;
228
+ }
229
+
230
+ export interface RemoteSandboxCore {
231
+ /** Feed one parsed message from the transport into the core. */
232
+ handleMessage(msg: BridgeMessage): void;
233
+ /** Send the attach handshake. `ready` settles on the ack. */
234
+ start(): void;
235
+ /** Resolves on `attach-ack`; rejects when no browser tab is connected. */
236
+ ready: Promise<void>;
237
+ channel: RemoteSandboxChannel;
238
+ /** Fail everything in flight (transport closed). Idempotent. */
239
+ dispose(reason?: string): void;
240
+ }
241
+
242
+ /**
243
+ * Transport-agnostic client core: correlation ids/subIds are minted HERE
244
+ * (this leg's id space; the bridge re-mints for the peer leg), pending ops
245
+ * carry a Node-side timeout above the bridge's 30s, and `{ __error }` snap
246
+ * values are routed to the subscription's error handler.
247
+ */
248
+ export function createRemoteSandboxCore(
249
+ transport: RemoteTransport,
250
+ opts: { serveUrl: string; opTimeoutMs?: number },
251
+ ): RemoteSandboxCore {
252
+ const serveUrl = opts.serveUrl;
253
+ const opTimeoutMs = opts.opTimeoutMs ?? DEFAULT_OP_TIMEOUT_MS;
254
+
255
+ let opCounter = 0;
256
+ let subCounter = 0;
257
+ let disposed: string | null = null;
258
+
259
+ const pending = new Map<
260
+ string,
261
+ { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }
262
+ >();
263
+ const subs = new Map<
264
+ string,
265
+ { onSnap: (value: unknown) => void; onError?: (err: Error & { code: string }) => void }
266
+ >();
267
+
268
+ let readyResolve!: () => void;
269
+ let readyReject!: (e: Error) => void;
270
+ const ready = new Promise<void>((resolve, reject) => {
271
+ readyResolve = resolve;
272
+ readyReject = reject;
273
+ });
274
+ // `ready` may legitimately go unobserved after dispose — never unhandled.
275
+ ready.catch(() => {});
276
+
277
+ /**
278
+ * Version-skew guidance (integration-smoke fix). Set once when the
279
+ * `attach-ack`'s `serveVersion` stamp is present AND differs from this
280
+ * client's own @pyric/cli version: an old worker can accept a newer op
281
+ * frame and die mid-handling, which surfaces as a bare timeout — so the
282
+ * mismatch warns ONCE on stderr at attach, and op-timeout errors append
283
+ * the same guidance. Old servers omit the stamp → stays null → silent.
284
+ */
285
+ let versionSkewGuidance: string | null = null;
286
+
287
+ function send(msg: BridgeMessage): void {
288
+ transport.send(msg);
289
+ }
290
+
291
+ /**
292
+ * Event-loop hold accounting (exit-hang fix): ref the transport while ANY
293
+ * op or subscription is outstanding, unref when the last one settles.
294
+ * Transition-edged so the hooks fire once per busy/idle flip.
295
+ */
296
+ let holdingLoop = false;
297
+ function updateLoopHold(): void {
298
+ const busy = pending.size + subs.size > 0;
299
+ if (busy && !holdingLoop) {
300
+ holdingLoop = true;
301
+ transport.ref?.();
302
+ } else if (!busy && holdingLoop) {
303
+ holdingLoop = false;
304
+ transport.unref?.();
305
+ }
306
+ }
307
+
308
+ function op(payload: WorkerOpPayload): Promise<unknown> {
309
+ if (disposed) {
310
+ return Promise.reject(remoteError('unavailable', disposed));
311
+ }
312
+ return new Promise<unknown>((resolve, reject) => {
313
+ const id = `rop-${++opCounter}`;
314
+ const timer = setTimeout(() => {
315
+ if (pending.has(id)) {
316
+ pending.delete(id);
317
+ updateLoopHold();
318
+ reject(
319
+ remoteError(
320
+ 'deadline-exceeded',
321
+ `remote sandbox op timed out after ${opTimeoutMs}ms (op: ${payload.method}) — is pyric dev still running?` +
322
+ // A version-skewed old worker accepts frames it cannot
323
+ // handle and never responds — a timeout is its signature.
324
+ (versionSkewGuidance ? ` ${versionSkewGuidance}` : ''),
325
+ ),
326
+ );
327
+ }
328
+ }, opTimeoutMs);
329
+ pending.set(id, { resolve, reject, timer });
330
+ updateLoopHold();
331
+ try {
332
+ send({ type: 'worker-op', id, op: payload });
333
+ } catch (err) {
334
+ clearTimeout(timer);
335
+ pending.delete(id);
336
+ updateLoopHold();
337
+ reject(
338
+ remoteError(
339
+ 'unavailable',
340
+ `failed to send op to serve: ${err instanceof Error ? err.message : String(err)}`,
341
+ ),
342
+ );
343
+ }
344
+ });
345
+ }
346
+
347
+ function subscribe(
348
+ sub: WorkerSubPayload,
349
+ onSnap: (value: unknown) => void,
350
+ onError?: (err: Error & { code: string }) => void,
351
+ ): () => void {
352
+ if (disposed) throw remoteError('unavailable', disposed);
353
+ const subId = `rsub-${++subCounter}`;
354
+ subs.set(subId, { onSnap, onError });
355
+ updateLoopHold();
356
+ send({ type: 'worker-sub', subId, sub });
357
+ return () => {
358
+ if (!subs.delete(subId)) return;
359
+ updateLoopHold();
360
+ if (!disposed) {
361
+ try {
362
+ send({ type: 'worker-unsub', subId });
363
+ } catch {}
364
+ }
365
+ };
366
+ }
367
+
368
+ function handleMessage(msg: BridgeMessage): void {
369
+ if (!isBridgeMessage(msg)) return;
370
+ switch (msg.type) {
371
+ case 'attach-ack': {
372
+ // Version-skew stamp: warn ONCE when the serve process runs a
373
+ // different @pyric/cli version (absent stamp = old server = silent).
374
+ if (
375
+ versionSkewGuidance === null &&
376
+ typeof msg.serveVersion === 'string' &&
377
+ msg.serveVersion !== cliVersion()
378
+ ) {
379
+ versionSkewGuidance =
380
+ `pyric dev is running version ${msg.serveVersion}, this client is ` +
381
+ `${cliVersion()} — restart pyric dev and reload the browser tab.`;
382
+ process.stderr.write(`pyric: ${versionSkewGuidance}\n`);
383
+ }
384
+ if (msg.peerConnected) readyResolve();
385
+ else readyReject(noTabError(serveUrl));
386
+ return;
387
+ }
388
+ case 'worker-res': {
389
+ const call = pending.get(msg.id);
390
+ if (!call) return; // late (already timed out) — drop
391
+ clearTimeout(call.timer);
392
+ pending.delete(msg.id);
393
+ updateLoopHold();
394
+ if (msg.ok) {
395
+ call.resolve(msg.value);
396
+ } else {
397
+ const code = msg.error?.code ?? 'unknown';
398
+ let message = msg.error?.message ?? 'unknown sandbox error';
399
+ // Enrich the bridge's generic no-peer error with actionable guidance.
400
+ if (message === NO_SANDBOX_ERROR_MESSAGE) {
401
+ message = noTabError(serveUrl).message;
402
+ } else if (/^Unknown method:/.test(message)) {
403
+ // Version skew: a live tab whose SharedWorker predates this op.
404
+ // Other open pages of this origin keep the old worker alive.
405
+ message +=
406
+ ' — the running sandbox may predate this feature; restart pyric dev ' +
407
+ 'and close other open pages of this origin, then reload.';
408
+ }
409
+ call.reject(remoteError(code, message, msg.error?.denialContext));
410
+ }
411
+ return;
412
+ }
413
+ case 'worker-snap': {
414
+ const sub = subs.get(msg.subId);
415
+ if (!sub) return; // unsubscribed — drop
416
+ const value = (msg.value ?? {}) as Record<string, unknown>;
417
+ if (value.__error) {
418
+ // A listener error is TERMINAL (Firestore's onSnapshot contract:
419
+ // after onError, no further snapshots and the listener is dead).
420
+ // Auto-unsubscribe BEFORE delivering: drop the local record (so a
421
+ // consumer's own unsubscribe becomes an idempotent no-op and the
422
+ // event-loop hold releases — an errored sub must not pin the
423
+ // process forever) and tell the worker to tear down whatever
424
+ // listener it may have registered (harmless no-op for a
425
+ // registration failure that never registered one).
426
+ subs.delete(msg.subId);
427
+ updateLoopHold();
428
+ if (!disposed) {
429
+ try {
430
+ send({ type: 'worker-unsub', subId: msg.subId });
431
+ } catch {}
432
+ }
433
+ const payload = value.__error as { code: string; message: string; denialContext?: unknown };
434
+ const err = remoteError(payload.code, payload.message, payload.denialContext);
435
+ if (sub.onError) sub.onError(err);
436
+ else console.error('pyric remote sandbox: uncaught error in subscription:', err);
437
+ return;
438
+ }
439
+ try {
440
+ sub.onSnap(msg.value);
441
+ } catch {
442
+ // Consumer callback failures must not break the relay loop.
443
+ }
444
+ return;
445
+ }
446
+ case 'ping': {
447
+ send({ type: 'pong', id: msg.id });
448
+ return;
449
+ }
450
+ default:
451
+ return;
452
+ }
453
+ }
454
+
455
+ function dispose(reason?: string): void {
456
+ if (disposed) return;
457
+ disposed = reason ?? 'remote sandbox connection closed';
458
+ const err = remoteError('unavailable', disposed);
459
+ for (const call of pending.values()) {
460
+ clearTimeout(call.timer);
461
+ call.reject(err);
462
+ }
463
+ pending.clear();
464
+ subs.clear();
465
+ updateLoopHold();
466
+ readyReject(err); // no-op if already settled
467
+ }
468
+
469
+ return {
470
+ handleMessage,
471
+ start: () => send({ type: 'attach', protocol: 1 }),
472
+ ready,
473
+ channel: { op, subscribe },
474
+ dispose,
475
+ };
476
+ }
477
+
478
+ // ─── Typed conveniences over the raw channel ───────────────────────────────
479
+
480
+ /** firebase-admin's rules-bypass lens, pinned on every RTDB convenience op. */
481
+ const ADMIN_LENS = { mode: 'admin' } as const;
482
+
483
+ export function buildRemoteRtdb(channel: RemoteSandboxChannel): RemoteRtdb {
484
+ return {
485
+ async get(path) {
486
+ const snap = (await channel.op({
487
+ method: 'rtdb.get',
488
+ path,
489
+ actAs: ADMIN_LENS,
490
+ })) as RemoteRtdbSnapshot;
491
+ return snap.value ?? null;
492
+ },
493
+ async set(path, value) {
494
+ await channel.op({ method: 'rtdb.set', path, value, actAs: ADMIN_LENS });
495
+ },
496
+ async update(path, values) {
497
+ await channel.op({ method: 'rtdb.update', path, values, actAs: ADMIN_LENS });
498
+ },
499
+ async remove(path) {
500
+ await channel.op({ method: 'rtdb.remove', path, actAs: ADMIN_LENS });
501
+ },
502
+ async push(path, value) {
503
+ const key = generatePushId();
504
+ const res = (await channel.op({
505
+ method: 'rtdb.push',
506
+ path,
507
+ key,
508
+ ...(value !== undefined ? { value } : {}),
509
+ actAs: ADMIN_LENS,
510
+ })) as { key: string; path: string };
511
+ return res;
512
+ },
513
+ onValue(path, callback, onError) {
514
+ return channel.subscribe(
515
+ { target: { service: 'rtdb', path }, actAs: ADMIN_LENS },
516
+ (value) => callback(value as RemoteRtdbSnapshot),
517
+ onError,
518
+ );
519
+ },
520
+ };
521
+ }
522
+
523
+ export function buildRemoteStorage(channel: RemoteSandboxChannel): RemoteStorage {
524
+ return {
525
+ async putBytes(path, data, options) {
526
+ // Client-side cap: reject BEFORE encoding/sending so an oversized
527
+ // payload never hits the wire (the host enforces the same cap on
528
+ // decode — belt and braces across the relay).
529
+ if (data.byteLength > MAX_STORAGE_OP_BYTES) {
530
+ throw storagePayloadTooLarge(data.byteLength, `storage payload for '${path}'`);
531
+ }
532
+ return (await channel.op({
533
+ method: 'storage.putBytes',
534
+ path,
535
+ dataB64: Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('base64'),
536
+ ...(options?.contentType !== undefined ? { contentType: options.contentType } : {}),
537
+ ...(options?.metadata !== undefined ? { metadata: options.metadata } : {}),
538
+ actAs: ADMIN_LENS,
539
+ })) as FullMetadata;
540
+ },
541
+ async getBytes(path) {
542
+ const res = (await channel.op({
543
+ method: 'storage.getBytes',
544
+ path,
545
+ actAs: ADMIN_LENS,
546
+ })) as { dataB64: string };
547
+ return Buffer.from(res.dataB64, 'base64');
548
+ },
549
+ async deleteObject(path) {
550
+ await channel.op({ method: 'storage.deleteObject', path, actAs: ADMIN_LENS });
551
+ },
552
+ async getMetadata(path) {
553
+ return (await channel.op({
554
+ method: 'storage.getMetadata',
555
+ path,
556
+ actAs: ADMIN_LENS,
557
+ })) as FullMetadata;
558
+ },
559
+ async exists(path) {
560
+ try {
561
+ await this.getMetadata(path);
562
+ return true;
563
+ } catch (err) {
564
+ if ((err as { code?: string }).code === 'storage/object-not-found') return false;
565
+ throw err;
566
+ }
567
+ },
568
+ async listAll(path) {
569
+ return (await channel.op({
570
+ method: 'storage.listAll',
571
+ path,
572
+ actAs: ADMIN_LENS,
573
+ })) as {
574
+ items: Array<{ fullPath: string; name: string }>;
575
+ prefixes: Array<{ fullPath: string; name: string }>;
576
+ };
577
+ },
578
+ };
579
+ }
580
+
581
+ export function buildRemoteAuthAdmin(channel: RemoteSandboxChannel): RemoteAuthAdmin {
582
+ return {
583
+ async listUsers() {
584
+ return (await channel.op({ method: 'auth.listUsers' })) as AuthUserRecord[];
585
+ },
586
+ async createUser(request) {
587
+ return (await channel.op({
588
+ method: 'auth.adminCreateUser',
589
+ request: request as Record<string, unknown>,
590
+ })) as AuthUserRecord;
591
+ },
592
+ async updateUser(uid, request) {
593
+ return (await channel.op({
594
+ method: 'auth.adminUpdateUser',
595
+ uid,
596
+ request: request as Record<string, unknown>,
597
+ })) as AuthUserRecord;
598
+ },
599
+ async deleteUser(uid) {
600
+ await channel.op({ method: 'auth.adminDeleteUser', uid });
601
+ },
602
+ async clearUsers() {
603
+ await channel.op({ method: 'auth.adminClearUsers' });
604
+ },
605
+ };
606
+ }
607
+
608
+ // ─── Handle construction ────────────────────────────────────────────────────
609
+
610
+ /** A member of `Sandbox` that slice 1 cannot express over the wire throws
611
+ * this branded, remediating error instead of silently misbehaving. */
612
+ function notAvailableRemotely(member: string, remedy: string): Error & { code: string } {
613
+ return remoteError(
614
+ 'unimplemented',
615
+ `${member} is not available on a remote sandbox — ${remedy}`,
616
+ );
617
+ }
618
+
619
+ /**
620
+ * Build the branded remote sandbox handle over an established channel.
621
+ *
622
+ * Split out of {@link connectRemoteSandbox} so the in-process test harness
623
+ * (fake ports + `createConsumerSession`, no WS) constructs the EXACT handle
624
+ * production hands to `pyric-admin`.
625
+ *
626
+ * The handle satisfies `Sandbox` structurally:
627
+ * - `withAuth` / `dispose` are real (pure local construction / teardown).
628
+ * - Everything whose contract is sync-only or worker-owned (`admin`,
629
+ * `snapshot`, `loadSnapshot`, `history`, `onEvent`, `reset`,
630
+ * `currentUser`, `onCurrentUserChanged`, tab sync, persistence) throws
631
+ * a remediating `unimplemented` error. Notably `onEvent`: the unified
632
+ * event stream (`target: 'events'`) is not relayable until slice 2's
633
+ * bounded backpressure lands, and no remote dispatch arm may depend on
634
+ * it — a throw (not a silent no-op) keeps a subscriber from believing
635
+ * it is observing events that will never arrive.
636
+ */
637
+ export function createRemoteSandboxHandle(opts: {
638
+ channel: RemoteSandboxChannel;
639
+ serveUrl: string;
640
+ close: () => void;
641
+ }): RemoteSandbox {
642
+ const { channel, serveUrl, close } = opts;
643
+ const throwMember = (member: string, remedy: string): never => {
644
+ throw notAvailableRemotely(member, remedy);
645
+ };
646
+ const handle: RemoteSandbox = {
647
+ [REMOTE_SANDBOX]: true as const,
648
+ serveUrl,
649
+ channel,
650
+ rtdb: buildRemoteRtdb(channel),
651
+ storage: buildRemoteStorage(channel),
652
+ auth: buildRemoteAuthAdmin(channel),
653
+ close,
654
+
655
+ // ── Implemented Sandbox members ─────────────────────────────────────
656
+ withAuth(auth: AuthState): SandboxContext {
657
+ // Pure local pair construction — contexts carry identity, the data
658
+ // stays behind the channel.
659
+ return new SandboxContextImpl(handle, auth);
660
+ },
661
+ dispose(): void {
662
+ close();
663
+ },
664
+
665
+ // ── Sync-only / worker-owned members: remediating throws ───────────
666
+ onEvent: () =>
667
+ throwMember(
668
+ 'onEvent',
669
+ 'the unified event stream is not relayed over the bridge yet (slice 2); ' +
670
+ `observe activity in the browser tab at ${serveUrl} or in Pyric Studio instead`,
671
+ ),
672
+ history: () =>
673
+ throwMember(
674
+ 'history()',
675
+ 'the event log lives in the browser worker; inspect it in Pyric Studio ' +
676
+ 'or subscribe from the page that hosts the sandbox',
677
+ ),
678
+ get admin(): never {
679
+ return throwMember(
680
+ 'admin',
681
+ 'the sync Firestore admin plane cannot span the wire; dispatch async worker ops ' +
682
+ "through the handle's channel instead (e.g. channel.op({ method: 'admin.getDocument', path }))",
683
+ );
684
+ },
685
+ reset: () =>
686
+ throwMember(
687
+ 'reset()',
688
+ `reset the sandbox from the browser tab at ${serveUrl} or from Pyric Studio`,
689
+ ),
690
+ resetAll: () =>
691
+ throwMember(
692
+ 'resetAll()',
693
+ "use the async worker op instead: channel.op({ method: 'resetAll' })",
694
+ ),
695
+ snapshot: () =>
696
+ throwMember(
697
+ 'snapshot()',
698
+ "use the async worker op instead: channel.op({ method: 'getSnapshot' })",
699
+ ),
700
+ loadSnapshot: () =>
701
+ throwMember(
702
+ 'loadSnapshot()',
703
+ "use the async worker ops instead: channel.op({ method: 'importState', bundle })",
704
+ ),
705
+ get currentUser(): never {
706
+ return throwMember(
707
+ 'currentUser',
708
+ 'the browser tab owns the auth session; ' +
709
+ "read it asynchronously via channel.op({ method: 'auth.getCurrentUser' })",
710
+ );
711
+ },
712
+ onCurrentUserChanged: () =>
713
+ throwMember(
714
+ 'onCurrentUserChanged',
715
+ 'the browser tab owns the auth session; subscribe there instead',
716
+ ),
717
+ enableTabSync: () =>
718
+ throwMember('enableTabSync', 'tab sync is a browser concern; enable it in the page'),
719
+ enablePersistence: () =>
720
+ throwMember(
721
+ 'enablePersistence',
722
+ 'the browser worker owns persistence; it is already enabled by `pyric dev`',
723
+ ),
724
+ flush: () =>
725
+ throwMember('flush()', 'the browser worker owns persistence and flushes itself'),
726
+ clearPersistence: () =>
727
+ throwMember(
728
+ 'clearPersistence()',
729
+ 'clear the persisted state from the browser tab or Pyric Studio',
730
+ ),
731
+ registerPersistableService: () =>
732
+ throwMember(
733
+ 'registerPersistableService',
734
+ 'the browser worker owns persistence; services register there',
735
+ ),
736
+ };
737
+ return handle;
738
+ }
739
+
740
+ // ─── connect ───────────────────────────────────────────────────────────────
741
+
742
+ /**
743
+ * Discover the running `pyric dev --bridge`, attach to its bridge WS as a
744
+ * worker-relay CONSUMER (never a peer — attaching cannot kick the browser
745
+ * tab out of last-connection-wins), and return the typed remote handle.
746
+ *
747
+ * Fails fast when no serve is discoverable or no browser tab is connected —
748
+ * there is deliberately no headless fallback (see module doc).
749
+ */
750
+ export async function connectRemoteSandbox(
751
+ options: ConnectRemoteSandboxOptions = {},
752
+ ): Promise<RemoteSandbox> {
753
+ const cwd = options.cwd ?? process.cwd();
754
+
755
+ // Two URLs on purpose: `serveUrl` is the CANONICAL display URL that lands in
756
+ // "open <url>" guidance — it MUST match the origin serve's banner/auto-open
757
+ // use (`http://localhost:<port>` for the default host), or a user following
758
+ // the guidance opens a DIFFERENT browser origin (127.0.0.1 vs localhost →
759
+ // separate SharedWorkers → split sandboxes). `wsBase` is connectivity: the
760
+ // literal loopback family the health probe actually reached.
761
+ let serveUrl: string;
762
+ let wsBase: string;
763
+ if (options.url) {
764
+ serveUrl = options.url.replace(/\/$/, '');
765
+ wsBase = serveUrl;
766
+ } else {
767
+ const found = await discoverServe(cwd);
768
+ if (!found) {
769
+ throw remoteError(
770
+ 'not-found',
771
+ 'no running `pyric dev --bridge` found (looked for .pyric/serve.json in ' +
772
+ `${cwd} and the default ports) — start your dev server with the bridge enabled and retry.`,
773
+ );
774
+ }
775
+ serveUrl = found.url;
776
+ wsBase = found.base;
777
+ }
778
+
779
+ const wsUrl = `${wsBase.replace(/^http/, 'ws')}/__pyric/sandbox`;
780
+ const ws = new WebSocket(wsUrl);
781
+
782
+ // Event-loop hold (exit-hang fix): `ws` exposes no ref/unref of its own —
783
+ // reach the underlying net.Socket (present once connected). Unref'ing only
784
+ // changes loop-exit accounting, never delivery: while ANY pending op or
785
+ // live subscription holds a ref (the core's updateLoopHold), frames flow
786
+ // normally; when idle, a finished script exits instead of hanging.
787
+ const wsSocket = (): { ref(): void; unref(): void } | undefined =>
788
+ (ws as unknown as { _socket?: { ref(): void; unref(): void } })._socket;
789
+
790
+ const core = createRemoteSandboxCore(
791
+ {
792
+ send: (msg) => ws.send(JSON.stringify(msg)),
793
+ ref: () => wsSocket()?.ref(),
794
+ unref: () => wsSocket()?.unref(),
795
+ },
796
+ { serveUrl, opTimeoutMs: options.opTimeoutMs },
797
+ );
798
+
799
+ ws.on('message', (raw) => {
800
+ let msg: unknown;
801
+ try {
802
+ msg = JSON.parse(raw.toString());
803
+ } catch {
804
+ return;
805
+ }
806
+ if (isBridgeMessage(msg)) core.handleMessage(msg);
807
+ });
808
+ ws.on('close', () => core.dispose('remote sandbox connection closed (serve stopped or connection lost)'));
809
+ ws.on('error', (err) => core.dispose(`remote sandbox connection failed: ${err.message}`));
810
+
811
+ await new Promise<void>((resolve, reject) => {
812
+ const timer = setTimeout(
813
+ () => reject(remoteError('deadline-exceeded', `timed out connecting to ${wsUrl}`)),
814
+ ATTACH_TIMEOUT_MS,
815
+ );
816
+ ws.once('open', () => {
817
+ clearTimeout(timer);
818
+ // Idle default: an open-but-idle connection must not pin the event
819
+ // loop (both the eager and lazy `remoteSandbox` paths come through
820
+ // here). The core re-refs while ops/subs are outstanding.
821
+ wsSocket()?.unref();
822
+ resolve();
823
+ });
824
+ ws.once('error', (err) => {
825
+ clearTimeout(timer);
826
+ reject(remoteError('unavailable', `failed to connect to ${wsUrl}: ${err.message}`));
827
+ });
828
+ });
829
+
830
+ core.start();
831
+ try {
832
+ await withTimeout(core.ready, ATTACH_TIMEOUT_MS, `timed out attaching to the bridge at ${wsUrl}`);
833
+ } catch (err) {
834
+ try {
835
+ ws.close();
836
+ } catch {}
837
+ throw err;
838
+ }
839
+
840
+ return createRemoteSandboxHandle({
841
+ serveUrl,
842
+ channel: core.channel,
843
+ close() {
844
+ core.dispose('remote sandbox connection closed by the client');
845
+ try {
846
+ ws.close();
847
+ } catch {}
848
+ },
849
+ });
850
+ }
851
+
852
+ // ─── Lazy connect (`remoteSandbox`) ────────────────────────────────────────
853
+
854
+ /**
855
+ * {@link remoteSandbox}'s return type: the branded handle plus `ready` for
856
+ * eager checkers. `ready` kicks off the connection when first accessed and
857
+ * settles with the same fail-fast errors {@link connectRemoteSandbox} throws
858
+ * (no serve discovered / no browser tab connected).
859
+ */
860
+ export interface LazyRemoteSandbox extends RemoteSandbox {
861
+ readonly ready: Promise<void>;
862
+ }
863
+
864
+ /**
865
+ * Synchronous construction, lazy connection — the ambient-init seam.
866
+ *
867
+ * `@pyric/cli/register` installs this behind the
868
+ * `Symbol.for('pyric.remote.sandboxFactory')` global so `pyric-admin`'s bare
869
+ * `initializeApp()` can mint a full branded handle without awaiting anything.
870
+ * The wire connection (discovery → WS attach) happens on the FIRST op (or
871
+ * `ready` access), so the existing fail-fast — "no browser tab is connected —
872
+ * open <url>" — surfaces on first use instead of at construction. A failed
873
+ * connect is NOT latched: the next op retries, matching the error's own
874
+ * "…and retry" guidance. `connectRemoteSandbox` (eager) is unchanged.
875
+ */
876
+ export function remoteSandbox(options: ConnectRemoteSandboxOptions = {}): LazyRemoteSandbox {
877
+ return createLazyRemoteSandbox(() => connectRemoteSandbox(options), options);
878
+ }
879
+
880
+ /**
881
+ * The lazy wrapper with the connect function injected — the test seam
882
+ * (tests inject a fake connect; production injects `connectRemoteSandbox`).
883
+ */
884
+ export function createLazyRemoteSandbox(
885
+ connect: () => Promise<RemoteSandbox>,
886
+ options: { url?: string } = {},
887
+ ): LazyRemoteSandbox {
888
+ let inner: Promise<RemoteSandbox> | null = null;
889
+ let closed = false;
890
+
891
+ function ensure(): Promise<RemoteSandbox> {
892
+ if (closed) {
893
+ return Promise.reject(
894
+ remoteError('unavailable', 'remote sandbox connection closed by the client'),
895
+ );
896
+ }
897
+ if (!inner) {
898
+ inner = connect().then(
899
+ (h) => {
900
+ if (closed) {
901
+ h.close();
902
+ throw remoteError('unavailable', 'remote sandbox connection closed by the client');
903
+ }
904
+ // Discovery resolved the real URL — reflect it on the outer handle.
905
+ (handle as { serveUrl: string }).serveUrl = h.serveUrl;
906
+ return h;
907
+ },
908
+ (err) => {
909
+ // Fail-fast surfaces on this op, but don't latch: the remediation
910
+ // is "open <url> and retry", so the next op re-attempts the connect.
911
+ inner = null;
912
+ throw err;
913
+ },
914
+ );
915
+ }
916
+ return inner;
917
+ }
918
+
919
+ const channel: RemoteSandboxChannel = {
920
+ op: (payload) => ensure().then((h) => h.channel.op(payload)),
921
+ subscribe(sub, onSnap, onError) {
922
+ let cancelled = false;
923
+ let innerUnsub: (() => void) | null = null;
924
+ ensure().then(
925
+ (h) => {
926
+ if (cancelled) return;
927
+ innerUnsub = h.channel.subscribe(sub, onSnap, onError);
928
+ },
929
+ (err) => {
930
+ if (cancelled) return;
931
+ const e =
932
+ err instanceof Error && 'code' in err
933
+ ? (err as Error & { code: string })
934
+ : remoteError('unavailable', String(err));
935
+ if (onError) onError(e);
936
+ else console.error('pyric remote sandbox: subscription failed to connect:', e);
937
+ },
938
+ );
939
+ return () => {
940
+ cancelled = true;
941
+ if (innerUnsub) {
942
+ innerUnsub();
943
+ innerUnsub = null;
944
+ }
945
+ };
946
+ },
947
+ };
948
+
949
+ const handle = createRemoteSandboxHandle({
950
+ channel,
951
+ // Before discovery the URL is unknown; op-level errors always carry the
952
+ // real URL (they come from the inner connect), and `serveUrl` is patched
953
+ // to the discovered value as soon as the first connect succeeds.
954
+ serveUrl: options.url?.replace(/\/$/, '') ?? '<pyric dev url (pending discovery)>',
955
+ close() {
956
+ if (closed) return;
957
+ closed = true;
958
+ const settled = inner;
959
+ inner = null;
960
+ if (settled) {
961
+ settled.then(
962
+ (h) => h.close(),
963
+ () => {},
964
+ );
965
+ }
966
+ },
967
+ }) as RemoteSandbox & { ready: Promise<void> };
968
+
969
+ // `ready` — the eager checker: accessing it starts the connection. Marked
970
+ // handled (same pattern as the core's `ready`) so a mere existence check
971
+ // (`handle.ready instanceof Promise`) never surfaces an unhandled rejection.
972
+ Object.defineProperty(handle, 'ready', {
973
+ get: (): Promise<void> => {
974
+ const p = ensure().then(() => undefined);
975
+ p.catch(() => {});
976
+ return p;
977
+ },
978
+ enumerable: true,
979
+ configurable: true,
980
+ });
981
+
982
+ return handle as LazyRemoteSandbox;
983
+ }
984
+
985
+ function withTimeout<T>(p: Promise<T>, ms: number, message: string): Promise<T> {
986
+ return new Promise<T>((resolve, reject) => {
987
+ const timer = setTimeout(() => reject(remoteError('deadline-exceeded', message)), ms);
988
+ p.then(
989
+ (v) => {
990
+ clearTimeout(timer);
991
+ resolve(v);
992
+ },
993
+ (e) => {
994
+ clearTimeout(timer);
995
+ reject(e);
996
+ },
997
+ );
998
+ });
999
+ }
1000
+
1001
+ // ─── RTDB push-id generator ────────────────────────────────────────────────
1002
+ // Client-minted push ids (the worker-protocol contract: `rtdb.push` carries
1003
+ // the key so `.key` is synchronously known to callers). Standard Firebase
1004
+ // algorithm — 8 chars of timestamp + 12 random, monotonic within one ms.
1005
+ // Inlined (like pyric-admin does) because pyric doesn't export its generator.
1006
+
1007
+ const PUSH_CHARS =
1008
+ '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz';
1009
+
1010
+ let lastPushTime = 0;
1011
+ const lastRandChars: number[] = new Array(12).fill(0);
1012
+
1013
+ function generatePushId(now: number = Date.now()): string {
1014
+ const duplicateTime = now === lastPushTime;
1015
+ lastPushTime = now;
1016
+
1017
+ const timeStampChars: string[] = new Array(8);
1018
+ let ts = now;
1019
+ for (let i = 7; i >= 0; i--) {
1020
+ timeStampChars[i] = PUSH_CHARS.charAt(ts % 64);
1021
+ ts = Math.floor(ts / 64);
1022
+ }
1023
+ if (ts !== 0) {
1024
+ throw new Error('RTDB push-id: timestamp overflow.');
1025
+ }
1026
+ let id = timeStampChars.join('');
1027
+
1028
+ if (!duplicateTime) {
1029
+ for (let i = 0; i < 12; i++) {
1030
+ lastRandChars[i] = Math.floor(Math.random() * 64);
1031
+ }
1032
+ } else {
1033
+ let i: number;
1034
+ for (i = 11; i >= 0 && lastRandChars[i] === 63; i--) {
1035
+ lastRandChars[i] = 0;
1036
+ }
1037
+ if (i < 0) {
1038
+ for (let j = 0; j < 12; j++) {
1039
+ lastRandChars[j] = Math.floor(Math.random() * 64);
1040
+ }
1041
+ } else {
1042
+ lastRandChars[i] = (lastRandChars[i] ?? 0) + 1;
1043
+ }
1044
+ }
1045
+
1046
+ for (let i = 0; i < 12; i++) {
1047
+ id += PUSH_CHARS.charAt(lastRandChars[i]!);
1048
+ }
1049
+ return id;
1050
+ }