From f0add87c6db2f0c573ecec07201bf223d97295a1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 21 Feb 2025 15:35:03 -0600 Subject: [PATCH] Create @ditto/nip98 package --- deno.json | 1 + packages/ditto/middleware/auth98Middleware.ts | 7 +---- packages/ditto/schema.ts | 13 ---------- packages/ditto/schemas/nostr.ts | 16 +----------- packages/nip98/deno.json | 7 +++++ packages/{ditto/utils => nip98}/nip98.ts | 26 ++++++++++++++----- packages/nip98/schema.ts | 20 ++++++++++++++ 7 files changed, 49 insertions(+), 41 deletions(-) create mode 100644 packages/nip98/deno.json rename packages/{ditto/utils => nip98}/nip98.ts (79%) create mode 100644 packages/nip98/schema.ts diff --git a/deno.json b/deno.json index 4466b7b3..20d87204 100644 --- a/deno.json +++ b/deno.json @@ -7,6 +7,7 @@ "./packages/lang", "./packages/mastoapi", "./packages/metrics", + "./packages/nip98", "./packages/policies", "./packages/ratelimiter", "./packages/router", diff --git a/packages/ditto/middleware/auth98Middleware.ts b/packages/ditto/middleware/auth98Middleware.ts index 48ac5875..6cab6566 100644 --- a/packages/ditto/middleware/auth98Middleware.ts +++ b/packages/ditto/middleware/auth98Middleware.ts @@ -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. diff --git a/packages/ditto/schema.ts b/packages/ditto/schema.ts index 30b4520a..56c9b998 100644 --- a/packages/ditto/schema.ts +++ b/packages/ditto/schema.ts @@ -13,18 +13,6 @@ function filteredArray(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, diff --git a/packages/ditto/schemas/nostr.ts b/packages/ditto/schemas/nostr.ts index 05cd0f31..558e6c13 100644 --- a/packages/ditto/schemas/nostr.ts +++ b/packages/ditto/schemas/nostr.ts @@ -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; -export { - type EmojiTag, - emojiTagSchema, - metadataSchema, - relayInfoDocSchema, - screenshotsSchema, - serverMetaSchema, - signedEventSchema, -}; +export { type EmojiTag, emojiTagSchema, metadataSchema, relayInfoDocSchema, screenshotsSchema, serverMetaSchema }; diff --git a/packages/nip98/deno.json b/packages/nip98/deno.json new file mode 100644 index 00000000..108e1bb8 --- /dev/null +++ b/packages/nip98/deno.json @@ -0,0 +1,7 @@ +{ + "name": "@ditto/nip98", + "version": "1.0.0", + "exports": { + ".": "./nip98.ts" + } +} diff --git a/packages/ditto/utils/nip98.ts b/packages/nip98/nip98.ts similarity index 79% rename from packages/ditto/utils/nip98.ts rename to packages/nip98/nip98.ts index f83fcddb..e8574c86 100644 --- a/packages/ditto/utils/nip98.ts +++ b/packages/nip98/nip98.ts @@ -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 }; diff --git a/packages/nip98/schema.ts b/packages/nip98/schema.ts new file mode 100644 index 00000000..a0cf627c --- /dev/null +++ b/packages/nip98/schema.ts @@ -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');