@volter/twin-clerk 0.1.0

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 (321) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +102 -0
  3. package/client/clerk-js-dist/167_clerk.headless_b86dff_5.127.2.js +1 -0
  4. package/client/clerk-js-dist/2172_clerk.browser_0cc2cc_5.127.2.js +1 -0
  5. package/client/clerk-js-dist/2172_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  6. package/client/clerk-js-dist/368_clerk.headless_b86dff_5.127.2.js +3 -0
  7. package/client/clerk-js-dist/377_clerk.headless_b86dff_5.127.2.js +1 -0
  8. package/client/clerk-js-dist/4170_clerk.browser_0cc2cc_5.127.2.js +1 -0
  9. package/client/clerk-js-dist/4170_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  10. package/client/clerk-js-dist/437_clerk.headless_b86dff_5.127.2.js +1 -0
  11. package/client/clerk-js-dist/5192_clerk.browser_0cc2cc_5.127.2.js +1 -0
  12. package/client/clerk-js-dist/5192_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  13. package/client/clerk-js-dist/553_clerk.headless_b86dff_5.127.2.js +1 -0
  14. package/client/clerk-js-dist/5645_clerk.browser_0cc2cc_5.127.2.js +1 -0
  15. package/client/clerk-js-dist/5645_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  16. package/client/clerk-js-dist/623_clerk.headless_b86dff_5.127.2.js +42 -0
  17. package/client/clerk-js-dist/7067_clerk.browser_0cc2cc_5.127.2.js +1 -0
  18. package/client/clerk-js-dist/7067_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  19. package/client/clerk-js-dist/7502_clerk.browser_0cc2cc_5.127.2.js +1 -0
  20. package/client/clerk-js-dist/7502_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  21. package/client/clerk-js-dist/785_clerk.headless.browser_540199_5.127.2.js +1 -0
  22. package/client/clerk-js-dist/785_clerk.headless_b86dff_5.127.2.js +1 -0
  23. package/client/clerk-js-dist/8043_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  24. package/client/clerk-js-dist/825_clerk.headless.browser_540199_5.127.2.js +1 -0
  25. package/client/clerk-js-dist/825_clerk.headless_b86dff_5.127.2.js +1 -0
  26. package/client/clerk-js-dist/890_clerk.headless_b86dff_5.127.2.js +2 -0
  27. package/client/clerk-js-dist/901_clerk.headless.browser_540199_5.127.2.js +1 -0
  28. package/client/clerk-js-dist/apiKeys_clerk.browser_0cc2cc_5.127.2.js +1 -0
  29. package/client/clerk-js-dist/apiKeys_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  30. package/client/clerk-js-dist/base-account-sdk_clerk.browser_0cc2cc_5.127.2.js +42 -0
  31. package/client/clerk-js-dist/base-account-sdk_clerk.headless.browser_540199_5.127.2.js +42 -0
  32. package/client/clerk-js-dist/base-account-sdk_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  33. package/client/clerk-js-dist/blankcaptcha_clerk.browser_0cc2cc_5.127.2.js +1 -0
  34. package/client/clerk-js-dist/blankcaptcha_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  35. package/client/clerk-js-dist/checkout_clerk.browser_0cc2cc_5.127.2.js +9 -0
  36. package/client/clerk-js-dist/checkout_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  37. package/client/clerk-js-dist/clerk.browser.js +19 -0
  38. package/client/clerk-js-dist/clerk.headless.browser.js +19 -0
  39. package/client/clerk-js-dist/clerk.headless.js +19 -0
  40. package/client/clerk-js-dist/clerk.js +809 -0
  41. package/client/clerk-js-dist/clerk.legacy.browser.js +1 -0
  42. package/client/clerk-js-dist/clerk.no-rhc.js +766 -0
  43. package/client/clerk-js-dist/coinbase-wallet-sdk_clerk.browser_0cc2cc_5.127.2.js +3 -0
  44. package/client/clerk-js-dist/coinbase-wallet-sdk_clerk.headless.browser_540199_5.127.2.js +3 -0
  45. package/client/clerk-js-dist/coinbase-wallet-sdk_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  46. package/client/clerk-js-dist/cookieSuffix_clerk.browser_0cc2cc_5.127.2.js +1 -0
  47. package/client/clerk-js-dist/cookieSuffix_clerk.headless.browser_540199_5.127.2.js +1 -0
  48. package/client/clerk-js-dist/cookieSuffix_clerk.headless_b86dff_5.127.2.js +1 -0
  49. package/client/clerk-js-dist/cookieSuffix_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  50. package/client/clerk-js-dist/copy-api-key-modal_clerk.browser_0cc2cc_5.127.2.js +1 -0
  51. package/client/clerk-js-dist/copy-api-key-modal_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  52. package/client/clerk-js-dist/createorganization_clerk.browser_0cc2cc_5.127.2.js +1 -0
  53. package/client/clerk-js-dist/createorganization_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  54. package/client/clerk-js-dist/enableOrganizationsPrompt_clerk.browser_0cc2cc_5.127.2.js +247 -0
  55. package/client/clerk-js-dist/enableOrganizationsPrompt_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  56. package/client/clerk-js-dist/framework_clerk.browser_0cc2cc_5.127.2.js +1 -0
  57. package/client/clerk-js-dist/framework_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  58. package/client/clerk-js-dist/impersonationfab_clerk.browser_0cc2cc_5.127.2.js +1 -0
  59. package/client/clerk-js-dist/impersonationfab_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  60. package/client/clerk-js-dist/keylessPrompt_clerk.browser_0cc2cc_5.127.2.js +228 -0
  61. package/client/clerk-js-dist/keylessPrompt_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  62. package/client/clerk-js-dist/oauthConsent_clerk.browser_0cc2cc_5.127.2.js +1 -0
  63. package/client/clerk-js-dist/oauthConsent_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  64. package/client/clerk-js-dist/onetap_clerk.browser_0cc2cc_5.127.2.js +1 -0
  65. package/client/clerk-js-dist/onetap_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  66. package/client/clerk-js-dist/op-api-keys-page_clerk.browser_0cc2cc_5.127.2.js +1 -0
  67. package/client/clerk-js-dist/op-api-keys-page_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  68. package/client/clerk-js-dist/op-billing-page_clerk.browser_0cc2cc_5.127.2.js +1 -0
  69. package/client/clerk-js-dist/op-billing-page_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  70. package/client/clerk-js-dist/op-plans-page_clerk.browser_0cc2cc_5.127.2.js +1 -0
  71. package/client/clerk-js-dist/op-plans-page_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  72. package/client/clerk-js-dist/organizationlist_clerk.browser_0cc2cc_5.127.2.js +1 -0
  73. package/client/clerk-js-dist/organizationlist_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  74. package/client/clerk-js-dist/organizationprofile_clerk.browser_0cc2cc_5.127.2.js +1 -0
  75. package/client/clerk-js-dist/organizationprofile_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  76. package/client/clerk-js-dist/organizationswitcher_clerk.browser_0cc2cc_5.127.2.js +1 -0
  77. package/client/clerk-js-dist/organizationswitcher_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  78. package/client/clerk-js-dist/payment-attempt-page_clerk.browser_0cc2cc_5.127.2.js +1 -0
  79. package/client/clerk-js-dist/payment-attempt-page_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  80. package/client/clerk-js-dist/planDetails_clerk.browser_0cc2cc_5.127.2.js +1 -0
  81. package/client/clerk-js-dist/planDetails_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  82. package/client/clerk-js-dist/prefetchorganizationlist_clerk.browser_0cc2cc_5.127.2.js +1 -0
  83. package/client/clerk-js-dist/prefetchorganizationlist_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  84. package/client/clerk-js-dist/pricingTable_clerk.browser_0cc2cc_5.127.2.js +1 -0
  85. package/client/clerk-js-dist/pricingTable_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  86. package/client/clerk-js-dist/query-core-vendors_clerk.browser_0cc2cc_5.127.2.js +1 -0
  87. package/client/clerk-js-dist/query-core-vendors_clerk.headless.browser_540199_5.127.2.js +1 -0
  88. package/client/clerk-js-dist/query-core-vendors_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  89. package/client/clerk-js-dist/revoke-api-key-modal_clerk.browser_0cc2cc_5.127.2.js +1 -0
  90. package/client/clerk-js-dist/revoke-api-key-modal_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  91. package/client/clerk-js-dist/sessionTasks_clerk.browser_0cc2cc_5.127.2.js +1 -0
  92. package/client/clerk-js-dist/sessionTasks_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  93. package/client/clerk-js-dist/signin_clerk.browser_0cc2cc_5.127.2.js +1 -0
  94. package/client/clerk-js-dist/signin_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  95. package/client/clerk-js-dist/signup_clerk.browser_0cc2cc_5.127.2.js +1 -0
  96. package/client/clerk-js-dist/signup_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  97. package/client/clerk-js-dist/statement-page_clerk.browser_0cc2cc_5.127.2.js +1 -0
  98. package/client/clerk-js-dist/statement-page_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  99. package/client/clerk-js-dist/stripe-vendors_clerk.browser_0cc2cc_5.127.2.js +1 -0
  100. package/client/clerk-js-dist/stripe-vendors_clerk.headless.browser_540199_5.127.2.js +1 -0
  101. package/client/clerk-js-dist/stripe-vendors_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  102. package/client/clerk-js-dist/subscriptionDetails_clerk.browser_0cc2cc_5.127.2.js +1 -0
  103. package/client/clerk-js-dist/subscriptionDetails_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  104. package/client/clerk-js-dist/taskChooseOrganization_clerk.browser_0cc2cc_5.127.2.js +1 -0
  105. package/client/clerk-js-dist/taskChooseOrganization_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  106. package/client/clerk-js-dist/taskResetPassword_clerk.browser_0cc2cc_5.127.2.js +1 -0
  107. package/client/clerk-js-dist/taskResetPassword_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  108. package/client/clerk-js-dist/taskSetupMFA_clerk.browser_0cc2cc_5.127.2.js +4 -0
  109. package/client/clerk-js-dist/taskSetupMFA_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  110. package/client/clerk-js-dist/ui-common_clerk.browser_0cc2cc_5.127.2.js +124 -0
  111. package/client/clerk-js-dist/ui-common_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  112. package/client/clerk-js-dist/up-api-keys-page_clerk.browser_0cc2cc_5.127.2.js +1 -0
  113. package/client/clerk-js-dist/up-api-keys-page_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  114. package/client/clerk-js-dist/up-billing-page_clerk.browser_0cc2cc_5.127.2.js +1 -0
  115. package/client/clerk-js-dist/up-billing-page_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  116. package/client/clerk-js-dist/up-plans-page_clerk.browser_0cc2cc_5.127.2.js +1 -0
  117. package/client/clerk-js-dist/up-plans-page_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  118. package/client/clerk-js-dist/useravatar_clerk.browser_0cc2cc_5.127.2.js +1 -0
  119. package/client/clerk-js-dist/useravatar_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  120. package/client/clerk-js-dist/userbutton_clerk.browser_0cc2cc_5.127.2.js +1 -0
  121. package/client/clerk-js-dist/userbutton_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  122. package/client/clerk-js-dist/userprofile_clerk.browser_0cc2cc_5.127.2.js +4 -0
  123. package/client/clerk-js-dist/userprofile_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  124. package/client/clerk-js-dist/userverification_clerk.browser_0cc2cc_5.127.2.js +1 -0
  125. package/client/clerk-js-dist/userverification_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  126. package/client/clerk-js-dist/vendors_clerk.browser_0cc2cc_5.127.2.js +20 -0
  127. package/client/clerk-js-dist/vendors_clerk.headless.browser_540199_5.127.2.js +1 -0
  128. package/client/clerk-js-dist/vendors_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  129. package/client/clerk-js-dist/waitlist_clerk.browser_0cc2cc_5.127.2.js +1 -0
  130. package/client/clerk-js-dist/waitlist_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  131. package/client/clerk-js-dist/web3-solana-wallet-buttons_clerk.browser_0cc2cc_5.127.2.js +143 -0
  132. package/client/clerk-js-dist/web3-solana-wallet-buttons_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  133. package/client/clerk-js-dist/zxcvbn-common_clerk.browser_0cc2cc_5.127.2.js +1 -0
  134. package/client/clerk-js-dist/zxcvbn-common_clerk.headless.browser_540199_5.127.2.js +1 -0
  135. package/client/clerk-js-dist/zxcvbn-common_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  136. package/client/clerk-js-dist/zxcvbn-ts-core_clerk.browser_0cc2cc_5.127.2.js +2 -0
  137. package/client/clerk-js-dist/zxcvbn-ts-core_clerk.headless.browser_540199_5.127.2.js +2 -0
  138. package/client/clerk-js-dist/zxcvbn-ts-core_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  139. package/client/clerk-mirror.css +72 -0
  140. package/client/clerk-mirror.tsx +204 -0
  141. package/client/clerk.browser.js +19 -0
  142. package/dist/client/clerk-js-dist/167_clerk.headless_b86dff_5.127.2.js +1 -0
  143. package/dist/client/clerk-js-dist/2172_clerk.browser_0cc2cc_5.127.2.js +1 -0
  144. package/dist/client/clerk-js-dist/2172_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  145. package/dist/client/clerk-js-dist/368_clerk.headless_b86dff_5.127.2.js +3 -0
  146. package/dist/client/clerk-js-dist/377_clerk.headless_b86dff_5.127.2.js +1 -0
  147. package/dist/client/clerk-js-dist/4170_clerk.browser_0cc2cc_5.127.2.js +1 -0
  148. package/dist/client/clerk-js-dist/4170_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  149. package/dist/client/clerk-js-dist/437_clerk.headless_b86dff_5.127.2.js +1 -0
  150. package/dist/client/clerk-js-dist/5192_clerk.browser_0cc2cc_5.127.2.js +1 -0
  151. package/dist/client/clerk-js-dist/5192_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  152. package/dist/client/clerk-js-dist/553_clerk.headless_b86dff_5.127.2.js +1 -0
  153. package/dist/client/clerk-js-dist/5645_clerk.browser_0cc2cc_5.127.2.js +1 -0
  154. package/dist/client/clerk-js-dist/5645_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  155. package/dist/client/clerk-js-dist/623_clerk.headless_b86dff_5.127.2.js +42 -0
  156. package/dist/client/clerk-js-dist/7067_clerk.browser_0cc2cc_5.127.2.js +1 -0
  157. package/dist/client/clerk-js-dist/7067_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  158. package/dist/client/clerk-js-dist/7502_clerk.browser_0cc2cc_5.127.2.js +1 -0
  159. package/dist/client/clerk-js-dist/7502_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  160. package/dist/client/clerk-js-dist/785_clerk.headless.browser_540199_5.127.2.js +1 -0
  161. package/dist/client/clerk-js-dist/785_clerk.headless_b86dff_5.127.2.js +1 -0
  162. package/dist/client/clerk-js-dist/8043_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  163. package/dist/client/clerk-js-dist/825_clerk.headless.browser_540199_5.127.2.js +1 -0
  164. package/dist/client/clerk-js-dist/825_clerk.headless_b86dff_5.127.2.js +1 -0
  165. package/dist/client/clerk-js-dist/890_clerk.headless_b86dff_5.127.2.js +2 -0
  166. package/dist/client/clerk-js-dist/901_clerk.headless.browser_540199_5.127.2.js +1 -0
  167. package/dist/client/clerk-js-dist/apiKeys_clerk.browser_0cc2cc_5.127.2.js +1 -0
  168. package/dist/client/clerk-js-dist/apiKeys_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  169. package/dist/client/clerk-js-dist/base-account-sdk_clerk.browser_0cc2cc_5.127.2.js +42 -0
  170. package/dist/client/clerk-js-dist/base-account-sdk_clerk.headless.browser_540199_5.127.2.js +42 -0
  171. package/dist/client/clerk-js-dist/base-account-sdk_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  172. package/dist/client/clerk-js-dist/blankcaptcha_clerk.browser_0cc2cc_5.127.2.js +1 -0
  173. package/dist/client/clerk-js-dist/blankcaptcha_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  174. package/dist/client/clerk-js-dist/checkout_clerk.browser_0cc2cc_5.127.2.js +9 -0
  175. package/dist/client/clerk-js-dist/checkout_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  176. package/dist/client/clerk-js-dist/clerk.browser.js +19 -0
  177. package/dist/client/clerk-js-dist/clerk.headless.browser.js +19 -0
  178. package/dist/client/clerk-js-dist/clerk.headless.js +19 -0
  179. package/dist/client/clerk-js-dist/clerk.js +809 -0
  180. package/dist/client/clerk-js-dist/clerk.legacy.browser.js +1 -0
  181. package/dist/client/clerk-js-dist/clerk.no-rhc.js +766 -0
  182. package/dist/client/clerk-js-dist/coinbase-wallet-sdk_clerk.browser_0cc2cc_5.127.2.js +3 -0
  183. package/dist/client/clerk-js-dist/coinbase-wallet-sdk_clerk.headless.browser_540199_5.127.2.js +3 -0
  184. package/dist/client/clerk-js-dist/coinbase-wallet-sdk_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  185. package/dist/client/clerk-js-dist/cookieSuffix_clerk.browser_0cc2cc_5.127.2.js +1 -0
  186. package/dist/client/clerk-js-dist/cookieSuffix_clerk.headless.browser_540199_5.127.2.js +1 -0
  187. package/dist/client/clerk-js-dist/cookieSuffix_clerk.headless_b86dff_5.127.2.js +1 -0
  188. package/dist/client/clerk-js-dist/cookieSuffix_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  189. package/dist/client/clerk-js-dist/copy-api-key-modal_clerk.browser_0cc2cc_5.127.2.js +1 -0
  190. package/dist/client/clerk-js-dist/copy-api-key-modal_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  191. package/dist/client/clerk-js-dist/createorganization_clerk.browser_0cc2cc_5.127.2.js +1 -0
  192. package/dist/client/clerk-js-dist/createorganization_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  193. package/dist/client/clerk-js-dist/enableOrganizationsPrompt_clerk.browser_0cc2cc_5.127.2.js +247 -0
  194. package/dist/client/clerk-js-dist/enableOrganizationsPrompt_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  195. package/dist/client/clerk-js-dist/framework_clerk.browser_0cc2cc_5.127.2.js +1 -0
  196. package/dist/client/clerk-js-dist/framework_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  197. package/dist/client/clerk-js-dist/impersonationfab_clerk.browser_0cc2cc_5.127.2.js +1 -0
  198. package/dist/client/clerk-js-dist/impersonationfab_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  199. package/dist/client/clerk-js-dist/keylessPrompt_clerk.browser_0cc2cc_5.127.2.js +228 -0
  200. package/dist/client/clerk-js-dist/keylessPrompt_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  201. package/dist/client/clerk-js-dist/oauthConsent_clerk.browser_0cc2cc_5.127.2.js +1 -0
  202. package/dist/client/clerk-js-dist/oauthConsent_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  203. package/dist/client/clerk-js-dist/onetap_clerk.browser_0cc2cc_5.127.2.js +1 -0
  204. package/dist/client/clerk-js-dist/onetap_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  205. package/dist/client/clerk-js-dist/op-api-keys-page_clerk.browser_0cc2cc_5.127.2.js +1 -0
  206. package/dist/client/clerk-js-dist/op-api-keys-page_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  207. package/dist/client/clerk-js-dist/op-billing-page_clerk.browser_0cc2cc_5.127.2.js +1 -0
  208. package/dist/client/clerk-js-dist/op-billing-page_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  209. package/dist/client/clerk-js-dist/op-plans-page_clerk.browser_0cc2cc_5.127.2.js +1 -0
  210. package/dist/client/clerk-js-dist/op-plans-page_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  211. package/dist/client/clerk-js-dist/organizationlist_clerk.browser_0cc2cc_5.127.2.js +1 -0
  212. package/dist/client/clerk-js-dist/organizationlist_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  213. package/dist/client/clerk-js-dist/organizationprofile_clerk.browser_0cc2cc_5.127.2.js +1 -0
  214. package/dist/client/clerk-js-dist/organizationprofile_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  215. package/dist/client/clerk-js-dist/organizationswitcher_clerk.browser_0cc2cc_5.127.2.js +1 -0
  216. package/dist/client/clerk-js-dist/organizationswitcher_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  217. package/dist/client/clerk-js-dist/payment-attempt-page_clerk.browser_0cc2cc_5.127.2.js +1 -0
  218. package/dist/client/clerk-js-dist/payment-attempt-page_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  219. package/dist/client/clerk-js-dist/planDetails_clerk.browser_0cc2cc_5.127.2.js +1 -0
  220. package/dist/client/clerk-js-dist/planDetails_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  221. package/dist/client/clerk-js-dist/prefetchorganizationlist_clerk.browser_0cc2cc_5.127.2.js +1 -0
  222. package/dist/client/clerk-js-dist/prefetchorganizationlist_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  223. package/dist/client/clerk-js-dist/pricingTable_clerk.browser_0cc2cc_5.127.2.js +1 -0
  224. package/dist/client/clerk-js-dist/pricingTable_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  225. package/dist/client/clerk-js-dist/query-core-vendors_clerk.browser_0cc2cc_5.127.2.js +1 -0
  226. package/dist/client/clerk-js-dist/query-core-vendors_clerk.headless.browser_540199_5.127.2.js +1 -0
  227. package/dist/client/clerk-js-dist/query-core-vendors_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  228. package/dist/client/clerk-js-dist/revoke-api-key-modal_clerk.browser_0cc2cc_5.127.2.js +1 -0
  229. package/dist/client/clerk-js-dist/revoke-api-key-modal_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  230. package/dist/client/clerk-js-dist/sessionTasks_clerk.browser_0cc2cc_5.127.2.js +1 -0
  231. package/dist/client/clerk-js-dist/sessionTasks_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  232. package/dist/client/clerk-js-dist/signin_clerk.browser_0cc2cc_5.127.2.js +1 -0
  233. package/dist/client/clerk-js-dist/signin_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  234. package/dist/client/clerk-js-dist/signup_clerk.browser_0cc2cc_5.127.2.js +1 -0
  235. package/dist/client/clerk-js-dist/signup_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  236. package/dist/client/clerk-js-dist/statement-page_clerk.browser_0cc2cc_5.127.2.js +1 -0
  237. package/dist/client/clerk-js-dist/statement-page_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  238. package/dist/client/clerk-js-dist/stripe-vendors_clerk.browser_0cc2cc_5.127.2.js +1 -0
  239. package/dist/client/clerk-js-dist/stripe-vendors_clerk.headless.browser_540199_5.127.2.js +1 -0
  240. package/dist/client/clerk-js-dist/stripe-vendors_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  241. package/dist/client/clerk-js-dist/subscriptionDetails_clerk.browser_0cc2cc_5.127.2.js +1 -0
  242. package/dist/client/clerk-js-dist/subscriptionDetails_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  243. package/dist/client/clerk-js-dist/taskChooseOrganization_clerk.browser_0cc2cc_5.127.2.js +1 -0
  244. package/dist/client/clerk-js-dist/taskChooseOrganization_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  245. package/dist/client/clerk-js-dist/taskResetPassword_clerk.browser_0cc2cc_5.127.2.js +1 -0
  246. package/dist/client/clerk-js-dist/taskResetPassword_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  247. package/dist/client/clerk-js-dist/taskSetupMFA_clerk.browser_0cc2cc_5.127.2.js +4 -0
  248. package/dist/client/clerk-js-dist/taskSetupMFA_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  249. package/dist/client/clerk-js-dist/ui-common_clerk.browser_0cc2cc_5.127.2.js +124 -0
  250. package/dist/client/clerk-js-dist/ui-common_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  251. package/dist/client/clerk-js-dist/up-api-keys-page_clerk.browser_0cc2cc_5.127.2.js +1 -0
  252. package/dist/client/clerk-js-dist/up-api-keys-page_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  253. package/dist/client/clerk-js-dist/up-billing-page_clerk.browser_0cc2cc_5.127.2.js +1 -0
  254. package/dist/client/clerk-js-dist/up-billing-page_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  255. package/dist/client/clerk-js-dist/up-plans-page_clerk.browser_0cc2cc_5.127.2.js +1 -0
  256. package/dist/client/clerk-js-dist/up-plans-page_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  257. package/dist/client/clerk-js-dist/useravatar_clerk.browser_0cc2cc_5.127.2.js +1 -0
  258. package/dist/client/clerk-js-dist/useravatar_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  259. package/dist/client/clerk-js-dist/userbutton_clerk.browser_0cc2cc_5.127.2.js +1 -0
  260. package/dist/client/clerk-js-dist/userbutton_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  261. package/dist/client/clerk-js-dist/userprofile_clerk.browser_0cc2cc_5.127.2.js +4 -0
  262. package/dist/client/clerk-js-dist/userprofile_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  263. package/dist/client/clerk-js-dist/userverification_clerk.browser_0cc2cc_5.127.2.js +1 -0
  264. package/dist/client/clerk-js-dist/userverification_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  265. package/dist/client/clerk-js-dist/vendors_clerk.browser_0cc2cc_5.127.2.js +20 -0
  266. package/dist/client/clerk-js-dist/vendors_clerk.headless.browser_540199_5.127.2.js +1 -0
  267. package/dist/client/clerk-js-dist/vendors_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  268. package/dist/client/clerk-js-dist/waitlist_clerk.browser_0cc2cc_5.127.2.js +1 -0
  269. package/dist/client/clerk-js-dist/waitlist_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  270. package/dist/client/clerk-js-dist/web3-solana-wallet-buttons_clerk.browser_0cc2cc_5.127.2.js +143 -0
  271. package/dist/client/clerk-js-dist/web3-solana-wallet-buttons_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  272. package/dist/client/clerk-js-dist/zxcvbn-common_clerk.browser_0cc2cc_5.127.2.js +1 -0
  273. package/dist/client/clerk-js-dist/zxcvbn-common_clerk.headless.browser_540199_5.127.2.js +1 -0
  274. package/dist/client/clerk-js-dist/zxcvbn-common_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  275. package/dist/client/clerk-js-dist/zxcvbn-ts-core_clerk.browser_0cc2cc_5.127.2.js +2 -0
  276. package/dist/client/clerk-js-dist/zxcvbn-ts-core_clerk.headless.browser_540199_5.127.2.js +2 -0
  277. package/dist/client/clerk-js-dist/zxcvbn-ts-core_clerk.legacy.browser_6070dd_5.127.2.js +1 -0
  278. package/dist/client/clerk-mirror.bundle.js +321 -0
  279. package/dist/client/clerk-mirror.css +72 -0
  280. package/dist/client/clerk-mirror.d.ts +33 -0
  281. package/dist/client/clerk-mirror.js +143 -0
  282. package/dist/client/clerk-mirror.tsx +204 -0
  283. package/dist/client/clerk.browser.js +19 -0
  284. package/dist/src/clerk-capabilities.d.ts +4 -0
  285. package/dist/src/clerk-capabilities.js +1132 -0
  286. package/dist/src/clerk-conformance.d.ts +32 -0
  287. package/dist/src/clerk-conformance.js +158 -0
  288. package/dist/src/clerk-connector.d.ts +170 -0
  289. package/dist/src/clerk-connector.js +223 -0
  290. package/dist/src/clerk-events.d.ts +55 -0
  291. package/dist/src/clerk-events.js +146 -0
  292. package/dist/src/clerk-fapi.d.ts +19 -0
  293. package/dist/src/clerk-fapi.js +665 -0
  294. package/dist/src/clerk-jwt.d.ts +109 -0
  295. package/dist/src/clerk-jwt.js +230 -0
  296. package/dist/src/clerk-mirror-ui.d.ts +34 -0
  297. package/dist/src/clerk-mirror-ui.js +160 -0
  298. package/dist/src/clerk-server.d.ts +35 -0
  299. package/dist/src/clerk-server.js +172 -0
  300. package/dist/src/clerk-twin.d.ts +28 -0
  301. package/dist/src/clerk-twin.js +1658 -0
  302. package/dist/src/cli.d.ts +2 -0
  303. package/dist/src/cli.js +35 -0
  304. package/dist/src/index.d.ts +12 -0
  305. package/dist/src/index.js +56 -0
  306. package/dist/test-fixtures/clerk-openapi-operations.SOURCE.md +16 -0
  307. package/dist/test-fixtures/clerk-openapi-operations.json +1989 -0
  308. package/package.json +76 -0
  309. package/src/clerk-capabilities.ts +1246 -0
  310. package/src/clerk-conformance.ts +172 -0
  311. package/src/clerk-connector.ts +268 -0
  312. package/src/clerk-events.ts +166 -0
  313. package/src/clerk-fapi.ts +673 -0
  314. package/src/clerk-jwt.ts +256 -0
  315. package/src/clerk-mirror-ui.ts +165 -0
  316. package/src/clerk-server.ts +175 -0
  317. package/src/clerk-twin.ts +1580 -0
  318. package/src/cli.ts +33 -0
  319. package/src/index.ts +94 -0
  320. package/test-fixtures/clerk-openapi-operations.SOURCE.md +16 -0
  321. package/test-fixtures/clerk-openapi-operations.json +1989 -0
@@ -0,0 +1,673 @@
1
+ // Clerk FRONTEND API twin — the clerk.<domain> surface the real `@clerk/clerk-js`
2
+ // browser bundle talks to. A sibling branch on the SAME seams as the Backend API twin:
3
+ // every read is a projection of the same row store and every mutation delegates to
4
+ // `handleClerkTwinRequest` (user lookup, scrypt password verify, session create/revoke,
5
+ // token mint), so the two surfaces can never disagree about the world.
6
+ //
7
+ // Scope (first slice): environment, client, password sign-in, session tokens, sign-out.
8
+ // Social/OAuth strategies, sign-ups, and MFA are absent from the environment document —
9
+ // the client never offers what the twin does not model.
10
+ //
11
+ // Personas are WORLD STATE: users exist in the store (seeded from a personas file via
12
+ // `--personas`, or created through the Backend API) and sign-in is a protocol against
13
+ // them — nothing is pre-signed-in.
14
+ import { applyTwinWrite, projectResources, worldNow } from '@volter/world-core';
15
+ import { handleClerkTwinRequest, stableHex } from './clerk-twin.ts';
16
+ import { buildJwks, verifyJwtWithJwks } from './clerk-jwt.ts';
17
+
18
+ export type ClerkFapiRequest = {
19
+ method: string;
20
+ path: string; // pathname only (no query)
21
+ query?: Record<string, string>;
22
+ body?: string; // form-encoded or JSON, as the client sent it
23
+ cookies?: Record<string, string>;
24
+ occurredAt?: string;
25
+ root?: string;
26
+ /** The browser request's Origin header, when it sent one — becomes a session token's `azp`. */
27
+ origin?: string;
28
+ };
29
+ export type ClerkFapiResponse = { status: number; body: unknown; setCookies?: string[]; html?: string; redirect?: string };
30
+
31
+ const CLIENT_COOKIE = '__client';
32
+
33
+ // FAPI error envelope (same shape family as the Backend API).
34
+ function ferr(message: string, status = 400, code = 'request_invalid', meta?: Record<string, unknown>): ClerkFapiResponse {
35
+ return { status, body: { errors: [{ code, message, long_message: message, meta: meta ?? {} }], clerk_trace_id: 'trace_twin_fapi' } };
36
+ }
37
+
38
+ // clerk-js sends application/x-www-form-urlencoded; tooling may send JSON. Accept both.
39
+ function parseBody(body?: string): Record<string, string> {
40
+ if (!body) return {};
41
+ const trimmed = body.trim();
42
+ if (trimmed.startsWith('{')) {
43
+ try {
44
+ const parsed = JSON.parse(trimmed) as Record<string, unknown>;
45
+ return Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
46
+ } catch {
47
+ return {};
48
+ }
49
+ }
50
+ return Object.fromEntries(new URLSearchParams(trimmed));
51
+ }
52
+
53
+ async function backend(method: string, path: string, body: Record<string, unknown> | undefined, req: ClerkFapiRequest): Promise<{ status: number; body: unknown }> {
54
+ return handleClerkTwinRequest({
55
+ method,
56
+ path,
57
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
58
+ ...(req.occurredAt !== undefined ? { occurredAt: req.occurredAt } : {}),
59
+ ...(req.root !== undefined ? { root: req.root } : {}),
60
+ ...(req.origin !== undefined ? { origin: req.origin } : {}),
61
+ });
62
+ }
63
+
64
+ const record = (v: unknown): Record<string, unknown> => v as Record<string, unknown>;
65
+
66
+ // ── the environment document ────────────────────────────────────────────────────
67
+ // The client's boot call. Password is the only first factor; no social providers, no
68
+ // sign-up flow beyond what the store models, no captcha, no MFA.
69
+ function environmentDoc(now: number): Record<string, unknown> {
70
+ return {
71
+ object: 'environment',
72
+ id: 'env_twin',
73
+ auth_config: {
74
+ object: 'auth_config',
75
+ id: 'aac_twin',
76
+ single_session_mode: true,
77
+ enhanced_email_deliverability: false,
78
+ test_mode: true,
79
+ demo: false,
80
+ cookieless_dev: false,
81
+ url_based_session_syncing: false,
82
+ claimed_at: now,
83
+ reverification: false,
84
+ },
85
+ display_config: {
86
+ object: 'display_config',
87
+ id: 'display_config_twin',
88
+ instance_environment_type: 'development',
89
+ application_name: 'twin',
90
+ branded: false,
91
+ preferred_sign_in_strategy: 'password',
92
+ logo_image_url: '',
93
+ favicon_image_url: '',
94
+ home_url: 'http://localhost',
95
+ sign_in_url: 'http://localhost/sign-in',
96
+ sign_up_url: 'http://localhost/sign-up',
97
+ after_sign_in_url: 'http://localhost',
98
+ after_sign_up_url: 'http://localhost',
99
+ after_sign_out_one_url: 'http://localhost',
100
+ after_sign_out_all_url: 'http://localhost',
101
+ after_switch_session_url: 'http://localhost',
102
+ captcha_public_key: null,
103
+ captcha_widget_type: null,
104
+ captcha_provider: 'turnstile',
105
+ captcha_public_key_invisible: null,
106
+ support_email: null,
107
+ clerk_js_version: '5',
108
+ },
109
+ user_settings: {
110
+ attributes: {
111
+ email_address: { enabled: true, required: true, used_for_first_factor: true, first_factors: ['email_code'], used_for_second_factor: false, second_factors: [], verifications: ['email_code'], verify_at_sign_up: false },
112
+ phone_number: { enabled: false, required: false, used_for_first_factor: false, first_factors: [], used_for_second_factor: false, second_factors: [], verifications: [], verify_at_sign_up: false },
113
+ username: { enabled: false, required: false, used_for_first_factor: false, first_factors: [], used_for_second_factor: false, second_factors: [], verifications: [], verify_at_sign_up: false },
114
+ first_name: { enabled: true, required: false, used_for_first_factor: false, first_factors: [], used_for_second_factor: false, second_factors: [], verifications: [], verify_at_sign_up: false },
115
+ last_name: { enabled: true, required: false, used_for_first_factor: false, first_factors: [], used_for_second_factor: false, second_factors: [], verifications: [], verify_at_sign_up: false },
116
+ password: { enabled: true, required: true, used_for_first_factor: true, first_factors: ['password'], used_for_second_factor: false, second_factors: [], verifications: [], verify_at_sign_up: false },
117
+ web3_wallet: { enabled: false, required: false, used_for_first_factor: false, first_factors: [], used_for_second_factor: false, second_factors: [], verifications: [], verify_at_sign_up: false },
118
+ authenticator_app: { enabled: false, required: false, used_for_first_factor: false, first_factors: [], used_for_second_factor: false, second_factors: [], verifications: [], verify_at_sign_up: false },
119
+ backup_code: { enabled: false, required: false, used_for_first_factor: false, first_factors: [], used_for_second_factor: false, second_factors: [], verifications: [], verify_at_sign_up: false },
120
+ passkey: { enabled: false, required: false, used_for_first_factor: false, first_factors: [], used_for_second_factor: false, second_factors: [], verifications: [], verify_at_sign_up: false },
121
+ },
122
+ sign_in: { second_factor: { required: false } },
123
+ sign_up: { captcha_enabled: false, captcha_widget_type: null, custom_action_required: false, progressive: true, mode: 'public', legal_consent_enabled: false },
124
+ social: {
125
+ oauth_apple: { enabled: true, required: false, authenticatable: true, block_email_subaddresses: false, strategy: 'oauth_apple', not_selectable: false, deprecated: false, name: 'Apple', logo_url: '' },
126
+ oauth_google: { enabled: true, required: false, authenticatable: true, block_email_subaddresses: false, strategy: 'oauth_google', not_selectable: false, deprecated: false, name: 'Google', logo_url: '' },
127
+ },
128
+ actions: { delete_self: false, create_organization: false },
129
+ attack_protection: { user_lockout: { enabled: false }, pii: { enabled: false }, email_link: { require_same_client: false } },
130
+ passkey_settings: { allow_autofill: false, show_sign_in_button: false },
131
+ password_settings: {
132
+ allowed_special_characters: '', disable_hibp: true, min_length: 1, max_length: 0,
133
+ require_special_char: false, require_numbers: false, require_uppercase: false,
134
+ require_lowercase: false, show_zxcvbn: false, min_zxcvbn_strength: 0, enforce_hibp_on_sign_in: false,
135
+ },
136
+ saml: { enabled: false },
137
+ enterprise_sso: { enabled: false },
138
+ },
139
+ organization_settings: {
140
+ enabled: false, max_allowed_memberships: 0, force_organization_selection: false,
141
+ actions: { admin_delete: false }, domains: { enabled: false, enrollment_modes: [], default_role: null },
142
+ creator_role: 'org:admin',
143
+ },
144
+ fraud_settings: { native: { device_attestation_mode: 'disabled' } },
145
+ commerce_settings: { billing: { enabled: false, stripe_publishable_key: null, has_paid_user_plans: false, has_paid_org_plans: false, organization: { enabled: false }, user: { enabled: false } } },
146
+ api_keys_settings: { enabled: false },
147
+ maintenance_mode: false,
148
+ };
149
+ }
150
+
151
+ // ── FAPI views (the client-facing shapes of the same rows) ──────────────────────
152
+ function fapiSession(session: Record<string, unknown>, user: Record<string, unknown> | null, lastTokenJwt?: string | null): Record<string, unknown> {
153
+ return {
154
+ object: 'session',
155
+ id: session.id,
156
+ status: session.status,
157
+ expire_at: session.expire_at,
158
+ abandon_at: session.abandon_at,
159
+ last_active_at: session.last_active_at,
160
+ last_active_organization_id: session.last_active_organization_id ?? null,
161
+ actor: session.actor ?? null,
162
+ tasks: [],
163
+ user,
164
+ public_user_data: user === null ? null : {
165
+ first_name: user.first_name ?? null,
166
+ last_name: user.last_name ?? null,
167
+ image_url: user.image_url ?? '',
168
+ has_image: false,
169
+ identifier: primaryIdentifier(user),
170
+ profile_image_url: user.profile_image_url ?? '',
171
+ },
172
+ created_at: session.created_at,
173
+ updated_at: session.updated_at,
174
+ // The envelope carries a REAL token per session — clerk-js derives sessionClaims from
175
+ // it on first render; a null here is an "Invalid state" throw in clerk-react.
176
+ last_active_token: lastTokenJwt === undefined || lastTokenJwt === null ? null : { object: 'token', jwt: lastTokenJwt },
177
+ factor_verification_age: null,
178
+ };
179
+ }
180
+
181
+ function primaryIdentifier(user: Record<string, unknown>): string {
182
+ const emails = Array.isArray(user.email_addresses) ? user.email_addresses as Array<Record<string, unknown>> : [];
183
+ return String(emails[0]?.email_address ?? user.id);
184
+ }
185
+
186
+ // The Backend user view is already snake_case Clerk shape; FAPI adds organization_memberships.
187
+ function fapiUser(user: Record<string, unknown>): Record<string, unknown> {
188
+ return { ...user, organization_memberships: [] };
189
+ }
190
+
191
+ // ── client state (a real row: the browser's client with its sessions) ───────────
192
+ async function getClientRow(clientId: string, req: ClerkFapiRequest): Promise<Record<string, unknown> | undefined> {
193
+ const all = await backend('GET', '/v1/sessions', undefined, req);
194
+ if (all.status !== 200 || !Array.isArray(all.body)) return undefined;
195
+ const sessions = (all.body as Array<Record<string, unknown>>).filter((s) => s.client_id === clientId && s.status === 'active');
196
+ return { id: clientId, sessions };
197
+ }
198
+
199
+ // PROTOCOL 2: a sign-in or sign-up attempt is a protocol scratchpad, and the browser's client remembers the one it is
200
+ // mid-way through — both live in the TREE (subjects `sign_in_attempt`, `sign_up_attempt`, `fapi_client`), never in a
201
+ // module-level map: a served twin holds no truth outside its root, and a branch sees its own attempts.
202
+ const SERVICE = 'clerk';
203
+ /** The instant this request happens at: the world's, never the process's. A served body that reads the wall
204
+ * clock is not a pure function of (request, stored state) — two replays of one world would differ. */
205
+ const atMs = (req: ClerkFapiRequest): number => {
206
+ const parsed = req.occurredAt ? Date.parse(req.occurredAt) : NaN;
207
+ return Number.isNaN(parsed) ? Date.parse(worldNow()) : parsed;
208
+ };
209
+ type Stored = Record<string, unknown>;
210
+ function readSubject(type: string, id: string, root?: string): Stored | undefined {
211
+ const r = projectResources(SERVICE, root).find((x) => x.type === type && x.id === id) as (Stored & { type?: string; updatedAt?: string }) | undefined;
212
+ if (r === undefined) return undefined;
213
+ const { type: _t, updatedAt: _u, id: _i, ...fields } = r; return fields;
214
+ }
215
+ async function writeSubject(type: string, id: string, fields: Stored, req: ClerkFapiRequest): Promise<void> {
216
+ await applyTwinWrite(SERVICE, { operation: `${type}.upsert`, subjectType: type, subjectId: id, fields: { _fapi: true, ...fields }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
217
+ }
218
+ const readSignIn = (id: string, root?: string): SignInAttempt | undefined => (id ? (readSubject('sign_in_attempt', id, root) as SignInAttempt | undefined) : undefined);
219
+ const readSignUp = (id: string, root?: string): SignUpAttempt | undefined => (id ? (readSubject('sign_up_attempt', id, root) as SignUpAttempt | undefined) : undefined);
220
+ const saveSignIn = (a: SignInAttempt, req: ClerkFapiRequest): Promise<void> => writeSubject('sign_in_attempt', a.id, { ...a }, req);
221
+ const saveSignUp = (a: SignUpAttempt, req: ClerkFapiRequest): Promise<void> => writeSubject('sign_up_attempt', a.id, { ...a, fields: { ...a.fields } }, req);
222
+ /** What the browser's client remembers: its current attempts and the social identity it was given per strategy. */
223
+ type ClientMemory = { sign_in_attempt?: string | null; sign_up_attempt?: string | null; identities?: Record<string, { email: string; firstName: string; lastName: string }> };
224
+ const clientMemory = (clientId: string, root?: string): ClientMemory => (readSubject('fapi_client', clientId, root) as ClientMemory | undefined) ?? {};
225
+ const rememberClient = (clientId: string, patch: ClientMemory, req: ClerkFapiRequest): Promise<void> => writeSubject('fapi_client', clientId, { ...clientMemory(clientId, req.root), ...patch }, req);
226
+
227
+ async function clientEnvelope(clientId: string, req: ClerkFapiRequest, signInAttempt?: Record<string, unknown>): Promise<Record<string, unknown>> {
228
+ const at = atMs(req);
229
+ const row = await getClientRow(clientId, req);
230
+ const sessions = row === undefined ? [] : (row.sessions as Array<Record<string, unknown>>);
231
+ const withUsers: Array<Record<string, unknown>> = [];
232
+ for (const s of sessions) {
233
+ const u = await backend('GET', `/v1/users/${encodeURIComponent(String(s.user_id))}`, undefined, req);
234
+ const minted = await backend('POST', `/v1/sessions/${encodeURIComponent(String(s.id))}/tokens`, {}, req);
235
+ withUsers.push(fapiSession(s, u.status === 200 ? fapiUser(record(u.body)) : null, minted.status === 200 ? String(record(minted.body).jwt) : null));
236
+ }
237
+ const last = withUsers.at(-1);
238
+ const memory = clientMemory(clientId, req.root);
239
+ const rememberedSignIn = readSignIn(memory.sign_in_attempt ?? '', req.root);
240
+ const rememberedSignUp = readSignUp(memory.sign_up_attempt ?? '', req.root);
241
+ return {
242
+ object: 'client',
243
+ id: clientId,
244
+ sessions: withUsers,
245
+ sign_in: signInAttempt ?? (rememberedSignIn === undefined ? null : signInView(rememberedSignIn, atMs(req))),
246
+ sign_up: rememberedSignUp === undefined ? null : signUpView(rememberedSignUp, atMs(req)),
247
+ last_active_session_id: last === undefined ? null : last.id,
248
+ cookie_expires_at: null,
249
+ captcha_bypass: true,
250
+ created_at: at,
251
+ updated_at: at,
252
+ };
253
+ }
254
+
255
+ // DETERMINISTIC (R9): a Frontend API id is a stable hash of (type, the world instant, the rows
256
+ // the type already holds) in Clerk's `<prefix>_<24 hex>` shape — never a random UUID, so two
257
+ // identical worlds mint identical clients and attempts.
258
+ function fapiId(prefix: string, type: string, req: ClerkFapiRequest): string {
259
+ const held = projectResources(SERVICE, req.root).filter((x) => x.type === type).length;
260
+ return `${prefix}_${stableHex(`${type}:${new Date(atMs(req)).toISOString()}:${held}`, 24)}`;
261
+ }
262
+ /** Mint the browser's client and record it, so the next browser at the same instant counts it
263
+ * and is minted a different id. */
264
+ async function newClientId(req: ClerkFapiRequest): Promise<string> {
265
+ const id = fapiId('client', 'fapi_client', req);
266
+ await writeSubject('fapi_client', id, {}, req);
267
+ return id;
268
+ }
269
+
270
+ function clientCookie(clientId: string): string {
271
+ // SameSite=None + Secure + Partitioned: the app runs EMBEDDED (a Room Surface iframe on
272
+ // another origin), where a Lax cookie is silently dropped as third-party — which kills
273
+ // exactly the flows that cross a navigation (oauth consent → callback) and spares the
274
+ // in-response ones (ticket). Partitioned keeps it working under third-party phaseout.
275
+ return `${CLIENT_COOKIE}=${clientId}; Path=/; SameSite=None; Secure; Partitioned`;
276
+ }
277
+
278
+ // sign-in attempts live in-memory per client: an attempt is a protocol scratchpad, not
279
+ // world state — the world changes only when the attempt completes (session row created).
280
+ type SignInAttempt = { id: string; userId: string; identifier: string; status: string; createdSessionId: string | null; strategy?: string; redirectUrl?: string; externalIdentity?: { email: string; firstName: string; lastName: string } };
281
+
282
+ // sign-up attempts: the same principle — a scratchpad of collected fields; the world
283
+ // changes only when the attempt completes (a real user row + session, via the Backend).
284
+ type SignUpAttempt = { id: string; fields: Record<string, string>; status: string; createdSessionId: string | null; createdUserId: string | null; strategy?: string; redirectUrl?: string; externalIdentity?: { email: string; firstName: string; lastName: string } };
285
+
286
+ // A social strategy's provider identity, as the fake provider would report it. Stable
287
+ // per attempt so the consent page and the completed user agree.
288
+ const OAUTH_PROVIDERS: Record<string, { name: string; domain: string }> = {
289
+ oauth_apple: { name: 'Apple', domain: 'twin-apple.test' },
290
+ oauth_google: { name: 'Google', domain: 'twin-google.test' },
291
+ };
292
+
293
+ // One browser, one social identity: the same client always resolves to the same provider
294
+ // account — exactly real-provider semantics, so a sign-up creates the member and a later
295
+ // sign-in from the same browser returns them instead of minting a stranger the app has
296
+ // never seen (which dead-ends any post-auth member lookup).
297
+ async function mintExternalIdentity(strategy: string, clientId: string | undefined, req: ClerkFapiRequest): Promise<{ email: string; firstName: string; lastName: string }> {
298
+ const provider = OAUTH_PROVIDERS[strategy] ?? { domain: 'twin-oauth.test', name: 'Provider' };
299
+ const retained = clientId === undefined ? undefined : clientMemory(clientId, req.root).identities?.[strategy];
300
+ if (retained !== undefined) return retained;
301
+ const tag = stableHex(`external_identity:${strategy}:${clientId ?? ''}:${new Date(atMs(req)).toISOString()}`, 8);
302
+ const identity = { email: `qa-${tag}@${provider.domain}`, firstName: 'Casey', lastName: `${provider.name}QA` };
303
+ if (clientId !== undefined) await rememberClient(clientId, { identities: { ...(clientMemory(clientId, req.root).identities ?? {}), [strategy]: identity } }, req);
304
+ return identity;
305
+ }
306
+
307
+ function consentUrlFor(attemptId: string, redirectUrl: string): string {
308
+ // The public origin the BROWSER must navigate to is exactly the origin the app gave in
309
+ // its own absolute redirect_url — no header plumbing through the mounts required.
310
+ const origin = new URL(redirectUrl).origin;
311
+ return `${origin}/v1/oauth-consent?attempt=${encodeURIComponent(attemptId)}`;
312
+ }
313
+
314
+ function signUpView(a: SignUpAttempt, at: number): Record<string, unknown> {
315
+ const missing: string[] = [];
316
+ if (a.strategy === undefined) {
317
+ if (a.fields.email_address === undefined) missing.push('email_address');
318
+ if (a.fields.password === undefined) missing.push('password');
319
+ }
320
+ const externalAccount = a.strategy === undefined ? null : a.status === 'complete'
321
+ ? { status: 'verified', strategy: a.strategy, attempts: null, expire_at: null, error: null }
322
+ : { status: 'unverified', strategy: a.strategy, attempts: null, expire_at: at + 600_000, error: null, external_verification_redirect_url: a.redirectUrl === undefined ? null : consentUrlFor(a.id, a.redirectUrl) };
323
+ return {
324
+ object: 'sign_up',
325
+ id: a.id,
326
+ status: a.status,
327
+ required_fields: a.strategy === undefined ? ['email_address', 'password'] : [],
328
+ optional_fields: ['first_name', 'last_name', 'phone_number', 'unsafe_metadata'],
329
+ missing_fields: a.status === 'complete' ? [] : missing,
330
+ unverified_fields: [],
331
+ verifications: { email_address: null, phone_number: null, external_account: externalAccount, web3_wallet: null },
332
+ username: null,
333
+ email_address: a.fields.email_address ?? null,
334
+ phone_number: a.fields.phone_number ?? null,
335
+ web3_wallet: null,
336
+ password_enabled: a.fields.password !== undefined,
337
+ first_name: a.fields.first_name ?? null,
338
+ last_name: a.fields.last_name ?? null,
339
+ unsafe_metadata: {},
340
+ public_metadata: {},
341
+ custom_action: false,
342
+ external_id: null,
343
+ created_session_id: a.createdSessionId,
344
+ created_user_id: a.createdUserId,
345
+ abandon_at: at + 3600_000,
346
+ legal_accepted_at: a.fields.legal_accepted === undefined ? null : at,
347
+ };
348
+ }
349
+
350
+ function signInView(a: { id: string; userId: string; identifier: string; status: string; createdSessionId: string | null; strategy?: string; redirectUrl?: string }, at: number): Record<string, unknown> {
351
+ return {
352
+ object: 'sign_in',
353
+ id: a.id,
354
+ status: a.status,
355
+ supported_identifiers: ['email_address'],
356
+ supported_first_factors: [{ strategy: 'password' }],
357
+ supported_second_factors: null,
358
+ first_factor_verification: a.status === 'complete'
359
+ ? { status: 'verified', strategy: a.strategy ?? 'password', attempts: 1, expire_at: null, error: null }
360
+ : a.strategy !== undefined
361
+ ? { status: 'unverified', strategy: a.strategy, attempts: null, expire_at: at + 600_000, error: null, external_verification_redirect_url: a.redirectUrl === undefined ? null : consentUrlFor(a.id, a.redirectUrl) }
362
+ : { status: null, strategy: null, attempts: null, expire_at: null, error: null },
363
+ second_factor_verification: null,
364
+ identifier: a.identifier,
365
+ user_data: {},
366
+ created_session_id: a.createdSessionId,
367
+ abandon_at: at + 3600_000,
368
+ };
369
+ }
370
+
371
+ // ── the handler ─────────────────────────────────────────────────────────────────
372
+ export async function handleClerkFapiRequest(req: ClerkFapiRequest): Promise<ClerkFapiResponse> {
373
+ const seg = req.path.split('/').filter((s) => s.length > 0);
374
+ const method = req.method.toUpperCase();
375
+ const params = parseBody(req.body);
376
+ const cookieClient = req.cookies?.[CLIENT_COOKIE];
377
+
378
+ if (seg[0] !== 'v1') return ferr(`Unknown route: ${req.path}`, 404, 'resource_not_found');
379
+
380
+ // GET /v1/environment — the boot document.
381
+ if (seg[1] === 'environment' && seg.length === 2 && method === 'GET') {
382
+ return { status: 200, body: environmentDoc(atMs(req)) };
383
+ }
384
+ // PATCH /v1/environment — clerk-js pings this in development; echo the document.
385
+ if (seg[1] === 'environment' && seg.length === 2 && method === 'PATCH') {
386
+ return { status: 200, body: environmentDoc(atMs(req)) };
387
+ }
388
+
389
+ // POST /v1/dev_browser — development-instance browser identification.
390
+ if (seg[1] === 'dev_browser' && method === 'POST') {
391
+ return { status: 200, body: { object: 'dev_browser', id: 'devb_twin', token: 'devb_twin_token' } };
392
+ }
393
+
394
+ // ── the twin provider's consent page: where an oauth strategy's external verification
395
+ // actually happens. GET renders the consent; POST completes the attempt (a REAL user row
396
+ // and session through the Backend doors) and redirects back to the app's callback.
397
+ if (seg[1] === 'oauth-consent') {
398
+ const attemptId = req.query?.attempt ?? params.attempt ?? '';
399
+ const su = readSignUp(attemptId, req.root);
400
+ const si = su === undefined ? readSignIn(attemptId, req.root) : undefined;
401
+ const holder = su ?? si;
402
+ if (holder === undefined || holder.strategy === undefined || holder.redirectUrl === undefined || holder.externalIdentity === undefined) return ferr('Unknown or non-oauth attempt', 404, 'resource_not_found');
403
+ const provider = OAUTH_PROVIDERS[holder.strategy] ?? { domain: 'twin-oauth.test', name: 'Provider' };
404
+ const identity = holder.externalIdentity;
405
+ if (method === 'GET') {
406
+ // The form posts back to this page's own path RELATIVELY: the page is reached at the app's
407
+ // origin (consentUrlFor) or under a served World's mount path, and a root-absolute action
408
+ // would escape the mount; a twinPublicBase action would leave the app's origin (and its cookies).
409
+ const html = `<!doctype html><html><head><meta charset="utf-8"><title>${provider.name} — Continue</title>
410
+ <style>body{font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#f5f5f5}main{background:#fff;border:1px solid #ddd;border-radius:12px;padding:32px;max-width:380px;text-align:center}button{margin-top:16px;padding:10px 24px;border-radius:8px;border:none;background:#1a73e8;color:#fff;font-size:15px;cursor:pointer}</style></head>
411
+ <body><main><h2>Sign in with ${provider.name}</h2><p>(twin provider — no real ${provider.name} account is involved)</p>
412
+ <p><strong>${identity.firstName} ${identity.lastName}</strong><br>${identity.email}</p>
413
+ <form method="post" action="oauth-consent"><input type="hidden" name="attempt" value="${attemptId}"><button type="submit">Continue as ${identity.firstName}</button></form></main></body></html>`;
414
+ return { status: 200, body: null, html };
415
+ }
416
+ if (method === 'POST') {
417
+ const clientId = cookieClient ?? await newClientId(req);
418
+ const found = await backend('GET', `/v1/users?email_address=${encodeURIComponent(identity.email)}`, undefined, req);
419
+ let user = Array.isArray(found.body) ? (found.body as Array<Record<string, unknown>>)[0] : undefined;
420
+ if (user === undefined) {
421
+ const created = await backend('POST', '/v1/users', { email_address: [identity.email], first_name: identity.firstName, last_name: identity.lastName }, req);
422
+ if (created.status !== 200) return { status: created.status, body: created.body };
423
+ user = record(created.body);
424
+ }
425
+ const session = await backend('POST', '/v1/sessions', { user_id: user.id, client_id: clientId }, req);
426
+ if (session.status !== 200) return { status: session.status, body: session.body };
427
+ holder.status = 'complete';
428
+ holder.createdSessionId = String(record(session.body).id);
429
+ if (su !== undefined) { su.createdUserId = String(user.id); su.fields.email_address = identity.email; await saveSignUp(su, req); }
430
+ else if (si !== undefined) { si.userId = String(user.id); si.identifier = identity.email; await saveSignIn(si, req); }
431
+ return { status: 303, body: null, redirect: holder.redirectUrl, setCookies: cookieClient === undefined ? [clientCookie(clientId)] : [] };
432
+ }
433
+ }
434
+
435
+ // ── /v1/me — the signed-in user's own profile, password and sessions: what clerk-js's UserProfile writes
436
+ // through (walks 3 and 4 saw "Unknown route: POST /v1/me" and an empty "Active devices").
437
+ if (seg[1] === 'me') {
438
+ const signedOut = { status: 401, body: { errors: [{ message: 'Signed out', long_message: 'You are signed out', code: 'signed_out' }] } };
439
+ if (cookieClient === undefined) return signedOut;
440
+ const row = await getClientRow(cookieClient, req);
441
+ const sessions = (row?.sessions as Array<Record<string, unknown>> | undefined) ?? [];
442
+ const current = sessions[sessions.length - 1];
443
+ if (current === undefined) return signedOut;
444
+ const userId = String(current.user_id);
445
+ const me = async (): Promise<Record<string, unknown>> => { const u = await backend('GET', `/v1/users/${encodeURIComponent(userId)}`, undefined, req); return fapiUser(record(u.body)); };
446
+ const envelope = async (response: unknown): Promise<ClerkFapiResponse> => ({ status: 200, body: { response, client: await clientEnvelope(cookieClient, req) } });
447
+ if (seg.length === 2 && method === 'GET') return envelope(await me());
448
+ if (seg.length === 2 && (method === 'PATCH' || method === 'POST')) {
449
+ const patch: Record<string, unknown> = {};
450
+ for (const k of ['first_name', 'last_name', 'username', 'primary_email_address_id', 'unsafe_metadata']) if (params[k] !== undefined) patch[k] = params[k];
451
+ const u = await backend('PATCH', `/v1/users/${encodeURIComponent(userId)}`, patch, req);
452
+ if (u.status !== 200) return { status: u.status, body: u.body };
453
+ return envelope(fapiUser(record(u.body)));
454
+ }
455
+ if (seg.length === 3 && seg[2] === 'change_password' && method === 'POST') {
456
+ const next = typeof params.new_password === 'string' ? params.new_password : '';
457
+ if (next.length < 8) return { status: 422, body: { errors: [{ message: 'Passwords must be 8 characters or more.', code: 'form_password_length_too_short', meta: { param_name: 'new_password' } }] } };
458
+ if (typeof params.current_password === 'string' && params.current_password !== '') {
459
+ const v = await backend('POST', `/v1/users/${encodeURIComponent(userId)}/verify_password`, { password: params.current_password }, req);
460
+ if (v.status !== 200) return { status: 422, body: { errors: [{ message: 'Incorrect password', long_message: 'The current password is incorrect.', code: 'form_password_incorrect', meta: { param_name: 'current_password' } }] } };
461
+ }
462
+ const u = await backend('PATCH', `/v1/users/${encodeURIComponent(userId)}`, { password: next }, req);
463
+ if (u.status !== 200) return { status: u.status, body: u.body };
464
+ return envelope(fapiUser(record(u.body)));
465
+ }
466
+ if (seg.length === 4 && seg[2] === 'sessions' && seg[3] === 'active' && method === 'GET') {
467
+ const all = await backend('GET', '/v1/sessions', undefined, req);
468
+ const mine = (Array.isArray(all.body) ? (all.body as Array<Record<string, unknown>>) : []).filter((s) => s.user_id === userId && s.status === 'active');
469
+ const u = await me();
470
+ return envelope(mine.map((s) => ({ ...fapiSession(s, u), latest_activity: { id: `activity_${String(s.id)}`, object: 'session_activity', device_type: 'Macintosh', is_mobile: false, browser_name: 'Chrome', browser_version: '', ip_address: '127.0.0.1', city: '', country: '' } })));
471
+ }
472
+ if (seg.length === 5 && seg[2] === 'sessions' && seg[4] === 'revoke' && method === 'POST') {
473
+ const r = await backend('POST', `/v1/sessions/${encodeURIComponent(String(seg[3]))}/revoke`, {}, req);
474
+ if (r.status !== 200) return { status: r.status, body: r.body };
475
+ return envelope(fapiSession(record(r.body), await me()));
476
+ }
477
+ return { status: 404, body: { errors: [{ message: `Unknown route: ${method} ${req.path}`, code: 'resource_not_found' }] } };
478
+ }
479
+
480
+ if (seg[1] === 'client') {
481
+ // PUT/POST /v1/client — ensure the browser's client exists.
482
+ if (seg.length === 2 && (method === 'POST' || method === 'PUT')) {
483
+ const id = cookieClient ?? await newClientId(req);
484
+ return { status: 200, body: { response: await clientEnvelope(id, req), client: null }, setCookies: [clientCookie(id)] };
485
+ }
486
+ // GET /v1/client — read it (200 with a null response when no cookie yet).
487
+ if (seg.length === 2 && method === 'GET') {
488
+ if (cookieClient === undefined) return { status: 200, body: { response: null, client: null } };
489
+ return { status: 200, body: { response: await clientEnvelope(cookieClient, req), client: null } };
490
+ }
491
+ // DELETE /v1/client — sign out everything on this client.
492
+ if (seg.length === 2 && method === 'DELETE') {
493
+ if (cookieClient !== undefined) {
494
+ const row = await getClientRow(cookieClient, req);
495
+ for (const s of (row?.sessions as Array<Record<string, unknown>> | undefined) ?? []) {
496
+ await backend('POST', `/v1/sessions/${encodeURIComponent(String(s.id))}/revoke`, {}, req);
497
+ }
498
+ }
499
+ const id = cookieClient ?? await newClientId(req);
500
+ return { status: 200, body: { response: await clientEnvelope(id, req), client: null } };
501
+ }
502
+
503
+ // The real Frontend API mints the browser's client lazily on first need — a sign-in
504
+ // beginning IS that need. Everything after an attempt exists rides the minted cookie.
505
+ const clientId = cookieClient ?? await newClientId(req);
506
+ const mintedCookies = cookieClient === undefined ? [clientCookie(clientId)] : [];
507
+ const withMint = (res: ClerkFapiResponse): ClerkFapiResponse =>
508
+ mintedCookies.length === 0 ? res : { ...res, setCookies: [...(res.setCookies ?? []), ...mintedCookies] };
509
+
510
+ // ── sign-ups: the app-visible registration protocol. A sign-up attempt collects
511
+ // fields progressively; when email + password are present it becomes a REAL user row
512
+ // (through the Backend door, password scrypt-hashed there) plus an active session.
513
+ if (seg[2] === 'sign_ups' && (seg.length === 3 || seg.length === 4) && (method === 'POST' || method === 'PATCH')) {
514
+ const existingId = seg.length === 4 ? seg[3] : undefined;
515
+ const attempt: SignUpAttempt | undefined = existingId === undefined
516
+ ? { id: fapiId('sua', 'sign_up_attempt', req), fields: {}, status: 'missing_requirements', createdSessionId: null, createdUserId: null }
517
+ : readSignUp(existingId, req.root);
518
+ if (attempt === undefined) return ferr('Sign-up attempt not found', 404, 'resource_not_found');
519
+ // A social strategy hands verification to the (twin) provider: the client navigates
520
+ // to the consent page, which completes the attempt and redirects back.
521
+ if (typeof params.strategy === 'string' && params.strategy.startsWith('oauth_')) {
522
+ if (typeof params.redirect_url !== 'string' || params.redirect_url === '') return ferr('redirect_url is required for an oauth strategy', 422, 'form_param_missing');
523
+ attempt.strategy = params.strategy;
524
+ attempt.redirectUrl = params.redirect_url;
525
+ attempt.externalIdentity ??= await mintExternalIdentity(params.strategy, clientId, req);
526
+ attempt.status = 'missing_requirements';
527
+ await saveSignUp(attempt, req);
528
+ await rememberClient(clientId, { sign_up_attempt: attempt.id }, req);
529
+ return withMint({ status: 200, body: { response: signUpView(attempt, atMs(req)), client: await clientEnvelope(clientId, req) } });
530
+ }
531
+ for (const key of ['email_address', 'password', 'first_name', 'last_name', 'phone_number', 'unsafe_metadata', 'legal_accepted'] as const) {
532
+ if (params[key] !== undefined) attempt.fields[key] = params[key];
533
+ }
534
+ if (attempt.status !== 'complete' && attempt.fields.email_address !== undefined && attempt.fields.password !== undefined) {
535
+ const email = attempt.fields.email_address;
536
+ const taken = await backend('GET', `/v1/users?email_address=${encodeURIComponent(email)}`, undefined, req);
537
+ if (Array.isArray(taken.body) && taken.body.length > 0) {
538
+ await saveSignUp(attempt, req);
539
+ return withMint({ status: 422, body: { errors: [{ code: 'form_identifier_exists', message: 'That email address is taken. Please try another.', long_message: 'That email address is taken. Please try another.', meta: { param_name: 'email_address' } }], clerk_trace_id: 'trace_twin_fapi' } });
540
+ }
541
+ const created = await backend('POST', '/v1/users', {
542
+ email_address: [email],
543
+ password: attempt.fields.password,
544
+ ...(attempt.fields.first_name !== undefined ? { first_name: attempt.fields.first_name } : {}),
545
+ ...(attempt.fields.last_name !== undefined ? { last_name: attempt.fields.last_name } : {}),
546
+ }, req);
547
+ if (created.status !== 200) return withMint({ status: created.status, body: created.body });
548
+ const session = await backend('POST', '/v1/sessions', { user_id: record(created.body).id, client_id: clientId }, req);
549
+ if (session.status !== 200) return withMint({ status: session.status, body: session.body });
550
+ attempt.status = 'complete';
551
+ attempt.createdUserId = String(record(created.body).id);
552
+ attempt.createdSessionId = String(record(session.body).id);
553
+ }
554
+ await saveSignUp(attempt, req);
555
+ await rememberClient(clientId, { sign_up_attempt: attempt.id }, req);
556
+ return withMint({ status: 200, body: { response: signUpView(attempt, atMs(req)), client: await clientEnvelope(clientId, req) } });
557
+ }
558
+ // GET /v1/client/sign_ups/:id — read an attempt back.
559
+ if (seg[2] === 'sign_ups' && seg.length === 4 && method === 'GET') {
560
+ const attempt = readSignUp(String(seg[3]), req.root);
561
+ if (attempt === undefined) return ferr('Sign-up attempt not found', 404, 'resource_not_found');
562
+ return withMint({ status: 200, body: { response: signUpView(attempt, atMs(req)), client: await clientEnvelope(clientId, req) } });
563
+ }
564
+
565
+ // POST /v1/client/sign_ins — begin a sign-in. An oauth strategy hands verification to
566
+ // the (twin) provider's consent page; the password path checks against a stored user.
567
+ if (seg[2] === 'sign_ins' && seg.length === 3 && method === 'POST') {
568
+ if (typeof params.strategy === 'string' && params.strategy.startsWith('oauth_')) {
569
+ if (typeof params.redirect_url !== 'string' || params.redirect_url === '') return withMint(ferr('redirect_url is required for an oauth strategy', 422, 'form_param_missing'));
570
+ const attempt = { id: fapiId('sia', 'sign_in_attempt', req), userId: '', identifier: '', status: 'needs_first_factor', createdSessionId: null, strategy: params.strategy, redirectUrl: params.redirect_url, externalIdentity: await mintExternalIdentity(params.strategy, clientId, req) };
571
+ await saveSignIn(attempt, req);
572
+ await rememberClient(clientId, { sign_in_attempt: attempt.id }, req);
573
+ return withMint({ status: 200, body: { response: signInView(attempt, atMs(req)), client: await clientEnvelope(clientId, req) } });
574
+ }
575
+ // The ticket strategy: a sign-in token (minted by the app's backend through the
576
+ // Backend API) exchanged directly for a session — the twin verifies it against its
577
+ // own JWKS, so only genuinely minted tickets sign in.
578
+ if (params.strategy === 'ticket') {
579
+ const ticket = params.ticket ?? '';
580
+ if (ticket === '') return withMint(ferr('ticket is required', 422, 'form_param_missing'));
581
+ const verdict = verifyJwtWithJwks(ticket, await buildJwks(req.root));
582
+ if (!verdict.valid) return withMint(ferr(`Invalid sign-in ticket: ${verdict.reason}`, 422, 'ticket_invalid'));
583
+ const payload = JSON.parse(Buffer.from(ticket.split('.')[1]!, 'base64url').toString('utf8')) as { sub?: string };
584
+ const userId = payload.sub ?? '';
585
+ const found = await backend('GET', `/v1/users/${encodeURIComponent(userId)}`, undefined, req);
586
+ if (found.status !== 200) return withMint(ferr('The ticket names an unknown user', 422, 'ticket_invalid'));
587
+ const created = await backend('POST', '/v1/sessions', { user_id: userId, client_id: clientId }, req);
588
+ if (created.status !== 200) return withMint({ status: created.status, body: created.body });
589
+ const attempt = { id: fapiId('sia', 'sign_in_attempt', req), userId, identifier: primaryIdentifier(record(found.body)), status: 'complete', createdSessionId: String(record(created.body).id), strategy: 'ticket' };
590
+ await saveSignIn(attempt, req);
591
+ await rememberClient(clientId, { sign_in_attempt: attempt.id }, req);
592
+ return withMint({ status: 200, body: { response: signInView(attempt, atMs(req)), client: await clientEnvelope(clientId, req, signInView(attempt, atMs(req))) } });
593
+ }
594
+ const identifier = params.identifier ?? '';
595
+ if (identifier === '') return withMint(ferr('identifier is required', 422, 'form_param_missing'));
596
+ const found = await backend('GET', `/v1/users?email_address=${encodeURIComponent(identifier)}`, undefined, req);
597
+ const user = Array.isArray(found.body) ? (found.body as Array<Record<string, unknown>>)[0] : undefined;
598
+ if (user === undefined) return withMint(ferr("Couldn't find your account.", 422, 'form_identifier_not_found'));
599
+ const attempt: SignInAttempt = { id: fapiId('sia', 'sign_in_attempt', req), userId: String(user.id), identifier, status: 'needs_first_factor', createdSessionId: null };
600
+ await saveSignIn(attempt, req); await rememberClient(clientId, { sign_in_attempt: attempt.id }, req);
601
+ // A one-shot sign-in (password supplied inline) completes immediately.
602
+ if (params.password !== undefined && params.password !== '') {
603
+ return withMint(await attemptPassword(attempt.id, params.password, clientId, req));
604
+ }
605
+ return withMint({ status: 200, body: { response: signInView(attempt, atMs(req)), client: await clientEnvelope(clientId, req, signInView(attempt, atMs(req))) } });
606
+ }
607
+
608
+ // POST /v1/client/sign_ins/:id/attempt_first_factor — the password check.
609
+ if (seg[2] === 'sign_ins' && seg.length === 5 && seg[4] === 'attempt_first_factor' && method === 'POST') {
610
+ if (params.strategy !== undefined && params.strategy !== 'password') {
611
+ return ferr(`strategy "${params.strategy}" is not enabled on this twin`, 422, 'strategy_for_user_invalid');
612
+ }
613
+ return attemptPassword(seg[3], params.password ?? '', clientId, req);
614
+ }
615
+
616
+ // GET /v1/client/sign_ins/:id — read an attempt back.
617
+ if (seg[2] === 'sign_ins' && seg.length === 4 && method === 'GET') {
618
+ const attempt = readSignIn(String(seg[3]), req.root);
619
+ if (attempt === undefined) return ferr('Sign-in attempt not found', 404, 'resource_not_found');
620
+ return { status: 200, body: { response: signInView(attempt, atMs(req)), client: await clientEnvelope(clientId, req) } };
621
+ }
622
+
623
+ // DELETE /v1/client/sessions — sign out of every session on this client (Clerk.signOut()).
624
+ if (seg[2] === 'sessions' && seg.length === 3 && method === 'DELETE') {
625
+ const row = await getClientRow(clientId, req);
626
+ for (const s of (row?.sessions as Array<Record<string, unknown>> | undefined) ?? []) {
627
+ await backend('POST', `/v1/sessions/${encodeURIComponent(String(s.id))}/revoke`, {}, req);
628
+ }
629
+ return withMint({ status: 200, body: { response: null, client: await clientEnvelope(clientId, req) } });
630
+ }
631
+
632
+ if (seg[2] === 'sessions' && seg.length >= 4) {
633
+ const sessionId = seg[3];
634
+ // POST /v1/client/sessions/:id/tokens[/:template] — delegate to the Backend mint.
635
+ if (seg[4] === 'tokens' && method === 'POST') {
636
+ const tmpl = seg[5] !== undefined ? `/${seg[5]}` : '';
637
+ const minted = await backend('POST', `/v1/sessions/${encodeURIComponent(sessionId)}/tokens${tmpl}`, {}, req);
638
+ return { status: minted.status, body: minted.body };
639
+ }
640
+ // POST /v1/client/sessions/:id/touch
641
+ if (seg[4] === 'touch' && method === 'POST') {
642
+ const touched = await backend('POST', `/v1/sessions/${encodeURIComponent(sessionId)}/touch`, {}, req);
643
+ if (touched.status !== 200) return { status: touched.status, body: touched.body };
644
+ const u = await backend('GET', `/v1/users/${encodeURIComponent(String(record(touched.body).user_id))}`, undefined, req);
645
+ return { status: 200, body: { response: fapiSession(record(touched.body), u.status === 200 ? fapiUser(record(u.body)) : null), client: await clientEnvelope(clientId, req) } };
646
+ }
647
+ // DELETE /v1/client/sessions/:id or POST .../remove|end — sign out one session.
648
+ if ((seg.length === 4 && method === 'DELETE') || (seg.length === 5 && (seg[4] === 'remove' || seg[4] === 'end') && method === 'POST')) {
649
+ const revoked = await backend('POST', `/v1/sessions/${encodeURIComponent(sessionId)}/revoke`, {}, req);
650
+ if (revoked.status !== 200) return { status: revoked.status, body: revoked.body };
651
+ return { status: 200, body: { response: fapiSession(record(revoked.body), null), client: await clientEnvelope(clientId, req) } };
652
+ }
653
+ }
654
+ }
655
+
656
+ return ferr(`Unknown route: ${req.path}`, 404, 'resource_not_found');
657
+ }
658
+
659
+ async function attemptPassword(attemptId: string, password: string, clientId: string, req: ClerkFapiRequest): Promise<ClerkFapiResponse> {
660
+ const attempt = readSignIn(attemptId, req.root);
661
+ if (attempt === undefined) return ferr('Sign-in attempt not found', 404, 'resource_not_found');
662
+ if (password === '') return ferr('password is required', 422, 'form_param_missing');
663
+ const verified = await backend('POST', `/v1/users/${encodeURIComponent(attempt.userId)}/verify_password`, { password }, req);
664
+ if (verified.status !== 200) {
665
+ return { status: 422, body: { errors: [{ code: 'form_password_incorrect', message: 'Password is incorrect. Try again, or use another method.', long_message: 'Password is incorrect. Try again, or use another method.', meta: { param_name: 'password' } }], clerk_trace_id: 'trace_twin_fapi' } };
666
+ }
667
+ const created = await backend('POST', '/v1/sessions', { user_id: attempt.userId, client_id: clientId }, req);
668
+ if (created.status !== 200) return { status: created.status, body: created.body };
669
+ attempt.status = 'complete';
670
+ attempt.createdSessionId = String(record(created.body).id);
671
+ await saveSignIn(attempt, req);
672
+ return { status: 200, body: { response: signInView(attempt, atMs(req)), client: await clientEnvelope(clientId, req, signInView(attempt, atMs(req))) } };
673
+ }