nostr-websocket-utils 0.2.4 → 0.3.0
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/LICENSE +1 -1
- package/README.md +151 -103
- package/dist/__mocks__/extendedWsMock.d.ts +35 -0
- package/dist/__mocks__/extendedWsMock.js +156 -0
- package/dist/__mocks__/logger.d.ts +9 -0
- package/dist/__mocks__/logger.js +6 -0
- package/dist/__mocks__/mockLogger.d.ts +41 -0
- package/dist/__mocks__/mockLogger.js +47 -0
- package/dist/__mocks__/mockserver.d.ts +31 -0
- package/dist/__mocks__/mockserver.js +39 -0
- package/dist/__mocks__/wsMock.d.ts +26 -0
- package/dist/__mocks__/wsMock.js +120 -0
- package/dist/client.d.ts +105 -0
- package/dist/client.js +105 -0
- package/dist/core/client.d.ts +94 -0
- package/dist/core/client.js +360 -0
- package/dist/core/nostr-server.d.ts +27 -0
- package/dist/core/nostr-server.js +95 -0
- package/dist/core/queue.d.ts +61 -0
- package/dist/core/queue.js +108 -0
- package/dist/core/server.d.ts +27 -0
- package/dist/core/server.js +114 -0
- package/dist/crypto/bech32.d.ts +26 -0
- package/dist/crypto/bech32.js +163 -0
- package/dist/crypto/handlers.d.ts +11 -0
- package/dist/crypto/handlers.js +36 -0
- package/dist/crypto/index.d.ts +5 -0
- package/dist/crypto/index.js +5 -0
- package/dist/crypto/schnorr.d.ts +16 -0
- package/dist/crypto/schnorr.js +51 -0
- package/dist/endpoints/metrics.d.ts +29 -0
- package/dist/endpoints/metrics.js +101 -0
- package/dist/index.d.ts +11 -6
- package/dist/index.js +16 -4
- package/dist/nips/index.d.ts +19 -0
- package/dist/nips/index.js +34 -0
- package/dist/nips/nip-01.d.ts +34 -0
- package/dist/nips/nip-01.js +145 -0
- package/dist/nips/nip-02.d.ts +83 -0
- package/dist/nips/nip-02.js +123 -0
- package/dist/nips/nip-04.d.ts +36 -0
- package/dist/nips/nip-04.js +105 -0
- package/dist/nips/nip-05.d.ts +86 -0
- package/dist/nips/nip-05.js +151 -0
- package/dist/nips/nip-09.d.ts +92 -0
- package/dist/nips/nip-09.js +190 -0
- package/dist/nips/nip-11.d.ts +64 -0
- package/dist/nips/nip-11.js +154 -0
- package/dist/nips/nip-13.d.ts +73 -0
- package/dist/nips/nip-13.js +128 -0
- package/dist/nips/nip-15.d.ts +83 -0
- package/dist/nips/nip-15.js +101 -0
- package/dist/nips/nip-16.d.ts +88 -0
- package/dist/nips/nip-16.js +150 -0
- package/dist/nips/nip-19.d.ts +28 -0
- package/dist/nips/nip-19.js +103 -0
- package/dist/nips/nip-20.d.ts +59 -0
- package/dist/nips/nip-20.js +95 -0
- package/dist/nips/nip-22.d.ts +89 -0
- package/dist/nips/nip-22.js +142 -0
- package/dist/nips/nip-26.d.ts +52 -0
- package/dist/nips/nip-26.js +139 -0
- package/dist/nips/nip-28.d.ts +103 -0
- package/dist/nips/nip-28.js +170 -0
- package/dist/nips/nip-33.d.ts +94 -0
- package/dist/nips/nip-33.js +133 -0
- package/dist/nostr-server.d.ts +23 -0
- package/dist/nostr-server.js +44 -0
- package/dist/server.d.ts +13 -3
- package/dist/server.js +60 -33
- package/dist/transport/base.d.ts +54 -0
- package/dist/transport/base.js +104 -0
- package/dist/transport/websocket.d.ts +22 -0
- package/dist/transport/websocket.js +122 -0
- package/dist/types/events.d.ts +63 -0
- package/dist/types/events.js +5 -0
- package/dist/types/filters.d.ts +19 -0
- package/dist/types/filters.js +5 -0
- package/dist/types/handlers.d.ts +80 -0
- package/dist/types/handlers.js +5 -0
- package/dist/types/index.d.ts +118 -39
- package/dist/types/index.js +21 -1
- package/dist/types/logger.d.ts +40 -0
- package/dist/types/logger.js +5 -0
- package/dist/types/messages.d.ts +135 -0
- package/dist/types/messages.js +40 -0
- package/dist/types/nostr.d.ts +120 -39
- package/dist/types/nostr.js +5 -10
- package/dist/types/options.d.ts +154 -0
- package/dist/types/options.js +5 -0
- package/dist/types/relays.d.ts +26 -0
- package/dist/types/relays.js +5 -0
- package/dist/types/scoring.d.ts +47 -0
- package/dist/types/scoring.js +29 -0
- package/dist/types/socket.d.ts +99 -0
- package/dist/types/socket.js +5 -0
- package/dist/types/transport.d.ts +97 -0
- package/dist/types/transport.js +5 -0
- package/dist/types/validation.d.ts +50 -0
- package/dist/types/validation.js +5 -0
- package/dist/types/websocket.d.ts +172 -0
- package/dist/types/websocket.js +5 -0
- package/dist/utils/http.d.ts +10 -0
- package/dist/utils/http.js +24 -0
- package/dist/utils/logger.d.ts +11 -2
- package/dist/utils/logger.js +18 -13
- package/dist/utils/metrics.d.ts +81 -0
- package/dist/utils/metrics.js +206 -0
- package/dist/utils/rate-limiter.d.ts +85 -0
- package/dist/utils/rate-limiter.js +175 -0
- package/package.json +18 -21
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-19: bech32-encoded entities
|
|
3
|
+
* @module nips/nip-19
|
|
4
|
+
*/
|
|
5
|
+
import { getLogger } from '../utils/logger';
|
|
6
|
+
import { encodeToBech32, decodeFromBech32 } from '../crypto/bech32';
|
|
7
|
+
const logger = getLogger('NIP-19');
|
|
8
|
+
/**
|
|
9
|
+
* Encode a public key to bech32 npub format
|
|
10
|
+
*/
|
|
11
|
+
export function encodePubkey(pubkey) {
|
|
12
|
+
try {
|
|
13
|
+
return encodeToBech32('npub', pubkey);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
17
|
+
logger.error(`Failed to encode pubkey: ${errorMessage}`);
|
|
18
|
+
throw new Error(`Failed to encode pubkey: ${errorMessage}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Encode a private key to bech32 nsec format
|
|
23
|
+
*/
|
|
24
|
+
export function encodePrivkey(privkey) {
|
|
25
|
+
try {
|
|
26
|
+
return encodeToBech32('nsec', privkey);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
30
|
+
logger.error(`Failed to encode private key: ${errorMessage}`);
|
|
31
|
+
throw new Error(`Failed to encode private key: ${errorMessage}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Decode a bech32 npub to hex pubkey
|
|
36
|
+
*/
|
|
37
|
+
export function decodePubkey(npub) {
|
|
38
|
+
try {
|
|
39
|
+
const { prefix, hex } = decodeFromBech32(npub);
|
|
40
|
+
if (prefix !== 'npub') {
|
|
41
|
+
throw new Error('Invalid prefix for public key');
|
|
42
|
+
}
|
|
43
|
+
return hex;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
47
|
+
logger.error(`Failed to decode pubkey: ${errorMessage}`);
|
|
48
|
+
throw new Error(`Failed to decode pubkey: ${errorMessage}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Decode a bech32 nsec to hex privkey
|
|
53
|
+
*/
|
|
54
|
+
export function decodePrivkey(nsec) {
|
|
55
|
+
try {
|
|
56
|
+
const { prefix, hex } = decodeFromBech32(nsec);
|
|
57
|
+
if (prefix !== 'nsec') {
|
|
58
|
+
throw new Error('Invalid prefix for private key');
|
|
59
|
+
}
|
|
60
|
+
return hex;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
64
|
+
logger.error(`Failed to decode private key: ${errorMessage}`);
|
|
65
|
+
throw new Error(`Failed to decode private key: ${errorMessage}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Process tags containing bech32-encoded entities
|
|
70
|
+
*/
|
|
71
|
+
export function processBech32Tags(tags) {
|
|
72
|
+
return tags.map(tag => {
|
|
73
|
+
try {
|
|
74
|
+
if (tag[0] === 'p' && tag[1].startsWith('npub')) {
|
|
75
|
+
return [tag[0], decodePubkey(tag[1])];
|
|
76
|
+
}
|
|
77
|
+
return tag;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
81
|
+
logger.debug(`Failed to decode pubkey ${tag[1]}: ${errorMessage}`);
|
|
82
|
+
return tag;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Encode tags containing hex pubkeys to bech32
|
|
88
|
+
*/
|
|
89
|
+
export function encodeBech32Tags(tags) {
|
|
90
|
+
return tags.map(tag => {
|
|
91
|
+
try {
|
|
92
|
+
if (tag[0] === 'p' && !tag[1].startsWith('npub')) {
|
|
93
|
+
return [tag[0], encodePubkey(tag[1])];
|
|
94
|
+
}
|
|
95
|
+
return tag;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
99
|
+
logger.debug(`Failed to encode pubkey ${tag[1]}: ${errorMessage}`);
|
|
100
|
+
return tag;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-20 Command Results implementation
|
|
3
|
+
* @module nips/nip-20
|
|
4
|
+
*/
|
|
5
|
+
import { NostrWSMessage } from '../types';
|
|
6
|
+
/**
|
|
7
|
+
* Command status types
|
|
8
|
+
*/
|
|
9
|
+
export declare enum CommandStatus {
|
|
10
|
+
SUCCESS = "success",
|
|
11
|
+
ERROR = "error",
|
|
12
|
+
PENDING = "pending",
|
|
13
|
+
RATE_LIMITED = "rate_limited",
|
|
14
|
+
AUTH_REQUIRED = "auth_required",
|
|
15
|
+
RESTRICTED = "restricted"
|
|
16
|
+
}
|
|
17
|
+
export type CommandStatusType = CommandStatus;
|
|
18
|
+
/**
|
|
19
|
+
* Command result interface
|
|
20
|
+
*/
|
|
21
|
+
export interface CommandResult {
|
|
22
|
+
status: boolean;
|
|
23
|
+
message?: string;
|
|
24
|
+
code?: CommandStatusType;
|
|
25
|
+
details?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Represents the data structure for command messages
|
|
29
|
+
* @interface CommandMessageData
|
|
30
|
+
* @property {string} [event_id] - ID of the event this command relates to
|
|
31
|
+
* @property {boolean} [status] - Status of the command execution
|
|
32
|
+
* @property {string} [message] - Human-readable message about the command result
|
|
33
|
+
* @property {CommandStatusType} [code] - Status code of the command result
|
|
34
|
+
* @property {Record<string, unknown>} [details] - Additional details about the command result
|
|
35
|
+
*/
|
|
36
|
+
interface CommandMessageData {
|
|
37
|
+
event_id?: string;
|
|
38
|
+
status?: boolean;
|
|
39
|
+
message?: string;
|
|
40
|
+
code?: CommandStatusType;
|
|
41
|
+
details?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validates a command message according to NIP-20
|
|
45
|
+
*/
|
|
46
|
+
export declare function validateCommandMessage(message: NostrWSMessage): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Creates a command result message
|
|
49
|
+
*/
|
|
50
|
+
export declare function createCommandResult(data: CommandMessageData): CommandResult;
|
|
51
|
+
/**
|
|
52
|
+
* Creates an OK message
|
|
53
|
+
*/
|
|
54
|
+
export declare function createOkMessage(eventId: string, success?: boolean, details?: Record<string, unknown>): NostrWSMessage;
|
|
55
|
+
/**
|
|
56
|
+
* Creates a NOTICE message
|
|
57
|
+
*/
|
|
58
|
+
export declare function createCommandNoticeMessage(code: CommandStatusType, message: string, details?: Record<string, unknown>): NostrWSMessage;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-20 Command Results implementation
|
|
3
|
+
* @module nips/nip-20
|
|
4
|
+
*/
|
|
5
|
+
import { getLogger } from '../utils/logger';
|
|
6
|
+
const logger = getLogger('NIP-20');
|
|
7
|
+
/**
|
|
8
|
+
* Command status types
|
|
9
|
+
*/
|
|
10
|
+
export var CommandStatus;
|
|
11
|
+
(function (CommandStatus) {
|
|
12
|
+
CommandStatus["SUCCESS"] = "success";
|
|
13
|
+
CommandStatus["ERROR"] = "error";
|
|
14
|
+
CommandStatus["PENDING"] = "pending";
|
|
15
|
+
CommandStatus["RATE_LIMITED"] = "rate_limited";
|
|
16
|
+
CommandStatus["AUTH_REQUIRED"] = "auth_required";
|
|
17
|
+
CommandStatus["RESTRICTED"] = "restricted";
|
|
18
|
+
})(CommandStatus || (CommandStatus = {}));
|
|
19
|
+
/**
|
|
20
|
+
* Validates a command message according to NIP-20
|
|
21
|
+
*/
|
|
22
|
+
export function validateCommandMessage(message) {
|
|
23
|
+
if (!message.data || typeof message.data !== 'object') {
|
|
24
|
+
logger.debug('Invalid command message data');
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const data = message.data;
|
|
28
|
+
// For OK/NOTICE messages
|
|
29
|
+
if (message.type === 'OK') {
|
|
30
|
+
if (!data.event_id || typeof data.event_id !== 'string') {
|
|
31
|
+
logger.debug('Invalid event_id in OK message');
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (typeof data.status !== 'boolean') {
|
|
35
|
+
logger.debug('Invalid status in OK message');
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// For NOTICE messages
|
|
40
|
+
if (message.type === 'NOTICE' && data.code) {
|
|
41
|
+
if (!Object.values(CommandStatus).includes(data.code)) {
|
|
42
|
+
logger.debug('Invalid command status code');
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Optional fields validation
|
|
47
|
+
if (data.message && typeof data.message !== 'string') {
|
|
48
|
+
logger.debug('Invalid message field');
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (data.details && typeof data.details !== 'object') {
|
|
52
|
+
logger.debug('Invalid details field');
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Creates a command result message
|
|
59
|
+
*/
|
|
60
|
+
export function createCommandResult(data) {
|
|
61
|
+
const status = data.status ?? false;
|
|
62
|
+
const code = status ? CommandStatus.SUCCESS : data.code;
|
|
63
|
+
return {
|
|
64
|
+
status,
|
|
65
|
+
code,
|
|
66
|
+
message: data.message,
|
|
67
|
+
details: data.details
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Creates an OK message
|
|
72
|
+
*/
|
|
73
|
+
export function createOkMessage(eventId, success = true, details) {
|
|
74
|
+
return {
|
|
75
|
+
type: 'OK',
|
|
76
|
+
data: {
|
|
77
|
+
event_id: eventId,
|
|
78
|
+
status: success,
|
|
79
|
+
...details && { details }
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Creates a NOTICE message
|
|
85
|
+
*/
|
|
86
|
+
export function createCommandNoticeMessage(code, message, details) {
|
|
87
|
+
return {
|
|
88
|
+
type: 'NOTICE',
|
|
89
|
+
data: {
|
|
90
|
+
code,
|
|
91
|
+
message,
|
|
92
|
+
...details && { details }
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-22: Event Created At Limits
|
|
3
|
+
* @module nips/nip-22
|
|
4
|
+
* @see https://github.com/nostr-protocol/nips/blob/master/22.md
|
|
5
|
+
*/
|
|
6
|
+
import type { NostrWSMessage } from '../types/messages';
|
|
7
|
+
import type { Logger } from '../types/logger';
|
|
8
|
+
/**
|
|
9
|
+
* Default time limits in seconds
|
|
10
|
+
*/
|
|
11
|
+
export declare const DEFAULT_TIME_LIMITS: {
|
|
12
|
+
FUTURE_LIMIT: number;
|
|
13
|
+
PAST_LIMIT: number;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Time validation result
|
|
17
|
+
*/
|
|
18
|
+
export interface TimeValidationResult {
|
|
19
|
+
valid: boolean;
|
|
20
|
+
reason?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Time validator interface
|
|
24
|
+
*/
|
|
25
|
+
export interface TimeValidator {
|
|
26
|
+
/**
|
|
27
|
+
* Validates event timestamp
|
|
28
|
+
* @param timestamp - Event timestamp
|
|
29
|
+
* @returns {TimeValidationResult} Validation result
|
|
30
|
+
*/
|
|
31
|
+
validateTime(timestamp: number): TimeValidationResult;
|
|
32
|
+
/**
|
|
33
|
+
* Updates server time offset
|
|
34
|
+
* @param serverTime - Server timestamp
|
|
35
|
+
*/
|
|
36
|
+
updateTimeOffset(serverTime: number): void;
|
|
37
|
+
/**
|
|
38
|
+
* Gets current adjusted timestamp
|
|
39
|
+
* @returns {number} Adjusted timestamp
|
|
40
|
+
*/
|
|
41
|
+
getCurrentTime(): number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Creates a time validator
|
|
45
|
+
* @param logger - Logger instance
|
|
46
|
+
* @param futureLimitSeconds - Future time limit in seconds
|
|
47
|
+
* @param pastLimitSeconds - Past time limit in seconds
|
|
48
|
+
* @returns {TimeValidator} Time validator
|
|
49
|
+
*/
|
|
50
|
+
export declare function createTimeValidator(logger: Logger, futureLimitSeconds?: number, pastLimitSeconds?: number): TimeValidator;
|
|
51
|
+
/**
|
|
52
|
+
* Validates event timestamp
|
|
53
|
+
* @param message - Message to validate
|
|
54
|
+
* @param validator - Time validator
|
|
55
|
+
* @param logger - Logger instance
|
|
56
|
+
* @returns {TimeValidationResult} Validation result
|
|
57
|
+
*/
|
|
58
|
+
export declare function validateEventTime(message: NostrWSMessage, validator: TimeValidator, logger: Logger): TimeValidationResult;
|
|
59
|
+
/**
|
|
60
|
+
* Time synchronization manager interface
|
|
61
|
+
*/
|
|
62
|
+
export interface TimeSyncManager {
|
|
63
|
+
/**
|
|
64
|
+
* Starts time synchronization
|
|
65
|
+
* @param wsUrl - WebSocket URL for time sync
|
|
66
|
+
*/
|
|
67
|
+
startSync(wsUrl: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* Stops time synchronization
|
|
70
|
+
*/
|
|
71
|
+
stopSync(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Gets current synchronized time
|
|
74
|
+
* @returns {number} Current timestamp
|
|
75
|
+
*/
|
|
76
|
+
getCurrentTime(): number;
|
|
77
|
+
/**
|
|
78
|
+
* Validates event timing
|
|
79
|
+
* @param event - Event to validate
|
|
80
|
+
* @returns {TimeValidationResult} Validation result
|
|
81
|
+
*/
|
|
82
|
+
validateEvent(event: NostrWSMessage): TimeValidationResult;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Creates a time synchronization manager
|
|
86
|
+
* @param logger - Logger instance
|
|
87
|
+
* @returns {TimeSyncManager} Time sync manager
|
|
88
|
+
*/
|
|
89
|
+
export declare function createTimeSyncManager(logger: Logger): TimeSyncManager;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-22: Event Created At Limits
|
|
3
|
+
* @module nips/nip-22
|
|
4
|
+
* @see https://github.com/nostr-protocol/nips/blob/master/22.md
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Default time limits in seconds
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_TIME_LIMITS = {
|
|
10
|
+
FUTURE_LIMIT: 900, // 15 minutes
|
|
11
|
+
PAST_LIMIT: 31536000 // 1 year
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Creates a time validator
|
|
15
|
+
* @param logger - Logger instance
|
|
16
|
+
* @param futureLimitSeconds - Future time limit in seconds
|
|
17
|
+
* @param pastLimitSeconds - Past time limit in seconds
|
|
18
|
+
* @returns {TimeValidator} Time validator
|
|
19
|
+
*/
|
|
20
|
+
export function createTimeValidator(logger, futureLimitSeconds = DEFAULT_TIME_LIMITS.FUTURE_LIMIT, pastLimitSeconds = DEFAULT_TIME_LIMITS.PAST_LIMIT) {
|
|
21
|
+
let timeOffset = 0; // Offset between local and server time
|
|
22
|
+
return {
|
|
23
|
+
validateTime(timestamp) {
|
|
24
|
+
const now = Date.now() / 1000 + timeOffset;
|
|
25
|
+
// Check future limit
|
|
26
|
+
if (timestamp > now + futureLimitSeconds) {
|
|
27
|
+
return {
|
|
28
|
+
valid: false,
|
|
29
|
+
reason: 'Event timestamp too far in the future'
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// Check past limit
|
|
33
|
+
if (timestamp < now - pastLimitSeconds) {
|
|
34
|
+
return {
|
|
35
|
+
valid: false,
|
|
36
|
+
reason: 'Event timestamp too far in the past'
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return { valid: true };
|
|
40
|
+
},
|
|
41
|
+
updateTimeOffset(serverTime) {
|
|
42
|
+
const localTime = Date.now() / 1000;
|
|
43
|
+
timeOffset = serverTime - localTime;
|
|
44
|
+
logger.debug(`Updated time offset to ${timeOffset} seconds`);
|
|
45
|
+
},
|
|
46
|
+
getCurrentTime() {
|
|
47
|
+
return Math.floor(Date.now() / 1000 + timeOffset);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validates event timestamp
|
|
53
|
+
* @param message - Message to validate
|
|
54
|
+
* @param validator - Time validator
|
|
55
|
+
* @param logger - Logger instance
|
|
56
|
+
* @returns {TimeValidationResult} Validation result
|
|
57
|
+
*/
|
|
58
|
+
export function validateEventTime(message, validator, logger) {
|
|
59
|
+
try {
|
|
60
|
+
if (message.type !== 'EVENT' || !message.data) {
|
|
61
|
+
return { valid: true }; // Not an event message
|
|
62
|
+
}
|
|
63
|
+
const event = message.data;
|
|
64
|
+
const timestamp = event.created_at;
|
|
65
|
+
if (typeof timestamp !== 'number') {
|
|
66
|
+
return {
|
|
67
|
+
valid: false,
|
|
68
|
+
reason: 'Missing or invalid timestamp'
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return validator.validateTime(timestamp);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
logger.error('Error validating event time:', error);
|
|
75
|
+
return {
|
|
76
|
+
valid: false,
|
|
77
|
+
reason: 'Error validating timestamp'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Creates a time synchronization manager
|
|
83
|
+
* @param logger - Logger instance
|
|
84
|
+
* @returns {TimeSyncManager} Time sync manager
|
|
85
|
+
*/
|
|
86
|
+
export function createTimeSyncManager(logger) {
|
|
87
|
+
const validator = createTimeValidator(logger);
|
|
88
|
+
let syncInterval = null;
|
|
89
|
+
async function syncTime(wsUrl) {
|
|
90
|
+
try {
|
|
91
|
+
const ws = new WebSocket(wsUrl);
|
|
92
|
+
await new Promise((resolve, reject) => {
|
|
93
|
+
ws.onopen = () => {
|
|
94
|
+
// Send time request
|
|
95
|
+
ws.send(JSON.stringify({
|
|
96
|
+
type: 'TIME',
|
|
97
|
+
data: { client_time: Math.floor(Date.now() / 1000) }
|
|
98
|
+
}));
|
|
99
|
+
};
|
|
100
|
+
ws.onmessage = (event) => {
|
|
101
|
+
try {
|
|
102
|
+
const response = JSON.parse(event.data);
|
|
103
|
+
if (response.type === 'TIME') {
|
|
104
|
+
validator.updateTimeOffset(response.data.server_time);
|
|
105
|
+
resolve();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
reject(error);
|
|
110
|
+
}
|
|
111
|
+
ws.close();
|
|
112
|
+
};
|
|
113
|
+
ws.onerror = reject;
|
|
114
|
+
// Timeout after 5 seconds
|
|
115
|
+
setTimeout(() => reject(new Error('Time sync timeout')), 5000);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
logger.error('Time sync failed:', error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
startSync(wsUrl) {
|
|
124
|
+
// Initial sync
|
|
125
|
+
syncTime(wsUrl);
|
|
126
|
+
// Periodic sync every 15 minutes
|
|
127
|
+
syncInterval = setInterval(() => syncTime(wsUrl), 900000);
|
|
128
|
+
},
|
|
129
|
+
stopSync() {
|
|
130
|
+
if (syncInterval) {
|
|
131
|
+
clearInterval(syncInterval);
|
|
132
|
+
syncInterval = null;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
getCurrentTime() {
|
|
136
|
+
return validator.getCurrentTime();
|
|
137
|
+
},
|
|
138
|
+
validateEvent(event) {
|
|
139
|
+
return validateEventTime(event, validator, logger);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-26: Delegated Event Signing
|
|
3
|
+
* @module nips/nip-26
|
|
4
|
+
*/
|
|
5
|
+
import type { NostrEvent } from '../types/events';
|
|
6
|
+
/**
|
|
7
|
+
* Represents the conditions for a Nostr event delegation
|
|
8
|
+
* @interface DelegationConditions
|
|
9
|
+
* @property {number} [kind] - The kind of events this delegation is valid for
|
|
10
|
+
* @property {number} [since] - Unix timestamp from which this delegation is valid
|
|
11
|
+
* @property {number} [until] - Unix timestamp until which this delegation is valid
|
|
12
|
+
* @property {unknown} [key: string] - Any additional conditions
|
|
13
|
+
*/
|
|
14
|
+
interface DelegationConditions {
|
|
15
|
+
kind?: number;
|
|
16
|
+
since?: number;
|
|
17
|
+
until?: number;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Represents a Nostr event delegation
|
|
22
|
+
* @interface Delegation
|
|
23
|
+
* @property {string} pubkey - The public key of the delegator
|
|
24
|
+
* @property {DelegationConditions} conditions - The conditions under which this delegation is valid
|
|
25
|
+
* @property {string} token - The delegation token signed by the delegator
|
|
26
|
+
*/
|
|
27
|
+
interface Delegation {
|
|
28
|
+
pubkey: string;
|
|
29
|
+
conditions: DelegationConditions;
|
|
30
|
+
token: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a delegation token
|
|
34
|
+
*/
|
|
35
|
+
export declare function createDelegation(delegatorPrivkey: string, delegateePubkey: string, conditions: DelegationConditions): Promise<string>;
|
|
36
|
+
/**
|
|
37
|
+
* Verify a delegation token
|
|
38
|
+
*/
|
|
39
|
+
export declare function verifyDelegation(delegatorPubkey: string, delegateePubkey: string, token: string, conditions: DelegationConditions): Promise<boolean>;
|
|
40
|
+
/**
|
|
41
|
+
* Add delegation tag to an event
|
|
42
|
+
*/
|
|
43
|
+
export declare function addDelegationTag(event: NostrEvent, delegation: Delegation): NostrEvent;
|
|
44
|
+
/**
|
|
45
|
+
* Extract delegation from an event
|
|
46
|
+
*/
|
|
47
|
+
export declare function extractDelegation(event: NostrEvent): Delegation | null;
|
|
48
|
+
/**
|
|
49
|
+
* Validate a delegated event
|
|
50
|
+
*/
|
|
51
|
+
export declare function validateDelegatedEvent(event: NostrEvent): Promise<boolean>;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-26: Delegated Event Signing
|
|
3
|
+
* @module nips/nip-26
|
|
4
|
+
*/
|
|
5
|
+
import { getLogger } from '../utils/logger';
|
|
6
|
+
import { signEvent, verifySignature } from 'nostr-crypto-utils';
|
|
7
|
+
const logger = getLogger('NIP-26');
|
|
8
|
+
/**
|
|
9
|
+
* Create a delegation token
|
|
10
|
+
*/
|
|
11
|
+
export async function createDelegation(delegatorPrivkey, delegateePubkey, conditions) {
|
|
12
|
+
try {
|
|
13
|
+
const conditionsString = Object.entries(conditions)
|
|
14
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
15
|
+
.sort()
|
|
16
|
+
.join('&');
|
|
17
|
+
const message = `nostr:delegation:${delegateePubkey}:${conditionsString}`;
|
|
18
|
+
// Create a NostrEvent object for signing
|
|
19
|
+
const event = {
|
|
20
|
+
id: '', // This will be set by signEvent
|
|
21
|
+
pubkey: delegateePubkey,
|
|
22
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
23
|
+
kind: 0, // Using kind 0 for delegation events
|
|
24
|
+
tags: [],
|
|
25
|
+
content: message,
|
|
26
|
+
sig: '' // This will be set by signEvent
|
|
27
|
+
};
|
|
28
|
+
const signedEvent = await signEvent(event, delegatorPrivkey);
|
|
29
|
+
return signedEvent.sig;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
33
|
+
logger.error(`Failed to create delegation: ${errorMessage}`);
|
|
34
|
+
throw new Error(`Failed to create delegation: ${errorMessage}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Verify a delegation token
|
|
39
|
+
*/
|
|
40
|
+
export async function verifyDelegation(delegatorPubkey, delegateePubkey, token, conditions) {
|
|
41
|
+
try {
|
|
42
|
+
const conditionsString = Object.entries(conditions)
|
|
43
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
44
|
+
.sort()
|
|
45
|
+
.join('&');
|
|
46
|
+
const message = `nostr:delegation:${delegateePubkey}:${conditionsString}`;
|
|
47
|
+
const verificationEvent = {
|
|
48
|
+
id: '',
|
|
49
|
+
pubkey: delegatorPubkey,
|
|
50
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
51
|
+
kind: 0,
|
|
52
|
+
tags: [],
|
|
53
|
+
content: message,
|
|
54
|
+
sig: token
|
|
55
|
+
};
|
|
56
|
+
return await verifySignature(verificationEvent);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
60
|
+
logger.error(`Failed to verify delegation: ${errorMessage}`);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Add delegation tag to an event
|
|
66
|
+
*/
|
|
67
|
+
export function addDelegationTag(event, delegation) {
|
|
68
|
+
const conditionsString = Object.entries(delegation.conditions)
|
|
69
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
70
|
+
.sort()
|
|
71
|
+
.join('&');
|
|
72
|
+
const delegationTag = ['delegation', delegation.pubkey, conditionsString, delegation.token];
|
|
73
|
+
return {
|
|
74
|
+
...event,
|
|
75
|
+
tags: [...event.tags, delegationTag]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Extract delegation from an event
|
|
80
|
+
*/
|
|
81
|
+
export function extractDelegation(event) {
|
|
82
|
+
try {
|
|
83
|
+
const delegationTag = event.tags.find(tag => tag[0] === 'delegation');
|
|
84
|
+
if (!delegationTag || delegationTag.length !== 4) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const [, pubkey, conditionsString, token] = delegationTag;
|
|
88
|
+
const conditions = {};
|
|
89
|
+
conditionsString.split('&').forEach(pair => {
|
|
90
|
+
const [key, value] = pair.split('=');
|
|
91
|
+
if (key === 'kind' || key === 'since' || key === 'until') {
|
|
92
|
+
conditions[key] = parseInt(value, 10);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
conditions[key] = value;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return { pubkey, conditions, token };
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
102
|
+
logger.error(`Failed to extract delegation: ${errorMessage}`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Validate a delegated event
|
|
108
|
+
*/
|
|
109
|
+
export async function validateDelegatedEvent(event) {
|
|
110
|
+
try {
|
|
111
|
+
const delegation = extractDelegation(event);
|
|
112
|
+
if (!delegation) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
const { kind, created_at } = event;
|
|
116
|
+
const { kind: allowedKind, since, until } = delegation.conditions;
|
|
117
|
+
// Check kind constraint
|
|
118
|
+
if (allowedKind !== undefined && kind !== allowedKind) {
|
|
119
|
+
logger.debug('Event kind does not match delegation conditions');
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
// Check time constraints
|
|
123
|
+
if (since !== undefined && created_at < since) {
|
|
124
|
+
logger.debug('Event is before delegation start time');
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (until !== undefined && created_at > until) {
|
|
128
|
+
logger.debug('Event is after delegation end time');
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
// Verify delegation token
|
|
132
|
+
return await verifyDelegation(delegation.pubkey, event.pubkey, delegation.token, delegation.conditions);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
136
|
+
logger.error(`Failed to validate delegated event: ${errorMessage}`);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|