@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,172 @@
|
|
|
1
|
+
// Clerk twin conformance — spec-true validation of the resources the twin serves
|
|
2
|
+
// against per-object JSON Schemas (object discriminator, required fields, types),
|
|
3
|
+
// offline (no key). Clerk's Backend API is one we cannot dual-run against a live
|
|
4
|
+
// sandbox in CI, so the published-shape schemas carry the weight here.
|
|
5
|
+
//
|
|
6
|
+
// For every resource the twin emits, validate its body via the shared specConformance
|
|
7
|
+
// harness: declared TYPES, REQUIRED fields (omission), ENUMS. Twin-internal `_`-prefixed
|
|
8
|
+
// fields are exempt; polymorphic/nested fields are left untyped to avoid false positives.
|
|
9
|
+
import { twinResources } from '@volter/world-core';
|
|
10
|
+
import { specConformance } from '@volter/world-tooling';
|
|
11
|
+
import type { TwinResource } from '@volter/world-core';
|
|
12
|
+
|
|
13
|
+
type JsonSchema = specConformance.JsonSchema;
|
|
14
|
+
type KnownDeviation = specConformance.KnownDeviation;
|
|
15
|
+
type SpecViolation = specConformance.SpecViolation;
|
|
16
|
+
|
|
17
|
+
// Per-object Clerk JSON Schemas (the fields/types the Backend API documents for each
|
|
18
|
+
// object). Kept inline (the surface is small + stable); grow as the twin grows. Only
|
|
19
|
+
// scalar/array required fields are typed — nested polymorphic blocks (verification,
|
|
20
|
+
// metadata bags, nested organization) are left untyped to avoid false positives.
|
|
21
|
+
const SCHEMAS: Record<string, JsonSchema> = {
|
|
22
|
+
user: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
required: ['object', 'id', 'email_addresses', 'banned', 'locked'],
|
|
25
|
+
properties: {
|
|
26
|
+
object: { type: 'string', enum: ['user'] },
|
|
27
|
+
id: { type: 'string' },
|
|
28
|
+
banned: { type: 'boolean' },
|
|
29
|
+
locked: { type: 'boolean' },
|
|
30
|
+
created_at: { type: 'integer' },
|
|
31
|
+
updated_at: { type: 'integer' },
|
|
32
|
+
email_addresses: { type: 'array' },
|
|
33
|
+
phone_numbers: { type: 'array' },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
session: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
required: ['object', 'id', 'user_id', 'status'],
|
|
39
|
+
properties: {
|
|
40
|
+
object: { type: 'string', enum: ['session'] },
|
|
41
|
+
id: { type: 'string' },
|
|
42
|
+
user_id: { type: 'string' },
|
|
43
|
+
status: { type: 'string', enum: ['active', 'revoked', 'ended', 'expired', 'removed', 'abandoned', 'replaced'] },
|
|
44
|
+
created_at: { type: 'integer' },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
organization: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
required: ['object', 'id', 'name', 'slug', 'members_count'],
|
|
50
|
+
properties: {
|
|
51
|
+
object: { type: 'string', enum: ['organization'] },
|
|
52
|
+
id: { type: 'string' },
|
|
53
|
+
name: { type: 'string' },
|
|
54
|
+
slug: { type: 'string' },
|
|
55
|
+
members_count: { type: 'integer' },
|
|
56
|
+
created_at: { type: 'integer' },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
organization_membership: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
required: ['object', 'id', 'role', 'organization_id'],
|
|
62
|
+
properties: {
|
|
63
|
+
object: { type: 'string', enum: ['organization_membership'] },
|
|
64
|
+
id: { type: 'string' },
|
|
65
|
+
role: { type: 'string' },
|
|
66
|
+
organization_id: { type: 'string' },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
organization_invitation: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
required: ['object', 'id', 'email_address', 'status', 'organization_id'],
|
|
72
|
+
properties: {
|
|
73
|
+
object: { type: 'string', enum: ['organization_invitation'] },
|
|
74
|
+
id: { type: 'string' },
|
|
75
|
+
email_address: { type: 'string' },
|
|
76
|
+
status: { type: 'string', enum: ['pending', 'accepted', 'revoked'] },
|
|
77
|
+
organization_id: { type: 'string' },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
invitation: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
required: ['object', 'id', 'email_address', 'status'],
|
|
83
|
+
properties: {
|
|
84
|
+
object: { type: 'string', enum: ['invitation'] },
|
|
85
|
+
id: { type: 'string' },
|
|
86
|
+
email_address: { type: 'string' },
|
|
87
|
+
status: { type: 'string', enum: ['pending', 'accepted', 'revoked'] },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
jwt_template: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
required: ['object', 'id', 'name', 'lifetime'],
|
|
93
|
+
properties: {
|
|
94
|
+
object: { type: 'string', enum: ['jwt_template'] },
|
|
95
|
+
id: { type: 'string' },
|
|
96
|
+
name: { type: 'string' },
|
|
97
|
+
lifetime: { type: 'integer' },
|
|
98
|
+
signing_algorithm: { type: 'string' },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// twin resource type → schema key. Internal/derived types (keypair, sign_in_token,
|
|
104
|
+
// email_address, allow/blocklist) are not schema-checked here.
|
|
105
|
+
const TYPE_TO_OBJECT: Record<string, string> = {
|
|
106
|
+
user: 'user', session: 'session', organization: 'organization',
|
|
107
|
+
organization_membership: 'organization_membership', organization_invitation: 'organization_invitation',
|
|
108
|
+
invitation: 'invitation', jwt_template: 'jwt_template',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export type ClerkViolation = SpecViolation & { object: string; id: string };
|
|
112
|
+
export type ClerkConformanceReport = {
|
|
113
|
+
ok: boolean;
|
|
114
|
+
resourcesChecked: number;
|
|
115
|
+
fieldsChecked: number;
|
|
116
|
+
violations: ClerkViolation[];
|
|
117
|
+
knownIgnored: number;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const isTwinExtra = (k: string): boolean => k.startsWith('_');
|
|
121
|
+
|
|
122
|
+
// The emitted body: drop kernel meta + twin-internal fields, restore the vendor `type`.
|
|
123
|
+
function emitted(r: TwinResource): Record<string, unknown> {
|
|
124
|
+
const { type, updatedAt, _clerk_type, ...rest } = r as TwinResource & { _clerk_type?: unknown };
|
|
125
|
+
const out: Record<string, unknown> = {};
|
|
126
|
+
for (const [k, v] of Object.entries(rest)) if (!k.startsWith('_')) out[k] = v;
|
|
127
|
+
out.id = r.id;
|
|
128
|
+
if (_clerk_type !== undefined) out.type = _clerk_type;
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function loadClerkSchemas(): Record<string, JsonSchema> {
|
|
133
|
+
return SCHEMAS;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validate every Clerk resource the twin serves (in `root`) against its schema. A wrong
|
|
138
|
+
* type, an undeclared missing-required field, or an enum violation fails the report.
|
|
139
|
+
*/
|
|
140
|
+
export function checkClerkConformance(opts: { root?: string; known?: KnownDeviation[] } = {}): ClerkConformanceReport {
|
|
141
|
+
const resources = twinResources('clerk', opts.root).filter((r) => !r.type.startsWith('_'));
|
|
142
|
+
const violations: ClerkViolation[] = [];
|
|
143
|
+
let fieldsChecked = 0;
|
|
144
|
+
let knownIgnored = 0;
|
|
145
|
+
for (const r of resources) {
|
|
146
|
+
const objectName = TYPE_TO_OBJECT[r.type];
|
|
147
|
+
if (!objectName) continue;
|
|
148
|
+
const schema = SCHEMAS[objectName];
|
|
149
|
+
if (!schema) continue;
|
|
150
|
+
const rep = specConformance.checkSpecConformance(emitted(r), schema, { exemptKey: isTwinExtra, known: opts.known ?? [], flagExtra: false });
|
|
151
|
+
fieldsChecked += rep.fieldsChecked;
|
|
152
|
+
knownIgnored += rep.knownIgnored;
|
|
153
|
+
for (const v of rep.violations) violations.push({ ...v, object: objectName, id: r.id });
|
|
154
|
+
}
|
|
155
|
+
return { ok: violations.length === 0, resourcesChecked: resources.length, fieldsChecked, violations, knownIgnored };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type ClerkCoverageReport = specConformance.SpecCoverageReport & { object: string };
|
|
159
|
+
|
|
160
|
+
/** Per-object coverage: of the fields each Clerk object declares, which does the twin emit? */
|
|
161
|
+
export function clerkCoverage(opts: { root?: string } = {}): ClerkCoverageReport[] {
|
|
162
|
+
const byType = new Map<string, TwinResource>();
|
|
163
|
+
for (const r of twinResources('clerk', opts.root)) if (!r.type.startsWith('_') && !byType.has(r.type)) byType.set(r.type, r);
|
|
164
|
+
const out: ClerkCoverageReport[] = [];
|
|
165
|
+
for (const [type, r] of byType) {
|
|
166
|
+
const objectName = TYPE_TO_OBJECT[type];
|
|
167
|
+
const schema = objectName ? SCHEMAS[objectName] : undefined;
|
|
168
|
+
if (!objectName || !schema) continue;
|
|
169
|
+
out.push({ object: objectName, ...specConformance.specCoverage(schema, emitted(r), { exemptKey: isTwinExtra }) });
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// Clerk CONNECTOR — the live-vendor pull/push path that gives the Clerk twin the full
|
|
2
|
+
// "git for SaaS" lifecycle over an INJECTED client (the auth boundary).
|
|
3
|
+
//
|
|
4
|
+
// PULL (real → twin): fetch real Clerk objects via the injected client, map them to
|
|
5
|
+
// SyncResource[], and fold them into the event log via syncPull
|
|
6
|
+
// (shadow-diff dedup, so a re-pull of identical state is a no-op).
|
|
7
|
+
// PUSH (twin → real): for every PENDING local action, call the injected client and
|
|
8
|
+
// confirmAction on success — recording the confirmed fields as an
|
|
9
|
+
// observed event and suppressing the local projection.
|
|
10
|
+
//
|
|
11
|
+
// The vendor I/O is an INJECTED client interface (`ClerkClient`): a fake in tests, the
|
|
12
|
+
// real `@clerk/backend` createClerkClient(...) in prod. The pack imports NO SDK and holds
|
|
13
|
+
// NO key. The shape below is structurally satisfied by `createClerkClient(...)`.
|
|
14
|
+
import { observeResources } from '@volter/world-core';
|
|
15
|
+
import type { PerformContext, PushOutcome, RemoteExecute, SyncResource, TwinAction } from '@volter/world-core';
|
|
16
|
+
|
|
17
|
+
const SERVICE = 'clerk';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The injected real-Clerk boundary — a minimal subset of `@clerk/backend`'s ClerkClient
|
|
21
|
+
* that the connector uses. The real client (createClerkClient({ secretKey })) is
|
|
22
|
+
* structurally assignable; tests pass a fake. Only the methods the connector calls are
|
|
23
|
+
* declared, so the surface stays honest.
|
|
24
|
+
*/
|
|
25
|
+
export interface ClerkClient {
|
|
26
|
+
users: {
|
|
27
|
+
getUserList(params?: { limit?: number; offset?: number }): Promise<{ data: ClerkUser[]; totalCount: number } | ClerkUser[]>;
|
|
28
|
+
createUser(params: Record<string, unknown>): Promise<ClerkUser>;
|
|
29
|
+
updateUser(userId: string, params: Record<string, unknown>): Promise<ClerkUser>;
|
|
30
|
+
deleteUser(userId: string): Promise<{ id: string; deleted?: boolean }>;
|
|
31
|
+
};
|
|
32
|
+
organizations: {
|
|
33
|
+
getOrganizationList(params?: { limit?: number; offset?: number }): Promise<{ data: ClerkOrg[]; totalCount: number } | ClerkOrg[]>;
|
|
34
|
+
createOrganization(params: Record<string, unknown>): Promise<ClerkOrg>;
|
|
35
|
+
updateOrganization(orgId: string, params: Record<string, unknown>): Promise<ClerkOrg>;
|
|
36
|
+
deleteOrganization(orgId: string): Promise<{ id: string; deleted?: boolean }>;
|
|
37
|
+
getOrganizationMembershipList?(params: { organizationId: string; limit?: number; offset?: number }): Promise<{ data: ClerkMembership[]; totalCount: number } | ClerkMembership[]>;
|
|
38
|
+
};
|
|
39
|
+
// Optional: the sessions resource is only used by the sessions/memberships pull path.
|
|
40
|
+
sessions?: {
|
|
41
|
+
getSessionList(params?: { userId?: string; status?: string; limit?: number; offset?: number }): Promise<{ data: ClerkSession[]; totalCount: number } | ClerkSession[]>;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ClerkUser = {
|
|
46
|
+
id: string; firstName?: string | null; lastName?: string | null; username?: string | null;
|
|
47
|
+
emailAddresses?: Array<{ emailAddress: string }>; banned?: boolean; locked?: boolean;
|
|
48
|
+
createdAt?: number; externalId?: string | null;
|
|
49
|
+
};
|
|
50
|
+
export type ClerkOrg = {
|
|
51
|
+
id: string; name?: string; slug?: string; membersCount?: number; maxAllowedMemberships?: number; createdAt?: number;
|
|
52
|
+
updatedAt?: number; imageUrl?: string; hasImage?: boolean; pendingInvitationsCount?: number; adminDeleteEnabled?: boolean; createdBy?: string | null; publicMetadata?: Record<string, unknown>; privateMetadata?: Record<string, unknown>;
|
|
53
|
+
};
|
|
54
|
+
export type ClerkSession = {
|
|
55
|
+
id: string; userId?: string; status?: string; clientId?: string;
|
|
56
|
+
lastActiveAt?: number; expireAt?: number; createdAt?: number; lastActiveOrganizationId?: string | null;
|
|
57
|
+
};
|
|
58
|
+
export type ClerkMembership = {
|
|
59
|
+
id: string; role?: string; organization?: { id?: string } | null; orgId?: string;
|
|
60
|
+
publicUserData?: { userId?: string } | null; createdAt?: number;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function listOf<T>(res: { data?: T[] } | T[]): T[] {
|
|
64
|
+
return Array.isArray(res) ? res : Array.isArray(res?.data) ? res.data : [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── PULL ──────────────────────────────────────────────────────────────────────
|
|
68
|
+
/** Map a real-Clerk user (camelCase SDK shape) → a twin sync resource (snake_case). */
|
|
69
|
+
export function mapUser(u: ClerkUser): SyncResource {
|
|
70
|
+
return {
|
|
71
|
+
type: 'user',
|
|
72
|
+
id: String(u.id),
|
|
73
|
+
fields: {
|
|
74
|
+
first_name: u.firstName ?? null,
|
|
75
|
+
last_name: u.lastName ?? null,
|
|
76
|
+
username: u.username ?? null,
|
|
77
|
+
primary_email: u.emailAddresses?.[0]?.emailAddress ?? null,
|
|
78
|
+
banned: typeof u.banned === 'boolean' ? u.banned : false,
|
|
79
|
+
locked: typeof u.locked === 'boolean' ? u.locked : false,
|
|
80
|
+
external_id: u.externalId ?? null,
|
|
81
|
+
created_at: typeof u.createdAt === 'number' ? u.createdAt : null,
|
|
82
|
+
object: 'user',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Map a real-Clerk organization → a twin sync resource. */
|
|
88
|
+
export function mapOrganization(o: ClerkOrg): SyncResource {
|
|
89
|
+
return {
|
|
90
|
+
type: 'organization',
|
|
91
|
+
id: String(o.id),
|
|
92
|
+
fields: {
|
|
93
|
+
name: o.name ?? null,
|
|
94
|
+
slug: o.slug ?? null,
|
|
95
|
+
members_count: typeof o.membersCount === 'number' ? o.membersCount : 0,
|
|
96
|
+
max_allowed_memberships: typeof o.maxAllowedMemberships === 'number' ? o.maxAllowedMemberships : null,
|
|
97
|
+
created_at: typeof o.createdAt === 'number' ? o.createdAt : null,
|
|
98
|
+
updated_at: typeof o.updatedAt === 'number' ? o.updatedAt : (typeof o.createdAt === 'number' ? o.createdAt : null),
|
|
99
|
+
// the twin's own write stores these too — shape parity (protocol 2) holds the two mappings equal
|
|
100
|
+
image_url: o.imageUrl ?? '', has_image: Boolean(o.hasImage), pending_invitations_count: o.pendingInvitationsCount ?? 0, admin_delete_enabled: o.adminDeleteEnabled ?? true, created_by: o.createdBy ?? null,
|
|
101
|
+
public_metadata: o.publicMetadata ?? {}, private_metadata: o.privateMetadata ?? {},
|
|
102
|
+
object: 'organization',
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Map a real-Clerk session → a twin sync resource. */
|
|
108
|
+
export function mapSession(s: ClerkSession): SyncResource {
|
|
109
|
+
return {
|
|
110
|
+
type: 'session',
|
|
111
|
+
id: String(s.id),
|
|
112
|
+
fields: {
|
|
113
|
+
user_id: s.userId ?? null,
|
|
114
|
+
status: s.status ?? 'active',
|
|
115
|
+
client_id: s.clientId ?? null,
|
|
116
|
+
last_active_at: typeof s.lastActiveAt === 'number' ? s.lastActiveAt : null,
|
|
117
|
+
expire_at: typeof s.expireAt === 'number' ? s.expireAt : null,
|
|
118
|
+
last_active_organization_id: s.lastActiveOrganizationId ?? null,
|
|
119
|
+
created_at: typeof s.createdAt === 'number' ? s.createdAt : null,
|
|
120
|
+
object: 'session',
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Map a real-Clerk organization membership → a twin sync resource. */
|
|
125
|
+
export function mapMembership(m: ClerkMembership): SyncResource {
|
|
126
|
+
return {
|
|
127
|
+
type: 'organization_membership',
|
|
128
|
+
id: String(m.id),
|
|
129
|
+
fields: {
|
|
130
|
+
role: m.role ?? 'basic_member',
|
|
131
|
+
organization_id: m.organization?.id ?? m.orgId ?? null,
|
|
132
|
+
_user_id: m.publicUserData?.userId ?? null,
|
|
133
|
+
created_at: typeof m.createdAt === 'number' ? m.createdAt : null,
|
|
134
|
+
object: 'organization_membership',
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function pullClerkUsers(client: ClerkClient, opts: { limit?: number } = {}): Promise<SyncResource[]> {
|
|
140
|
+
const res = await client.users.getUserList({ limit: opts.limit ?? 100 });
|
|
141
|
+
return listOf<ClerkUser>(res).map(mapUser);
|
|
142
|
+
}
|
|
143
|
+
export async function pullClerkOrganizations(client: ClerkClient, opts: { limit?: number } = {}): Promise<SyncResource[]> {
|
|
144
|
+
const res = await client.organizations.getOrganizationList({ limit: opts.limit ?? 100 });
|
|
145
|
+
return listOf<ClerkOrg>(res).map(mapOrganization);
|
|
146
|
+
}
|
|
147
|
+
/** Pull active sessions from real Clerk (no-op if the client doesn't expose the resource). */
|
|
148
|
+
export async function pullClerkSessions(client: ClerkClient, opts: { limit?: number } = {}): Promise<SyncResource[]> {
|
|
149
|
+
if (!client.sessions?.getSessionList) return [];
|
|
150
|
+
const res = await client.sessions.getSessionList({ limit: opts.limit ?? 100 });
|
|
151
|
+
return listOf<ClerkSession>(res).map(mapSession);
|
|
152
|
+
}
|
|
153
|
+
/** Pull organization memberships from real Clerk for the given orgs (best-effort per org). */
|
|
154
|
+
export async function pullClerkMemberships(client: ClerkClient, orgIds: string[], opts: { limit?: number } = {}): Promise<SyncResource[]> {
|
|
155
|
+
if (!client.organizations.getOrganizationMembershipList) return [];
|
|
156
|
+
const out: SyncResource[] = [];
|
|
157
|
+
for (const organizationId of orgIds) {
|
|
158
|
+
const res = await client.organizations.getOrganizationMembershipList({ organizationId, limit: opts.limit ?? 100 });
|
|
159
|
+
for (const m of listOf<ClerkMembership>(res)) out.push(mapMembership(m));
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Pull from real Clerk (users + organizations, plus sessions + memberships when the
|
|
166
|
+
* injected client exposes them) and fold into the twin (mirror seeding).
|
|
167
|
+
*/
|
|
168
|
+
export async function syncClerkFromReal(
|
|
169
|
+
client: ClerkClient,
|
|
170
|
+
opts: { root?: string; occurredAt: string; limit?: number; includeSessions?: boolean },
|
|
171
|
+
): Promise<{ observed: number; deltasAppended: number }> {
|
|
172
|
+
const limitOpt = opts.limit ? { limit: opts.limit } : {};
|
|
173
|
+
const users = await pullClerkUsers(client, limitOpt);
|
|
174
|
+
const orgs = await pullClerkOrganizations(client, limitOpt);
|
|
175
|
+
const resources = [...users, ...orgs];
|
|
176
|
+
if (opts.includeSessions !== false) {
|
|
177
|
+
const sessions = await pullClerkSessions(client, limitOpt);
|
|
178
|
+
const memberships = await pullClerkMemberships(client, orgs.map((o) => o.id), limitOpt);
|
|
179
|
+
resources.push(...sessions, ...memberships);
|
|
180
|
+
}
|
|
181
|
+
// protocol 2: an observation lands on the head through the kernel's fold — one batch, one instant
|
|
182
|
+
const report = observeResources(SERVICE, resources.map((r) => ({ type: r.type, id: r.id, fields: r.fields })), { ...(opts.root !== undefined ? { root: opts.root } : {}), at: opts.occurredAt, batch: `obs:${SERVICE}:${opts.occurredAt}` });
|
|
183
|
+
return { observed: report.observed, deltasAppended: report.appended };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── PROTOCOL 2: the state system's two adapters, over the kernel's executor ──────────
|
|
187
|
+
/** Clerk's Backend API answers snake_case; the SDK's objects are camelCase, and the maps read the SDK's shape. */
|
|
188
|
+
function camel(v: unknown): unknown {
|
|
189
|
+
if (Array.isArray(v)) return v.map(camel);
|
|
190
|
+
if (v && typeof v === 'object') return Object.fromEntries(Object.entries(v as Record<string, unknown>).map(([k, x]) => [k.replace(/_([a-z])/g, (_m, ch: string) => ch.toUpperCase()), camel(x)]));
|
|
191
|
+
return v;
|
|
192
|
+
}
|
|
193
|
+
/** A ClerkClient over a RemoteExecute: the calls the SDK makes, as wire requests the executor carries. */
|
|
194
|
+
export function clerkClientOver(execute: RemoteExecute): ClerkClient {
|
|
195
|
+
const call = async <T>(method: string, path: string, body?: Record<string, unknown>): Promise<T> => {
|
|
196
|
+
const res = await execute({ method, path, headers: { accept: 'application/json', ...(body ? { 'content-type': 'application/json' } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}) });
|
|
197
|
+
if (res.status >= 300) throw new Error(`clerk: ${method} ${path} answered ${res.status}: ${res.body.slice(0, 200)}`);
|
|
198
|
+
return camel(res.body === '' ? {} : JSON.parse(res.body)) as T;
|
|
199
|
+
};
|
|
200
|
+
const listOf = async <T>(path: string, params?: Record<string, unknown>): Promise<T[]> => { const q = new URLSearchParams(); for (const [k, v] of Object.entries(params ?? {})) if (v !== undefined) q.set(k, String(v)); const r = await call<T[] | { data: T[] }>('GET', `${path}${q.size ? `?${q}` : ''}`); return Array.isArray(r) ? r : (r.data ?? []); };
|
|
201
|
+
return {
|
|
202
|
+
users: { getUserList: (p) => listOf<ClerkUser>('/v1/users', p), createUser: (p) => call<ClerkUser>('POST', '/v1/users', p), updateUser: (id, p) => call<ClerkUser>('PATCH', `/v1/users/${encodeURIComponent(id)}`, p), deleteUser: (id) => call<{ id: string }>('DELETE', `/v1/users/${encodeURIComponent(id)}`) },
|
|
203
|
+
organizations: { getOrganizationList: (p) => listOf<ClerkOrg>('/v1/organizations', p), createOrganization: (p) => call<ClerkOrg>('POST', '/v1/organizations', p), updateOrganization: (id, p) => call<ClerkOrg>('PATCH', `/v1/organizations/${encodeURIComponent(id)}`, p), deleteOrganization: (id) => call<{ id: string }>('DELETE', `/v1/organizations/${encodeURIComponent(id)}`), getOrganizationMembershipList: ({ organizationId, ...p }) => listOf<ClerkMembership>(`/v1/organizations/${encodeURIComponent(organizationId)}/memberships`, p) },
|
|
204
|
+
sessions: { getSessionList: (p) => listOf<ClerkSession>('/v1/sessions', p) },
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/** The refresh adapter: pull Clerk's users, organizations, sessions and memberships through the executor and fold them into the root. */
|
|
208
|
+
export async function syncClerkFromRemote(execute: RemoteExecute, opts: { root?: string; origin?: string; occurredAt?: string } = {}): Promise<{ observed: number; deltasAppended: number }> {
|
|
209
|
+
return syncClerkFromReal(clerkClientOver(execute), { ...(opts.root !== undefined ? { root: opts.root } : {}), occurredAt: opts.occurredAt ?? new Date().toISOString() });
|
|
210
|
+
}
|
|
211
|
+
/** The perform adapter: one entry crosses to Clerk through the executor; an entry that is the twin's own (an attempt, a session, a totp) crosses nothing. */
|
|
212
|
+
export async function performClerkAction(execute: RemoteExecute, action: TwinAction, ctx: PerformContext): Promise<PushOutcome> {
|
|
213
|
+
const op = action.operation ?? `${action.subject.type}.update`;
|
|
214
|
+
if (!PUSHABLE.has(op)) return { externalId: action.subject.id, data: { performed: false, reason: 'the twin\'s own record — nothing at Clerk to write' } };
|
|
215
|
+
const fields = { ...(action.fields ?? {}) };
|
|
216
|
+
if (typeof fields.organization_id === 'string') fields.organization_id = ctx.resolve('organization', fields.organization_id);
|
|
217
|
+
if (typeof fields.created_by === 'string') fields.created_by = ctx.resolve('user', fields.created_by);
|
|
218
|
+
const { externalId } = await pushClerkAction(clerkClientOver(execute), { operation: op, subject: action.subject, fields });
|
|
219
|
+
return { externalId };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── PUSH ──────────────────────────────────────────────────────────────────────
|
|
223
|
+
// The twin operations this connector knows how to push to real Clerk. Anything not here
|
|
224
|
+
// FAILS LOUDLY rather than being silently dropped.
|
|
225
|
+
const PUSHABLE = new Set([
|
|
226
|
+
'user.create', 'user.update', 'user.delete',
|
|
227
|
+
'organization.create', 'organization.update', 'organization.delete',
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
function assertPushable(op: string): void {
|
|
231
|
+
if (!PUSHABLE.has(op)) {
|
|
232
|
+
throw new Error(`clerk push: unsupported operation '${op}' — refusing to silently drop a local write`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Push ONE pending action to REAL Clerk via the injected client. Returns the real
|
|
238
|
+
* external id (the Clerk object id from the response — a fresh id for a create, the
|
|
239
|
+
* subject id otherwise). WRITES TO THE REAL ACCOUNT.
|
|
240
|
+
*/
|
|
241
|
+
export async function pushClerkAction(
|
|
242
|
+
client: ClerkClient,
|
|
243
|
+
action: Pick<TwinAction, 'operation' | 'subject' | 'fields'>,
|
|
244
|
+
): Promise<{ externalId: string }> {
|
|
245
|
+
const op = action.operation ?? `${action.subject.type}.update`;
|
|
246
|
+
assertPushable(op);
|
|
247
|
+
const { type, id } = action.subject;
|
|
248
|
+
const fields = action.fields ?? {};
|
|
249
|
+
let externalId = id;
|
|
250
|
+
if (type === 'user') {
|
|
251
|
+
if (op === 'user.create') externalId = (await client.users.createUser(fields)).id;
|
|
252
|
+
else if (op === 'user.update') externalId = (await client.users.updateUser(id, fields)).id;
|
|
253
|
+
else if (op === 'user.delete') await client.users.deleteUser(id);
|
|
254
|
+
} else if (type === 'organization') {
|
|
255
|
+
if (op === 'organization.create') externalId = (await client.organizations.createOrganization(fields)).id;
|
|
256
|
+
else if (op === 'organization.update') externalId = (await client.organizations.updateOrganization(id, fields)).id;
|
|
257
|
+
else if (op === 'organization.delete') await client.organizations.deleteOrganization(id);
|
|
258
|
+
}
|
|
259
|
+
return { externalId: externalId || id };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Push the twin's PENDING local actions to real Clerk and CONFIRM each (R18). A confirmed
|
|
264
|
+
* action is no longer pending, so a re-push enacts NOTHING. `client` is injected (fake
|
|
265
|
+
* offline / real SDK live). Unpushable ops are skipped (only modeled write ops are pushed).
|
|
266
|
+
*/
|
|
267
|
+
// protocol 2: the pending-actions loop is the kernel's (the head performs each entry through `performClerkAction`);
|
|
268
|
+
// the v1 `pushPendingClerkActions` is gone with the log it read.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { nodeBuiltin } from '@volter/world-core';
|
|
2
|
+
import { worldEgressRefusal } from '@volter/world-core/network-policy';
|
|
3
|
+
// Clerk webhook emission — svix-style signed webhooks on user/session/org events.
|
|
4
|
+
//
|
|
5
|
+
// Real Clerk delivers webhooks through Svix: each delivery carries `svix-id`,
|
|
6
|
+
// `svix-timestamp`, and `svix-signature` headers, where the signature is
|
|
7
|
+
// base64(HMAC-SHA256(secret, `${svix-id}.${svix-timestamp}.${payload}`)) prefixed
|
|
8
|
+
// `v1,`. The signing secret is `whsec_<base64>`; the HMAC key is the base64-decoded
|
|
9
|
+
// portion. An app verifies with the `svix` library; we reproduce the scheme
|
|
10
|
+
// byte-for-byte so a real consumer verifies twin-delivered webhooks unchanged.
|
|
11
|
+
//
|
|
12
|
+
// `node:crypto` is required LAZILY (server-only), like the JWT helpers, so this module
|
|
13
|
+
// is safe to pull into the browser bundle.
|
|
14
|
+
type NodeCrypto = typeof import('node:crypto');
|
|
15
|
+
function nodeCrypto(): NodeCrypto {
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
17
|
+
return nodeBuiltin('node:crypto') as NodeCrypto;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ClerkWebhookEvent = {
|
|
21
|
+
type: string;
|
|
22
|
+
object: 'event';
|
|
23
|
+
timestamp: number;
|
|
24
|
+
data: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
export type ClerkWebhookDelivery = (url: string, body: string, headers: Record<string, string>) => Promise<void> | void;
|
|
27
|
+
|
|
28
|
+
/** Decode a `whsec_<base64>` signing secret to its raw HMAC key bytes. */
|
|
29
|
+
function secretKeyBytes(secret: string): Buffer {
|
|
30
|
+
const b64 = secret.startsWith('whsec_') ? secret.slice('whsec_'.length) : secret;
|
|
31
|
+
return Buffer.from(b64, 'base64');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Compute the Svix `svix-signature` header value for a delivery: `v1,<base64 sig>`
|
|
36
|
+
* over `${id}.${timestamp}.${payload}` keyed by the (base64-decoded) signing secret.
|
|
37
|
+
* Mirrors what `svix`'s Webhook.sign produces, so `wh.verify(payload, headers)` passes.
|
|
38
|
+
*/
|
|
39
|
+
export function computeSvixSignature(id: string, timestamp: number, payload: string, secret: string): string {
|
|
40
|
+
const key = secretKeyBytes(secret);
|
|
41
|
+
const signed = `${id}.${timestamp}.${payload}`;
|
|
42
|
+
const sig = nodeCrypto().createHmac('sha256', key).update(signed, 'utf8').digest('base64');
|
|
43
|
+
return `v1,${sig}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class ClerkWebhookVerificationError extends Error {
|
|
47
|
+
constructor(message: string) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = 'ClerkWebhookVerificationError';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Verify a webhook payload + Svix headers against the signing secret and return the
|
|
55
|
+
* parsed event (mirrors `svix` `Webhook.verify`). Throws on a missing/malformed
|
|
56
|
+
* header, a signature mismatch, or (when `tolerance` is given) a stale timestamp.
|
|
57
|
+
*/
|
|
58
|
+
export function verifyWebhook(payload: string, headers: Record<string, string>, secret: string, opts: { tolerance?: number; now?: number } = {}): ClerkWebhookEvent {
|
|
59
|
+
const lower: Record<string, string> = {};
|
|
60
|
+
for (const [k, v] of Object.entries(headers)) lower[k.toLowerCase()] = v;
|
|
61
|
+
const id = lower['svix-id'];
|
|
62
|
+
const ts = Number(lower['svix-timestamp']);
|
|
63
|
+
const sigHeader = lower['svix-signature'];
|
|
64
|
+
if (!id || !sigHeader || !Number.isFinite(ts)) throw new ClerkWebhookVerificationError('Missing required svix headers');
|
|
65
|
+
if (opts.tolerance !== undefined) {
|
|
66
|
+
const now = opts.now ?? Math.floor(Date.now() / 1000);
|
|
67
|
+
if (Math.abs(now - ts) > opts.tolerance) throw new ClerkWebhookVerificationError('Message timestamp too old');
|
|
68
|
+
}
|
|
69
|
+
const expected = computeSvixSignature(id, ts, payload, secret).slice('v1,'.length);
|
|
70
|
+
const expectedBuf = Buffer.from(expected, 'utf8');
|
|
71
|
+
// The header may carry multiple space-separated `v<n>,<sig>` pairs.
|
|
72
|
+
const matched = sigHeader.split(' ').some((part) => {
|
|
73
|
+
const comma = part.indexOf(',');
|
|
74
|
+
if (comma === -1) return false;
|
|
75
|
+
const sig = part.slice(comma + 1);
|
|
76
|
+
const sigBuf = Buffer.from(sig, 'utf8');
|
|
77
|
+
return sigBuf.length === expectedBuf.length && nodeCrypto().timingSafeEqual(sigBuf, expectedBuf);
|
|
78
|
+
});
|
|
79
|
+
if (!matched) throw new ClerkWebhookVerificationError('No matching signature found');
|
|
80
|
+
return JSON.parse(payload) as ClerkWebhookEvent;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── endpoint registry + event mapping ───────────────────────────────────────────
|
|
84
|
+
/** A webhook endpoint a World holds in its tree: where to POST, what to sign with, which event types it filters to. */
|
|
85
|
+
export type ClerkWebhookTarget = { url: string; secret: string; filterTypes?: string[] };
|
|
86
|
+
// URLs a HOST registered directly (registerClerkWebhook). A World's own endpoints are not here:
|
|
87
|
+
// POST /v1/webhooks/endpoints writes the webhook_endpoint to the tree, and every emit reads the tree
|
|
88
|
+
// (the caller passes them as `endpoints`), so delivery survives a restart and belongs to one World.
|
|
89
|
+
const registry: Array<{ url: string; secret: string }> = [];
|
|
90
|
+
export function registerClerkWebhook(url: string, secret = 'whsec_dGVzdHNlY3JldHRlc3RzZWNyZXQ='): void {
|
|
91
|
+
if (!registry.some((r) => r.url === url)) registry.push({ url, secret });
|
|
92
|
+
}
|
|
93
|
+
export function clearClerkWebhooks(): void {
|
|
94
|
+
registry.length = 0;
|
|
95
|
+
}
|
|
96
|
+
export function listClerkWebhooks(): Array<{ url: string; secret: string }> {
|
|
97
|
+
return registry.map((r) => ({ ...r }));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// twin write operation → Clerk webhook event type (the ones an app's webhooks care about).
|
|
101
|
+
export function eventTypeFor(operation: string): string | null {
|
|
102
|
+
switch (operation) {
|
|
103
|
+
case 'user.create': return 'user.created';
|
|
104
|
+
case 'user.update': return 'user.updated';
|
|
105
|
+
case 'user.delete': return 'user.deleted';
|
|
106
|
+
case 'session.create': return 'session.created';
|
|
107
|
+
case 'session.revoke': return 'session.revoked';
|
|
108
|
+
case 'session.end': return 'session.ended';
|
|
109
|
+
case 'organization.create': return 'organization.created';
|
|
110
|
+
case 'organization.update': return 'organization.updated';
|
|
111
|
+
case 'organization.delete': return 'organization.deleted';
|
|
112
|
+
case 'organization_membership.create': return 'organizationMembership.created';
|
|
113
|
+
case 'organization_membership.update': return 'organizationMembership.updated';
|
|
114
|
+
case 'organization_membership.delete': return 'organizationMembership.deleted';
|
|
115
|
+
case 'organization_invitation.create': return 'organizationInvitation.created';
|
|
116
|
+
case 'email.create': return 'email.created';
|
|
117
|
+
default: return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A stable svix message id for one delivery: FNV-1a over the destination, the instant and the body — no process counter (protocol 2). */
|
|
122
|
+
function svixMessageId(url: string, timestamp: number, body: string): string { let h = 0x811c9dc5; const s = `${url}|${timestamp}|${body}`; for (let i = 0; i < s.length; i += 1) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; } return `msg_twin_${h.toString(16).padStart(8, '0')}`; }
|
|
123
|
+
const httpDelivery: ClerkWebhookDelivery = async (url, body, headers) => {
|
|
124
|
+
if (worldEgressRefusal(url) !== null) return; // the World's egress rule: refused like an unreachable endpoint
|
|
125
|
+
try {
|
|
126
|
+
await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body });
|
|
127
|
+
} catch {
|
|
128
|
+
/* fire-and-forget, like the real vendor */
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Emit a Clerk webhook for a twin write to the World's endpoints (`endpoints`, read from its
|
|
134
|
+
* tree; an endpoint with `filter_types` receives only those types) and host-registered URLs. Builds the Svix
|
|
135
|
+
* envelope + signed headers and delivers it (injected delivery: fake in tests, HTTP
|
|
136
|
+
* POST live). Returns the delivered bodies+headers (for assertions); no-op when nothing
|
|
137
|
+
* is registered or the operation has no mapped event type.
|
|
138
|
+
*/
|
|
139
|
+
export async function emitClerkEvent(
|
|
140
|
+
operation: string,
|
|
141
|
+
data: Record<string, unknown>,
|
|
142
|
+
opts: { occurredAt?: string; deliver?: ClerkWebhookDelivery; endpoints?: ClerkWebhookTarget[] } = {},
|
|
143
|
+
): Promise<Array<{ url: string; body: string; headers: Record<string, string>; secret: string }>> {
|
|
144
|
+
const type = eventTypeFor(operation);
|
|
145
|
+
// the World's own endpoints (its tree is the record), then any URL a host registered directly
|
|
146
|
+
const targets: ClerkWebhookTarget[] = [...(opts.endpoints ?? [])];
|
|
147
|
+
for (const r of registry) if (!targets.some((t) => t.url === r.url)) targets.push({ ...r });
|
|
148
|
+
if (!type || targets.length === 0) return [];
|
|
149
|
+
const deliver = opts.deliver ?? httpDelivery;
|
|
150
|
+
const timestamp = Math.floor((opts.occurredAt ? Date.parse(opts.occurredAt) : Date.now()) / 1000) || 0;
|
|
151
|
+
const event: ClerkWebhookEvent = { type, object: 'event', timestamp, data };
|
|
152
|
+
const body = JSON.stringify(event);
|
|
153
|
+
const out: Array<{ url: string; body: string; headers: Record<string, string>; secret: string }> = [];
|
|
154
|
+
for (const { url, secret, filterTypes } of targets) {
|
|
155
|
+
if (filterTypes && filterTypes.length > 0 && !filterTypes.includes(type)) continue;
|
|
156
|
+
const svixId = svixMessageId(url, timestamp, body);
|
|
157
|
+
const headers = {
|
|
158
|
+
'svix-id': svixId,
|
|
159
|
+
'svix-timestamp': String(timestamp),
|
|
160
|
+
'svix-signature': computeSvixSignature(svixId, timestamp, body, secret),
|
|
161
|
+
};
|
|
162
|
+
await deliver(url, body, headers);
|
|
163
|
+
out.push({ url, body, headers, secret });
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|