Create @ditto/nip98 package

This commit is contained in:
Alex Gleason 2025-02-21 15:35:03 -06:00
parent 72851bc536
commit f0add87c6d
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
7 changed files with 49 additions and 41 deletions

View file

@ -7,6 +7,7 @@
"./packages/lang",
"./packages/mastoapi",
"./packages/metrics",
"./packages/nip98",
"./packages/policies",
"./packages/ratelimiter",
"./packages/router",

View file

@ -1,15 +1,10 @@
import { buildAuthEventTemplate, parseAuthRequest, type ParseAuthRequestOpts, validateAuthEvent } from '@ditto/nip98';
import { HTTPException } from '@hono/hono/http-exception';
import { NostrEvent } from '@nostrify/nostrify';
import { type AppContext, type AppMiddleware } from '@/app.ts';
import { ReadOnlySigner } from '@/signers/ReadOnlySigner.ts';
import { localRequest } from '@/utils/api.ts';
import {
buildAuthEventTemplate,
parseAuthRequest,
type ParseAuthRequestOpts,
validateAuthEvent,
} from '@/utils/nip98.ts';
/**
* NIP-98 auth.

View file

@ -13,18 +13,6 @@ function filteredArray<T extends z.ZodTypeAny>(schema: T) {
));
}
/** https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem */
const decode64Schema = z.string().transform((value, ctx) => {
try {
const binString = atob(value);
const bytes = Uint8Array.from(binString, (m) => m.codePointAt(0)!);
return new TextDecoder().decode(bytes);
} catch (_e) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid base64', fatal: true });
return z.NEVER;
}
});
/** Parses a hashtag, eg `#yolo`. */
const hashtagSchema = z.string().regex(/^\w{1,30}$/);
@ -96,7 +84,6 @@ const walletSchema = z.object({
export {
booleanParamSchema,
decode64Schema,
fileSchema,
filteredArray,
hashtagSchema,

View file

@ -1,14 +1,8 @@
import { NSchema as n } from '@nostrify/nostrify';
import { getEventHash, verifyEvent } from 'nostr-tools';
import { z } from 'zod';
import { safeUrlSchema, sizesSchema } from '@/schema.ts';
/** Nostr event schema that also verifies the event's signature. */
const signedEventSchema = n.event()
.refine((event) => event.id === getEventHash(event), 'Event ID does not match hash')
.refine(verifyEvent, 'Event signature is invalid');
/** Kind 0 standardized fields extended with Ditto custom fields. */
const metadataSchema = n.metadata().and(z.object({
fields: z.tuple([z.string(), z.string()]).array().optional().catch(undefined),
@ -68,12 +62,4 @@ const emojiTagSchema = z.tuple([z.literal('emoji'), z.string(), z.string().url()
/** NIP-30 custom emoji tag. */
type EmojiTag = z.infer<typeof emojiTagSchema>;
export {
type EmojiTag,
emojiTagSchema,
metadataSchema,
relayInfoDocSchema,
screenshotsSchema,
serverMetaSchema,
signedEventSchema,
};
export { type EmojiTag, emojiTagSchema, metadataSchema, relayInfoDocSchema, screenshotsSchema, serverMetaSchema };

7
packages/nip98/deno.json Normal file
View file

@ -0,0 +1,7 @@
{
"name": "@ditto/nip98",
"version": "1.0.0",
"exports": {
".": "./nip98.ts"
}
}

View file

@ -1,11 +1,8 @@
import { NostrEvent, NSchema as n } from '@nostrify/nostrify';
import { type NostrEvent, NSchema as n } from '@nostrify/nostrify';
import { encodeHex } from '@std/encoding/hex';
import { EventTemplate, nip13 } from 'nostr-tools';
import { type EventTemplate, nip13 } from 'nostr-tools';
import { decode64Schema } from '@/schema.ts';
import { signedEventSchema } from '@/schemas/nostr.ts';
import { eventAge, findTag, nostrNow } from '@/utils.ts';
import { Time } from '@/utils/time.ts';
import { decode64Schema, signedEventSchema } from './schema.ts';
/** Decode a Nostr event from a base64 encoded string. */
const decode64EventSchema = decode64Schema.pipe(n.json()).pipe(signedEventSchema);
@ -32,7 +29,7 @@ async function parseAuthRequest(req: Request, opts: ParseAuthRequestOpts = {}) {
/** Compare the auth event with the request, returning a zod SafeParse type. */
function validateAuthEvent(req: Request, event: NostrEvent, opts: ParseAuthRequestOpts = {}) {
const { maxAge = Time.minutes(1), validatePayload = true, pow = 0 } = opts;
const { maxAge = 60_000, validatePayload = true, pow = 0 } = opts;
const schema = signedEventSchema
.refine((event) => event.kind === 27235, 'Event must be kind 27235')
@ -87,4 +84,19 @@ function tagValue(event: NostrEvent, tagName: string): string | undefined {
return findTag(event.tags, tagName)?.[1];
}
/** Get the current time in Nostr format. */
const nostrNow = (): number => Math.floor(Date.now() / 1000);
/** Convenience function to convert Nostr dates into native Date objects. */
const nostrDate = (seconds: number): Date => new Date(seconds * 1000);
/** Return the event's age in milliseconds. */
function eventAge(event: NostrEvent): number {
return Date.now() - nostrDate(event.created_at).getTime();
}
function findTag(tags: string[][], name: string): string[] | undefined {
return tags.find((tag) => tag[0] === name);
}
export { buildAuthEventTemplate, parseAuthRequest, type ParseAuthRequestOpts, validateAuthEvent };

20
packages/nip98/schema.ts Normal file
View file

@ -0,0 +1,20 @@
import { NSchema as n } from '@nostrify/nostrify';
import { getEventHash, verifyEvent } from 'nostr-tools';
import z from 'zod';
/** https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem */
export const decode64Schema = z.string().transform((value, ctx) => {
try {
const binString = atob(value);
const bytes = Uint8Array.from(binString, (m) => m.codePointAt(0)!);
return new TextDecoder().decode(bytes);
} catch (_e) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid base64', fatal: true });
return z.NEVER;
}
});
/** Nostr event schema that also verifies the event's signature. */
export const signedEventSchema = n.event()
.refine((event) => event.id === getEventHash(event), 'Event ID does not match hash')
.refine(verifyEvent, 'Event signature is invalid');