NIP-70: 受保護事件
使用 - 標籤防止事件被中繼器廣播到其他地方
概述
NIP-70 定義了 "-" 標籤,用於標記事件為「受保護」狀態。 當事件包含此標籤時,中繼器應該拒絕將該事件廣播到其他中繼器,
確保事件僅保留在用戶選擇發布的中繼器上。
標籤格式
["-"] 這是一個簡單的單元素標籤,不需要任何額外參數。 當中繼器看到包含此標籤的事件時,應遵守以下行為準則。
中繼器行為
接收事件時
- 接受並儲存來自原始作者的受保護事件
- 拒絕來自其他客戶端/中繼器的轉發
- 驗證事件確實來自原始發布者
回應訂閱時
- 正常回應訂閱該事件的客戶端
- 不主動將事件推送到其他中繼器
錯誤回應
["OK", "<event-id>", false, "blocked: event is protected"] 使用場景
付費中繼器內容
作者希望將獨家內容限制在付費中繼器,防止免費中繼器獲取並重新發布。
私密社群
社群成員在專屬中繼器分享內容,不希望被外部索引或存檔。
草稿與測試
開發者或作者發布草稿內容進行測試,不希望被其他中繼器永久保存。
地理限制
某些內容僅適合在特定區域的中繼器發布,不希望全球傳播。
範例
受保護的短文
{
"kind": 1,
"content": "這是僅限此中繼器的獨家內容!",
"tags": [
["-"]
],
"created_at": 1234567890,
"pubkey": "...",
"id": "...",
"sig": "..."
} 受保護的長文
{
"kind": 30023,
"content": "# 付費會員專屬文章\n\n這篇文章僅供付費中繼器會員閱讀...",
"tags": [
["d", "exclusive-article-2024"],
["title", "會員專屬文章"],
["-"]
]
} 受保護的媒體事件
{
"kind": 1063,
"content": "獨家影片內容",
"tags": [
["url", "https://exclusive-relay.com/video.mp4"],
["m", "video/mp4"],
["-"]
]
} TypeScript 實作
建立受保護事件
import { finalizeEvent } from 'nostr-tools';
function createProtectedEvent(
content: string,
kind: number = 1,
additionalTags: string[][] = [],
secretKey: Uint8Array
) {
const tags: string[][] = [
['-'], // 保護標籤
...additionalTags,
];
const event = {
kind,
content,
tags,
created_at: Math.floor(Date.now() / 1000),
};
return finalizeEvent(event, secretKey);
}
// 使用範例:建立受保護短文
const protectedNote = createProtectedEvent(
'這是僅限此中繼器的內容',
1,
[],
secretKey
);
// 使用範例:建立受保護長文
const protectedArticle = createProtectedEvent(
'# 會員專屬\n\n詳細內容...',
30023,
[
['d', 'members-only-article'],
['title', '會員專屬文章'],
],
secretKey
); 檢測受保護事件
function isProtectedEvent(event: any): boolean {
return event.tags.some((tag: string[]) => tag[0] === '-' && tag.length === 1);
}
function getProtectionStatus(event: any): {
isProtected: boolean;
canRebroadcast: boolean;
} {
const isProtected = isProtectedEvent(event);
return {
isProtected,
canRebroadcast: !isProtected,
};
}
// 使用範例
const status = getProtectionStatus(event);
if (status.isProtected) {
console.log('此事件受保護,不應轉發到其他中繼器');
} 中繼器端驗證
interface RelayValidationResult {
accepted: boolean;
message?: string;
}
function validateIncomingEvent(
event: any,
sourceType: 'client' | 'relay',
senderPubkey?: string
): RelayValidationResult {
const isProtected = isProtectedEvent(event);
// 受保護事件只接受來自原始作者的直接發布
if (isProtected) {
if (sourceType === 'relay') {
return {
accepted: false,
message: 'blocked: event is protected',
};
}
// 如果是客戶端發送,驗證是否為作者本人
if (senderPubkey && senderPubkey !== event.pubkey) {
return {
accepted: false,
message: 'blocked: protected events can only be published by author',
};
}
}
return { accepted: true };
}
// 使用範例
const result = validateIncomingEvent(event, 'relay');
if (!result.accepted) {
// 發送 OK 回應表示拒絕
sendMessage(['OK', event.id, false, result.message]);
} 客戶端處理
interface PublishOptions {
relays: string[];
protect?: boolean;
}
async function publishEvent(
content: string,
kind: number,
options: PublishOptions,
secretKey: Uint8Array
) {
const tags: string[][] = [];
// 如果需要保護,添加 - 標籤
if (options.protect) {
tags.push(['-']);
}
const event = finalizeEvent(
{
kind,
content,
tags,
created_at: Math.floor(Date.now() / 1000),
},
secretKey
);
// 發布到指定中繼器
const results = await Promise.allSettled(
options.relays.map((relay) => publishToRelay(relay, event))
);
return {
event,
results: results.map((r, i) => ({
relay: options.relays[i],
success: r.status === 'fulfilled' && r.value.success,
message: r.status === 'fulfilled' ? r.value.message : r.reason,
})),
};
}
// 使用範例:發布受保護事件到付費中繼器
const result = await publishEvent(
'會員專屬內容',
1,
{
relays: ['wss://paid-relay.example.com'],
protect: true,
},
secretKey
);
console.log(`已發布到 ${result.results.filter((r) => r.success).length} 個中繼器`); UI 指示器
// React 元件範例
function ProtectedBadge({ event }: { event: any }) {
if (!isProtectedEvent(event)) {
return null;
}
return (
<div className="protected-badge">
<span className="icon">🔒</span>
<span>受保護事件</span>
<span className="tooltip">
此內容僅在特定中繼器可見
</span>
</div>
);
}
function EventActions({ event }: { event: any }) {
const isProtected = isProtectedEvent(event);
return (
<div className="actions">
<button disabled={isProtected}>
轉發
</button>
{isProtected && (
<span className="hint">受保護事件無法轉發</span>
)}
</div>
);
} 安全考量
非加密保護
"-" 標籤僅表達作者意圖,不提供加密保護。 任何接收到事件的客戶端仍然可以讀取內容。此機制依賴於中繼器的合作。
中繼器信任
- 惡意中繼器可能忽略此標籤並轉發事件
- 選擇信譽良好的中繼器很重要
- 對於真正敏感的內容,應使用加密(如 NIP-44)
客戶端責任
- 客戶端應尊重此標籤,不主動轉發受保護事件
- 應在 UI 中清楚標示受保護狀態
- 禁用或警告可能導致轉發的操作
最佳實踐
- 明確標示:在 UI 中清楚顯示事件的保護狀態
- 選擇性發布:僅發布到信任的中繼器
- 結合加密:對敏感內容同時使用加密和保護標籤
- 告知用戶:解釋保護機制的限制
- 驗證來源:中繼器應驗證事件確實來自作者
限制
- 依賴中繼器合作,無法強制執行
- 不防止已接收內容的用戶手動分享
- 不提供內容加密
- 可能影響內容的可發現性和傳播
相關 NIPs
參考資源
已複製連結