mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Create @ditto/nip98 package
This commit is contained in:
parent
72851bc536
commit
f0add87c6d
7 changed files with 49 additions and 41 deletions
|
|
@ -7,6 +7,7 @@
|
||||||
"./packages/lang",
|
"./packages/lang",
|
||||||
"./packages/mastoapi",
|
"./packages/mastoapi",
|
||||||
"./packages/metrics",
|
"./packages/metrics",
|
||||||
|
"./packages/nip98",
|
||||||
"./packages/policies",
|
"./packages/policies",
|
||||||
"./packages/ratelimiter",
|
"./packages/ratelimiter",
|
||||||
"./packages/router",
|
"./packages/router",
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,10 @@
|
||||||
|
import { buildAuthEventTemplate, parseAuthRequest, type ParseAuthRequestOpts, validateAuthEvent } from '@ditto/nip98';
|
||||||
import { HTTPException } from '@hono/hono/http-exception';
|
import { HTTPException } from '@hono/hono/http-exception';
|
||||||
import { NostrEvent } from '@nostrify/nostrify';
|
import { NostrEvent } from '@nostrify/nostrify';
|
||||||
|
|
||||||
import { type AppContext, type AppMiddleware } from '@/app.ts';
|
import { type AppContext, type AppMiddleware } from '@/app.ts';
|
||||||
import { ReadOnlySigner } from '@/signers/ReadOnlySigner.ts';
|
import { ReadOnlySigner } from '@/signers/ReadOnlySigner.ts';
|
||||||
import { localRequest } from '@/utils/api.ts';
|
import { localRequest } from '@/utils/api.ts';
|
||||||
import {
|
|
||||||
buildAuthEventTemplate,
|
|
||||||
parseAuthRequest,
|
|
||||||
type ParseAuthRequestOpts,
|
|
||||||
validateAuthEvent,
|
|
||||||
} from '@/utils/nip98.ts';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NIP-98 auth.
|
* NIP-98 auth.
|
||||||
|
|
|
||||||
|
|
@ -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`. */
|
/** Parses a hashtag, eg `#yolo`. */
|
||||||
const hashtagSchema = z.string().regex(/^\w{1,30}$/);
|
const hashtagSchema = z.string().regex(/^\w{1,30}$/);
|
||||||
|
|
||||||
|
|
@ -96,7 +84,6 @@ const walletSchema = z.object({
|
||||||
|
|
||||||
export {
|
export {
|
||||||
booleanParamSchema,
|
booleanParamSchema,
|
||||||
decode64Schema,
|
|
||||||
fileSchema,
|
fileSchema,
|
||||||
filteredArray,
|
filteredArray,
|
||||||
hashtagSchema,
|
hashtagSchema,
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,8 @@
|
||||||
import { NSchema as n } from '@nostrify/nostrify';
|
import { NSchema as n } from '@nostrify/nostrify';
|
||||||
import { getEventHash, verifyEvent } from 'nostr-tools';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { safeUrlSchema, sizesSchema } from '@/schema.ts';
|
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. */
|
/** Kind 0 standardized fields extended with Ditto custom fields. */
|
||||||
const metadataSchema = n.metadata().and(z.object({
|
const metadataSchema = n.metadata().and(z.object({
|
||||||
fields: z.tuple([z.string(), z.string()]).array().optional().catch(undefined),
|
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. */
|
/** NIP-30 custom emoji tag. */
|
||||||
type EmojiTag = z.infer<typeof emojiTagSchema>;
|
type EmojiTag = z.infer<typeof emojiTagSchema>;
|
||||||
|
|
||||||
export {
|
export { type EmojiTag, emojiTagSchema, metadataSchema, relayInfoDocSchema, screenshotsSchema, serverMetaSchema };
|
||||||
type EmojiTag,
|
|
||||||
emojiTagSchema,
|
|
||||||
metadataSchema,
|
|
||||||
relayInfoDocSchema,
|
|
||||||
screenshotsSchema,
|
|
||||||
serverMetaSchema,
|
|
||||||
signedEventSchema,
|
|
||||||
};
|
|
||||||
|
|
|
||||||
7
packages/nip98/deno.json
Normal file
7
packages/nip98/deno.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "@ditto/nip98",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"exports": {
|
||||||
|
".": "./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 { encodeHex } from '@std/encoding/hex';
|
||||||
import { EventTemplate, nip13 } from 'nostr-tools';
|
import { type EventTemplate, nip13 } from 'nostr-tools';
|
||||||
|
|
||||||
import { decode64Schema } from '@/schema.ts';
|
import { decode64Schema, signedEventSchema } from './schema.ts';
|
||||||
import { signedEventSchema } from '@/schemas/nostr.ts';
|
|
||||||
import { eventAge, findTag, nostrNow } from '@/utils.ts';
|
|
||||||
import { Time } from '@/utils/time.ts';
|
|
||||||
|
|
||||||
/** Decode a Nostr event from a base64 encoded string. */
|
/** Decode a Nostr event from a base64 encoded string. */
|
||||||
const decode64EventSchema = decode64Schema.pipe(n.json()).pipe(signedEventSchema);
|
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. */
|
/** Compare the auth event with the request, returning a zod SafeParse type. */
|
||||||
function validateAuthEvent(req: Request, event: NostrEvent, opts: ParseAuthRequestOpts = {}) {
|
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
|
const schema = signedEventSchema
|
||||||
.refine((event) => event.kind === 27235, 'Event must be kind 27235')
|
.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];
|
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 };
|
export { buildAuthEventTemplate, parseAuthRequest, type ParseAuthRequestOpts, validateAuthEvent };
|
||||||
20
packages/nip98/schema.ts
Normal file
20
packages/nip98/schema.ts
Normal 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');
|
||||||
Loading…
Add table
Reference in a new issue