extension-email-verification — скилл для Claude Code: письма
Добавляет в приложение на Caffeine AI подтверждение email по ссылке: получатель кликает кнопку и адрес помечается проверенным.
npx skills add caffeinelabs/skills --skill extension-email-verification -a claude-codeЧто делает extension-email-verification
Скилл закрывает весь путь подтверждения адреса. Бэкенд выпускает токен, кладёт его в verificationTokens и шлёт письмо со ссылкой на страницу /verify-email самого приложения. Когда получатель нажимает кнопку, вызывается _caffeineEmailConfirmVerification(token), адрес попадает в verifiedEmails. Проверка статуса — через contains в модуле verifiedEmails.mo, вести свой учёт в профиле пользователя не нужно.
Настройка состоит из пяти обязательных шагов: зависимости mops add caffeineai-email-verification и mops add caffeineai-email, два поля состояния в цепочке миграций, подключение MixinEmailVerification(verifiedEmails, verificationTokens) в main.mo, отправка через EmailVerification.sendVerificationEmail(...) и публичный маршрут /verify-email на фронтенде с npm-пакетом @caffeineai/email-verification. В теле письма обязательно должен стоять {{VERIFICATION_URL}}, иначе функция вернёт #err.
Токен одноразовый и живёт 24 часа. Подтвердить адрес может любой, у кого есть валидный токен, включая анонимного пользователя — аккаунт для этого не требуется. Письма уходят через почтовый шлюз с настройками, которые платформа подставляет при каждом деплое (INTEGRATIONS_GATEWAY_URL и INTEGRATIONS_GATEWAY_API_KEY); без них отправка вернёт #err. Если забыть про фронтенд-маршрут, получатель попадёт на несуществующую страницу, и адрес так и останется неподтверждённым.
Какие задачи решает
- подтвердить, что адрес принадлежит пользователю, до создания аккаунта
- отправить письмо со ссылкой на страницу самого приложения
- проверить статус адреса без отдельного поля в профиле
- выпустить одноразовый токен и не хранить его вручную
- дать анонимному получателю подтвердить адрес без входа
Примеры запросов агенту
- Добавь в проект подтверждение email через скилл extension-email-verification
- Подключи MixinEmailVerification и отправь письмо со ссылкой на /verify-email
- Сделай публичный маршрут /verify-email с кнопкой, которая вызывает confirm()
Как установить extension-email-verification
1Через Skills CLI (Vercel)
npx skills add caffeinelabs/skills --skill extension-email-verification -a claude-code2Вручную: положить файлы
~/.claude/skills/extension-email-verification/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/extension-email-verification/
1Через Skills CLI (Vercel)
npx skills add caffeinelabs/skills --skill extension-email-verification -a codex2Вручную: положить файлы
.agents/skills/extension-email-verification/SKILL.mdДля всех проектов — ~/.agents/skills/
1Через Skills CLI (Vercel)
npx skills add caffeinelabs/skills --skill extension-email-verification -a cursor2Вручную: положить файлы
.cursor/skills/extension-email-verification/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
1GitHub CLI
gh skill install caffeinelabs/skills extension-email-verification2Вручную: положить файлы
.github/skills/extension-email-verification/SKILL.md1Через Skills CLI (Vercel)
npx skills add caffeinelabs/skills --skill extension-email-verification -a gemini-cli2Командой платформы
gemini skills install https://github.com/caffeinelabs/skills --path skills/extension-email-verificationВручную: положить файлы
.devin/skills/extension-email-verification/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Через Skills CLI (Vercel)
npx skills add caffeinelabs/skills --skill extension-email-verification -a opencodeClaude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов
Вопросы о extension-email-verification
Как установить extension-email-verification в Claude?
Выполните npx skills add caffeinelabs/skills --skill extension-email-verification -a claude-code. Скилл окажется в ~/.claude/skills/extension-email-verification/SKILL.md.
Что делает extension-email-verification?
Добавляет подтверждение email по ссылке: бэкенд выпускает токен, отправляет письмо, а фронтенд на странице /verify-email подтверждает адрес по нажатию кнопки.
Что нужно подготовить до подключения скилла?
Две зависимости mops add caffeineai-email-verification и mops add caffeineai-email, npm-пакет @caffeineai/email-verification и публичный маршрут /verify-email. В теле письма обязателен плейсхолдер {{VERIFICATION_URL}}.
Исходный текст
SKILL.md · Apache-2.0
---
name: extension-email-verification
description: Support for sending an email with a link the recipient can click to prove they own the email address.
version: 0.3.0
compatibility:
mops:
caffeineai-email-verification: "~0.3.0"
caffeineai-email: "~0.4.0"
npm:
"@caffeineai/email-verification": "~0.3.0"
caffeineai-subscription: [plus, pro]
---
# Email — Verification
Email verification extension for [Caffeine AI](https://caffeine.ai?utm_source=caffeine-skill&utm_medium=referral).
## Overview
This skill adds email address verification via a click-to-verify link that lands on the app's own frontend. The backend mints and stores the link token (`verificationTokens`) and confirms it through `MixinEmailVerification`; `verifiedEmails` tracks verified addresses. The frontend renders the `/verify-email` page, where the recipient clicks a button to confirm.
## Required Setup Checklist
All five steps are mandatory.
1. **mops dependencies** — `mops add caffeineai-email-verification` and `mops add caffeineai-email`.
2. **Two stable state fields** — `verifiedEmails : VerifiedEmails.State` and `verificationTokens : VerificationTokens.State`, initialised in the migration chain head with `VerifiedEmails.new()` and `VerificationTokens.new()`.
3. **Mixin invocation** — `include MixinEmailVerification(verifiedEmails, verificationTokens)` in `main.mo`.
4. **Sending** — `EmailVerification.sendVerificationEmail(verificationTokens, fromUsername, recipients, subject, htmlBody)`; the body MUST contain `{{VERIFICATION_URL}}`.
5. **Frontend npm package and route** — `@caffeineai/email-verification` installed, and a public route `/verify-email` (reachable without signing in) that renders a button calling `confirm()`.
CRITICAL: the link in the email is `https://<app domain>/verify-email?token=…`. Without the frontend route the recipient lands on a missing page and the address is never verified.
# Backend
## This component is for sending an email to users with a verification link which the user can click to prove they own the email address.
### To check if an email address has been verified
Use the prefabricated module `mo:caffeineai-email-verification/verifiedEmails.mo` which cannot be modified.
```mo:caffeineai-email-verification/verifiedEmails.mo
module {
public type State = {
verifiedEmails : Set.Set<Text>;
};
public func new() : State;
public func contains(state : State, email : Text) : Bool;
public func iter(state : State) : Iter.Iter<Text>;
public func size(state : State) : Nat;
};
```
To check whether an email is verified use the `contains` function. Do NOT try to track the email verification status independently by storing it against the user profile.
### Pending verification links
Use the prefabricated module `mo:caffeineai-email-verification/verificationTokens.mo` which cannot be modified. It holds the tokens of links sent but not yet clicked; a token is single use and expires after 24 hours. The app only declares the state and passes it around.
```mo:caffeineai-email-verification/verificationTokens.mo
module {
public type State = {
tokens : Map.Map<Text, { email : Text; createdAt : Time.Time }>;
};
public func new() : State;
public func size(state : State) : Nat;
};
```
### To handle the verification link
Use the prefabricated module `mo:caffeineai-email-verification/verificationMixin.mo` which cannot be modified.
`MixinEmailVerification` takes both states. It adds `_caffeineEmailConfirmVerification(token)`, which the app's frontend calls from the verification page: it marks the address the token was sent to as verified and returns it. Anyone holding a valid token can call it, the anonymous principal included, so the recipient needs no account.
```mo:caffeineai-email-verification/verificationMixin.mo
import MixinEmailVerification "mo:caffeineai-email-verification/verificationMixin";
mixin (verifiedEmails : VerifiedEmails.State, verificationTokens : VerificationTokens.State) {
public shared func _caffeineEmailConfirmVerification(token : Text) : async { #ok : Text; #err : Text };
};
```
### For sending users a verification email
- Use the `sendVerificationEmail` function of the prefabricated module `mo:caffeineai-email-verification/verification.mo` which cannot be modified. Do NOT call `EmailClient.broadcast` from app code: this module prepares the tokens and the link template it sends.
- It returns a SendResult which is #ok if the email is sent successfully otherwise #err(error) with the error text.
- Each recipient receives an individual email with a specific verification link for them, pointing at the app's `/verify-email` page.
- The htmlBody MUST contain the placeholder text {{VERIFICATION_URL}}; the function returns #err when it is missing.
- The mail goes through the mail gateway with the settings the platform injects on every deploy (`INTEGRATIONS_GATEWAY_URL` / `INTEGRATIONS_GATEWAY_API_KEY`); without them the function returns `#err`.
```mo:caffeineai-email-verification/verification.mo
module {
public func sendVerificationEmail(
tokens : VerificationTokens.State,
fromUsername : Text,
recipients : [Text],
subject : Text,
htmlBody : Text,
) : async { #ok; #err : Text };
};
```
### Example usage with endpoints for registering a user and for checking whether a user is verified.
```motoko filepath=src/backend/main.mo
import Map "mo:core/Map";
import Runtime "mo:core/Runtime";
import Principal "mo:core/Principal";
import Text "mo:core/Text";
import EmailVerification "mo:caffeineai-email-verification/verification";
import MixinEmailVerification "mo:caffeineai-email-verification/verificationMixin";
import VerificationTokens "mo:caffeineai-email-verification/verificationTokens";
import VerifiedEmails "mo:caffeineai-email-verification/verifiedEmails";
actor {
// Stores which emails are verified
let verifiedEmails : VerifiedEmails.State;
// Pending verification links, until clicked or expired
let verificationTokens : VerificationTokens.State;
// User profiles storage
let users : Map.Map<Principal, User>;
// Email to principal mapping for uniqueness check
let emailToPrincipal : Map.Map<Text, Principal>;
// Handles the verification link and updates the verifiedEmails store
include MixinEmailVerification(verifiedEmails, verificationTokens);
type User = {
name : Text;
email : Text;
};
public shared ({ caller }) func registerUser(email : Text, name : Text) : async () {
if (users.containsKey(caller)) {
Runtime.trap("User already registered");
};
if (emailToPrincipal.containsKey(email)) {
Runtime.trap("Email already registered");
};
let user : User = {
name;
email;
};
users.add(caller, user);
emailToPrincipal.add(email, caller);
let result = await EmailVerification.sendVerificationEmail(
verificationTokens,
"no-reply",
[email],
"Welcome to Our Service",
"Hello " # name # ",<br><br>Thank you for registering with our service. Please <a href=\"{{VERIFICATION_URL}}\">click here</a> to verify your email address<br><br>Best regards,<br>The Team",
);
switch (result) {
case (#ok) {};
case (#err(error)) {
Runtime.trap("Couldn't send verification email: " # error);
};
};
};
public shared ({ caller }) func isEmailVerified() : async Bool {
let user = users.get(caller) ?? Runtime.trap("User not registered");
VerifiedEmails.contains(verifiedEmails, user.email);
};
};
```
The migration chain head:
```motoko filepath=src/backend/migrations/00000000_000000.mo
import Map "mo:core/Map";
import VerificationTokens "mo:caffeineai-email-verification/verificationTokens";
import VerifiedEmails "mo:caffeineai-email-verification/verifiedEmails";
module {
type User = {
name : Text;
email : Text;
};
type NewActor = {
verifiedEmails : VerifiedEmails.State;
verificationTokens : VerificationTokens.State;
users : Map.Map<Principal, User>;
emailToPrincipal : Map.Map<Text, Principal>;
};
public func migration(_old : {}) : NewActor {
{
verifiedEmails = VerifiedEmails.new();
verificationTokens = VerificationTokens.new();
users = Map.empty<Principal, User>();
emailToPrincipal = Map.empty<Text, Principal>();
};
};
};
```
## Upgrading from the previous version
An app built with `caffeineai-email-verification` 0.2.x re-pins with `mops add [email protected]` and `mops add [email protected]`, and bumps `@caffeineai/email-verification` to 0.3.0. Nothing in the app code changes. The mixin no longer has the `_caffeineEmailVerify` callback of the transport canister: verification mail now always goes through the mail gateway, and the links in mail the transport sent before 0.2.0 stop working.
An app built with `caffeineai-email-verification` 0.1.x and `caffeineai-email` 0.2.x needs all of these, then the re-pin above:
1. Re-pin: `mops add [email protected]` and `mops add [email protected]`.
2. Declare the new stable field `verificationTokens : VerificationTokens.State` and add it to the migration chain (a new migration file initialising it with `VerificationTokens.new()`; `verifiedEmails` keeps its shape and its data).
3. Change `include MixinEmailVerification(verifiedEmails)` to `include MixinEmailVerification(verifiedEmails, verificationTokens)`.
4. Replace `EmailClient.sendVerificationEmail(fromUsername, recipients, subject, htmlBody)` with `EmailVerification.sendVerificationEmail(verificationTokens, fromUsername, recipients, subject, htmlBody)` (import `mo:caffeineai-email-verification/verification`).
5. Add the frontend package and the `/verify-email` route described below.
# Frontend
Install `@caffeineai/email-verification`. It exports the prefabricated hook `useEmailVerification`, which cannot be modified:
```typescript filepath=@caffeineai/email-verification/hooks/useEmailVerification.ts
export interface EmailVerificationActor {
_caffeineEmailConfirmVerification(token: string): Promise<{ ok: string } | { err: string }>;
}
// ready: a token is in the URL and nothing was sent yet
// pending: the backend call is in flight
// done: the address is verified; `email` holds it
// invalid: the URL has no token, or the backend rejected it
// error: the backend call failed; `error` holds the message
export type EmailVerificationStatus = 'ready' | 'pending' | 'done' | 'invalid' | 'error';
export declare function useEmailVerification(actor: EmailVerificationActor | null | undefined): {
status: EmailVerificationStatus;
email: string | null;
error: string | null;
hasToken: boolean;
// True once the actor is available, a token is present and nothing was sent yet
isReady: boolean;
// Confirms the address through the backend. Call it from a button click only.
confirm: () => Promise<void>;
};
```
Rules for the verification page:
- Add a route at `/verify-email` that is reachable WITHOUT signing in: recipients open it from their mail client and have no session. The backend method takes the token as its only credential, so the anonymous actor from `useActor` is enough.
- Read the token with the hook (it takes `?token=` from the URL itself) and render a button that calls `confirm()`. NEVER call `confirm()` on page load or in an effect: mail clients, link previewers and scanners fetch the URL without a person behind it, and a fetch must verify nothing.
- Show the outcome from `status`: `done` with the verified `email`, `invalid` for a missing, used or expired link (offer to request a new email), `error` with the message.
Example page:
```typescript filepath=src/frontend/src/pages/VerifyEmailPage.tsx
import { useEmailVerification } from '@caffeineai/email-verification';
import { useActor } from '@caffeineai/core-infrastructure';
import { createActor } from '@/backend';
export function VerifyEmailPage() {
const { actor } = useActor(createActor);
const { status, email, error, isReady, confirm } = useEmailVerification(actor);
if (status === 'done') return <p>{email} is verified. You can close this page.</p>;
if (status === 'invalid') return <p>{error ?? 'This verification link is invalid or has expired.'}</p>;
if (status === 'error') return <p>Something went wrong: {error}</p>;
return (
<button onClick={() => void confirm()} disabled={!isReady}>
{status === 'pending' ? 'Verifying…' : 'Verify my email address'}
</button>
);
}
```
Register it with the app's TanStack Router alongside the other routes, as a child of the root route with `path: '/verify-email'`, outside any authenticated layout.
If there is a UI for the admin to enter the content of a verification email then indicate that the placeholder text {{VERIFICATION_URL}} must be present in the email body.
© caffeinelabs · Apache-2.0 · caffeinelabs/skills