@pyric/cli 0.1.0-alpha.16 → 0.1.0-alpha.17

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 (378) hide show
  1. package/README.md +13 -9
  2. package/dist/assurance/.generated/conformance-verdicts.d.ts +80 -72
  3. package/dist/assurance/.generated/conformance-verdicts.d.ts.map +1 -1
  4. package/dist/assurance/.generated/conformance-verdicts.js +80 -72
  5. package/dist/assurance/.generated/conformance-verdicts.js.map +1 -1
  6. package/dist/cli/database-rules.d.ts.map +1 -1
  7. package/dist/cli/database-rules.js +6 -4
  8. package/dist/cli/database-rules.js.map +1 -1
  9. package/dist/cli/dev-runner.d.ts +9 -0
  10. package/dist/cli/dev-runner.d.ts.map +1 -1
  11. package/dist/cli/dev-runner.js +18 -0
  12. package/dist/cli/dev-runner.js.map +1 -1
  13. package/dist/cli/serve.d.ts.map +1 -1
  14. package/dist/cli/serve.js +61 -6
  15. package/dist/cli/serve.js.map +1 -1
  16. package/dist/cli/verify.js +2 -2
  17. package/dist/cli/verify.js.map +1 -1
  18. package/dist/conformance/.generated/can-i-use-browser.d.ts.map +1 -1
  19. package/dist/conformance/.generated/can-i-use-browser.js +1 -1
  20. package/dist/conformance/.generated/can-i-use-browser.js.map +1 -1
  21. package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
  22. package/dist/conformance/.generated/can-i-use.js +1 -1
  23. package/dist/conformance/.generated/can-i-use.js.map +1 -1
  24. package/dist/conformance/.generated/conformance-docs.d.ts.map +1 -1
  25. package/dist/conformance/.generated/conformance-docs.js +1 -1
  26. package/dist/conformance/.generated/conformance-docs.js.map +1 -1
  27. package/dist/next/client-aliases.d.ts +11 -0
  28. package/dist/next/client-aliases.d.ts.map +1 -0
  29. package/dist/next/client-aliases.js +38 -0
  30. package/dist/next/client-aliases.js.map +1 -0
  31. package/dist/next/dev-rewrites.d.ts +9 -0
  32. package/dist/next/dev-rewrites.d.ts.map +1 -0
  33. package/dist/next/dev-rewrites.js +53 -0
  34. package/dist/next/dev-rewrites.js.map +1 -0
  35. package/dist/next/guard.d.ts +15 -0
  36. package/dist/next/guard.d.ts.map +1 -0
  37. package/dist/next/guard.js +52 -0
  38. package/dist/next/guard.js.map +1 -0
  39. package/dist/next/index.d.ts +12 -0
  40. package/dist/next/index.d.ts.map +1 -0
  41. package/dist/next/index.js +11 -0
  42. package/dist/next/index.js.map +1 -0
  43. package/dist/next/runtime-env.d.ts +7 -0
  44. package/dist/next/runtime-env.d.ts.map +1 -0
  45. package/dist/next/runtime-env.js +13 -0
  46. package/dist/next/runtime-env.js.map +1 -0
  47. package/dist/next/server-external-packages.d.ts +10 -0
  48. package/dist/next/server-external-packages.d.ts.map +1 -0
  49. package/dist/next/server-external-packages.js +34 -0
  50. package/dist/next/server-external-packages.js.map +1 -0
  51. package/dist/next/target-url.d.ts +11 -0
  52. package/dist/next/target-url.d.ts.map +1 -0
  53. package/dist/next/target-url.js +36 -0
  54. package/dist/next/target-url.js.map +1 -0
  55. package/dist/next/turbopack-config.d.ts +9 -0
  56. package/dist/next/turbopack-config.d.ts.map +1 -0
  57. package/dist/next/turbopack-config.js +39 -0
  58. package/dist/next/turbopack-config.js.map +1 -0
  59. package/dist/next/types.d.ts +40 -0
  60. package/dist/next/types.d.ts.map +1 -0
  61. package/dist/next/types.js +5 -0
  62. package/dist/next/types.js.map +1 -0
  63. package/dist/next/webpack-config.d.ts +11 -0
  64. package/dist/next/webpack-config.d.ts.map +1 -0
  65. package/dist/next/webpack-config.js +51 -0
  66. package/dist/next/webpack-config.js.map +1 -0
  67. package/dist/next/with-pyric.d.ts +22 -0
  68. package/dist/next/with-pyric.d.ts.map +1 -0
  69. package/dist/next/with-pyric.js +49 -0
  70. package/dist/next/with-pyric.js.map +1 -0
  71. package/dist/register/app-bridge.d.ts +9 -0
  72. package/dist/register/app-bridge.d.ts.map +1 -0
  73. package/dist/register/app-bridge.js +56 -0
  74. package/dist/register/app-bridge.js.map +1 -0
  75. package/dist/register/hooks.d.ts.map +1 -1
  76. package/dist/register/hooks.js +6 -1
  77. package/dist/register/hooks.js.map +1 -1
  78. package/dist/register/index.js +1 -1
  79. package/dist/register/index.js.map +1 -1
  80. package/dist/register/mapping.d.ts +3 -1
  81. package/dist/register/mapping.d.ts.map +1 -1
  82. package/dist/register/mapping.js +41 -5
  83. package/dist/register/mapping.js.map +1 -1
  84. package/dist/rtdb/rules-json.d.ts +12 -0
  85. package/dist/rtdb/rules-json.d.ts.map +1 -1
  86. package/dist/rtdb/rules-json.js +98 -0
  87. package/dist/rtdb/rules-json.js.map +1 -1
  88. package/dist/serve/bundler.d.ts.map +1 -1
  89. package/dist/serve/bundler.js +2 -0
  90. package/dist/serve/bundler.js.map +1 -1
  91. package/dist/serve/discovery.d.ts +4 -4
  92. package/dist/serve/discovery.d.ts.map +1 -1
  93. package/dist/serve/discovery.js +59 -38
  94. package/dist/serve/discovery.js.map +1 -1
  95. package/dist/serve/entries/app-ai-passthrough.d.ts +8 -0
  96. package/dist/serve/entries/app-ai-passthrough.d.ts.map +1 -0
  97. package/dist/serve/entries/app-ai-passthrough.js +59 -0
  98. package/dist/serve/entries/app-ai-passthrough.js.map +1 -0
  99. package/dist/serve/entries/auth-helper-core.js +9 -7
  100. package/dist/serve/entries/auth-helper-core.js.map +1 -1
  101. package/dist/serve/entries/auth.d.ts +1 -0
  102. package/dist/serve/entries/auth.d.ts.map +1 -1
  103. package/dist/serve/entries/auth.js +1 -0
  104. package/dist/serve/entries/auth.js.map +1 -1
  105. package/dist/serve/entries/bridge-url.d.ts.map +1 -1
  106. package/dist/serve/entries/bridge-url.js +7 -2
  107. package/dist/serve/entries/bridge-url.js.map +1 -1
  108. package/dist/serve/entries/database.d.ts +1 -0
  109. package/dist/serve/entries/database.d.ts.map +1 -1
  110. package/dist/serve/entries/database.js +1 -0
  111. package/dist/serve/entries/database.js.map +1 -1
  112. package/dist/serve/entries/firestore.d.ts +1 -0
  113. package/dist/serve/entries/firestore.d.ts.map +1 -1
  114. package/dist/serve/entries/firestore.js +1 -0
  115. package/dist/serve/entries/firestore.js.map +1 -1
  116. package/dist/serve/entries/init.d.ts.map +1 -1
  117. package/dist/serve/entries/init.js +4 -2
  118. package/dist/serve/entries/init.js.map +1 -1
  119. package/dist/serve/entries/runtime.js +23 -0
  120. package/dist/serve/entries/runtime.js.map +1 -1
  121. package/dist/serve/entries/storage.d.ts +2 -0
  122. package/dist/serve/entries/storage.d.ts.map +1 -1
  123. package/dist/serve/entries/storage.js +9 -2
  124. package/dist/serve/entries/storage.js.map +1 -1
  125. package/dist/serve/html-injection.d.ts +3 -1
  126. package/dist/serve/html-injection.d.ts.map +1 -1
  127. package/dist/serve/html-injection.js +27 -1
  128. package/dist/serve/html-injection.js.map +1 -1
  129. package/dist/serve/rules.js +2 -2
  130. package/dist/serve/rules.js.map +1 -1
  131. package/dist/serve/runtime/chip-config.d.ts +1 -1
  132. package/dist/serve/runtime/chip-config.d.ts.map +1 -1
  133. package/dist/serve/runtime/chip-config.js +16 -8
  134. package/dist/serve/runtime/chip-config.js.map +1 -1
  135. package/dist/serve/sandbox-session.d.ts +1 -0
  136. package/dist/serve/sandbox-session.d.ts.map +1 -1
  137. package/dist/serve/sandbox-session.js +30 -2
  138. package/dist/serve/sandbox-session.js.map +1 -1
  139. package/dist/serve/site-ui/_astro/_studio_.B6b_G1ev.css +1 -0
  140. package/dist/serve/site-ui/_astro/app.bnmVTkIY.js +11 -0
  141. package/dist/serve/site-ui/_astro/app.t6u-adB_.js +1 -0
  142. package/dist/serve/site-ui/_astro/{chess-showcase.CnoxIle5.js → chess-showcase.kC8dXLKY.js} +1 -1
  143. package/dist/serve/site-ui/_astro/{example-runtime.R1d96L6-.js → example-runtime.D79PnoSk.js} +1 -1
  144. package/dist/serve/site-ui/_astro/index.DzVde04M.js +1 -0
  145. package/dist/serve/site-ui/_astro/reads.BYBRLbsE.js +1 -0
  146. package/dist/serve/site-ui/_astro/sandbox-controls.C9Aaazt8.js +1252 -0
  147. package/dist/serve/site-ui/_astro/{seed.EyI3VYEu.js → seed.DANAIyRw.js} +1 -1
  148. package/dist/serve/site-ui/auth/index.html +2 -2
  149. package/dist/serve/site-ui/docs/_rhythm/index.html +1 -1
  150. package/dist/serve/site-ui/docs/agent/set-up-your-agent/index.html +1 -1
  151. package/dist/serve/site-ui/docs/agent/watch-and-review/index.html +1 -1
  152. package/dist/serve/site-ui/docs/agent/work-with-an-agent/index.html +1 -1
  153. package/dist/serve/site-ui/docs/ai-compat/index.html +85 -85
  154. package/dist/serve/site-ui/docs/ai-compat.md +84 -84
  155. package/dist/serve/site-ui/docs/api-reference/index.html +3 -1
  156. package/dist/serve/site-ui/docs/api-reference.md +2 -0
  157. package/dist/serve/site-ui/docs/app-compat/index.html +28 -28
  158. package/dist/serve/site-ui/docs/app-compat.md +27 -27
  159. package/dist/serve/site-ui/docs/auth-compat/index.html +122 -122
  160. package/dist/serve/site-ui/docs/auth-compat.md +121 -121
  161. package/dist/serve/site-ui/docs/build/ai-logic/index.html +1 -1
  162. package/dist/serve/site-ui/docs/build/authentication/index.html +1 -1
  163. package/dist/serve/site-ui/docs/build/cloud-firestore/index.html +1 -1
  164. package/dist/serve/site-ui/docs/build/cloud-messaging/index.html +1 -1
  165. package/dist/serve/site-ui/docs/build/cloud-storage/index.html +1 -1
  166. package/dist/serve/site-ui/docs/build/realtime-database/index.html +1 -1
  167. package/dist/serve/site-ui/docs/conformance-scores/index.html +4 -4
  168. package/dist/serve/site-ui/docs/conformance-scores.md +3 -3
  169. package/dist/serve/site-ui/docs/create-pyric-reference-api/index.html +6 -6
  170. package/dist/serve/site-ui/docs/create-pyric-reference-api.md +5 -5
  171. package/dist/serve/site-ui/docs/database-compat/index.html +200 -448
  172. package/dist/serve/site-ui/docs/database-compat.md +198 -461
  173. package/dist/serve/site-ui/docs/examples/chess/index.html +2 -2
  174. package/dist/serve/site-ui/docs/examples/index.html +1 -1
  175. package/dist/serve/site-ui/docs/firestore-compat/index.html +186 -195
  176. package/dist/serve/site-ui/docs/firestore-compat.md +184 -195
  177. package/dist/serve/site-ui/docs/functions-rtdb-compat/index.html +15 -15
  178. package/dist/serve/site-ui/docs/functions-rtdb-compat.md +14 -14
  179. package/dist/serve/site-ui/docs/get-started/how-the-swap-works/index.html +1 -1
  180. package/dist/serve/site-ui/docs/get-started/nextjs/index.html +57 -0
  181. package/dist/serve/site-ui/docs/get-started/nextjs.md +86 -0
  182. package/dist/serve/site-ui/docs/get-started/start-building/index.html +25 -8
  183. package/dist/serve/site-ui/docs/get-started/start-building.md +34 -5
  184. package/dist/serve/site-ui/docs/get-started/vite/index.html +26 -0
  185. package/dist/serve/site-ui/docs/get-started/vite.md +45 -0
  186. package/dist/serve/site-ui/docs/index.json +668 -103
  187. package/dist/serve/site-ui/docs/messaging-compat/index.html +56 -56
  188. package/dist/serve/site-ui/docs/messaging-compat.md +55 -55
  189. package/dist/serve/site-ui/docs/observe/resolve-runtime-status/index.html +1 -1
  190. package/dist/serve/site-ui/docs/observe/see-whats-happening/index.html +1 -1
  191. package/dist/serve/site-ui/docs/observe/shape-your-data/index.html +1 -1
  192. package/dist/serve/site-ui/docs/overview/index.html +1 -1
  193. package/dist/serve/site-ui/docs/pyric-admin-app-reference-api/index.html +39 -13
  194. package/dist/serve/site-ui/docs/pyric-admin-app-reference-api.md +28 -2
  195. package/dist/serve/site-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
  196. package/dist/serve/site-ui/docs/pyric-admin-database-reference-api/index.html +38 -6
  197. package/dist/serve/site-ui/docs/pyric-admin-database-reference-api.md +43 -0
  198. package/dist/serve/site-ui/docs/pyric-admin-firestore-reference-api/index.html +9 -1
  199. package/dist/serve/site-ui/docs/pyric-admin-firestore-reference-api.md +8 -0
  200. package/dist/serve/site-ui/docs/pyric-admin-messaging-reference-api/index.html +1 -1
  201. package/dist/serve/site-ui/docs/pyric-admin-storage-reference-api/index.html +27 -4
  202. package/dist/serve/site-ui/docs/pyric-admin-storage-reference-api.md +23 -0
  203. package/dist/serve/site-ui/docs/pyric-ai-reference-api/index.html +1 -1
  204. package/dist/serve/site-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
  205. package/dist/serve/site-ui/docs/pyric-app-reference-api/index.html +1 -1
  206. package/dist/serve/site-ui/docs/pyric-auth-reference-api/index.html +1 -1
  207. package/dist/serve/site-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
  208. package/dist/serve/site-ui/docs/pyric-cli-assurance-reference-api/index.html +1 -1
  209. package/dist/serve/site-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
  210. package/dist/serve/site-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
  211. package/dist/serve/site-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
  212. package/dist/serve/site-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
  213. package/dist/serve/site-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
  214. package/dist/serve/site-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
  215. package/dist/serve/site-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
  216. package/dist/serve/site-ui/docs/pyric-cli-next-reference-api/index.html +123 -0
  217. package/dist/serve/site-ui/docs/pyric-cli-next-reference-api.md +99 -0
  218. package/dist/serve/site-ui/docs/pyric-cli-register-app-bridge-reference-api/index.html +127 -0
  219. package/dist/serve/site-ui/docs/pyric-cli-register-app-bridge-reference-api.md +174 -0
  220. package/dist/serve/site-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
  221. package/dist/serve/site-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
  222. package/dist/serve/site-ui/docs/pyric-cli-serve-worker-reference-api/index.html +570 -326
  223. package/dist/serve/site-ui/docs/pyric-cli-serve-worker-reference-api.md +198 -6
  224. package/dist/serve/site-ui/docs/pyric-cli-verify-reference-api/index.html +1 -1
  225. package/dist/serve/site-ui/docs/pyric-cli-vite-reference-api/index.html +1 -1
  226. package/dist/serve/site-ui/docs/pyric-database-reference-api/index.html +1 -1
  227. package/dist/serve/site-ui/docs/pyric-firestore-reference-api/index.html +2971 -627
  228. package/dist/serve/site-ui/docs/pyric-firestore-reference-api.md +2873 -517
  229. package/dist/serve/site-ui/docs/pyric-messaging-reference-api/index.html +1 -1
  230. package/dist/serve/site-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
  231. package/dist/serve/site-ui/docs/pyric-rules-reference-api/index.html +43 -3
  232. package/dist/serve/site-ui/docs/pyric-rules-reference-api.md +8 -0
  233. package/dist/serve/site-ui/docs/pyric-sandbox-database-reference-api/index.html +26 -2
  234. package/dist/serve/site-ui/docs/pyric-sandbox-database-reference-api.md +24 -0
  235. package/dist/serve/site-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
  236. package/dist/serve/site-ui/docs/pyric-sandbox-reference-api/index.html +77 -9
  237. package/dist/serve/site-ui/docs/pyric-sandbox-reference-api.md +28 -13
  238. package/dist/serve/site-ui/docs/pyric-storage-reference-api/index.html +240 -63
  239. package/dist/serve/site-ui/docs/pyric-storage-reference-api.md +164 -5
  240. package/dist/serve/site-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
  241. package/dist/serve/site-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
  242. package/dist/serve/site-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
  243. package/dist/serve/site-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
  244. package/dist/serve/site-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
  245. package/dist/serve/site-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
  246. package/dist/serve/site-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
  247. package/dist/serve/site-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
  248. package/dist/serve/site-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
  249. package/dist/serve/site-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
  250. package/dist/serve/site-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
  251. package/dist/serve/site-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
  252. package/dist/serve/site-ui/docs/pyric-ui-storage-reference-api/index.html +2 -1
  253. package/dist/serve/site-ui/docs/pyric-ui-storage-reference-api.md +1 -0
  254. package/dist/serve/site-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
  255. package/dist/serve/site-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
  256. package/dist/serve/site-ui/docs/rules-compat/index.html +87 -79
  257. package/dist/serve/site-ui/docs/rules-compat.md +86 -78
  258. package/dist/serve/site-ui/docs/secure/audit-your-rules/index.html +1 -1
  259. package/dist/serve/site-ui/docs/secure/firestore-rules-limits/index.html +1 -1
  260. package/dist/serve/site-ui/docs/secure/read-a-denial/index.html +1 -1
  261. package/dist/serve/site-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
  262. package/dist/serve/site-ui/docs/secure/rules-standard-library/index.html +1 -1
  263. package/dist/serve/site-ui/docs/secure/secure-it-with-rules/index.html +1 -1
  264. package/dist/serve/site-ui/docs/secure/simulate-and-lint/index.html +1 -1
  265. package/dist/serve/site-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
  266. package/dist/serve/site-ui/docs/ship/ship-to-production/index.html +1 -1
  267. package/dist/serve/site-ui/docs/ship/test-in-node/index.html +1 -1
  268. package/dist/serve/site-ui/docs/storage-compat/index.html +119 -125
  269. package/dist/serve/site-ui/docs/storage-compat.md +118 -124
  270. package/dist/serve/site-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
  271. package/dist/serve/site-ui/docs/trust/rules-standard-library/index.html +1 -1
  272. package/dist/serve/site-ui/docs/trust/storage-rules-stdlib-research/index.html +1 -1
  273. package/dist/serve/site-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
  274. package/dist/serve/site-ui/docs/tutorial/index.html +1 -1
  275. package/dist/serve/site-ui/examples/chess/index.html +1 -1
  276. package/dist/serve/site-ui/examples/firestore-first-write/index.html +1 -1
  277. package/dist/serve/site-ui/firestore/index.html +2 -2
  278. package/dist/serve/site-ui/index.html +2 -2
  279. package/dist/serve/site-ui/llms.txt +6 -2
  280. package/dist/serve/site-ui/rtdb/index.html +2 -2
  281. package/dist/serve/site-ui/settings/index.html +2 -2
  282. package/dist/serve/site-ui/storage/index.html +2 -2
  283. package/dist/serve/site-ui/traffic/index.html +2 -2
  284. package/dist/serve/vite-ai-config.d.ts +4 -1
  285. package/dist/serve/vite-ai-config.d.ts.map +1 -1
  286. package/dist/serve/vite-ai-config.js +83 -19
  287. package/dist/serve/vite-ai-config.js.map +1 -1
  288. package/dist/serve/vite-generation-rules-watch.d.ts.map +1 -1
  289. package/dist/serve/vite-generation-rules-watch.js +61 -14
  290. package/dist/serve/vite-generation-rules-watch.js.map +1 -1
  291. package/dist/serve/vite-module-swap.d.ts +4 -1
  292. package/dist/serve/vite-module-swap.d.ts.map +1 -1
  293. package/dist/serve/vite-module-swap.js +106 -21
  294. package/dist/serve/vite-module-swap.js.map +1 -1
  295. package/dist/serve/vite-plugin.d.ts.map +1 -1
  296. package/dist/serve/vite-plugin.js +45 -5
  297. package/dist/serve/vite-plugin.js.map +1 -1
  298. package/dist/serve/vite-sandbox-generation.js +1 -1
  299. package/dist/serve/vite-sandbox-generation.js.map +1 -1
  300. package/dist/serve/worker/client/resumable.d.ts +30 -0
  301. package/dist/serve/worker/client/resumable.d.ts.map +1 -0
  302. package/dist/serve/worker/client/resumable.js +416 -0
  303. package/dist/serve/worker/client/resumable.js.map +1 -0
  304. package/dist/serve/worker/client/rtdb-references.d.ts.map +1 -1
  305. package/dist/serve/worker/client/rtdb-references.js +4 -1
  306. package/dist/serve/worker/client/rtdb-references.js.map +1 -1
  307. package/dist/serve/worker/client/storage.d.ts +16 -2
  308. package/dist/serve/worker/client/storage.d.ts.map +1 -1
  309. package/dist/serve/worker/client/storage.js +57 -6
  310. package/dist/serve/worker/client/storage.js.map +1 -1
  311. package/dist/serve/worker/host/rules.d.ts.map +1 -1
  312. package/dist/serve/worker/host/rules.js +2 -1
  313. package/dist/serve/worker/host/rules.js.map +1 -1
  314. package/dist/serve/worker/index.d.ts +1 -1
  315. package/dist/serve/worker/index.d.ts.map +1 -1
  316. package/dist/serve/worker/index.js +1 -1
  317. package/dist/serve/worker/index.js.map +1 -1
  318. package/dist/serve/worker/serve-init.d.ts.map +1 -1
  319. package/dist/serve/worker/serve-init.js +31 -0
  320. package/dist/serve/worker/serve-init.js.map +1 -1
  321. package/package.json +15 -4
  322. package/src/assurance/.generated/conformance-verdicts.ts +80 -72
  323. package/src/cli/cli.test.ts +14 -0
  324. package/src/cli/database-rules.ts +6 -4
  325. package/src/cli/dev-runner.ts +21 -0
  326. package/src/cli/serve.ts +62 -5
  327. package/src/cli/verify.ts +2 -2
  328. package/src/conformance/.generated/can-i-use-browser.ts +1 -1
  329. package/src/conformance/.generated/can-i-use.ts +1 -1
  330. package/src/conformance/.generated/conformance-docs.ts +1 -1
  331. package/src/next/client-aliases.ts +39 -0
  332. package/src/next/dev-rewrites.ts +77 -0
  333. package/src/next/guard.ts +61 -0
  334. package/src/next/index.ts +16 -0
  335. package/src/next/runtime-env.ts +18 -0
  336. package/src/next/server-external-packages.ts +43 -0
  337. package/src/next/target-url.ts +46 -0
  338. package/src/next/turbopack-config.ts +54 -0
  339. package/src/next/types.ts +47 -0
  340. package/src/next/webpack-config.ts +71 -0
  341. package/src/next/with-pyric.ts +58 -0
  342. package/src/register/app-bridge.ts +79 -0
  343. package/src/register/hooks.ts +6 -1
  344. package/src/register/index.ts +1 -1
  345. package/src/register/mapping.ts +50 -4
  346. package/src/rtdb/rules-json.ts +110 -0
  347. package/src/serve/bundler.ts +2 -0
  348. package/src/serve/discovery.ts +65 -44
  349. package/src/serve/entries/app-ai-passthrough.ts +83 -0
  350. package/src/serve/entries/auth-helper-core.ts +9 -7
  351. package/src/serve/entries/auth.ts +1 -0
  352. package/src/serve/entries/bridge-url.ts +9 -2
  353. package/src/serve/entries/database.ts +1 -0
  354. package/src/serve/entries/firestore.ts +1 -0
  355. package/src/serve/entries/init.ts +5 -2
  356. package/src/serve/entries/runtime.ts +25 -0
  357. package/src/serve/entries/storage.ts +12 -1
  358. package/src/serve/html-injection.ts +26 -1
  359. package/src/serve/rules.ts +2 -2
  360. package/src/serve/runtime/chip-config.ts +19 -6
  361. package/src/serve/sandbox-session.ts +31 -2
  362. package/src/serve/vite-ai-config.ts +86 -19
  363. package/src/serve/vite-generation-rules-watch.ts +67 -15
  364. package/src/serve/vite-module-swap.ts +121 -21
  365. package/src/serve/vite-plugin.ts +44 -6
  366. package/src/serve/vite-sandbox-generation.ts +1 -1
  367. package/src/serve/worker/client/resumable.ts +506 -0
  368. package/src/serve/worker/client/rtdb-references.ts +4 -1
  369. package/src/serve/worker/client/storage.ts +77 -7
  370. package/src/serve/worker/host/rules.ts +2 -1
  371. package/src/serve/worker/index.ts +4 -0
  372. package/src/serve/worker/serve-init.ts +30 -0
  373. package/dist/serve/site-ui/_astro/_studio_.BtYpqe1A.css +0 -1
  374. package/dist/serve/site-ui/_astro/app.4HeLP_FA.js +0 -109
  375. package/dist/serve/site-ui/_astro/app.BgSnCR5G.js +0 -1
  376. package/dist/serve/site-ui/_astro/index.BJiOemMX.js +0 -1
  377. package/dist/serve/site-ui/_astro/reads.BrKNdKAa.js +0 -1
  378. package/dist/serve/site-ui/_astro/sandbox-controls.BTb3EbQp.js +0 -1153
@@ -28,11 +28,11 @@
28
28
  <div class="compat-list">
29
29
  <details class="compat-row" data-status="ok">
30
30
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">getAuth(target)</code><span class="compat-sub"><span class="compat-behavior">Returns a stable <code>Auth</code> handle for repeated calls with the same input</span></span></span></summary>
31
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-anonymous.test.ts</code> + canonical Node register child (<code>register-child.test.ts</code>)</div></div>
31
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-anonymous.test.ts#L13" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-anonymous.test.ts</code></a> + canonical Node register child (<a href="https://github.com/davideast/pyric/blob/main/packages/cli/test/register/register-child.test.ts#L152" target="_blank" rel="noopener noreferrer"><code>register-child.test.ts</code></a>)</div></div>
32
32
  </details>
33
33
  <details class="compat-row" data-status="ok">
34
34
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">getAuth(target)</code><span class="compat-sub"><span class="compat-behavior"><code>getAuth(sandbox)</code> dispatches to the sandbox backend</span></span></span></summary>
35
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-anonymous.test.ts</code></div></div>
35
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-anonymous.test.ts#L13" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-anonymous.test.ts</code></a></div></div>
36
36
  </details>
37
37
  <details class="compat-row" data-status="unverified">
38
38
  <summary class="compat-line"><span class="compat-dot" data-status="unverified" role="img" aria-label="Unverified" title="Unverified"></span><span class="compat-main"><code class="compat-api">getAuth(target)</code><span class="compat-sub"><span class="compat-behavior">Without sandbox package swapping, canonical <code>firebase/auth</code> imports remain Firebase and never enter this mirror</span></span></span></summary>
@@ -40,12 +40,12 @@
40
40
  </details>
41
41
  <details class="compat-row" data-status="ok">
42
42
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">getAuth(target)</code><span class="compat-sub"><span class="compat-behavior">After package swapping, bare <code>getAuth()</code> resolves the registered default sandbox app; without swapping, Firebase retains its <code>app/no-app</code> behavior when no default app exists</span></span></span></summary>
43
- <div class="compat-evidence"><div class="compat-probe">canonical Node register child (<code>register-child.test.ts</code>) + oracle: <code>packages/conformance/observations/auth/auth-bare-getauth-no-default-app.json</code> (<code>code: 'app/no-app'</code> against blockingfun, fb-js-sdk 12.13.0 — confirms unswapped Firebase behavior)</div>
43
+ <div class="compat-evidence"><div class="compat-probe">canonical Node register child (<a href="https://github.com/davideast/pyric/blob/main/packages/cli/test/register/register-child.test.ts#L152" target="_blank" rel="noopener noreferrer"><code>register-child.test.ts</code></a>) + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-bare-getauth-no-default-app.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-bare-getauth-no-default-app.json</code></a> (<code>code: 'app/no-app'</code> against blockingfun, fb-js-sdk 12.13.0 — confirms unswapped Firebase behavior)</div>
44
44
  <div class="compat-note">(wrap)</div></div>
45
45
  </details>
46
46
  <details class="compat-row" data-status="ok">
47
47
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">getAuth(target)</code><span class="compat-sub"><span class="compat-behavior"><code>auth.currentUser</code> is a live getter, not a snapshot — reads through to the backend on every access</span></span></span></summary>
48
- <div class="compat-evidence"><div class="compat-probe">implicit in <code>unit:sandbox-anonymous.test.ts</code></div></div>
48
+ <div class="compat-evidence"><div class="compat-probe">implicit in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-anonymous.test.ts#L13" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-anonymous.test.ts</code></a></div></div>
49
49
  </details>
50
50
  </div>
51
51
 
@@ -54,11 +54,11 @@
54
54
  <div class="compat-list">
55
55
  <details class="compat-row" data-status="ok">
56
56
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">getAuth(app)</code><span class="compat-sub"><span class="compat-behavior">Equal-config Firebase apps have independent active Auth sessions; signing in or out on one app does not change another app's currentUser</span></span></span></summary>
57
- <div class="compat-evidence"><div class="compat-probe">oracle: <code>app-production-multi-app-topology</code> (firebase 12.13.0, real Chromium against production) + twin replay: <code>packages/pyric/test/app/production-multi-app-oracle.test.ts</code></div></div>
57
+ <div class="compat-evidence"><div class="compat-probe">oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/app/app-production-multi-app-topology.json" target="_blank" rel="noopener noreferrer"><code>app-production-multi-app-topology</code></a> (firebase 12.13.0, real Chromium against production) + twin replay: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/app/production-multi-app-oracle.test.ts#L15" target="_blank" rel="noopener noreferrer"><code>packages/pyric/test/app/production-multi-app-oracle.test.ts</code></a></div></div>
58
58
  </details>
59
59
  <details class="compat-row" data-status="ok">
60
60
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">browserLocalPersistence across same-named app instances in sibling tabs</code><span class="compat-sub"><span class="compat-behavior">After the sibling receives the LOCAL persistence event, same-named Firebase app instances in sibling tabs keep independent active Auth sessions; an anonymous sign-in in one tab does not update the sibling tab's currentUser during the bounded post-signal observation window</span></span></span></summary>
61
- <div class="compat-evidence"><div class="compat-probe">oracle: <code>app-production-cross-tab-auth-persistence</code> (firebase 12.13.0, two real Chromium pages in one browser context) + served twin: <code>packages/cli/test/e2e/app-multi-app.pw.ts</code></div></div>
61
+ <div class="compat-evidence"><div class="compat-probe">oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/app/app-production-cross-tab-auth-persistence.json" target="_blank" rel="noopener noreferrer"><code>app-production-cross-tab-auth-persistence</code></a> (firebase 12.13.0, two real Chromium pages in one browser context) + served twin: <a href="https://github.com/davideast/pyric/blob/main/packages/cli/test/e2e/app-multi-app.pw.ts#L12" target="_blank" rel="noopener noreferrer"><code>packages/cli/test/e2e/app-multi-app.pw.ts</code></a></div></div>
62
62
  </details>
63
63
  </div>
64
64
 
@@ -67,28 +67,28 @@
67
67
  <div class="compat-list">
68
68
  <details class="compat-row" data-status="ok">
69
69
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInAnonymously(auth)</code><span class="compat-sub"><span class="compat-behavior">Returns <code>UserCredential</code> with <code>providerId: null</code>, <code>operationType: 'signIn'</code>, and a <code>User</code> with <code>isAnonymous: true</code>, <code>email: null</code>, <code>displayName: null</code></span></span></span></summary>
70
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-anonymous.test.ts</code>, <code>playground:auth-anonymous</code> (bundled) + <code>playground:auth-row-6-anon-credential-shape</code> (one-claim) + oracle: <code>packages/conformance/observations/auth/auth-anonymous-credential-providerid.json</code> (<code>providerId: null</code> against blockingfun, fb-js-sdk 12.13.0). Prior matrix language said <code>providerId: 'anonymous'</code>; corrected after empirical observation. Sandbox aligned to prod in the same commit.</div></div>
70
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-anonymous.test.ts#L30" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-anonymous.test.ts</code></a>, <code>playground:auth-anonymous</code> (bundled) + <code>playground:auth-row-6-anon-credential-shape</code> (one-claim) + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-anonymous-credential-providerid.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-anonymous-credential-providerid.json</code></a> (<code>providerId: null</code> against blockingfun, fb-js-sdk 12.13.0). Prior matrix language said <code>providerId: 'anonymous'</code>; corrected after empirical observation. Sandbox aligned to prod in the same commit.</div></div>
71
71
  </details>
72
72
  <details class="compat-row" data-status="diverged">
73
73
  <summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">signInAnonymously(auth)</code><span class="compat-sub"><span class="compat-behavior">Auto-generates a uid for fresh sign-ins (sandbox format: <code>anonymous-{N}</code>)</span></span></span></summary>
74
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-anonymous.test.ts</code> — prod uids are 28-char base64-ish; sandbox uses a readable counter for debuggability</div>
74
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-anonymous.test.ts#L7" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-anonymous.test.ts</code></a> — prod uids are 28-char base64-ish; sandbox uses a readable counter for debuggability</div>
75
75
  <div class="compat-note">format</div></div>
76
76
  </details>
77
77
  <details class="compat-row" data-status="ok">
78
78
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInAnonymously(auth)</code><span class="compat-sub"><span class="compat-behavior">If an anonymous user is already signed in, returns the SAME user (no fresh uid mint)</span></span></span></summary>
79
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-anonymous.test.ts</code> ("idempotent while signed in") — fix from #399</div></div>
79
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-anonymous.test.ts#L7" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-anonymous.test.ts</code></a> ("idempotent while signed in") — fix from #399</div></div>
80
80
  </details>
81
81
  <details class="compat-row" data-status="ok">
82
82
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInAnonymously(auth)</code><span class="compat-sub"><span class="compat-behavior">After <code>signOut</code>, a subsequent <code>signInAnonymously</code> mints a fresh uid</span></span></span></summary>
83
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-anonymous.test.ts</code>, <code>playground:auth-anonymous</code> (bundled) + <code>playground:auth-row-9-anon-fresh-uid-after-signout</code> (one-claim)</div></div>
83
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-anonymous.test.ts#L7" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-anonymous.test.ts</code></a>, <code>playground:auth-anonymous</code> (bundled) + <code>playground:auth-row-9-anon-fresh-uid-after-signout</code> (one-claim)</div></div>
84
84
  </details>
85
85
  <details class="compat-row" data-status="ok">
86
86
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInAnonymously(auth)</code><span class="compat-sub"><span class="compat-behavior">Fires <code>onAuthStateChanged</code> exactly once per state transition (no same-value double-fire)</span></span></span></summary>
87
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-listeners.test.ts</code>, <code>playground:auth-anonymous</code> (bundled) + <code>playground:auth-row-10-onauthstatechanged-one-per-transition</code> (one-claim) — fix from #399 + oracle: <code>packages/conformance/observations/auth/auth-row-10-onauthstatechanged-one-per-transition.json</code> (against blockingfun, fb-js-sdk 12.13.0: subscribe → 1 initial null fire → <code>signInAnonymously</code> → +1 → <code>signOut</code> → +1 → <code>signInAnonymously</code> → +1. <code>eachTransitionFiredExactlyOnce: true</code> — every transition produces exactly one fire)</div></div>
87
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-listeners.test.ts#L24" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-listeners.test.ts</code></a>, <code>playground:auth-anonymous</code> (bundled) + <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-10-onauthstatechanged-one-per-transition.json" target="_blank" rel="noopener noreferrer"><code>playground:auth-row-10-onauthstatechanged-one-per-transition</code></a> (one-claim) — fix from #399 + oracle: <code>packages/conformance/observations/auth/auth-row-10-onauthstatechanged-one-per-transition.json</code> (against blockingfun, fb-js-sdk 12.13.0: subscribe → 1 initial null fire → <code>signInAnonymously</code> → +1 → <code>signOut</code> → +1 → <code>signInAnonymously</code> → +1. <code>eachTransitionFiredExactlyOnce: true</code> — every transition produces exactly one fire)</div></div>
88
88
  </details>
89
89
  <details class="compat-row" data-status="ok">
90
90
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInAnonymously(auth)</code><span class="compat-sub"><span class="compat-behavior">Writes through to <code>sandbox.currentUser</code> so rules engines see <code>request.auth.uid</code></span></span></span></summary>
91
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-anonymous.test.ts</code>, <code>playground:auth-anonymous</code> (bundled) + <code>playground:auth-row-11-anon-uid-visible-to-rules</code> (one-claim)</div></div>
91
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-anonymous.test.ts#L7" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-anonymous.test.ts</code></a>, <code>playground:auth-anonymous</code> (bundled) + <code>playground:auth-row-11-anon-uid-visible-to-rules</code> (one-claim)</div></div>
92
92
  </details>
93
93
  <details class="compat-row" data-status="diverged">
94
94
  <summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">signInAnonymously(auth)</code><span class="compat-sub"><span class="compat-behavior">Anonymous users persist across page reload via configured <code>Persistence</code> (prod only — sandbox has no persistence layer)</span></span></span></summary>
@@ -101,19 +101,19 @@
101
101
  <div class="compat-list">
102
102
  <details class="compat-row" data-status="ok">
103
103
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithEmailAndPassword(auth, email, password)</code><span class="compat-sub"><span class="compat-behavior">Returns <code>UserCredential</code> with <code>providerId: null</code> (NOT <code>'password'</code> — only OAuth/phone responses carry a providerId; upstream <code>providerIdForResponse</code> returns null for email/password), <code>operationType: 'signIn'</code>, and a <code>User</code> with the stored uid + email</span></span></span></summary>
104
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-email-password.test.ts</code> + oracle: <code>packages/conformance/observations/auth/auth-createUser-operationType.json</code> (<code>providerId: null</code> against blockingfun, fb-js-sdk 12.13.0). Prior matrix language said <code>providerId: 'password'</code>; corrected after the oracle contradicted it (AUTH-B2).</div></div>
104
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-password.test.ts#L29" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-password.test.ts</code></a> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-createUser-operationType.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-createUser-operationType.json</code></a> (<code>providerId: null</code> against blockingfun, fb-js-sdk 12.13.0). Prior matrix language said <code>providerId: 'password'</code>; corrected after the oracle contradicted it (AUTH-B2).</div></div>
105
105
  </details>
106
106
  <details class="compat-row" data-status="ok">
107
107
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithEmailAndPassword(auth, email, password)</code><span class="compat-sub"><span class="compat-behavior">Throws <code>auth/user-not-found</code> when the email isn't seeded / hasn't been created</span></span></span></summary>
108
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-email-password.test.ts</code> + oracle: <code>packages/conformance/observations/auth/auth-user-not-found-error-code.json</code> (<code>code: 'auth/user-not-found'</code> against blockingfun, fb-js-sdk 12.13.0; matches sandbox)</div></div>
108
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-password.test.ts#L4" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-password.test.ts</code></a> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-user-not-found-error-code.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-user-not-found-error-code.json</code></a> (<code>code: 'auth/user-not-found'</code> against blockingfun, fb-js-sdk 12.13.0; matches sandbox)</div></div>
109
109
  </details>
110
110
  <details class="compat-row" data-status="ok">
111
111
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithEmailAndPassword(auth, email, password)</code><span class="compat-sub"><span class="compat-behavior">Throws <code>auth/wrong-password</code> when the password doesn't match</span></span></span></summary>
112
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-email-password.test.ts</code>, <code>playground:auth-email-password</code> (bundled) + <code>playground:auth-row-15-wrong-password-rejects</code> (one-claim) + oracle: <code>packages/conformance/observations/auth/auth-wrong-password-error-code.json</code> (<code>code: 'auth/wrong-password'</code> against blockingfun, fb-js-sdk 12.13.0; matches sandbox)</div></div>
112
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-password.test.ts#L4" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-password.test.ts</code></a>, <code>playground:auth-email-password</code> (bundled) + <code>playground:auth-row-15-wrong-password-rejects</code> (one-claim) + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-wrong-password-error-code.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-wrong-password-error-code.json</code></a> (<code>code: 'auth/wrong-password'</code> against blockingfun, fb-js-sdk 12.13.0; matches sandbox)</div></div>
113
113
  </details>
114
114
  <details class="compat-row" data-status="diverged">
115
115
  <summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">signInWithEmailAndPassword(auth, email, password)</code><span class="compat-sub"><span class="compat-behavior">An EMPTY password throws <code>auth/missing-password</code> (message "A non-empty password must be provided"), fired before the user-DB lookup so it can't be used to enumerate seeded emails. Upstream maps the <code>MISSING_PASSWORD</code> server error (<code>core/errors.ts:92,282,563</code>). ⚠ best-known semantics — message text not yet captured against a live project (STOP-flagged for an oracle pass; the <code>.code</code> is the load-bearing part).</span></span></span></summary>
116
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-cluster-b9-b12.test.ts</code> (locks AUTH-B11)</div></div>
116
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-cluster-b9-b12.test.ts#L18" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-cluster-b9-b12.test.ts</code></a> (locks AUTH-B11)</div></div>
117
117
  </details>
118
118
  <details class="compat-row" data-status="ok">
119
119
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithEmailAndPassword(auth, email, password)</code><span class="compat-sub"><span class="compat-behavior">Re-signing in after <code>signOut</code> returns the <strong>same</strong> uid (passwords persist within the sandbox lifetime)</span></span></span></summary>
@@ -121,19 +121,19 @@
121
121
  </details>
122
122
  <details class="compat-row" data-status="ok">
123
123
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithEmailAndPassword(auth, email, password)</code><span class="compat-sub"><span class="compat-behavior">Fires <code>onAuthStateChanged</code> with the new user once</span></span></span></summary>
124
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-listeners.test.ts</code> + oracle: <code>packages/conformance/observations/auth/auth-row-17-signin-email-password-fires-once.json</code> (against blockingfun, fb-js-sdk 12.13.0: createUser → signOut → subscribe (1 initial null fire) → <code>signInWithEmailAndPassword</code> → <code>firesForSignIn: 1</code> with the signed-in uid, <code>lastFireUidMatches: true</code>)</div></div>
124
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-listeners.test.ts#L25" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-listeners.test.ts</code></a> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-17-signin-email-password-fires-once.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-row-17-signin-email-password-fires-once.json</code></a> (against blockingfun, fb-js-sdk 12.13.0: createUser → signOut → subscribe (1 initial null fire) → <code>signInWithEmailAndPassword</code> → <code>firesForSignIn: 1</code> with the signed-in uid, <code>lastFireUidMatches: true</code>)</div></div>
125
125
  </details>
126
126
  <details class="compat-row" data-status="ok">
127
127
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithEmailAndPassword(auth, email, password)</code><span class="compat-sub"><span class="compat-behavior">Email validation (RFC 5322ish) — rejects empty, missing <code>@</code>, missing local-part, missing domain with <code>auth/invalid-email</code>. Runs on both <code>signInWithEmailAndPassword</code> and <code>createUserWithEmailAndPassword</code> before any user-DB lookup, so consumers shipping malformed input see the same error sandbox vs prod.</span></span></span></summary>
128
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-email-password.test.ts</code> + oracle: <code>packages/conformance/observations/auth/auth-row-18-invalid-email-error-code.json</code> (<code>code: 'auth/invalid-email'</code>, message <code>Firebase: Error (auth/invalid-email).</code> against blockingfun, fb-js-sdk 12.13.0)</div></div>
128
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-password.test.ts#L151" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-password.test.ts</code></a> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-18-invalid-email-error-code.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-row-18-invalid-email-error-code.json</code></a> (<code>code: 'auth/invalid-email'</code>, message <code>Firebase: Error (auth/invalid-email).</code> against blockingfun, fb-js-sdk 12.13.0)</div></div>
129
129
  </details>
130
130
  <details class="compat-row" data-status="ok">
131
131
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithEmailAndPassword(auth, email, password)</code><span class="compat-sub"><span class="compat-behavior">Password strength requirements — rejects passwords shorter than 6 chars with <code>auth/weak-password</code> on <code>createUserWithEmailAndPassword</code>. Strength is NOT enforced on <code>signInWithEmailAndPassword</code> so previously-seeded weak passwords still let the user in (matches prod's separation of registration vs sign-in).</span></span></span></summary>
132
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-email-password.test.ts</code> + oracle: <code>packages/conformance/observations/auth/auth-row-19-weak-password-error-code.json</code> (<code>code: 'auth/weak-password'</code>, message <code>Firebase: Password should be at least 6 characters (auth/weak-password).</code> against blockingfun, fb-js-sdk 12.13.0; matrix language "≥6 chars per prod default" empirically confirmed)</div></div>
132
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-password.test.ts#L225" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-password.test.ts</code></a> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-19-weak-password-error-code.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-row-19-weak-password-error-code.json</code></a> (<code>code: 'auth/weak-password'</code>, message <code>Firebase: Password should be at least 6 characters (auth/weak-password).</code> against blockingfun, fb-js-sdk 12.13.0; matrix language "≥6 chars per prod default" empirically confirmed)</div></div>
133
133
  </details>
134
134
  <details class="compat-row" data-status="ok">
135
135
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithEmailAndPassword(auth, email, password)</code><span class="compat-sub"><span class="compat-behavior">Disabled accounts (<code>sandbox.updateUser(…, {disabled: true})</code>) reject sign-in with <code>auth/user-disabled</code> and prod's documented message (<code>The user account has been disabled by an administrator.</code>). Sandbox checks disabled BEFORE the password compare (anti-probing); the exact prod ordering of disabled-vs-wrong-password needs an oracle capture</span></span></span></summary>
136
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-user-admin.test.ts</code> ("disabled users")</div>
136
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-user-admin.test.ts#L19" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-user-admin.test.ts</code></a> ("disabled users")</div>
137
137
  <div class="compat-note">code / ? ordering</div></div>
138
138
  </details>
139
139
  </div>
@@ -143,23 +143,23 @@
143
143
  <div class="compat-list">
144
144
  <details class="compat-row" data-status="ok">
145
145
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">createUserWithEmailAndPassword(auth, email, password)</code><span class="compat-sub"><span class="compat-behavior">Creates a new user, signs them in automatically (currentUser becomes the new user)</span></span></span></summary>
146
- <div class="compat-evidence"><div class="compat-probe"><code>playground:auth-email-password</code> (bundled) + <code>playground:auth-row-20-create-user-auto-signs-in</code> (one-claim), <code>unit:sandbox-email-password.test.ts</code></div></div>
146
+ <div class="compat-evidence"><div class="compat-probe"><code>playground:auth-email-password</code> (bundled) + <code>playground:auth-row-20-create-user-auto-signs-in</code> (one-claim), <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-password.test.ts#L6" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-password.test.ts</code></a></div></div>
147
147
  </details>
148
148
  <details class="compat-row" data-status="ok">
149
149
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">createUserWithEmailAndPassword(auth, email, password)</code><span class="compat-sub"><span class="compat-behavior">Returns <code>UserCredential</code> with <code>operationType: 'signIn'</code> (NOT <code>'register'</code> — matches prod)</span></span></span></summary>
150
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-email-password.test.ts</code> + oracle: <code>packages/conformance/observations/auth/auth-createUser-operationType.json</code> (<code>operationType: 'signIn'</code> against blockingfun, fb-js-sdk 12.13.0; matches sandbox)</div></div>
150
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-password.test.ts#L29" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-password.test.ts</code></a> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-createUser-operationType.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-createUser-operationType.json</code></a> (<code>operationType: 'signIn'</code> against blockingfun, fb-js-sdk 12.13.0; matches sandbox) (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/oracle-conformance.test.ts#L145" target="_blank" rel="noopener noreferrer"><code>oracle-conformance.test.ts</code></a>)</div></div>
151
151
  </details>
152
152
  <details class="compat-row" data-status="ok">
153
153
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">createUserWithEmailAndPassword(auth, email, password)</code><span class="compat-sub"><span class="compat-behavior">Throws <code>auth/email-already-in-use</code> when the email is already registered</span></span></span></summary>
154
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-email-password.test.ts</code> + oracle: <code>packages/conformance/observations/auth/auth-email-already-in-use-error-code.json</code> (<code>code: 'auth/email-already-in-use'</code> against blockingfun, fb-js-sdk 12.13.0; matches sandbox)</div></div>
154
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-password.test.ts#L6" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-password.test.ts</code></a> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-email-already-in-use-error-code.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-email-already-in-use-error-code.json</code></a> (<code>code: 'auth/email-already-in-use'</code> against blockingfun, fb-js-sdk 12.13.0; matches sandbox)</div></div>
155
155
  </details>
156
156
  <details class="compat-row" data-status="ok">
157
157
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">createUserWithEmailAndPassword(auth, email, password)</code><span class="compat-sub"><span class="compat-behavior">The created user has <code>isAnonymous: false</code>, <code>email: &lt;input&gt;</code>, <code>displayName: null</code></span></span></span></summary>
158
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-email-password.test.ts</code> + <code>playground:auth-row-23-create-user-shape</code> (one-claim)</div></div>
158
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-password.test.ts#L6" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-password.test.ts</code></a> + <code>playground:auth-row-23-create-user-shape</code> (one-claim)</div></div>
159
159
  </details>
160
160
  <details class="compat-row" data-status="ok">
161
161
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">createUserWithEmailAndPassword(auth, email, password)</code><span class="compat-sub"><span class="compat-behavior">Fires <code>onAuthStateChanged</code> with the new user once</span></span></span></summary>
162
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-listeners.test.ts</code> + oracle: <code>packages/conformance/observations/auth/auth-row-24-createuser-fires-once.json</code> (against blockingfun, fb-js-sdk 12.13.0: subscribe (1 initial null fire) → <code>createUserWithEmailAndPassword</code> → <code>firesForCreate: 1</code> with the newly-created uid, <code>lastFireUidMatches: true</code>)</div></div>
162
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-listeners.test.ts#L23" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-listeners.test.ts</code></a> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-24-createuser-fires-once.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-row-24-createuser-fires-once.json</code></a> (against blockingfun, fb-js-sdk 12.13.0: subscribe (1 initial null fire) → <code>createUserWithEmailAndPassword</code> → <code>firesForCreate: 1</code> with the newly-created uid, <code>lastFireUidMatches: true</code>)</div></div>
163
163
  </details>
164
164
  </div>
165
165
 
@@ -168,15 +168,15 @@
168
168
  <div class="compat-list">
169
169
  <details class="compat-row" data-status="ok">
170
170
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signOut(auth)</code><span class="compat-sub"><span class="compat-behavior">Sets <code>currentUser</code> to <code>null</code> synchronously after resolution</span></span></span></summary>
171
- <div class="compat-evidence"><div class="compat-probe"><code>playground:auth-anonymous</code> (bundled) + <code>playground:auth-row-25-signout-currentuser-null</code> (one-claim), <code>unit:sandbox-anonymous.test.ts</code> + oracle: <code>packages/conformance/observations/auth/auth-row-25-signout-currentuser-null-sync.json</code> (<code>currentUserIsNullSync: true</code> against blockingfun, fb-js-sdk 12.13.0 — <code>auth.currentUser</code> read in the synchronous continuation immediately after <code>await signOut(auth)</code> is already <code>null</code>, with no microtask/macrotask required to settle)</div></div>
171
+ <div class="compat-evidence"><div class="compat-probe"><code>playground:auth-anonymous</code> (bundled) + <code>playground:auth-row-25-signout-currentuser-null</code> (one-claim), <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-anonymous.test.ts#L7" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-anonymous.test.ts</code></a> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-25-signout-currentuser-null-sync.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-row-25-signout-currentuser-null-sync.json</code></a> (<code>currentUserIsNullSync: true</code> against blockingfun, fb-js-sdk 12.13.0 — <code>auth.currentUser</code> read in the synchronous continuation immediately after <code>await signOut(auth)</code> is already <code>null</code>, with no microtask/macrotask required to settle)</div></div>
172
172
  </details>
173
173
  <details class="compat-row" data-status="ok">
174
174
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signOut(auth)</code><span class="compat-sub"><span class="compat-behavior">Fires <code>onAuthStateChanged</code> with <code>null</code> exactly once</span></span></span></summary>
175
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-listeners.test.ts</code>, <code>playground:auth-anonymous</code> (bundled) + <code>playground:auth-row-26-signout-fires-null-once</code> (one-claim) + oracle: <code>packages/conformance/observations/auth/auth-row-26-signout-fires-null-once.json</code> (against blockingfun, fb-js-sdk 12.13.0: subscribe (1 initial null fire) → <code>signInAnonymously</code> → <code>signOut</code> → <code>firesForSignOut: 1</code> with <code>lastFireUidWasNull: true</code>)</div></div>
175
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-listeners.test.ts#L26" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-listeners.test.ts</code></a>, <code>playground:auth-anonymous</code> (bundled) + <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-26-signout-fires-null-once.json" target="_blank" rel="noopener noreferrer"><code>playground:auth-row-26-signout-fires-null-once</code></a> (one-claim) + oracle: <code>packages/conformance/observations/auth/auth-row-26-signout-fires-null-once.json</code> (against blockingfun, fb-js-sdk 12.13.0: subscribe (1 initial null fire) → <code>signInAnonymously</code> → <code>signOut</code> → <code>firesForSignOut: 1</code> with <code>lastFireUidWasNull: true</code>)</div></div>
176
176
  </details>
177
177
  <details class="compat-row" data-status="ok">
178
178
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signOut(auth)</code><span class="compat-sub"><span class="compat-behavior">Idempotent — <code>signOut</code> on already-signed-out user is a no-op (no listener fire)</span></span></span></summary>
179
- <div class="compat-evidence"><div class="compat-probe"><code>playground:auth-signout-idempotent</code> + oracle-confirmed: <code>packages/conformance/observations/auth/auth-signout-idempotent.json</code> (<code>threw: false, redundantSignOutFiredListener: false</code> against blockingfun)</div></div>
179
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-signout-idempotent.json" target="_blank" rel="noopener noreferrer"><code>playground:auth-signout-idempotent</code></a> + oracle-confirmed: <code>packages/conformance/observations/auth/auth-signout-idempotent.json</code> (<code>threw: false, redundantSignOutFiredListener: false</code> against blockingfun)</div></div>
180
180
  </details>
181
181
  <details class="compat-row" data-status="diverged">
182
182
  <summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">signOut(auth)</code><span class="compat-sub"><span class="compat-behavior">Clears the active session's persistence in prod; sandbox has no persistence</span></span></span></summary>
@@ -189,47 +189,47 @@
189
189
  <div class="compat-list">
190
190
  <details class="compat-row" data-status="ok">
191
191
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">onAuthStateChanged(auth, observer)</code><span class="compat-sub"><span class="compat-behavior">Fires immediately on subscribe with the current value (microtask-deferred)</span></span></span></summary>
192
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-listeners.test.ts</code> + oracle: <code>packages/conformance/observations/auth/auth-row-29-onauthstatechanged-initial-fire-timing.json</code> (<code>firedSynchronously: 0, firedAfterMicrotask: 1</code> against blockingfun, fb-js-sdk 12.13.0 — initial fire does NOT arrive in the synchronous tick of <code>onAuthStateChanged(...)</code>; it lands after the first microtask flush)</div></div>
192
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-listeners.test.ts#L5" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-listeners.test.ts</code></a> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-29-onauthstatechanged-initial-fire-timing.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-row-29-onauthstatechanged-initial-fire-timing.json</code></a> (<code>firedSynchronously: 0, firedAfterMicrotask: 1</code> against blockingfun, fb-js-sdk 12.13.0 — initial fire does NOT arrive in the synchronous tick of <code>onAuthStateChanged(...)</code>; it lands after the first microtask flush)</div></div>
193
193
  </details>
194
194
  <details class="compat-row" data-status="ok">
195
195
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">onAuthStateChanged(auth, observer)</code><span class="compat-sub"><span class="compat-behavior">Fires on every subsequent identity change</span></span></span></summary>
196
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-listeners.test.ts</code>, <code>playground:auth-anonymous</code> + oracle: <code>packages/conformance/observations/auth/auth-row-30-onauthstatechanged-fires-on-every-transition.json</code> (against blockingfun, fb-js-sdk 12.13.0: subscribe → <code>signIn</code> → <code>signOut</code> → <code>signIn</code> → <code>signOut</code>, each of the 4 transitions produced exactly 1 fire; <code>eachTransitionFiredExactlyOnce: true</code>)</div></div>
196
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-listeners.test.ts#L5" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-listeners.test.ts</code></a>, <code>playground:auth-anonymous</code> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-30-onauthstatechanged-fires-on-every-transition.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-row-30-onauthstatechanged-fires-on-every-transition.json</code></a> (against blockingfun, fb-js-sdk 12.13.0: subscribe → <code>signIn</code> → <code>signOut</code> → <code>signIn</code> → <code>signOut</code>, each of the 4 transitions produced exactly 1 fire; <code>eachTransitionFiredExactlyOnce: true</code>)</div></div>
197
197
  </details>
198
198
  <details class="compat-row" data-status="ok">
199
199
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">onAuthStateChanged(auth, observer)</code><span class="compat-sub"><span class="compat-behavior">Dedups by uid: a same-uid update (profile-shape change, or a same-uid re-sign-in) does NOT re-fire <code>onAuthStateChanged</code> — only an actual uid change does. Mirrors upstream <code>notifyAuthListeners</code>'s <code>lastNotifiedUid</code> gate (<code>auth_impl.ts:718-723</code>). (<code>onIdTokenChanged</code> still fires on those same-uid updates — see row 38a.)</span></span></span></summary>
200
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-uid-dedup.test.ts</code> (locks AUTH-B7 / B8)</div></div>
200
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-uid-dedup.test.ts#L7" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-uid-dedup.test.ts</code></a> (locks AUTH-B7 / B8)</div></div>
201
201
  </details>
202
202
  <details class="compat-row" data-status="ok">
203
203
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">onAuthStateChanged(auth, observer)</code><span class="compat-sub"><span class="compat-behavior"><strong>No duplicate fire</strong> when subscribe is followed by a synchronous <code>setCurrentUser</code> — dedup ensures observer sees the new value once, not twice. Sandbox-only concern: prod has no synchronous state-change API, so the dedup window can't be exercised against the cloud SDK; subscribe-then-async-signIn naturally fires twice (initial + new value) because the microtask between them flushes the initial fire</span></span></span></summary>
204
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-listeners.test.ts</code> (regression test from #399), <code>playground:auth-anonymous</code> + oracle baseline: <code>packages/conformance/observations/auth/auth-row-31-onauthstatechanged-no-dup-on-sync-transition.json</code> (against blockingfun, fb-js-sdk 12.13.0: subscribe + async <code>signInAnonymously</code> in the same tick produces <code>totalFires: 2</code> — <code>{uid: null, ts: 0}</code> then <code>{uid: &lt;user&gt;, ts: ~400ms}</code>. Confirms prod cannot exhibit the same-tick race; the dedup behavior remains a sandbox-only property)</div></div>
204
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-listeners.test.ts#L5" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-listeners.test.ts</code></a> (regression test from #399), <code>playground:auth-anonymous</code> + oracle baseline: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-31-onauthstatechanged-no-dup-on-sync-transition.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-row-31-onauthstatechanged-no-dup-on-sync-transition.json</code></a> (against blockingfun, fb-js-sdk 12.13.0: subscribe + async <code>signInAnonymously</code> in the same tick produces <code>totalFires: 2</code> — <code>{uid: null, ts: 0}</code> then <code>{uid: &lt;user&gt;, ts: ~400ms}</code>. Confirms prod cannot exhibit the same-tick race; the dedup behavior remains a sandbox-only property)</div></div>
205
205
  </details>
206
206
  <details class="compat-row" data-status="ok">
207
207
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">onAuthStateChanged(auth, observer)</code><span class="compat-sub"><span class="compat-behavior">Returned <code>Unsubscribe</code> removes the observer; subsequent state changes do NOT fire it</span></span></span></summary>
208
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-listeners.test.ts</code>, <code>playground:auth-listener-unsub</code> (bundled) + <code>playground:auth-row-32-unsubscribe-stops-fires</code> (one-claim) + oracle: <code>packages/conformance/observations/auth/auth-row-32-unsubscribe-stops-fires.json</code> (against blockingfun, fb-js-sdk 12.13.0: subscribe → 1 fire on <code>signInAnonymously</code> → call <code>unsub()</code> → <code>signOut</code> + <code>signInAnonymously</code> + <code>signOut</code> produce zero further fires; <code>postUnsubFires: 0, unsubscribeStoppedFires: true</code>)</div></div>
208
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-listeners.test.ts#L5" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-listeners.test.ts</code></a>, <code>playground:auth-listener-unsub</code> (bundled) + <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-32-unsubscribe-stops-fires.json" target="_blank" rel="noopener noreferrer"><code>playground:auth-row-32-unsubscribe-stops-fires</code></a> (one-claim) + oracle: <code>packages/conformance/observations/auth/auth-row-32-unsubscribe-stops-fires.json</code> (against blockingfun, fb-js-sdk 12.13.0: subscribe → 1 fire on <code>signInAnonymously</code> → call <code>unsub()</code> → <code>signOut</code> + <code>signInAnonymously</code> + <code>signOut</code> produce zero further fires; <code>postUnsubFires: 0, unsubscribeStoppedFires: true</code>)</div></div>
209
209
  </details>
210
210
  <details class="compat-row" data-status="ok">
211
211
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">onAuthStateChanged(auth, observer)</code><span class="compat-sub"><span class="compat-behavior">Multiple subscribers all fire on each change</span></span></span></summary>
212
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-listeners.test.ts</code> + oracle: <code>packages/conformance/observations/auth/auth-row-33-multiple-subscribers-all-fire.json</code> (against blockingfun, fb-js-sdk 12.13.0: two subscribers registered back-to-back each see 1 initial null fire, +1 on <code>signInAnonymously</code>, +1 on <code>signOut</code>; <code>bothFiredOnSignIn: true, bothFiredOnSignOut: true</code>)</div></div>
212
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-listeners.test.ts#L5" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-listeners.test.ts</code></a> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-33-multiple-subscribers-all-fire.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-row-33-multiple-subscribers-all-fire.json</code></a> (against blockingfun, fb-js-sdk 12.13.0: two subscribers registered back-to-back each see 1 initial null fire, +1 on <code>signInAnonymously</code>, +1 on <code>signOut</code>; <code>bothFiredOnSignIn: true, bothFiredOnSignOut: true</code>)</div></div>
213
213
  </details>
214
214
  <details class="compat-row" data-status="ok">
215
215
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">onAuthStateChanged(auth, observer)</code><span class="compat-sub"><span class="compat-behavior">Registry is array-backed (matches upstream <code>util/subscribe.ts</code>): the SAME observer fn registered N times produces N independent registrations that each fire, and one <code>Unsubscribe</code> removes exactly one registration. A resubscribe of a previously-unsubscribed fn fires its initial value again. (Per-registration initial-fire bookkeeping, not a shared per-observer dedup.)</span></span></span></summary>
216
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-listener-registry.test.ts</code> (locks AUTH-B3 + AUTH-B4)</div></div>
216
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-listener-registry.test.ts#L19" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-listener-registry.test.ts</code></a> (locks AUTH-B3 + AUTH-B4)</div></div>
217
217
  </details>
218
218
  <details class="compat-row" data-status="ok">
219
219
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">onAuthStateChanged(auth, observer)</code><span class="compat-sub"><span class="compat-behavior">Unsubscribing during emission does not skip remaining subscribers (snapshotted iteration)</span></span></span></summary>
220
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-listeners.test.ts</code></div></div>
220
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-listeners.test.ts#L5" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-listeners.test.ts</code></a></div></div>
221
221
  </details>
222
222
  <details class="compat-row" data-status="ok">
223
223
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">onAuthStateChanged(auth, observer)</code><span class="compat-sub"><span class="compat-behavior">A throwing observer doesn't block other observers from firing</span></span></span></summary>
224
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-listeners.test.ts</code> + oracle: <code>packages/conformance/observations/auth/auth-row-35-throwing-observer-doesnt-block-others.json</code> (<code>secondObserverContinuedFiring: true</code> against blockingfun, fb-js-sdk 12.13.0 — observer #1 throws on every call, observer #2 still counts the initial fire AND the post-sign-in fire)</div></div>
224
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-listeners.test.ts#L5" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-listeners.test.ts</code></a> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-35-throwing-observer-doesnt-block-others.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-row-35-throwing-observer-doesnt-block-others.json</code></a> (<code>secondObserverContinuedFiring: true</code> against blockingfun, fb-js-sdk 12.13.0 — observer #1 throws on every call, observer #2 still counts the initial fire AND the post-sign-in fire)</div></div>
225
225
  </details>
226
226
  <details class="compat-row" data-status="ok">
227
227
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">onAuthStateChanged(auth, observer)</code><span class="compat-sub"><span class="compat-behavior">Observer object form (<code>{next, error, complete}</code>) works alongside the function form</span></span></span></summary>
228
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-listeners.test.ts</code> + oracle: <code>packages/conformance/observations/auth/auth-row-36-observer-object-form-works.json</code> (against blockingfun, fb-js-sdk 12.13.0: one observer as <code>NextFn</code>, another as <code>{next, error, complete}</code>. Both forms see 1 initial null fire, +1 on <code>signInAnonymously</code>, +1 on <code>signOut</code>; <code>bothFormsFiredOnSignIn: true, bothFormsFiredOnSignOut: true</code>)</div></div>
228
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-listeners.test.ts#L5" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-listeners.test.ts</code></a> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-36-observer-object-form-works.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-row-36-observer-object-form-works.json</code></a> (against blockingfun, fb-js-sdk 12.13.0: one observer as <code>NextFn</code>, another as <code>{next, error, complete}</code>. Both forms see 1 initial null fire, +1 on <code>signInAnonymously</code>, +1 on <code>signOut</code>; <code>bothFormsFiredOnSignIn: true, bothFormsFiredOnSignOut: true</code>)</div></div>
229
229
  </details>
230
230
  <details class="compat-row" data-status="ok">
231
231
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">onAuthStateChanged(auth, observer)</code><span class="compat-sub"><span class="compat-behavior">Setting the same user twice does NOT double-fire (structural-equality no-op). Sandbox-internal <code>setCurrentUser</code> claim; the prod analog is <code>signInAnonymously</code> called twice in a row (per fix #399, the second call returns the same user).</span></span></span></summary>
232
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-listeners.test.ts</code> + oracle: <code>packages/conformance/observations/auth/auth-row-37-same-user-no-double-fire.json</code> (against blockingfun, fb-js-sdk 12.13.0: first <code>signInAnonymously</code> fires the listener once; second call returns the same uid (<code>sameUserAcrossCalls: true</code>) and does NOT produce a fresh fire (<code>secondSignInProducedFire: false</code>). Prod also recognizes the same-user no-op)</div></div>
232
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-listeners.test.ts#L5" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-listeners.test.ts</code></a> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-37-same-user-no-double-fire.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-row-37-same-user-no-double-fire.json</code></a> (against blockingfun, fb-js-sdk 12.13.0: first <code>signInAnonymously</code> fires the listener once; second call returns the same uid (<code>sameUserAcrossCalls: true</code>) and does NOT produce a fresh fire (<code>secondSignInProducedFire: false</code>). Prod also recognizes the same-user no-op)</div></div>
233
233
  </details>
234
234
  </div>
235
235
 
@@ -238,19 +238,19 @@
238
238
  <div class="compat-list">
239
239
  <details class="compat-row" data-status="ok">
240
240
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">onIdTokenChanged(auth, observer)</code><span class="compat-sub"><span class="compat-behavior">Fires on user change (sandbox shares the auth-state path)</span></span></span></summary>
241
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-listeners.test.ts</code> + oracle: <code>packages/conformance/observations/auth/auth-row-38-onidtokenchanged-fires-on-user-change.json</code> (against blockingfun, fb-js-sdk 12.13.0: subscribe → 1 initial fire (null) → <code>signInAnonymously</code> → +1 (user₁) → <code>signOut</code> → +1 (null) → <code>signInAnonymously</code> → +1 (user₂ with fresh uid). Every identity transition produces exactly one fire, matching <code>onAuthStateChanged</code>)</div></div>
241
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-listeners.test.ts#L12" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-listeners.test.ts</code></a> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-38-onidtokenchanged-fires-on-user-change.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-row-38-onidtokenchanged-fires-on-user-change.json</code></a> (against blockingfun, fb-js-sdk 12.13.0: subscribe → 1 initial fire (null) → <code>signInAnonymously</code> → +1 (user₁) → <code>signOut</code> → +1 (null) → <code>signInAnonymously</code> → +1 (user₂ with fresh uid). Every identity transition produces exactly one fire, matching <code>onAuthStateChanged</code>)</div></div>
242
242
  </details>
243
243
  <details class="compat-row" data-status="ok">
244
244
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">onIdTokenChanged(auth, observer)</code><span class="compat-sub"><span class="compat-behavior">Fires on EVERY sign-in, including a same-uid re-sign-in (no <code>signOut</code> first) — which mints a fresh token, so a subsequent <code>getIdToken()</code> returns a new string. Mirrors upstream <code>notifyAuthListeners</code>, which calls <code>idTokenSubscription.next</code> on every identity update (<code>auth_impl.ts:716</code>). <code>onAuthStateChanged</code> stays silent on the same-uid case (row 30a).</span></span></span></summary>
245
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-uid-dedup.test.ts</code> (locks AUTH-B8)</div></div>
245
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-uid-dedup.test.ts#L5" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-uid-dedup.test.ts</code></a> (locks AUTH-B8)</div></div>
246
246
  </details>
247
247
  <details class="compat-row" data-status="ok">
248
248
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">onIdTokenChanged(auth, observer)</code><span class="compat-sub"><span class="compat-behavior">Fires on token refresh (<code>getIdToken(true)</code>)</span></span></span></summary>
249
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-token-refresh.test.ts</code> — was ⚠ (documented divergence); aligned to prod in commit — sandbox now mints a fresh token on forceRefresh and fires <code>onIdTokenChanged</code> (NOT <code>onAuthStateChanged</code>, since identity is unchanged). Oracle: <code>packages/conformance/observations/auth/auth-onidtokenchanged-force-refresh.json</code> defines the target shape (<code>refreshFiredListener: true</code> against blockingfun; subscribe → null fire → <code>signInAnonymously</code> → +1 → <code>getIdToken(true)</code> → +1 for a total of 3 fires).</div></div>
249
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-token-refresh.test.ts#L12" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-token-refresh.test.ts</code></a> — was ⚠ (documented divergence); aligned to prod in commit — sandbox now mints a fresh token on forceRefresh and fires <code>onIdTokenChanged</code> (NOT <code>onAuthStateChanged</code>, since identity is unchanged). Oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-onidtokenchanged-force-refresh.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-onidtokenchanged-force-refresh.json</code></a> defines the target shape (<code>refreshFiredListener: true</code> against blockingfun; subscribe → null fire → <code>signInAnonymously</code> → +1 → <code>getIdToken(true)</code> → +1 for a total of 3 fires).</div></div>
250
250
  </details>
251
251
  <details class="compat-row" data-status="ok">
252
252
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">onIdTokenChanged(auth, observer)</code><span class="compat-sub"><span class="compat-behavior">Initial-fire semantics match <code>onAuthStateChanged</code></span></span></span></summary>
253
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-listeners.test.ts</code> + oracle: <code>packages/conformance/observations/auth/auth-row-40-onidtokenchanged-matches-onauthstatechanged-initial-fire.json</code> (against blockingfun, fb-js-sdk 12.13.0: subscribing both listeners in the same tick yields <code>sync: {auth: 0, idToken: 0}</code> → <code>microtask: {auth: 1, idToken: 1}</code> → no further fires. Both listeners share the microtask-deferred initial-fire timing)</div></div>
253
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-listeners.test.ts#L12" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-listeners.test.ts</code></a> + oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-row-40-onidtokenchanged-matches-onauthstatechanged-initial-fire.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-row-40-onidtokenchanged-matches-onauthstatechanged-initial-fire.json</code></a> (against blockingfun, fb-js-sdk 12.13.0: subscribing both listeners in the same tick yields <code>sync: {auth: 0, idToken: 0}</code> → <code>microtask: {auth: 1, idToken: 1}</code> → no further fires. Both listeners share the microtask-deferred initial-fire timing)</div></div>
254
254
  </details>
255
255
  </div>
256
256
 
@@ -259,11 +259,11 @@
259
259
  <div class="compat-list">
260
260
  <details class="compat-row" data-status="ok">
261
261
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">setPersistence(auth, persistence)</code><span class="compat-sub"><span class="compat-behavior">Accepts <code>inMemoryPersistence</code> / <code>browserSessionPersistence</code> / <code>browserLocalPersistence</code> markers without throwing</span></span></span></summary>
262
- <div class="compat-evidence"><div class="compat-probe"><code>unit:types.test.ts</code></div></div>
262
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/types.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:types.test.ts</code></a></div></div>
263
263
  </details>
264
264
  <details class="compat-row" data-status="ok">
265
265
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">setPersistence(auth, persistence)</code><span class="compat-sub"><span class="compat-behavior">Returns <code>Promise&lt;void&gt;</code></span></span></span></summary>
266
- <div class="compat-evidence"><div class="compat-probe"><code>unit:types.test.ts</code></div></div>
266
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/types.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:types.test.ts</code></a></div></div>
267
267
  </details>
268
268
  <details class="compat-row" data-status="diverged">
269
269
  <summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">setPersistence(auth, persistence)</code><span class="compat-sub"><span class="compat-behavior">Actually changes where the auth state is persisted</span></span></span></summary>
@@ -271,7 +271,7 @@
271
271
  </details>
272
272
  <details class="compat-row" data-status="ok">
273
273
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">setPersistence(auth, persistence)</code><span class="compat-sub"><span class="compat-behavior">An unrecognized persistence marker is rejected with <code>auth/argument-error</code> rather than silently coerced to LOCAL</span></span></span></summary>
274
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-cluster-b9-b12.test.ts</code> (locks AUTH-B12)</div></div>
274
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-cluster-b9-b12.test.ts#L17" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-cluster-b9-b12.test.ts</code></a> (locks AUTH-B12)</div></div>
275
275
  </details>
276
276
  </div>
277
277
 
@@ -280,23 +280,23 @@
280
280
  <div class="compat-list">
281
281
  <details class="compat-row" data-status="ok">
282
282
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithPopup(auth, provider)<code> / </code>signInWithCredential(auth, credential)</code><span class="compat-sub"><span class="compat-behavior">Returns the pre-staged <code>UserCredential</code> registered via <code>sandbox.mockSignInResult(auth, …)</code></span></span></span></summary>
283
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-providers.test.ts</code></div></div>
283
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-providers.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-providers.test.ts</code></a></div></div>
284
284
  </details>
285
285
  <details class="compat-row" data-status="ok">
286
286
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithPopup(auth, provider)<code> / </code>signInWithCredential(auth, credential)</code><span class="compat-sub"><span class="compat-behavior">Throws <code>auth/no-mock-configured</code> when no mock is pre-staged</span></span></span></summary>
287
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-providers.test.ts</code></div></div>
287
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-providers.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-providers.test.ts</code></a></div></div>
288
288
  </details>
289
289
  <details class="compat-row" data-status="ok">
290
290
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithPopup(auth, provider)<code> / </code>signInWithCredential(auth, credential)</code><span class="compat-sub"><span class="compat-behavior">Mock is consumed after one read (subsequent call without a fresh stage throws again)</span></span></span></summary>
291
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-providers.test.ts</code></div></div>
291
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-providers.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-providers.test.ts</code></a></div></div>
292
292
  </details>
293
293
  <details class="compat-row" data-status="ok">
294
294
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithPopup(auth, provider)<code> / </code>signInWithCredential(auth, credential)</code><span class="compat-sub"><span class="compat-behavior">After successful sign-in, <code>currentUser</code> becomes the mock's <code>user</code>, listeners fire</span></span></span></summary>
295
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-providers.test.ts</code></div></div>
295
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-providers.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-providers.test.ts</code></a></div></div>
296
296
  </details>
297
297
  <details class="compat-row" data-status="ok">
298
298
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithPopup(auth, provider)<code> / </code>signInWithCredential(auth, credential)</code><span class="compat-sub"><span class="compat-behavior">The credential's rich <code>User</code> (email / displayName / isAnonymous) survives the transition — popup/redirect/credential/<code>setUser</code> do NOT clobber it down to the bare <code>AuthState</code>; <code>cred.user === auth.currentUser</code> (reference identity, matches upstream <code>_updateCurrentUser(userCredential.user)</code>)</span></span></span></summary>
299
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-user-identity.test.ts</code> (locks AUTH-B1)</div></div>
299
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-user-identity.test.ts#L21" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-user-identity.test.ts</code></a> (locks AUTH-B1)</div></div>
300
300
  </details>
301
301
  <details class="compat-row" data-status="diverged">
302
302
  <summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">signInWithPopup(auth, provider)<code> / </code>signInWithCredential(auth, credential)</code><span class="compat-sub"><span class="compat-behavior">Opens a popup window in prod</span></span></span></summary>
@@ -308,7 +308,7 @@
308
308
  </details>
309
309
  <details class="compat-row" data-status="ok">
310
310
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithPopup(auth, provider)<code> / </code>signInWithCredential(auth, credential)</code><span class="compat-sub"><span class="compat-behavior">Provider-flow sign-ins (popup / redirect / credential) record the flow's <code>providerId</code> on the identity in the user DB (upsert for unknown uids; append-if-missing for known ones) and reject disabled accounts with <code>auth/user-disabled</code> before any state change</span></span></span></summary>
311
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-user-admin.test.ts</code> ("provider tracking", "disabled users") — provider recording is sandbox bookkeeping for <code>listIdentities</code>/<code>listUsers</code>; prod's auto-link semantics are narrower (same-email Google auto-link only) and not modeled</div></div>
311
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-user-admin.test.ts#L20" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-user-admin.test.ts</code></a> ("provider tracking", "disabled users") — provider recording is sandbox bookkeeping for <code>listIdentities</code>/<code>listUsers</code>; prod's auto-link semantics are narrower (same-email Google auto-link only) and not modeled</div></div>
312
312
  </details>
313
313
  </div>
314
314
 
@@ -317,19 +317,19 @@
317
317
  <div class="compat-list">
318
318
  <details class="compat-row" data-status="ok">
319
319
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithRedirect<code> / </code>getRedirectResult` / resolver seam</code><span class="compat-sub"><span class="compat-behavior"><code>signInWithRedirect(auth, provider, resolver?)</code> resolves the flow (per-call resolver → injected → one-shot mock → <code>auth/argument-error</code>), signs the user in, and stashes the credential for one <code>getRedirectResult</code></span></span></span></summary>
320
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-resolver.test.ts</code></div></div>
320
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-resolver.test.ts#L5" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-resolver.test.ts</code></a></div></div>
321
321
  </details>
322
322
  <details class="compat-row" data-status="ok">
323
323
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithRedirect<code> / </code>getRedirectResult` / resolver seam</code><span class="compat-sub"><span class="compat-behavior"><code>getRedirectResult(auth)</code> returns the stashed credential once, then <code>null</code> (one-shot, matches prod)</span></span></span></summary>
324
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-resolver.test.ts</code></div></div>
324
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-resolver.test.ts#L5" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-resolver.test.ts</code></a></div></div>
325
325
  </details>
326
326
  <details class="compat-row" data-status="ok">
327
327
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithRedirect<code> / </code>getRedirectResult` / resolver seam</code><span class="compat-sub"><span class="compat-behavior"><code>sandbox.setAuthFlowResolver(auth, resolver | null)</code> installs / clears the popup/redirect resolver (the analog of browser <code>getAuth</code> wiring <code>browserPopupRedirectResolver</code>)</span></span></span></summary>
328
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-resolver.test.ts</code></div></div>
328
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-resolver.test.ts#L5" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-resolver.test.ts</code></a></div></div>
329
329
  </details>
330
330
  <details class="compat-row" data-status="ok">
331
331
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithRedirect<code> / </code>getRedirectResult` / resolver seam</code><span class="compat-sub"><span class="compat-behavior"><code>sandbox.listIdentities(auth)</code> snapshots every seeded/created identity for a host account-picker (sandbox-only — no <code>firebase/auth</code> equivalent)</span></span></span></summary>
332
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-resolver.test.ts</code></div></div>
332
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-resolver.test.ts#L5" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-resolver.test.ts</code></a></div></div>
333
333
  </details>
334
334
  </div>
335
335
 
@@ -338,11 +338,11 @@
338
338
  <div class="compat-list">
339
339
  <details class="compat-row" data-status="ok">
340
340
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Auth` surface + error shape</code><span class="compat-sub"><span class="compat-behavior"><code>auth.signOut()</code> method form works alongside the free <code>signOut(auth)</code> function (<code>firebase/auth</code>'s <code>Auth</code> exposes both) (AUTH-GAP)</span></span></span></summary>
341
- <div class="compat-evidence"><div class="compat-probe"><code>unit:auth-gap-surface.test.ts</code></div></div>
341
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/auth-gap-surface.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:auth-gap-surface.test.ts</code></a></div></div>
342
342
  </details>
343
343
  <details class="compat-row" data-status="ok">
344
344
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Auth` surface + error shape</code><span class="compat-sub"><span class="compat-behavior">Sandbox auth errors are real <code>FirebaseError</code> instances (<code>err instanceof FirebaseError</code>) carrying the prod message wrapper <code>Firebase: &lt;message&gt; (&lt;auth/...&gt;).</code> — e.g. <code>Firebase: Error (auth/invalid-email).</code>, matching the oracle (AUTH-GAP)</span></span></span></summary>
345
- <div class="compat-evidence"><div class="compat-probe"><code>unit:auth-gap-surface.test.ts</code></div></div>
345
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/auth-gap-surface.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:auth-gap-surface.test.ts</code></a></div></div>
346
346
  </details>
347
347
  </div>
348
348
 
@@ -351,15 +351,15 @@
351
351
  <div class="compat-list">
352
352
  <details class="compat-row" data-status="ok">
353
353
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><span class="compat-behavior">Exports the same constructor signatures as <code>firebase/auth</code> for each provider</span></span></summary>
354
- <div class="compat-evidence"><div class="compat-probe">type-only smoke in <code>unit:types.test.ts</code></div></div>
354
+ <div class="compat-evidence"><div class="compat-probe">type-only smoke in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/types.test.ts#L8" target="_blank" rel="noopener noreferrer"><code>unit:types.test.ts</code></a></div></div>
355
355
  </details>
356
356
  <details class="compat-row" data-status="ok">
357
357
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><span class="compat-behavior"><code>Provider.credential(...)</code> static factories produce <code>AuthCredential</code>-shaped objects</span></span></summary>
358
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-providers.test.ts</code></div></div>
358
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-providers.test.ts#L14" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-providers.test.ts</code></a></div></div>
359
359
  </details>
360
360
  <details class="compat-row" data-status="ok">
361
361
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><span class="compat-behavior"><code>GoogleAuthProvider.providerId === 'google.com'</code> (and per-provider analogs)</span></span></summary>
362
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-providers.test.ts</code></div></div>
362
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-providers.test.ts#L18" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-providers.test.ts</code></a></div></div>
363
363
  </details>
364
364
  <details class="compat-row" data-status="unsupported">
365
365
  <summary class="compat-line"><span class="compat-dot" data-status="unsupported" role="img" aria-label="Not implemented yet" title="Not implemented yet"></span><span class="compat-main"><span class="compat-behavior">Custom scopes / params / language code</span></span></summary>
@@ -372,15 +372,15 @@
372
372
  <div class="compat-list">
373
373
  <details class="compat-row" data-status="ok">
374
374
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">User` methods</code><span class="compat-sub"><span class="compat-behavior"><code>user.getIdToken()</code> returns a stable opaque token in sandbox (<code>sandbox-id-token-…</code>)</span></span></span></summary>
375
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-anonymous.test.ts</code></div></div>
375
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-anonymous.test.ts#L4" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-anonymous.test.ts</code></a></div></div>
376
376
  </details>
377
377
  <details class="compat-row" data-status="ok">
378
378
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">User` methods</code><span class="compat-sub"><span class="compat-behavior"><code>user.getIdToken(true)</code> (forceRefresh) returns a NEW token; subsequent <code>getIdToken(false)</code> returns the cached new token</span></span></span></summary>
379
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-token-refresh.test.ts</code> — was ⚠ (documented divergence); aligned to prod in commit — sandbox now mints a fresh token on forceRefresh and fires <code>onIdTokenChanged</code>. Oracle: <code>packages/conformance/observations/auth/auth-getidtoken-force-refresh.json</code> defines the target shape (<code>forceRefreshReturnedDifferentString: true</code>, <code>token1EqualsToken2: true</code> against blockingfun — the refreshed token is cached, so a subsequent non-forced read returns it, not yet another fresh one). Sandbox tokens stay <code>sandbox-id-token-&lt;uid&gt;-&lt;hash&gt;</code> strings; prod's are real JWTs.</div></div>
379
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-token-refresh.test.ts#L8" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-token-refresh.test.ts</code></a> — was ⚠ (documented divergence); aligned to prod in commit — sandbox now mints a fresh token on forceRefresh and fires <code>onIdTokenChanged</code>. Oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-getidtoken-force-refresh.json" target="_blank" rel="noopener noreferrer"><code>packages/conformance/observations/auth/auth-getidtoken-force-refresh.json</code></a> defines the target shape (<code>forceRefreshReturnedDifferentString: true</code>, <code>token1EqualsToken2: true</code> against blockingfun — the refreshed token is cached, so a subsequent non-forced read returns it, not yet another fresh one). Sandbox tokens stay <code>sandbox-id-token-&lt;uid&gt;-&lt;serial&gt;:&lt;json&gt;</code> strings; prod's are real JWTs.</div></div>
380
380
  </details>
381
381
  <details class="compat-row" data-status="ok">
382
382
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">User` methods</code><span class="compat-sub"><span class="compat-behavior"><code>user.getIdTokenResult()</code> returns claims</span></span></span></summary>
383
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-providers.test.ts</code> (custom-claims path)</div></div>
383
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-providers.test.ts#L24" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-providers.test.ts</code></a> (custom-claims path)</div></div>
384
384
  </details>
385
385
  <details class="compat-row" data-status="ok">
386
386
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">User` methods</code><span class="compat-sub"><span class="compat-behavior"><code>user.uid</code>, <code>user.email</code>, <code>user.displayName</code>, <code>user.isAnonymous</code> reflect the source</span></span></span></summary>
@@ -388,15 +388,15 @@
388
388
  </details>
389
389
  <details class="compat-row" data-status="ok">
390
390
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">User` methods</code><span class="compat-sub"><span class="compat-behavior"><code>user.emailVerified</code> — present on every sandbox-minted <code>User</code> (default <code>false</code>; sandbox has no verification flow). Prod passes the real value through (no longer stripped). The admin record (<code>sandbox.listUsers</code>) carries it too</span></span></span></summary>
391
- <div class="compat-evidence"><div class="compat-probe"><code>unit:auth-gap-surface.test.ts</code> (locks AUTH-GAP)</div></div>
391
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/auth-gap-surface.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:auth-gap-surface.test.ts</code></a> (locks AUTH-GAP)</div></div>
392
392
  </details>
393
393
  <details class="compat-row" data-status="ok">
394
394
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">User` methods</code><span class="compat-sub"><span class="compat-behavior"><code>user.photoURL</code> / <code>user.phoneNumber</code> — present (sandbox default <code>null</code>; prod passes through, no longer stripped)</span></span></span></summary>
395
- <div class="compat-evidence"><div class="compat-probe"><code>unit:auth-gap-surface.test.ts</code></div></div>
395
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/auth-gap-surface.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:auth-gap-surface.test.ts</code></a></div></div>
396
396
  </details>
397
397
  <details class="compat-row" data-status="ok">
398
398
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">User` methods</code><span class="compat-sub"><span class="compat-behavior"><code>user.providerId</code> (aggregate, <code>'firebase'</code>) + <code>user.providerData: UserInfo[]</code> — sandbox synthesizes one provider entry for non-anonymous users, empty for anonymous; prod passes the real array through (no longer stripped). The admin record carries the emulator-shaped <code>providerUserInfo</code></span></span></span></summary>
399
- <div class="compat-evidence"><div class="compat-probe"><code>unit:auth-gap-surface.test.ts</code></div></div>
399
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/auth-gap-surface.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:auth-gap-surface.test.ts</code></a></div></div>
400
400
  </details>
401
401
  <details class="compat-row" data-status="unsupported">
402
402
  <summary class="compat-line"><span class="compat-dot" data-status="unsupported" role="img" aria-label="Not implemented yet" title="Not implemented yet"></span><span class="compat-main"><code class="compat-api">User` methods</code><span class="compat-sub"><span class="compat-behavior"><code>user.metadata.creationTime</code> / <code>lastSignInTime</code></span></span></span></summary>
@@ -404,11 +404,11 @@
404
404
  </details>
405
405
  <details class="compat-row" data-status="ok">
406
406
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">User` methods</code><span class="compat-sub"><span class="compat-behavior"><code>IdTokenResult.signInProvider</code> reflects the session's provider per flow (<code>'anonymous'</code> / <code>'password'</code> / <code>'google.com'</code> / …); claims include the reserved <code>firebase.sign_in_provider</code> (custom claims can't shadow it)</span></span></span></summary>
407
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-user-admin.test.ts</code> ("IdTokenResult.signInProvider") — prod shape is documented SDK behavior; no oracle capture yet</div></div>
407
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-user-admin.test.ts#L9" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-user-admin.test.ts</code></a> ("IdTokenResult.signInProvider") — prod shape is documented SDK behavior; no oracle capture yet (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-signinprovider-per-flow.json" target="_blank" rel="noopener noreferrer"><code>auth-signinprovider-per-flow</code></a>)</div></div>
408
408
  </details>
409
409
  <details class="compat-row" data-status="ok">
410
410
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">User` methods</code><span class="compat-sub"><span class="compat-behavior">Custom-claims changes (<code>sandbox.updateUser</code> / re-seed) reach an active session on the next FORCED token refresh, not immediately — claims are read live from the user DB at mint time (prod's refresh-propagation story; AUTH-B10)</span></span></span></summary>
411
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-user-admin.test.ts</code>, <code>unit:sandbox-cluster-b9-b12.test.ts</code></div></div>
411
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-user-admin.test.ts#L9" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-user-admin.test.ts</code></a>, <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-cluster-b9-b12.test.ts#L24" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-cluster-b9-b12.test.ts</code></a> (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-claims-forced-refresh-propagation.json" target="_blank" rel="noopener noreferrer"><code>auth-claims-forced-refresh-propagation</code></a>)</div></div>
412
412
  </details>
413
413
  <details class="compat-row" data-status="unsupported">
414
414
  <summary class="compat-line"><span class="compat-dot" data-status="unsupported" role="img" aria-label="Not implemented yet" title="Not implemented yet"></span><span class="compat-main"><code class="compat-api">User` methods</code><span class="compat-sub"><span class="compat-behavior"><code>user.reload()</code> / <code>user.delete()</code> / <code>user.toJSON()</code> / <code>user.refreshToken</code> / <code>user.tenantId</code></span></span></span></summary>
@@ -416,7 +416,7 @@
416
416
  </details>
417
417
  <details class="compat-row" data-status="ok">
418
418
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">updateProfile(user, {displayName, photoURL})</code><span class="compat-sub"><span class="compat-behavior"><code>updateProfile(user, {displayName, photoURL})</code> mutates the held user + stored record in place (null clears a field; omitted fields untouched) without an extra <code>onAuthStateChanged</code> fire. Profile survives <code>signOut</code> → <code>signInWithEmailAndPassword</code>.</span></span></span></summary>
419
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-update-profile.test.ts</code> (in-place mutation, listUsers persist, null clear, no extra listener); re-sign-in persistence: <code>unit:upstream-auth-probes.test.ts</code></div></div>
419
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-update-profile.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-update-profile.test.ts</code></a> (in-place mutation, listUsers persist, null clear, no extra listener); re-sign-in persistence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/upstream-auth-probes.test.ts#L8" target="_blank" rel="noopener noreferrer"><code>unit:upstream-auth-probes.test.ts</code></a></div></div>
420
420
  </details>
421
421
  </div>
422
422
 
@@ -425,19 +425,19 @@
425
425
  <div class="compat-list">
426
426
  <details class="compat-row" data-status="ok">
427
427
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">beforeAuthStateChanged(auth, callback, onAbort?)</code><span class="compat-sub"><span class="compat-behavior">Registers a BLOCKING gate that runs before a real sign-in/sign-out transition commits. Callbacks run in registration order; a callback that throws (or returns a rejected promise) aborts the transition entirely: the pending <code>signInWith…</code>/<code>signOut</code> call rejects with <code>auth/login-blocked</code>, <code>currentUser</code> is unchanged, and <code>onAuthStateChanged</code>/<code>onIdTokenChanged</code> do NOT fire. Covers every sign-in path that exists in <code>pyric/auth</code>: <code>signInAnonymously</code>, <code>signInWithEmailAndPassword</code>, <code>createUserWithEmailAndPassword</code>, <code>signInWithPopup</code>, <code>signInWithRedirect</code>, <code>signInWithCredential</code>, <code>signInWithCustomToken</code>, and <code>signOut</code>. Modeled after the real <code>@firebase/auth</code> <code>AuthMiddlewareQueue.runMiddleware</code> (<code>auth_impl.ts</code>), read directly from the installed SDK source, not just its <code>.d.ts</code>.</span></span></span></summary>
428
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-before-auth-state-changed.test.ts</code> — implementation cross-checked against <code>@firebase/auth</code>'s <code>AuthMiddlewareQueue</code> source (registration-order queue, per-callback try/await, <code>auth/login-blocked</code> wrap). Custom-token allow/block: <code>unit:upstream-auth-probes.test.ts</code>. No live-oracle capture (no observable client-visible signal to probe beyond the documented+sourced contract already read).</div></div>
428
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-before-auth-state-changed.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-before-auth-state-changed.test.ts</code></a> — implementation cross-checked against <code>@firebase/auth</code>'s <code>AuthMiddlewareQueue</code> source (registration-order queue, per-callback try/await, <code>auth/login-blocked</code> wrap). Custom-token allow/block: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/upstream-auth-probes.test.ts#L9" target="_blank" rel="noopener noreferrer"><code>unit:upstream-auth-probes.test.ts</code></a>. No live-oracle capture (no observable client-visible signal to probe beyond the documented+sourced contract already read).</div></div>
429
429
  </details>
430
430
  <details class="compat-row" data-status="ok">
431
431
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">beforeAuthStateChanged(auth, callback, onAbort?)</code><span class="compat-sub"><span class="compat-behavior"><code>onAbort</code> semantics: when a callback throws, every <code>onAbort</code> registered by a callback that ALREADY SUCCEEDED in the same pass runs, in REVERSE registration order — matches upstream's rollback-stack (<code>runMiddleware</code>'s <code>onAbortStack</code>). An <code>onAbort</code> that itself throws is swallowed so it can't mask the original block reason or skip the remaining rollbacks.</span></span></span></summary>
432
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-before-auth-state-changed.test.ts</code> ("onAbort runs (in reverse order)…", "a callback whose own onAbort throws…")</div></div>
432
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-before-auth-state-changed.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-before-auth-state-changed.test.ts</code></a> ("onAbort runs (in reverse order)…", "a callback whose own onAbort throws…")</div></div>
433
433
  </details>
434
434
  <details class="compat-row" data-status="ok">
435
435
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">beforeAuthStateChanged(auth, callback, onAbort?)</code><span class="compat-sub"><span class="compat-behavior">Fires on BOTH directions: a real sign-in (<code>nextUser</code> non-null) and a real sign-out (<code>nextUser === null</code>) — a throwing callback blocks <code>signOut</code> too, leaving the previous user signed in. Matches upstream, where the same middleware queue gates <code>_updateCurrentUser</code> and <code>signOut</code>.</span></span></span></summary>
436
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-before-auth-state-changed.test.ts</code> ("fires on sign-out too…")</div></div>
436
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-before-auth-state-changed.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-before-auth-state-changed.test.ts</code></a> ("fires on sign-out too…")</div></div>
437
437
  </details>
438
438
  <details class="compat-row" data-status="diverged">
439
439
  <summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">beforeAuthStateChanged(auth, callback, onAbort?)</code><span class="compat-sub"><span class="compat-behavior"><code>sandbox.setUser</code> (the sandbox-only test driver) BYPASSES the gate entirely — it is a raw identity force with no prod analog (same bypass it already has for provider enforcement / <code>signInProvider</code> tracking), so no registered <code>beforeAuthStateChanged</code> callback runs and none can block it.</span></span></span></summary>
440
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-before-auth-state-changed.test.ts</code> ("sandbox.setUser test driver bypasses the gate")</div></div>
440
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-before-auth-state-changed.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-before-auth-state-changed.test.ts</code></a> ("sandbox.setUser test driver bypasses the gate")</div></div>
441
441
  </details>
442
442
  <details class="compat-row" data-status="diverged">
443
443
  <summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">beforeAuthStateChanged(auth, callback, onAbort?)</code><span class="compat-sub"><span class="compat-behavior">Served-worker path (SharedWorker-backed auth in <code>@pyric/cli</code>): the worker owns the shared user pool and commits transitions on its own side of the port, so a page-local <code>beforeAuthStateChanged</code> registration can't actually gate a worker-driven sign-in. Rather than silently accept a callback that could never run, registering THROWS immediately (<code>auth/operation-not-supported-in-this-environment</code>) — same defensive pattern as <code>signInWithCredential</code> over the worker.</span></span></span></summary>
@@ -450,33 +450,33 @@
450
450
  <div class="compat-list">
451
451
  <details class="compat-row" data-status="ok">
452
452
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">initializeAuth(app, deps?)</code><span class="compat-sub"><span class="compat-behavior">Aliases <code>getAuth(app)</code> and returns the same stable <code>Auth</code> handle — an app that calls <code>initializeAuth</code> instead of <code>getAuth</code> gets an equivalent, working instance. The optional <code>Dependencies</code> arg (persistence / popupRedirectResolver) is accepted for signature parity but not applied (persistence is already a documented sandbox no-op). Repeated calls return the cached handle rather than throwing <code>auth/already-initialized</code></span></span></span></summary>
453
- <div class="compat-evidence"><div class="compat-probe"><code>unit:fruit-aliases.test.ts</code> — returns the same instance as <code>getAuth</code>, with a live <code>currentUser</code></div></div>
453
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/fruit-aliases.test.ts#L13" target="_blank" rel="noopener noreferrer"><code>unit:fruit-aliases.test.ts</code></a> — returns the same instance as <code>getAuth</code>, with a live <code>currentUser</code></div></div>
454
454
  </details>
455
455
  <details class="compat-row" data-status="ok">
456
456
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">deleteUser(user)</code><span class="compat-sub"><span class="compat-behavior">Deletes the account from the user store AND signs the user out if they are current (fires <code>onAuthStateChanged(null)</code>), matching prod where deleting the signed-in user clears <code>auth.currentUser</code>. Real behavior: a subsequent <code>signInWithEmailAndPassword</code> for that identity throws <code>auth/user-not-found</code>; <code>reload(user)</code> after delete rejects with <code>auth/user-token-expired</code></span></span></span></summary>
457
- <div class="compat-evidence"><div class="compat-probe"><code>unit:fruit-aliases.test.ts</code> — user removed from the store, sign-out fired, re-sign-in throws <code>auth/user-not-found</code>; post-delete <code>reload</code> → <code>auth/user-token-expired</code>: <code>unit:upstream-auth-probes.test.ts</code></div></div>
457
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/fruit-aliases.test.ts#L17" target="_blank" rel="noopener noreferrer"><code>unit:fruit-aliases.test.ts</code></a> — user removed from the store, sign-out fired, re-sign-in throws <code>auth/user-not-found</code>; post-delete <code>reload</code> → <code>auth/user-token-expired</code>: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/upstream-auth-probes.test.ts#L7" target="_blank" rel="noopener noreferrer"><code>unit:upstream-auth-probes.test.ts</code></a></div></div>
458
458
  </details>
459
459
  <details class="compat-row" data-status="diverged">
460
460
  <summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">updateEmail(user, newEmail)</code><span class="compat-sub"><span class="compat-behavior">Changes the stored email (via the same path as <code>sandbox.updateUser</code>, rejecting <code>auth/email-already-in-use</code> / <code>auth/invalid-email</code>) and mutates the held <code>user</code> in place, so the next sign-in resolves against the new email. Leniency vs prod: the sandbox does NOT enforce <code>auth/requires-recent-login</code> and is not routed through <code>verifyBeforeUpdateEmail</code> (which the real SDK requires when email-enumeration protection is on)</span></span></span></summary>
461
- <div class="compat-evidence"><div class="compat-probe"><code>unit:fruit-aliases.test.ts</code> — stored email changes; re-sign-in works with the new email, fails with the old</div>
461
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/fruit-aliases.test.ts#L18" target="_blank" rel="noopener noreferrer"><code>unit:fruit-aliases.test.ts</code></a> — stored email changes; re-sign-in works with the new email, fails with the old</div>
462
462
  <div class="compat-note">email really changes; no requires-recent-login / verifyBeforeUpdateEmail enforcement</div></div>
463
463
  </details>
464
464
  <details class="compat-row" data-status="diverged">
465
465
  <summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">updatePassword(user, newPassword)</code><span class="compat-sub"><span class="compat-behavior">Sets the stored password (validated for strength). The sandbox DOES store and verify passwords, so this is a real mutation: a subsequent <code>signInWithEmailAndPassword</code> with the new password succeeds and the old one throws <code>auth/wrong-password</code>. Leniency vs prod: no <code>auth/requires-recent-login</code> enforcement</span></span></span></summary>
466
- <div class="compat-evidence"><div class="compat-probe"><code>unit:fruit-aliases.test.ts</code> — new password signs in, old password throws <code>auth/wrong-password</code></div>
466
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/fruit-aliases.test.ts#L19" target="_blank" rel="noopener noreferrer"><code>unit:fruit-aliases.test.ts</code></a> — new password signs in, old password throws <code>auth/wrong-password</code></div>
467
467
  <div class="compat-note">password really changes + is verified; no requires-recent-login enforcement</div></div>
468
468
  </details>
469
469
  <details class="compat-row" data-status="ok">
470
470
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">reload(user)</code><span class="compat-sub"><span class="compat-behavior">Re-reads the stored record into the <code>user</code> object in place, so a change made out of band (e.g. <code>sandbox.updateUser</code>) is reflected on the held reference — matching prod's server refresh. If the identity is absent from the store (including after <code>deleteUser</code>), rejects with <code>auth/user-token-expired</code></span></span></span></summary>
471
- <div class="compat-evidence"><div class="compat-probe"><code>unit:fruit-aliases.test.ts</code> — an out-of-band <code>sandbox.updateUser</code> displayName change is visible on the held user after <code>reload</code>; missing-store / post-delete: <code>unit:upstream-auth-probes.test.ts</code></div></div>
471
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/fruit-aliases.test.ts#L20" target="_blank" rel="noopener noreferrer"><code>unit:fruit-aliases.test.ts</code></a> — an out-of-band <code>sandbox.updateUser</code> displayName change is visible on the held user after <code>reload</code>; missing-store / post-delete: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/upstream-auth-probes.test.ts#L7" target="_blank" rel="noopener noreferrer"><code>unit:upstream-auth-probes.test.ts</code></a></div></div>
472
472
  </details>
473
473
  <details class="compat-row" data-status="ok">
474
474
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">updateCurrentUser(auth, user)</code><span class="compat-sub"><span class="compat-behavior">Sets the sandbox's current user (pass <code>null</code> to sign out), firing <code>onAuthStateChanged</code> — <code>auth.currentUser</code> reflects the passed user afterward</span></span></span></summary>
475
- <div class="compat-evidence"><div class="compat-probe"><code>unit:fruit-aliases.test.ts</code> — <code>auth.currentUser</code> becomes the passed user; <code>null</code> signs out</div></div>
475
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/fruit-aliases.test.ts#L21" target="_blank" rel="noopener noreferrer"><code>unit:fruit-aliases.test.ts</code></a> — <code>auth.currentUser</code> becomes the passed user; <code>null</code> signs out</div></div>
476
476
  </details>
477
477
  <details class="compat-row" data-status="diverged">
478
478
  <summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">useDeviceLanguage(auth)</code><span class="compat-sub"><span class="compat-behavior">Accepted no-op — the sandbox has no device locale to read, so there is no language to set; accepted so init code that calls it compiles + runs</span></span></span></summary>
479
- <div class="compat-evidence"><div class="compat-probe"><code>unit:fruit-aliases.test.ts</code> — resolves/returns without error</div>
479
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/fruit-aliases.test.ts#L22" target="_blank" rel="noopener noreferrer"><code>unit:fruit-aliases.test.ts</code></a> — resolves/returns without error</div>
480
480
  <div class="compat-note">no device locale in the sandbox</div></div>
481
481
  </details>
482
482
  </div>
@@ -486,47 +486,47 @@
486
486
  <div class="compat-list">
487
487
  <details class="compat-row" data-status="ok">
488
488
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">ActionCodeURL.parseLink(link) / parseActionCodeURL(link)</code><span class="compat-sub"><span class="compat-behavior">Parses an out-of-band action link. The <code>mode</code> query param maps to a NORMALIZED operation (<code>mode=resetPassword</code> -&gt; <code>PASSWORD_RESET</code>, <code>mode=signIn</code> -&gt; <code>EMAIL_SIGNIN</code>), <code>oobCode</code> surfaces as <code>code</code>, <code>lang</code> as <code>languageCode</code>, and <code>continueUrl</code> comes out URL-DECODED. A link missing <code>mode</code>, missing <code>oobCode</code>, carrying an unknown mode, or that is not a URL at all parses to <code>null</code> — the parse NEVER throws. <code>parseActionCodeURL</code> and <code>ActionCodeURL.parseLink</code> agree.</span></span></span></summary>
489
- <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED and project-independent: this is a PURE client-side parse (no network, no project, no mailbox), so the sandbox owes prod an exact match and there is no room for a divergence. Oracle: <code>auth-actioncodeurl-parse</code> against firebase-js-sdk 12.13.0. Replayed in <code>unit:oracle-conformance.test.ts</code>.</div></div>
489
+ <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED and project-independent: this is a PURE client-side parse (no network, no project, no mailbox), so the sandbox owes prod an exact match and there is no room for a divergence. Oracle: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-actioncodeurl-parse.json" target="_blank" rel="noopener noreferrer"><code>auth-actioncodeurl-parse</code></a> against firebase-js-sdk 12.13.0. Replayed in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/oracle-conformance.test.ts#L563" target="_blank" rel="noopener noreferrer"><code>unit:oracle-conformance.test.ts</code></a>. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-link.test.ts#L15" target="_blank" rel="noopener noreferrer"><code>sandbox-email-link.test.ts</code></a>)</div></div>
490
490
  </details>
491
491
  <details class="compat-row" data-status="ok">
492
492
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">isSignInWithEmailLink(auth, link)</code><span class="compat-sub"><span class="compat-behavior">Pure predicate over the link string — no network. <code>true</code> only for a link whose mode is <code>signIn</code> AND which carries an <code>oobCode</code>; <code>false</code> for a password-reset link, for <code>mode=signIn</code> with no code, for garbage, and for the empty string. Never throws.</span></span></span></summary>
493
- <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED, project-independent (no server involved): <code>auth-issigninwithemaillink-predicate</code> captured all five cases against prod. Replayed in <code>unit:oracle-conformance.test.ts</code>.</div></div>
493
+ <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED, project-independent (no server involved): <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-issigninwithemaillink-predicate.json" target="_blank" rel="noopener noreferrer"><code>auth-issigninwithemaillink-predicate</code></a> captured all five cases against prod. Replayed in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/oracle-conformance.test.ts#L599" target="_blank" rel="noopener noreferrer"><code>unit:oracle-conformance.test.ts</code></a>. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-link.test.ts#L22" target="_blank" rel="noopener noreferrer"><code>sandbox-email-link.test.ts</code></a>)</div></div>
494
494
  </details>
495
495
  <details class="compat-row" data-status="ok">
496
496
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">applyActionCode(auth, code)</code><span class="compat-sub"><span class="compat-behavior">Redeems an out-of-band code and performs its state change (<code>VERIFY_EMAIL</code> sets <code>emailVerified</code>, <code>VERIFY_AND_CHANGE_EMAIL</code> moves the account to the new address). Throws <code>auth/invalid-action-code</code> for a code the project never issued, for the empty string, and for a code already redeemed (single-use). A <code>PASSWORD_RESET</code> code is refused — <code>confirmPasswordReset</code> owns that one.</span></span></span></summary>
497
- <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED on the reject path: <code>auth-action-code-invalid</code> confirmed prod emits <code>auth/invalid-action-code</code> for both a bogus code and the empty string (this endpoint is NOT gated on the password provider, so it answered honestly). The APPLY path (a real code, redeemed) cannot be probed from a client — it needs a code from a real inbox — and is unit-backed end to end against the sandbox outbox in <code>unit:sandbox-email-link.test.ts</code>.</div></div>
497
+ <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED on the reject path: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-action-code-invalid.json" target="_blank" rel="noopener noreferrer"><code>auth-action-code-invalid</code></a> confirmed prod emits <code>auth/invalid-action-code</code> for both a bogus code and the empty string (this endpoint is NOT gated on the password provider, so it answered honestly). The APPLY path (a real code, redeemed) cannot be probed from a client — it needs a code from a real inbox — and is unit-backed end to end against the sandbox outbox in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-link.test.ts#L16" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-link.test.ts</code></a>. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/oracle-conformance.test.ts#L609" target="_blank" rel="noopener noreferrer"><code>oracle-conformance.test.ts</code></a>)</div></div>
498
498
  </details>
499
499
  <details class="compat-row" data-status="ok">
500
500
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">sendSignInLinkToEmail(auth, email, settings)</code><span class="compat-sub"><span class="compat-behavior">ActionCodeSettings validation, enforced CLIENT-side before any request leaves the process: a missing or unparseable <code>url</code> throws <code>auth/invalid-continue-uri</code> (NOT <code>auth/missing-continue-uri</code>, despite the constant's name), and <code>handleCodeInApp</code> other than <code>true</code> throws <code>auth/argument-error</code>. On success the sandbox mints a single-use code and posts the message to the outbox.</span></span></span></summary>
501
- <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED on both client-side arms (project-independent — prod threw before any network call): <code>auth-sendsigninlinktoemail-settings-validation</code> captured <code>missingUrl: auth/invalid-continue-uri</code> and <code>handleCodeInAppFalse: auth/argument-error</code>. Replayed in <code>unit:oracle-conformance.test.ts</code>; the send path is unit-backed in <code>unit:sandbox-email-link.test.ts</code>.</div></div>
501
+ <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED on both client-side arms (project-independent — prod threw before any network call): <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-sendsigninlinktoemail-settings-validation.json" target="_blank" rel="noopener noreferrer"><code>auth-sendsigninlinktoemail-settings-validation</code></a> captured <code>missingUrl: auth/invalid-continue-uri</code> and <code>handleCodeInAppFalse: auth/argument-error</code>. Replayed in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/oracle-conformance.test.ts#L624" target="_blank" rel="noopener noreferrer"><code>unit:oracle-conformance.test.ts</code></a>; the send path is unit-backed in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-link.test.ts#L26" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-link.test.ts</code></a>.</div></div>
502
502
  </details>
503
503
  <details class="compat-row" data-status="ok">
504
504
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">signInWithEmailLink(auth, email, link)</code><span class="compat-sub"><span class="compat-behavior">Redeems the code in a sign-in link. A link carrying no <code>oobCode</code> throws <code>auth/argument-error</code> CLIENT-side (the SDK never reaches the server to ask about a code it cannot find in the link). A first-time sign-in for an address CREATES the account, <code>getAdditionalUserInfo(cred).isNewUser</code> is <code>true</code>, and the account arrives <code>emailVerified: true</code> — redeeming a code mailed to that address is proof of control. The code is single-use.</span></span></span></summary>
505
- <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED on the client-side reject (<code>auth-signinwithemaillink-invalid-link</code>: <code>noOobCode: auth/argument-error</code>). The REDEMPTION path could not be probed: the oracle project has email-link sign-in disabled, so the server arm answered <code>auth/operation-not-allowed</code>. That half is unit-backed end to end against the sandbox outbox (<code>unit:sandbox-email-link.test.ts</code> drives send -&gt; read the outbox -&gt; sign in).</div></div>
505
+ <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED on the client-side reject (<a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-signinwithemaillink-invalid-link.json" target="_blank" rel="noopener noreferrer"><code>auth-signinwithemaillink-invalid-link</code></a>: <code>noOobCode: auth/argument-error</code>). The REDEMPTION path could not be probed: the oracle project has email-link sign-in disabled, so the server arm answered <code>auth/operation-not-allowed</code>. That half is unit-backed end to end against the sandbox outbox (<a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-link.test.ts#L28" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-link.test.ts</code></a> drives send -&gt; read the outbox -&gt; sign in). (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/oracle-conformance.test.ts#L644" target="_blank" rel="noopener noreferrer"><code>oracle-conformance.test.ts</code></a>)</div></div>
506
506
  </details>
507
507
  <details class="compat-row" data-status="ok">
508
508
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">sendPasswordResetEmail(auth, email)</code><span class="compat-sub"><span class="compat-behavior">For an address NO account owns, prod RESOLVES silently — it does not throw <code>auth/user-not-found</code>. Email Enumeration Protection is on by default and refusing to leak account existence is the point. The sandbox matches: it resolves and mails nothing. A malformed address still throws <code>auth/invalid-email</code>. For a real account, the sandbox mails a reset code; <code>confirmPasswordReset</code> redeems it and the new password signs in while the old one throws <code>auth/wrong-password</code>.</span></span></span></summary>
509
- <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED on the enumeration-protection behavior — the fact most likely to be got wrong: <code>auth-sendpasswordresetemail-unknown-user</code> captured <code>resolvedForUnknownUser: true</code>, <code>unknownUserCode: null</code>, <code>malformedEmailCode: auth/invalid-email</code>. A shim that threw <code>auth/user-not-found</code> here would hand agent code an account oracle production deliberately removed. Reset round trip unit-backed in <code>unit:sandbox-email-link.test.ts</code>.</div></div>
509
+ <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED on the enumeration-protection behavior — the fact most likely to be got wrong: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-sendpasswordresetemail-unknown-user.json" target="_blank" rel="noopener noreferrer"><code>auth-sendpasswordresetemail-unknown-user</code></a> captured <code>resolvedForUnknownUser: true</code>, <code>unknownUserCode: null</code>, <code>malformedEmailCode: auth/invalid-email</code>. A shim that threw <code>auth/user-not-found</code> here would hand agent code an account oracle production deliberately removed. Reset round trip unit-backed in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-link.test.ts#L25" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-link.test.ts</code></a>. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/oracle-conformance.test.ts#L656" target="_blank" rel="noopener noreferrer"><code>oracle-conformance.test.ts</code></a>)</div></div>
510
510
  </details>
511
511
  <details class="compat-row" data-status="ok">
512
512
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">sendEmailVerification(user)</code><span class="compat-sub"><span class="compat-behavior">Throws <code>auth/missing-email</code> for a user with no email on the account (an anonymous user). For a real account the mail goes out and NOTHING ELSE HAPPENS: <code>user.emailVerified</code> stays <code>false</code>. Verification happens when the code in the message is redeemed (<code>applyActionCode</code>), not when it is sent.</span></span></span></summary>
513
- <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED on the anonymous reject: <code>auth-sendemailverification-shape</code> captured <code>anonymousUserCode: auth/missing-email</code> against prod. The send-does-not-verify property is the one the whole flow turns on and is unit-backed (<code>unit:sandbox-email-link.test.ts</code> asserts <code>emailVerified</code> is still false after the send and true only after the code is applied) — it cannot be oracle-confirmed end to end because confirming it would require reading a real inbox.</div></div>
513
+ <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED on the anonymous reject: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-sendemailverification-shape.json" target="_blank" rel="noopener noreferrer"><code>auth-sendemailverification-shape</code></a> captured <code>anonymousUserCode: auth/missing-email</code> against prod. The send-does-not-verify property is the one the whole flow turns on and is unit-backed (<a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-link.test.ts#L24" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-link.test.ts</code></a> asserts <code>emailVerified</code> is still false after the send and true only after the code is applied) — it cannot be oracle-confirmed end to end because confirming it would require reading a real inbox. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/oracle-conformance.test.ts#L676" target="_blank" rel="noopener noreferrer"><code>oracle-conformance.test.ts</code></a>)</div></div>
514
514
  </details>
515
515
  <details class="compat-row" data-status="ok">
516
516
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">verifyBeforeUpdateEmail(user, newEmail)</code><span class="compat-sub"><span class="compat-behavior">Mails a code to the NEW address and returns. The account's email does NOT change until that code is redeemed — the single guarantee separating this API from a bare <code>updateEmail</code>: the user must prove control of the new address before it becomes theirs. On redemption the account moves and the new address arrives <code>emailVerified: true</code>. <code>checkActionCode</code> on the code reports <code>data.email</code> = the new address and <code>data.previousEmail</code> = the old one.</span></span></span></summary>
517
- <div class="compat-evidence"><div class="compat-probe">UNIT-BACKED, not oracle-backed, and the reason is recorded rather than hidden: the probe (<code>auth-verifybeforeupdateemail-shape</code>) ran against the real project and came back <code>auth/operation-not-allowed</code> on every arm, because the oracle project has the Email/Password provider DISABLED and the probe could not even create the user whose email it would change. The capture is committed showing exactly that. Behavior is proven end to end against the sandbox in <code>unit:sandbox-email-link.test.ts</code>.</div></div>
517
+ <div class="compat-evidence"><div class="compat-probe">UNIT-BACKED, not oracle-backed, and the reason is recorded rather than hidden: the probe (<a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-verifybeforeupdateemail-shape.json" target="_blank" rel="noopener noreferrer"><code>auth-verifybeforeupdateemail-shape</code></a>) ran against the real project and came back <code>auth/operation-not-allowed</code> on every arm, because the oracle project has the Email/Password provider DISABLED and the probe could not even create the user whose email it would change. The capture is committed showing exactly that. Behavior is proven end to end against the sandbox in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-link.test.ts#L30" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-link.test.ts</code></a>. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/oracle-conformance.test.ts#L89" target="_blank" rel="noopener noreferrer"><code>oracle-conformance.test.ts</code></a>)</div></div>
518
518
  </details>
519
519
  <details class="compat-row" data-status="ok">
520
520
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">sandbox.takeAuthMail(auth) / sandbox.setAuthMailResolver(auth, resolver)</code><span class="compat-sub"><span class="compat-behavior">The sandbox IS the mail server. Every send posts a real message (operation, recipient, single-use code, and the full action link) to an outbox; <code>sandbox.takeAuthMail</code> reads it — the program's substitute for a human opening an inbox. The mailed link round-trips through the public <code>isSignInWithEmailLink</code> / <code>parseActionCodeURL</code> unchanged, and its code is the code the redeemer accepts, so the round trip production cannot complete without a human closes in-process. An installed <code>AuthMailResolver</code> is additionally notified per message; a resolver that THROWS does not fail the send that produced it.</span></span></span></summary>
521
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-email-link.test.ts</code> — sandbox-only driver (no <code>firebase/auth</code> counterpart; this is the seam that makes the family testable at all).</div></div>
521
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-link.test.ts#L102" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-link.test.ts</code></a> — sandbox-only driver (no <code>firebase/auth</code> counterpart; this is the seam that makes the family testable at all).</div></div>
522
522
  </details>
523
523
  <details class="compat-row" data-status="ok">
524
524
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">checkActionCode(auth, code) / verifyPasswordResetCode(auth, code) / confirmPasswordReset(auth, code, newPassword)</code><span class="compat-sub"><span class="compat-behavior"><code>checkActionCode</code> and <code>verifyPasswordResetCode</code> INSPECT a code without burning it — a check must not destroy the code the subsequent apply needs. <code>confirmPasswordReset</code> redeems it and sets the password, running the same strength check <code>createUserWithEmailAndPassword</code> runs; a weak new password throws <code>auth/weak-password</code> and does NOT burn the code (a typo must not destroy the user's one reset link). A code staged as expired throws <code>auth/expired-action-code</code>.</span></span></span></summary>
525
- <div class="compat-evidence"><div class="compat-probe">UNIT-BACKED, stated honestly: these three endpoints answered <code>auth/operation-not-allowed</code> in the oracle run (<code>auth-action-code-invalid</code>) because the oracle project's disabled password provider replies before the invalid-code contract can. That is a fact about the project's configuration, not about the API, so it is NOT asserted as evidence. The error codes used here are matched to the oracle-CAPTURED <code>AuthErrorCodes</code> map (<code>INVALID_OOB_CODE = auth/invalid-action-code</code>, <code>EXPIRED_OOB_CODE = auth/expired-action-code</code> — see auth#172). Behavior proven in <code>unit:sandbox-email-link.test.ts</code>.</div></div>
525
+ <div class="compat-evidence"><div class="compat-probe">UNIT-BACKED, stated honestly: these three endpoints answered <code>auth/operation-not-allowed</code> in the oracle run (<code>auth-action-code-invalid</code>) because the oracle project's disabled password provider replies before the invalid-code contract can. That is a fact about the project's configuration, not about the API, so it is NOT asserted as evidence. The error codes used here are matched to the oracle-CAPTURED <code>AuthErrorCodes</code> map (<code>INVALID_OOB_CODE = auth/invalid-action-code</code>, <code>EXPIRED_OOB_CODE = auth/expired-action-code</code> — see auth#172). Behavior proven in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-link.test.ts#L17" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-link.test.ts</code></a>.</div></div>
526
526
  </details>
527
527
  <details class="compat-row" data-status="diverged">
528
528
  <summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">sendSignInLinkToEmail</code><span class="compat-sub"><span class="compat-category"><code>auth/unauthorized-continue-uri</code> (domain allowlist)</span> · <span class="compat-behavior">Prod rejects a continue URL whose domain is not on the project's authorized-domains list. The sandbox has NO domain allowlist and does not invent one: it accepts any parseable continue URL. A continue URL that would be rejected in production is accepted here.</span></span></span></summary>
529
- <div class="compat-evidence"><div class="compat-probe">The sandbox has no project config to hold an authorized-domains list, so there is nothing to check against; inventing an allowlist would mean inventing a policy the user never set. The probe attempted the server arm (<code>auth-sendsigninlinktoemail-settings-validation</code>, <code>unauthorizedDomain</code>) but the oracle project has email-link sign-in disabled, so it answered <code>auth/operation-not-allowed</code> rather than <code>auth/unauthorized-continue-uri</code> — the divergence is declared from the documented contract, not from a capture we do not have.</div></div>
529
+ <div class="compat-evidence"><div class="compat-probe">The sandbox has no project config to hold an authorized-domains list, so there is nothing to check against; inventing an allowlist would mean inventing a policy the user never set. The probe attempted the server arm (<code>auth-sendsigninlinktoemail-settings-validation</code>, <code>unauthorizedDomain</code>) but the oracle project has email-link sign-in disabled, so it answered <code>auth/operation-not-allowed</code> rather than <code>auth/unauthorized-continue-uri</code> — the divergence is declared from the documented contract, not from a capture we do not have. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-link.test.ts#L26" target="_blank" rel="noopener noreferrer"><code>sandbox-email-link.test.ts</code></a>)</div></div>
530
530
  </details>
531
531
  </div>
532
532
 
@@ -535,19 +535,19 @@
535
535
  <div class="compat-list">
536
536
  <details class="compat-row" data-status="ok">
537
537
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">linkWithCredential(user, credential)</code><span class="compat-sub"><span class="compat-behavior">The ANONYMOUS UPGRADE. Linking an email credential onto an anonymous account upgrades it IN PLACE: the uid is PRESERVED, <code>isAnonymous</code> flips to <code>false</code>, the email is set, and <code>providerData</code> gains the provider. Preserving the uid is what keeps the data the user created while anonymous theirs. Returns a <code>UserCredential</code> with <code>operationType: 'link'</code> and <code>getAdditionalUserInfo().isNewUser === false</code> (a link never creates an identity). The linked credential then works as a first-class <code>signInWithEmailAndPassword</code>.</span></span></span></summary>
538
- <div class="compat-evidence"><div class="compat-probe">UNIT-BACKED, not oracle-backed — stated plainly. The probe (<code>auth-link-email-credential-to-anonymous</code>) ran against the real project and returned <code>linkCode: auth/operation-not-allowed</code>: the oracle project has the Email/Password provider DISABLED, so no email credential can be minted there and the flow cannot be reached. The capture is committed showing exactly that rather than being dropped. Behavior proven in <code>unit:sandbox-linking-reauth.test.ts</code>, including the uid-preservation invariant.</div></div>
538
+ <div class="compat-evidence"><div class="compat-probe">UNIT-BACKED, not oracle-backed — stated plainly. The probe (<a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-link-email-credential-to-anonymous.json" target="_blank" rel="noopener noreferrer"><code>auth-link-email-credential-to-anonymous</code></a>) ran against the real project and returned <code>linkCode: auth/operation-not-allowed</code>: the oracle project has the Email/Password provider DISABLED, so no email credential can be minted there and the flow cannot be reached. The capture is committed showing exactly that rather than being dropped. Behavior proven in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-linking-reauth.test.ts#L25" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-linking-reauth.test.ts</code></a>, including the uid-preservation invariant.</div></div>
539
539
  </details>
540
540
  <details class="compat-row" data-status="ok">
541
541
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">linkWithCredential</code><span class="compat-sub"><span class="compat-category">conflict codes</span> · <span class="compat-behavior"><code>auth/provider-already-linked</code> when the account already carries the provider (one identity per provider). <code>auth/email-already-in-use</code> when the email credential belongs to a DIFFERENT account — an address can back only one identity, so the link cannot be granted without stealing it.</span></span></span></summary>
542
- <div class="compat-evidence"><div class="compat-probe">UNIT-BACKED. The probe (<code>auth-link-conflicts</code>) ran and every arm returned <code>auth/operation-not-allowed</code> (same disabled Email/Password provider on the oracle project), so the conflict codes could not be observed against prod. They are matched instead to the oracle-CAPTURED <code>AuthErrorCodes</code> map (<code>PROVIDER_ALREADY_LINKED = auth/provider-already-linked</code>, <code>CREDENTIAL_ALREADY_IN_USE = auth/credential-already-in-use</code> — auth#172) and proven in <code>unit:sandbox-linking-reauth.test.ts</code>. NOTE: for an EMAIL credential the sandbox emits <code>auth/email-already-in-use</code>, the code prod uses for an address collision; <code>credential-already-in-use</code> remains the OAuth-credential case.</div></div>
542
+ <div class="compat-evidence"><div class="compat-probe">UNIT-BACKED. The probe (<a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-link-conflicts.json" target="_blank" rel="noopener noreferrer"><code>auth-link-conflicts</code></a>) ran and every arm returned <code>auth/operation-not-allowed</code> (same disabled Email/Password provider on the oracle project), so the conflict codes could not be observed against prod. They are matched instead to the oracle-CAPTURED <code>AuthErrorCodes</code> map (<code>PROVIDER_ALREADY_LINKED = auth/provider-already-linked</code>, <code>CREDENTIAL_ALREADY_IN_USE = auth/credential-already-in-use</code> — auth#172) and proven in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-linking-reauth.test.ts#L25" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-linking-reauth.test.ts</code></a>. NOTE: for an EMAIL credential the sandbox emits <code>auth/email-already-in-use</code>, the code prod uses for an address collision; <code>credential-already-in-use</code> remains the OAuth-credential case.</div></div>
543
543
  </details>
544
544
  <details class="compat-row" data-status="ok">
545
545
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">unlink(user, providerId)</code><span class="compat-sub"><span class="compat-behavior">Detaches a provider and returns the updated user with a SHRUNKEN <code>providerData</code>. Unlinking a provider that was never linked throws <code>auth/no-such-provider</code>. Unlinking <code>'password'</code> takes the password with it, so <code>signInWithEmailAndPassword</code> for that account stops working. Unlinking the LAST provider does NOT re-anonymize the account — <code>isAnonymous</code> describes how an identity was born, not what it currently carries.</span></span></span></summary>
546
- <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED on the reject path — the ONE linking fact the oracle could reach, because it needs no email credential: <code>auth-unlink-provider</code> captured <code>noSuchProviderCode: auth/no-such-provider</code> against prod on an anonymous user. Replayed in <code>unit:oracle-conformance.test.ts</code>. The detach path is unit-backed (<code>unit:sandbox-linking-reauth.test.ts</code>).</div></div>
546
+ <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED on the reject path — the ONE linking fact the oracle could reach, because it needs no email credential: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-unlink-provider.json" target="_blank" rel="noopener noreferrer"><code>auth-unlink-provider</code></a> captured <code>noSuchProviderCode: auth/no-such-provider</code> against prod on an anonymous user. Replayed in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/oracle-conformance.test.ts#L685" target="_blank" rel="noopener noreferrer"><code>unit:oracle-conformance.test.ts</code></a>. The detach path is unit-backed (<a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-linking-reauth.test.ts#L33" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-linking-reauth.test.ts</code></a>).</div></div>
547
547
  </details>
548
548
  <details class="compat-row" data-status="ok">
549
549
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">linkWithPopup(user, provider, resolver?) / linkWithRedirect(...)</code><span class="compat-sub"><span class="compat-behavior">Route through the SAME <code>AuthFlowResolver</code> seam as <code>signInWithPopup</code>, with <code>authType: 'link'</code> on the request so a host UI can present 'link your Google account' rather than 'sign in'. The resolved credential names the provider to attach; the sandbox performs the attach and the uid is preserved. A disabled provider throws <code>auth/operation-not-allowed</code> AHEAD of the resolver check — a code deliberately distinct from <code>auth/argument-error</code>, which keeps meaning 'enabled, but no resolver/mock wired'.</span></span></span></summary>
550
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-linking-reauth.test.ts</code> — asserts the resolver sees <code>authType: 'link'</code>, the uid is preserved, and the two error codes stay distinct. The OAuth arm cannot be oracle-probed at all (it needs a real IdP popup and a human), which is precisely why it goes through the resolver seam.</div></div>
550
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-linking-reauth.test.ts#L26" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-linking-reauth.test.ts</code></a> — asserts the resolver sees <code>authType: 'link'</code>, the uid is preserved, and the two error codes stay distinct. The OAuth arm cannot be oracle-probed at all (it needs a real IdP popup and a human), which is precisely why it goes through the resolver seam.</div></div>
551
551
  </details>
552
552
  </div>
553
553
 
@@ -556,15 +556,15 @@
556
556
  <div class="compat-list">
557
557
  <details class="compat-row" data-status="ok">
558
558
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">reauthenticateWithCredential(user, credential)</code><span class="compat-sub"><span class="compat-behavior">Really re-verifies: an email credential is checked against the stored password exactly as <code>signInWithEmailAndPassword</code> checks it. A wrong password throws <code>auth/wrong-password</code>. A credential belonging to a DIFFERENT account throws <code>auth/user-mismatch</code> — checked BEFORE the password compare, so it cannot leak whether the other account's password was right. On success a fresh ID token is minted (a new <code>authTime</code>), and the returned <code>UserCredential</code> carries <code>operationType: 'reauthenticate'</code>.</span></span></span></summary>
559
- <div class="compat-evidence"><div class="compat-probe">UNIT-BACKED, and the reason it is not oracle-backed is recorded rather than glossed: the probe (<code>auth-reauthenticate-with-credential</code>) ran against the real project and could not even create the two accounts it needs — <code>setupCode: auth/operation-not-allowed</code>, because the oracle project has the Email/Password provider DISABLED. The capture is committed showing that. <code>auth/user-mismatch</code> is matched to the oracle-captured <code>AuthErrorCodes</code> map (auth#172). Behavior proven in <code>unit:sandbox-linking-reauth.test.ts</code>.</div></div>
559
+ <div class="compat-evidence"><div class="compat-probe">UNIT-BACKED, and the reason it is not oracle-backed is recorded rather than glossed: the probe (<a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-reauthenticate-with-credential.json" target="_blank" rel="noopener noreferrer"><code>auth-reauthenticate-with-credential</code></a>) ran against the real project and could not even create the two accounts it needs — <code>setupCode: auth/operation-not-allowed</code>, because the oracle project has the Email/Password provider DISABLED. The capture is committed showing that. <code>auth/user-mismatch</code> is matched to the oracle-captured <code>AuthErrorCodes</code> map (auth#172). Behavior proven in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-linking-reauth.test.ts#L27" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-linking-reauth.test.ts</code></a>.</div></div>
560
560
  </details>
561
561
  <details class="compat-row" data-status="diverged">
562
562
  <summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">reauthenticate*</code><span class="compat-sub"><span class="compat-category"><code>auth/requires-recent-login</code> is NOT enforced</span> · <span class="compat-behavior">In production the POINT of re-authentication is the <code>auth/requires-recent-login</code> gate: <code>updateEmail</code> / <code>updatePassword</code> / <code>deleteUser</code> refuse to run on a session whose sign-in is older than a few minutes, and re-auth is how you clear it. The sandbox does NOT enforce that gate — those three mutations already run on a session of any age (a pre-existing documented divergence), so there is no gate here for re-auth to clear. What re-auth DOES do here is real but narrower: it genuinely re-verifies the credential, mints a fresh token, and returns <code>operationType: 'reauthenticate'</code>. Code that calls it runs unchanged against prod, where it also clears the gate.</span></span></span></summary>
563
- <div class="compat-evidence"><div class="compat-probe">Declared divergence, not a bug: inventing a recent-login gate would break every existing sandbox flow (which never re-authenticates) while proving nothing. <code>unit:sandbox-linking-reauth.test.ts</code> pins the behavior that IS provided (real credential re-verification + a fresh token).</div></div>
563
+ <div class="compat-evidence"><div class="compat-probe">Declared divergence, not a bug: inventing a recent-login gate would break every existing sandbox flow (which never re-authenticates) while proving nothing. <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-linking-reauth.test.ts#L27" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-linking-reauth.test.ts</code></a> pins the behavior that IS provided (real credential re-verification + a fresh token).</div></div>
564
564
  </details>
565
565
  <details class="compat-row" data-status="ok">
566
566
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">reauthenticateWithPopup(user, provider, resolver?) / reauthenticateWithRedirect(...)</code><span class="compat-sub"><span class="compat-behavior">Route through the shared resolver seam with <code>authType: 'reauth'</code>. The identity the resolver produces MUST be the user being re-authenticated — a resolver that hands back a different uid throws <code>auth/user-mismatch</code>. That check is the entire security content of the flow; without it 're-authentication' would accept anyone.</span></span></span></summary>
567
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-linking-reauth.test.ts</code> — asserts the resolver sees <code>authType: 'reauth'</code> and that an impostor identity is rejected with <code>auth/user-mismatch</code>.</div></div>
567
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-linking-reauth.test.ts#L28" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-linking-reauth.test.ts</code></a> — asserts the resolver sees <code>authType: 'reauth'</code> and that an impostor identity is rejected with <code>auth/user-mismatch</code>.</div></div>
568
568
  </details>
569
569
  </div>
570
570
 
@@ -573,43 +573,43 @@
573
573
  <div class="compat-list">
574
574
  <details class="compat-row" data-status="ok">
575
575
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">getAdditionalUserInfo(userCredential)</code><span class="compat-sub"><span class="compat-behavior">Returns <code>{ isNewUser, profile, providerId }</code>. For a fresh anonymous sign-in prod reports <code>{ isNewUser: true, providerId: null, profile: {} }</code> — <code>providerId</code> is NULL, not <code>'anonymous'</code>, because anonymous is not a federated provider. <code>isNewUser</code> is <code>true</code> for <code>createUserWithEmailAndPassword</code>, a first-time email-link sign-in, and a custom-token sign-in that minted the account; <code>false</code> for a returning <code>signInWithEmailAndPassword</code> and for every <code>link</code> / <code>reauthenticate</code>.</span></span></span></summary>
576
- <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED on the anonymous shape: <code>auth-additional-user-info-shape</code> captured <code>{isNewUser: true, providerId: null, profile: {}}</code> against prod. Replayed in <code>unit:oracle-conformance.test.ts</code>. Email create/sign-in + custom-token mint/return isNewUser matrix (providerId null per AUTH-B2): <code>unit:upstream-auth-probes.test.ts</code>. Email-link + link/reauth arms: <code>unit:sandbox-email-link.test.ts</code>, <code>unit:sandbox-linking-reauth.test.ts</code>.</div></div>
576
+ <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED on the anonymous shape: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-additional-user-info-shape.json" target="_blank" rel="noopener noreferrer"><code>auth-additional-user-info-shape</code></a> captured <code>{isNewUser: true, providerId: null, profile: {}}</code> against prod. Replayed in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/oracle-conformance.test.ts#L695" target="_blank" rel="noopener noreferrer"><code>unit:oracle-conformance.test.ts</code></a>. Email create/sign-in + custom-token mint/return isNewUser matrix (providerId null per AUTH-B2): <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/upstream-auth-probes.test.ts#L6" target="_blank" rel="noopener noreferrer"><code>unit:upstream-auth-probes.test.ts</code></a>. Email-link + link/reauth arms: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-link.test.ts#L20" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-link.test.ts</code></a>, <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-linking-reauth.test.ts#L22" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-linking-reauth.test.ts</code></a>.</div></div>
577
577
  </details>
578
578
  <details class="compat-row" data-status="ok">
579
579
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">ProviderId / SignInMethod / OperationType / ActionCodeOperation / AuthErrorCodes</code><span class="compat-sub"><span class="compat-behavior">The constant maps, value for value. <code>OperationType.SIGN_IN === 'signIn'</code>, <code>SignInMethod.EMAIL_LINK === 'emailLink'</code>, <code>ProviderId.GOOGLE === 'google.com'</code>, <code>ActionCodeOperation.PASSWORD_RESET === 'PASSWORD_RESET'</code>, and the 106-entry <code>AuthErrorCodes</code> map (<code>INVALID_OOB_CODE === 'auth/invalid-action-code'</code>, <code>PROVIDER_ALREADY_LINKED === 'auth/provider-already-linked'</code>, <code>NO_SUCH_PROVIDER === 'auth/no-such-provider'</code>, <code>USER_MISMATCH === 'auth/user-mismatch'</code>, …).</span></span></span></summary>
580
- <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED, snapshotted straight from the shipped SDK: <code>auth-mechanical-surface-constants</code> against firebase-js-sdk 12.13.0, replayed value-for-value in <code>unit:oracle-conformance.test.ts</code>. This matters more than it looks: consumer code COMPARES against these constants, so a mirror that got a string wrong would turn every such comparison into a silent <code>false</code> — a worse failure than the export simply being absent, because it typechecks and runs. NOTE: the capture's <code>persistenceTypes</code> block is deliberately NOT asserted — the harness runs under Node, where firebase/auth stubs the browser-only persistence tokens to <code>type: 'NONE'</code> (it reports 'NONE' even for <code>browserLocalPersistence</code>, which is unambiguously 'LOCAL'); asserting it would be asserting a harness artifact. See auth#178.</div></div>
580
+ <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED, snapshotted straight from the shipped SDK: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-mechanical-surface-constants.json" target="_blank" rel="noopener noreferrer"><code>auth-mechanical-surface-constants</code></a> against firebase-js-sdk 12.13.0, replayed value-for-value in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/oracle-conformance.test.ts#L711" target="_blank" rel="noopener noreferrer"><code>unit:oracle-conformance.test.ts</code></a>. This matters more than it looks: consumer code COMPARES against these constants, so a mirror that got a string wrong would turn every such comparison into a silent <code>false</code> — a worse failure than the export simply being absent, because it typechecks and runs. NOTE: the capture's <code>persistenceTypes</code> block is deliberately NOT asserted — the harness runs under Node, where firebase/auth stubs the browser-only persistence tokens to <code>type: 'NONE'</code> (it reports 'NONE' even for <code>browserLocalPersistence</code>, which is unambiguously 'LOCAL'); asserting it would be asserting a harness artifact. See auth#178.</div></div>
581
581
  </details>
582
582
  <details class="compat-row" data-status="diverged">
583
583
  <summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">signInWithCustomToken(auth, customToken)</code><span class="compat-sub"><span class="compat-behavior">Throws <code>auth/invalid-custom-token</code> for a malformed token and for the empty string. The sandbox accepts a token in the two shapes it can honestly read: a JSON (optionally base64url) <code>{uid, claims}</code> payload — exactly what <code>admin.auth().createCustomToken</code> signs, so the pyric-admin mint and this redeem compose — or a real three-part JWT whose payload segment carries <code>uid</code>/<code>sub</code>. The SIGNATURE IS NOT VERIFIED: the sandbox has no service-account key. The identity is created if it does not exist, and the credential carries <code>providerId: null</code> (custom-token sign-in is not a federated provider).</span></span></span></summary>
584
- <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED on the reject path: <code>auth-signinwithcustomtoken-invalid</code> captured <code>auth/invalid-custom-token</code> for both a malformed token and the empty string, replayed in <code>unit:oracle-conformance.test.ts</code>. Diverged on the ACCEPT path, declared rather than hidden: prod verifies an RS256 signature against the project's service-account key and the sandbox has no key, so it reads the token's claims WITHOUT verifying them. A forged token that prod would reject is accepted here. The happy path cannot be oracle-probed from a Web SDK client at all (it needs an Admin-SDK-signed JWT).</div></div>
584
+ <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED on the reject path: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-signinwithcustomtoken-invalid.json" target="_blank" rel="noopener noreferrer"><code>auth-signinwithcustomtoken-invalid</code></a> captured <code>auth/invalid-custom-token</code> for both a malformed token and the empty string, replayed in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/oracle-conformance.test.ts#L742" target="_blank" rel="noopener noreferrer"><code>unit:oracle-conformance.test.ts</code></a>. Diverged on the ACCEPT path, declared rather than hidden: prod verifies an RS256 signature against the project's service-account key and the sandbox has no key, so it reads the token's claims WITHOUT verifying them. A forged token that prod would reject is accepted here. The happy path cannot be oracle-probed from a Web SDK client at all (it needs an Admin-SDK-signed JWT).</div></div>
585
585
  </details>
586
586
  <details class="compat-row" data-status="ok">
587
587
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">validatePassword(auth, password)</code><span class="compat-sub"><span class="compat-behavior">Returns a <code>PasswordValidationStatus</code> against the project's password policy, WITHOUT attempting a sign-up (so a UI can show live strength feedback). The sandbox's policy is <code>minPasswordLength: 6</code>, <code>maxPasswordLength: 4096</code>, <code>enforcementState: 'ENFORCE'</code>, with every character-class requirement UNSET — so a password this function calls valid is exactly one <code>createUserWithEmailAndPassword</code> will accept. The character-class fields are <code>undefined</code>, not <code>false</code>: upstream distinguishes 'not required' from 'required and unmet', and reporting <code>false</code> would claim the password failed a rule the project never had.</span></span></span></summary>
588
- <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED: <code>auth-validatepassword-status-shape</code> captured prod's live policy (minPasswordLength 6, maxPasswordLength 4096, ENFORCE, character classes unset) and the status shape for a weak and a strong password. Replayed in <code>unit:oracle-conformance.test.ts</code>. The 6-char minimum agrees with the separately oracle-pinned <code>auth/weak-password</code> threshold on the create path, so the two surfaces draw the same line here exactly as they do in prod.</div></div>
588
+ <div class="compat-evidence"><div class="compat-probe">ORACLE-BACKED: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-validatepassword-status-shape.json" target="_blank" rel="noopener noreferrer"><code>auth-validatepassword-status-shape</code></a> captured prod's live policy (minPasswordLength 6, maxPasswordLength 4096, ENFORCE, character classes unset) and the status shape for a weak and a strong password. Replayed in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/oracle-conformance.test.ts#L753" target="_blank" rel="noopener noreferrer"><code>unit:oracle-conformance.test.ts</code></a>. The 6-char minimum agrees with the separately oracle-pinned <code>auth/weak-password</code> threshold on the create path, so the two surfaces draw the same line here exactly as they do in prod.</div></div>
589
589
  </details>
590
590
  <details class="compat-row" data-status="unsupported">
591
591
  <summary class="compat-line"><span class="compat-dot" data-status="unsupported" role="img" aria-label="Not implemented yet" title="Not implemented yet"></span><span class="compat-main"><code class="compat-api">fetchSignInMethodsForEmail(auth, email)</code><span class="compat-sub"><span class="compat-behavior">Not implemented; disposition <code>auth.email-enumeration</code> owns the reviewed availability and rationale.</span></span></span></summary>
592
- <div class="compat-evidence"><div class="compat-probe">disposition:auth.email-enumeration; the <code>auth-fetchsigninmethodsforemail-deprecated</code> capture is retained but inconclusive because the oracle project's Email/Password provider is disabled.</div></div>
592
+ <div class="compat-evidence"><div class="compat-probe">disposition:auth.email-enumeration; the <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-fetchsigninmethodsforemail-deprecated.json" target="_blank" rel="noopener noreferrer"><code>auth-fetchsigninmethodsforemail-deprecated</code></a> capture is retained but inconclusive because the oracle project's Email/Password provider is disabled.</div></div>
593
593
  </details>
594
594
  <details class="compat-row" data-status="diverged">
595
595
  <summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">inMemoryPersistence / browserSessionPersistence / browserLocalPersistence / indexedDBLocalPersistence / browserCookiePersistence / browserPopupRedirectResolver / debugErrorMap / prodErrorMap</code><span class="compat-sub"><span class="compat-behavior">Inert configuration tokens, accepted so the idiomatic <code>initializeAuth(app, { persistence: indexedDBLocalPersistence })</code> and <code>setPersistence(auth, browserCookiePersistence)</code> compile, run, and behave identically. The <code>.type</code> discriminant matches upstream exactly (<code>NONE</code> / <code>SESSION</code> / <code>LOCAL</code> / <code>LOCAL</code> / <code>COOKIE</code>) because consumer code branches on it. <code>browserPopupRedirectResolver</code> is accepted and ignored — the sandbox has its own first-class pluggable equivalent (<code>sandbox.setAuthFlowResolver</code>). <code>prodErrorMap</code> is accepted and DELIBERATELY not honored: installing it upstream strips error messages, and doing that in a sandbox whose purpose is to tell a developer what went wrong would be actively hostile.</span></span></span></summary>
596
- <div class="compat-evidence"><div class="compat-probe">In an in-memory sandbox the persistence CHOICE has no observable consequence, so accepting the token and recording the mode is the honest behavior — the same inert-token pattern <code>pyric/firestore</code> uses for its cache-factory tokens. <code>unit:types.test.ts</code> + <code>unit:sandbox-email-link.test.ts</code> cover the exports. Oracle caveat recorded on auth#172: the <code>persistenceTypes</code> block of <code>auth-mechanical-surface-constants</code> reports <code>NONE</code> for every token including <code>browserLocalPersistence</code> — a Node-build stub artifact, not the browser contract, so the <code>.type</code> values here follow the documented <code>Persistence.type</code> union instead.</div></div>
596
+ <div class="compat-evidence"><div class="compat-probe">In an in-memory sandbox the persistence CHOICE has no observable consequence, so accepting the token and recording the mode is the honest behavior — the same inert-token pattern <code>pyric/firestore</code> uses for its cache-factory tokens. <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/types.test.ts#L1" target="_blank" rel="noopener noreferrer"><code>unit:types.test.ts</code></a> + <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-link.test.ts#L1" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-link.test.ts</code></a> cover the exports. Oracle caveat recorded on auth#172: the <code>persistenceTypes</code> block of <code>auth-mechanical-surface-constants</code> reports <code>NONE</code> for every token including <code>browserLocalPersistence</code> — a Node-build stub artifact, not the browser contract, so the <code>.type</code> values here follow the documented <code>Persistence.type</code> union instead.</div></div>
597
597
  </details>
598
598
  <details class="compat-row" data-status="diverged">
599
599
  <summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">revokeAccessToken(auth, token)</code><span class="compat-sub"><span class="compat-behavior">Accepted no-op. In production this reaches OUTSIDE Firebase entirely — it tells the identity provider (in practice Apple) to revoke an OAuth access token, which is a call landing on Apple's servers. There is no external IdP behind a sandbox sign-in, so there is no token out there to revoke and nothing this call could truthfully do. It resolves (so the account-deletion flow Apple requires an app to ship runs end to end against the sandbox) and changes no sandbox state (because claiming otherwise would be a lie).</span></span></span></summary>
600
- <div class="compat-evidence"><div class="compat-probe"><code>unit:types.test.ts</code> — the export resolves. Deliberately not oracle-probed: a successful probe would revoke a real token at a real IdP, which is a side effect on someone else's system that a conformance run has no business causing.</div></div>
600
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/types.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:types.test.ts</code></a> — the export resolves. Deliberately not oracle-probed: a successful probe would revoke a real token at a real IdP, which is a side effect on someone else's system that a conformance run has no business causing.</div></div>
601
601
  </details>
602
602
  <details class="compat-row" data-status="ok">
603
603
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">AuthCredential / EmailAuthCredential / OAuthCredential</code><span class="compat-sub"><span class="compat-behavior">Real classes, not markers. <code>EmailAuthProvider.credential(email, password)</code> returns an <code>EmailAuthCredential</code> that CARRIES THE SECRET — which is what makes <code>linkWithCredential</code>, <code>reauthenticateWithCredential</code>, and <code>signInWithCredential</code> decidable in the sandbox with no resolver and no mock (the backend already stores and verifies passwords). <code>credentialWithLink</code> carries the link instead, and <code>signInMethod</code> discriminates (<code>'password'</code> vs <code>'emailLink'</code>). The backing secret is non-enumerable, so a spread or <code>Object.keys</code> walk does not pick it up; <code>toJSON()</code> DOES carry it, matching upstream (whose <code>fromJSON</code> needs it to round-trip). <code>OAuthCredential</code> carries the IdP tokens, which the sandbox cannot verify — those flows keep going through the resolver seam.</span></span></span></summary>
604
- <div class="compat-evidence"><div class="compat-probe"><code>unit:sandbox-linking-reauth.test.ts</code> — pins the secret-carrying behavior, the enumerable/toJSON split (so nobody 'hardens' it into a divergence later), and that a real email credential now signs in via <code>signInWithCredential</code> instead of throwing the sandbox-only <code>auth/no-mock-configured</code> it used to.</div></div>
604
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-linking-reauth.test.ts#L323" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-linking-reauth.test.ts</code></a> — pins the secret-carrying behavior, the enumerable/toJSON split (so nobody 'hardens' it into a divergence later), and that a real email credential now signs in via <code>signInWithCredential</code> instead of throwing the sandbox-only <code>auth/no-mock-configured</code> it used to.</div></div>
605
605
  </details>
606
606
  <details class="compat-row" data-status="ok">
607
607
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">TwitterAuthProvider / SAMLAuthProvider</code><span class="compat-sub"><span class="compat-behavior">Provider marker classes. <code>TwitterAuthProvider.credential(token, secret)</code> takes a token AND a secret — Twitter is the one OAuth 1.0a provider in the set, where the OAuth 2.0 providers take a single access token. <code>SAMLAuthProvider</code>'s constructor ENFORCES the <code>saml.</code> providerId prefix (throwing <code>auth/argument-error</code> otherwise), because that id is what routes an assertion to the right configured IdP and a typo there would silently target nothing. SAML has no client-constructible credential, so the class has no <code>credential()</code> factory.</span></span></span></summary>
608
- <div class="compat-evidence"><div class="compat-probe"><code>unit:types.test.ts</code> — construction, PROVIDER_ID values, and the SAML prefix guard.</div></div>
608
+ <div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/types.test.ts#L1" target="_blank" rel="noopener noreferrer"><code>unit:types.test.ts</code></a> — construction, PROVIDER_ID values, and the SAML prefix guard.</div></div>
609
609
  </details>
610
610
  <details class="compat-row" data-status="ok">
611
611
  <summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">User.providerData</code><span class="compat-sub"><span class="compat-behavior">One entry PER LINKED PROVIDER, read from the identity's stored record. Previously the sandbox synthesized a single <code>{providerId: 'password'}</code> entry for EVERY non-anonymous user, so a Google popup sign-in reported its provider as <code>'password'</code> and a linked account could never show more than one. Consumer code branches on this array (that is what it is for — 'is this account linked to Google?'), so the synthesized version was actively misleading. Empty for anonymous users; after <code>unlink</code> of the last provider it is genuinely empty (it does not resurrect the removed provider).</span></span></span></summary>
612
- <div class="compat-evidence"><div class="compat-probe">Was a BUG until this climb, and worth naming as one: the array was fabricated, not read. Now fixed and locked. <code>unit:sandbox-linking-reauth.test.ts</code> now pins that a Google link surfaces <code>google.com</code> in <code>providerData</code>, that <code>unlink</code> shrinks it, and that unlinking the last provider leaves it empty rather than falling back to a synthesized <code>'password'</code> entry.</div></div>
612
+ <div class="compat-evidence"><div class="compat-probe">Was a BUG until this climb, and worth naming as one: the array was fabricated, not read. Now fixed and locked. <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-linking-reauth.test.ts#L54" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-linking-reauth.test.ts</code></a> now pins that a Google link surfaces <code>google.com</code> in <code>providerData</code>, that <code>unlink</code> shrinks it, and that unlinking the last provider leaves it empty rather than falling back to a synthesized <code>'password'</code> entry.</div></div>
613
613
  </details>
614
614
  </div>
615
615
 
@@ -621,11 +621,11 @@ Known differences between Pyric and production Firebase. Each remains tracked as
621
621
 
622
622
  <div class="compat-list">
623
623
  <details class="compat-row" data-status="diverged"><summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">signInAnonymously(auth)</code><span class="compat-sub"><span class="compat-behavior">Auto-generates a uid for fresh sign-ins (sandbox format: <code>anonymous-{N}</code>)</span></span></span></summary>
624
- <div class="compat-evidence"><div class="compat-note"><code>unit:sandbox-anonymous.test.ts</code> — prod uids are 28-char base64-ish; sandbox uses a readable counter for debuggability</div></div></details>
624
+ <div class="compat-evidence"><div class="compat-note"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-anonymous.test.ts#L7" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-anonymous.test.ts</code></a> — prod uids are 28-char base64-ish; sandbox uses a readable counter for debuggability</div></div></details>
625
625
  <details class="compat-row" data-status="diverged"><summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">signInAnonymously(auth)</code><span class="compat-sub"><span class="compat-behavior">Anonymous users persist across page reload via configured <code>Persistence</code> (prod only — sandbox has no persistence layer)</span></span></span></summary>
626
626
  <div class="compat-evidence"><div class="compat-note">divergence: sandbox memory only; within one tab the user persists, across reload they don't</div></div></details>
627
627
  <details class="compat-row" data-status="diverged"><summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">signInWithEmailAndPassword(auth, email, password)</code><span class="compat-sub"><span class="compat-behavior">An EMPTY password throws <code>auth/missing-password</code> (message "A non-empty password must be provided"), fired before the user-DB lookup so it can't be used to enumerate seeded emails. Upstream maps the <code>MISSING_PASSWORD</code> server error (<code>core/errors.ts:92,282,563</code>). ⚠ best-known semantics — message text not yet captured against a live project (STOP-flagged for an oracle pass; the <code>.code</code> is the load-bearing part).</span></span></span></summary>
628
- <div class="compat-evidence"><div class="compat-note"><code>unit:sandbox-cluster-b9-b12.test.ts</code> (locks AUTH-B11)</div></div></details>
628
+ <div class="compat-evidence"><div class="compat-note"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-cluster-b9-b12.test.ts#L18" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-cluster-b9-b12.test.ts</code></a> (locks AUTH-B11)</div></div></details>
629
629
  <details class="compat-row" data-status="diverged"><summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">signOut(auth)</code><span class="compat-sub"><span class="compat-behavior">Clears the active session's persistence in prod; sandbox has no persistence</span></span></span></summary>
630
630
  <div class="compat-evidence"><div class="compat-note">divergence: same memory-only constraint as the anonymous persistence row</div></div></details>
631
631
  <details class="compat-row" data-status="diverged"><summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">setPersistence(auth, persistence)</code><span class="compat-sub"><span class="compat-behavior">Actually changes where the auth state is persisted</span></span></span></summary>
@@ -633,25 +633,25 @@ Known differences between Pyric and production Firebase. Each remains tracked as
633
633
  <details class="compat-row" data-status="diverged"><summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">signInWithPopup(auth, provider)` / `signInWithCredential(auth, credential)</code><span class="compat-sub"><span class="compat-behavior">Opens a popup window in prod</span></span></span></summary>
634
634
  <div class="compat-evidence"><div class="compat-note">divergence: sandbox skips the popup; mock pre-stage replaces the popup result</div></div></details>
635
635
  <details class="compat-row" data-status="diverged"><summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">beforeAuthStateChanged(auth, callback, onAbort?)</code><span class="compat-sub"><span class="compat-behavior"><code>sandbox.setUser</code> (the sandbox-only test driver) BYPASSES the gate entirely — it is a raw identity force with no prod analog (same bypass it already has for provider enforcement / <code>signInProvider</code> tracking), so no registered <code>beforeAuthStateChanged</code> callback runs and none can block it.</span></span></span></summary>
636
- <div class="compat-evidence"><div class="compat-note"><code>unit:sandbox-before-auth-state-changed.test.ts</code> ("sandbox.setUser test driver bypasses the gate")</div></div></details>
636
+ <div class="compat-evidence"><div class="compat-note"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-before-auth-state-changed.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-before-auth-state-changed.test.ts</code></a> ("sandbox.setUser test driver bypasses the gate")</div></div></details>
637
637
  <details class="compat-row" data-status="diverged"><summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">beforeAuthStateChanged(auth, callback, onAbort?)</code><span class="compat-sub"><span class="compat-behavior">Served-worker path (SharedWorker-backed auth in <code>@pyric/cli</code>): the worker owns the shared user pool and commits transitions on its own side of the port, so a page-local <code>beforeAuthStateChanged</code> registration can't actually gate a worker-driven sign-in. Rather than silently accept a callback that could never run, registering THROWS immediately (<code>auth/operation-not-supported-in-this-environment</code>) — same defensive pattern as <code>signInWithCredential</code> over the worker.</span></span></span></summary>
638
638
  <div class="compat-evidence"><div class="compat-note"><code>packages/cli/src/serve/worker/client.ts</code> (<code>beforeAuthStateChanged</code> throws <code>makeUnsupported</code>)</div></div></details>
639
639
  <details class="compat-row" data-status="diverged"><summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">updateEmail(user, newEmail)</code><span class="compat-sub"><span class="compat-behavior">Changes the stored email (via the same path as <code>sandbox.updateUser</code>, rejecting <code>auth/email-already-in-use</code> / <code>auth/invalid-email</code>) and mutates the held <code>user</code> in place, so the next sign-in resolves against the new email. Leniency vs prod: the sandbox does NOT enforce <code>auth/requires-recent-login</code> and is not routed through <code>verifyBeforeUpdateEmail</code> (which the real SDK requires when email-enumeration protection is on)</span></span></span></summary>
640
- <div class="compat-evidence"><div class="compat-note"><code>unit:fruit-aliases.test.ts</code> — stored email changes; re-sign-in works with the new email, fails with the old</div></div></details>
640
+ <div class="compat-evidence"><div class="compat-note"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/fruit-aliases.test.ts#L18" target="_blank" rel="noopener noreferrer"><code>unit:fruit-aliases.test.ts</code></a> — stored email changes; re-sign-in works with the new email, fails with the old</div></div></details>
641
641
  <details class="compat-row" data-status="diverged"><summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">updatePassword(user, newPassword)</code><span class="compat-sub"><span class="compat-behavior">Sets the stored password (validated for strength). The sandbox DOES store and verify passwords, so this is a real mutation: a subsequent <code>signInWithEmailAndPassword</code> with the new password succeeds and the old one throws <code>auth/wrong-password</code>. Leniency vs prod: no <code>auth/requires-recent-login</code> enforcement</span></span></span></summary>
642
- <div class="compat-evidence"><div class="compat-note"><code>unit:fruit-aliases.test.ts</code> — new password signs in, old password throws <code>auth/wrong-password</code></div></div></details>
642
+ <div class="compat-evidence"><div class="compat-note"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/fruit-aliases.test.ts#L19" target="_blank" rel="noopener noreferrer"><code>unit:fruit-aliases.test.ts</code></a> — new password signs in, old password throws <code>auth/wrong-password</code></div></div></details>
643
643
  <details class="compat-row" data-status="diverged"><summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">useDeviceLanguage(auth)</code><span class="compat-sub"><span class="compat-behavior">Accepted no-op — the sandbox has no device locale to read, so there is no language to set; accepted so init code that calls it compiles + runs</span></span></span></summary>
644
- <div class="compat-evidence"><div class="compat-note"><code>unit:fruit-aliases.test.ts</code> — resolves/returns without error</div></div></details>
644
+ <div class="compat-evidence"><div class="compat-note"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/fruit-aliases.test.ts#L22" target="_blank" rel="noopener noreferrer"><code>unit:fruit-aliases.test.ts</code></a> — resolves/returns without error</div></div></details>
645
645
  <details class="compat-row" data-status="diverged"><summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">sendSignInLinkToEmail</code><span class="compat-sub"><span class="compat-behavior">Prod rejects a continue URL whose domain is not on the project's authorized-domains list. The sandbox has NO domain allowlist and does not invent one: it accepts any parseable continue URL. A continue URL that would be rejected in production is accepted here.</span></span></span></summary>
646
- <div class="compat-evidence"><div class="compat-note">The sandbox has no project config to hold an authorized-domains list, so there is nothing to check against; inventing an allowlist would mean inventing a policy the user never set. The probe attempted the server arm (<code>auth-sendsigninlinktoemail-settings-validation</code>, <code>unauthorizedDomain</code>) but the oracle project has email-link sign-in disabled, so it answered <code>auth/operation-not-allowed</code> rather than <code>auth/unauthorized-continue-uri</code> — the divergence is declared from the documented contract, not from a capture we do not have.</div></div></details>
646
+ <div class="compat-evidence"><div class="compat-note">The sandbox has no project config to hold an authorized-domains list, so there is nothing to check against; inventing an allowlist would mean inventing a policy the user never set. The probe attempted the server arm (<code>auth-sendsigninlinktoemail-settings-validation</code>, <code>unauthorizedDomain</code>) but the oracle project has email-link sign-in disabled, so it answered <code>auth/operation-not-allowed</code> rather than <code>auth/unauthorized-continue-uri</code> — the divergence is declared from the documented contract, not from a capture we do not have. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-link.test.ts#L26" target="_blank" rel="noopener noreferrer"><code>sandbox-email-link.test.ts</code></a>)</div></div></details>
647
647
  <details class="compat-row" data-status="diverged"><summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">reauthenticate*</code><span class="compat-sub"><span class="compat-behavior">In production the POINT of re-authentication is the <code>auth/requires-recent-login</code> gate: <code>updateEmail</code> / <code>updatePassword</code> / <code>deleteUser</code> refuse to run on a session whose sign-in is older than a few minutes, and re-auth is how you clear it. The sandbox does NOT enforce that gate — those three mutations already run on a session of any age (a pre-existing documented divergence), so there is no gate here for re-auth to clear. What re-auth DOES do here is real but narrower: it genuinely re-verifies the credential, mints a fresh token, and returns <code>operationType: 'reauthenticate'</code>. Code that calls it runs unchanged against prod, where it also clears the gate.</span></span></span></summary>
648
- <div class="compat-evidence"><div class="compat-note">Declared divergence, not a bug: inventing a recent-login gate would break every existing sandbox flow (which never re-authenticates) while proving nothing. <code>unit:sandbox-linking-reauth.test.ts</code> pins the behavior that IS provided (real credential re-verification + a fresh token).</div></div></details>
648
+ <div class="compat-evidence"><div class="compat-note">Declared divergence, not a bug: inventing a recent-login gate would break every existing sandbox flow (which never re-authenticates) while proving nothing. <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-linking-reauth.test.ts#L27" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-linking-reauth.test.ts</code></a> pins the behavior that IS provided (real credential re-verification + a fresh token).</div></div></details>
649
649
  <details class="compat-row" data-status="diverged"><summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">signInWithCustomToken(auth, customToken)</code><span class="compat-sub"><span class="compat-behavior">Throws <code>auth/invalid-custom-token</code> for a malformed token and for the empty string. The sandbox accepts a token in the two shapes it can honestly read: a JSON (optionally base64url) <code>{uid, claims}</code> payload — exactly what <code>admin.auth().createCustomToken</code> signs, so the pyric-admin mint and this redeem compose — or a real three-part JWT whose payload segment carries <code>uid</code>/<code>sub</code>. The SIGNATURE IS NOT VERIFIED: the sandbox has no service-account key. The identity is created if it does not exist, and the credential carries <code>providerId: null</code> (custom-token sign-in is not a federated provider).</span></span></span></summary>
650
- <div class="compat-evidence"><div class="compat-note">ORACLE-BACKED on the reject path: <code>auth-signinwithcustomtoken-invalid</code> captured <code>auth/invalid-custom-token</code> for both a malformed token and the empty string, replayed in <code>unit:oracle-conformance.test.ts</code>. Diverged on the ACCEPT path, declared rather than hidden: prod verifies an RS256 signature against the project's service-account key and the sandbox has no key, so it reads the token's claims WITHOUT verifying them. A forged token that prod would reject is accepted here. The happy path cannot be oracle-probed from a Web SDK client at all (it needs an Admin-SDK-signed JWT).</div></div></details>
650
+ <div class="compat-evidence"><div class="compat-note">ORACLE-BACKED on the reject path: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-signinwithcustomtoken-invalid.json" target="_blank" rel="noopener noreferrer"><code>auth-signinwithcustomtoken-invalid</code></a> captured <code>auth/invalid-custom-token</code> for both a malformed token and the empty string, replayed in <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/oracle-conformance.test.ts#L742" target="_blank" rel="noopener noreferrer"><code>unit:oracle-conformance.test.ts</code></a>. Diverged on the ACCEPT path, declared rather than hidden: prod verifies an RS256 signature against the project's service-account key and the sandbox has no key, so it reads the token's claims WITHOUT verifying them. A forged token that prod would reject is accepted here. The happy path cannot be oracle-probed from a Web SDK client at all (it needs an Admin-SDK-signed JWT).</div></div></details>
651
651
  <details class="compat-row" data-status="diverged"><summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">inMemoryPersistence / browserSessionPersistence / browserLocalPersistence / indexedDBLocalPersistence / browserCookiePersistence / browserPopupRedirectResolver / debugErrorMap / prodErrorMap</code><span class="compat-sub"><span class="compat-behavior">Inert configuration tokens, accepted so the idiomatic <code>initializeAuth(app, { persistence: indexedDBLocalPersistence })</code> and <code>setPersistence(auth, browserCookiePersistence)</code> compile, run, and behave identically. The <code>.type</code> discriminant matches upstream exactly (<code>NONE</code> / <code>SESSION</code> / <code>LOCAL</code> / <code>LOCAL</code> / <code>COOKIE</code>) because consumer code branches on it. <code>browserPopupRedirectResolver</code> is accepted and ignored — the sandbox has its own first-class pluggable equivalent (<code>sandbox.setAuthFlowResolver</code>). <code>prodErrorMap</code> is accepted and DELIBERATELY not honored: installing it upstream strips error messages, and doing that in a sandbox whose purpose is to tell a developer what went wrong would be actively hostile.</span></span></span></summary>
652
- <div class="compat-evidence"><div class="compat-note">In an in-memory sandbox the persistence CHOICE has no observable consequence, so accepting the token and recording the mode is the honest behavior — the same inert-token pattern <code>pyric/firestore</code> uses for its cache-factory tokens. <code>unit:types.test.ts</code> + <code>unit:sandbox-email-link.test.ts</code> cover the exports. Oracle caveat recorded on auth#172: the <code>persistenceTypes</code> block of <code>auth-mechanical-surface-constants</code> reports <code>NONE</code> for every token including <code>browserLocalPersistence</code> — a Node-build stub artifact, not the browser contract, so the <code>.type</code> values here follow the documented <code>Persistence.type</code> union instead.</div></div></details>
652
+ <div class="compat-evidence"><div class="compat-note">In an in-memory sandbox the persistence CHOICE has no observable consequence, so accepting the token and recording the mode is the honest behavior — the same inert-token pattern <code>pyric/firestore</code> uses for its cache-factory tokens. <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/types.test.ts#L1" target="_blank" rel="noopener noreferrer"><code>unit:types.test.ts</code></a> + <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/sandbox-email-link.test.ts#L1" target="_blank" rel="noopener noreferrer"><code>unit:sandbox-email-link.test.ts</code></a> cover the exports. Oracle caveat recorded on auth#172: the <code>persistenceTypes</code> block of <code>auth-mechanical-surface-constants</code> reports <code>NONE</code> for every token including <code>browserLocalPersistence</code> — a Node-build stub artifact, not the browser contract, so the <code>.type</code> values here follow the documented <code>Persistence.type</code> union instead.</div></div></details>
653
653
  <details class="compat-row" data-status="diverged"><summary class="compat-line"><span class="compat-dot" data-status="diverged" role="img" aria-label="Diverged (documented)" title="Diverged (documented)"></span><span class="compat-main"><code class="compat-api">revokeAccessToken(auth, token)</code><span class="compat-sub"><span class="compat-behavior">Accepted no-op. In production this reaches OUTSIDE Firebase entirely — it tells the identity provider (in practice Apple) to revoke an OAuth access token, which is a call landing on Apple's servers. There is no external IdP behind a sandbox sign-in, so there is no token out there to revoke and nothing this call could truthfully do. It resolves (so the account-deletion flow Apple requires an app to ship runs end to end against the sandbox) and changes no sandbox state (because claiming otherwise would be a lie).</span></span></span></summary>
654
- <div class="compat-evidence"><div class="compat-note"><code>unit:types.test.ts</code> — the export resolves. Deliberately not oracle-probed: a successful probe would revoke a real token at a real IdP, which is a side effect on someone else's system that a conformance run has no business causing.</div></div></details>
654
+ <div class="compat-evidence"><div class="compat-note"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/auth/types.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:types.test.ts</code></a> — the export resolves. Deliberately not oracle-probed: a successful probe would revoke a real token at a real IdP, which is a side effect on someone else's system that a conformance run has no business causing.</div></div></details>
655
655
  </div>
656
656
 
657
657
  ### Not implemented yet
@@ -668,7 +668,7 @@ Tracked behavior that is not implemented in the current contract.
668
668
  <details class="compat-row" data-status="unsupported"><summary class="compat-line"><span class="compat-dot" data-status="unsupported" role="img" aria-label="Not implemented yet" title="Not implemented yet"></span><span class="compat-main"><code class="compat-api">User` methods</code><span class="compat-sub"><span class="compat-behavior"><code>user.reload()</code> / <code>user.delete()</code> / <code>user.toJSON()</code> / <code>user.refreshToken</code> / <code>user.tenantId</code></span></span></span></summary>
669
669
  <div class="compat-evidence"><div class="compat-note">not modeled by the sandbox; retained as an explicit unsupported behavior row rather than synthesized (AUTH-GAP)</div></div></details>
670
670
  <details class="compat-row" data-status="unsupported"><summary class="compat-line"><span class="compat-dot" data-status="unsupported" role="img" aria-label="Not implemented yet" title="Not implemented yet"></span><span class="compat-main"><code class="compat-api">fetchSignInMethodsForEmail(auth, email)</code><span class="compat-sub"><span class="compat-behavior">Not implemented; disposition <code>auth.email-enumeration</code> owns the reviewed availability and rationale.</span></span></span></summary>
671
- <div class="compat-evidence"><div class="compat-note">disposition:auth.email-enumeration; the <code>auth-fetchsigninmethodsforemail-deprecated</code> capture is retained but inconclusive because the oracle project's Email/Password provider is disabled.</div></div></details>
671
+ <div class="compat-evidence"><div class="compat-note">disposition:auth.email-enumeration; the <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/auth/auth-fetchsigninmethodsforemail-deprecated.json" target="_blank" rel="noopener noreferrer"><code>auth-fetchsigninmethodsforemail-deprecated</code></a> capture is retained but inconclusive because the oracle project's Email/Password provider is disabled.</div></div></details>
672
672
  </div>
673
673
 
674
674
  ### Unverified