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