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"