@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,1132 @@
|
|
|
1
|
+
// Clerk capability manifest — the EXPECTED REAL-PRODUCT SURFACE (the target), authored
|
|
2
|
+
// top-down from what the Clerk Backend API actually does — NOT from what this twin has
|
|
3
|
+
// built. This is the honest denominator: most entries start as `todo` and coverage reads
|
|
4
|
+
// LOW until the twin truly reaches 100% of Clerk. `verify()` (required to count as done)
|
|
5
|
+
// is ground truth; `expected: 'done'` only on capabilities we genuinely claim, so a broken
|
|
6
|
+
// one shows as a regression. Grow this toward Clerk's *full* surface every cycle.
|
|
7
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { createElement } from 'react';
|
|
11
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
12
|
+
import { checkCapabilities, uiDataCoupled, verifyBoundary, isInfrastructureError, harnessError } from '@volter/world-tooling';
|
|
13
|
+
import { buildJwks, networklessVerifyToken, signJwt, verifyJwtHs256, verifyJwtWithJwks } from "./clerk-jwt.js";
|
|
14
|
+
import { clearClerkWebhooks, computeSvixSignature, emitClerkEvent, registerClerkWebhook, verifyWebhook } from "./clerk-events.js";
|
|
15
|
+
import { handleClerkTwinRequest } from "./clerk-twin.js";
|
|
16
|
+
import { SECTIONS, CreateUserForm, ImpersonateButton, NestedLines, StatusPill } from "../client/clerk-mirror.js";
|
|
17
|
+
import { flattenClerkValue, statusTone } from "./clerk-mirror-ui.js";
|
|
18
|
+
/** Run a sequence of real Clerk requests against an isolated root; return all responses. */
|
|
19
|
+
async function withRoot(steps) {
|
|
20
|
+
const root = mkdtempSync(join(tmpdir(), 'clerk-cap-'));
|
|
21
|
+
const h = (s) => handleClerkTwinRequest({ method: s.m, path: s.p, body: s.b === undefined ? undefined : JSON.stringify(s.b), root, ...(s.at ? { occurredAt: s.at } : {}) });
|
|
22
|
+
try {
|
|
23
|
+
return await verifyBoundary('clerk.withRoot', () => steps(h));
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
rmSync(root, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const ok = (r) => r.status >= 200 && r.status < 300;
|
|
30
|
+
const id = (r) => r.body?.id;
|
|
31
|
+
const field = (r, k) => r.body?.[k];
|
|
32
|
+
// A user with one email — the common fixture.
|
|
33
|
+
const mkUser = (email = 'ada@twin.test') => ({ email_address: [email], first_name: 'Ada', last_name: 'Lovelace' });
|
|
34
|
+
// The mirror's OWN per-section title()/subtitle() row-shaping closures (imported from the
|
|
35
|
+
// client, exported as `SECTIONS`) — used by the UI verifies below to shape fetched rows
|
|
36
|
+
// EXACTLY as the dashboard's row list would, instead of re-implementing the shaping.
|
|
37
|
+
const section = (key) => SECTIONS.find((s) => s.key === key);
|
|
38
|
+
// ── shorthands (mirror the stripe/linear manifests) ──
|
|
39
|
+
const done = (id, area, title, dimension, tier, verify) => ({ id, area, title, dimension, tier, expected: 'done', verify });
|
|
40
|
+
const todo = (id, area, title, dimension, tier) => ({ id, area, title, dimension, tier, expected: 'todo' });
|
|
41
|
+
export const CLERK_CAPABILITIES = [
|
|
42
|
+
// ── Users ─────────────────────────────────────────────────────────────────────
|
|
43
|
+
done('clerk.users.create', 'users', 'Users: create with email + name', 'api', 'core', () => withRoot(async (h) => {
|
|
44
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
45
|
+
return ok(u) && field(u, 'object') === 'user' && Array.isArray(field(u, 'email_addresses')) && field(u, 'email_addresses')[0].email_address === 'ada@twin.test';
|
|
46
|
+
})),
|
|
47
|
+
done('clerk.users.retrieve', 'users', 'Users: retrieve by id', 'api', 'core', () => withRoot(async (h) => {
|
|
48
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
49
|
+
const g = await h({ m: 'GET', p: `/v1/users/${id(u)}` });
|
|
50
|
+
const missing = await h({ m: 'GET', p: '/v1/users/user_nope' });
|
|
51
|
+
return ok(g) && id(g) === id(u) && missing.status === 404;
|
|
52
|
+
})),
|
|
53
|
+
done('clerk.users.list', 'users', 'Users: list (bare array)', 'api', 'core', () => withRoot(async (h) => {
|
|
54
|
+
await h({ m: 'POST', p: '/v1/users', b: mkUser('a@twin.test') });
|
|
55
|
+
await h({ m: 'POST', p: '/v1/users', b: mkUser('b@twin.test') });
|
|
56
|
+
const l = await h({ m: 'GET', p: '/v1/users' });
|
|
57
|
+
return ok(l) && Array.isArray(l.body) && l.body.length === 2;
|
|
58
|
+
})),
|
|
59
|
+
done('clerk.users.update', 'users', 'Users: update name + metadata', 'api', 'core', () => withRoot(async (h) => {
|
|
60
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
61
|
+
const upd = await h({ m: 'PATCH', p: `/v1/users/${id(u)}`, b: { first_name: 'Grace', public_metadata: { plan: 'pro' } } });
|
|
62
|
+
return ok(upd) && field(upd, 'first_name') === 'Grace' && field(upd, 'public_metadata').plan === 'pro';
|
|
63
|
+
})),
|
|
64
|
+
done('clerk.users.delete', 'users', 'Users: delete (then 404 + dropped from list)', 'api', 'core', () => withRoot(async (h) => {
|
|
65
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
66
|
+
const del = await h({ m: 'DELETE', p: `/v1/users/${id(u)}` });
|
|
67
|
+
const gone = await h({ m: 'GET', p: `/v1/users/${id(u)}` });
|
|
68
|
+
const l = await h({ m: 'GET', p: '/v1/users' });
|
|
69
|
+
return ok(del) && field(del, 'deleted') === true && gone.status === 404 && l.body.length === 0;
|
|
70
|
+
})),
|
|
71
|
+
done('clerk.users.ban', 'users', 'Users: ban / unban', 'api', 'core', () => withRoot(async (h) => {
|
|
72
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
73
|
+
const banned = await h({ m: 'POST', p: `/v1/users/${id(u)}/ban` });
|
|
74
|
+
if (!ok(banned) || field(banned, 'banned') !== true)
|
|
75
|
+
return false;
|
|
76
|
+
const unbanned = await h({ m: 'POST', p: `/v1/users/${id(u)}/unban` });
|
|
77
|
+
return ok(unbanned) && field(unbanned, 'banned') === false;
|
|
78
|
+
})),
|
|
79
|
+
done('clerk.users.lock', 'users', 'Users: lock / unlock', 'api', 'common', () => withRoot(async (h) => {
|
|
80
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
81
|
+
const locked = await h({ m: 'POST', p: `/v1/users/${id(u)}/lock` });
|
|
82
|
+
const unlocked = await h({ m: 'POST', p: `/v1/users/${id(u)}/unlock` });
|
|
83
|
+
return ok(locked) && field(locked, 'locked') === true && ok(unlocked) && field(unlocked, 'locked') === false;
|
|
84
|
+
})),
|
|
85
|
+
done('clerk.users.metadata', 'users', 'Users: merge metadata (PATCH /metadata)', 'api', 'common', () => withRoot(async (h) => {
|
|
86
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
87
|
+
await h({ m: 'PATCH', p: `/v1/users/${id(u)}/metadata`, b: { public_metadata: { a: 1 } } });
|
|
88
|
+
const merged = await h({ m: 'PATCH', p: `/v1/users/${id(u)}/metadata`, b: { public_metadata: { b: 2 } } });
|
|
89
|
+
const pm = field(merged, 'public_metadata');
|
|
90
|
+
return ok(merged) && pm.a === 1 && pm.b === 2;
|
|
91
|
+
})),
|
|
92
|
+
done('clerk.users.list_filter', 'users', 'Users: list filtered by email_address / username / external_id', 'api', 'common', () => withRoot(async (h) => {
|
|
93
|
+
await h({ m: 'POST', p: '/v1/users', b: { email_address: ['match@twin.test'], username: 'matcher' } });
|
|
94
|
+
await h({ m: 'POST', p: '/v1/users', b: { email_address: ['other@twin.test'] } });
|
|
95
|
+
const byEmail = await h({ m: 'GET', p: '/v1/users?email_address=match@twin.test' });
|
|
96
|
+
const byUser = await h({ m: 'GET', p: '/v1/users?username=matcher' });
|
|
97
|
+
return ok(byEmail) && byEmail.body.length === 1 && ok(byUser) && byUser.body.length === 1;
|
|
98
|
+
})),
|
|
99
|
+
done('clerk.users.validation', 'users', 'Users: rejects empty identifiers + duplicate email', 'api', 'core', () => withRoot(async (h) => {
|
|
100
|
+
const empty = await h({ m: 'POST', p: '/v1/users', b: {} });
|
|
101
|
+
await h({ m: 'POST', p: '/v1/users', b: mkUser('dup@twin.test') });
|
|
102
|
+
const dup = await h({ m: 'POST', p: '/v1/users', b: mkUser('dup@twin.test') });
|
|
103
|
+
return empty.status === 422 && dup.status === 422;
|
|
104
|
+
})),
|
|
105
|
+
done('clerk.users.count', 'users', 'Users: count endpoint', 'api', 'common', () => withRoot(async (h) => {
|
|
106
|
+
await h({ m: 'POST', p: '/v1/users', b: mkUser('c1@twin.test') });
|
|
107
|
+
await h({ m: 'POST', p: '/v1/users', b: mkUser('c2@twin.test') });
|
|
108
|
+
const c = await h({ m: 'GET', p: '/v1/users/count' });
|
|
109
|
+
return ok(c) && field(c, 'total_count') === 2;
|
|
110
|
+
})),
|
|
111
|
+
done('clerk.users.search_query', 'users', 'Users: full-text query param across name/email', 'api', 'common', () => withRoot(async (h) => {
|
|
112
|
+
await h({ m: 'POST', p: '/v1/users', b: { email_address: ['grace.hopper@twin.test'], first_name: 'Grace', last_name: 'Hopper' } });
|
|
113
|
+
await h({ m: 'POST', p: '/v1/users', b: mkUser('someone.else@twin.test') });
|
|
114
|
+
const byName = await h({ m: 'GET', p: '/v1/users?query=hopper' });
|
|
115
|
+
const byEmail = await h({ m: 'GET', p: '/v1/users?query=grace.hopper' });
|
|
116
|
+
const noMatch = await h({ m: 'GET', p: '/v1/users?query=zzz_nobody' });
|
|
117
|
+
return ok(byName) && byName.body.length === 1 && byEmail.body.length === 1 && noMatch.body.length === 0;
|
|
118
|
+
})),
|
|
119
|
+
done('clerk.users.oauth_accounts', 'users', 'Users: external OAuth account management', 'api', 'common', () => withRoot(async (h) => {
|
|
120
|
+
// seed a user that already has a linked external (OAuth) account
|
|
121
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: { ...mkUser(), external_accounts: [{ id: 'eac_twin_1', object: 'external_account', provider: 'oauth_google', email_address: 'ada@twin.test', approved_scopes: ['email'], _access_token: 'ya29.token' }] } });
|
|
122
|
+
const tokens = await h({ m: 'GET', p: `/v1/users/${id(u)}/oauth_access_tokens/google` });
|
|
123
|
+
const tokenList = field(tokens, 'data');
|
|
124
|
+
if (!ok(tokens) || tokenList.length !== 1 || tokenList[0].token !== 'ya29.token')
|
|
125
|
+
return false;
|
|
126
|
+
const del = await h({ m: 'DELETE', p: `/v1/users/${id(u)}/external_accounts/eac_twin_1` });
|
|
127
|
+
const after = await h({ m: 'GET', p: `/v1/users/${id(u)}` });
|
|
128
|
+
const missing = await h({ m: 'DELETE', p: `/v1/users/${id(u)}/external_accounts/eac_nope` });
|
|
129
|
+
return ok(del) && field(del, 'deleted') === true && field(after, 'external_accounts').length === 0 && missing.status === 404;
|
|
130
|
+
})),
|
|
131
|
+
done('clerk.users.totp', 'users', 'Users: TOTP / 2FA secret + backup codes', 'api', 'niche', () => withRoot(async (h) => {
|
|
132
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
133
|
+
const totp = await h({ m: 'POST', p: `/v1/users/${id(u)}/totp` });
|
|
134
|
+
const secret = field(totp, 'secret');
|
|
135
|
+
// a real base32 secret + a valid otpauth uri carrying it; the user flips totp_enabled
|
|
136
|
+
if (!ok(totp) || field(totp, 'object') !== 'totp' || !/^[A-Z2-7]+$/.test(secret) || !String(field(totp, 'uri')).includes(`secret=${secret}`))
|
|
137
|
+
return false;
|
|
138
|
+
const after = await h({ m: 'GET', p: `/v1/users/${id(u)}` });
|
|
139
|
+
if (field(after, 'totp_enabled') !== true || field(after, 'two_factor_enabled') !== true)
|
|
140
|
+
return false;
|
|
141
|
+
const codes = await h({ m: 'POST', p: `/v1/users/${id(u)}/backup_codes` });
|
|
142
|
+
const list = field(codes, 'codes');
|
|
143
|
+
if (!ok(codes) || !Array.isArray(list) || list.length !== 10)
|
|
144
|
+
return false;
|
|
145
|
+
const del = await h({ m: 'DELETE', p: `/v1/users/${id(u)}/totp` });
|
|
146
|
+
const afterDel = await h({ m: 'GET', p: `/v1/users/${id(u)}` });
|
|
147
|
+
const missing = await h({ m: 'POST', p: '/v1/users/user_nope/totp' });
|
|
148
|
+
return ok(del) && field(del, 'deleted') === true && field(afterDel, 'totp_enabled') === false && missing.status === 404;
|
|
149
|
+
})),
|
|
150
|
+
done('clerk.users.password', 'users', 'Users: set / verify password (real scrypt hash, never plaintext)', 'api', 'common', () => withRoot(async (h) => {
|
|
151
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: { ...mkUser(), password: 's3cret-pw' } });
|
|
152
|
+
if (!ok(u) || field(u, 'password_enabled') !== true || 'password' in u.body)
|
|
153
|
+
return false;
|
|
154
|
+
const good = await h({ m: 'POST', p: `/v1/users/${id(u)}/verify_password`, b: { password: 's3cret-pw' } });
|
|
155
|
+
const bad = await h({ m: 'POST', p: `/v1/users/${id(u)}/verify_password`, b: { password: 'wrong' } });
|
|
156
|
+
// a user without a password rejects verification too
|
|
157
|
+
const u2 = await h({ m: 'POST', p: '/v1/users', b: mkUser('nopass@twin.test') });
|
|
158
|
+
const noPw = await h({ m: 'POST', p: `/v1/users/${id(u2)}/verify_password`, b: { password: 'x' } });
|
|
159
|
+
return ok(good) && field(good, 'verified') === true && bad.status === 422 && noPw.status === 422;
|
|
160
|
+
})),
|
|
161
|
+
done('clerk.users.email_verification', 'users', 'Users: add + verify email address flow', 'api', 'common', () => withRoot(async (h) => {
|
|
162
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
163
|
+
const add = await h({ m: 'POST', p: '/v1/email_addresses', b: { user_id: id(u), email_address: 'second@twin.test' } });
|
|
164
|
+
if (!ok(add) || field(add, 'object') !== 'email_address' || field(add, 'verification') !== null)
|
|
165
|
+
return false;
|
|
166
|
+
const eaid = id(add);
|
|
167
|
+
const ver = await h({ m: 'PATCH', p: `/v1/email_addresses/${eaid}`, b: { verified: true } });
|
|
168
|
+
const get = await h({ m: 'GET', p: `/v1/email_addresses/${eaid}` });
|
|
169
|
+
const del = await h({ m: 'DELETE', p: `/v1/email_addresses/${eaid}` });
|
|
170
|
+
const gone = await h({ m: 'GET', p: `/v1/email_addresses/${eaid}` });
|
|
171
|
+
return ok(ver) && field(ver, 'verification').status === 'verified' && field(get, 'verification').status === 'verified' && ok(del) && gone.status === 404;
|
|
172
|
+
})),
|
|
173
|
+
done('clerk.users.phone_verification', 'users', 'Users: add + verify phone number flow', 'api', 'common', () => withRoot(async (h) => {
|
|
174
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
175
|
+
const add = await h({ m: 'POST', p: '/v1/phone_numbers', b: { user_id: id(u), phone_number: '+15551230000' } });
|
|
176
|
+
if (!ok(add) || field(add, 'object') !== 'phone_number')
|
|
177
|
+
return false;
|
|
178
|
+
const ver = await h({ m: 'PATCH', p: `/v1/phone_numbers/${id(add)}`, b: { verified: true } });
|
|
179
|
+
const onUser = await h({ m: 'GET', p: `/v1/users/${id(u)}` });
|
|
180
|
+
const phones = field(onUser, 'phone_numbers');
|
|
181
|
+
return ok(ver) && field(ver, 'verification').status === 'verified' && phones.some((p) => p.phone_number === '+15551230000');
|
|
182
|
+
})),
|
|
183
|
+
done('clerk.users.profile_image', 'users', 'Users: upload / delete profile image', 'api', 'niche', () => withRoot(async (h) => {
|
|
184
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
185
|
+
if (field(u, 'has_image') !== false)
|
|
186
|
+
return false;
|
|
187
|
+
const set = await h({ m: 'POST', p: `/v1/users/${id(u)}/profile_image`, b: { file: 'https://cdn.test/me.png' } });
|
|
188
|
+
if (!ok(set) || field(set, 'has_image') !== true || field(set, 'image_url') !== 'https://cdn.test/me.png')
|
|
189
|
+
return false;
|
|
190
|
+
const del = await h({ m: 'DELETE', p: `/v1/users/${id(u)}/profile_image` });
|
|
191
|
+
const missing = await h({ m: 'DELETE', p: '/v1/users/user_nope/profile_image' });
|
|
192
|
+
return ok(del) && field(del, 'has_image') === false && field(del, 'image_url') === '' && missing.status === 404;
|
|
193
|
+
})),
|
|
194
|
+
done('clerk.users.web3_wallets', 'users', 'Users: web3 wallet identifiers (add / verify / delete)', 'api', 'niche', () => withRoot(async (h) => {
|
|
195
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
196
|
+
const addr = '0xAbC0000000000000000000000000000000000000';
|
|
197
|
+
const w = await h({ m: 'POST', p: '/v1/web3_wallets', b: { user_id: id(u), web3_wallet: addr } });
|
|
198
|
+
if (!ok(w) || field(w, 'object') !== 'web3_wallet' || field(w, 'web3_wallet') !== addr || field(w, 'verification') !== null)
|
|
199
|
+
return false;
|
|
200
|
+
const ver = await h({ m: 'PATCH', p: `/v1/web3_wallets/${id(w)}`, b: { verified: true } });
|
|
201
|
+
const onUser = await h({ m: 'GET', p: `/v1/users/${id(u)}` });
|
|
202
|
+
const wallets = field(onUser, 'web3_wallets');
|
|
203
|
+
if (!ok(ver) || field(ver, 'verification').status !== 'verified' || !wallets.some((x) => x.web3_wallet === addr))
|
|
204
|
+
return false;
|
|
205
|
+
const del = await h({ m: 'DELETE', p: `/v1/web3_wallets/${id(w)}` });
|
|
206
|
+
const missingUser = await h({ m: 'POST', p: '/v1/web3_wallets', b: { user_id: 'user_nope', web3_wallet: addr } });
|
|
207
|
+
return ok(del) && field(del, 'deleted') === true && missingUser.status === 404;
|
|
208
|
+
})),
|
|
209
|
+
// ── Sessions ────────────────────────────────────────────────────────────────────
|
|
210
|
+
done('clerk.sessions.create', 'sessions', 'Sessions: create for a user', 'api', 'core', () => withRoot(async (h) => {
|
|
211
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
212
|
+
const s = await h({ m: 'POST', p: '/v1/sessions', b: { user_id: id(u) } });
|
|
213
|
+
return ok(s) && field(s, 'object') === 'session' && field(s, 'status') === 'active' && field(s, 'user_id') === id(u);
|
|
214
|
+
})),
|
|
215
|
+
done('clerk.sessions.retrieve_list', 'sessions', 'Sessions: retrieve + list filtered by user', 'api', 'core', () => withRoot(async (h) => {
|
|
216
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
217
|
+
const s = await h({ m: 'POST', p: '/v1/sessions', b: { user_id: id(u) } });
|
|
218
|
+
const g = await h({ m: 'GET', p: `/v1/sessions/${id(s)}` });
|
|
219
|
+
const l = await h({ m: 'GET', p: `/v1/sessions?user_id=${id(u)}` });
|
|
220
|
+
return ok(g) && id(g) === id(s) && ok(l) && l.body.length === 1;
|
|
221
|
+
})),
|
|
222
|
+
done('clerk.sessions.revoke', 'sessions', 'Sessions: revoke → status revoked', 'api', 'core', () => withRoot(async (h) => {
|
|
223
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
224
|
+
const s = await h({ m: 'POST', p: '/v1/sessions', b: { user_id: id(u) } });
|
|
225
|
+
const rev = await h({ m: 'POST', p: `/v1/sessions/${id(s)}/revoke` });
|
|
226
|
+
return ok(rev) && field(rev, 'status') === 'revoked';
|
|
227
|
+
})),
|
|
228
|
+
done('clerk.sessions.create_validation', 'sessions', 'Sessions: create rejects missing/unknown user', 'api', 'core', () => withRoot(async (h) => {
|
|
229
|
+
const noUser = await h({ m: 'POST', p: '/v1/sessions', b: {} });
|
|
230
|
+
const badUser = await h({ m: 'POST', p: '/v1/sessions', b: { user_id: 'user_nope' } });
|
|
231
|
+
return noUser.status === 422 && badUser.status === 404;
|
|
232
|
+
})),
|
|
233
|
+
done('clerk.sessions.verify', 'sessions', 'Sessions: verify a signed token round-trip', 'api', 'core', () => withRoot(async (h) => {
|
|
234
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
235
|
+
const s = await h({ m: 'POST', p: '/v1/sessions', b: { user_id: id(u) } });
|
|
236
|
+
const tok = await h({ m: 'POST', p: `/v1/sessions/${id(s)}/tokens` });
|
|
237
|
+
const jwt = field(tok, 'jwt');
|
|
238
|
+
const verified = await h({ m: 'POST', p: `/v1/sessions/${id(s)}/verify`, b: { token: jwt } });
|
|
239
|
+
const badToken = await h({ m: 'POST', p: `/v1/sessions/${id(s)}/verify`, b: { token: 'not.a.jwt' } });
|
|
240
|
+
return ok(verified) && badToken.status === 400;
|
|
241
|
+
})),
|
|
242
|
+
done('clerk.sessions.touch', 'sessions', 'Sessions: touch (refresh last_active_at)', 'api', 'niche', () => withRoot(async (h) => {
|
|
243
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser(), at: '2026-01-01T00:00:00.000Z' });
|
|
244
|
+
const s = await h({ m: 'POST', p: '/v1/sessions', b: { user_id: id(u) }, at: '2026-01-01T00:00:00.000Z' });
|
|
245
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Touch Co' } });
|
|
246
|
+
const before = field(await h({ m: 'GET', p: `/v1/sessions/${id(s)}` }), 'last_active_at');
|
|
247
|
+
// touch with a later occurredAt bumps last_active_at and records last active org
|
|
248
|
+
const touched = await h({ m: 'POST', p: `/v1/sessions/${id(s)}/touch`, b: { active_organization_id: id(o) }, at: '2026-01-01T01:00:00.000Z' });
|
|
249
|
+
if (!ok(touched) || field(touched, 'last_active_at') <= before || field(touched, 'last_active_organization_id') !== id(o))
|
|
250
|
+
return false;
|
|
251
|
+
// touching a revoked session is rejected; an unknown session 404s
|
|
252
|
+
await h({ m: 'POST', p: `/v1/sessions/${id(s)}/revoke` });
|
|
253
|
+
const onRevoked = await h({ m: 'POST', p: `/v1/sessions/${id(s)}/touch` });
|
|
254
|
+
const missing = await h({ m: 'POST', p: '/v1/sessions/sess_nope/touch' });
|
|
255
|
+
return onRevoked.status === 400 && missing.status === 404;
|
|
256
|
+
})),
|
|
257
|
+
done('clerk.sessions.revoke_all_for_user', 'sessions', 'Sessions: revoke all of a user’s sessions (list → revoke each, the SDK pattern)', 'api', 'common', () => withRoot(async (h) => {
|
|
258
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
259
|
+
const other = await h({ m: 'POST', p: '/v1/users', b: mkUser('other@twin.test') });
|
|
260
|
+
const s1 = await h({ m: 'POST', p: '/v1/sessions', b: { user_id: id(u) } });
|
|
261
|
+
const s2 = await h({ m: 'POST', p: '/v1/sessions', b: { user_id: id(u) } });
|
|
262
|
+
const sOther = await h({ m: 'POST', p: '/v1/sessions', b: { user_id: id(other) } });
|
|
263
|
+
// The real backend has no bulk endpoint: list the user's active sessions, revoke each.
|
|
264
|
+
const list = await h({ m: 'GET', p: `/v1/sessions?user_id=${id(u)}&status=active` });
|
|
265
|
+
const active = list.body;
|
|
266
|
+
if (active.length !== 2)
|
|
267
|
+
return false;
|
|
268
|
+
for (const s of active)
|
|
269
|
+
await h({ m: 'POST', p: `/v1/sessions/${s.id}/revoke` });
|
|
270
|
+
const afterU = await h({ m: 'GET', p: `/v1/sessions?user_id=${id(u)}&status=active` });
|
|
271
|
+
const afterOther = await h({ m: 'GET', p: `/v1/sessions/${id(sOther)}` });
|
|
272
|
+
// the other user's session is untouched
|
|
273
|
+
return afterU.body.length === 0
|
|
274
|
+
&& field(await h({ m: 'GET', p: `/v1/sessions/${id(s1)}` }), 'status') === 'revoked'
|
|
275
|
+
&& field(await h({ m: 'GET', p: `/v1/sessions/${id(s2)}` }), 'status') === 'revoked'
|
|
276
|
+
&& field(afterOther, 'status') === 'active';
|
|
277
|
+
})),
|
|
278
|
+
// ── JWT / JWKS (the highest-value capability) ─────────────────────────────────────
|
|
279
|
+
done('clerk.jwt.session_token', 'jwt', 'Session token: issue a REAL signed RS256 JWT', 'api', 'core', () => withRoot(async (h) => {
|
|
280
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
281
|
+
const s = await h({ m: 'POST', p: '/v1/sessions', b: { user_id: id(u) } });
|
|
282
|
+
const tok = await h({ m: 'POST', p: `/v1/sessions/${id(s)}/tokens` });
|
|
283
|
+
const jwt = field(tok, 'jwt');
|
|
284
|
+
return ok(tok) && field(tok, 'object') === 'token' && typeof jwt === 'string' && jwt.split('.').length === 3;
|
|
285
|
+
})),
|
|
286
|
+
done('clerk.jwt.jwks_endpoint', 'jwt', 'JWKS: serve a real JWKS document at the well-known path', 'api', 'core', () => withRoot(async (h) => {
|
|
287
|
+
const jwks = await h({ m: 'GET', p: '/.well-known/jwks.json' });
|
|
288
|
+
const keys = field(jwks, 'keys');
|
|
289
|
+
return ok(jwks) && Array.isArray(keys) && keys.length === 1 && keys[0].kty === 'RSA' && keys[0].alg === 'RS256' && typeof keys[0].n === 'string';
|
|
290
|
+
})),
|
|
291
|
+
done('clerk.jwt.verify_against_jwks', 'jwt', 'JWT/JWKS: a twin-issued token verifies against the served JWKS (claims round-trip)', 'api', 'core', () => withRoot(async (h) => {
|
|
292
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
293
|
+
const s = await h({ m: 'POST', p: '/v1/sessions', b: { user_id: id(u) } });
|
|
294
|
+
const tok = await h({ m: 'POST', p: `/v1/sessions/${id(s)}/tokens` });
|
|
295
|
+
const jwt = field(tok, 'jwt');
|
|
296
|
+
const jwksResp = await h({ m: 'GET', p: '/.well-known/jwks.json' });
|
|
297
|
+
const result = verifyJwtWithJwks(jwt, jwksResp.body);
|
|
298
|
+
// claims must round-trip: sub == user id, sid == session id, plus standard iss
|
|
299
|
+
return result.valid === true && result.payload?.sub === id(u) && result.payload?.sid === id(s) && typeof result.payload?.iss === 'string';
|
|
300
|
+
})),
|
|
301
|
+
done('clerk.jwt.tamper_rejected', 'jwt', 'JWT/JWKS: a tampered or expired token fails verification', 'api', 'core', () => withRoot(async () => {
|
|
302
|
+
const root = mkdtempSync(join(tmpdir(), 'clerk-jwt-'));
|
|
303
|
+
try {
|
|
304
|
+
const jwt = await signJwt({ sub: 'user_x' }, { root, now: 1_000 });
|
|
305
|
+
const jwks = await buildJwks(root);
|
|
306
|
+
const tampered = `${jwt.slice(0, -4)}AAAA`;
|
|
307
|
+
const expired = verifyJwtWithJwks(jwt, jwks, { now: 10_000 }); // long past exp
|
|
308
|
+
const bad = verifyJwtWithJwks(tampered, jwks, { now: 1_001 });
|
|
309
|
+
const good = verifyJwtWithJwks(jwt, jwks, { now: 1_001 });
|
|
310
|
+
return good.valid === true && bad.valid === false && expired.valid === false;
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
rmSync(root, { recursive: true, force: true });
|
|
314
|
+
}
|
|
315
|
+
})),
|
|
316
|
+
done('clerk.jwt.template_token', 'jwt', 'JWT template: issue a token with the template’s custom claims', 'api', 'common', () => withRoot(async (h) => {
|
|
317
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
318
|
+
const s = await h({ m: 'POST', p: '/v1/sessions', b: { user_id: id(u) } });
|
|
319
|
+
await h({ m: 'POST', p: '/v1/jwt_templates', b: { name: 'supabase', claims: { role: 'authenticated' } } });
|
|
320
|
+
const tok = await h({ m: 'POST', p: `/v1/sessions/${id(s)}/tokens/supabase` });
|
|
321
|
+
const jwt = field(tok, 'jwt');
|
|
322
|
+
const jwks = await h({ m: 'GET', p: '/.well-known/jwks.json' });
|
|
323
|
+
const result = verifyJwtWithJwks(jwt, jwks.body);
|
|
324
|
+
return ok(tok) && result.valid === true && result.payload?.role === 'authenticated';
|
|
325
|
+
})),
|
|
326
|
+
done('clerk.jwt.stable_kid', 'jwt', 'JWKS: token header kid matches a JWKS key kid (stable across calls)', 'api', 'common', () => withRoot(async () => {
|
|
327
|
+
const root = mkdtempSync(join(tmpdir(), 'clerk-kid-'));
|
|
328
|
+
try {
|
|
329
|
+
const jwt = await signJwt({ sub: 'user_y' }, { root });
|
|
330
|
+
const headerKid = JSON.parse(Buffer.from(jwt.split('.')[0].replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString()).kid;
|
|
331
|
+
const jwks = await buildJwks(root);
|
|
332
|
+
const jwks2 = await buildJwks(root); // same persisted key → same kid
|
|
333
|
+
return headerKid === jwks.keys[0].kid && jwks.keys[0].kid === jwks2.keys[0].kid;
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
rmSync(root, { recursive: true, force: true });
|
|
337
|
+
}
|
|
338
|
+
})),
|
|
339
|
+
done('clerk.jwt.custom_signing_key', 'jwt', 'JWT template: bring-your-own custom signing key (HS256)', 'api', 'niche', () => withRoot(async (h) => {
|
|
340
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
341
|
+
const s = await h({ m: 'POST', p: '/v1/sessions', b: { user_id: id(u) } });
|
|
342
|
+
const secret = 'super-shared-hs256-secret';
|
|
343
|
+
const tmpl = await h({ m: 'POST', p: '/v1/jwt_templates', b: { name: 'supabase', claims: { role: 'authenticated' }, custom_signing_key: true, signing_algorithm: 'HS256', key: secret } });
|
|
344
|
+
// the secret must never be serialized back (twin-internal)
|
|
345
|
+
if (!ok(tmpl) || field(tmpl, 'custom_signing_key') !== true || field(tmpl, 'signing_algorithm') !== 'HS256' || 'key' in tmpl.body || '_signing_key' in tmpl.body)
|
|
346
|
+
return false;
|
|
347
|
+
const tok = await h({ m: 'POST', p: `/v1/sessions/${id(s)}/tokens/supabase` });
|
|
348
|
+
const jwt = field(tok, 'jwt');
|
|
349
|
+
// the token is genuinely HS256 (header alg) and verifies against the shared secret
|
|
350
|
+
const header = JSON.parse(Buffer.from(jwt.split('.')[0].replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString());
|
|
351
|
+
const v = verifyJwtHs256(jwt, secret);
|
|
352
|
+
const wrong = verifyJwtHs256(jwt, 'not-the-secret');
|
|
353
|
+
// it must NOT verify against the RS256 JWKS (HS256 is symmetric, not in the JWKS)
|
|
354
|
+
const jwks = (await h({ m: 'GET', p: '/.well-known/jwks.json' })).body;
|
|
355
|
+
const viaJwks = verifyJwtWithJwks(jwt, jwks);
|
|
356
|
+
// custom_signing_key=true requires a key + HS256 (validation)
|
|
357
|
+
const missingKey = await h({ m: 'POST', p: '/v1/jwt_templates', b: { name: 'bad', custom_signing_key: true } });
|
|
358
|
+
const badAlgo = await h({ m: 'POST', p: '/v1/jwt_templates', b: { name: 'bad2', custom_signing_key: true, signing_algorithm: 'RS512', key: 'x' } });
|
|
359
|
+
return ok(tok) && header.alg === 'HS256' && v.valid === true && v.payload?.role === 'authenticated' && v.payload?.sub === id(u)
|
|
360
|
+
&& wrong.valid === false && viaJwks.valid === false && missingKey.status === 422 && badAlgo.status === 422;
|
|
361
|
+
})),
|
|
362
|
+
done('clerk.jwt.networkless_verify', 'jwt', 'authenticateRequest networkless verification helper (signed-in actor from claims)', 'api', 'common', () => withRoot(async (h) => {
|
|
363
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
364
|
+
const s = await h({ m: 'POST', p: '/v1/sessions', b: { user_id: id(u) } });
|
|
365
|
+
const tok = await h({ m: 'POST', p: `/v1/sessions/${id(s)}/tokens` });
|
|
366
|
+
const jwt = field(tok, 'jwt');
|
|
367
|
+
const jwks = (await h({ m: 'GET', p: '/.well-known/jwks.json' })).body;
|
|
368
|
+
const state = networklessVerifyToken(jwt, jwks);
|
|
369
|
+
// negative: a tampered token yields a signed-out state with no actor (never throws)
|
|
370
|
+
const tampered = networklessVerifyToken(`${jwt.slice(0, -4)}AAAA`, jwks);
|
|
371
|
+
return state.isSignedIn === true && state.userId === id(u) && state.sessionId === id(s)
|
|
372
|
+
&& tampered.isSignedIn === false && tampered.userId === null;
|
|
373
|
+
})),
|
|
374
|
+
// ── Sign-in tokens ────────────────────────────────────────────────────────────
|
|
375
|
+
done('clerk.sign_in_tokens.create', 'sign_in_tokens', 'Sign-in tokens: create a real signed token for a user', 'api', 'common', () => withRoot(async (h) => {
|
|
376
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
377
|
+
const t = await h({ m: 'POST', p: '/v1/sign_in_tokens', b: { user_id: id(u) } });
|
|
378
|
+
const jwt = field(t, 'token');
|
|
379
|
+
return ok(t) && field(t, 'user_id') === id(u) && typeof jwt === 'string' && jwt.split('.').length === 3;
|
|
380
|
+
})),
|
|
381
|
+
done('clerk.sign_in_tokens.revoke', 'sign_in_tokens', 'Sign-in tokens: revoke', 'api', 'niche', () => withRoot(async (h) => {
|
|
382
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
383
|
+
const t = await h({ m: 'POST', p: '/v1/sign_in_tokens', b: { user_id: id(u) } });
|
|
384
|
+
const rev = await h({ m: 'POST', p: `/v1/sign_in_tokens/${id(t)}/revoke` });
|
|
385
|
+
if (!ok(rev) || field(rev, 'status') !== 'revoked')
|
|
386
|
+
return false;
|
|
387
|
+
// re-revoking a non-pending token is rejected; an unknown id 404s
|
|
388
|
+
const again = await h({ m: 'POST', p: `/v1/sign_in_tokens/${id(t)}/revoke` });
|
|
389
|
+
const missing = await h({ m: 'POST', p: '/v1/sign_in_tokens/sit_nope/revoke' });
|
|
390
|
+
return again.status === 422 && missing.status === 404;
|
|
391
|
+
})),
|
|
392
|
+
done('clerk.actor_tokens.create', 'actor_tokens', 'Actor tokens (impersonation): create + revoke a real signed token', 'api', 'niche', () => withRoot(async (h) => {
|
|
393
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
394
|
+
const t = await h({ m: 'POST', p: '/v1/actor_tokens', b: { user_id: id(u), actor: { sub: 'user_admin' } } });
|
|
395
|
+
const jwt = field(t, 'token');
|
|
396
|
+
if (!ok(t) || field(t, 'user_id') !== id(u) || field(t, 'actor').sub !== 'user_admin' || jwt.split('.').length !== 3)
|
|
397
|
+
return false;
|
|
398
|
+
// the minted token verifies against the JWKS and carries the impersonation `act` claim
|
|
399
|
+
const jwks = (await h({ m: 'GET', p: '/.well-known/jwks.json' })).body;
|
|
400
|
+
const v = verifyJwtWithJwks(jwt, jwks);
|
|
401
|
+
if (!v.valid || v.payload?.sub !== id(u) || v.payload?.act?.sub !== 'user_admin')
|
|
402
|
+
return false;
|
|
403
|
+
const noActor = await h({ m: 'POST', p: '/v1/actor_tokens', b: { user_id: id(u) } });
|
|
404
|
+
const missingUser = await h({ m: 'POST', p: '/v1/actor_tokens', b: { user_id: 'user_nope', actor: { sub: 'x' } } });
|
|
405
|
+
const rev = await h({ m: 'POST', p: `/v1/actor_tokens/${id(t)}/revoke` });
|
|
406
|
+
return noActor.status === 422 && missingUser.status === 404 && ok(rev) && field(rev, 'status') === 'revoked';
|
|
407
|
+
})),
|
|
408
|
+
done('clerk.testing_tokens.create', 'testing', 'Testing tokens (bypass bot protection in E2E)', 'api', 'niche', () => withRoot(async (h) => {
|
|
409
|
+
const t = await h({ m: 'POST', p: '/v1/testing_tokens' });
|
|
410
|
+
const tok = field(t, 'token');
|
|
411
|
+
const t2 = await h({ m: 'POST', p: '/v1/testing_tokens' });
|
|
412
|
+
return ok(t) && field(t, 'object') === 'testing_token' && typeof tok === 'string' && tok.startsWith('testing_token_')
|
|
413
|
+
&& typeof field(t, 'expires_at') === 'number' && field(t2, 'token') !== tok;
|
|
414
|
+
})),
|
|
415
|
+
// ── Organizations ─────────────────────────────────────────────────────────────
|
|
416
|
+
done('clerk.orgs.create', 'organizations', 'Organizations: create with name + slug', 'api', 'core', () => withRoot(async (h) => {
|
|
417
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Acme Inc' } });
|
|
418
|
+
return ok(o) && field(o, 'object') === 'organization' && field(o, 'name') === 'Acme Inc' && field(o, 'slug') === 'acme-inc';
|
|
419
|
+
})),
|
|
420
|
+
done('clerk.orgs.crud', 'organizations', 'Organizations: retrieve / update / list / delete', 'api', 'core', () => withRoot(async (h) => {
|
|
421
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Acme' } });
|
|
422
|
+
const g = await h({ m: 'GET', p: `/v1/organizations/${id(o)}` });
|
|
423
|
+
const upd = await h({ m: 'PATCH', p: `/v1/organizations/${id(o)}`, b: { name: 'Acme Two' } });
|
|
424
|
+
const l = await h({ m: 'GET', p: '/v1/organizations' });
|
|
425
|
+
const del = await h({ m: 'DELETE', p: `/v1/organizations/${id(o)}` });
|
|
426
|
+
const gone = await h({ m: 'GET', p: `/v1/organizations/${id(o)}` });
|
|
427
|
+
return ok(g) && id(g) === id(o) && ok(upd) && field(upd, 'name') === 'Acme Two' && field(l, 'total_count') === 1 && ok(del) && field(del, 'deleted') === true && gone.status === 404;
|
|
428
|
+
})),
|
|
429
|
+
done('clerk.orgs.lookup_by_slug', 'organizations', 'Organizations: retrieve by slug', 'api', 'common', () => withRoot(async (h) => {
|
|
430
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Globex' } });
|
|
431
|
+
const bySlug = await h({ m: 'GET', p: '/v1/organizations/globex' });
|
|
432
|
+
return ok(o) && typeof id(o) === 'string' && id(o).length > 0 && ok(bySlug) && id(bySlug) === id(o) && field(bySlug, 'slug') === 'globex' && field(bySlug, 'name') === 'Globex';
|
|
433
|
+
})),
|
|
434
|
+
done('clerk.orgs.validation', 'organizations', 'Organizations: rejects missing name + duplicate slug', 'api', 'core', () => withRoot(async (h) => {
|
|
435
|
+
const noName = await h({ m: 'POST', p: '/v1/organizations', b: {} });
|
|
436
|
+
await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Dup', slug: 'dup' } });
|
|
437
|
+
const dup = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Dup2', slug: 'dup' } });
|
|
438
|
+
return noName.status === 422 && dup.status === 422;
|
|
439
|
+
})),
|
|
440
|
+
done('clerk.orgs.metadata', 'organizations', 'Organizations: merge metadata', 'api', 'common', () => withRoot(async (h) => {
|
|
441
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Meta Co' } });
|
|
442
|
+
await h({ m: 'PATCH', p: `/v1/organizations/${id(o)}/metadata`, b: { public_metadata: { tier: 'gold' } } });
|
|
443
|
+
const g = await h({ m: 'GET', p: `/v1/organizations/${id(o)}` });
|
|
444
|
+
return ok(g) && field(g, 'public_metadata').tier === 'gold';
|
|
445
|
+
})),
|
|
446
|
+
done('clerk.orgs.domains', 'organizations', 'Organization domains (verified domains / enrollment modes)', 'api', 'common', () => withRoot(async (h) => {
|
|
447
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Domains Co' } });
|
|
448
|
+
const d = await h({ m: 'POST', p: `/v1/organizations/${id(o)}/domains`, b: { name: 'acme.test', enrollment_mode: 'automatic_invitation' } });
|
|
449
|
+
if (!ok(d) || field(d, 'name') !== 'acme.test' || field(d, 'enrollment_mode') !== 'automatic_invitation')
|
|
450
|
+
return false;
|
|
451
|
+
const list = await h({ m: 'GET', p: `/v1/organizations/${id(o)}/domains` });
|
|
452
|
+
const patch = await h({ m: 'PATCH', p: `/v1/organizations/${id(o)}/domains/${id(d)}`, b: { enrollment_mode: 'manual_invitation' } });
|
|
453
|
+
const dup = await h({ m: 'POST', p: `/v1/organizations/${id(o)}/domains`, b: { name: 'acme.test' } });
|
|
454
|
+
const badMode = await h({ m: 'POST', p: `/v1/organizations/${id(o)}/domains`, b: { name: 'x.test', enrollment_mode: 'bogus' } });
|
|
455
|
+
const del = await h({ m: 'DELETE', p: `/v1/organizations/${id(o)}/domains/${id(d)}` });
|
|
456
|
+
const after = await h({ m: 'GET', p: `/v1/organizations/${id(o)}/domains` });
|
|
457
|
+
return field(list, 'data').length === 1 && ok(patch) && field(patch, 'enrollment_mode') === 'manual_invitation'
|
|
458
|
+
&& dup.status === 422 && badMode.status === 422 && ok(del) && field(del, 'deleted') === true && field(after, 'data').length === 0;
|
|
459
|
+
})),
|
|
460
|
+
done('clerk.orgs.roles_permissions', 'organizations', 'Custom organization roles + permissions (membership resolves its permissions)', 'api', 'common', () => withRoot(async (h) => {
|
|
461
|
+
// system roles + permissions are enumerable
|
|
462
|
+
const roles = await h({ m: 'GET', p: '/v1/organizations_roles' });
|
|
463
|
+
const perms = await h({ m: 'GET', p: '/v1/organizations_permissions' });
|
|
464
|
+
const roleKeys = field(roles, 'data').map((r) => r.key);
|
|
465
|
+
if (!roleKeys.includes('admin') || !roleKeys.includes('basic_member') || field(perms, 'data').length === 0)
|
|
466
|
+
return false;
|
|
467
|
+
// a custom role can be created and assigned; its permissions flow onto the membership
|
|
468
|
+
const custom = await h({ m: 'POST', p: '/v1/organizations_roles', b: { key: 'org:billing', name: 'Billing', permissions: ['org:sys_profile:read', 'org:sys_memberships:read'] } });
|
|
469
|
+
if (!ok(custom))
|
|
470
|
+
return false;
|
|
471
|
+
const dup = await h({ m: 'POST', p: '/v1/organizations_roles', b: { key: 'admin', name: 'Dup' } });
|
|
472
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
473
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Roles Co' } });
|
|
474
|
+
const mAdmin = await h({ m: 'POST', p: `/v1/organizations/${id(o)}/memberships`, b: { user_id: id(u), role: 'admin' } });
|
|
475
|
+
const adminPerms = field(mAdmin, 'permissions');
|
|
476
|
+
const upd = await h({ m: 'PATCH', p: `/v1/organizations/${id(o)}/memberships/${id(u)}`, b: { role: 'org:billing' } });
|
|
477
|
+
const billingPerms = field(upd, 'permissions');
|
|
478
|
+
return dup.status === 422 && adminPerms.includes('org:sys_memberships:manage')
|
|
479
|
+
&& !billingPerms.includes('org:sys_memberships:manage') && billingPerms.includes('org:sys_profile:read');
|
|
480
|
+
})),
|
|
481
|
+
done('clerk.orgs.logo', 'organizations', 'Organization logo upload / delete', 'api', 'niche', () => withRoot(async (h) => {
|
|
482
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Logo Co' } });
|
|
483
|
+
if (field(o, 'has_image') !== false)
|
|
484
|
+
return false;
|
|
485
|
+
const set = await h({ m: 'PUT', p: `/v1/organizations/${id(o)}/logo`, b: { file: 'https://cdn.test/logo.png' } });
|
|
486
|
+
if (!ok(set) || field(set, 'has_image') !== true || field(set, 'image_url') !== 'https://cdn.test/logo.png')
|
|
487
|
+
return false;
|
|
488
|
+
const del = await h({ m: 'DELETE', p: `/v1/organizations/${id(o)}/logo` });
|
|
489
|
+
const missing = await h({ m: 'PUT', p: '/v1/organizations/org_nope/logo', b: { file: 'x' } });
|
|
490
|
+
return ok(del) && field(del, 'has_image') === false && field(del, 'image_url') === '' && missing.status === 404;
|
|
491
|
+
})),
|
|
492
|
+
// ── Organization memberships ────────────────────────────────────────────────────
|
|
493
|
+
done('clerk.org_memberships.add', 'org_memberships', 'Org memberships: add a member with a role', 'api', 'core', () => withRoot(async (h) => {
|
|
494
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
495
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Acme' } });
|
|
496
|
+
const m = await h({ m: 'POST', p: `/v1/organizations/${id(o)}/memberships`, b: { user_id: id(u), role: 'admin' } });
|
|
497
|
+
return ok(m) && field(m, 'object') === 'organization_membership' && field(m, 'role') === 'admin' && field(m, 'public_user_data').user_id === id(u);
|
|
498
|
+
})),
|
|
499
|
+
done('clerk.org_memberships.list', 'org_memberships', 'Org memberships: list for an org (with nested user + org)', 'api', 'core', () => withRoot(async (h) => {
|
|
500
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
501
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Acme' } });
|
|
502
|
+
await h({ m: 'POST', p: `/v1/organizations/${id(o)}/memberships`, b: { user_id: id(u) } });
|
|
503
|
+
const l = await h({ m: 'GET', p: `/v1/organizations/${id(o)}/memberships` });
|
|
504
|
+
const data = field(l, 'data');
|
|
505
|
+
return ok(l) && data.length === 1 && data[0].organization?.id === id(o);
|
|
506
|
+
})),
|
|
507
|
+
done('clerk.org_memberships.update_role', 'org_memberships', 'Org memberships: update a member’s role', 'api', 'core', () => withRoot(async (h) => {
|
|
508
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
509
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Acme' } });
|
|
510
|
+
await h({ m: 'POST', p: `/v1/organizations/${id(o)}/memberships`, b: { user_id: id(u), role: 'basic_member' } });
|
|
511
|
+
const upd = await h({ m: 'PATCH', p: `/v1/organizations/${id(o)}/memberships/${id(u)}`, b: { role: 'admin' } });
|
|
512
|
+
return ok(upd) && field(upd, 'role') === 'admin';
|
|
513
|
+
})),
|
|
514
|
+
done('clerk.org_memberships.remove', 'org_memberships', 'Org memberships: remove a member', 'api', 'core', () => withRoot(async (h) => {
|
|
515
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
516
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Acme' } });
|
|
517
|
+
await h({ m: 'POST', p: `/v1/organizations/${id(o)}/memberships`, b: { user_id: id(u) } });
|
|
518
|
+
const del = await h({ m: 'DELETE', p: `/v1/organizations/${id(o)}/memberships/${id(u)}` });
|
|
519
|
+
const l = await h({ m: 'GET', p: `/v1/organizations/${id(o)}/memberships` });
|
|
520
|
+
return ok(del) && field(del, 'deleted') === true && field(l, 'data').length === 0;
|
|
521
|
+
})),
|
|
522
|
+
done('clerk.org_memberships.members_count', 'org_memberships', 'Org memberships: members_count tracks add/remove', 'api', 'common', () => withRoot(async (h) => {
|
|
523
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
524
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Acme' } });
|
|
525
|
+
await h({ m: 'POST', p: `/v1/organizations/${id(o)}/memberships`, b: { user_id: id(u) } });
|
|
526
|
+
const afterAdd = await h({ m: 'GET', p: `/v1/organizations/${id(o)}` });
|
|
527
|
+
await h({ m: 'DELETE', p: `/v1/organizations/${id(o)}/memberships/${id(u)}` });
|
|
528
|
+
const afterRemove = await h({ m: 'GET', p: `/v1/organizations/${id(o)}` });
|
|
529
|
+
return field(afterAdd, 'members_count') === 1 && field(afterRemove, 'members_count') === 0;
|
|
530
|
+
})),
|
|
531
|
+
done('clerk.org_memberships.duplicate_rejected', 'org_memberships', 'Org memberships: a duplicate add is rejected', 'api', 'common', () => withRoot(async (h) => {
|
|
532
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
533
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Acme' } });
|
|
534
|
+
await h({ m: 'POST', p: `/v1/organizations/${id(o)}/memberships`, b: { user_id: id(u) } });
|
|
535
|
+
const dup = await h({ m: 'POST', p: `/v1/organizations/${id(o)}/memberships`, b: { user_id: id(u) } });
|
|
536
|
+
return dup.status === 422;
|
|
537
|
+
})),
|
|
538
|
+
done('clerk.users.org_memberships', 'org_memberships', 'Users: list a user’s organization memberships', 'api', 'common', () => withRoot(async (h) => {
|
|
539
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
540
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Acme' } });
|
|
541
|
+
await h({ m: 'POST', p: `/v1/organizations/${id(o)}/memberships`, b: { user_id: id(u), role: 'admin' } });
|
|
542
|
+
const l = await h({ m: 'GET', p: `/v1/users/${id(u)}/organization_memberships` });
|
|
543
|
+
const data = field(l, 'data');
|
|
544
|
+
return ok(l) && data.length === 1 && data[0].organization?.id === id(o);
|
|
545
|
+
})),
|
|
546
|
+
// ── Organization invitations ────────────────────────────────────────────────────
|
|
547
|
+
done('clerk.org_invitations.create', 'org_invitations', 'Org invitations: create (pending) + bumps count', 'api', 'core', () => withRoot(async (h) => {
|
|
548
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Acme' } });
|
|
549
|
+
const inv = await h({ m: 'POST', p: `/v1/organizations/${id(o)}/invitations`, b: { email_address: 'new@twin.test', role: 'basic_member' } });
|
|
550
|
+
const org = await h({ m: 'GET', p: `/v1/organizations/${id(o)}` });
|
|
551
|
+
return ok(inv) && field(inv, 'status') === 'pending' && field(inv, 'email_address') === 'new@twin.test' && field(org, 'pending_invitations_count') === 1;
|
|
552
|
+
})),
|
|
553
|
+
done('clerk.org_invitations.list_revoke', 'org_invitations', 'Org invitations: list + revoke', 'api', 'core', () => withRoot(async (h) => {
|
|
554
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Acme' } });
|
|
555
|
+
const inv = await h({ m: 'POST', p: `/v1/organizations/${id(o)}/invitations`, b: { email_address: 'x@twin.test' } });
|
|
556
|
+
const l = await h({ m: 'GET', p: `/v1/organizations/${id(o)}/invitations` });
|
|
557
|
+
const rev = await h({ m: 'POST', p: `/v1/organizations/${id(o)}/invitations/${id(inv)}/revoke` });
|
|
558
|
+
return field(l, 'data').length === 1 && ok(rev) && field(rev, 'status') === 'revoked';
|
|
559
|
+
})),
|
|
560
|
+
done('clerk.org_invitations.accept', 'org_invitations', 'Org invitations: accept → creates membership + decrements pending count', 'api', 'common', () => withRoot(async (h) => {
|
|
561
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser('new@twin.test') });
|
|
562
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Accept Co' } });
|
|
563
|
+
const inv = await h({ m: 'POST', p: `/v1/organizations/${id(o)}/invitations`, b: { email_address: 'new@twin.test', role: 'admin' } });
|
|
564
|
+
const acc = await h({ m: 'POST', p: `/v1/organizations/${id(o)}/invitations/${id(inv)}/accept`, b: { user_id: id(u) } });
|
|
565
|
+
if (!ok(acc) || field(acc, 'object') !== 'organization_membership' || field(acc, 'role') !== 'admin')
|
|
566
|
+
return false;
|
|
567
|
+
const invAfter = await h({ m: 'GET', p: `/v1/organizations/${id(o)}/invitations` });
|
|
568
|
+
const orgAfter = await h({ m: 'GET', p: `/v1/organizations/${id(o)}` });
|
|
569
|
+
const members = await h({ m: 'GET', p: `/v1/organizations/${id(o)}/memberships` });
|
|
570
|
+
// re-accepting an already-accepted invite is rejected
|
|
571
|
+
const reAccept = await h({ m: 'POST', p: `/v1/organizations/${id(o)}/invitations/${id(inv)}/accept`, b: { user_id: id(u) } });
|
|
572
|
+
return field(invAfter, 'data')[0].status === 'accepted'
|
|
573
|
+
&& field(orgAfter, 'pending_invitations_count') === 0
|
|
574
|
+
&& field(members, 'data').length === 1 && reAccept.status === 422;
|
|
575
|
+
})),
|
|
576
|
+
done('clerk.org_invitations.bulk', 'org_invitations', 'Org invitations: bulk create', 'api', 'niche', () => withRoot(async (h) => {
|
|
577
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Bulk Co' } });
|
|
578
|
+
const bulk = await h({ m: 'POST', p: `/v1/organizations/${id(o)}/invitations/bulk`, b: { invitations: [
|
|
579
|
+
{ email_address: 'a@twin.test', role: 'admin' },
|
|
580
|
+
{ email_address: 'b@twin.test' },
|
|
581
|
+
] } });
|
|
582
|
+
const created = bulk.body;
|
|
583
|
+
if (!ok(bulk) || !Array.isArray(created) || created.length !== 2 || created[0].role !== 'admin' || created[0].status !== 'pending')
|
|
584
|
+
return false;
|
|
585
|
+
const org = await h({ m: 'GET', p: `/v1/organizations/${id(o)}` });
|
|
586
|
+
const list = await h({ m: 'GET', p: `/v1/organizations/${id(o)}/invitations` });
|
|
587
|
+
// an empty array + a missing email are rejected
|
|
588
|
+
const empty = await h({ m: 'POST', p: `/v1/organizations/${id(o)}/invitations/bulk`, b: { invitations: [] } });
|
|
589
|
+
const noEmail = await h({ m: 'POST', p: `/v1/organizations/${id(o)}/invitations/bulk`, b: { invitations: [{ role: 'admin' }] } });
|
|
590
|
+
return field(org, 'pending_invitations_count') === 2 && field(list, 'data').length === 2 && empty.status === 422 && noEmail.status === 422;
|
|
591
|
+
})),
|
|
592
|
+
// ── Application invitations ─────────────────────────────────────────────────────
|
|
593
|
+
done('clerk.invitations.crud', 'invitations', 'Invitations: create / list / revoke', 'api', 'common', () => withRoot(async (h) => {
|
|
594
|
+
const inv = await h({ m: 'POST', p: '/v1/invitations', b: { email_address: 'invitee@twin.test' } });
|
|
595
|
+
if (!ok(inv) || field(inv, 'status') !== 'pending')
|
|
596
|
+
return false;
|
|
597
|
+
const l = await h({ m: 'GET', p: '/v1/invitations' });
|
|
598
|
+
const rev = await h({ m: 'POST', p: `/v1/invitations/${id(inv)}/revoke` });
|
|
599
|
+
return l.body.length === 1 && ok(rev) && field(rev, 'status') === 'revoked';
|
|
600
|
+
})),
|
|
601
|
+
done('clerk.invitations.notify', 'invitations', 'Invitations: notify flag / redirect_url handling', 'api', 'niche', () => withRoot(async (h) => {
|
|
602
|
+
// notify defaults true → the twin records an invite url (it never delivers email)
|
|
603
|
+
const notified = await h({ m: 'POST', p: '/v1/invitations', b: { email_address: 'n1@twin.test', redirect_url: 'https://app.test/welcome' } });
|
|
604
|
+
if (!ok(notified) || field(notified, 'notified') !== true || typeof field(notified, 'url') !== 'string' || !String(field(notified, 'url')).includes('welcome'))
|
|
605
|
+
return false;
|
|
606
|
+
// notify:false → no url recorded, redirect_url null
|
|
607
|
+
const quiet = await h({ m: 'POST', p: '/v1/invitations', b: { email_address: 'n2@twin.test', notify: false } });
|
|
608
|
+
return ok(quiet) && field(quiet, 'notified') === false && field(quiet, 'url') === null;
|
|
609
|
+
})),
|
|
610
|
+
// ── Allowlist / Blocklist ────────────────────────────────────────────────────────
|
|
611
|
+
done('clerk.allowlist.crud', 'access_lists', 'Allowlist identifiers: create / list / delete', 'api', 'common', () => withRoot(async (h) => {
|
|
612
|
+
const a = await h({ m: 'POST', p: '/v1/allowlist_identifiers', b: { identifier: 'vip@twin.test' } });
|
|
613
|
+
if (!ok(a) || field(a, 'identifier') !== 'vip@twin.test' || field(a, 'identifier_type') !== 'email_address')
|
|
614
|
+
return false;
|
|
615
|
+
const l = await h({ m: 'GET', p: '/v1/allowlist_identifiers' });
|
|
616
|
+
const del = await h({ m: 'DELETE', p: `/v1/allowlist_identifiers/${id(a)}` });
|
|
617
|
+
const l2 = await h({ m: 'GET', p: '/v1/allowlist_identifiers' });
|
|
618
|
+
return l.body.length === 1 && ok(del) && field(del, 'deleted') === true && l2.body.length === 0;
|
|
619
|
+
})),
|
|
620
|
+
done('clerk.blocklist.crud', 'access_lists', 'Blocklist identifiers: create / list / delete', 'api', 'common', () => withRoot(async (h) => {
|
|
621
|
+
const b = await h({ m: 'POST', p: '/v1/blocklist_identifiers', b: { identifier: '+15551234567' } });
|
|
622
|
+
if (!ok(b) || field(b, 'identifier_type') !== 'phone_number')
|
|
623
|
+
return false;
|
|
624
|
+
const l = await h({ m: 'GET', p: '/v1/blocklist_identifiers' });
|
|
625
|
+
const del = await h({ m: 'DELETE', p: `/v1/blocklist_identifiers/${id(b)}` });
|
|
626
|
+
return l.body.length === 1 && ok(del) && field(del, 'deleted') === true;
|
|
627
|
+
})),
|
|
628
|
+
// ── JWT templates (CRUD, separate from token issuance) ────────────────────────────
|
|
629
|
+
done('clerk.jwt_templates.crud', 'jwt_templates', 'JWT templates: create / list / retrieve / update / delete', 'api', 'common', () => withRoot(async (h) => {
|
|
630
|
+
const t = await h({ m: 'POST', p: '/v1/jwt_templates', b: { name: 'firebase', claims: { x: 1 }, lifetime: 120 } });
|
|
631
|
+
if (!ok(t) || field(t, 'name') !== 'firebase' || field(t, 'lifetime') !== 120)
|
|
632
|
+
return false;
|
|
633
|
+
const g = await h({ m: 'GET', p: `/v1/jwt_templates/${id(t)}` });
|
|
634
|
+
const upd = await h({ m: 'PATCH', p: `/v1/jwt_templates/${id(t)}`, b: { lifetime: 300 } });
|
|
635
|
+
const l = await h({ m: 'GET', p: '/v1/jwt_templates' });
|
|
636
|
+
const del = await h({ m: 'DELETE', p: `/v1/jwt_templates/${id(t)}` });
|
|
637
|
+
return ok(g) && id(g) === id(t) && ok(upd) && field(upd, 'lifetime') === 300 && l.body.length === 1 && ok(del) && field(del, 'deleted') === true;
|
|
638
|
+
})),
|
|
639
|
+
// ── Webhooks (svix-style signed emission via injected sink) ───────────────────────
|
|
640
|
+
done('clerk.webhooks.user_created', 'webhooks', 'Webhooks: a signed user.created webhook is emitted + verifies', 'api', 'core', () => withRoot(async (h) => {
|
|
641
|
+
clearClerkWebhooks();
|
|
642
|
+
const secret = 'whsec_dGVzdHNlY3JldHRlc3RzZWNyZXQ=';
|
|
643
|
+
const delivered = [];
|
|
644
|
+
registerClerkWebhook('https://app.test/webhook', secret);
|
|
645
|
+
try {
|
|
646
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
647
|
+
// the created user must be REAL (a genuine id + echoed shape), not just a 2xx status
|
|
648
|
+
if (!ok(u) || typeof id(u) !== 'string' || id(u).length === 0 || field(u, 'object') !== 'user')
|
|
649
|
+
return false;
|
|
650
|
+
// emit deterministically via the injected sink and verify svix signature
|
|
651
|
+
const out = await emitClerkEvent('user.create', { id: id(u) }, { deliver: (url, body, headers) => { delivered.push({ body, headers }); } });
|
|
652
|
+
if (out.length !== 1)
|
|
653
|
+
return false;
|
|
654
|
+
const ev = verifyWebhook(delivered[0].body, delivered[0].headers, secret);
|
|
655
|
+
return ev.type === 'user.created' && typeof ev.data.id === 'string' && ev.data.id === id(u);
|
|
656
|
+
}
|
|
657
|
+
finally {
|
|
658
|
+
clearClerkWebhooks();
|
|
659
|
+
}
|
|
660
|
+
})),
|
|
661
|
+
done('clerk.webhooks.signature_scheme', 'webhooks', 'Webhooks: svix signature recompute matches (tamper rejected)', 'api', 'core', () => {
|
|
662
|
+
const secret = 'whsec_dGVzdHNlY3JldHRlc3RzZWNyZXQ=';
|
|
663
|
+
const sig = computeSvixSignature('msg_1', 1700000000, '{"hello":"world"}', secret);
|
|
664
|
+
const good = (() => {
|
|
665
|
+
try {
|
|
666
|
+
verifyWebhook('{"hello":"world"}', { 'svix-id': 'msg_1', 'svix-timestamp': '1700000000', 'svix-signature': sig }, secret);
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
catch (err) {
|
|
670
|
+
if (isInfrastructureError(err))
|
|
671
|
+
throw harnessError('clerk.webhooks.signature_scheme', err);
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
})();
|
|
675
|
+
const bad = (() => {
|
|
676
|
+
try {
|
|
677
|
+
verifyWebhook('{"hello":"TAMPERED"}', { 'svix-id': 'msg_1', 'svix-timestamp': '1700000000', 'svix-signature': sig }, secret);
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
})();
|
|
684
|
+
return Promise.resolve(sig.startsWith('v1,') && good && bad);
|
|
685
|
+
}),
|
|
686
|
+
done('clerk.webhooks.org_events', 'webhooks', 'Webhooks: organization.created maps to the right event type', 'api', 'common', () => withRoot(async (h) => {
|
|
687
|
+
clearClerkWebhooks();
|
|
688
|
+
const delivered = [];
|
|
689
|
+
registerClerkWebhook('https://app.test/webhook');
|
|
690
|
+
try {
|
|
691
|
+
const o = await h({ m: 'POST', p: '/v1/organizations', b: { name: 'Hook Co' } });
|
|
692
|
+
// the org must be REAL (genuine id + echoed shape) before we fold it into the event
|
|
693
|
+
if (!ok(o) || typeof id(o) !== 'string' || id(o).length === 0 || field(o, 'object') !== 'organization')
|
|
694
|
+
return false;
|
|
695
|
+
const out = await emitClerkEvent('organization.create', { id: id(o) }, { deliver: (_u, body) => { delivered.push({ body }); } });
|
|
696
|
+
if (out.length !== 1)
|
|
697
|
+
return false;
|
|
698
|
+
const parsed = JSON.parse(delivered[0].body);
|
|
699
|
+
return parsed.type === 'organization.created' && parsed.data?.id === id(o);
|
|
700
|
+
}
|
|
701
|
+
finally {
|
|
702
|
+
clearClerkWebhooks();
|
|
703
|
+
}
|
|
704
|
+
})),
|
|
705
|
+
done('clerk.webhooks.delivery_retries', 'webhooks', 'Webhooks: delivery attempts log', 'api', 'niche', () => withRoot(async (h) => {
|
|
706
|
+
clearClerkWebhooks();
|
|
707
|
+
try {
|
|
708
|
+
const ep = await h({ m: 'POST', p: '/v1/webhooks/endpoints', b: { url: 'https://app.test/hook' } });
|
|
709
|
+
// a write that maps to an event records a delivery attempt for the endpoint
|
|
710
|
+
await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
711
|
+
const attempts = await h({ m: 'GET', p: `/v1/webhooks/endpoints/${id(ep)}/attempts` });
|
|
712
|
+
const data = field(attempts, 'data');
|
|
713
|
+
if (!ok(attempts) || data.length < 1 || data[0].event_type !== 'user.created' || data[0].status !== 'succeeded')
|
|
714
|
+
return false;
|
|
715
|
+
// a disabled endpoint records no further attempts
|
|
716
|
+
await h({ m: 'PATCH', p: `/v1/webhooks/endpoints/${id(ep)}`, b: { disabled: true } });
|
|
717
|
+
await h({ m: 'POST', p: '/v1/users', b: mkUser('two@twin.test') });
|
|
718
|
+
const after = await h({ m: 'GET', p: `/v1/webhooks/endpoints/${id(ep)}/attempts` });
|
|
719
|
+
const missing = await h({ m: 'GET', p: '/v1/webhooks/endpoints/ep_nope/attempts' });
|
|
720
|
+
return field(after, 'data').length === data.length && missing.status === 404;
|
|
721
|
+
}
|
|
722
|
+
finally {
|
|
723
|
+
clearClerkWebhooks();
|
|
724
|
+
}
|
|
725
|
+
})),
|
|
726
|
+
done('clerk.webhooks.endpoints_api', 'webhooks', 'Webhooks: manage endpoints via the Backend API', 'api', 'niche', () => withRoot(async (h) => {
|
|
727
|
+
clearClerkWebhooks();
|
|
728
|
+
try {
|
|
729
|
+
const ep = await h({ m: 'POST', p: '/v1/webhooks/endpoints', b: { url: 'https://app.test/hook', description: 'prod' } });
|
|
730
|
+
const secret = field(ep, 'signing_secret');
|
|
731
|
+
if (!ok(ep) || field(ep, 'object') !== 'webhook_endpoint' || field(ep, 'url') !== 'https://app.test/hook' || !secret.startsWith('whsec_'))
|
|
732
|
+
return false;
|
|
733
|
+
const get = await h({ m: 'GET', p: `/v1/webhooks/endpoints/${id(ep)}` });
|
|
734
|
+
const list = await h({ m: 'GET', p: '/v1/webhooks/endpoints' });
|
|
735
|
+
const upd = await h({ m: 'PATCH', p: `/v1/webhooks/endpoints/${id(ep)}`, b: { description: 'staging' } });
|
|
736
|
+
const del = await h({ m: 'DELETE', p: `/v1/webhooks/endpoints/${id(ep)}` });
|
|
737
|
+
const gone = await h({ m: 'GET', p: `/v1/webhooks/endpoints/${id(ep)}` });
|
|
738
|
+
const noUrl = await h({ m: 'POST', p: '/v1/webhooks/endpoints', b: {} });
|
|
739
|
+
return ok(get) && id(get) === id(ep) && field(list, 'data').length === 1
|
|
740
|
+
&& ok(upd) && field(upd, 'description') === 'staging' && ok(del) && field(del, 'deleted') === true && gone.status === 404 && noUrl.status === 422;
|
|
741
|
+
}
|
|
742
|
+
finally {
|
|
743
|
+
clearClerkWebhooks();
|
|
744
|
+
}
|
|
745
|
+
})),
|
|
746
|
+
// ── read-only guard ──────────────────────────────────────────────────────────────
|
|
747
|
+
done('clerk.serve.read_only', 'serve', 'Read-only twin rejects writes (405)', 'api', 'core', async () => {
|
|
748
|
+
const root = mkdtempSync(join(tmpdir(), 'clerk-ro-'));
|
|
749
|
+
try {
|
|
750
|
+
const r = await handleClerkTwinRequest({ method: 'POST', path: '/v1/users', body: JSON.stringify(mkUser()), readOnly: true, root });
|
|
751
|
+
return r.status === 405;
|
|
752
|
+
}
|
|
753
|
+
finally {
|
|
754
|
+
rmSync(root, { recursive: true, force: true });
|
|
755
|
+
}
|
|
756
|
+
}),
|
|
757
|
+
done('clerk.serve.unknown_route', 'serve', 'Unknown route fails like Clerk (404), never fabricates success', 'api', 'core', () => withRoot(async (h) => {
|
|
758
|
+
const r = await h({ m: 'GET', p: '/v1/nonexistent_collection' });
|
|
759
|
+
return r.status === 404 && Array.isArray(r.body.errors);
|
|
760
|
+
})),
|
|
761
|
+
// ── Connector ─────────────────────────────────────────────────────────────────
|
|
762
|
+
done('clerk.connector.pull_users', 'connector', 'Connector: pull users from an injected client, fold into the twin', 'connector', 'core', async () => {
|
|
763
|
+
const { pullClerkUsers, syncClerkFromReal } = await import("./clerk-connector.js");
|
|
764
|
+
const fakeClient = {
|
|
765
|
+
users: { getUserList: async () => ({ data: [{ id: 'user_real_1', firstName: 'Real', emailAddresses: [{ emailAddress: 'real@x.test' }] }], totalCount: 1 }) },
|
|
766
|
+
organizations: { getOrganizationList: async () => ({ data: [], totalCount: 0 }) },
|
|
767
|
+
};
|
|
768
|
+
const pulled = await pullClerkUsers(fakeClient);
|
|
769
|
+
if (pulled.length !== 1 || pulled[0].id !== 'user_real_1')
|
|
770
|
+
return false;
|
|
771
|
+
const root = mkdtempSync(join(tmpdir(), 'clerk-pull-'));
|
|
772
|
+
try {
|
|
773
|
+
const res = await syncClerkFromReal(fakeClient, { root, occurredAt: '2026-01-01T00:00:00.000Z' });
|
|
774
|
+
const got = await handleClerkTwinRequest({ method: 'GET', path: '/v1/users', root });
|
|
775
|
+
return res.observed >= 1 && got.body.some((u) => u.id === 'user_real_1');
|
|
776
|
+
}
|
|
777
|
+
finally {
|
|
778
|
+
rmSync(root, { recursive: true, force: true });
|
|
779
|
+
}
|
|
780
|
+
}),
|
|
781
|
+
done('clerk.connector.push_confirm', 'connector', 'Connector: push pending writes to an injected client + confirm (idempotent re-push)', 'connector', 'core', async () => {
|
|
782
|
+
const { performClerkAction } = await import("./clerk-connector.js");
|
|
783
|
+
const root = mkdtempSync(join(tmpdir(), 'clerk-push-'));
|
|
784
|
+
const wire = [];
|
|
785
|
+
const execute = async (r) => { wire.push(r); return { status: 200, headers: {}, body: JSON.stringify({ id: 'user_real_pushed' }) }; };
|
|
786
|
+
try {
|
|
787
|
+
const made = await handleClerkTwinRequest({ method: 'POST', path: '/v1/users', body: JSON.stringify(mkUser()), root });
|
|
788
|
+
const id = String(made.body.id);
|
|
789
|
+
const out = await performClerkAction(execute, { operation: 'user.create', subject: { type: 'user', id }, fields: mkUser() }, { resolve: (_t, l) => l, service: 'clerk', root });
|
|
790
|
+
// one entry, one wire write, the vendor's id back; the twin's own records (a session, an attempt) cross nothing
|
|
791
|
+
const own = await performClerkAction(execute, { operation: 'sign_in_attempt.upsert', subject: { type: 'sign_in_attempt', id: 'sia_x' }, fields: {} }, { resolve: (_t, l) => l, service: 'clerk', root });
|
|
792
|
+
return out.externalId === 'user_real_pushed' && wire.length === 1 && wire[0].path === '/v1/users' && own.externalId === 'sia_x' && wire.length === 1;
|
|
793
|
+
}
|
|
794
|
+
finally {
|
|
795
|
+
rmSync(root, { recursive: true, force: true });
|
|
796
|
+
}
|
|
797
|
+
}),
|
|
798
|
+
done('clerk.connector.push_unsupported', 'connector', 'Connector: an unsupported push op fails loudly (no silent drop)', 'connector', 'common', async () => {
|
|
799
|
+
const { pushClerkAction } = await import("./clerk-connector.js");
|
|
800
|
+
const fakeClient = { users: {}, organizations: {} };
|
|
801
|
+
try {
|
|
802
|
+
await pushClerkAction(fakeClient, { operation: 'session.create', subject: { type: 'session', id: 'sess_1' }, fields: {} });
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
catch {
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
}),
|
|
809
|
+
done('clerk.connector.pull_sessions', 'connector', 'Connector: pull sessions / memberships from real Clerk, fold into the twin', 'connector', 'common', async () => {
|
|
810
|
+
const { pullClerkSessions, pullClerkMemberships, syncClerkFromReal } = await import("./clerk-connector.js");
|
|
811
|
+
const fakeClient = {
|
|
812
|
+
users: { getUserList: async () => ({ data: [{ id: 'user_real_1', emailAddresses: [{ emailAddress: 'real@x.test' }] }], totalCount: 1 }) },
|
|
813
|
+
organizations: {
|
|
814
|
+
getOrganizationList: async () => ({ data: [{ id: 'org_real_1', name: 'Real Org', slug: 'real-org' }], totalCount: 1 }),
|
|
815
|
+
getOrganizationMembershipList: async ({ organizationId }) => ({ data: [{ id: 'orgmem_real_1', role: 'admin', organization: { id: organizationId }, publicUserData: { userId: 'user_real_1' } }], totalCount: 1 }),
|
|
816
|
+
},
|
|
817
|
+
sessions: { getSessionList: async () => ({ data: [{ id: 'sess_real_1', userId: 'user_real_1', status: 'active' }], totalCount: 1 }) },
|
|
818
|
+
};
|
|
819
|
+
const sessions = await pullClerkSessions(fakeClient);
|
|
820
|
+
const memberships = await pullClerkMemberships(fakeClient, ['org_real_1']);
|
|
821
|
+
if (sessions.length !== 1 || sessions[0].id !== 'sess_real_1' || memberships.length !== 1 || memberships[0].fields._user_id !== 'user_real_1')
|
|
822
|
+
return false;
|
|
823
|
+
const root = mkdtempSync(join(tmpdir(), 'clerk-pull-sess-'));
|
|
824
|
+
try {
|
|
825
|
+
await syncClerkFromReal(fakeClient, { root, occurredAt: '2026-01-01T00:00:00.000Z' });
|
|
826
|
+
const gotSessions = await handleClerkTwinRequest({ method: 'GET', path: '/v1/sessions', root });
|
|
827
|
+
const gotMembers = await handleClerkTwinRequest({ method: 'GET', path: '/v1/organizations/org_real_1/memberships', root });
|
|
828
|
+
return gotSessions.body.some((s) => s.id === 'sess_real_1')
|
|
829
|
+
&& gotMembers.body.data.some((m) => m.id === 'orgmem_real_1' && m.public_user_data?.user_id === 'user_real_1');
|
|
830
|
+
}
|
|
831
|
+
finally {
|
|
832
|
+
rmSync(root, { recursive: true, force: true });
|
|
833
|
+
}
|
|
834
|
+
}),
|
|
835
|
+
// ── UI mirror ──────────────────────────────────────────────────────────────────
|
|
836
|
+
// TWIN-14 (B4) migration — was a marker-grep over the literals 'Users', '/v1/', 'primaryEmail';
|
|
837
|
+
// now uiDataCoupled: seed a real user with a distinct email → fetch the SAME `/v1/users` list
|
|
838
|
+
// the Users screen reads → shape it through the mirror's OWN `users` section title()/
|
|
839
|
+
// subtitle() closures (imported as `SECTIONS`, the exact functions the row list calls) →
|
|
840
|
+
// assert the seeded name + email survive. Failable: an empty root's list has no [0] row, so
|
|
841
|
+
// `section(...).subtitle` throws on `undefined` and the outer `withRoot` try/catch → false.
|
|
842
|
+
done('clerk.ui.users_screen', 'ui', 'UI mirror: Users screen renders from the twin REST API (data-coupled)', 'ui', 'core', uiDataCoupled({
|
|
843
|
+
withRoot,
|
|
844
|
+
seed: (h) => h({ m: 'POST', p: '/v1/users', b: mkUser('ui-users@twin.test') }),
|
|
845
|
+
fetch: async (h) => (await h({ m: 'GET', p: '/v1/users' })).body[0],
|
|
846
|
+
render: (user) => `${section('users').title(user)} | ${section('users').subtitle(user)}`,
|
|
847
|
+
assert: (_user, markup) => !!markup && markup.includes('Ada Lovelace') && markup.includes('ui-users@twin.test') && markup.includes('active'),
|
|
848
|
+
})),
|
|
849
|
+
// TWIN-14 (B4) migration — was a marker-grep over the literals 'Sessions', 'user_id'; now
|
|
850
|
+
// uiDataCoupled: seed a user + a session bound to it → fetch the SAME `/v1/sessions` list
|
|
851
|
+
// the Sessions screen reads → shape via the mirror's OWN `sessions` section subtitle() (which
|
|
852
|
+
// reads `status`/`user_id`) → assert the seeded user id + active status survive. Failable: an
|
|
853
|
+
// empty root's session list has no [0] row → subtitle throws on undefined → false.
|
|
854
|
+
done('clerk.ui.sessions_screen', 'ui', 'UI mirror: Sessions screen (data-coupled)', 'ui', 'common', uiDataCoupled({
|
|
855
|
+
withRoot,
|
|
856
|
+
seed: async (h) => {
|
|
857
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser('ui-sessions@twin.test') });
|
|
858
|
+
return h({ m: 'POST', p: '/v1/sessions', b: { user_id: id(u) } });
|
|
859
|
+
},
|
|
860
|
+
fetch: async (h) => (await h({ m: 'GET', p: '/v1/sessions' })).body[0],
|
|
861
|
+
render: (session) => section('sessions').subtitle(session),
|
|
862
|
+
assert: (session, markup) => typeof session.user_id === 'string' && markup === `active · ${session.user_id}`,
|
|
863
|
+
})),
|
|
864
|
+
// TWIN-14 (B4) migration — was a marker-grep over the literals 'Organizations', 'members'; now
|
|
865
|
+
// uiDataCoupled: seed an org with a distinct name → fetch the SAME `/v1/organizations` list the
|
|
866
|
+
// Organizations screen reads → shape via the mirror's OWN `organizations` section title()/
|
|
867
|
+
// subtitle() (which read `orgMemberCount`/`slug`) → assert the seeded slug survives. Failable:
|
|
868
|
+
// an empty root's org list has no [0] row → subtitle throws on undefined → false.
|
|
869
|
+
done('clerk.ui.organizations_screen', 'ui', 'UI mirror: Organizations screen (data-coupled)', 'ui', 'core', uiDataCoupled({
|
|
870
|
+
withRoot,
|
|
871
|
+
seed: (h) => h({ m: 'POST', p: '/v1/organizations', b: { name: 'UI Check Org' } }),
|
|
872
|
+
fetch: async (h) => field(await h({ m: 'GET', p: '/v1/organizations' }), 'data')[0],
|
|
873
|
+
render: (org) => `${section('organizations').title(org)} | ${section('organizations').subtitle(org)}`,
|
|
874
|
+
assert: (_org, markup) => markup === 'UI Check Org | 0 members · ui-check-org',
|
|
875
|
+
})),
|
|
876
|
+
// TWIN-14 (B4) migration — was a marker-grep over the literals 'Allowlist', 'Blocklist',
|
|
877
|
+
// 'identifier'; now uiDataCoupled: seed one allowlist + one blocklist identifier, each with a
|
|
878
|
+
// distinct value → fetch the SAME `/v1/allowlist_identifiers` + `/v1/blocklist_identifiers`
|
|
879
|
+
// lists those screens read → shape via the mirror's OWN section title()/subtitle() closures →
|
|
880
|
+
// assert both seeded identifiers + their derived types survive. Failable: an empty root's
|
|
881
|
+
// lists have no [0] rows → the closures throw on undefined → false.
|
|
882
|
+
done('clerk.ui.access_lists_screen', 'ui', 'UI mirror: Allowlist + Blocklist screens (data-coupled)', 'ui', 'common', uiDataCoupled({
|
|
883
|
+
withRoot,
|
|
884
|
+
seed: async (h) => {
|
|
885
|
+
await h({ m: 'POST', p: '/v1/allowlist_identifiers', b: { identifier: 'ui-allow@twin.test' } });
|
|
886
|
+
return h({ m: 'POST', p: '/v1/blocklist_identifiers', b: { identifier: '+15559990000' } });
|
|
887
|
+
},
|
|
888
|
+
fetch: async (h) => ({
|
|
889
|
+
allow: (await h({ m: 'GET', p: '/v1/allowlist_identifiers' })).body[0],
|
|
890
|
+
block: (await h({ m: 'GET', p: '/v1/blocklist_identifiers' })).body[0],
|
|
891
|
+
}),
|
|
892
|
+
render: ({ allow, block }) => `${section('allowlist_identifiers').title(allow)} ${section('allowlist_identifiers').subtitle(allow)} || ` +
|
|
893
|
+
`${section('blocklist_identifiers').title(block)} ${section('blocklist_identifiers').subtitle(block)}`,
|
|
894
|
+
assert: (_data, markup) => !!markup && markup.includes('ui-allow@twin.test') && markup.includes('email_address')
|
|
895
|
+
&& markup.includes('+15559990000') && markup.includes('phone_number'),
|
|
896
|
+
})),
|
|
897
|
+
// TWIN-14 (B4) migration — was a marker-grep over the literals 'JWT Templates', 'lifetime';
|
|
898
|
+
// now uiDataCoupled: seed a JWT template with a distinct name + lifetime → fetch the SAME
|
|
899
|
+
// `/v1/jwt_templates` list the JWT Templates screen reads → shape via the mirror's OWN
|
|
900
|
+
// section title()/subtitle() → assert the seeded name + lifetime survive. Failable: an empty
|
|
901
|
+
// root's list has no [0] row → subtitle throws on undefined → false.
|
|
902
|
+
done('clerk.ui.jwt_templates_screen', 'ui', 'UI mirror: JWT Templates screen (data-coupled)', 'ui', 'common', uiDataCoupled({
|
|
903
|
+
withRoot,
|
|
904
|
+
seed: (h) => h({ m: 'POST', p: '/v1/jwt_templates', b: { name: 'ui-check-template', claims: { role: 'authenticated' }, lifetime: 90 } }),
|
|
905
|
+
fetch: async (h) => (await h({ m: 'GET', p: '/v1/jwt_templates' })).body[0],
|
|
906
|
+
render: (tmpl) => `${section('jwt_templates').title(tmpl)} | ${section('jwt_templates').subtitle(tmpl)}`,
|
|
907
|
+
assert: (_tmpl, markup) => markup === 'ui-check-template | lifetime 90s · RS256',
|
|
908
|
+
})),
|
|
909
|
+
// TWIN-14 (B4) migration — was a marker-grep over the literals 'detail-line', 'pill ',
|
|
910
|
+
// 'Clerk twin'; now uiDataCoupled: seed an invitation with a distinct email (status starts
|
|
911
|
+
// 'pending') → fetch the SAME `/v1/invitations` list the detail panel would select into →
|
|
912
|
+
// render it through the mirror's OWN exported `NestedLines`/`StatusPill` components (exactly
|
|
913
|
+
// what the detail panel renders for any selected record) → assert the seeded email survives
|
|
914
|
+
// flattening AND the seeded 'pending' status drives the correct pill tone. Failable: an
|
|
915
|
+
// empty root's list has no [0] row → flattenClerkValue(undefined) yields no seeded email.
|
|
916
|
+
done('clerk.ui.detail_panel', 'ui', 'UI mirror: detail panel flattens nested fields + status pill (data-coupled)', 'ui', 'common', uiDataCoupled({
|
|
917
|
+
withRoot,
|
|
918
|
+
seed: (h) => h({ m: 'POST', p: '/v1/invitations', b: { email_address: 'ui-detail@twin.test' } }),
|
|
919
|
+
fetch: async (h) => (await h({ m: 'GET', p: '/v1/invitations' })).body[0],
|
|
920
|
+
render: (inv) => renderToStaticMarkup(createElement('div', null, createElement(NestedLines, { lines: flattenClerkValue(inv) }), inv?.status ? createElement(StatusPill, { value: inv.status }) : null)),
|
|
921
|
+
assert: (inv, markup) => !!markup && markup.includes('detail-line') && markup.includes('ui-detail@twin.test')
|
|
922
|
+
&& inv.status === 'pending' && markup.includes(`pill ${statusTone(inv.status)}`),
|
|
923
|
+
})),
|
|
924
|
+
// TWIN-14 (B4) migration — was a marker-grep over the literals 'Create user', 'CreateUserForm',
|
|
925
|
+
// "method: 'POST'", '/v1/users'; now uiDataCoupled: `seed` issues the EXACT request
|
|
926
|
+
// `CreateUserForm`'s submit handler sends (POST /v1/users with the same body shape) → `fetch`
|
|
927
|
+
// reads the SAME `/v1/users` list the dashboard reloads into on `onCreated` → `render` mounts
|
|
928
|
+
// the mirror's OWN exported `CreateUserForm` component (structural proof the wired action
|
|
929
|
+
// exists) → assert the seeded user is really there. Failable: an unseeded root's list omits
|
|
930
|
+
// the seeded email.
|
|
931
|
+
done('clerk.ui.create_user_action', 'ui', 'UI mirror: create-a-user action from the dashboard (data-coupled)', 'ui', 'niche', uiDataCoupled({
|
|
932
|
+
withRoot,
|
|
933
|
+
seed: (h) => h({ m: 'POST', p: '/v1/users', b: { email_address: ['ui-create@twin.test'], first_name: 'Uic' } }),
|
|
934
|
+
fetch: async (h) => (await h({ m: 'GET', p: '/v1/users' })).body,
|
|
935
|
+
render: () => renderToStaticMarkup(createElement(CreateUserForm, { onCreated: () => { } })),
|
|
936
|
+
assert: (users, markup) => !!markup && markup.includes('Create user') &&
|
|
937
|
+
users.some((u) => u.email_addresses[0]?.email_address === 'ui-create@twin.test' && u.first_name === 'Uic'),
|
|
938
|
+
})),
|
|
939
|
+
// TWIN-14 (B4) migration — was a marker-grep over the literals 'Impersonate user',
|
|
940
|
+
// 'ImpersonateButton', '/v1/actor_tokens'; now uiDataCoupled: seed a user, then `fetch` issues
|
|
941
|
+
// the EXACT request `ImpersonateButton`'s click handler sends (POST /v1/actor_tokens with
|
|
942
|
+
// actor.sub `dashboard_admin`) — there is no separate list endpoint, the button reads its own
|
|
943
|
+
// POST response — → `render` mounts the mirror's OWN exported `ImpersonateButton` over the
|
|
944
|
+
// seeded user → assert the minted token really carries the seeded user + the button's actor
|
|
945
|
+
// sub. Failable: a missing/unseeded user 404s the actor-token mint.
|
|
946
|
+
done('clerk.ui.impersonate', 'ui', 'UI mirror: impersonate / sign-in-as a user (data-coupled)', 'ui', 'niche', uiDataCoupled({
|
|
947
|
+
withRoot,
|
|
948
|
+
seed: (h) => h({ m: 'POST', p: '/v1/users', b: mkUser('ui-impersonate@twin.test') }),
|
|
949
|
+
fetch: async (h) => {
|
|
950
|
+
// re-fetch the seeded user (seed's response isn't threaded into fetch) via the list.
|
|
951
|
+
const user = (await h({ m: 'GET', p: '/v1/users' })).body[0];
|
|
952
|
+
const minted = await h({ m: 'POST', p: '/v1/actor_tokens', b: { user_id: user.id, actor: { sub: 'dashboard_admin' } } });
|
|
953
|
+
return Object.assign(minted, { user });
|
|
954
|
+
},
|
|
955
|
+
render: ({ user }) => renderToStaticMarkup(createElement(ImpersonateButton, { user })),
|
|
956
|
+
assert: (data, markup) => !!markup && markup.includes('Impersonate user') &&
|
|
957
|
+
ok(data) && field(data, 'user_id') === data.user.id && field(data, 'actor')?.sub === 'dashboard_admin',
|
|
958
|
+
})),
|
|
959
|
+
// ── Long-tail Backend API surface (todo) ──────────────────────────────────────────
|
|
960
|
+
done('clerk.clients.verify', 'clients', 'Clients: verify a session token → bound client + session', 'api', 'common', () => withRoot(async (h) => {
|
|
961
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
962
|
+
const s = await h({ m: 'POST', p: '/v1/sessions', b: { user_id: id(u) } });
|
|
963
|
+
const tok = await h({ m: 'POST', p: `/v1/sessions/${id(s)}/tokens` });
|
|
964
|
+
const jwt = field(tok, 'jwt');
|
|
965
|
+
const verified = await h({ m: 'POST', p: '/v1/clients/verify', b: { token: jwt } });
|
|
966
|
+
if (!ok(verified) || field(verified, 'object') !== 'client' || !field(verified, 'session_ids').includes(id(s)))
|
|
967
|
+
return false;
|
|
968
|
+
const bad = await h({ m: 'POST', p: '/v1/clients/verify', b: { token: 'not.a.jwt' } });
|
|
969
|
+
const missing = await h({ m: 'POST', p: '/v1/clients/verify', b: {} });
|
|
970
|
+
return bad.status === 401 && missing.status === 422;
|
|
971
|
+
})),
|
|
972
|
+
done('clerk.clients.list', 'clients', 'Clients: list active clients', 'api', 'niche', () => withRoot(async (h) => {
|
|
973
|
+
const empty = await h({ m: 'GET', p: '/v1/clients' });
|
|
974
|
+
if (!ok(empty) || field(empty, 'data').length !== 0)
|
|
975
|
+
return false;
|
|
976
|
+
const u = await h({ m: 'POST', p: '/v1/users', b: mkUser() });
|
|
977
|
+
const s = await h({ m: 'POST', p: '/v1/sessions', b: { user_id: id(u) } });
|
|
978
|
+
const list = await h({ m: 'GET', p: '/v1/clients' });
|
|
979
|
+
const clients = field(list, 'data');
|
|
980
|
+
if (!ok(list) || clients.length !== 1 || !clients[0].session_ids.includes(id(s)))
|
|
981
|
+
return false;
|
|
982
|
+
// revoking the session drops the active client
|
|
983
|
+
await h({ m: 'POST', p: `/v1/sessions/${id(s)}/revoke` });
|
|
984
|
+
const after = await h({ m: 'GET', p: '/v1/clients' });
|
|
985
|
+
return field(after, 'data').length === 0;
|
|
986
|
+
})),
|
|
987
|
+
done('clerk.redirect_urls.crud', 'redirect_urls', 'Redirect URLs allowlist CRUD', 'api', 'niche', () => withRoot(async (h) => {
|
|
988
|
+
const r = await h({ m: 'POST', p: '/v1/redirect_urls', b: { url: 'https://app.test/callback' } });
|
|
989
|
+
if (!ok(r) || field(r, 'object') !== 'redirect_url' || field(r, 'url') !== 'https://app.test/callback')
|
|
990
|
+
return false;
|
|
991
|
+
const get = await h({ m: 'GET', p: `/v1/redirect_urls/${id(r)}` });
|
|
992
|
+
const list = await h({ m: 'GET', p: '/v1/redirect_urls' });
|
|
993
|
+
const del = await h({ m: 'DELETE', p: `/v1/redirect_urls/${id(r)}` });
|
|
994
|
+
const gone = await h({ m: 'GET', p: `/v1/redirect_urls/${id(r)}` });
|
|
995
|
+
const noUrl = await h({ m: 'POST', p: '/v1/redirect_urls', b: {} });
|
|
996
|
+
return ok(get) && id(get) === id(r) && list.body.length === 1 && ok(del) && field(del, 'deleted') === true && gone.status === 404 && noUrl.status === 422;
|
|
997
|
+
})),
|
|
998
|
+
done('clerk.oauth_apps.crud', 'oauth_applications', 'OAuth applications CRUD (+ secret rotation)', 'api', 'niche', () => withRoot(async (h) => {
|
|
999
|
+
const a = await h({ m: 'POST', p: '/v1/oauth_applications', b: { name: 'My App', callback_url: 'https://app.test/cb' } });
|
|
1000
|
+
const secret = field(a, 'client_secret');
|
|
1001
|
+
if (!ok(a) || field(a, 'object') !== 'oauth_application' || typeof field(a, 'client_id') !== 'string' || !secret.startsWith('secret_'))
|
|
1002
|
+
return false;
|
|
1003
|
+
const get = await h({ m: 'GET', p: `/v1/oauth_applications/${id(a)}` });
|
|
1004
|
+
const upd = await h({ m: 'PATCH', p: `/v1/oauth_applications/${id(a)}`, b: { name: 'Renamed' } });
|
|
1005
|
+
const rot = await h({ m: 'POST', p: `/v1/oauth_applications/${id(a)}/rotate_secret` });
|
|
1006
|
+
const list = await h({ m: 'GET', p: '/v1/oauth_applications' });
|
|
1007
|
+
const del = await h({ m: 'DELETE', p: `/v1/oauth_applications/${id(a)}` });
|
|
1008
|
+
const noName = await h({ m: 'POST', p: '/v1/oauth_applications', b: {} });
|
|
1009
|
+
return ok(get) && ok(upd) && field(upd, 'name') === 'Renamed' && ok(rot) && field(rot, 'client_secret') !== secret
|
|
1010
|
+
&& field(list, 'data').length === 1 && ok(del) && field(del, 'deleted') === true && noName.status === 422;
|
|
1011
|
+
})),
|
|
1012
|
+
done('clerk.saml_connections.crud', 'saml_connections', 'SAML connections CRUD', 'api', 'niche', () => withRoot(async (h) => {
|
|
1013
|
+
const c = await h({ m: 'POST', p: '/v1/saml_connections', b: { name: 'Okta', domain: 'acme.test', provider: 'saml_okta' } });
|
|
1014
|
+
if (!ok(c) || field(c, 'object') !== 'saml_connection' || field(c, 'domain') !== 'acme.test' || typeof field(c, 'acs_url') !== 'string')
|
|
1015
|
+
return false;
|
|
1016
|
+
const get = await h({ m: 'GET', p: `/v1/saml_connections/${id(c)}` });
|
|
1017
|
+
const upd = await h({ m: 'PATCH', p: `/v1/saml_connections/${id(c)}`, b: { active: true } });
|
|
1018
|
+
const list = await h({ m: 'GET', p: '/v1/saml_connections' });
|
|
1019
|
+
const del = await h({ m: 'DELETE', p: `/v1/saml_connections/${id(c)}` });
|
|
1020
|
+
const missing = await h({ m: 'POST', p: '/v1/saml_connections', b: { name: 'X' } });
|
|
1021
|
+
return ok(get) && id(get) === id(c) && ok(upd) && field(upd, 'active') === true && field(list, 'data').length === 1
|
|
1022
|
+
&& ok(del) && field(del, 'deleted') === true && missing.status === 422;
|
|
1023
|
+
})),
|
|
1024
|
+
done('clerk.domains.crud', 'domains', 'Instance domains (primary + satellite domains)', 'api', 'niche', () => withRoot(async (h) => {
|
|
1025
|
+
const primary = await h({ m: 'POST', p: '/v1/domains', b: { name: 'app.test' } });
|
|
1026
|
+
if (!ok(primary) || field(primary, 'is_satellite') !== false || typeof field(primary, 'accounts_portal_url') !== 'string')
|
|
1027
|
+
return false;
|
|
1028
|
+
// a second primary is rejected; satellites are allowed
|
|
1029
|
+
const dupPrimary = await h({ m: 'POST', p: '/v1/domains', b: { name: 'other.test' } });
|
|
1030
|
+
const sat = await h({ m: 'POST', p: '/v1/domains', b: { name: 'sat.test', is_satellite: true } });
|
|
1031
|
+
if (dupPrimary.status !== 422 || !ok(sat) || field(sat, 'is_satellite') !== true)
|
|
1032
|
+
return false;
|
|
1033
|
+
const list = await h({ m: 'GET', p: '/v1/domains' });
|
|
1034
|
+
const upd = await h({ m: 'PATCH', p: `/v1/domains/${id(sat)}`, b: { proxy_url: 'https://proxy.test' } });
|
|
1035
|
+
const delPrimary = await h({ m: 'DELETE', p: `/v1/domains/${id(primary)}` });
|
|
1036
|
+
const delSat = await h({ m: 'DELETE', p: `/v1/domains/${id(sat)}` });
|
|
1037
|
+
return field(list, 'data').length === 2 && ok(upd) && field(upd, 'proxy_url') === 'https://proxy.test'
|
|
1038
|
+
&& delPrimary.status === 422 && ok(delSat) && field(delSat, 'deleted') === true;
|
|
1039
|
+
})),
|
|
1040
|
+
done('clerk.instance_settings.update', 'instance', 'Instance settings: read + update (restrictions / MFA / attack protection)', 'api', 'common', () => withRoot(async (h) => {
|
|
1041
|
+
const get = await h({ m: 'GET', p: '/v1/instance' });
|
|
1042
|
+
if (!ok(get) || field(get, 'object') !== 'instance' || field(get, 'restrictions').allowlist !== false)
|
|
1043
|
+
return false;
|
|
1044
|
+
const upd = await h({ m: 'PATCH', p: '/v1/instance', b: { restrictions: { allowlist: true }, mfa: { totp: true } } });
|
|
1045
|
+
const r = field(upd, 'restrictions');
|
|
1046
|
+
const mfa = field(upd, 'mfa');
|
|
1047
|
+
// deep-merge: the untouched restriction keys survive alongside the new one
|
|
1048
|
+
if (r.allowlist !== true || r.blocklist !== false || mfa.totp !== true)
|
|
1049
|
+
return false;
|
|
1050
|
+
// the change persists on a subsequent read, and the focused sub-resource path works too
|
|
1051
|
+
const after = await h({ m: 'GET', p: '/v1/instance' });
|
|
1052
|
+
const sub = await h({ m: 'PATCH', p: '/v1/instance/restrictions', b: { blocklist: true } });
|
|
1053
|
+
return field(after, 'restrictions').allowlist === true && field(sub, 'restrictions').blocklist === true && field(sub, 'restrictions').allowlist === true;
|
|
1054
|
+
})),
|
|
1055
|
+
done('clerk.waitlist_entries.crud', 'waitlist', 'Waitlist entries: create + list (idempotent on email)', 'api', 'niche', () => withRoot(async (h) => {
|
|
1056
|
+
const a = await h({ m: 'POST', p: '/v1/waitlist_entries', b: { email_address: 'wait1@twin.test' } });
|
|
1057
|
+
if (!ok(a) || field(a, 'object') !== 'waitlist_entry' || field(a, 'email_address') !== 'wait1@twin.test' || field(a, 'status') !== 'pending')
|
|
1058
|
+
return false;
|
|
1059
|
+
if (field(a, 'is_locked') !== false || field(a, 'invitation') !== null || typeof field(a, 'created_at') !== 'number')
|
|
1060
|
+
return false;
|
|
1061
|
+
// idempotent on email_address: a second POST returns the SAME entry, not a new one
|
|
1062
|
+
const dup = await h({ m: 'POST', p: '/v1/waitlist_entries', b: { email_address: 'wait1@twin.test' } });
|
|
1063
|
+
if (!ok(dup) || id(dup) !== id(a))
|
|
1064
|
+
return false;
|
|
1065
|
+
await h({ m: 'POST', p: '/v1/waitlist_entries', b: { email_address: 'wait2@twin.test' } });
|
|
1066
|
+
const l = await h({ m: 'GET', p: '/v1/waitlist_entries' });
|
|
1067
|
+
const data = field(l, 'data');
|
|
1068
|
+
if (!ok(l) || !Array.isArray(data) || data.length !== 2 || field(l, 'total_count') !== 2)
|
|
1069
|
+
return false;
|
|
1070
|
+
// status filter narrows; an unknown status yields none
|
|
1071
|
+
const filtered = await h({ m: 'GET', p: '/v1/waitlist_entries?status=invited' });
|
|
1072
|
+
if (field(filtered, 'data').length !== 0)
|
|
1073
|
+
return false;
|
|
1074
|
+
// negative: a missing/invalid email is a 422
|
|
1075
|
+
const bad = await h({ m: 'POST', p: '/v1/waitlist_entries', b: { email_address: 'not-an-email' } });
|
|
1076
|
+
return bad.status === 422;
|
|
1077
|
+
})),
|
|
1078
|
+
done('clerk.proxy_checks.create', 'proxy', 'Proxy checks: validate a proxy_url for a domain', 'api', 'niche', () => withRoot(async (h) => {
|
|
1079
|
+
const dom = await h({ m: 'POST', p: '/v1/domains', b: { name: 'twin.test' } });
|
|
1080
|
+
if (!ok(dom))
|
|
1081
|
+
return false;
|
|
1082
|
+
const chk = await h({ m: 'POST', p: '/v1/proxy_checks', b: { domain_id: id(dom), proxy_url: 'https://twin.test/__clerk' } });
|
|
1083
|
+
if (!ok(chk) || field(chk, 'object') !== 'proxy_check' || field(chk, 'domain_id') !== id(dom))
|
|
1084
|
+
return false;
|
|
1085
|
+
if (field(chk, 'proxy_url') !== 'https://twin.test/__clerk' || field(chk, 'successful') !== true || typeof field(chk, 'last_run_at') !== 'number')
|
|
1086
|
+
return false;
|
|
1087
|
+
// negative: unknown domain → 404; malformed (non-https) proxy_url → 422; missing field → 422
|
|
1088
|
+
const noDom = await h({ m: 'POST', p: '/v1/proxy_checks', b: { domain_id: 'dmn_nope', proxy_url: 'https://twin.test/__clerk' } });
|
|
1089
|
+
const badUrl = await h({ m: 'POST', p: '/v1/proxy_checks', b: { domain_id: id(dom), proxy_url: 'http://insecure' } });
|
|
1090
|
+
const missing = await h({ m: 'POST', p: '/v1/proxy_checks', b: { domain_id: id(dom) } });
|
|
1091
|
+
return noDom.status === 404 && badUrl.status === 422 && missing.status === 422;
|
|
1092
|
+
})),
|
|
1093
|
+
done('clerk.beta_features.update', 'beta', 'Beta features: update beta instance settings', 'api', 'niche', () => withRoot(async (h) => {
|
|
1094
|
+
// defaults are present on a fresh instance
|
|
1095
|
+
const before = await h({ m: 'GET', p: '/v1/instance' });
|
|
1096
|
+
if (!ok(before) || field(before, 'progressive_sign_up') !== false || field(before, 'test_mode') !== false)
|
|
1097
|
+
return false;
|
|
1098
|
+
const upd = await h({ m: 'PATCH', p: '/v1/beta_features/instance_settings', b: { progressive_sign_up: true, test_mode: true, from_email_address: 'hello' } });
|
|
1099
|
+
if (!ok(upd) || field(upd, 'object') !== 'instance' || field(upd, 'progressive_sign_up') !== true || field(upd, 'test_mode') !== true || field(upd, 'from_email_address') !== 'hello')
|
|
1100
|
+
return false;
|
|
1101
|
+
// the change persists on a subsequent read and does NOT clobber other instance settings
|
|
1102
|
+
const after = await h({ m: 'GET', p: '/v1/instance' });
|
|
1103
|
+
if (field(after, 'progressive_sign_up') !== true || field(after, 'restrictions').allowlist !== false)
|
|
1104
|
+
return false;
|
|
1105
|
+
// negative: an unmodeled beta sub-resource path is not a 200 write
|
|
1106
|
+
const bad = await h({ m: 'PATCH', p: '/v1/beta_features/nope', b: { x: 1 } });
|
|
1107
|
+
return bad.status === 404;
|
|
1108
|
+
})),
|
|
1109
|
+
// ── New honest niche todos surfaced from the real Backend API (keep todo>0) ──
|
|
1110
|
+
// Machine auth (M2M tokens, GA 2025-10): the Backend API exposes /machines + /m2m_tokens —
|
|
1111
|
+
// create/list a machine, then mint/list/revoke an M2M token for it. None modeled yet.
|
|
1112
|
+
todo('clerk.machines.crud', 'machine_auth', 'Machines: create/list (POST/GET /machines)', 'api', 'niche'),
|
|
1113
|
+
todo('clerk.m2m_tokens.crud', 'machine_auth', 'M2M tokens: create/list/revoke (POST/GET /m2m_tokens)', 'api', 'niche'),
|
|
1114
|
+
// The Backend API exposes the instance’s sign-up attempts: GET /sign_ups (and GET /sign_ups/:id).
|
|
1115
|
+
todo('clerk.sign_ups.list', 'sign_ups', 'Sign-ups: list/retrieve attempts (GET /sign_ups)', 'api', 'niche'),
|
|
1116
|
+
// TWIN-88 (reviews/2026-07-06-philosophy-first/PART2-VENDOR-PACKS.md finding F2): the twin
|
|
1117
|
+
// fakes auth — ANY Backend API key is accepted, so the real vendor's 401-on-bad-key is not
|
|
1118
|
+
// modeled (see clerk-twin.ts:9-10 and the README's Auth section). A known, written-down gap
|
|
1119
|
+
// that now has a manifest entry instead of only living in prose (contrast
|
|
1120
|
+
// openai.errors.auth_401/openai.auth.faked, openweather.auth.missing_key, polar's auth.token).
|
|
1121
|
+
todo('clerk.auth.bad_key_401', 'security', 'Backend API key auth: reject a missing/invalid key with a real 401 (currently faked — any key accepted)', 'api', 'niche'),
|
|
1122
|
+
// ── Pull-surface coverage audit gaps (TWIN-46 / G2) — filed as manifest todos, which is
|
|
1123
|
+
// what the demand-ordered build list is drawn from. See pull-audit.json (repo root) for the
|
|
1124
|
+
// per-pack pull-vs-read-surface evidence.
|
|
1125
|
+
todo('clerk.connector.pull_org_invitations', 'connector', 'Connector: pull organization invitations from the real account', 'connector', 'core'),
|
|
1126
|
+
todo('clerk.connector.pull_invitations', 'connector', 'Connector: pull instance-level invitations from the real account', 'connector', 'common'),
|
|
1127
|
+
todo('clerk.connector.pull_access_lists', 'connector', 'Connector: pull the allowlist/blocklist from the real account', 'connector', 'common'),
|
|
1128
|
+
];
|
|
1129
|
+
/** Run the Clerk capability manifest into the categorized gap report. */
|
|
1130
|
+
export function clerkCapabilities() {
|
|
1131
|
+
return checkCapabilities('clerk', CLERK_CAPABILITIES);
|
|
1132
|
+
}
|