diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c719670..68460c46 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,9 +70,29 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: rm -rf node_modules/.cache/rollup-plugin-typescript2 + - name: Determine version specifier from commit title + id: specifier + env: + COMMIT_MSG: ${{ github.event.head_commit.message }} + run: | + TITLE=$(printf '%s' "$COMMIT_MSG" | head -n1) + echo "Commit title: $TITLE" + # Breaking change (feat!:, fix!:, feat(scope)!:) -> major + if printf '%s' "$TITLE" | grep -qE '^[a-z]+(\(.+\))?!:'; then + SPECIFIER=major + elif printf '%s' "$TITLE" | grep -qE '^feat(\(.+\))?:'; then + SPECIFIER=minor + elif printf '%s' "$TITLE" | grep -qE '^fix(\(.+\))?:'; then + SPECIFIER=patch + else + # perf/refactor/chore/docs/etc. default to patch + SPECIFIER=patch + fi + echo "Resolved specifier: $SPECIFIER" + echo "specifier=$SPECIFIER" >> "$GITHUB_OUTPUT" - name: Release run: | - npx nx release --specifier=patch --yes + npx nx release --specifier="${{ steps.specifier.outputs.specifier }}" --yes env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index e5c49852..da64f306 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ node_modules !.vscode/extensions.json .cursor +/.env +/.env.local + # misc /.sass-cache /connect.lock diff --git a/apps/console/next.config.js b/apps/console/next.config.js index 5f753952..b6ed75a7 100644 --- a/apps/console/next.config.js +++ b/apps/console/next.config.js @@ -15,51 +15,37 @@ const nextConfig = { svgr: false, }, async rewrites() { + // PostHog reverse proxy (EU region). posthog-js is configured with + // api_host: '/analytics' so the browser only ever hits this first-party + // path (ad-blocker resistant); Next forwards server-side to PostHog. + // + // IMPORTANT: PostHog retired `eu.posthog.com` for ingestion. Static assets + // must go to `eu-assets.i.posthog.com` and everything else (event capture, + // /decide, session recording /s) to `eu.i.posthog.com`. The dedicated + // ingestion subdomain is the supported, durable target. return [ - // Posthog + // Static assets (array.js, recorder.js, ...) -> assets host. { - source: '/analytics/:path*', - destination: 'https://eu.posthog.com/:path*', + source: '/analytics/static/:path*', + destination: 'https://eu-assets.i.posthog.com/static/:path*', }, + // Event ingestion / decide / session recording -> ingestion host. + // posthog-js sends to `/e/`, `/decide/`, `/s/` WITH a trailing slash, so + // both the trailing-slash and bare variants are required (a single + // `:path*` rule does not match `/analytics/e/`). { source: '/analytics/:path*/', - destination: 'https://eu.posthog.com/:path*/', + destination: 'https://eu.i.posthog.com/:path*/', }, - ]; - }, - skipTrailingSlashRedirect: true, - async headers() { - async function getMyIp() { - const x = await fetch('https://api.ipify.org'); - // const x = await fetch('https://api.my-ip.io/ip') - return await x.text(); - } - const ip = await getMyIp(); - return [ { source: '/analytics/:path*', - headers: [ - { key: 'X-Forwarded-Proto', value: 'https' }, - { - key: 'X-Forwarded-Host', - value: 'https://www.useflytrap.com', - }, - { key: 'X-Forwarded-For', value: ip }, - ], - }, - { - source: '/analytics/:path*/', - headers: [ - { key: 'X-Forwarded-Proto', value: 'https' }, - { - key: 'X-Forwarded-Host', - value: 'https://www.useflytrap.com', - }, - { key: 'X-Forwarded-For', value: ip }, - ], + destination: 'https://eu.i.posthog.com/:path*', }, ]; }, + // Prevent Next from 308-redirecting `/analytics/e/` -> `/analytics/e`, which + // would strip the trailing slash posthog-js depends on. + skipTrailingSlashRedirect: true, experimental: { serverComponentsExternalPackages: ['@xmtp/user-preferences-bindings-wasm'], }, diff --git a/apps/console/src/analytics/events/claimSection/ens-by-api-key-called.ts b/apps/console/src/analytics/events/claimSection/ens-by-api-key-called.ts index 8508bebe..3ae3dcc7 100644 --- a/apps/console/src/analytics/events/claimSection/ens-by-api-key-called.ts +++ b/apps/console/src/analytics/events/claimSection/ens-by-api-key-called.ts @@ -1,3 +1,8 @@ export const ENS_BY_API_KEY_CALLED = 'ENS_BY_API_KEY_CALLED'; -export interface EnsByApiKeyCalledPayload {} +export interface EnsByApiKeyCalledPayload { + /** Where in the UI the lookup was triggered (e.g. 'claim_section'). */ + location: string; + /** Number of domains returned for the supplied API key. */ + domainCount: number; +} diff --git a/apps/console/src/analytics/events/code/code-copied.ts b/apps/console/src/analytics/events/code/code-copied.ts index d0ce9396..9a0d43b4 100644 --- a/apps/console/src/analytics/events/code/code-copied.ts +++ b/apps/console/src/analytics/events/code/code-copied.ts @@ -1,3 +1,10 @@ export const CODE_COPIED = 'CODE_COPIED'; -export interface CodeCopiedPayload {} +export type CodeSnippet = 'integration' | 'dependencies'; + +export interface CodeCopiedPayload { + /** Where in the UI the copy happened (e.g. 'code_section'). */ + location: string; + /** Which snippet was copied: the integration code or the install command. */ + snippet: CodeSnippet; +} diff --git a/apps/console/src/analytics/events/navigation/dashboard-link-clicked.ts b/apps/console/src/analytics/events/navigation/dashboard-link-clicked.ts deleted file mode 100644 index 8fdbd749..00000000 --- a/apps/console/src/analytics/events/navigation/dashboard-link-clicked.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DASHBOARD_LINK_CLICKED = 'DASHBOARD_LINK_CLICKED'; - -export interface DashboardLinkClickedPayload {} diff --git a/apps/console/src/analytics/events/navigation/docs-link-clicked.ts b/apps/console/src/analytics/events/navigation/docs-link-clicked.ts deleted file mode 100644 index 04f2f392..00000000 --- a/apps/console/src/analytics/events/navigation/docs-link-clicked.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DOCS_LINK_CLICKED = 'DOCS_LINK_CLICKED'; - -export interface DocsLinkClickedPayload {} diff --git a/apps/console/src/analytics/events/navigation/index.ts b/apps/console/src/analytics/events/navigation/index.ts index bc695925..5c6e24cb 100644 --- a/apps/console/src/analytics/events/navigation/index.ts +++ b/apps/console/src/analytics/events/navigation/index.ts @@ -1,18 +1,12 @@ -import { - DASHBOARD_LINK_CLICKED, - DashboardLinkClickedPayload, -} from './dashboard-link-clicked'; -import { DOCS_LINK_CLICKED, DocsLinkClickedPayload } from './docs-link-clicked'; +import { LINK_CLICKED, LinkClickedPayload } from './link-clicked'; import { PROFILE_VIEWED, ProfileViewedPayload } from './profile-viewed'; export const NAVIGATION_EVENTS = { - DOCS_LINK_CLICKED, + LINK_CLICKED, PROFILE_VIEWED, - DASHBOARD_LINK_CLICKED, } as const; export interface NavigationEventPayload { - [DOCS_LINK_CLICKED]: DocsLinkClickedPayload; + [LINK_CLICKED]: LinkClickedPayload; [PROFILE_VIEWED]: ProfileViewedPayload; - [DASHBOARD_LINK_CLICKED]: DashboardLinkClickedPayload; } diff --git a/apps/console/src/analytics/events/navigation/link-clicked.ts b/apps/console/src/analytics/events/navigation/link-clicked.ts new file mode 100644 index 00000000..762a8ddf --- /dev/null +++ b/apps/console/src/analytics/events/navigation/link-clicked.ts @@ -0,0 +1,10 @@ +export const LINK_CLICKED = 'LINK_CLICKED'; + +export type LinkTarget = 'docs' | 'dashboard'; + +export interface LinkClickedPayload { + /** Which external destination the user navigated to. */ + target: LinkTarget; + /** Where in the UI the link lives (e.g. 'navbar', 'claim_section'). */ + location: string; +} diff --git a/apps/console/src/analytics/events/navigation/profile-viewed.ts b/apps/console/src/analytics/events/navigation/profile-viewed.ts index dec1398a..ec06f1aa 100644 --- a/apps/console/src/analytics/events/navigation/profile-viewed.ts +++ b/apps/console/src/analytics/events/navigation/profile-viewed.ts @@ -2,5 +2,7 @@ export const PROFILE_VIEWED = 'PROFILE_VIEWED'; export interface ProfileViewedPayload { ens: string; + /** Where the profile was opened from (e.g. 'demo_card'). */ + location: string; chainId?: number; } diff --git a/apps/console/src/analytics/events/network/network-changed.ts b/apps/console/src/analytics/events/network/network-changed.ts index 6af14f5e..6e2197a7 100644 --- a/apps/console/src/analytics/events/network/network-changed.ts +++ b/apps/console/src/analytics/events/network/network-changed.ts @@ -1,5 +1,9 @@ export const NETWORK_CHANGED = 'NETWORK_CHANGED'; +export type NetworkName = 'mainnet' | 'sepolia'; + export interface NetworkChangedPayload { chainId: number; + /** Human-readable network the user switched to. */ + network: NetworkName; } diff --git a/apps/console/src/analytics/events/plugins/dentity-disabled.ts b/apps/console/src/analytics/events/plugins/dentity-disabled.ts deleted file mode 100644 index 22298504..00000000 --- a/apps/console/src/analytics/events/plugins/dentity-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DENTITY_DISABLED = 'DENTITY_DISABLED'; - -export interface DentityDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/dentity-enabled.ts b/apps/console/src/analytics/events/plugins/dentity-enabled.ts deleted file mode 100644 index 0daf4dc6..00000000 --- a/apps/console/src/analytics/events/plugins/dentity-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DENTITY_ENABLED = 'DENTITY_ENABLED'; - -export interface DentityEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/efp-disabled.ts b/apps/console/src/analytics/events/plugins/efp-disabled.ts deleted file mode 100644 index adc5b174..00000000 --- a/apps/console/src/analytics/events/plugins/efp-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const EFP_DISABLED = 'EFP_DISABLED'; - -export interface EfpDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/efp-enabled.ts b/apps/console/src/analytics/events/plugins/efp-enabled.ts deleted file mode 100644 index 2c1819db..00000000 --- a/apps/console/src/analytics/events/plugins/efp-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const EFP_ENABLED = 'EFP_ENABLED'; - -export interface EfpEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/index.ts b/apps/console/src/analytics/events/plugins/index.ts index 26b2de7d..ca32fba8 100644 --- a/apps/console/src/analytics/events/plugins/index.ts +++ b/apps/console/src/analytics/events/plugins/index.ts @@ -1,47 +1,15 @@ -import { DENTITY_ENABLED, DentityEnabledPayload } from './dentity-enabled'; -import { DENTITY_DISABLED, DentityDisabledPayload } from './dentity-disabled'; -import { EFP_DISABLED, EfpDisabledPayload } from './efp-disabled'; -import { EFP_ENABLED, EfpEnabledPayload } from './efp-enabled'; +import { PLUGIN_TOGGLED, PluginToggledPayload } from './plugin-toggled'; import { - JUST_VERIFIED_DISABLED, - JustVerifiedDisabledPayload, -} from './just-verified-disabled'; -import { - JUST_VERIFIED_ENABLED, - JustVerifiedEnabledPayload, -} from './just-verified-enabled'; -import { - JUST_VERIFIED_EVENTS, - JustVerifiedEventsPayload, -} from './justVerified'; -import { POAP_DISABLED, PoapDisabledPayload } from './poap-disabled'; -import { POAP_ENABLED, PoapEnabledPayload } from './poap-enabled'; -import { XMTP_DISABLED, XmtpDisabledPayload } from './xmtp-disabled'; -import { XMTP_ENABLED, XmtpEnabledPayload } from './xmtp-enabled'; + VERIFICATION_TOGGLED, + VerificationToggledPayload, +} from './verification-toggled'; export const PLUGINS_EVENTS = { - JUST_VERIFIED_DISABLED, - JUST_VERIFIED_ENABLED, - EFP_DISABLED, - EFP_ENABLED, - POAP_DISABLED, - POAP_ENABLED, - XMTP_DISABLED, - XMTP_ENABLED, - DENTITY_DISABLED, - DENTITY_ENABLED, - ...JUST_VERIFIED_EVENTS, + PLUGIN_TOGGLED, + VERIFICATION_TOGGLED, } as const; -export interface PluginsEventPayload extends JustVerifiedEventsPayload { - [JUST_VERIFIED_DISABLED]: JustVerifiedDisabledPayload; - [JUST_VERIFIED_ENABLED]: JustVerifiedEnabledPayload; - [EFP_DISABLED]: EfpDisabledPayload; - [EFP_ENABLED]: EfpEnabledPayload; - [POAP_DISABLED]: PoapDisabledPayload; - [POAP_ENABLED]: PoapEnabledPayload; - [XMTP_DISABLED]: XmtpDisabledPayload; - [XMTP_ENABLED]: XmtpEnabledPayload; - [DENTITY_DISABLED]: DentityDisabledPayload; - [DENTITY_ENABLED]: DentityEnabledPayload; +export interface PluginsEventPayload { + [PLUGIN_TOGGLED]: PluginToggledPayload; + [VERIFICATION_TOGGLED]: VerificationToggledPayload; } diff --git a/apps/console/src/analytics/events/plugins/just-verified-disabled.ts b/apps/console/src/analytics/events/plugins/just-verified-disabled.ts deleted file mode 100644 index 04e21599..00000000 --- a/apps/console/src/analytics/events/plugins/just-verified-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const JUST_VERIFIED_DISABLED = 'JUST_VERIFIED_DISABLED'; - -export interface JustVerifiedDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/just-verified-enabled.ts b/apps/console/src/analytics/events/plugins/just-verified-enabled.ts deleted file mode 100644 index 638c66c7..00000000 --- a/apps/console/src/analytics/events/plugins/just-verified-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const JUST_VERIFIED_ENABLED = 'JUST_VERIFIED_ENABLED'; - -export interface JustVerifiedEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/discord-disabled.ts b/apps/console/src/analytics/events/plugins/justVerified/discord-disabled.ts deleted file mode 100644 index daaac7d4..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/discord-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DISCORD_DISABLED = 'DISCORD_DISABLED'; - -export interface DiscordDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/discord-enabled.ts b/apps/console/src/analytics/events/plugins/justVerified/discord-enabled.ts deleted file mode 100644 index 87d874ff..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/discord-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DISCORD_ENABLED = 'DISCORD_ENABLED'; - -export interface DiscordEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/email-disabled.ts b/apps/console/src/analytics/events/plugins/justVerified/email-disabled.ts deleted file mode 100644 index bce005b9..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/email-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const EMAIL_DISABLED = 'EMAIL_DISABLED'; - -export interface EmailDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/email-enabled.ts b/apps/console/src/analytics/events/plugins/justVerified/email-enabled.ts deleted file mode 100644 index f1ca9986..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/email-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const EMAIL_ENABLED = 'EMAIL_ENABLED'; - -export interface EmailEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/github-disabled.ts b/apps/console/src/analytics/events/plugins/justVerified/github-disabled.ts deleted file mode 100644 index 279b1c9c..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/github-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const GITHUB_DISABLED = 'GITHUB_DISABLED'; - -export interface GithubDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/github-enabled.ts b/apps/console/src/analytics/events/plugins/justVerified/github-enabled.ts deleted file mode 100644 index f2238060..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/github-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const GITHUB_ENABLED = 'GITHUB_ENABLED'; - -export interface GithubEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/index.ts b/apps/console/src/analytics/events/plugins/justVerified/index.ts deleted file mode 100644 index 9b9aea16..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { DISCORD_DISABLED, DiscordDisabledPayload } from './discord-disabled'; -import { DISCORD_ENABLED, DiscordEnabledPayload } from './discord-enabled'; -import { EMAIL_DISABLED, EmailDisabledPayload } from './email-disabled'; -import { EMAIL_ENABLED, EmailEnabledPayload } from './email-enabled'; -import { GITHUB_DISABLED, GithubDisabledPayload } from './github-disabled'; -import { GITHUB_ENABLED, GithubEnabledPayload } from './github-enabled'; -import { - TELEGRAM_DISABLED, - TelegramDisabledPayload, -} from './telegram-disabled'; -import { TELEGRAM_ENABLED, TelegramEnabledPayload } from './telegram-enabled'; -import { TWITTER_DISABLED, TwitterDisabledPayload } from './twitter-disabled'; -import { TWITTER_ENABLED, TwitterEnabledPayload } from './twitter-enabled'; - -export const JUST_VERIFIED_EVENTS = { - DISCORD_DISABLED, - DISCORD_ENABLED, - EMAIL_DISABLED, - EMAIL_ENABLED, - GITHUB_DISABLED, - GITHUB_ENABLED, - TELEGRAM_DISABLED, - TELEGRAM_ENABLED, - TWITTER_DISABLED, - TWITTER_ENABLED, -} as const; - -export interface JustVerifiedEventsPayload { - [DISCORD_DISABLED]: DiscordDisabledPayload; - [DISCORD_ENABLED]: DiscordEnabledPayload; - [EMAIL_DISABLED]: EmailDisabledPayload; - [EMAIL_ENABLED]: EmailEnabledPayload; - [GITHUB_DISABLED]: GithubDisabledPayload; - [GITHUB_ENABLED]: GithubEnabledPayload; - [TELEGRAM_DISABLED]: TelegramDisabledPayload; - [TELEGRAM_ENABLED]: TelegramEnabledPayload; - [TWITTER_DISABLED]: TwitterDisabledPayload; - [TWITTER_ENABLED]: TwitterEnabledPayload; -} diff --git a/apps/console/src/analytics/events/plugins/justVerified/telegram-disabled.ts b/apps/console/src/analytics/events/plugins/justVerified/telegram-disabled.ts deleted file mode 100644 index 43030e15..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/telegram-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const TELEGRAM_DISABLED = 'TELEGRAM_DISABLED'; - -export interface TelegramDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/telegram-enabled.ts b/apps/console/src/analytics/events/plugins/justVerified/telegram-enabled.ts deleted file mode 100644 index 064870ac..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/telegram-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const TELEGRAM_ENABLED = 'TELEGRAM_ENABLED'; - -export interface TelegramEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/twitter-disabled.ts b/apps/console/src/analytics/events/plugins/justVerified/twitter-disabled.ts deleted file mode 100644 index 340e4970..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/twitter-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const TWITTER_DISABLED = 'TWITTER_DISABLED'; - -export interface TwitterDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/twitter-enabled.ts b/apps/console/src/analytics/events/plugins/justVerified/twitter-enabled.ts deleted file mode 100644 index 7afd8f7a..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/twitter-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const TWITTER_ENABLED = 'TWITTER_ENABLED'; - -export interface TwitterEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/plugin-toggled.ts b/apps/console/src/analytics/events/plugins/plugin-toggled.ts new file mode 100644 index 00000000..2daacf7b --- /dev/null +++ b/apps/console/src/analytics/events/plugins/plugin-toggled.ts @@ -0,0 +1,13 @@ +export const PLUGIN_TOGGLED = 'PLUGIN_TOGGLED'; + +export type PluginName = + | 'efp' + | 'poap' + | 'xmtp' + | 'dentity' + | 'just_verified'; + +export interface PluginToggledPayload { + plugin: PluginName; + enabled: boolean; +} diff --git a/apps/console/src/analytics/events/plugins/poap-disabled.ts b/apps/console/src/analytics/events/plugins/poap-disabled.ts deleted file mode 100644 index aeec0b0f..00000000 --- a/apps/console/src/analytics/events/plugins/poap-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const POAP_DISABLED = 'POAP_DISABLED'; - -export interface PoapDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/poap-enabled.ts b/apps/console/src/analytics/events/plugins/poap-enabled.ts deleted file mode 100644 index 676920bf..00000000 --- a/apps/console/src/analytics/events/plugins/poap-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const POAP_ENABLED = 'POAP_ENABLED'; - -export interface PoapEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/verification-toggled.ts b/apps/console/src/analytics/events/plugins/verification-toggled.ts new file mode 100644 index 00000000..392db143 --- /dev/null +++ b/apps/console/src/analytics/events/plugins/verification-toggled.ts @@ -0,0 +1,13 @@ +export const VERIFICATION_TOGGLED = 'VERIFICATION_TOGGLED'; + +export type VerificationProvider = + | 'twitter' + | 'telegram' + | 'github' + | 'discord' + | 'email'; + +export interface VerificationToggledPayload { + provider: VerificationProvider; + enabled: boolean; +} diff --git a/apps/console/src/analytics/events/plugins/xmtp-disabled.ts b/apps/console/src/analytics/events/plugins/xmtp-disabled.ts deleted file mode 100644 index c806cf9e..00000000 --- a/apps/console/src/analytics/events/plugins/xmtp-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const XMTP_DISABLED = 'XMTP_DISABLED'; - -export interface XmtpDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/xmtp-enabled.ts b/apps/console/src/analytics/events/plugins/xmtp-enabled.ts deleted file mode 100644 index a6070183..00000000 --- a/apps/console/src/analytics/events/plugins/xmtp-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const XMTP_ENABLED = 'XMTP_ENABLED'; - -export interface XmtpEnabledPayload {} diff --git a/apps/console/src/analytics/events/signSection/any-ens-selected.ts b/apps/console/src/analytics/events/signSection/any-ens-selected.ts deleted file mode 100644 index b0b49643..00000000 --- a/apps/console/src/analytics/events/signSection/any-ens-selected.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const ANY_ENS_SELECTED = 'ANY_ENS_SELECTED'; - -export interface AnyEnsSelectedPayload {} diff --git a/apps/console/src/analytics/events/signSection/claimable-ens-selected.ts b/apps/console/src/analytics/events/signSection/claimable-ens-selected.ts deleted file mode 100644 index ed6b503a..00000000 --- a/apps/console/src/analytics/events/signSection/claimable-ens-selected.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const CLAIMABLE_ENS_SELECTED = 'CLAIMABLE_ENS_SELECTED'; - -export interface ClaimableEnsSelectedPayload {} diff --git a/apps/console/src/analytics/events/signSection/ens-selection-changed.ts b/apps/console/src/analytics/events/signSection/ens-selection-changed.ts new file mode 100644 index 00000000..110eb266 --- /dev/null +++ b/apps/console/src/analytics/events/signSection/ens-selection-changed.ts @@ -0,0 +1,10 @@ +export const ENS_SELECTION_CHANGED = 'ENS_SELECTION_CHANGED'; + +export type EnsSelectionMode = 'any' | 'claimable' | 'specific'; + +export interface EnsSelectionChangedPayload { + /** Which sign-in allow-list mode the user picked. */ + mode: EnsSelectionMode; + /** The specific ENS entered, only present when mode === 'specific'. */ + ens?: string; +} diff --git a/apps/console/src/analytics/events/signSection/index.ts b/apps/console/src/analytics/events/signSection/index.ts index 9af93a0d..d0715189 100644 --- a/apps/console/src/analytics/events/signSection/index.ts +++ b/apps/console/src/analytics/events/signSection/index.ts @@ -1,21 +1,12 @@ -import { ANY_ENS_SELECTED, AnyEnsSelectedPayload } from './any-ens-selected'; import { - CLAIMABLE_ENS_SELECTED, - ClaimableEnsSelectedPayload, -} from './claimable-ens-selected'; -import { - SPECIFIC_ENS_SELECTED, - SpecificEnsSelectedPayload, -} from './specific-ens-selected'; + ENS_SELECTION_CHANGED, + EnsSelectionChangedPayload, +} from './ens-selection-changed'; export const SIGN_SECTION_EVENTS = { - ANY_ENS_SELECTED, - SPECIFIC_ENS_SELECTED, - CLAIMABLE_ENS_SELECTED, + ENS_SELECTION_CHANGED, } as const; export interface SignSectionEventPayload { - [ANY_ENS_SELECTED]: AnyEnsSelectedPayload; - [SPECIFIC_ENS_SELECTED]: SpecificEnsSelectedPayload; - [CLAIMABLE_ENS_SELECTED]: ClaimableEnsSelectedPayload; + [ENS_SELECTION_CHANGED]: EnsSelectionChangedPayload; } diff --git a/apps/console/src/analytics/events/signSection/specific-ens-selected.ts b/apps/console/src/analytics/events/signSection/specific-ens-selected.ts deleted file mode 100644 index 21fcacda..00000000 --- a/apps/console/src/analytics/events/signSection/specific-ens-selected.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const SPECIFIC_ENS_SELECTED = 'SPECIFIC_ENS_SELECTED'; - -export interface SpecificEnsSelectedPayload { - ens: string; -} diff --git a/apps/console/src/analytics/index.ts b/apps/console/src/analytics/index.ts index ed7f119e..fca36ef8 100644 --- a/apps/console/src/analytics/index.ts +++ b/apps/console/src/analytics/index.ts @@ -20,20 +20,33 @@ class Index { } posthog.init(key, { api_host: host, + // With a reverse proxy api_host, posthog-js can't infer the app URL, so + // toolbar / session-replay / "view in PostHog" links break without this. + ui_host: 'https://eu.posthog.com', capture_pageview: true, capture_pageleave: true, autocapture: false, + // Only create person profiles once we identify() a wallet; anonymous + // demo traffic is still captured but doesn't spawn empty profiles. + person_profiles: 'identified_only', loaded: (posthog) => { if (process.env.NODE_ENV === 'development') posthog.debug(); }, }); + // Super properties: attached to EVERY event so prod/preview traffic and + // the source app are always sliceable in PostHog. + posthog.register({ + app: 'console', + environment: process.env.NODE_ENV, + }); } } identify(id: string) { if (!analyticsEnabled) return; + // No alias() here: aliasing a distinct_id to itself is a no-op at best and + // can corrupt identity merges. identify() alone links anon -> wallet. posthog.identify(id); - posthog.alias(id); this.register({ id }); } diff --git a/apps/console/src/app/page.tsx b/apps/console/src/app/page.tsx index 0b7ff44f..e77e8c60 100644 --- a/apps/console/src/app/page.tsx +++ b/apps/console/src/app/page.tsx @@ -18,6 +18,7 @@ export default function Page() { const handleEnsClick = (ens: string) => { getAnalyticsClient().track('PROFILE_VIEWED', { ens, + location: 'demo_card', }); }; diff --git a/apps/console/src/components/sections/code/CodeSection/index.tsx b/apps/console/src/components/sections/code/CodeSection/index.tsx index 4ca36802..bc19825e 100644 --- a/apps/console/src/components/sections/code/CodeSection/index.tsx +++ b/apps/console/src/components/sections/code/CodeSection/index.tsx @@ -200,11 +200,18 @@ export default App;`.trim(); ]); const handleDependenciesCopy = () => { + getAnalyticsClient().track('CODE_COPIED', { + location: 'code_section', + snippet: 'dependencies', + }); navigator.clipboard.writeText(dependencies); }; const handleCopy = () => { - getAnalyticsClient().track('CODE_COPIED', {}); + getAnalyticsClient().track('CODE_COPIED', { + location: 'code_section', + snippet: 'integration', + }); navigator.clipboard.writeText(code); }; diff --git a/apps/console/src/components/sections/customizer/ClaimSection/index.tsx b/apps/console/src/components/sections/customizer/ClaimSection/index.tsx index a13b2412..40f20cfe 100644 --- a/apps/console/src/components/sections/customizer/ClaimSection/index.tsx +++ b/apps/console/src/components/sections/customizer/ClaimSection/index.tsx @@ -91,8 +91,12 @@ export const ClaimSection = () => { } ) .then((res) => { - getAnalyticsClient().track('ENS_BY_API_KEY_CALLED', {}); - setEnsByApiKey(res.data.result.data.domains); + const domains = res.data.result.data.domains; + getAnalyticsClient().track('ENS_BY_API_KEY_CALLED', { + location: 'claim_section', + domainCount: domains?.length ?? 0, + }); + setEnsByApiKey(domains); }) .catch((err) => { setEnsByApiKey([]); @@ -179,7 +183,10 @@ export const ClaimSection = () => { target={'_blank'} className={'text-primary'} onClick={() => { - getAnalyticsClient().track('DASHBOARD_LINK_CLICKED', {}); + getAnalyticsClient().track('LINK_CLICKED', { + target: 'dashboard', + location: 'claim_section', + }); }} > Dashboard diff --git a/apps/console/src/components/sections/customizer/Customizer/index.tsx b/apps/console/src/components/sections/customizer/Customizer/index.tsx index 8cf20e65..20c8bac4 100644 --- a/apps/console/src/components/sections/customizer/Customizer/index.tsx +++ b/apps/console/src/components/sections/customizer/Customizer/index.tsx @@ -49,6 +49,7 @@ export const Customizer = ({ mobile }: CustomizerProps) => { onCheckedChange={() => { getAnalyticsClient().track('NETWORK_CHANGED', { chainId: chainId === 1 ? 11155111 : 1, + network: chainId === 1 ? 'sepolia' : 'mainnet', }); switchChainAsync({ chainId: chainId === 1 ? 11155111 : 1, diff --git a/apps/console/src/components/sections/customizer/PluginsSection/Dentity/index.tsx b/apps/console/src/components/sections/customizer/PluginsSection/Dentity/index.tsx index 471090f3..a8b01b0b 100644 --- a/apps/console/src/components/sections/customizer/PluginsSection/Dentity/index.tsx +++ b/apps/console/src/components/sections/customizer/PluginsSection/Dentity/index.tsx @@ -18,7 +18,10 @@ export const Dentity = () => { DentityPlugin, ], }); - getAnalyticsClient().track('DENTITY_ENABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'dentity', + enabled: true, + }); } else { handleJustWeb3Config({ ...config, @@ -26,7 +29,10 @@ export const Dentity = () => { (plugin) => plugin.name !== DentityPlugin.name ), }); - getAnalyticsClient().track('DENTITY_DISABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'dentity', + enabled: false, + }); } }; diff --git a/apps/console/src/components/sections/customizer/PluginsSection/EFP/index.tsx b/apps/console/src/components/sections/customizer/PluginsSection/EFP/index.tsx index 7f608bb9..26ceced3 100644 --- a/apps/console/src/components/sections/customizer/PluginsSection/EFP/index.tsx +++ b/apps/console/src/components/sections/customizer/PluginsSection/EFP/index.tsx @@ -19,7 +19,10 @@ export const EFP = () => { EFPPlugin, ], }); - getAnalyticsClient().track('EFP_ENABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'efp', + enabled: true, + }); } else { handleJustWeb3Config({ ...config, @@ -27,7 +30,10 @@ export const EFP = () => { (plugin) => plugin.name !== EFPPlugin.name ), }); - getAnalyticsClient().track('EFP_DISABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'efp', + enabled: false, + }); } }; diff --git a/apps/console/src/components/sections/customizer/PluginsSection/JustVerified/index.tsx b/apps/console/src/components/sections/customizer/PluginsSection/JustVerified/index.tsx index 6d37d1ca..5f905d6a 100644 --- a/apps/console/src/components/sections/customizer/PluginsSection/JustVerified/index.tsx +++ b/apps/console/src/components/sections/customizer/PluginsSection/JustVerified/index.tsx @@ -67,7 +67,10 @@ export const JustVerified = () => { ), ], }); - getAnalyticsClient().track('JUST_VERIFIED_ENABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'just_verified', + enabled: true, + }); } else { handleJustWeb3Config({ ...config, @@ -75,31 +78,22 @@ export const JustVerified = () => { (plugin) => plugin.name !== 'JustVerifiedPlugin' ), }); - getAnalyticsClient().track('JUST_VERIFIED_DISABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'just_verified', + enabled: false, + }); } }; const handleSocialEnabledAnalytics = (credential: Credentials, unCheck: boolean) => { - switch (credential) { - case 'twitter': - getAnalyticsClient().track(unCheck ? 'TWITTER_DISABLED' : 'TWITTER_ENABLED', {}); - break; - case 'telegram': - getAnalyticsClient().track(unCheck ? 'TELEGRAM_DISABLED' : 'TELEGRAM_ENABLED', {}); - break; - case 'github': - getAnalyticsClient().track(unCheck ? 'GITHUB_DISABLED' : 'GITHUB_ENABLED', {}); - break; - case 'discord': - getAnalyticsClient().track(unCheck ? 'DISCORD_DISABLED' : 'DISCORD_ENABLED', {}); - break; - case 'email': - getAnalyticsClient().track(unCheck ? 'EMAIL_DISABLED' : 'EMAIL_ENABLED', {}); - break; - default: - break; - } + // `Credentials` ('twitter' | 'telegram' | ...) maps 1:1 onto our + // VerificationProvider union, so a single typed event replaces the old + // 10-event switch. + getAnalyticsClient().track('VERIFICATION_TOGGLED', { + provider: credential, + enabled: !unCheck, + }); showToast('success', "Code Updated!", `justverified-social-${credential}`) }; diff --git a/apps/console/src/components/sections/customizer/PluginsSection/POAP/index.tsx b/apps/console/src/components/sections/customizer/PluginsSection/POAP/index.tsx index e3c92f19..d5b640b7 100644 --- a/apps/console/src/components/sections/customizer/PluginsSection/POAP/index.tsx +++ b/apps/console/src/components/sections/customizer/PluginsSection/POAP/index.tsx @@ -19,7 +19,10 @@ export const POAP = () => { POAPPluginInstance, ], }); - getAnalyticsClient().track('POAP_ENABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'poap', + enabled: true, + }); } else { handleJustWeb3Config({ ...config, @@ -27,7 +30,10 @@ export const POAP = () => { (plugin) => plugin.name !== POAPPluginInstance.name ), }); - getAnalyticsClient().track('POAP_DISABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'poap', + enabled: false, + }); } }; diff --git a/apps/console/src/components/sections/customizer/PluginsSection/XMTP/index.tsx b/apps/console/src/components/sections/customizer/PluginsSection/XMTP/index.tsx index 15f801c9..c471f0df 100644 --- a/apps/console/src/components/sections/customizer/PluginsSection/XMTP/index.tsx +++ b/apps/console/src/components/sections/customizer/PluginsSection/XMTP/index.tsx @@ -18,7 +18,10 @@ export const XMTP = () => { XMTPPlugin('production'), ], }); - getAnalyticsClient().track('XMTP_ENABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'xmtp', + enabled: true, + }); } else { handleJustWeb3Config({ ...config, @@ -26,7 +29,10 @@ export const XMTP = () => { (plugin) => plugin.name !== 'XMTPPlugin' ), }); - getAnalyticsClient().track('XMTP_DISABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'xmtp', + enabled: false, + }); }; } diff --git a/apps/console/src/components/sections/customizer/SignSection/index.tsx b/apps/console/src/components/sections/customizer/SignSection/index.tsx index b6b1fcd4..6874be23 100644 --- a/apps/console/src/components/sections/customizer/SignSection/index.tsx +++ b/apps/console/src/components/sections/customizer/SignSection/index.tsx @@ -31,10 +31,14 @@ export const SignSection = () => { const action = e as JustWeb3ProviderConfig['allowedEns']; switch (action) { case 'all': - getAnalyticsClient().track('ANY_ENS_SELECTED', {}); + getAnalyticsClient().track('ENS_SELECTION_CHANGED', { + mode: 'any', + }); break; case 'claimable': - getAnalyticsClient().track('CLAIMABLE_ENS_SELECTED', {}); + getAnalyticsClient().track('ENS_SELECTION_CHANGED', { + mode: 'claimable', + }); break; } handleJustWeb3Config({ @@ -62,7 +66,8 @@ export const SignSection = () => { placeholder="Add ENS" onKeyUp={(e) => { if (e.key === 'Enter') { - getAnalyticsClient().track('SPECIFIC_ENS_SELECTED', { + getAnalyticsClient().track('ENS_SELECTION_CHANGED', { + mode: 'specific', ens: ensInput, }); setEnsList([...ensList, ensInput]); diff --git a/apps/console/src/layout/navbar/index.tsx b/apps/console/src/layout/navbar/index.tsx index 65c66c1a..4b6fdd59 100644 --- a/apps/console/src/layout/navbar/index.tsx +++ b/apps/console/src/layout/navbar/index.tsx @@ -13,7 +13,12 @@ export const Navbar = () => { href={'https://docs.justaname.id'} passHref target="_blank" - onClick={() => getAnalyticsClient().track('DOCS_LINK_CLICKED', {})} + onClick={() => + getAnalyticsClient().track('LINK_CLICKED', { + target: 'docs', + location: 'navbar', + }) + } > diff --git a/docs/guide/wallet-providers/para.md b/docs/guide/wallet-providers/para.md index f7a4f66c..f5e65160 100644 --- a/docs/guide/wallet-providers/para.md +++ b/docs/guide/wallet-providers/para.md @@ -8,19 +8,19 @@ Run the following command to install the necessary packages: {% tabs %} {% tab title="npm" %} -
npm install @justweb3/widget @getpara/react-sdk @getpara/wagmi-v2-integration wagmi @tanstack/react-query ethers
+
npm install @justweb3/widget @getpara/react-sdk @getpara/wagmi-v2-integration wagmi @tanstack/react-query
 
{% endtab %} {% tab title="pnpm" %} ```bash -pnpm install @justweb3/widget @getpara/react-sdk @getpara/wagmi-v2-integration wagmi @tanstack/react-query ethers +pnpm install @justweb3/widget @getpara/react-sdk @getpara/wagmi-v2-integration wagmi @tanstack/react-query ``` {% endtab %} {% tab title="yarn" %} ```bash -yarn add @justweb3/widget @getpara/react-sdk @getpara/wagmi-v2-integration wagmi @tanstack/react-query ethers +yarn add @justweb3/widget @getpara/react-sdk @getpara/wagmi-v2-integration wagmi @tanstack/react-query ``` {% endtab %} {% endtabs %} diff --git a/docs/sdk/JustaName Core SDK/README.md b/docs/sdk/JustaName Core SDK/README.md index fc98e0d4..4fb8e676 100644 --- a/docs/sdk/JustaName Core SDK/README.md +++ b/docs/sdk/JustaName Core SDK/README.md @@ -60,7 +60,7 @@ First, import the JustaName SDK and initialize it with your configuration: ```typescript import { JustaName } from '@justaname.id/sdk'; -import { ethers } from 'ethers'; +import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'; // Initialize the SDK with your configuration const justaname = JustaName.init({ @@ -85,7 +85,7 @@ const justaname = JustaName.init({ }); // Create a signer (for example purposes, we're creating a random wallet) -const signer = ethers.Wallet.createRandom(); +const signer = privateKeyToAccount(generatePrivateKey()); ``` ### Issuing a Subname @@ -98,7 +98,7 @@ async function issueSubname() { chainId: 1 // Ethereum Mainnet }); - const signature = await signer.signMessage(challenge.challenge); + const signature = await signer.signMessage({ message: challenge.challenge }); const response = await justaname.subnames.addSubname( { @@ -125,7 +125,7 @@ async function updateSubname() { chainId: 1 }); - const signature = await signer.signMessage(challenge.challenge); + const signature = await signer.signMessage({ message: challenge.challenge }); const response = await justaname.subnames.updateSubname( { @@ -159,7 +159,7 @@ async function signIn() { address: signer.address }); - const signature = await signer.signMessage(message); + const signature = await signer.signMessage({ message }); const response = await justaname.signIn.signIn({ message: message, diff --git a/docs/sdk/siwens/README.md b/docs/sdk/siwens/README.md index e912f1a9..de9e47b9 100644 --- a/docs/sdk/siwens/README.md +++ b/docs/sdk/siwens/README.md @@ -48,12 +48,12 @@ yarn add @justaname.id/siwens ### Example Usage ```typescript import { SIWENS, InvalidDomainException, InvalidENSException, InvalidStatementException, InvalidTimeException } f, InvalidDomainException, InvalidENSException, InvalidStatementException, InvalidTimeException } from '@justaname.id/siwens';rom '@justaname.id/siwens'; -import { ethers } from 'ethers'; +import { privateKeyToAccount } from 'viem/accounts'; // Define your provider URL (e.g., Infura) const providerUrl = 'https://mainnet.infura.io/v3/YOUR_INFURA_KEY'; -const signer = new ethers.Wallet('YOUR_PRIVATE_KEY_ENS_HOLDER') +const signer = privateKeyToAccount('YOUR_PRIVATE_KEY_ENS_HOLDER') async function signInUser() { const siwens = new SIWENS({ @@ -66,7 +66,7 @@ async function signInUser() { providerUrl }); const message = await siwens.prepareMessage(); - const signature = await signer.signMessage(message); + const signature = await signer.signMessage({ message }); return signature; } diff --git a/package.json b/package.json index d488bb73..1a7840c7 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ "react-router-dom": "6.11.2", "react-timer-hook": "3.0.8", "react-tiny-popover": "8.0.4", - "siwe": "2.3.2", "tailwind-merge": "2.5.2", "tailwindcss-animate": "1.0.7", "tslib": "2.3.0", diff --git a/packages/@justaname.id/sdk/README.md b/packages/@justaname.id/sdk/README.md index 136f2e2d..3759edef 100644 --- a/packages/@justaname.id/sdk/README.md +++ b/packages/@justaname.id/sdk/README.md @@ -56,7 +56,7 @@ First, import the JustaName SDK and initialize it with your configuration: ```typescript import { JustaName } from '@justaname.id/sdk'; -import { ethers } from 'ethers'; +import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'; // Initialize the SDK with your configuration const justaname = JustaName.init({ @@ -80,8 +80,8 @@ const justaname = JustaName.init({ } }); -// Create a signer (for example purposes, we're creating a random wallet) -const signer = ethers.Wallet.createRandom(); +// Create a signer (for example purposes, we're creating a random account) +const signer = privateKeyToAccount(generatePrivateKey()); ``` ### Issuing a Subname @@ -94,7 +94,7 @@ async function issueSubname() { chainId: 1 // Ethereum Mainnet }); - const signature = await signer.signMessage(challenge.challenge); + const signature = await signer.signMessage({ message: challenge.challenge }); const response = await justaname.subnames.addSubname( { @@ -121,7 +121,7 @@ async function updateSubname() { chainId: 1 }); - const signature = await signer.signMessage(challenge.challenge); + const signature = await signer.signMessage({ message: challenge.challenge }); const response = await justaname.subnames.updateSubname( { @@ -155,7 +155,7 @@ async function signIn() { address: signer.address }); - const signature = await signer.signMessage(message); + const signature = await signer.signMessage({ message }); const response = await justaname.signIn.signIn({ message: message, diff --git a/packages/@justaname.id/sdk/package.json b/packages/@justaname.id/sdk/package.json index 547f71be..1839ea75 100644 --- a/packages/@justaname.id/sdk/package.json +++ b/packages/@justaname.id/sdk/package.json @@ -10,7 +10,6 @@ "jest": "^29.4.1" }, "peerDependencies": { - "siwe": ">=2.0.0", "viem": "^2.48.0" }, "exports": { diff --git a/packages/@justaname.id/sdk/src/lib/features/sign-in/index.ts b/packages/@justaname.id/sdk/src/lib/features/sign-in/index.ts index c7619201..7f7ce548 100644 --- a/packages/@justaname.id/sdk/src/lib/features/sign-in/index.ts +++ b/packages/@justaname.id/sdk/src/lib/features/sign-in/index.ts @@ -8,8 +8,6 @@ import { } from '../../errors'; import { OffchainResolvers } from '../offchain-resolvers'; import { RequestSignInParams, SignInFunctionParams } from '../../types/signin'; -import { createPublicClient, http } from 'viem'; -import { mainnet, sepolia } from 'viem/chains'; import { normalize } from 'viem/ens'; export interface SignInResponse extends SiwensResponse { @@ -110,62 +108,13 @@ export class SignIn { providerUrl: network.providerUrl, }); - const siwensResponse = await siwens.verify( - { - signature: params.signature, - nonce: params.nonce, - domain: params.domain, - }, - { - // Smart-contract (EIP-1271) verification is handled inside - // `verificationFallback` below using viem's `verifySiweMessage`. - // We no longer pass `provider` here because it must be an ethers - // `Provider`, and the SDK is now viem-only. - verificationFallback: async (params, opts, message, EIP1271Promise) => { - // Use the chainId extracted from the SIWE message itself, not the - // SDK-default. Otherwise contract-wallet (EIP-1271) verification - // runs against the wrong chain when the message is cross-chain. - const publicClient = createPublicClient({ - chain: chainId === 1 ? mainnet : sepolia, - transport: http(network.providerUrl), - }); - - const result = await EIP1271Promise; - - if (result.success) { - return result; - } else { - let signature = params.signature; - const lastByte = parseInt(params.signature.slice(-2), 16); - if (lastByte < 27) { - const adjustedV = (27 + (lastByte % 2)) - .toString(16) - .padStart(2, '0'); - signature = signature.slice(0, -2) + adjustedV; - } - - const viemResponse = await publicClient.verifySiweMessage({ - message: message.toMessage(), - signature: signature as `0x${string}`, - address: result.data.address as `0x${string}`, - nonce: params.nonce, - domain: params.domain as string, - time: params.time ? new Date(params.time) : undefined, - scheme: params.scheme as string, - }); - - if (viemResponse) { - return { - data: result.data, - success: true, - }; - } - - return result; - } - }, - } - ); + // SIWENS.verify performs EOA recovery, EIP-1271 and ERC-6492 verification + // internally via viem (`verifySiweMessage`) against the message's chain. + const siwensResponse = await siwens.verify({ + signature: params.signature, + nonce: params.nonce, + domain: params.domain, + }); if (siwensResponse.data.chainId !== chainId) { throw InvalidSignInException.chainIdMismatch( diff --git a/packages/@justaname.id/sdk/src/lib/features/subname-challenge/index.ts b/packages/@justaname.id/sdk/src/lib/features/subname-challenge/index.ts index 5d065d22..e8fea9e5 100644 --- a/packages/@justaname.id/sdk/src/lib/features/subname-challenge/index.ts +++ b/packages/@justaname.id/sdk/src/lib/features/subname-challenge/index.ts @@ -6,7 +6,9 @@ import { } from '../../types'; import { SiweConfig } from '../../types/siwe/siwe-config'; import { ChallengeRequestException } from '../../errors/ChallengeRequest.expection'; -import { SiweMessage } from 'siwe'; +import { generateNonce } from '@justaname.id/siwens'; +import { getAddress } from 'viem'; +import { createSiweMessage } from 'viem/siwe'; /** * Represents the Sign-In with Ethereum (SIWE) functionality, providing methods @@ -104,19 +106,18 @@ export class SubnameChallenge { const { expirationTime, issuedAt } = this.generateIssuedAndExpirationTime(_ttl); - const siweMessage = new SiweMessage({ + const prepared = createSiweMessage({ domain: _domain, uri: _origin, - address: _address, + address: getAddress(_address), statement: statement, chainId: _chainId, version: '1', - issuedAt, - expirationTime, + nonce: generateNonce(), + issuedAt: new Date(issuedAt), + expirationTime: new Date(expirationTime), }); - const prepared = siweMessage.prepareMessage(); - if (this.dev) { // eslint-disable-next-line no-console console.debug( diff --git a/packages/@justaname.id/siwens/README.md b/packages/@justaname.id/siwens/README.md index 727d6e1b..3fc11f3c 100644 --- a/packages/@justaname.id/siwens/README.md +++ b/packages/@justaname.id/siwens/README.md @@ -44,14 +44,13 @@ yarn add @justaname.id/siwens ### Example Usage ```typescript import { SIWENS, InvalidENSException } from '@justaname.id/siwens'; -import { Wallet } from 'ethers'; +import { privateKeyToAccount } from 'viem/accounts'; // Define your provider URL (e.g., Infura) const infuraProjectId = 'YOUR_INFURA_PROJECT_ID'; const providerUrl = 'https://mainnet.infura.io/v3/' + infuraProjectId; -// const signer = Wallet.createRandom(); -const signer = new Wallet('YOUR_PRIVATE_KEY'); +const signer = privateKeyToAccount('YOUR_PRIVATE_KEY'); async function signInUser() { const siwens = new SIWENS({ @@ -67,7 +66,7 @@ async function signInUser() { providerUrl }); const message = await siwens.prepareMessage(); - const signature = await signer.signMessage(message); + const signature = await signer.signMessage({ message }); return {signature, message}; } diff --git a/packages/@justaname.id/siwens/package.json b/packages/@justaname.id/siwens/package.json index 2dc2e0d7..1da79b45 100644 --- a/packages/@justaname.id/siwens/package.json +++ b/packages/@justaname.id/siwens/package.json @@ -2,10 +2,10 @@ "name": "@justaname.id/siwens", "version": "0.0.148", "dependencies": { + "@stablelib/random": "^1.0.2", "punycode": "^2.3.1" }, "peerDependencies": { - "siwe": ">=2.0.0", "viem": "^2.48.0" }, "exports": { diff --git a/packages/@justaname.id/siwens/src/lib/index.ts b/packages/@justaname.id/siwens/src/lib/index.ts index 10ca5f1f..652b428b 100644 --- a/packages/@justaname.id/siwens/src/lib/index.ts +++ b/packages/@justaname.id/siwens/src/lib/index.ts @@ -1,3 +1,4 @@ export * from './errors'; +export * from './types'; export * from './siwens/siwens'; export * from './utils'; \ No newline at end of file diff --git a/packages/@justaname.id/siwens/src/lib/siwens/siwens.ts b/packages/@justaname.id/siwens/src/lib/siwens/siwens.ts index 735df385..e97ec5d3 100644 --- a/packages/@justaname.id/siwens/src/lib/siwens/siwens.ts +++ b/packages/@justaname.id/siwens/src/lib/siwens/siwens.ts @@ -1,10 +1,3 @@ -import { - generateNonce, - SiweMessage, - SiweResponse, - VerifyOpts, - VerifyParams, -} from 'siwe'; import { InvalidConfigurationException, InvalidENSException, @@ -16,7 +9,16 @@ import { checkTTL, constructSignInStatement, extractDataFromStatement, + generateNonce, } from '../utils'; +import { + SiweError, + SiweErrorType, + SiweMessageFields, + SiweResponse, + VerifyOpts, + VerifyParams, +} from '../types'; import { toASCII, toUnicode } from 'punycode'; import { createPublicClient, @@ -28,6 +30,11 @@ import { import { mainnet, sepolia } from 'viem/chains'; import type { Chain } from 'viem'; import { normalize } from 'viem/ens'; +import { + createSiweMessage, + parseSiweMessage, + verifySiweMessage, +} from 'viem/siwe'; const SUPPORTED_CHAINS: Record = { 1: mainnet, @@ -43,14 +50,18 @@ const buildPublicClient = ( transport: http(providerUrl), }); +const toISOStringOrUndefined = (value?: string | Date): string | undefined => { + if (!value) { + return undefined; + } + return value instanceof Date ? value.toISOString() : value; +}; + export interface SiwensResponse extends SiweResponse { ens: string; } -export interface SiwensParams - extends Partial< - Omit - > { +export interface SiwensParams extends Partial { ens: string; ttl?: number; expirationTime?: string; @@ -62,19 +73,57 @@ export interface SiwensConfig { providerUrl?: string; } -export class SIWENS extends SiweMessage { +/** + * Sign-In with ENS message. Previously this extended `siwe`'s `SiweMessage`; + * it is now a standalone, ethers-free implementation backed by viem's native + * SIWE module (`viem/siwe`). The public surface (fields, `prepareMessage`, + * `verify`, `generateNonce`) is preserved. + */ +export class SIWENS { + readonly scheme?: string; + readonly domain: string; + readonly address: string; + readonly statement?: string; + readonly uri: string; + readonly version: string; + readonly chainId: number; + readonly nonce: string; + readonly issuedAt?: string; + readonly expirationTime?: string; + readonly notBefore?: string; + readonly requestId?: string; + readonly resources?: string[]; readonly provider: PublicClient; readonly providerUrl: string | undefined; + /** The raw EIP-4361 message string (parsed input, or the built message). */ + private readonly message: string; constructor(signInConfig: SiwensConfig) { const { params, providerUrl } = signInConfig; + if (typeof params === 'string') { - super(params); if (!providerUrl) { throw InvalidConfigurationException.providerUrlRequired(); } - this.provider = buildPublicClient(providerUrl, this.chainId); + const parsed = parseSiweMessage(params); + this.scheme = parsed.scheme; + this.domain = parsed.domain as string; + // Normalize to EIP-55 checksum so `data.address` matches the casing that + // `siwe` always returned (it rejected non-checksummed addresses). + this.address = viemGetAddress(parsed.address as string); + this.statement = parsed.statement; + this.uri = parsed.uri as string; + this.version = (parsed.version as string) || '1'; + this.chainId = (parsed.chainId as number) ?? 1; + this.nonce = parsed.nonce as string; + this.issuedAt = toISOStringOrUndefined(parsed.issuedAt); + this.expirationTime = toISOStringOrUndefined(parsed.expirationTime); + this.notBefore = toISOStringOrUndefined(parsed.notBefore); + this.requestId = parsed.requestId; + this.resources = parsed.resources; + this.message = params; this.providerUrl = providerUrl; + this.provider = buildPublicClient(providerUrl, this.chainId); return; } @@ -86,18 +135,11 @@ export class SIWENS extends SiweMessage { throw InvalidConfigurationException.domainRequired(); } - let issuedAt = params.issuedAt; - let expirationTime = params.expirationTime; - - if (params.ttl) { - checkTTL(params.ttl); - const { - issuedAt: issuedAtGenerated, - expirationTime: expirationTimeGenerated, - } = SIWENS.generateIssuedAndExpirationTime(params.ttl); - issuedAt = issuedAt || issuedAtGenerated; - expirationTime = expirationTime || expirationTimeGenerated; - } + checkTTL(params.ttl); + const { + issuedAt: issuedAtGenerated, + expirationTime: expirationTimeGenerated, + } = SIWENS.generateIssuedAndExpirationTime(params.ttl); checkDomainValid(params.ens); @@ -106,46 +148,140 @@ export class SIWENS extends SiweMessage { params?.statement || '' ); - super({ - ...params, - statement, - version: params.version || '1', - issuedAt, - expirationTime, - }); + this.scheme = params.scheme; + this.domain = params.domain; + this.address = viemGetAddress(params.address as string); + this.statement = statement; + this.uri = params.uri as string; + this.version = params.version || '1'; + this.chainId = (params.chainId as number) ?? 1; + this.nonce = params.nonce || generateNonce(); + this.issuedAt = params.issuedAt || issuedAtGenerated; + this.expirationTime = params.expirationTime || expirationTimeGenerated; + this.notBefore = params.notBefore; + this.requestId = params.requestId; + this.resources = params.resources; this.providerUrl = providerUrl; this.provider = buildPublicClient(providerUrl, this.chainId); + this.message = this.buildMessage(); + } + + toMessage(): string { + return this.message; + } + + prepareMessage(): string { + return this.message; } - override async verify( + async verify( params: VerifyParams, opts?: VerifyOpts ): Promise { - let verification: SiweResponse; + const suppress = opts?.suppressExceptions ?? false; + const data = this.toFields(); - try { - const { signature, ...rest } = params; - const _tempParams = { - signature, - ...rest, - }; - const lastByte = parseInt(signature.slice(-2), 16); - if (lastByte < 27) { - const adjustedV = (27 + (lastByte % 2)).toString(16).padStart(2, '0'); - _tempParams['signature'] = signature.slice(0, -2) + adjustedV; + const computeEns = (): string | undefined => { + try { + return this.statement + ? toUnicode(extractDataFromStatement(this.statement).ens) + : undefined; + } catch { + return undefined; } + }; - verification = await super.verify(_tempParams, opts); - } catch (e) { - const statement = e.data.statement; - const { ens } = extractDataFromStatement(statement); - throw { - ...e, - ens: toUnicode(ens), + const fail = (error: SiweError): SiwensResponse => { + const result: SiwensResponse = { + success: false, + data, + error, + ens: computeEns() as string, }; + if (suppress) { + return result; + } + throw result; + }; + + // Normalize legacy `v` values (< 27) to canonical 27/28 before verifying. + let signature = params.signature; + const lastByte = parseInt(signature.slice(-2), 16); + if (lastByte < 27) { + const adjustedV = (27 + (lastByte % 2)).toString(16).padStart(2, '0'); + signature = signature.slice(0, -2) + adjustedV; } - const statement = verification.data.statement; + // Field validation — mirrors `siwe`'s order and error types so the thrown + // shape is unchanged for consumers. + if (params.scheme && params.scheme !== this.scheme) { + return fail( + new SiweError(SiweErrorType.SCHEME_MISMATCH, params.scheme, this.scheme) + ); + } + if (params.domain && params.domain !== this.domain) { + return fail( + new SiweError(SiweErrorType.DOMAIN_MISMATCH, params.domain, this.domain) + ); + } + if (params.nonce && params.nonce !== this.nonce) { + return fail( + new SiweError(SiweErrorType.NONCE_MISMATCH, params.nonce, this.nonce) + ); + } + + const checkTime = new Date(params.time || new Date()); + if (this.expirationTime) { + const expirationDate = new Date(this.expirationTime); + if (checkTime.getTime() >= expirationDate.getTime()) { + return fail( + new SiweError( + SiweErrorType.EXPIRED_MESSAGE, + `${checkTime.toISOString()} < ${expirationDate.toISOString()}`, + `${checkTime.toISOString()} >= ${expirationDate.toISOString()}` + ) + ); + } + } + if (this.notBefore) { + const notBefore = new Date(this.notBefore); + if (checkTime.getTime() < notBefore.getTime()) { + return fail( + new SiweError( + SiweErrorType.NOT_YET_VALID_MESSAGE, + `${checkTime.toISOString()} >= ${notBefore.toISOString()}`, + `${checkTime.toISOString()} < ${notBefore.toISOString()}` + ) + ); + } + } + + // Signature verification — EOA recovery + EIP-1271 + ERC-6492 in a single + // viem call against the configured public client. A genuine signature + // mismatch resolves to `false`; operational errors (RPC/transport failures) + // are intentionally left to propagate rather than be masked as an invalid + // signature, so contract-wallet checks on a flaky RPC surface a real error. + const valid = await verifySiweMessage(this.provider, { + message: this.message, + signature: signature as `0x${string}`, + address: this.address as `0x${string}`, + ...(params.domain ? { domain: params.domain } : {}), + ...(params.nonce ? { nonce: params.nonce } : {}), + ...(params.scheme ? { scheme: params.scheme } : {}), + time: checkTime, + }); + + if (!valid) { + return fail( + new SiweError( + SiweErrorType.INVALID_SIGNATURE, + undefined, + `Resolved address to be ${this.address}` + ) + ); + } + + const statement = this.statement; if (!statement) { throw InvalidStatementException.invalidStatement(); } @@ -154,7 +290,8 @@ export class SIWENS extends SiweMessage { await this.verifyEnsAddress(ens, this.address); return { - ...verification, + success: true, + data, ens, }; } @@ -169,10 +306,48 @@ export class SIWENS extends SiweMessage { }; } - static generateNonce() { + static generateNonce(): string { return generateNonce(); } + private toFields(): SiweMessageFields { + return { + scheme: this.scheme, + domain: this.domain, + address: this.address, + statement: this.statement, + uri: this.uri, + version: this.version, + chainId: this.chainId, + nonce: this.nonce, + issuedAt: this.issuedAt, + expirationTime: this.expirationTime, + notBefore: this.notBefore, + requestId: this.requestId, + resources: this.resources, + }; + } + + private buildMessage(): string { + return createSiweMessage({ + ...(this.scheme ? { scheme: this.scheme } : {}), + domain: this.domain, + address: viemGetAddress(this.address), + ...(this.statement ? { statement: this.statement } : {}), + uri: this.uri, + version: this.version as '1', + chainId: this.chainId, + nonce: this.nonce, + ...(this.issuedAt ? { issuedAt: new Date(this.issuedAt) } : {}), + ...(this.expirationTime + ? { expirationTime: new Date(this.expirationTime) } + : {}), + ...(this.notBefore ? { notBefore: new Date(this.notBefore) } : {}), + ...(this.requestId ? { requestId: this.requestId } : {}), + ...(this.resources ? { resources: this.resources } : {}), + }); + } + private async verifyEnsAddress(ens: string, address: string) { const resolvedAddress = await this.provider.getEnsAddress({ name: normalize(ens), diff --git a/packages/@justaname.id/siwens/src/lib/types/index.ts b/packages/@justaname.id/siwens/src/lib/types/index.ts new file mode 100644 index 00000000..97f5bb99 --- /dev/null +++ b/packages/@justaname.id/siwens/src/lib/types/index.ts @@ -0,0 +1,88 @@ +/** + * Local, ethers-free replacements for the SIWE types that used to be imported + * from the `siwe` package. Keeping the same shapes (and the same `SiweError` + * `type` strings) preserves the public API and the error contract that the + * SDK's sign-in flow and downstream consumers depend on. + */ + +/** EIP-4361 message fields, mirroring the public surface of `siwe`'s SiweMessage. */ +export interface SiweMessageFields { + scheme?: string; + domain: string; + address: string; + statement?: string; + uri: string; + version: string; + chainId: number; + nonce: string; + issuedAt?: string; + expirationTime?: string; + notBefore?: string; + requestId?: string; + resources?: string[]; +} + +/** Result returned (or thrown) by a verification. */ +export interface SiweResponse { + success: boolean; + data: SiweMessageFields; + error?: SiweError; +} + +/** Parameters accepted by `SIWENS.verify`. */ +export interface VerifyParams { + signature: string; + scheme?: string; + domain?: string; + nonce?: string; + time?: string; +} + +/** Options accepted by `SIWENS.verify`. */ +export interface VerifyOpts { + suppressExceptions?: boolean; +} + +/** + * Mirrors `siwe`'s SiweError so thrown/returned error shapes are unchanged. + */ +export class SiweError { + constructor( + public type: SiweErrorType, + public expected?: string, + public received?: string + ) {} +} + +/** + * Possible message error types. Values are copied verbatim from `siwe` so any + * consumer matching on the message string keeps working. + */ +export enum SiweErrorType { + /** `expirationTime` is present and in the past. */ + EXPIRED_MESSAGE = 'Expired message.', + /** `domain` is not a valid authority or is empty. */ + INVALID_DOMAIN = 'Invalid domain.', + /** `scheme` don't match the scheme provided for verification. */ + SCHEME_MISMATCH = 'Scheme does not match provided scheme for verification.', + /** `domain` don't match the domain provided for verification. */ + DOMAIN_MISMATCH = 'Domain does not match provided domain for verification.', + /** `nonce` don't match the nonce provided for verification. */ + NONCE_MISMATCH = 'Nonce does not match provided nonce for verification.', + /** `address` does not conform to EIP-55 or is not a valid address. */ + INVALID_ADDRESS = 'Invalid address.', + /** `uri` does not conform to RFC 3986. */ + INVALID_URI = 'URI does not conform to RFC 3986.', + /** `nonce` is smaller then 8 characters or is not alphanumeric */ + INVALID_NONCE = 'Nonce size smaller then 8 characters or is not alphanumeric.', + /** `notBefore` is present and in the future. */ + NOT_YET_VALID_MESSAGE = 'Message is not valid yet.', + /** Signature doesn't match the address of the message. */ + INVALID_SIGNATURE = 'Signature does not match address of the message.', + /** `expirationTime`, `notBefore` or `issuedAt` not complient to ISO-8601. */ + INVALID_TIME_FORMAT = 'Invalid time format.', + /** `version` is not 1. */ + INVALID_MESSAGE_VERSION = 'Invalid message version.', + /** Thrown when some required field is missing. */ + UNABLE_TO_PARSE = 'Unable to parse the message.', +} diff --git a/packages/@justaname.id/siwens/src/lib/utils/generateNonce/index.ts b/packages/@justaname.id/siwens/src/lib/utils/generateNonce/index.ts new file mode 100644 index 00000000..dcc32914 --- /dev/null +++ b/packages/@justaname.id/siwens/src/lib/utils/generateNonce/index.ts @@ -0,0 +1,19 @@ +import { randomStringForEntropy } from '@stablelib/random'; + +/** + * Generates a cryptographically-secure, EIP-4361-compliant nonce. + * + * This mirrors `siwe`'s `generateNonce` (96 bits of entropy via a CSPRNG) so we + * keep identical nonce strength/format after dropping the `siwe` dependency. + * Intentionally NOT viem's `generateSiweNonce`, which is backed by `Math.random` + * and would be a security regression. + * + * @returns {string} A randomly generated alphanumeric nonce. + */ +export function generateNonce(): string { + const nonce = randomStringForEntropy(96); + if (!nonce || nonce.length < 8) { + throw new Error('Error during nonce creation.'); + } + return nonce; +} diff --git a/packages/@justaname.id/siwens/src/lib/utils/index.ts b/packages/@justaname.id/siwens/src/lib/utils/index.ts index 4f35d9ee..b20631c5 100644 --- a/packages/@justaname.id/siwens/src/lib/utils/index.ts +++ b/packages/@justaname.id/siwens/src/lib/utils/index.ts @@ -1,3 +1,4 @@ export * from './checkTTL' export * from './checkDomainValid' -export * from './signInStatementHelpers' \ No newline at end of file +export * from './signInStatementHelpers' +export * from './generateNonce' \ No newline at end of file diff --git a/packages/@justaname.id/siwens/src/test/siwens.format.spec.ts b/packages/@justaname.id/siwens/src/test/siwens.format.spec.ts new file mode 100644 index 00000000..16e1cb68 --- /dev/null +++ b/packages/@justaname.id/siwens/src/test/siwens.format.spec.ts @@ -0,0 +1,119 @@ +import { SIWENS, SiweErrorType } from '../'; + +/** + * CI-safe tests (no RPC required). These lock the EIP-4361 message format to be + * byte-identical to what `siwe` produced before the viem migration, and verify + * that field-mismatch checks throw the same `SiweError` types. Signature + * verification (which needs a provider) is covered by the integration tests in + * siwens.spec.ts. + */ + +const ADDRESS = '0x59c44836630760F97b74b569B379ca94c37B93ca'; +const DUMMY_SIGNATURE = '0x' + '00'.repeat(65); + +// Golden string captured from `siwe`'s SiweMessage.prepareMessage() for the +// SIWENS object-construction inputs below (statement from `alice.eth`). +const GOLDEN_MESSAGE = `localhost wants you to sign in with your Ethereum account: +0x59c44836630760F97b74b569B379ca94c37B93ca + +I am signing in with my ENS: alice.eth + +URI: http://localhost:3333 +Version: 1 +Chain ID: 1 +Nonce: abcdef1234567890 +Issued At: 2024-01-01T00:00:00.000Z +Expiration Time: 2024-01-01T00:01:00.000Z`; + +const baseParams = { + domain: 'localhost', + address: ADDRESS, + uri: 'http://localhost:3333', + version: '1', + nonce: 'abcdef1234567890', + chainId: 1, + ttl: 60 * 1000, + ens: 'alice.eth', + issuedAt: '2024-01-01T00:00:00.000Z', + expirationTime: '2024-01-01T00:01:00.000Z', +}; + +describe('SIWENS message format (golden)', () => { + it('builds a byte-identical EIP-4361 message', () => { + const siwens = new SIWENS({ params: { ...baseParams } }); + expect(siwens.prepareMessage()).toBe(GOLDEN_MESSAGE); + expect(siwens.toMessage()).toBe(GOLDEN_MESSAGE); + }); + + it('round-trips when re-parsed from the string form', () => { + const message = new SIWENS({ params: { ...baseParams } }).prepareMessage(); + const reparsed = new SIWENS({ + params: message, + providerUrl: 'http://127.0.0.1:1', + }); + expect(reparsed.address).toBe(ADDRESS); + expect(reparsed.chainId).toBe(1); + expect(reparsed.domain).toBe('localhost'); + expect(reparsed.nonce).toBe('abcdef1234567890'); + expect(reparsed.statement).toBe('I am signing in with my ENS: alice.eth'); + }); +}); + +describe('SIWENS.verify field validation (no RPC)', () => { + it('throws DOMAIN_MISMATCH with the preserved error shape', async () => { + const siwens = new SIWENS({ params: { ...baseParams } }); + await expect( + siwens.verify({ signature: DUMMY_SIGNATURE, domain: 'evil.com' }) + ).rejects.toMatchObject({ + success: false, + error: { type: SiweErrorType.DOMAIN_MISMATCH }, + ens: 'alice.eth', + }); + }); + + it('throws NONCE_MISMATCH', async () => { + const siwens = new SIWENS({ params: { ...baseParams } }); + await expect( + siwens.verify({ signature: DUMMY_SIGNATURE, nonce: 'someOtherNonce123' }) + ).rejects.toMatchObject({ + error: { type: SiweErrorType.NONCE_MISMATCH }, + }); + }); + + it('throws EXPIRED_MESSAGE when the message is past expiry', async () => { + const siwens = new SIWENS({ + params: { + ...baseParams, + issuedAt: '2020-01-01T00:00:00.000Z', + expirationTime: '2020-01-01T00:01:00.000Z', + }, + }); + await expect( + siwens.verify({ signature: DUMMY_SIGNATURE }) + ).rejects.toMatchObject({ + error: { type: SiweErrorType.EXPIRED_MESSAGE }, + }); + }); + + it('returns a failure result instead of throwing when suppressExceptions is set', async () => { + const siwens = new SIWENS({ params: { ...baseParams } }); + const result = await siwens.verify( + { signature: DUMMY_SIGNATURE, domain: 'evil.com' }, + { suppressExceptions: true } + ); + expect(result.success).toBe(false); + expect(result.error?.type).toBe(SiweErrorType.DOMAIN_MISMATCH); + expect(result.ens).toBe('alice.eth'); + }); +}); + +describe('generateNonce', () => { + it('produces alphanumeric nonces of sufficient length', () => { + const nonce = SIWENS.generateNonce(); + expect(nonce).toMatch(/^[a-zA-Z0-9]{8,}$/); + }); + + it('produces a different nonce each call', () => { + expect(SIWENS.generateNonce()).not.toBe(SIWENS.generateNonce()); + }); +}); diff --git a/yarn.lock b/yarn.lock index 68a8742d..7fe2d831 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4487,7 +4487,6 @@ __metadata: jest: "npm:^29.4.1" qs: "npm:6.12.0" peerDependencies: - siwe: ">=2.0.0" viem: ^2.48.0 languageName: unknown linkType: soft @@ -4496,9 +4495,9 @@ __metadata: version: 0.0.0-use.local resolution: "@justaname.id/siwens@workspace:packages/@justaname.id/siwens" dependencies: + "@stablelib/random": "npm:^1.0.2" punycode: "npm:^2.3.1" peerDependencies: - siwe: ">=2.0.0" viem: ^2.48.0 languageName: unknown linkType: soft @@ -5640,7 +5639,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.7.1, @noble/hashes@npm:^1.1.2, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.2, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.5.0, @noble/hashes@npm:~1.7.1": +"@noble/hashes@npm:1.7.1, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.2, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.5.0, @noble/hashes@npm:~1.7.1": version: 1.7.1 resolution: "@noble/hashes@npm:1.7.1" checksum: 10c0/2f8ec0338ccc92b576a0f5c16ab9c017a3a494062f1fbb569ae641c5e7eab32072f9081acaa96b5048c0898f972916c818ea63cbedda707886a4b5ffcfbf94e3 @@ -9277,18 +9276,6 @@ __metadata: languageName: node linkType: hard -"@spruceid/siwe-parser@npm:^2.1.2": - version: 2.1.2 - resolution: "@spruceid/siwe-parser@npm:2.1.2" - dependencies: - "@noble/hashes": "npm:^1.1.2" - apg-js: "npm:^4.3.0" - uri-js: "npm:^4.4.1" - valid-url: "npm:^1.0.9" - checksum: 10c0/79005ae8978b9dd0c1ece949dbc2294d6a641db757c14ae0864b6803358cc498bac882d8031e656b4dbf3e838be043ce6517c857b6e2df26a1e8922baeb2c07d - languageName: node - linkType: hard - "@stablelib/binary@npm:^1.0.1": version: 1.0.1 resolution: "@stablelib/binary@npm:1.0.1" @@ -9305,7 +9292,7 @@ __metadata: languageName: node linkType: hard -"@stablelib/random@npm:^1.0.1": +"@stablelib/random@npm:^1.0.2": version: 1.0.2 resolution: "@stablelib/random@npm:1.0.2" dependencies: @@ -14270,13 +14257,6 @@ __metadata: languageName: node linkType: hard -"apg-js@npm:^4.3.0": - version: 4.4.0 - resolution: "apg-js@npm:4.4.0" - checksum: 10c0/b3e60e2ba8b25fe1c9fcc648f43b98f02f0eff3bbd593fd2866302fe57b1b7840ee9be894ebed6214876a6feecd543cc717d7b68351bf2df831db110ae01e6bb - languageName: node - linkType: hard - "app-root-dir@npm:^1.0.2": version: 1.0.2 resolution: "app-root-dir@npm:1.0.2" @@ -25427,7 +25407,6 @@ __metadata: rollup-plugin-tailwindcss: "npm:1.0.0" rollup-plugin-typescript2: "npm:0.36.0" rollup-preserve-directives: "npm:1.1.1" - siwe: "npm:2.3.2" storybook: "npm:8.2.8" tailwind-merge: "npm:2.5.2" tailwindcss: "npm:3.4.3" @@ -33040,20 +33019,6 @@ __metadata: languageName: node linkType: hard -"siwe@npm:2.3.2": - version: 2.3.2 - resolution: "siwe@npm:2.3.2" - dependencies: - "@spruceid/siwe-parser": "npm:^2.1.2" - "@stablelib/random": "npm:^1.0.1" - uri-js: "npm:^4.4.1" - valid-url: "npm:^1.0.9" - peerDependencies: - ethers: ^5.6.8 || ^6.0.8 - checksum: 10c0/05ee09cdabef72a8ec54ffe24e517c386eb49bb6385ffc7ec159e266b3661a98405ca88b8e75278b376754e943a5118790c2add98b5012e1c4ec13bce4e6ee03 - languageName: node - linkType: hard - "slash@npm:3.0.0, slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -35798,7 +35763,7 @@ __metadata: languageName: node linkType: hard -"uri-js@npm:^4.2.2, uri-js@npm:^4.4.1": +"uri-js@npm:^4.2.2": version: 4.4.1 resolution: "uri-js@npm:4.4.1" dependencies: @@ -35998,13 +35963,6 @@ __metadata: languageName: node linkType: hard -"valid-url@npm:^1.0.9": - version: 1.0.9 - resolution: "valid-url@npm:1.0.9" - checksum: 10c0/3995e65f9942dbcb1621754c0f9790335cec61e9e9310c0a809e9ae0e2ae91bb7fc6a471fba788e979db0418d9806639f681ecebacc869bc8c3de88efa562ee6 - languageName: node - linkType: hard - "validate-npm-package-license@npm:^3.0.1, validate-npm-package-license@npm:^3.0.4": version: 3.0.4 resolution: "validate-npm-package-license@npm:3.0.4"