bd68a32708
Extract eleven topic areas into loadable skills so AGENTS.md can serve as a scannable overview instead of a specification dump. The file shrinks from 1480 to 358 lines (~76%) while keeping every concrete rule, critical code pattern, and pointer that an agent needs on first read. New Ditto-specific skills: - nostr-kinds: NIP-vs-custom-kind decision framework, kind ranges, tag design, content-vs-tags, NIP.md update rule, and Ditto's seven-location UI registration checklist for new kinds (NoteCard, PostDetailPage, extraKinds.ts, KIND_LABELS/KIND_ICONS in CommentContext, WELL_KNOWN_KIND_LABELS in ExternalContentHeader, EmbeddedNote/EmbeddedNaddr, ReplyComposeModal). - nostr-publishing: useNostrPublish, the read-modify-write pattern via fetchFreshEvent + prev for replaceable/addressable events, published_at contract, and d-tag collision prevention. - nostr-queries: the standard useNostr + useQuery pattern, combining kinds into one filter to avoid rate limits, and the NIP-52 validator walkthrough. - theming: @fontsource install flow, the Ditto runtime font-loader path (sanitizeUrl + sanitizeCssString), color scheme variables, useTheme toggle, and the isolate + negative-z-index gotcha. - ci-cd-publishing: Zapstore NIP-46 bunker auth (zsp + nip46-auth.mjs), nsite deploys (nsyte nbunksec + configured relays/servers), and Google Play AAB uploads via fastlane supply (service-account JSON base64 encoding and rotation). - capacitor-compat: WKWebView/WebView limitations, the downloadTextFile / openUrl helpers in src/lib/downloadFile.ts, platform detection, and the full plugin list. - git-workflow: pre-commit validation order and the Regression-of: trailer convention used by the release skill's changelog generator. Ported from mkstack, lightly adapted where needed: - nip19-routing: root-level /:nip19 routing and filter construction patterns (adapted to reference Ditto's existing NIP19Page). - nostr-relay-pools: nostr.relay() and nostr.group() for targeted queries. - nostr-encryption: NIP-44 / NIP-04 via the user's signer. - file-uploads: useUploadFile + Blossom + NIP-94 imeta tag construction. AGENTS.md itself now follows mkstack's density — concrete rules inline, one code example per section, pointer to the matching skill for details. The enumerations that previously bloated it (every shadcn primitive, every hook, every Capacitor plugin, the full NostrMetadata type dump, the NIP-19 prefix reference table, etc.) are either removed in favor of "ls the directory" or moved into their skill.
147 lines
6.7 KiB
Markdown
147 lines
6.7 KiB
Markdown
---
|
|
name: nip19-routing
|
|
description: Implement or populate the root-level NIP-19 router (/:nip19) that handles npub, nprofile, note, nevent, and naddr identifiers. Covers decoding, secure filter construction, and type-specific rendering for profiles, notes, events, and addressable events.
|
|
---
|
|
|
|
# NIP-19 Identifier Routing
|
|
|
|
NIP-19 defines the bech32-encoded identifiers used throughout Nostr (`npub1...`, `note1...`, `naddr1...`, etc.). This project routes all of them through a single root-level page at `/:nip19`, implemented by `src/pages/NIP19Page.tsx`.
|
|
|
|
Use this skill when the user wants to populate the `NIP19Page` sections with real views, add a new identifier type, or build links that point into the Nostr routing system.
|
|
|
|
## Identifier Reference
|
|
|
|
| Prefix | Payload | Use when… |
|
|
|--------------|------------------------------------------------------------------|--------------------------------------------------------------|
|
|
| `npub1` | 32-byte public key | Simple user reference |
|
|
| `nprofile1` | Public key + optional relay hints + petname | User reference with relay context |
|
|
| `note1` | 32-byte event ID (kind:1 text notes only, per NIP-10) | Referencing a short text note/thread |
|
|
| `nevent1` | Event ID + optional relay hints + author pubkey + kind | Any event kind, or notes where you need relay/author context |
|
|
| `naddr1` | `kind` + `pubkey` + `identifier` (`d` tag) + optional relay hints | Addressable events (kind 30000-39999): articles, products |
|
|
| `nsec1` | Private key | **Never display or route** — treat as a 404 |
|
|
| `nrelay1` | Relay URL | Deprecated |
|
|
|
|
### `note1` vs `nevent1`
|
|
|
|
- `note1` carries only an event ID, and is canonically tied to kind:1 text notes.
|
|
- `nevent1` can reference **any** kind and can carry relay hints + author pubkey. Prefer `nevent1` for non-kind-1 events or when you want to ship relay hints with a link.
|
|
|
|
### `npub1` vs `nprofile1`
|
|
|
|
- `npub1` is just a pubkey.
|
|
- `nprofile1` adds relay hints and a petname. Prefer it for shareable profile links where discoverability matters.
|
|
|
|
## Routing Rules
|
|
|
|
1. **All NIP-19 identifiers are handled at the URL root**: `/:nip19` in `AppRouter.tsx`. Never nest them under paths like `/note/:id` or `/profile/:npub`.
|
|
2. **Invalid, vacant, or unsupported identifiers** (including `nsec1` and `nrelay1`) render the 404 page. The `NIP19Page` boilerplate already handles this.
|
|
3. **Addressable event URLs must include the author**. `naddr1` already encodes `pubkey` + `kind` + `identifier`, which is exactly what a secure query filter needs. If you ever design an alternative URL, use the shape `/:npub/:dtag`, never `/:dtag` alone — otherwise anyone can publish a conflicting event with the same `d` tag.
|
|
|
|
## Decoding and Filtering
|
|
|
|
Nostr relay filters only accept hex strings. Always decode the NIP-19 identifier before building a filter.
|
|
|
|
```ts
|
|
import { nip19 } from 'nostr-tools';
|
|
|
|
const decoded = nip19.decode(value); // throws on invalid input
|
|
|
|
switch (decoded.type) {
|
|
case 'npub': {
|
|
const pubkey = decoded.data; // hex string
|
|
return nostr.query([{ kinds: [0], authors: [pubkey], limit: 1 }]);
|
|
}
|
|
|
|
case 'nprofile': {
|
|
const { pubkey /*, relays */ } = decoded.data;
|
|
return nostr.query([{ kinds: [0], authors: [pubkey], limit: 1 }]);
|
|
}
|
|
|
|
case 'note': {
|
|
const id = decoded.data;
|
|
return nostr.query([{ ids: [id], kinds: [1], limit: 1 }]);
|
|
}
|
|
|
|
case 'nevent': {
|
|
const { id /*, relays, author, kind */ } = decoded.data;
|
|
return nostr.query([{ ids: [id], limit: 1 }]);
|
|
}
|
|
|
|
case 'naddr': {
|
|
const { kind, pubkey, identifier } = decoded.data;
|
|
return nostr.query([{
|
|
kinds: [kind],
|
|
authors: [pubkey], // critical: prevents d-tag spoofing
|
|
'#d': [identifier],
|
|
limit: 1,
|
|
}]);
|
|
}
|
|
|
|
default:
|
|
// nsec, nrelay, unknown → 404
|
|
throw new Error('Unsupported Nostr identifier');
|
|
}
|
|
```
|
|
|
|
### Common mistakes
|
|
|
|
```ts
|
|
// ❌ Passing bech32 into a filter
|
|
nostr.query([{ ids: [naddr] }]);
|
|
|
|
// ❌ Addressable lookup without the author — anyone can spoof the d-tag
|
|
nostr.query([{ kinds: [30023], '#d': [slug] }]);
|
|
|
|
// ✅ Decode first, then include author
|
|
const { kind, pubkey, identifier } = nip19.decode(naddr).data;
|
|
nostr.query([{ kinds: [kind], authors: [pubkey], '#d': [identifier] }]);
|
|
```
|
|
|
|
## Populating `NIP19Page`
|
|
|
|
`src/pages/NIP19Page.tsx` already:
|
|
|
|
- Decodes `params.nip19` with `nip19.decode`.
|
|
- Branches on `decoded.type` with a section for each supported identifier.
|
|
- Redirects invalid / unsupported identifiers to the 404 page.
|
|
- Provides a responsive container wrapper.
|
|
|
|
To turn it into a real router, replace each placeholder section with a concrete component:
|
|
|
|
| `decoded.type` | Typical view |
|
|
|-----------------------|---------------------------------------------------------------|
|
|
| `npub` / `nprofile` | Profile page: header from kind 0, feed of the user's events |
|
|
| `note` | Single kind:1 text note with thread + replies |
|
|
| `nevent` | Generic event renderer; branch on `kind` for specialized UIs |
|
|
| `naddr` | Addressable-event view (article, product, community, etc.) |
|
|
|
|
Inside each branch, pass the decoded payload (not the raw bech32 string) to a child component. That keeps filter construction colocated with the fetching hook and removes any chance of a re-decode mismatch.
|
|
|
|
## Linking to NIP-19 Routes
|
|
|
|
When building links elsewhere in the app:
|
|
|
|
```tsx
|
|
import { nip19 } from 'nostr-tools';
|
|
import { Link } from 'react-router-dom';
|
|
|
|
// To a profile
|
|
<Link to={`/${nip19.npubEncode(pubkey)}`}>Profile</Link>
|
|
|
|
// To an addressable event (article, product, …)
|
|
<Link to={`/${nip19.naddrEncode({ kind, pubkey, identifier, relays })}`}>
|
|
Open
|
|
</Link>
|
|
|
|
// To a specific event of any kind, with relay hints
|
|
<Link to={`/${nip19.neventEncode({ id, relays, author, kind })}`}>Open</Link>
|
|
```
|
|
|
|
Always encode with the **most specific** identifier you have context for (`nprofile` > `npub`, `nevent` > `note`, `naddr` for addressable). The extra metadata makes links more robust across relays.
|
|
|
|
## Security Recap
|
|
|
|
- Decode **before** querying.
|
|
- For addressable events, always include `authors: [pubkey]` in the filter — the `d` tag alone is not a trust boundary.
|
|
- Treat `nsec1` and any unknown/invalid identifier as 404. Never render, log, or echo a decoded `nsec`.
|