git clone https://github.com/forgesworn/nostr-veil.git
cd nostr-veil
npm install
npm run build
npm testnpm test # run all tests once
npm run test:watch # watch mode
npm run lint # type-check only (no emit)
npm run demo # start the interactive demo (Vite dev server)The demo is a separate Vite app in demo/ with its own package.json. Run cd demo && npm install if it hasn't been set up.
src/nip85/— NIP-85 event builders, parsers, validators, filterssrc/proof/— LSAG ring-signature trust circles, contributions, aggregation, verificationsrc/signing.ts— BIP-340 Schnorr event signingtest/— mirrorssrc/structuredemo/— Vite + React interactive demo
- British English — licence, serialise, initialise, behaviour
- ESM only —
.jsextensions in all local imports - TDD — write the failing test first, then the implementation
- Commits —
type: descriptionformat (e.g.feat:,fix:,docs:,test:)
Tests use Vitest. Each module has its own test file mirroring the source path:
src/proof/circle.ts → test/proof/circle.test.ts
Run a single test file:
npx vitest run test/proof/circle.test.ts- Fork and create a branch from
main - Write tests for any new functionality
- Ensure
npm testandnpm run buildpass - Keep commits focused — one logical change per commit
- Open a PR with a clear description of what and why
By contributing you agree your contributions will be licensed under MIT.
This guide walks through adding full support for a new NIP-85 assertion kind, using the hypothetical kind 30386 as the running example.
Add a metrics interface for the new kind and a constant to NIP85_KINDS:
// In NIP85_KINDS:
NEW_KIND: 30386,
// New metrics interface:
export interface NewKindMetrics {
rank?: number
my_custom_metric?: number
}Follow the pattern of the existing builders. Import your new metrics type,
choose the correct structural tags (d + an identifying tag such as e or
a), and convert metrics with metricsToTags:
export function buildNewKindAssertion(subject: string, metrics: NewKindMetrics): EventTemplate {
return {
kind: NIP85_KINDS.NEW_KIND,
tags: [['d', subject], ['x', subject], ...metricsToTags(metrics)],
content: '',
}
}Add full JSDoc with @param, @returns, and @example tags.
parseAssertion in src/nip85/parsers.ts is generic — it works for any kind
that uses a d tag. If your new kind introduces a new structural tag (like x
in the example above) that should be skipped during metric extraction, add it to
the META_TAGS set:
const META_TAGS = new Set(['d', 'p', 'e', 'a', 'k', 'x'])Add the new kind to ASSERTION_KINDS so validateAssertion accepts it:
const ASSERTION_KINDS = new Set<number>([
NIP85_KINDS.USER,
NIP85_KINDS.EVENT,
NIP85_KINDS.ADDRESSABLE,
NIP85_KINDS.IDENTIFIER,
NIP85_KINDS.NEW_KIND, // add this
])If the new kind has metric-specific validation rules (value ranges, required
fields), add them inside validateAssertion after the existing rank check,
guarded by event.kind === NIP85_KINDS.NEW_KIND.
Create test/nip85/newkind.test.ts (mirror the source path). Cover at minimum:
buildNewKindAssertionproduces the correct kind, d-tag, and metric tagsparseAssertioncorrectly extracts the subject and metricsvalidateAssertionaccepts a valid event and rejects each invalid caseassertionFilterreturns the right filter for the new kind
import { describe, it, expect } from 'vitest'
import { buildNewKindAssertion, parseAssertion, validateAssertion, NIP85_KINDS } from '../../src/nip85/index.js'
describe('kind 30386', () => {
it('builds with correct kind and tags', () => {
const tmpl = buildNewKindAssertion('subject123', { rank: 50 })
expect(tmpl.kind).toBe(NIP85_KINDS.NEW_KIND)
expect(tmpl.tags).toContainEqual(['d', 'subject123'])
})
})src/nip85/index.ts already re-exports everything via export * from './builders.js'
etc., so no changes are needed there. If you created a new file (e.g. a separate
src/nip85/newkind.ts), add it:
export * from './newkind.js'- Add the new kind to
NIP85_KINDSsection ofllms.txtandllms-full.txt - Update the API surface list in
llms.txt - Add a row to the README kind table if one exists
This guide walks through adding a new veil-* tag to the ring-signature proof
layer, using a hypothetical veil-algo tag as the running example.
Inside aggregateContributions, add the new tag to the returned event's tags
array:
return {
kind: NIP85_KINDS.USER,
tags: [
['d', subject],
['p', subject],
...metricTags,
['veil-ring', ...circle.members],
['veil-threshold', String(contributions.length), String(circle.size)],
['veil-algo', 'lsag-secp256k1'], // new tag
...sigTags,
],
content: '',
}If the tag value is derived from inputs (e.g. the aggregation function name), thread it through the function signature and update callers.
Read the new tag in verifyProof and incorporate it into validation logic or
the returned ProofVerification object. If the tag is informational only, you
can expose it as an additional field. If it affects validity, add error messages
to the errors array:
const algoTag = event.tags.find(t => t[0] === 'veil-algo')
const algorithm = algoTag?.[1] ?? 'unknown'
// Optionally validate: if (algorithm !== 'lsag-secp256k1') errors.push(…)If the new field is meaningful to callers, extend ProofVerification in
src/proof/types.ts:
export interface ProofVerification {
// …existing fields…
algorithm?: string
}parseAssertion skips tags that start with veil- automatically via:
if (META_TAGS.has(name) || name.startsWith('veil-')) continueNo change is needed — all veil-* tags are already excluded from metric
extraction. If your tag does not start with veil-, add it to META_TAGS
explicitly.
Add test cases to test/proof/ covering:
aggregateContributionsemits the new tag with the correct valueverifyProofreads the tag correctly and surfaces the value (or rejects when invalid)- Round-trip: build → aggregate → verify produces a consistent result
it('emits veil-algo tag', () => {
const tmpl = aggregateContributions(circle, subject, [contrib])
const algoTag = tmpl.tags.find(t => t[0] === 'veil-algo')
expect(algoTag?.[1]).toBe('lsag-secp256k1')
})Add the new tag to the proof tag table in both files:
| `veil-algo` | `['veil-algo', algorithmId]` | Identifies the ring signature algorithm used |
Update the "Key Concepts" section in the README accordingly.