fix: reject non-integer/negative spell kinds, de-flake a timing test

Per remaining "previously missed" findings:
- parseSpell() now requires k tags to be non-negative integers
  (Number.isInteger && >= 0), not just finite — a relay-sourced spell
  claiming kind "1.5" or "-1" no longer passes through into a
  malformed filter.
- The resolveTimestamp wall-clock tests asserted toBeCloseTo a single
  captured `now`, which a slow runner or timing skew between the two
  Date.now() calls could flake. Replaced with a before/after range
  assertion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto
This commit is contained in:
2026-09-06 18:32:25 +02:00
parent e8d4d5d20b
commit 56927e79e0
2 changed files with 31 additions and 5 deletions

View File

@@ -27,13 +27,22 @@ function parsedSpell(overrides: Partial<ParsedSpell>): ParsedSpell {
describe('resolveTimestamp', () => {
it('resolves a relative duration against now', () => {
const now = Math.floor(Date.now() / 1000);
expect(resolveTimestamp('7d')).toBeCloseTo(now - 7 * 86400, -1);
const before = Math.floor(Date.now() / 1000) - 7 * 86400;
const result = resolveTimestamp('7d');
const after = Math.floor(Date.now() / 1000) - 7 * 86400;
// A range, not toBeCloseTo against a single captured `now` — a slow test
// runner or wall-clock skew between the two Date.now() calls could
// otherwise make this flaky.
expect(result).toBeGreaterThanOrEqual(before);
expect(result).toBeLessThanOrEqual(after);
});
it('resolves "now"', () => {
const now = Math.floor(Date.now() / 1000);
expect(resolveTimestamp('now')).toBeCloseTo(now, -1);
const before = Math.floor(Date.now() / 1000);
const result = resolveTimestamp('now');
const after = Math.floor(Date.now() / 1000);
expect(result).toBeGreaterThanOrEqual(before);
expect(result).toBeLessThanOrEqual(after);
});
it('resolves an absolute unix timestamp', () => {
@@ -88,6 +97,19 @@ describe('encodeSpellTags / parseSpell round-trip', () => {
});
});
describe('parseSpell', () => {
it('drops non-integer and negative k tags — kinds are non-negative integers', () => {
const event = spellEvent([
['cmd', 'REQ'],
['k', '1'],
['k', '1.5'],
['k', '-1'],
['k', 'not-a-number'],
]);
expect(parseSpell(event).kinds).toEqual([1]);
});
});
describe('isValidTagLetter', () => {
it('accepts a single letter', () => {
expect(isValidTagLetter('t')).toBe(true);

View File

@@ -107,7 +107,11 @@ export function encodeSpellTags(input: SpellInput): string[][] {
}
export function parseSpell(event: NostrEvent): ParsedSpell {
const kinds = tagValues(event, 'k').map(Number).filter((n) => Number.isFinite(n));
// Nostr kinds are non-negative integers — a relay-sourced spell claiming
// e.g. "1.5" or "-1" would otherwise pass through into a malformed filter.
const kinds = tagValues(event, 'k')
.map(Number)
.filter((n) => Number.isInteger(n) && n >= 0);
const authorsTag = event.tags.find(([name]) => name === 'authors');
const tagFilterTag = event.tags.find(([name]) => name === 'tag');
const limitValue = tagValue(event, 'limit');