@nostrify/nostrify 0.48.2 → 0.48.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -3
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +8 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/dist/BunkerURI.ts +0 -58
- package/dist/NBrowserSigner.ts +0 -100
- package/dist/NCache.ts +0 -73
- package/dist/NConnectSigner.ts +0 -188
- package/dist/NIP05.ts +0 -51
- package/dist/NIP50.ts +0 -24
- package/dist/NIP98.ts +0 -111
- package/dist/NIP98Client.ts +0 -36
- package/dist/NKinds.ts +0 -26
- package/dist/NPool.ts +0 -243
- package/dist/NRelay1.ts +0 -447
- package/dist/NSchema.ts +0 -291
- package/dist/NSecSigner.ts +0 -62
- package/dist/NSet.ts +0 -210
- package/dist/RelayError.ts +0 -22
- package/dist/ln/LNURL.ts +0 -146
- package/dist/ln/mod.ts +0 -4
- package/dist/ln/types/LNURLCallback.ts +0 -7
- package/dist/ln/types/LNURLDetails.ts +0 -19
- package/dist/mod.ts +0 -17
- package/dist/test/ErrorRelay.ts +0 -52
- package/dist/test/MockRelay.ts +0 -92
- package/dist/test/TestRelayServer.ts +0 -185
- package/dist/test/mod.ts +0 -28
- package/dist/uploaders/BlossomUploader.ts +0 -100
- package/dist/uploaders/NostrBuildUploader.ts +0 -89
- package/dist/uploaders/mod.ts +0 -2
- package/dist/utils/CircularSet.ts +0 -36
- package/dist/utils/Machina.ts +0 -66
- package/dist/utils/N64.ts +0 -23
- package/dist/utils/mod.ts +0 -2
package/dist/ln/LNURL.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import type { NostrEvent } from '@nostrify/types';
|
|
2
|
-
import { bech32 } from '@scure/base';
|
|
3
|
-
|
|
4
|
-
import { LNURLCallback } from './types/LNURLCallback.ts';
|
|
5
|
-
import { LNURLDetails } from './types/LNURLDetails.ts';
|
|
6
|
-
|
|
7
|
-
import { NSchema as n, z } from '../NSchema.ts';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Represents an LNURL, with methods to fetch details and generate invoices.
|
|
11
|
-
*/
|
|
12
|
-
export class LNURL {
|
|
13
|
-
/** Underlying HTTP(s) URL of the user. */
|
|
14
|
-
readonly url: URL;
|
|
15
|
-
/** Fetch function to use for HTTP requests. */
|
|
16
|
-
private fetch: typeof globalThis.fetch;
|
|
17
|
-
|
|
18
|
-
constructor(
|
|
19
|
-
/** Underlying HTTP(s) URL of the user. */
|
|
20
|
-
url: URL,
|
|
21
|
-
/** Options for the LNURL class. */
|
|
22
|
-
opts?: {
|
|
23
|
-
/** Fetch function to use for HTTP requests. */
|
|
24
|
-
fetch: typeof globalThis.fetch;
|
|
25
|
-
},
|
|
26
|
-
) {
|
|
27
|
-
this.url = url;
|
|
28
|
-
this.fetch = opts?.fetch ?? globalThis.fetch.bind(globalThis);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Create an LNURL object from a bech32 `lnurl1...` string.
|
|
33
|
-
* Throws if the value is not a valid lnurl.
|
|
34
|
-
*/
|
|
35
|
-
static fromString(
|
|
36
|
-
value: string,
|
|
37
|
-
opts?: { fetch: typeof globalThis.fetch },
|
|
38
|
-
): LNURL {
|
|
39
|
-
if (!n.bech32().safeParse(value).success) {
|
|
40
|
-
throw new Error('Expected a bech32 string starting with "lnurl1"');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const { prefix, words } = bech32.decode(
|
|
44
|
-
value as `${string}1${string}`,
|
|
45
|
-
20000,
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
if (prefix !== 'lnurl') {
|
|
49
|
-
throw new Error('Expected a bech32 string starting with "lnurl1"');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const data = bech32.fromWords(words);
|
|
53
|
-
const url = new URL(new TextDecoder().decode(data));
|
|
54
|
-
|
|
55
|
-
return new LNURL(url, opts);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Create an LNURL object from a lightning address (email-like format).
|
|
60
|
-
* Throws if the value is not a valid lightning address.
|
|
61
|
-
*/
|
|
62
|
-
static fromLightningAddress(
|
|
63
|
-
ln: string,
|
|
64
|
-
opts?: { fetch: typeof globalThis.fetch },
|
|
65
|
-
): LNURL {
|
|
66
|
-
if (!z.string().email().safeParse(ln).success) {
|
|
67
|
-
throw new Error(
|
|
68
|
-
'Expected a lightning address in email-like format (eg "example@getalby.com")',
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const [name, host] = ln.split('@');
|
|
73
|
-
const url = new URL(`/.well-known/lnurlp/${name}`, `https://${host}`);
|
|
74
|
-
|
|
75
|
-
return new LNURL(url, opts);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Returns the LNURL object as a bech32-encoded `lnurl1...` string. */
|
|
79
|
-
toString(): `lnurl1${string}` {
|
|
80
|
-
const data = new TextEncoder().encode(this.url.toString());
|
|
81
|
-
const words = bech32.toWords(data);
|
|
82
|
-
return bech32.encode('lnurl', words, 20000);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Resolve an LNURL to its details. */
|
|
86
|
-
async getDetails(opts?: { signal?: AbortSignal }): Promise<LNURLDetails> {
|
|
87
|
-
const response = await this.fetch(this.url, opts);
|
|
88
|
-
const json = await response.json();
|
|
89
|
-
return LNURL.lnurlDetailsSchema().parse(json);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Generate an LNURL invoice from the params. */
|
|
93
|
-
async getInvoice(opts: {
|
|
94
|
-
/** Amount in millisatoshis to send to the user. */
|
|
95
|
-
amount: number;
|
|
96
|
-
/** NIP-57 Zap Request (kind 9734) event. */
|
|
97
|
-
nostr?: NostrEvent;
|
|
98
|
-
/** Signal to abort the request. */
|
|
99
|
-
signal?: AbortSignal;
|
|
100
|
-
}): Promise<LNURLCallback> {
|
|
101
|
-
const details = await this.getDetails(opts);
|
|
102
|
-
const callback = new URL(details.callback);
|
|
103
|
-
|
|
104
|
-
callback.searchParams.set('amount', opts.amount.toString());
|
|
105
|
-
callback.searchParams.set('lnurl', this.toString());
|
|
106
|
-
|
|
107
|
-
if (opts.nostr) {
|
|
108
|
-
callback.searchParams.set('nostr', JSON.stringify(opts.nostr));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const response = await this.fetch(callback, opts);
|
|
112
|
-
const json = await response.json();
|
|
113
|
-
|
|
114
|
-
return LNURL.lnurlCallbackSchema().parse(json);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** LNURL response schema. */
|
|
118
|
-
static lnurlDetailsSchema(): z.ZodType<LNURLDetails> {
|
|
119
|
-
return z.object({
|
|
120
|
-
allowsNostr: z.boolean().optional(),
|
|
121
|
-
callback: z.string().url(),
|
|
122
|
-
commentAllowed: z.number().nonnegative().int().optional(),
|
|
123
|
-
maxSendable: z.number().positive().int(),
|
|
124
|
-
minSendable: z.number().positive().int(),
|
|
125
|
-
metadata: z.string(),
|
|
126
|
-
nostrPubkey: n.id().optional(),
|
|
127
|
-
tag: z.literal('payRequest'),
|
|
128
|
-
}).superRefine((details, ctx) => {
|
|
129
|
-
if (details.minSendable > details.maxSendable) {
|
|
130
|
-
ctx.addIssue({
|
|
131
|
-
code: z.ZodIssueCode.custom,
|
|
132
|
-
message: 'minSendable must be less than or equal to maxSendable',
|
|
133
|
-
path: ['minSendable'],
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
}) as z.ZodType<LNURLDetails>;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** LNURL callback schema. */
|
|
140
|
-
static lnurlCallbackSchema(): z.ZodType<LNURLCallback> {
|
|
141
|
-
return z.object({
|
|
142
|
-
pr: n.bech32('lnbc'),
|
|
143
|
-
routes: z.tuple([]),
|
|
144
|
-
}) as unknown as z.ZodType<LNURLCallback>;
|
|
145
|
-
}
|
|
146
|
-
}
|
package/dist/ln/mod.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/** LNURL `payRequest` details, as defined by LUD-06. Includes additional properties from NIP-57. */
|
|
2
|
-
export interface LNURLDetails {
|
|
3
|
-
/** Whether the LN SERVICE supports NIP-57 Lightning Zaps. */
|
|
4
|
-
allowsNostr?: boolean;
|
|
5
|
-
/** The URL from LN SERVICE which will accept the pay request parameters. */
|
|
6
|
-
callback: string;
|
|
7
|
-
/** The number of characters accepted for the `comment` query parameter on subsequent callback. (Should be interpreted as 0 if not provided). */
|
|
8
|
-
commentAllowed?: number;
|
|
9
|
-
/** Max millisatoshi amount LN SERVICE is willing to receive. */
|
|
10
|
-
maxSendable: number;
|
|
11
|
-
/** Min millisatoshi amount LN SERVICE is willing to receive, can not be less than 1 or more than `maxSendable`. */
|
|
12
|
-
minSendable: number;
|
|
13
|
-
/** Metadata json which must be presented as raw string here, this is required to pass signature verification at a later step. */
|
|
14
|
-
metadata: string;
|
|
15
|
-
/** The Nostr pubkey LN SERVICE will use to sign zap receipt events. Clients will use this to validate zap receipts. */
|
|
16
|
-
nostrPubkey?: string;
|
|
17
|
-
/** Type of LNURL. */
|
|
18
|
-
tag: 'payRequest';
|
|
19
|
-
}
|
package/dist/mod.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export { BunkerURI } from './BunkerURI.ts';
|
|
2
|
-
export { NBrowserSigner } from './NBrowserSigner.ts';
|
|
3
|
-
export { NCache } from './NCache.ts';
|
|
4
|
-
export { NConnectSigner, type NConnectSignerOpts } from './NConnectSigner.ts';
|
|
5
|
-
export { NIP05 } from './NIP05.ts';
|
|
6
|
-
export { NIP50 } from './NIP50.ts';
|
|
7
|
-
export { NIP98 } from './NIP98.ts';
|
|
8
|
-
export { NIP98Client, type NIP98ClientOpts } from './NIP98Client.ts';
|
|
9
|
-
export { NKinds } from './NKinds.ts';
|
|
10
|
-
export { NPool, type NPoolOpts } from './NPool.ts';
|
|
11
|
-
export { NRelay1, type NRelay1Opts } from './NRelay1.ts';
|
|
12
|
-
export { NSchema } from './NSchema.ts';
|
|
13
|
-
export { NSecSigner } from './NSecSigner.ts';
|
|
14
|
-
export { NSet } from './NSet.ts';
|
|
15
|
-
export { RelayError } from './RelayError.ts';
|
|
16
|
-
|
|
17
|
-
export type * from '@nostrify/types';
|
package/dist/test/ErrorRelay.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// deno-lint-ignore-file require-await require-yield
|
|
2
|
-
import type {
|
|
3
|
-
NostrEvent,
|
|
4
|
-
NostrFilter,
|
|
5
|
-
NostrRelayCLOSED,
|
|
6
|
-
NostrRelayCOUNT,
|
|
7
|
-
NostrRelayEOSE,
|
|
8
|
-
NostrRelayEVENT,
|
|
9
|
-
NRelay,
|
|
10
|
-
} from '@nostrify/types';
|
|
11
|
-
|
|
12
|
-
/** A relay storage class that intentionally throws errors for every method. */
|
|
13
|
-
export class ErrorRelay implements NRelay {
|
|
14
|
-
async *req(
|
|
15
|
-
_filters: NostrFilter[],
|
|
16
|
-
_opts?: { signal?: AbortSignal },
|
|
17
|
-
): AsyncIterable<NostrRelayEVENT | NostrRelayEOSE | NostrRelayCLOSED> {
|
|
18
|
-
throw new Error('This error is intentional.');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async event(
|
|
22
|
-
_event: NostrEvent,
|
|
23
|
-
_opts?: { signal?: AbortSignal },
|
|
24
|
-
): Promise<void> {
|
|
25
|
-
throw new Error('This error is intentional.');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async query(
|
|
29
|
-
_filters: NostrFilter[],
|
|
30
|
-
_opts?: { signal?: AbortSignal },
|
|
31
|
-
): Promise<NostrEvent[]> {
|
|
32
|
-
throw new Error('This error is intentional.');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async count(
|
|
36
|
-
_filters: NostrFilter[],
|
|
37
|
-
_opts?: { signal?: AbortSignal },
|
|
38
|
-
): Promise<NostrRelayCOUNT[2]> {
|
|
39
|
-
throw new Error('This error is intentional.');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async remove(
|
|
43
|
-
_filters: NostrFilter[],
|
|
44
|
-
_opts?: { signal?: AbortSignal },
|
|
45
|
-
): Promise<void> {
|
|
46
|
-
throw new Error('This error is intentional.');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async close(): Promise<void> {
|
|
50
|
-
throw new Error('This error is intentional.');
|
|
51
|
-
}
|
|
52
|
-
}
|
package/dist/test/MockRelay.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
// deno-lint-ignore-file require-await
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
NostrEvent,
|
|
5
|
-
NostrFilter,
|
|
6
|
-
NostrRelayCLOSED,
|
|
7
|
-
NostrRelayCOUNT,
|
|
8
|
-
NostrRelayEOSE,
|
|
9
|
-
NostrRelayEVENT,
|
|
10
|
-
NRelay,
|
|
11
|
-
} from '@nostrify/types';
|
|
12
|
-
import { matchFilters } from 'nostr-tools';
|
|
13
|
-
|
|
14
|
-
import { Machina } from '../utils/Machina.ts';
|
|
15
|
-
import { NSet } from '../NSet.ts';
|
|
16
|
-
|
|
17
|
-
/** Mock relay for testing. */
|
|
18
|
-
export class MockRelay extends NSet implements NRelay {
|
|
19
|
-
readonly subs: Map<
|
|
20
|
-
string,
|
|
21
|
-
{ filters: NostrFilter[]; machina: Machina<NostrEvent> }
|
|
22
|
-
> = new Map();
|
|
23
|
-
|
|
24
|
-
async *req(
|
|
25
|
-
filters: NostrFilter[],
|
|
26
|
-
opts?: { signal?: AbortSignal },
|
|
27
|
-
): AsyncIterable<NostrRelayEVENT | NostrRelayEOSE | NostrRelayCLOSED> {
|
|
28
|
-
const uuid = crypto.randomUUID();
|
|
29
|
-
const machina = new Machina<NostrEvent>(opts?.signal);
|
|
30
|
-
|
|
31
|
-
this.subs.set(uuid, { filters, machina });
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
for (const event of await this.query(filters)) {
|
|
35
|
-
yield ['EVENT', uuid, event];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
yield ['EOSE', uuid];
|
|
39
|
-
|
|
40
|
-
for await (const event of machina) {
|
|
41
|
-
yield ['EVENT', uuid, event];
|
|
42
|
-
}
|
|
43
|
-
} finally {
|
|
44
|
-
this.subs.delete(uuid);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async event(event: NostrEvent): Promise<void> {
|
|
49
|
-
this.add(event);
|
|
50
|
-
|
|
51
|
-
for (const { filters, machina } of this.subs.values()) {
|
|
52
|
-
if (matchFilters(filters, event)) {
|
|
53
|
-
machina.push(event);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async query(filters: NostrFilter[]): Promise<NostrEvent[]> {
|
|
59
|
-
const events: NostrEvent[] = [];
|
|
60
|
-
|
|
61
|
-
for (const event of this) {
|
|
62
|
-
if (matchFilters(filters, event)) {
|
|
63
|
-
this.cache.get(event.id);
|
|
64
|
-
events.push(event);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return events;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async remove(filters: NostrFilter[]): Promise<void> {
|
|
72
|
-
for (const event of this) {
|
|
73
|
-
if (matchFilters(filters, event)) {
|
|
74
|
-
this.delete(event);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async count(filters: NostrFilter[]): Promise<NostrRelayCOUNT[2]> {
|
|
80
|
-
const events = await this.query(filters);
|
|
81
|
-
return {
|
|
82
|
-
count: events.length,
|
|
83
|
-
approximate: false,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
close(): Promise<void> {
|
|
88
|
-
return Promise.resolve();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
override [Symbol.toStringTag] = 'MockRelay';
|
|
92
|
-
}
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { MockRelay } from './mod.ts';
|
|
2
|
-
import type { NostrClientMsg, NostrEvent, NostrRelayMsg } from '@nostrify/types';
|
|
3
|
-
import { NSchema as n } from '../NSchema.ts';
|
|
4
|
-
import { WebSocket, WebSocketServer } from 'ws';
|
|
5
|
-
import { createServer, Server } from 'node:http';
|
|
6
|
-
import type { AddressInfo } from 'node:net';
|
|
7
|
-
import { Buffer } from 'node:buffer';
|
|
8
|
-
|
|
9
|
-
interface TestRelayServerOpts {
|
|
10
|
-
handleMessage?(socket: WebSocket, msg: NostrClientMsg): Promise<void> | void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class TestRelayServer {
|
|
14
|
-
private port = 0;
|
|
15
|
-
private inited = false;
|
|
16
|
-
private httpServer: Server;
|
|
17
|
-
private wsServer: WebSocketServer;
|
|
18
|
-
private opts: TestRelayServerOpts;
|
|
19
|
-
private connections = new Set<WebSocket>();
|
|
20
|
-
private controllers = new Map<string, AbortController>();
|
|
21
|
-
private store = new MockRelay();
|
|
22
|
-
|
|
23
|
-
constructor(opts?: TestRelayServerOpts) {
|
|
24
|
-
this.opts = opts || {};
|
|
25
|
-
this.httpServer = createServer();
|
|
26
|
-
this.wsServer = new WebSocketServer({ server: this.httpServer });
|
|
27
|
-
this.setupWebSocketServer();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
init() {
|
|
31
|
-
const { resolve, promise } = Promise.withResolvers<void>();
|
|
32
|
-
this.httpServer.listen(0, '127.0.0.1', () => {
|
|
33
|
-
this.port = (this.httpServer.address() as AddressInfo).port;
|
|
34
|
-
this.inited = true;
|
|
35
|
-
resolve();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
return promise;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
private setupWebSocketServer(): void {
|
|
42
|
-
this.wsServer.on('connection', (socket: WebSocket) => {
|
|
43
|
-
this.connections.add(socket);
|
|
44
|
-
|
|
45
|
-
socket.on('close', () => {
|
|
46
|
-
this.connections.delete(socket);
|
|
47
|
-
for (const [subId, controller] of this.controllers.entries()) {
|
|
48
|
-
controller.abort();
|
|
49
|
-
this.controllers.delete(subId);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
socket.on('message', (data: Buffer) => {
|
|
54
|
-
try {
|
|
55
|
-
const result = n.json().pipe(n.clientMsg()).safeParse(data.toString());
|
|
56
|
-
if (result.success) {
|
|
57
|
-
const handleMessage = this.opts?.handleMessage ??
|
|
58
|
-
this.handleMessage.bind(this);
|
|
59
|
-
handleMessage(socket, result.data);
|
|
60
|
-
}
|
|
61
|
-
} catch {
|
|
62
|
-
// do nothing
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
socket.on('error', (error: any) => {
|
|
67
|
-
console.error('WebSocket error:', error);
|
|
68
|
-
this.connections.delete(socket);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
send(socket: WebSocket, msg: NostrRelayMsg): void {
|
|
74
|
-
if (!this.inited) throw new Error('TestRelayServer not initialized');
|
|
75
|
-
if (socket.readyState === WebSocket.OPEN) {
|
|
76
|
-
socket.send(JSON.stringify(msg));
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private async handleMessage(
|
|
81
|
-
socket: WebSocket,
|
|
82
|
-
msg: NostrClientMsg,
|
|
83
|
-
): Promise<void> {
|
|
84
|
-
if (!this.inited) throw new Error('TestRelayServer not initialized');
|
|
85
|
-
|
|
86
|
-
switch (msg[0]) {
|
|
87
|
-
case 'REQ': {
|
|
88
|
-
const [_, subId, ...filters] = msg;
|
|
89
|
-
|
|
90
|
-
const controller = new AbortController();
|
|
91
|
-
this.controllers.set(subId, controller);
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
for await (
|
|
95
|
-
const msg of this.store.req(filters, { signal: controller.signal })
|
|
96
|
-
) {
|
|
97
|
-
msg[1] = subId;
|
|
98
|
-
this.send(socket, msg);
|
|
99
|
-
}
|
|
100
|
-
} catch {
|
|
101
|
-
// do nothing
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
case 'CLOSE': {
|
|
108
|
-
const subId = msg[1];
|
|
109
|
-
this.controllers.get(subId)?.abort();
|
|
110
|
-
this.controllers.delete(subId);
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
case 'EVENT': {
|
|
115
|
-
const [_, event] = msg;
|
|
116
|
-
await this.store.event(event);
|
|
117
|
-
this.send(socket, ['OK', event.id, true, '']);
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
get url(): string {
|
|
124
|
-
if (!this.inited) throw new Error('TestRelayServer not initialized');
|
|
125
|
-
const addr = this.httpServer.address() as AddressInfo;
|
|
126
|
-
return `ws://${addr.address}:${addr.port}`;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// deno-lint-ignore require-await
|
|
130
|
-
async close(): Promise<void> {
|
|
131
|
-
if (!this.inited) throw new Error('TestRelayServer not initialized');
|
|
132
|
-
return new Promise((resolve) => {
|
|
133
|
-
this.connections.forEach((conn) => {
|
|
134
|
-
if (conn.readyState === WebSocket.OPEN) {
|
|
135
|
-
conn.close();
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
this.connections.clear();
|
|
139
|
-
|
|
140
|
-
this.controllers.forEach((controller) => controller.abort());
|
|
141
|
-
this.controllers.clear();
|
|
142
|
-
|
|
143
|
-
this.wsServer.close(() => {
|
|
144
|
-
this.httpServer.close(() => {
|
|
145
|
-
this.inited = false;
|
|
146
|
-
resolve();
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
open(): Promise<void> {
|
|
153
|
-
if (this.inited) throw new Error('TestRelayServer already initialized');
|
|
154
|
-
if (!this.httpServer.listening) {
|
|
155
|
-
const { resolve, promise } = Promise.withResolvers<void>();
|
|
156
|
-
this.httpServer = createServer();
|
|
157
|
-
this.wsServer = new WebSocketServer({ server: this.httpServer });
|
|
158
|
-
this.setupWebSocketServer();
|
|
159
|
-
this.httpServer.listen(0, '127.0.0.1', () => {
|
|
160
|
-
this.port = (this.httpServer.address() as AddressInfo).port;
|
|
161
|
-
this.inited = true;
|
|
162
|
-
resolve();
|
|
163
|
-
});
|
|
164
|
-
return promise;
|
|
165
|
-
}
|
|
166
|
-
return Promise.resolve();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
event(event: NostrEvent): Promise<void> {
|
|
170
|
-
if (!this.inited) throw new Error('TestRelayServer not initialized');
|
|
171
|
-
return this.store.event(event);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async [Symbol.asyncDispose](): Promise<void> {
|
|
175
|
-
if (this.inited) {
|
|
176
|
-
await this.close();
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
static async create(opts?: TestRelayServerOpts) {
|
|
181
|
-
const server = new TestRelayServer(opts);
|
|
182
|
-
await server.init();
|
|
183
|
-
return server;
|
|
184
|
-
}
|
|
185
|
-
}
|
package/dist/test/mod.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { NostrEvent } from '@nostrify/types';
|
|
2
|
-
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
4
|
-
|
|
5
|
-
export { ErrorRelay } from './ErrorRelay.ts';
|
|
6
|
-
export { MockRelay } from './MockRelay.ts';
|
|
7
|
-
|
|
8
|
-
/** Import a JSONL fixture by name in tests. */
|
|
9
|
-
export async function jsonlEvents(path: string): Promise<NostrEvent[]> {
|
|
10
|
-
const data = await readFile(path, { encoding: 'utf8' });
|
|
11
|
-
return data.split('\n').map((line: string) => JSON.parse(line));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Generate an event for use in tests. */
|
|
15
|
-
export function genEvent(
|
|
16
|
-
t: Partial<NostrEvent> = {},
|
|
17
|
-
sk: Uint8Array = generateSecretKey(),
|
|
18
|
-
): NostrEvent {
|
|
19
|
-
const { id, kind, pubkey, tags, content, created_at, sig } = finalizeEvent({
|
|
20
|
-
kind: 255,
|
|
21
|
-
created_at: 0,
|
|
22
|
-
content: '',
|
|
23
|
-
tags: [],
|
|
24
|
-
...t,
|
|
25
|
-
}, sk);
|
|
26
|
-
|
|
27
|
-
return { id, kind, pubkey, tags, content, created_at, sig };
|
|
28
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import type { NostrSigner, NUploader } from "@nostrify/types";
|
|
2
|
-
import { toHex } from "@smithy/util-hex-encoding";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
|
|
5
|
-
import { N64 } from "../utils/N64.ts";
|
|
6
|
-
|
|
7
|
-
/** BlossomUploader options. */
|
|
8
|
-
export interface BlossomUploaderOpts {
|
|
9
|
-
/** Blossom servers to use. */
|
|
10
|
-
servers: Request["url"][];
|
|
11
|
-
/** Signer for Blossom authorizations. */
|
|
12
|
-
signer: NostrSigner;
|
|
13
|
-
/** Custom fetch implementation. */
|
|
14
|
-
fetch?: typeof fetch;
|
|
15
|
-
/** Number of milliseconds until each request should expire. (Default: `60_000`) */
|
|
16
|
-
expiresIn?: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Upload files to Blossom servers. */
|
|
20
|
-
export class BlossomUploader implements NUploader {
|
|
21
|
-
private servers: Request["url"][];
|
|
22
|
-
private signer: NostrSigner;
|
|
23
|
-
private fetch: typeof fetch;
|
|
24
|
-
private expiresIn: number;
|
|
25
|
-
|
|
26
|
-
constructor(opts: BlossomUploaderOpts) {
|
|
27
|
-
this.servers = opts.servers;
|
|
28
|
-
this.signer = opts.signer;
|
|
29
|
-
this.fetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
30
|
-
this.expiresIn = opts.expiresIn ?? 60_000;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async upload(
|
|
34
|
-
file: File,
|
|
35
|
-
opts?: { signal?: AbortSignal },
|
|
36
|
-
): Promise<[["url", string], ...string[][]]> {
|
|
37
|
-
const digest = await crypto.subtle.digest(
|
|
38
|
-
"SHA-256",
|
|
39
|
-
await file.arrayBuffer(),
|
|
40
|
-
);
|
|
41
|
-
const x = toHex(new Uint8Array(digest));
|
|
42
|
-
|
|
43
|
-
const now = Date.now();
|
|
44
|
-
const expiration = now + this.expiresIn;
|
|
45
|
-
|
|
46
|
-
const event = await this.signer.signEvent({
|
|
47
|
-
kind: 24242,
|
|
48
|
-
content: `Upload ${file.name}`,
|
|
49
|
-
created_at: Math.floor(now / 1000),
|
|
50
|
-
tags: [
|
|
51
|
-
["t", "upload"],
|
|
52
|
-
["x", x],
|
|
53
|
-
["size", file.size.toString()],
|
|
54
|
-
["expiration", Math.floor(expiration / 1000).toString()],
|
|
55
|
-
],
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const authorization = `Nostr ${N64.encodeEvent(event)}`;
|
|
59
|
-
|
|
60
|
-
return Promise.any(this.servers.map(async (server) => {
|
|
61
|
-
const url = new URL("/upload", server);
|
|
62
|
-
|
|
63
|
-
const response = await this.fetch(url, {
|
|
64
|
-
method: "PUT",
|
|
65
|
-
body: file,
|
|
66
|
-
headers: {
|
|
67
|
-
authorization,
|
|
68
|
-
"content-type": file.type,
|
|
69
|
-
},
|
|
70
|
-
signal: opts?.signal,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const json = await response.json();
|
|
74
|
-
const data = BlossomUploader.schema().parse(json);
|
|
75
|
-
|
|
76
|
-
const tags: [["url", string], ...string[][]] = [
|
|
77
|
-
["url", data.url],
|
|
78
|
-
["x", data.sha256],
|
|
79
|
-
["ox", data.sha256],
|
|
80
|
-
["size", data.size.toString()],
|
|
81
|
-
];
|
|
82
|
-
|
|
83
|
-
if (data.type) {
|
|
84
|
-
tags.push(["m", data.type]);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return tags;
|
|
88
|
-
}));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Blossom "BlobDescriptor" schema. */
|
|
92
|
-
private static schema() {
|
|
93
|
-
return z.object({
|
|
94
|
-
url: z.string(),
|
|
95
|
-
sha256: z.string(),
|
|
96
|
-
size: z.number(),
|
|
97
|
-
type: z.string().optional(),
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
}
|