@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,165 @@
1
+ /**
2
+ * SharedWorker host — AI op + stream-subscription handlers (pyric/ai).
3
+ *
4
+ * Implements cdd-deltas "Where the engines live under pyric dev (#98)":
5
+ *
6
+ * 1. The broker and engines run IN-PROCESS with the sandbox — here, in the
7
+ * worker host. The scripted engine does no I/O anywhere.
8
+ * 2. The openai engine's default base URL in this (browser) context is the
9
+ * same-origin `/__pyric/ai-proxy` route, so a localhost upstream (Ollama)
10
+ * needs no CORS setup. Relative URLs resolve against the worker script's
11
+ * origin, so the plain path works from inside the SharedWorker.
12
+ * 3. `ai.generateContent` / `ai.countTokens` are unary ops exactly like
13
+ * `rtdb.*` / `auth.*`; `ai.streamGenerateContent` is a FINITE
14
+ * subscription that delivers `{ chunk }` snaps in order, then a terminal
15
+ * `{ done: true }` snap and auto-unsubscribes (see AiStreamSubMessage).
16
+ * 4. Engine choice + model mapping are per-sandbox config: the broker is
17
+ * recovered from `getAI(ctx.sandbox, …)` (the mirror's OWN per-sandbox
18
+ * broker — its events land on the shared sandbox's unified stream), and
19
+ * is created ONCE — host config (`ctx.aiEngine`) wins over the first
20
+ * op's `engine` field; later configs are ignored, mirroring `getAI`'s
21
+ * first-call-wins idempotence.
22
+ *
23
+ * Decomposition mirrors host-auth.ts / host-events.ts: host.ts routes
24
+ * `ai.*` ops and `{ service: 'ai' }` subs here; this module never imports
25
+ * host.ts (no cycles — it leans only on the host-context foundation).
26
+ */
27
+
28
+ import { getAI, TARGET_SYMBOL, type AIOptions } from 'pyric/ai';
29
+
30
+ import { serializeError, type OpMessage, type AiStreamSubMessage, type AiEngineConfigWire } from './protocol.js';
31
+ import { type HostCtx, type PortLike, type AiBrokerLike, post, ok, fail } from './host-context.js';
32
+
33
+ /** The serve proxy route the browser openai engine defaults to (#98.2). */
34
+ export const AI_PROXY_PATH = '/__pyric/ai-proxy';
35
+
36
+ /** Is this op an AI op (routed here instead of handleOp)? */
37
+ export function isAiOp(method: OpMessage['method']): boolean {
38
+ return method.startsWith('ai.');
39
+ }
40
+
41
+ /** The two unary AI op variants, extracted from the OpMessage union. */
42
+ type AiOpMessage = Extract<OpMessage, { method: 'ai.generateContent' | 'ai.countTokens' }>;
43
+
44
+ /**
45
+ * Wire config → the mirror's `EngineConfig`. The only transformation is the
46
+ * browser default: an openai config with no `baseUrl` targets the serve
47
+ * proxy path. Script entries pass through as-is (they are the broker's own
48
+ * plain authoring shapes; predicate matchers never made it across the port —
49
+ * structured clone rejects functions loudly at send time).
50
+ */
51
+ function resolveEngineConfig(wire: AiEngineConfigWire): NonNullable<AIOptions['engine']> {
52
+ if (wire.kind === 'openai') {
53
+ return {
54
+ kind: 'openai',
55
+ baseUrl: wire.baseUrl ?? AI_PROXY_PATH,
56
+ ...(wire.model !== undefined ? { model: wire.model } : {}),
57
+ ...(wire.modelMap !== undefined ? { modelMap: wire.modelMap } : {}),
58
+ };
59
+ }
60
+ return {
61
+ kind: 'scripted',
62
+ ...(wire.script !== undefined
63
+ ? { script: wire.script as unknown as Extract<NonNullable<AIOptions['engine']>, { kind: 'scripted' }>['script'] }
64
+ : {}),
65
+ };
66
+ }
67
+
68
+ /**
69
+ * The sandbox's AiBroker, created once per worker lifetime. `getAI` is
70
+ * idempotent per sandbox+backend, so this IS the mirror's broker — a page
71
+ * embedding host and this handler resolve the same instance, and every
72
+ * broker op lands `service: 'ai'` events on the shared sandbox's unified
73
+ * event stream (what Studio consumes).
74
+ *
75
+ * `AiBroker` is not exported from `pyric/ai`'s public surface; the instance
76
+ * is recovered from the branded handle via the exported `TARGET_SYMBOL`
77
+ * (the mirror's own dispatch seam).
78
+ */
79
+ export function ensureAiBroker(ctx: HostCtx, opEngine?: AiEngineConfigWire): AiBrokerLike {
80
+ if (ctx.aiBroker) return ctx.aiBroker;
81
+ const wire = ctx.aiEngine ?? opEngine;
82
+ const ai = getAI(ctx.sandbox, wire ? { engine: resolveEngineConfig(wire) } : undefined);
83
+ const target = (ai as unknown as {
84
+ [TARGET_SYMBOL]?: { broker: AiBrokerLike };
85
+ })[TARGET_SYMBOL];
86
+ if (!target?.broker) {
87
+ // Unreachable for a real Sandbox ctx; guards a mis-wired embedding host.
88
+ throw new Error('pyric worker: getAI(sandbox) did not produce a sandbox-target AI handle');
89
+ }
90
+ ctx.aiBroker = target.broker;
91
+ return target.broker;
92
+ }
93
+
94
+ /** Handle the unary AI ops. Requests/replies are plain Gemini-wire JSON. */
95
+ export async function handleAiOp(ctx: HostCtx, port: PortLike, msg: OpMessage): Promise<void> {
96
+ const aiMsg = msg as AiOpMessage;
97
+ try {
98
+ const broker = ensureAiBroker(ctx, aiMsg.engine);
99
+ switch (aiMsg.method) {
100
+ case 'ai.generateContent': {
101
+ ok(port, aiMsg.id, await broker.generateContent(aiMsg.request, aiMsg.model));
102
+ break;
103
+ }
104
+ case 'ai.countTokens': {
105
+ ok(port, aiMsg.id, await broker.countTokens(aiMsg.request, aiMsg.model));
106
+ break;
107
+ }
108
+ default: {
109
+ fail(port, msg.id, new Error(`Unknown ai method: ${String((msg as { method: unknown }).method)}`));
110
+ }
111
+ }
112
+ } catch (e) {
113
+ // AiBrokerError envelopes ride the SerializedError whole (aiEnvelope) —
114
+ // serializeError detects them structurally.
115
+ fail(port, aiMsg.id, e);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Handle an AI stream subscription: pump the broker's chunk stream to the
121
+ * port as `{ chunk }` snaps IN ORDER, then post the terminal `{ done: true }`
122
+ * snap and drop the sub (finite — auto-unsub on done). The pump is detached
123
+ * (sub handling is synchronous, like the other sub handlers); a client
124
+ * `unsub` mid-stream flips the registered cancel and delivery stops.
125
+ *
126
+ * Broker validation runs eagerly inside `streamGenerateContent` (before
127
+ * iteration) — either way a throw lands as the shared `{ __error }` snap
128
+ * convention, terminal for this sub.
129
+ */
130
+ export function handleAiSub(ctx: HostCtx, port: PortLike, msg: AiStreamSubMessage): void {
131
+ let portSubs = ctx.subs.get(port);
132
+ if (!portSubs) {
133
+ portSubs = new Map();
134
+ ctx.subs.set(port, portSubs);
135
+ }
136
+ if (portSubs.has(msg.subId)) return; // idempotent
137
+
138
+ let cancelled = false;
139
+ const cancel = (): void => {
140
+ cancelled = true;
141
+ };
142
+ portSubs.set(msg.subId, cancel);
143
+
144
+ void (async () => {
145
+ try {
146
+ const broker = ensureAiBroker(ctx, msg.engine);
147
+ for await (const chunk of broker.streamGenerateContent(msg.request, msg.model)) {
148
+ if (cancelled) return;
149
+ post(port, { t: 'snap', subId: msg.subId, value: { chunk } });
150
+ }
151
+ if (!cancelled) {
152
+ post(port, { t: 'snap', subId: msg.subId, value: { done: true } });
153
+ }
154
+ } catch (e) {
155
+ if (!cancelled) {
156
+ post(port, { t: 'snap', subId: msg.subId, value: { __error: serializeError(e) } });
157
+ }
158
+ } finally {
159
+ // Auto-unsub: drop OUR registration only (an early client unsub may
160
+ // have already removed it — handleUnsub ran the cancel and deleted it).
161
+ const subs = ctx.subs.get(port);
162
+ if (subs?.get(msg.subId) === cancel) subs.delete(msg.subId);
163
+ }
164
+ })();
165
+ }
@@ -0,0 +1,469 @@
1
+ /**
2
+ * SharedWorker host — auth subsystem: PER-PORT SESSIONS (#754).
3
+ *
4
+ * The worker hosts ONE sandbox (one user pool, one data store, one ruleset)
5
+ * but each connected port (tab / client) owns its OWN session: sign-ins mint
6
+ * an authentic identity via `sandbox.mintSession` (credentials validated /
7
+ * identity really created — NOT the impersonation lens) and bind it to the
8
+ * port. Auth-state fan-out is port-scoped: tab A signing in does not fire
9
+ * tab B's `onAuthStateChanged`. Data ops without an explicit Studio lens
10
+ * run under the port's session (see `sessionDb` in host.ts).
11
+ *
12
+ * Session persistence is CLIENT-side: the page records its uid in web
13
+ * storage (SessionStore, honoring setPersistence) and re-establishes it on
14
+ * reload via the `auth.restorePortSession` op. The worker holds no session
15
+ * record — the user POOL still rides the sandbox snapshot (#629).
16
+ *
17
+ * Imports only from `./host-context.js` + external packages (no circular
18
+ * imports).
19
+ */
20
+
21
+ import { type HostCtx, type PortLike, post, ok, fail, bestEffortFlush } from './host-context.js';
22
+ import {
23
+ getAuth,
24
+ sandbox as authSandboxOps,
25
+ type Auth,
26
+ type MintedSession,
27
+ type User,
28
+ } from 'pyric/auth';
29
+ import {
30
+ serializeUser,
31
+ type OpMessage,
32
+ type AuthSubMessage,
33
+ } from './protocol.js';
34
+
35
+ /**
36
+ * Synthetic password seeded for bridged provider identities (popup/redirect).
37
+ * Provider users never authenticate with a password — this just satisfies the
38
+ * SeedUser shape. Matches `ServeAuthHelper`'s in-page constant in spirit.
39
+ */
40
+ const PROVIDER_SYNTHETIC_PASSWORD = '__pyric_popup_no_password__';
41
+
42
+ // ─── Auth: per-port sessions + port-scoped fan-out ────────────────────────
43
+
44
+ /**
45
+ * Per-port auth subscription registry. Map<port, Map<subId, target>>.
46
+ *
47
+ * SEPARATE FROM `ctx.subs` (Firestore listeners): auth subs are routing
48
+ * entries only. With per-port sessions there is no worker-wide auth
49
+ * listener at all — a port's session changes fan out to THAT port's subs
50
+ * (see {@link setPortSession}).
51
+ */
52
+ const _authSubs = new WeakMap<HostCtx, Map<PortLike, Map<string, 'authState' | 'idToken'>>>();
53
+
54
+ export function authSubsFor(ctx: HostCtx): Map<PortLike, Map<string, 'authState' | 'idToken'>> {
55
+ let m = _authSubs.get(ctx);
56
+ if (!m) {
57
+ m = new Map();
58
+ _authSubs.set(ctx, m);
59
+ }
60
+ return m;
61
+ }
62
+
63
+ /**
64
+ * Lazily create the ONE shared auth handle — the user pool + admin surface.
65
+ * Calling `getAuth(sandbox)` also (re)registers the auth service with the
66
+ * persistence registry, so the user DB rides the snapshot (#629). Its
67
+ * `currentUser` is never set in served mode; sessions live per-port.
68
+ *
69
+ * Idempotent: returns the cached `ctx.auth` on repeat calls.
70
+ */
71
+ export function ensureAuth(ctx: HostCtx): Auth {
72
+ if (ctx.auth) return ctx.auth;
73
+ ctx.auth = getAuth(ctx.sandbox);
74
+ if (ctx.sessionMode === undefined) ctx.sessionMode = 'LOCAL';
75
+ return ctx.auth;
76
+ }
77
+
78
+ function portSessionsFor(ctx: HostCtx): Map<PortLike, MintedSession | null> {
79
+ return (ctx.portSessions ??= new Map());
80
+ }
81
+
82
+ /** The port's signed-in session, or null (absent from the map = signed out). */
83
+ export function portSession(ctx: HostCtx, port: PortLike): MintedSession | null {
84
+ return portSessionsFor(ctx).get(port) ?? null;
85
+ }
86
+
87
+ /**
88
+ * Bind (or clear) a port's session and fan the change out to THAT PORT's
89
+ * authState/idToken subs — the per-port analog of the old cross-tab
90
+ * broadcast. Every sign-in mints a fresh token (mintSession did), so the
91
+ * idToken stream fires alongside authState, matching the real observers.
92
+ */
93
+ function setPortSession(ctx: HostCtx, port: PortLike, session: MintedSession | null): void {
94
+ portSessionsFor(ctx).set(port, session);
95
+
96
+ // Prod parity on auth transitions: re-establish this port's session-bound
97
+ // Firestore listeners under the NEW identity, so a sign-out re-evaluates
98
+ // live streams (auth-gated data is revoked, not leaked) and a sign-in
99
+ // grants them. Studio-lens subs (explicit actAs) are untouched.
100
+ ctx.resubscribePortSubs?.(port);
101
+
102
+ const serialized = serializeUser(session?.user ?? null);
103
+ const bySubId = authSubsFor(ctx).get(port);
104
+ if (!bySubId) return;
105
+ for (const [subId] of bySubId) {
106
+ post(port, { t: 'snap', subId, value: serialized });
107
+ }
108
+ }
109
+
110
+ /** Apply a forced ID-token refresh to this port's data authorization state. */
111
+ function refreshPortAuthorization(
112
+ ctx: HostCtx,
113
+ port: PortLike,
114
+ session: MintedSession,
115
+ claims: Record<string, unknown>,
116
+ ): void {
117
+ session.state.token = { ...claims };
118
+
119
+ // Frozen sandbox.withAuth handles capture the old token. Clear all
120
+ // session-handle caches so this port and any same-uid sibling rebuild from
121
+ // their own current session state on the next operation.
122
+ ctx.sessionDbs?.clear();
123
+ ctx.sessionRtdbs?.clear();
124
+ ctx.sessionStorages?.clear();
125
+
126
+ // Existing Firestore/RTDB streams must reauthorize under the refreshed
127
+ // claims. A token refresh is not an auth-state transition, so notify only
128
+ // onIdTokenChanged observers with the still-current user.
129
+ ctx.resubscribePortSubs?.(port);
130
+ const serialized = serializeUser(session.user);
131
+ for (const [subId, target] of authSubsFor(ctx).get(port) ?? []) {
132
+ if (target === 'idToken') post(port, { t: 'snap', subId, value: serialized });
133
+ }
134
+ }
135
+
136
+ /** Tear down a disconnected port's session (called from cleanupPort). */
137
+ export function cleanupPortSession(ctx: HostCtx, port: PortLike): void {
138
+ ctx.portSessions?.delete(port);
139
+ }
140
+
141
+ // ─── Auth op handlers ─────────────────────────────────────────────────────
142
+
143
+ /** Serialized UserCredential reply shape for a minted session. */
144
+ function credReply(session: MintedSession, providerId: string | null) {
145
+ return {
146
+ user: serializeUser(session.user),
147
+ providerId,
148
+ operationType: 'signIn' as const,
149
+ };
150
+ }
151
+
152
+ export async function handleAuthOp(ctx: HostCtx, port: PortLike, msg: OpMessage): Promise<void> {
153
+ const auth = ensureAuth(ctx);
154
+
155
+ switch (msg.method) {
156
+ case 'auth.createUser': {
157
+ try {
158
+ const session = authSandboxOps.mintSession(auth, {
159
+ kind: 'createPassword', email: msg.email, password: msg.password,
160
+ });
161
+ setPortSession(ctx, port, session);
162
+ await bestEffortFlush(ctx); // new user record must be durable at ack
163
+ ok(port, msg.id, credReply(session, null));
164
+ } catch (e) { fail(port, msg.id, e); }
165
+ break;
166
+ }
167
+
168
+ case 'auth.signInEmail': {
169
+ try {
170
+ const session = authSandboxOps.mintSession(auth, {
171
+ kind: 'password', email: msg.email, password: msg.password,
172
+ });
173
+ setPortSession(ctx, port, session);
174
+ ok(port, msg.id, credReply(session, null));
175
+ } catch (e) { fail(port, msg.id, e); }
176
+ break;
177
+ }
178
+
179
+ case 'auth.signInAnonymously': {
180
+ try {
181
+ // Match `firebase/auth` semantics per session: if THIS PORT is
182
+ // already anonymous, reuse that identity (StrictMode double-mounts
183
+ // must not leak a fresh uid per mount). Other ports' anonymous
184
+ // sessions are other users — that's the multi-user point.
185
+ const existing = portSession(ctx, port);
186
+ if (existing && existing.user.isAnonymous) {
187
+ ok(port, msg.id, credReply(existing, null));
188
+ break;
189
+ }
190
+ const session = authSandboxOps.mintSession(auth, { kind: 'anonymous' });
191
+ setPortSession(ctx, port, session);
192
+ await bestEffortFlush(ctx); // anonymous sign-in creates a user record
193
+ ok(port, msg.id, credReply(session, null));
194
+ } catch (e) { fail(port, msg.id, e); }
195
+ break;
196
+ }
197
+
198
+ case 'auth.signOut': {
199
+ try {
200
+ setPortSession(ctx, port, null);
201
+ ok(port, msg.id, null);
202
+ } catch (e) { fail(port, msg.id, e); }
203
+ break;
204
+ }
205
+
206
+ case 'auth.restorePortSession': {
207
+ // Per-tab reload restore (#754): soft — an unknown/disabled uid means
208
+ // signed out (null), never an error. The page clears its stale record.
209
+ try {
210
+ let session: MintedSession | null = null;
211
+ try {
212
+ session = authSandboxOps.mintSession(auth, { kind: 'uid', uid: msg.uid });
213
+ } catch {
214
+ session = null;
215
+ }
216
+ if (session) setPortSession(ctx, port, session);
217
+ ok(port, msg.id, session ? serializeUser(session.user) : null);
218
+ } catch (e) { fail(port, msg.id, e); }
219
+ break;
220
+ }
221
+
222
+ case 'auth.getIdToken': {
223
+ try {
224
+ const session = requirePortSession(ctx, port, 'getIdToken');
225
+ const user = session.user;
226
+ const token = await user.getIdToken(msg.forceRefresh);
227
+ if (msg.forceRefresh) {
228
+ const result = await user.getIdTokenResult(false);
229
+ refreshPortAuthorization(ctx, port, session, result.claims);
230
+ }
231
+ ok(port, msg.id, token);
232
+ } catch (e) { fail(port, msg.id, e); }
233
+ break;
234
+ }
235
+
236
+ case 'auth.getIdTokenResult': {
237
+ try {
238
+ const session = requirePortSession(ctx, port, 'getIdTokenResult');
239
+ const user = session.user;
240
+ const r = await user.getIdTokenResult(msg.forceRefresh);
241
+ if (msg.forceRefresh) refreshPortAuthorization(ctx, port, session, r.claims);
242
+ ok(port, msg.id, {
243
+ token: r.token,
244
+ claims: r.claims,
245
+ expirationTime: r.expirationTime,
246
+ issuedAtTime: r.issuedAtTime,
247
+ authTime: r.authTime,
248
+ signInProvider: r.signInProvider ?? null,
249
+ });
250
+ } catch (e) { fail(port, msg.id, e); }
251
+ break;
252
+ }
253
+
254
+ case 'auth.setPersistence': {
255
+ // Accepted for surface parity; the CLIENT's SessionStore owns where
256
+ // (or whether) the session uid is recorded. Nothing to do worker-side.
257
+ ctx.sessionMode = msg.mode;
258
+ ok(port, msg.id, null);
259
+ break;
260
+ }
261
+
262
+ case 'auth.getCurrentUser': {
263
+ try {
264
+ ok(port, msg.id, serializeUser(portSession(ctx, port)?.user ?? null));
265
+ } catch (e) { fail(port, msg.id, e); }
266
+ break;
267
+ }
268
+
269
+ case 'auth.updateProfile': {
270
+ // Update THIS PORT's signed-in user's profile (displayName / photoURL).
271
+ // Updates the stored record (by uid) via the sandbox op, then mutates the
272
+ // port session's `User` in place so a subsequent `auth.getCurrentUser`
273
+ // (and the client mirror hydrated from the reply) is consistent. Does NOT
274
+ // fire onAuthStateChanged/onIdTokenChanged — matching firebase/auth.
275
+ try {
276
+ const session = portSession(ctx, port);
277
+ if (!session) throw makeNoUserError('updateProfile');
278
+ const profile = { displayName: msg.displayName, photoURL: msg.photoURL };
279
+ authSandboxOps.updateProfile(auth, session.user.uid, profile);
280
+ applyProfileToUser(session.user, profile);
281
+ await bestEffortFlush(ctx);
282
+ ok(port, msg.id, serializeUser(session.user));
283
+ } catch (e) { fail(port, msg.id, e); }
284
+ break;
285
+ }
286
+
287
+ case 'auth.acceptIdentity': {
288
+ // Provider sign-in bridge: the page resolved a popup/redirect identity
289
+ // in-page (ServeAuthHelper) and hands it here. GATE FIRST: the page
290
+ // sandbox delegates provider enforcement to this worker (its picker
291
+ // opens unconditionally), so THIS is where Studio's provider toggles
292
+ // bite — a disabled provider throws `auth/operation-not-allowed`
293
+ // before any user-DB write. Then seed it into the user DB (so rules
294
+ // `request.auth.token.*` claims resolve AND it shows in the picker
295
+ // next time), and mint THIS PORT's session for it — provider users
296
+ // have no password. Mirrors ServeAuthHelper.add's seeding.
297
+ try {
298
+ const { uid, email, displayName, customClaims, providerId } = msg.identity;
299
+ authSandboxOps.assertAuthProviderEnabled(auth, providerId);
300
+ authSandboxOps.seedUsers(auth, [{
301
+ uid,
302
+ email: email ?? '',
303
+ password: PROVIDER_SYNTHETIC_PASSWORD,
304
+ displayName: displayName ?? undefined,
305
+ customClaims: customClaims ?? {},
306
+ providerId,
307
+ }]);
308
+ const session = authSandboxOps.mintSession(auth, { kind: 'uid', uid });
309
+ setPortSession(ctx, port, session);
310
+ await bestEffortFlush(ctx); // seeded provider identity must be durable
311
+ ok(port, msg.id, credReply(session, providerId));
312
+ } catch (e) { fail(port, msg.id, e); }
313
+ break;
314
+ }
315
+
316
+ case 'auth.listUsers': {
317
+ // Admin user-DB enumeration (Pyric Studio data browse).
318
+ try {
319
+ ok(port, msg.id, authSandboxOps.listUsers(auth));
320
+ } catch (e) { fail(port, msg.id, e); }
321
+ break;
322
+ }
323
+
324
+ case 'auth.adminCreateUser': {
325
+ try {
326
+ const created = authSandboxOps.createUser(
327
+ auth,
328
+ msg.request as Parameters<typeof authSandboxOps.createUser>[1],
329
+ );
330
+ await bestEffortFlush(ctx);
331
+ ok(port, msg.id, created);
332
+ } catch (e) { fail(port, msg.id, e); }
333
+ break;
334
+ }
335
+
336
+ case 'auth.adminUpdateUser': {
337
+ try {
338
+ const updated = authSandboxOps.updateUser(
339
+ auth,
340
+ msg.uid,
341
+ msg.request as Parameters<typeof authSandboxOps.updateUser>[2],
342
+ );
343
+ await bestEffortFlush(ctx);
344
+ ok(port, msg.id, updated);
345
+ } catch (e) { fail(port, msg.id, e); }
346
+ break;
347
+ }
348
+
349
+ case 'auth.adminDeleteUser': {
350
+ try {
351
+ authSandboxOps.deleteUser(auth, msg.uid);
352
+ await bestEffortFlush(ctx);
353
+ ok(port, msg.id, null);
354
+ } catch (e) { fail(port, msg.id, e); }
355
+ break;
356
+ }
357
+
358
+ case 'auth.adminClearUsers': {
359
+ try {
360
+ authSandboxOps.clearUsers(auth);
361
+ await bestEffortFlush(ctx);
362
+ ok(port, msg.id, null);
363
+ } catch (e) { fail(port, msg.id, e); }
364
+ break;
365
+ }
366
+
367
+ case 'auth.getProviderConfig': {
368
+ // Sign-in provider config (Pyric Studio S-AUTH "Sign-in providers"
369
+ // section). `setProviderConfig` emits a `provider_config_update`
370
+ // sandbox event, so a caller subscribed to the event stream (the
371
+ // worker's event feed, which `subscribeUsers` already rides) sees
372
+ // toggles live without a dedicated subscription message here.
373
+ try {
374
+ ok(port, msg.id, authSandboxOps.getAuthProviderConfig(auth));
375
+ } catch (e) { fail(port, msg.id, e); }
376
+ break;
377
+ }
378
+
379
+ case 'auth.setProviderConfig': {
380
+ try {
381
+ authSandboxOps.setAuthProviderConfig(auth, msg.providerId, msg.enabled);
382
+ ok(port, msg.id, null);
383
+ } catch (e) { fail(port, msg.id, e); }
384
+ break;
385
+ }
386
+
387
+ default: {
388
+ fail(port, msg.id, new Error(`Unknown auth method: ${String((msg as { method: unknown }).method)}`));
389
+ }
390
+ }
391
+ }
392
+
393
+ /** The port session's User, or throw `auth/no-current-user`. */
394
+ function requireSessionUser(ctx: HostCtx, port: PortLike, api: string): User {
395
+ return requirePortSession(ctx, port, api).user;
396
+ }
397
+
398
+ function requirePortSession(ctx: HostCtx, port: PortLike, api: string): MintedSession {
399
+ const session = portSession(ctx, port);
400
+ if (!session) throw makeNoUserError(api);
401
+ return session;
402
+ }
403
+
404
+ /**
405
+ * Mutate a port session's `User` `displayName` / `photoURL` (and the first
406
+ * `providerData` entry's) in place so a subsequent `auth.getCurrentUser`
407
+ * reflects an `auth.updateProfile`. Fields are `readonly` at the type level
408
+ * but plain data at runtime; only an explicitly-provided field is applied
409
+ * (`null` clears, `undefined` leaves untouched).
410
+ */
411
+ function applyProfileToUser(
412
+ user: User,
413
+ profile: { displayName?: string | null; photoURL?: string | null },
414
+ ): void {
415
+ const mutable = user as { -readonly [K in keyof User]: User[K] };
416
+ if (profile.displayName !== undefined) mutable.displayName = profile.displayName;
417
+ if (profile.photoURL !== undefined) mutable.photoURL = profile.photoURL;
418
+ const provider0 = user.providerData?.[0] as
419
+ | { -readonly [K in keyof NonNullable<User['providerData']>[number]]: NonNullable<User['providerData']>[number][K] }
420
+ | undefined;
421
+ if (provider0) {
422
+ if (profile.displayName !== undefined) provider0.displayName = profile.displayName;
423
+ if (profile.photoURL !== undefined) provider0.photoURL = profile.photoURL;
424
+ }
425
+ }
426
+
427
+ /** Build an `auth/no-current-user`-style error for token ops with no user. */
428
+ function makeNoUserError(api: string): Error & { code: string } {
429
+ const err = new Error(`${api}: no current user is signed in.`) as Error & { code: string };
430
+ err.code = 'auth/no-current-user';
431
+ return err;
432
+ }
433
+
434
+ export function isAuthOp(method: OpMessage['method']): boolean {
435
+ return method.startsWith('auth.');
436
+ }
437
+
438
+ // ─── Auth subscription handlers ───────────────────────────────────────────
439
+
440
+ /**
441
+ * Register an auth subscription for a port. A routing entry only — the
442
+ * port's session changes (its own sign-ins/outs) fan out to it via
443
+ * {@link setPortSession}. Fires the PORT's current session immediately
444
+ * (initial-fire parity with onAuthStateChanged/onIdTokenChanged).
445
+ */
446
+ export function handleAuthSub(ctx: HostCtx, port: PortLike, msg: AuthSubMessage): void {
447
+ ensureAuth(ctx);
448
+ const subs = authSubsFor(ctx);
449
+ let bySubId = subs.get(port);
450
+ if (!bySubId) {
451
+ bySubId = new Map();
452
+ subs.set(port, bySubId);
453
+ }
454
+ if (bySubId.has(msg.subId)) return; // idempotent
455
+ bySubId.set(msg.subId, msg.target);
456
+
457
+ // Initial fire — mirror the real observers, which invoke the callback once
458
+ // with the current state on registration. Per-port: THIS port's session.
459
+ post(port, { t: 'snap', subId: msg.subId, value: serializeUser(portSession(ctx, port)?.user ?? null) });
460
+ }
461
+
462
+ export function handleAuthUnsub(ctx: HostCtx, port: PortLike, subId: string): boolean {
463
+ const subs = authSubsFor(ctx);
464
+ const bySubId = subs.get(port);
465
+ if (!bySubId || !bySubId.has(subId)) return false;
466
+ bySubId.delete(subId);
467
+ if (bySubId.size === 0) subs.delete(port);
468
+ return true;
469
+ }