跳至主要內容
入門

NIP-07 瀏覽器擴充

深入了解 Nostr 的瀏覽器擴充簽名標準,window.nostr API 和安全密鑰管理。

10 分鐘

什麼是 NIP-07?

NIP-07 定義了瀏覽器擴充功能與網頁應用之間的標準介面(window.nostr)。 這讓網頁應用可以請求簽名和加密操作,而不需要直接存取用戶的私鑰, 大幅提升安全性。

安全優勢: 私鑰永遠不會暴露給網頁應用。所有簽名操作都在擴充功能內部完成, 用戶可以在授權前審核每個請求。

window.nostr API

符合 NIP-07 的擴充功能會在 window 物件上注入 nostr 屬性:

interface Nostr {
  // 取得公鑰
  getPublicKey(): Promise<string>

  // 簽署事件
  signEvent(event: UnsignedEvent): Promise<SignedEvent>

  // NIP-04 加密(可選)
  nip04?: {
    encrypt(pubkey: string, plaintext: string): Promise<string>
    decrypt(pubkey: string, ciphertext: string): Promise<string>
  }

  // NIP-44 加密(可選)
  nip44?: {
    encrypt(pubkey: string, plaintext: string): Promise<string>
    decrypt(pubkey: string, ciphertext: string): Promise<string>
  }

  // 取得中繼器列表(可選)
  getRelays?(): Promise<RelayMap>
}

核心方法

getPublicKey()

取得用戶的公鑰(hex 格式):

// 檢查擴充功能是否存在
if (!window.nostr) {
  alert('請安裝 Nostr 瀏覽器擴充功能')
  return
}

// 取得公鑰
const pubkey = await window.nostr.getPublicKey()
console.log('公鑰:', pubkey)
// => "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"

signEvent()

簽署未簽名的事件:

// 建立未簽名事件
const unsignedEvent = {
  kind: 1,
  created_at: Math.floor(Date.now() / 1000),
  tags: [],
  content: 'Hello, Nostr!'
  // 注意:不包含 id, pubkey, sig
}

// 請求擴充功能簽名
const signedEvent = await window.nostr.signEvent(unsignedEvent)

console.log(signedEvent)
// {
//   id: "abc123...",
//   pubkey: "3bf0c63f...",
//   created_at: 1704067200,
//   kind: 1,
//   tags: [],
//   content: "Hello, Nostr!",
//   sig: "sig123..."
// }

加密方法

NIP-04 加密

// 加密訊息
const recipientPubkey = "npub1..."
const encrypted = await window.nostr.nip04.encrypt(
  recipientPubkey,
  "這是一條私密訊息"
)
// => "encrypted_content?iv=..."

// 解密訊息
const decrypted = await window.nostr.nip04.decrypt(
  senderPubkey,
  encrypted
)
// => "這是一條私密訊息"

NIP-44 加密(推薦)

// 檢查是否支援 NIP-44
if (window.nostr.nip44) {
  const encrypted = await window.nostr.nip44.encrypt(
    recipientPubkey,
    "這是一條私密訊息"
  )

  const decrypted = await window.nostr.nip44.decrypt(
    senderPubkey,
    encrypted
  )
}

完整使用範例

async function postNote(content) {
  // 1. 檢查擴充功能
  if (!window.nostr) {
    throw new Error('請安裝 Nostr 擴充功能')
  }

  // 2. 取得公鑰
  const pubkey = await window.nostr.getPublicKey()

  // 3. 建立事件
  const event = {
    kind: 1,
    created_at: Math.floor(Date.now() / 1000),
    tags: [],
    content
  }

  // 4. 簽名
  const signedEvent = await window.nostr.signEvent(event)

  // 5. 發送到中繼器
  const relay = new WebSocket('wss://relay.damus.io')
  relay.onopen = () => {
    relay.send(JSON.stringify(['EVENT', signedEvent]))
  }

  return signedEvent
}

// 使用
postNote('Hello from my web app!')
  .then(event => console.log('已發布:', event.id))
  .catch(err => console.error('錯誤:', err))

熱門擴充功能

錯誤處理

async function safeGetPublicKey() {
  try {
    // 檢查擴充功能
    if (typeof window.nostr === 'undefined') {
      return { error: 'NO_EXTENSION', message: '未安裝擴充功能' }
    }

    // 請求公鑰(用戶可能拒絕)
    const pubkey = await window.nostr.getPublicKey()
    return { pubkey }

  } catch (err) {
    // 用戶拒絕授權
    if (err.message?.includes('rejected') || err.message?.includes('denied')) {
      return { error: 'USER_REJECTED', message: '用戶拒絕授權' }
    }
    // 其他錯誤
    return { error: 'UNKNOWN', message: err.message }
  }
}

// 使用
const result = await safeGetPublicKey()
if (result.error) {
  console.log('錯誤:', result.message)
} else {
  console.log('公鑰:', result.pubkey)
}

TypeScript 類型定義

// types/nostr.d.ts
interface Window {
  nostr?: Nostr
}

interface Nostr {
  getPublicKey(): Promise<string>
  signEvent(event: UnsignedEvent): Promise<Event>
  getRelays?(): Promise<Record<string, RelayPolicy>>
  nip04?: {
    encrypt(pubkey: string, plaintext: string): Promise<string>
    decrypt(pubkey: string, ciphertext: string): Promise<string>
  }
  nip44?: {
    encrypt(pubkey: string, plaintext: string): Promise<string>
    decrypt(pubkey: string, ciphertext: string): Promise<string>
  }
}

interface UnsignedEvent {
  kind: number
  created_at: number
  tags: string[][]
  content: string
}

interface Event extends UnsignedEvent {
  id: string
  pubkey: string
  sig: string
}

interface RelayPolicy {
  read: boolean
  write: boolean
}

安全最佳實踐

應該做

  • • 總是檢查 window.nostr 是否存在
  • • 處理用戶拒絕授權的情況
  • • 使用 try-catch 包裝所有呼叫
  • • 清楚告知用戶將簽名的內容

不應該做

  • • 要求用戶輸入私鑰
  • • 假設擴充功能一定存在
  • • 忽略錯誤處理
  • • 批量簽名而不讓用戶確認

功能偵測

function detectNostrCapabilities() {
  if (!window.nostr) {
    return { available: false }
  }

  return {
    available: true,
    getPublicKey: true,
    signEvent: true,
    nip04: !!window.nostr.nip04,
    nip44: !!window.nostr.nip44,
    getRelays: !!window.nostr.getRelays
  }
}

const caps = detectNostrCapabilities()
console.log('Nostr 功能:', caps)
// {
//   available: true,
//   getPublicKey: true,
//   signEvent: true,
//   nip04: true,
//   nip44: true,
//   getRelays: true
// }

開發提示: 在開發時可以使用 nos2x 進行測試,因為它是開源且輕量的。 正式環境建議支援多種擴充功能,提供更好的用戶體驗。

已複製連結
已複製到剪貼簿