{"id":52085805,"url":"https://github.com/akiomik/zod-nostr","last_synced_at":"2026-08-05T23:00:50.130Z","repository":{"id":371533132,"uuid":"1301557199","full_name":"akiomik/zod-nostr","owner":"akiomik","description":"Spec-faithful, tunable Zod schemas \u0026 codecs for Nostr — strict by default, loosen deliberately. Classic + zod/mini.","archived":false,"fork":false,"pushed_at":"2026-07-30T17:59:57.000Z","size":585,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-07-30T18:10:50.387Z","etag":null,"topics":["nips","nostr","zod","zod-validation","zod-validators"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/akiomik.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null,"disclosure":null},"funding":{"github":["akiomik"],"patreon":null,"open_collective":null,"ko_fi":"akiomik","tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2026-07-15T11:48:49.000Z","updated_at":"2026-07-30T18:01:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/akiomik/zod-nostr","commit_stats":null,"previous_names":["akiomik/zod-nostr"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/akiomik/zod-nostr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akiomik%2Fzod-nostr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akiomik%2Fzod-nostr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akiomik%2Fzod-nostr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akiomik%2Fzod-nostr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/akiomik","download_url":"https://codeload.github.com/akiomik/zod-nostr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akiomik%2Fzod-nostr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":36323912,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-08-05T02:00:06.619Z","response_time":104,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["nips","nostr","zod","zod-validation","zod-validators"],"created_at":"2026-08-04T01:00:15.636Z","updated_at":"2026-08-05T23:00:50.120Z","avatar_url":"https://github.com/akiomik.png","language":"TypeScript","funding_links":["https://github.com/sponsors/akiomik","https://ko-fi.com/akiomik"],"categories":["Other"],"sub_categories":[],"readme":"# zod-nostr\n\n[![npm version](https://badge.fury.io/js/zod-nostr.svg)](https://badge.fury.io/js/zod-nostr)\n[![CI](https://github.com/akiomik/zod-nostr/actions/workflows/ci.yml/badge.svg)](https://github.com/akiomik/zod-nostr/actions/workflows/ci.yml)\n[![codecov](https://codecov.io/gh/akiomik/zod-nostr/graph/badge.svg?token=GDL3P5N6L7)](https://codecov.io/gh/akiomik/zod-nostr)\n\n**Spec-faithful, tunable Zod schemas \u0026 codecs for [Nostr](https://nostr.com) —\nstrict by default, loosen deliberately.**\n\n- **Strict, spec-faithful atoms** — each schema validates to exactly what its NIP\n  permits, and never rejects spec-valid input.\n- **Tunable in both directions** — strict base schemas compose with\n  `.optional()`, `.catch()`, `.default()`, and `.refine()`, so you can\n  deliberately loosen them to absorb the messy data real relays serve.\n- **Classic zod and zod/mini** — one set of rules, written once against\n  `zod/v4/core` and re-exposed through two entry points with each flavor's native\n  `.check()` chaining; reach for tree-shakeable\n  [zod/mini](https://zod.dev/packages/mini) or ergonomic\n  [classic zod](https://zod.dev).\n- **Precise type inference** — schemas are the single source of truth, so\n  inferred types and runtime checks can't drift apart.\n- **Bidirectional codecs** — NIP-19 bech32 entities and NIP-21 `nostr:` URIs\n  decode *and* encode, not just validate.\n- **Opt-in checks** — expensive or context-dependent verification (signatures,\n  proof of work, expiration, authentication) composes via `.check()` instead of\n  being baked into every parse.\n- **Framework-agnostic** — a pure schema layer you can drop into any Nostr stack.\n\nCovers NIP-01, NIP-05, NIP-10, NIP-11, NIP-13, NIP-19, NIP-21, NIP-40, NIP-42,\nNIP-45, NIP-50, NIP-67, and NIP-70 — see [Supported NIPs](#supported-nips).\n\n## Installation\n\n```sh\nnpm install zod-nostr zod\n```\n\n`zod` (`^4.4.3`) is a peer dependency — bring your own version.\n\nzod-nostr ships as ESM only.\n\n## Quick start\n\n### classic zod\n\n```ts\nimport { z } from \"zod\";\nimport { zostr } from \"zod-nostr\";\n\nconst schema = z.object({ pubkey: zostr.pubkey() });\nschema.parse({ pubkey: \"3bf0c63f...\" });\n\n// Structure only, no signature check:\nzostr.event().parse(someEvent);\n\n// Structure + signature verification, composed explicitly:\nzostr.event().check(zostr.signatureCheck()).parse(someEvent);\n```\n\n### zod/mini\n\n```ts\nimport * as z from \"zod/mini\";\nimport { zostr } from \"zod-nostr/mini\";\n\nconst schema = z.object({ pubkey: zostr.pubkey() });\nz.parse(schema, { pubkey: \"3bf0c63f...\" });\n\nz.parse(zostr.event().check(zostr.signatureCheck()), someEvent);\n```\n\nThe `zostr` object exposes the identical set of functions from both entry\npoints — only the import path and the ambient zod flavor differ.\n\nEvery API has one **canonical owner path** — usually its spec namespace\n(`zostr.nip19.npub()`), a domain namespace for a cross-spec catalog\n(`zostr.nip01.metadataFields.*`), or the root for a cross-spec utility\n(`zostr.jsonCodec()`). Frequently used Nostr-wide concepts are also re-exposed at\nthe root as an ergonomic alias that is a direct reference to the same factory:\n\n```ts\nzostr.event(); // alias of zostr.nip01.event()\nzostr.event === zostr.nip01.event; // true\n```\n\n## Design notes\n\nThese notes summarize a few user-facing choices. The full public-API design\nprinciples — controllability, strict atoms, opt-in checks, versioning, and the\nverification bar for new APIs — live in [docs/design.md](docs/design.md).\n\n### Why two entry points?\n\nzod v4 ships two API flavors: classic zod (chainable methods, e.g.\n`z.string().min(1)`) and zod/mini (functional composition, e.g.\n`z.string().check(z.minLength(1))`, optimized for tree-shaking). The two\nflavors don't share method chains, but both are built on the same schema\nrepresentation in `zod/v4/core`.\n\nzod-nostr's validation logic (hex/bech32 formats, event structure, signature\nchecks, codecs) is written once against `zod/v4/core` and has no dependency\non `zod` or `zod/mini` itself. The `zod-nostr` (classic) and `zod-nostr/mini`\nentry points each re-wrap that shared logic through their own flavor's native\n`z.object()`, which is what makes `.check()`/`.optional()` and friends work\nnaturally on the schemas they return — there's no custom chaining sugar layered\non top.\n\n### Signature verification is opt-in, via `.check()`\n\n`zostr.event()` validates NIP-01 event *structure* (field shapes, hex\nlengths, tag shape) but does **not** verify the cryptographic signature by\ndefault. Verifying every event's signature is comparatively expensive, so\nforcing it into every `.parse()` call would be a poor default for bulk\ningestion paths that don't need it. Compose it explicitly instead:\n\n```ts\nzostr.event().check(zostr.signatureCheck())\n```\n\nThis mirrors zod's own check-composition style (e.g. `z.string().check(z.minLength(1))`)\nrather than inventing a bespoke `.verified()`-style chain method.\n\n### bech32 format check vs. codec\n\n- `zostr.bech32(prefix)` — validates that a string is a well-formed bech32\n  entity with the given prefix (`npub`, `nsec`, `note`, `nprofile`, `nevent`,\n  `naddr`). Returns the string as-is.\n- `zostr.npub()`, `zostr.nsec()`, etc. — full **codecs**: decode a bech32\n  string to its underlying value, and encode the value back to a bech32\n  string. Use `z.decode(zostr.npub(), npub)` / `z.encode(zostr.npub(), pubkey)`\n  (or `.decode()`/`.encode()` methods on the classic schema). See\n  [docs/API.md](docs/API.md) for each codec's decoded value.\n\nNote that `nsec()` decodes to raw bytes (`Uint8Array`), not a hex string,\nmatching how `nostr-tools` represents secret keys elsewhere\n(`generateSecretKey`, `finalizeEvent`, ...).\n\n## Supported NIPs\n\nCanonical paths below are `zostr.nipXX.*`; the curated Nostr-wide ones are also\naliased at the root (`zostr.event`, `zostr.npub`, …).\n\n| NIP | Coverage | Key canonical paths |\n| --- | --- | --- |\n| **NIP-01** | Event structure and templates, opt-in signature verification, kind:0 profile metadata (content codec and field-level atoms), the `REQ`/`COUNT` filter object, and relay/client protocol messages | `nip01.event`, `nip01.unsignedEvent`, `nip01.eventTemplate`, `nip01.signatureCheck`, `nip01.metadata`, `nip01.filter`, `nip01.relayMessage.*`, `nip01.clientMessage.*` |\n| **NIP-05** | Identifier and `.well-known/nostr.json` document validation | `nip05.identifier`, `nip05.nostrJsonDocument` |\n| **NIP-10** | kind:1 text notes and threads, marked reply/citation tags, and opt-in reply/thread checks | `nip10.textNote` |\n| **NIP-11** | Relay information document | `nip11.relayInformationDocument` |\n| **NIP-13** | Proof of work: the `nonce` tag schema and opt-in checks for achieved difficulty and the committed target | `nip13.nonceTag`, `nip13.powCheck`, `nip13.commitmentCheck` |\n| **NIP-19** | bech32 entities | `nip19.npub`, `nip19.nsec`, `nip19.note`, `nip19.nprofile`, `nip19.nevent`, `nip19.naddr` |\n| **NIP-21** | `nostr:` URIs over the supported NIP-19 entities (`nsec` excluded): validation-only, per-entity codecs, and decoding to a `{ type, data }` discriminated union | `nip21.any` |\n| **NIP-40** | Expiration timestamps: the `expiration` tag schema and an opt-in not-expired check | `nip40.expirationTag`, `nip40.expirationCheck` |\n| **NIP-42** | Authentication (`AUTH`): the `kind: 22242` auth event, the relay/client `AUTH` messages, and opt-in verification checks | `nip42.authEvent` |\n| **NIP-45** | Event counts (`COUNT`): request/response messages and the response body object | `nip45.count` |\n| **NIP-50** | Search: the filter extended with a `search` string and the `REQ` that carries it, an intentional superset of NIP-01's | `nip50.filter` |\n| **NIP-67** | EOSE completeness hint: `EOSE` extended with an optional hints array, a strict superset of NIP-01's | `nip67.relayMessage.eose` |\n| **NIP-70** | Protected events: the `[\"-\"]` marker tag schema and an opt-in check that a protected event's author is an authenticated pubkey | `nip70.protectedTag`, `nip70.protectedCheck` |\n\nSee [docs/API.md](docs/API.md) for the full API reference, and\n[docs/guides.md](docs/guides.md) for task-oriented how-to guides (building a\ntunable profile schema, composing opt-in checks).\n\n## Development\n\n```sh\nnpm run typecheck    # tsc --noEmit\nnpm run check        # biome check . (lint + format check)\nnpm run check:write  # biome check --write . (auto-fix)\nnpm test             # vitest run\nnpm run build        # emit dist/ (classic.js + mini.js)\n```\n\nCI (`.github/workflows/ci.yml`) runs all of the above on every push and pull\nrequest to `main`.\n\n## Release process\n\nVersioning follows\n[docs/design.md](docs/design.md#compatibility-and-versioning): before 1.0,\nbackward-incompatible public API changes bump the minor version, and\nbackward-compatible additions and fixes bump the patch version.\n\n1. Bump `version` in `package.json` and add a dated section to\n   `CHANGELOG.md` (move the relevant `[Unreleased]` entries under it).\n2. Merge that to `main`.\n3. Create a GitHub Release with tag `vX.Y.Z` (matching `package.json`'s\n   version) targeting `main`.\n\nPublishing a release triggers `.github/workflows/publish.yml`, which\ntype-checks, lints, tests, builds, verifies the tag matches\n`package.json`'s version, and runs `npm publish --access public`.\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fakiomik%2Fzod-nostr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fakiomik%2Fzod-nostr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fakiomik%2Fzod-nostr/lists"}