Nostr kind:30000 ユーザリスト編集アプリ 外部仕様
目的
Nostr 上で kind:30000(NIP-51 のリスト) を編集する Web アプリを提供する。利用者は自分のリストを取得・作成・編集し、公開/非公開のエントリを混在させて運用できる。
本書は「利用者視点の提供機能」「採用NIP/kind」「複数リレーの扱い」「相互運用性の方針」「リレーに格納するイベント設計」を規定する。
適用範囲
- 対象:利用者本人が作成する kind:30000 リスト(複数)
- 入出力:
- 入力:npub(bech32)
- 内部保存・イベント格納:hex 公開鍵
- 実行環境:PC ブラウザ前提
- 永続化:ブラウザを閉じた後のローカル永続化は行わない(反映ボタン押下までメモリ保持)
採用する NIP と kind
採用NIP
- NIP-51:リスト(kind:30000)とタグ設計
- NIP-19:npub/nsec の bech32 エンコード/デコード
- NIP-04:非公開エントリの暗号化(自分だけが復号可能)
- NIP-09:削除(kind:5)慣習
利用する kind
- kind:30000:リスト本体(NIP-51)
- kind:0:ユーザプロファイル(アイコン・名前・表示名)
- kind:10002:推奨リレー(読み取り/書き込み先決定に利用)
- kind:5:削除イベント(NIP-09)
画面と利用者体験(UI/動作)
1) ログイン
- 起動直後に nsec(bech32) を入力してログインする(PoC)。
- nsec はアプリ内メモリにのみ保持し、保存しない。
注:nsec の直接入力はセキュリティ上推奨されない。運用上の注意をUIに明示する。
2) リスト一覧画面
- 自分の kind:30000 を購読し、d タグごとにグルーピングして一覧表示する。
- 同一 d が複数イベントとして存在する場合は、created_at が最大のイベントを最新として採用する。
- 一覧には以下を表示する:
- 表示名:title タグがあれば title、なければ d
- d 値
- updated(created_at)
- 参考:イベント id(短縮表示)
- 各リストに「編集」ボタンを用意し、編集画面に遷移する。
- 「新規リスト」ボタンで新規作成モーダルを開く。
3) 新規リスト作成
- d は ユーザ入力必須。
- 同一 pubkey の既存 kind:30000 に同じ d が存在する場合はエラー(作成を拒否)。
- title は任意(未設定の場合、表示名は d を用いる)。
- description は任意。
4) リスト編集画面
- リストのメタ情報を編集できる:
- title(任意)
- description(任意)
- d(固定識別子として扱う。UI上に表示し、基本は変更しない)
- エントリ一覧を表示し、各エントリで以下を編集できる:
- petname(横長テキスト入力、任意)
- 「非公開」チェックボックス(公開/非公開の切替)
- 削除(削除予定としてグレー表示)
エントリ追加
- 追加は npub の貼り付けのみを受け付ける。
- 追加前に kind:0 を取得し、アイコン・名前・表示名をプレビュー表示して、利用者が追加するか判断する。
- 既に同一公開鍵(hex)が存在する場合は「追加済み」を表示して追加しない。
- petname の未入力時は 空欄のまま保持する(プロフィール名などで自動補完しない)。
エントリ並び順
件数制限
- 編集時の最大件数は 1,000件。
- 1,000件に到達した場合:新規追加を拒否。
- 既存データが 1,000件を超えている場合:処理を中断せず、削除のみ可能。
反映(publish)
- 「反映」ボタン押下時に、kind:30000 を フルリストで再送する。
- 反映前はブラウザ内メモリに保持し、リレーへ書き出さない。
- publish 後にのみ、リレーごとの成功/失敗/タイムアウトをダイアログで表示する。
リスト削除
- 「リスト削除」操作で kind:5 を発行する(詳細は後述)。
- 削除要求後、リレーによってはデータが残る可能性があるため、注意を表示する。
複数リレーの扱い(読み取り/書き込み)
リレー選定方針
- 既定の bootstrap relay(初期値)を持つ。
- ログイン後、kind:10002 を取得できた場合は、そこに記載された推奨リレーを優先して使用する。
- 取得できない場合は bootstrap relay を継続使用する。
- リレーURLは正規化し、重複を排除し、最大 20 件までを扱う。
読み取り
- kind:30000:authors に自分の pubkey を指定して購読する。
- kind:0:エントリ追加時や一覧表示に必要なプロファイル取得のため、authors に対象 pubkey を指定して購読する。
- kind:10002:authors に自分の pubkey を指定して取得する。
書き込み
- kind:30000 の publish は、選定した複数リレーへ並列送信する。
- kind:5 の削除イベントも同様に複数リレーへ送信する。
- 書き込み結果は relay 単位で可視化する。
競合と統合
- 同一 d の kind:30000 が複数取得できる場合、created_at 最大を最新として採用する。
- created_at が極端に未来の値であるイベントは、悪意・誤設定の可能性があるため無視する(詳細は残課題)。
相互運用性(他クライアントとの整合性)
NIP-51 準拠のタグ運用
- 公開エントリは kind:3 形式の p タグとして格納する:
- relay_hint が無い場合でも、第3要素に空文字
"" を入れ、petname を第4要素に固定する。
- リストメタ情報は NIP-51 標準タグで保持する:
- title:
["title", "..."]
- description:
["description", "..."]
非公開エントリの表現
- 非公開エントリは 同一 kind:30000 イベント内に混在させる。
- 非公開エントリは
content フィールドに、NIP-04 で暗号化した JSON として格納する。
- 暗号化対象の平文は、p タグ配列(公開と同じ kind:3 形式)の JSON:
- 例:
[["p", <hex>, "", <petname>], ...]
- 復号対象は 自分の公開鍵のみ(自分だけが読める前提)。
リレーに格納するイベントのデータ設計
kind:30000(リスト)
- 必須タグ:
- 任意タグ:
title:表示名
description:リスト説明
- 公開エントリ:
- 非公開エントリ:
content に NIP-04 暗号化データを格納(前述のJSON)
- 更新:
- publish 時はフルリストを再送し、最新は created_at で選定する。
kind:5(削除)
- 対象指定は a タグで行う:
a = "30000:<pubkey_hex>:<d>"
- 併せて k タグを付与する:
k = "30000"(タグ配列は ["k","30000"])
- content は空とする。
文字数・値の制約
- リスト/入力の上限:
- エントリ数:最大 1,000(追加拒否、超過時は削除のみ可)
- title:最大 120 文字
- description:最大 2,000 文字
- petname:最大 200 文字
- リレーURL:最大 20 件、
wss://(または ws://)のみ
残課題(TODO)
- created_at が未来過ぎるイベントを無視する防御規則を定義し、実装する(例:現在時刻 + 許容差を超えた場合は除外)。
- 複数リレー間でデータが不整合な場合(最新がリレーごとに異なる、削除が反映されない等)の可視化・警告の仕様を詰める。
- nsec 直接入力を本番向けに置き換える方針(NIP-07 等)を別途策定する(PoC外)。
付録:イベント例(概要)
kind:30000(公開エントリのみ)
- tags:
["d", "friends"], ["title", "Close friends"], ["description", "..."] , ["p", "<hex>", "", "..."] ...
- content:空
kind:30000(非公開エントリあり)
- tags:公開分の p タグのみ
- content:
[["p", "<hex>", "", "<petname>"], ...] を NIP-04 で暗号化した文字列
kind:5(削除)
- tags:
["a", "30000:<pubkey_hex>:friends"], ["k","30000"]
- content:空