ニュースレター:ダブルオプトイン、ワンクリック配信停止(RFC 8058)、タグ、再開可能なバッチ配信
コード11 ファイルコンテキスト約 889 トークンスキャン合格
インストール
$
genpm add @core/newsletter含まれるもの
- src/lib/newsletter/ にソースコード(11 ファイル)。 (40.7 KB)
- src/lib/newsletter/AGENTS.md に AI ルール、加えて IDE 用のルールファイル。
- .env.example に追加される環境変数: NEWSLETTER_FROM, NEWSLETTER_POSTAL_ADDRESS, NEWSLETTER_SECRET, SITE_URL。
- @core/antispam, @core/contracts, @core/db, @core/email, @core/jobs, @core/rich-text を自動で解決します。
README
このパッケージには README がありません。
約 889 トークン→ src/lib/newsletter/AGENTS.md→ .cursor/rules/genpm-core-newsletter.mdc
これは AI が src/lib/newsletter で作業するときに読む内容そのものです。それ以外はコンテキストに追加されません。
@core/newsletter — rules for AI agents
Purpose
Own-list newsletter: sign-ups with double opt-in and consent record, tags, one-click unsubscribe (RFC 8058 headers,
required by Gmail/Yahoo for bulk senders) plus a visible link and postal address in every email, rich-text campaigns
sent in resumable batches by @core/jobs (no duplicates), bounce/complaint suppression and GDPR erasure.
Tables: subscribers, newsletter_campaigns, newsletter_sends. No open-tracking pixels, no automations.
Map
index.ts— public API:subscribe,confirmSubscription,unsubscribe,createCampaign,sendCampaign,markUndeliverable,deleteSubscriber,newsletterAdminResources,setNewsletterMessages.newsletter.ts— logic and jobs.sender.ts— sending (Resend API with headers, or @core/email in dev).tokens.ts— signed links.adapters/hono.ts—newsletterRoutes().adapters/next.ts—subscribeRoute,confirmRoute,unsubscribeRoute.
Integration
- Env:
NEWSLETTER_FROM,NEWSLETTER_POSTAL_ADDRESS(legal requirement),NEWSLETTER_SECRET(≥ 32 chars),SITE_URL, andRESEND_API_KEYin production (other providers: implementNewsletterSenderand callsetNewsletterSender). Verify the sending domain (SPF, DKIM, DMARC) with the provider — tell the user how; you can't do it for them. - Migrations as in
src/lib/db/AGENTS.md; the @core/jobs cron must run. - Routes under
/newsletter(Honoapp.route('/newsletter', newsletterRoutes()), or the Next handlers) and three simple pages:/newsletter/confirmed,/newsletter/unsubscribed,/newsletter/invalid.confirmandunsubscribeneed GET and POST: GET only renders a minimal page with a button (mail link scanners open every URL), the button POSTs and acts; any other POST tounsubscribeis the RFC 8058 one-click (200, no redirect). Translate the page withtexts(newsletterRoutes({ texts })or the handlers' third argument). - Sign-up form:
POST /newsletter/subscribewithemail, the @core/antispam fields and an unchecked, required consent checkboxconsentwhose text version goes in a hiddenconsentVersion(withconsentVersion, the server rejects the sign-up unlessconsentis checked:consent_required). Always show the same "check your inbox" message. A plain HTML form posted from the same site (Accept: text/htmland a same-hostReferer, what browsers send) is redirected back with?newsletter=<status>#newsletter; other clients get JSON (202, or 400/429 witherror). - Translate system emails with
setNewsletterMessages((locale) => ({ confirmSubject, confirmText, unsubscribeLabel })). - Add
...newsletterAdminResources()tosrc/genpm/admin.ts; wire the provider's bounce/complaint webhook tomarkUndeliverable. - Verify: subscribe, open the confirmation link, send a campaign to yourself and unsubscribe with one click.
Conventions
- Only
active(confirmed) subscribers receive campaigns; tags segment them. - Batches claim recipients atomically (
newsletter_sends.status = 'sending',FOR UPDATE SKIP LOCKED), so concurrent batch jobs never mail the same subscriber twice; claims older than 15 min (a dead worker) are retried. - Permissions:
subscribers:read|update|delete,campaigns:read|create|update|send.
Don't
- Don't import contacts without documented consent, and never pre-check the consent box.
- Don't send campaigns from a request handler or remove the unsubscribe link/headers.
- Don't reveal whether an email is already subscribed.
# @core/newsletter — rules for AI agents
## Purpose
Own-list newsletter: sign-ups with double opt-in and consent record, tags, one-click unsubscribe (RFC 8058 headers,
required by Gmail/Yahoo for bulk senders) plus a visible link and postal address in every email, rich-text campaigns
sent in resumable batches by @core/jobs (no duplicates), bounce/complaint suppression and GDPR erasure.
Tables: `subscribers`, `newsletter_campaigns`, `newsletter_sends`. No open-tracking pixels, no automations.
## Map
- `index.ts` — public API: `subscribe`, `confirmSubscription`, `unsubscribe`, `createCampaign`, `sendCampaign`, `markUndeliverable`, `deleteSubscriber`, `newsletterAdminResources`, `setNewsletterMessages`.
- `newsletter.ts` — logic and jobs. `sender.ts` — sending (Resend API with headers, or @core/email in dev). `tokens.ts` — signed links.
- `adapters/hono.ts` — `newsletterRoutes()`. `adapters/next.ts` — `subscribeRoute`, `confirmRoute`, `unsubscribeRoute`.
## Integration
1. Env: `NEWSLETTER_FROM`, `NEWSLETTER_POSTAL_ADDRESS` (legal requirement), `NEWSLETTER_SECRET` (≥ 32 chars), `SITE_URL`,
and `RESEND_API_KEY` in production (other providers: implement `NewsletterSender` and call `setNewsletterSender`).
Verify the sending domain (SPF, DKIM, DMARC) with the provider — tell the user how; you can't do it for them.
2. Migrations as in `src/lib/db/AGENTS.md`; the @core/jobs cron must run.
3. Routes under `/newsletter` (Hono `app.route('/newsletter', newsletterRoutes())`, or the Next handlers) and three
simple pages: `/newsletter/confirmed`, `/newsletter/unsubscribed`, `/newsletter/invalid`. `confirm` and `unsubscribe`
need GET and POST: GET only renders a minimal page with a button (mail link scanners open every URL), the button
POSTs and acts; any other POST to `unsubscribe` is the RFC 8058 one-click (200, no redirect). Translate the page
with `texts` (`newsletterRoutes({ texts })` or the handlers' third argument).
4. Sign-up form: `POST /newsletter/subscribe` with `email`, the @core/antispam fields and an unchecked, required consent
checkbox `consent` whose text version goes in a hidden `consentVersion` (with `consentVersion`, the server rejects
the sign-up unless `consent` is checked: `consent_required`). Always show the same "check your inbox" message.
A plain HTML form posted from the same site (`Accept: text/html` and a same-host `Referer`, what browsers send) is
redirected back with `?newsletter=<status>#newsletter`; other clients get JSON (202, or 400/429 with `error`).
5. Translate system emails with `setNewsletterMessages((locale) => ({ confirmSubject, confirmText, unsubscribeLabel }))`.
6. Add `...newsletterAdminResources()` to `src/genpm/admin.ts`; wire the provider's bounce/complaint webhook to `markUndeliverable`.
7. Verify: subscribe, open the confirmation link, send a campaign to yourself and unsubscribe with one click.
## Conventions
- Only `active` (confirmed) subscribers receive campaigns; tags segment them.
- Batches claim recipients atomically (`newsletter_sends.status = 'sending'`, `FOR UPDATE SKIP LOCKED`), so concurrent
batch jobs never mail the same subscriber twice; claims older than 15 min (a dead worker) are retried.
- Permissions: `subscribers:read|update|delete`, `campaigns:read|create|update|send`.
## Don't
- Don't import contacts without documented consent, and never pre-check the consent box.
- Don't send campaigns from a request handler or remove the unsubscribe link/headers.
- Don't reveal whether an email is already subscribed.
.genpmignore 適用後に組み込まれる正確なツリーです。固定先:
// Tokens firmados (sin estado) para confirmar y darse de baja: `<sub>.<kind>.<exp>.<hmac>`.
const enc = new TextEncoder();
function secret(): string {
const s = process.env.NEWSLETTER_SECRET;
if (!s || s.length < 32) throw new Error('NEWSLETTER_SECRET must be set (>= 32 chars)');
return s;
}
async function hmac(data: string): Promise<string> {
const key = await crypto.subtle.importKey('raw', enc.encode(secret()), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const sig = new Uint8Array(await crypto.subtle.sign('HMAC', key, enc.encode(data)));
return btoa(String.fromCharCode(...sig)).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
}
export type TokenKind = 'confirm' | 'unsubscribe';
/** Confirmación: caduca en 7 días. Baja: no caduca (los enlaces de emails antiguos deben seguir funcionando). */
export async function makeToken(subscriberId: string, kind: TokenKind): Promise<string> {
const exp = kind === 'confirm' ? Math.floor(Date.now() / 1000) + 7 * 86_400 : 0;
const body = `${subscriberId}.${kind}.${exp}`;
return `${body}.${await hmac(body)}`;
}
export async function readToken(token: string | null | undefined, kind: TokenKind): Promise<string | null> {
const parts = (token ?? '').split('.');
if (parts.length !== 4) return null;
const [id, k, exp, sig] = parts as [string, string, string, string];
const expected = await hmac(`${id}.${k}.${exp}`);
let diff = sig.length ^ expected.length;
for (let i = 0; i < expected.length; i++) diff |= (sig.charCodeAt(i) || 0) ^ expected.charCodeAt(i);
if (diff !== 0 || k !== kind) return null;
if (Number(exp) !== 0 && Number(exp) * 1000 < Date.now()) return null;
return id;
}
このパッケージは MCP サーバーを宣言していません。
| バージョン | コミット | 公開日 | スキャン |
|---|---|---|---|
| 1.1.0 | 48a34fe | 3 時間前 | スキャン合格 |
- genpm
- @core/antispam ^1.0.0@core/contracts ^1.0.0@core/db ^1.0.0@core/email ^1.0.1@core/jobs ^1.0.0@core/rich-text ^1.0.0
- npm
- zod ^4.0.0
- 提案
- GenPM は npm コマンドを提案し、あなたが承認した場合にのみ実行します。
- 利用元(1)
- @core/kit-blog ^1.1.0
- スキャン
- スキャン合格 · 指摘 0 件
- コミット
- v1.1.0 → 48a34feb692c5ef63dc34278296cb446d534233b · 取得後に検証済み
- スクリプト
- なし。GenPM はパッケージのコードを実行しません。
- ライセンス
- MIT
- 品質
- 100/100
- 認識されたライセンス達成
- AGENTS.md に目的の説明がある達成
- AGENTS.md に統合手順がある達成
- AGENTS.md に規約や禁止事項がある達成
- テストを含む達成
- セキュリティスキャンに合格達成
- 過去 6 か月以内に公開達成
- 認証済みの公開者達成
- 概要とキーワード達成
- 報告
- 問題を見つけましたか?