ncc-02-js 0.3.1 → 0.4.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/README.md +89 -21
- package/dist/index.cjs +217 -85
- package/dist/index.mjs +216 -85
- package/dist/models.d.ts +29 -7
- package/dist/resolver.d.ts +74 -14
- package/package.json +1 -1
- package/src/models.js +105 -21
- package/src/resolver.js +153 -88
package/src/models.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { finalizeEvent, verifyEvent, getPublicKey } from 'nostr-tools/pure';
|
|
2
2
|
import { hexToBytes } from 'nostr-tools/utils';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} NostrSigner
|
|
6
|
+
* @property {() => Promise<string>} getPublicKey
|
|
7
|
+
* @property {(event: any) => Promise<any>} signEvent
|
|
8
|
+
* @property {(event: any) => Promise<any>} [decryptEvent]
|
|
9
|
+
*/
|
|
10
|
+
|
|
4
11
|
/**
|
|
5
12
|
* NCC-02 Nostr Event Kinds
|
|
6
13
|
*/
|
|
@@ -15,12 +22,78 @@ export const KINDS = {
|
|
|
15
22
|
*/
|
|
16
23
|
export class NCC02Builder {
|
|
17
24
|
/**
|
|
18
|
-
* @param {string | Uint8Array}
|
|
25
|
+
* @param {string | Uint8Array | NostrSigner} signer - Raw private key or asynchronous signer.
|
|
26
|
+
*/
|
|
27
|
+
constructor(signer) {
|
|
28
|
+
if (!signer) throw new Error('Signer or private key is required');
|
|
29
|
+
this.signer = this._normalizeSigner(signer);
|
|
30
|
+
this._pubkeyPromise = this.signer.getPublicKey();
|
|
31
|
+
this._pubkey = undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async _getPublicKey() {
|
|
35
|
+
if (!this._pubkey) {
|
|
36
|
+
this._pubkey = await this._pubkeyPromise;
|
|
37
|
+
}
|
|
38
|
+
return this._pubkey;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {any} event
|
|
43
|
+
*/
|
|
44
|
+
async _finalizeEvent(event) {
|
|
45
|
+
const pubkey = await this._getPublicKey();
|
|
46
|
+
const eventWithPubkey = { ...event, pubkey };
|
|
47
|
+
const signed = await this.signer.signEvent(eventWithPubkey);
|
|
48
|
+
if (!signed || typeof signed.id !== 'string' || typeof signed.sig !== 'string') {
|
|
49
|
+
throw new Error('Signer must return a signed event with id and sig');
|
|
50
|
+
}
|
|
51
|
+
return signed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {any} signer
|
|
56
|
+
* @returns {NostrSigner}
|
|
19
57
|
*/
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
58
|
+
_normalizeSigner(signer) {
|
|
59
|
+
if (typeof signer === 'string' || signer instanceof Uint8Array) {
|
|
60
|
+
const privateKey = typeof signer === 'string' ? hexToBytes(signer) : signer;
|
|
61
|
+
const pubkey = getPublicKey(privateKey);
|
|
62
|
+
return {
|
|
63
|
+
getPublicKey: async () => pubkey,
|
|
64
|
+
/** @param {any} event */
|
|
65
|
+
signEvent: async (event) => {
|
|
66
|
+
const clonedEvent = {
|
|
67
|
+
...event,
|
|
68
|
+
tags: Array.isArray(event.tags) ? event.tags.map((/** @type {any[]} */ tag) => [...tag]) : []
|
|
69
|
+
};
|
|
70
|
+
return finalizeEvent(clonedEvent, privateKey);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof signer === 'object' && signer !== null) {
|
|
76
|
+
if (typeof signer.getPublicKey === 'function' && typeof signer.signEvent === 'function') {
|
|
77
|
+
return {
|
|
78
|
+
getPublicKey: async () => {
|
|
79
|
+
const pubkey = await signer.getPublicKey();
|
|
80
|
+
if (typeof pubkey !== 'string') throw new Error('Signer.getPublicKey must return a hex string');
|
|
81
|
+
return pubkey;
|
|
82
|
+
},
|
|
83
|
+
/** @param {any} event */
|
|
84
|
+
signEvent: async (event) => {
|
|
85
|
+
const signed = await signer.signEvent(event);
|
|
86
|
+
if (!signed || typeof signed.id !== 'string' || typeof signed.sig !== 'string') {
|
|
87
|
+
throw new Error('Signer.signEvent must return a signed event');
|
|
88
|
+
}
|
|
89
|
+
return signed;
|
|
90
|
+
},
|
|
91
|
+
decryptEvent: typeof signer.decryptEvent === 'function' ? signer.decryptEvent.bind(signer) : undefined
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
throw new Error('Unsupported signer provided to NCC02Builder');
|
|
24
97
|
}
|
|
25
98
|
|
|
26
99
|
/**
|
|
@@ -31,7 +104,7 @@ export class NCC02Builder {
|
|
|
31
104
|
* @param {string} [options.fingerprint] - The 'k' tag fingerprint.
|
|
32
105
|
* @param {number} [options.expiryDays=14] - Expiry in days.
|
|
33
106
|
*/
|
|
34
|
-
createServiceRecord(options) {
|
|
107
|
+
async createServiceRecord(options) {
|
|
35
108
|
const { serviceId, endpoint, fingerprint, expiryDays = 14 } = options;
|
|
36
109
|
if (!serviceId) throw new Error('serviceId (d tag) is required');
|
|
37
110
|
|
|
@@ -40,17 +113,16 @@ export class NCC02Builder {
|
|
|
40
113
|
['d', serviceId],
|
|
41
114
|
['exp', expiry.toString()]
|
|
42
115
|
];
|
|
43
|
-
if(endpoint) tags.push(['u', endpoint]);
|
|
44
|
-
if(fingerprint) tags.push(['k', fingerprint]);
|
|
116
|
+
if (endpoint) tags.push(['u', endpoint]);
|
|
117
|
+
if (fingerprint) tags.push(['k', fingerprint]);
|
|
45
118
|
|
|
46
119
|
const event = {
|
|
47
120
|
kind: KINDS.SERVICE_RECORD,
|
|
48
121
|
created_at: Math.floor(Date.now() / 1000),
|
|
49
|
-
tags
|
|
50
|
-
content: `NCC-02 Service Record for ${serviceId}
|
|
51
|
-
pubkey: this.pk
|
|
122
|
+
tags,
|
|
123
|
+
content: `NCC-02 Service Record for ${serviceId}`
|
|
52
124
|
};
|
|
53
|
-
return
|
|
125
|
+
return this._finalizeEvent(event);
|
|
54
126
|
}
|
|
55
127
|
|
|
56
128
|
/**
|
|
@@ -62,7 +134,7 @@ export class NCC02Builder {
|
|
|
62
134
|
* @param {string} [options.level='verified'] - The 'lvl' tag level.
|
|
63
135
|
* @param {number} [options.validDays=30] - Validity in days.
|
|
64
136
|
*/
|
|
65
|
-
createAttestation(options) {
|
|
137
|
+
async createAttestation(options) {
|
|
66
138
|
const { subjectPubkey, serviceId, serviceEventId, level = 'verified', validDays = 30 } = options;
|
|
67
139
|
if (!subjectPubkey) throw new Error('subjectPubkey is required');
|
|
68
140
|
if (!serviceId) throw new Error('serviceId is required');
|
|
@@ -82,10 +154,9 @@ export class NCC02Builder {
|
|
|
82
154
|
['nbf', now.toString()],
|
|
83
155
|
['exp', expiry.toString()]
|
|
84
156
|
],
|
|
85
|
-
content: 'NCC-02 Attestation'
|
|
86
|
-
pubkey: this.pk
|
|
157
|
+
content: 'NCC-02 Attestation'
|
|
87
158
|
};
|
|
88
|
-
return
|
|
159
|
+
return this._finalizeEvent(event);
|
|
89
160
|
}
|
|
90
161
|
|
|
91
162
|
/**
|
|
@@ -94,7 +165,7 @@ export class NCC02Builder {
|
|
|
94
165
|
* @param {string} options.attestationId - The 'e' tag referencing the attestation.
|
|
95
166
|
* @param {string} [options.reason=''] - Optional reason.
|
|
96
167
|
*/
|
|
97
|
-
createRevocation(options) {
|
|
168
|
+
async createRevocation(options) {
|
|
98
169
|
const { attestationId, reason = '' } = options;
|
|
99
170
|
if (!attestationId) throw new Error('attestationId (e tag) is required');
|
|
100
171
|
|
|
@@ -104,11 +175,10 @@ export class NCC02Builder {
|
|
|
104
175
|
const event = {
|
|
105
176
|
kind: KINDS.REVOCATION,
|
|
106
177
|
created_at: Math.floor(Date.now() / 1000),
|
|
107
|
-
tags
|
|
108
|
-
content: 'NCC-02 Revocation'
|
|
109
|
-
pubkey: this.pk
|
|
178
|
+
tags,
|
|
179
|
+
content: 'NCC-02 Revocation'
|
|
110
180
|
};
|
|
111
|
-
return
|
|
181
|
+
return this._finalizeEvent(event);
|
|
112
182
|
}
|
|
113
183
|
}
|
|
114
184
|
|
|
@@ -119,3 +189,17 @@ export class NCC02Builder {
|
|
|
119
189
|
export function verifyNCC02Event(event) {
|
|
120
190
|
return verifyEvent(event);
|
|
121
191
|
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Checks whether an NCC event has expired based on its 'exp' tag.
|
|
195
|
+
* @param {any} event
|
|
196
|
+
* @returns {boolean}
|
|
197
|
+
*/
|
|
198
|
+
export function isExpired(event) {
|
|
199
|
+
if (!event || !Array.isArray(event.tags)) return false;
|
|
200
|
+
const expTag = event.tags.find((/** @type {any[]} */ tag) => tag[0] === 'exp');
|
|
201
|
+
if (!expTag) return false;
|
|
202
|
+
const expiry = parseInt(expTag[1], 10);
|
|
203
|
+
if (Number.isNaN(expiry)) return false;
|
|
204
|
+
return expiry <= Math.floor(Date.now() / 1000);
|
|
205
|
+
}
|
package/src/resolver.js
CHANGED
|
@@ -18,15 +18,17 @@ export class NCC02Error extends Error {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* @typedef {Object}
|
|
21
|
+
* @typedef {Object} ServiceStatus
|
|
22
22
|
* @property {string|undefined} endpoint
|
|
23
23
|
* @property {string|undefined} fingerprint
|
|
24
24
|
* @property {number} expiry
|
|
25
|
-
* @property {
|
|
25
|
+
* @property {boolean} isRevoked
|
|
26
|
+
* @property {number} attestationCount
|
|
27
|
+
* @property {Array<{eventId:string, level:string, pubkey:string}>} attestations
|
|
26
28
|
* @property {string} eventId
|
|
27
29
|
* @property {string} pubkey
|
|
30
|
+
* @property {any} serviceEvent
|
|
28
31
|
*/
|
|
29
|
-
|
|
30
32
|
/**
|
|
31
33
|
* Resolver for NCC-02 Service Records.
|
|
32
34
|
* Implements the client-side resolution and trust verification algorithm.
|
|
@@ -78,6 +80,29 @@ export class NCC02Resolver {
|
|
|
78
80
|
});
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Returns the first event sorted by freshness (newest created_at, tie broken by id).
|
|
85
|
+
* @param {import('nostr-tools').Event[]} events
|
|
86
|
+
* @returns {import('nostr-tools').Event|null}
|
|
87
|
+
*/
|
|
88
|
+
_freshestEvent(events) {
|
|
89
|
+
if (!events || !events.length) return null;
|
|
90
|
+
return events.sort((a, b) => {
|
|
91
|
+
if (b.created_at !== a.created_at) return b.created_at - a.created_at;
|
|
92
|
+
return a.id.localeCompare(b.id);
|
|
93
|
+
})[0];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Query helper that returns only the freshest event matching the filter.
|
|
98
|
+
* @param {import('nostr-tools').Filter} filter
|
|
99
|
+
* @returns {Promise<import('nostr-tools').Event | null>}
|
|
100
|
+
*/
|
|
101
|
+
async _queryFreshest(filter) {
|
|
102
|
+
const events = await this._query(filter);
|
|
103
|
+
return this._freshestEvent(events);
|
|
104
|
+
}
|
|
105
|
+
|
|
81
106
|
/**
|
|
82
107
|
* Resolves a service for a given pubkey and service identifier.
|
|
83
108
|
*
|
|
@@ -87,8 +112,8 @@ export class NCC02Resolver {
|
|
|
87
112
|
* @param {boolean} [options.requireAttestation=false] - If true, fails if no trusted attestation is found.
|
|
88
113
|
* @param {string} [options.minLevel=null] - Minimum trust level ('self', 'verified', 'hardened').
|
|
89
114
|
* @param {string} [options.standard='nostr-service-trust-v0.1'] - Expected trust standard.
|
|
90
|
-
|
|
91
|
-
|
|
115
|
+
* @throws {NCC02Error} If verification or policy checks fail.
|
|
116
|
+
* @returns {Promise<ServiceStatus>} The service status including trust metadata.
|
|
92
117
|
*/
|
|
93
118
|
async resolve(pubkey, serviceId, options = {}) {
|
|
94
119
|
const {
|
|
@@ -97,9 +122,9 @@ export class NCC02Resolver {
|
|
|
97
122
|
standard = 'nostr-service-trust-v0.1'
|
|
98
123
|
} = options;
|
|
99
124
|
|
|
100
|
-
let
|
|
125
|
+
let serviceEvent;
|
|
101
126
|
try {
|
|
102
|
-
|
|
127
|
+
serviceEvent = await this._queryFreshest({
|
|
103
128
|
kinds: [KINDS.SERVICE_RECORD],
|
|
104
129
|
authors: [pubkey],
|
|
105
130
|
'#d': [serviceId]
|
|
@@ -108,16 +133,10 @@ export class NCC02Resolver {
|
|
|
108
133
|
throw new NCC02Error('RELAY_ERROR', `Failed to query relay for ${serviceId}`, err);
|
|
109
134
|
}
|
|
110
135
|
|
|
111
|
-
if (!
|
|
136
|
+
if (!serviceEvent) {
|
|
112
137
|
throw new NCC02Error('NOT_FOUND', `No service record found for ${serviceId}`);
|
|
113
138
|
}
|
|
114
139
|
|
|
115
|
-
// Stable tie-breaking: Sort by created_at DESC, then ID ASC
|
|
116
|
-
const serviceEvent = serviceEvents.sort((/** @type {any} */ a, /** @type {any} */ b) => {
|
|
117
|
-
if (b.created_at !== a.created_at) return b.created_at - a.created_at;
|
|
118
|
-
return a.id.localeCompare(b.id);
|
|
119
|
-
})[0];
|
|
120
|
-
|
|
121
140
|
if (!verifyEvent(serviceEvent)) {
|
|
122
141
|
throw new NCC02Error('INVALID_SIGNATURE', 'Service record signature verification failed');
|
|
123
142
|
}
|
|
@@ -143,112 +162,158 @@ export class NCC02Resolver {
|
|
|
143
162
|
throw new NCC02Error('EXPIRED', 'Service record has expired');
|
|
144
163
|
}
|
|
145
164
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
[attestations, revocations] = await Promise.all([
|
|
154
|
-
this._query({ kinds: [KINDS.ATTESTATION], '#e': [serviceEvent.id] }),
|
|
155
|
-
this._query({ kinds: [KINDS.REVOCATION] })
|
|
156
|
-
]);
|
|
157
|
-
} catch (err) {
|
|
158
|
-
throw new NCC02Error('RELAY_ERROR', 'Failed to query relay for attestations/revocations', err);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
for (const att of attestations) {
|
|
162
|
-
if (this.trustedCAPubkeys.has(att.pubkey)) {
|
|
163
|
-
const attTags = Object.fromEntries(att.tags);
|
|
164
|
-
|
|
165
|
-
// Cross-validate subject, service ID, and standard
|
|
166
|
-
if (attTags.subj !== pubkey) continue;
|
|
167
|
-
if (attTags.srv !== serviceId) continue;
|
|
168
|
-
if (standard && attTags.std !== standard) continue;
|
|
169
|
-
|
|
170
|
-
// Trust Level Filtering
|
|
171
|
-
if (minLevel && !this._isLevelSufficient(attTags.lvl, minLevel)) continue;
|
|
172
|
-
|
|
173
|
-
if (this._isAttestationValid(att, attTags, revocations)) {
|
|
174
|
-
validAttestations.push({
|
|
175
|
-
pubkey: att.pubkey,
|
|
176
|
-
level: attTags.lvl,
|
|
177
|
-
eventId: att.id
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
165
|
+
let trustData;
|
|
166
|
+
try {
|
|
167
|
+
trustData = await this._buildTrustData(serviceEvent, { pubkey, serviceId, standard, minLevel });
|
|
168
|
+
} catch (err) {
|
|
169
|
+
throw new NCC02Error('RELAY_ERROR', 'Failed to query relay for attestations/revocations', err);
|
|
170
|
+
}
|
|
182
171
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
172
|
+
if (requireAttestation && trustData.validAttestations.length === 0) {
|
|
173
|
+
throw new NCC02Error('POLICY_FAILURE', `No trusted attestations meet the required policy for ${serviceId}`);
|
|
186
174
|
}
|
|
187
175
|
|
|
188
176
|
return {
|
|
189
177
|
endpoint: serviceTags.u,
|
|
190
178
|
fingerprint: serviceTags.k,
|
|
191
179
|
expiry: exp,
|
|
192
|
-
attestations: validAttestations,
|
|
180
|
+
attestations: trustData.validAttestations,
|
|
181
|
+
attestationCount: trustData.validAttestations.length,
|
|
182
|
+
isRevoked: trustData.isRevoked,
|
|
193
183
|
eventId: serviceEvent.id,
|
|
194
|
-
pubkey: serviceEvent.pubkey
|
|
184
|
+
pubkey: serviceEvent.pubkey,
|
|
185
|
+
serviceEvent
|
|
195
186
|
};
|
|
196
187
|
}
|
|
197
188
|
|
|
198
189
|
/**
|
|
199
|
-
* @param {
|
|
200
|
-
* @param {
|
|
190
|
+
* @param {any} serviceEvent
|
|
191
|
+
* @param {Object} options
|
|
192
|
+
* @param {string} options.pubkey
|
|
193
|
+
* @param {string} options.serviceId
|
|
194
|
+
* @param {string|null} options.standard
|
|
195
|
+
* @param {string|null} options.minLevel
|
|
201
196
|
*/
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
197
|
+
async _buildTrustData(serviceEvent, options) {
|
|
198
|
+
const attestations = await this._query({
|
|
199
|
+
kinds: [KINDS.ATTESTATION],
|
|
200
|
+
'#e': [serviceEvent.id]
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const attestationIds = attestations.map(att => att.id);
|
|
204
|
+
/** @type {any[]} */
|
|
205
|
+
let revocations = [];
|
|
206
|
+
if (attestationIds.length) {
|
|
207
|
+
revocations = await this._query({
|
|
208
|
+
kinds: [KINDS.REVOCATION],
|
|
209
|
+
'#e': attestationIds
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** @type {Record<string, any[]>} */
|
|
214
|
+
const revocationIndex = this._groupValidRevocations(revocations);
|
|
215
|
+
const validAttestations = [];
|
|
216
|
+
let isRevoked = false;
|
|
217
|
+
|
|
218
|
+
for (const att of attestations) {
|
|
219
|
+
if (!this.trustedCAPubkeys.has(att.pubkey)) continue;
|
|
220
|
+
const attTags = Object.fromEntries(att.tags);
|
|
221
|
+
if (attTags.subj !== options.pubkey) continue;
|
|
222
|
+
if (attTags.srv !== options.serviceId) continue;
|
|
223
|
+
if (options.standard && attTags.std !== options.standard) continue;
|
|
224
|
+
|
|
225
|
+
const { valid, revoked } = this._evaluateAttestation(att, attTags, revocationIndex[att.id]);
|
|
226
|
+
if (revoked) {
|
|
227
|
+
isRevoked = true;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (!valid) continue;
|
|
231
|
+
|
|
232
|
+
if (options.minLevel && !this._isLevelSufficient(attTags.lvl, options.minLevel)) continue;
|
|
233
|
+
|
|
234
|
+
validAttestations.push({
|
|
235
|
+
pubkey: att.pubkey,
|
|
236
|
+
level: attTags.lvl,
|
|
237
|
+
eventId: att.id
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { validAttestations, isRevoked };
|
|
208
242
|
}
|
|
209
243
|
|
|
210
244
|
/**
|
|
211
|
-
* @param {any}
|
|
212
|
-
* @
|
|
213
|
-
|
|
245
|
+
* @param {any[]} revocations
|
|
246
|
+
* @returns {Record<string, any[]>}
|
|
247
|
+
*/
|
|
248
|
+
/**
|
|
249
|
+
* @param {any[]} revocations
|
|
250
|
+
* @returns {Record<string, any[]>}
|
|
214
251
|
*/
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
252
|
+
_groupValidRevocations(revocations) {
|
|
253
|
+
/** @type {Record<string, any[]>} */
|
|
254
|
+
const indexed = {};
|
|
255
|
+
for (const rev of revocations) {
|
|
256
|
+
if (!verifyEvent(rev)) continue;
|
|
257
|
+
const tags = Object.fromEntries(rev.tags);
|
|
258
|
+
const targetId = tags.e;
|
|
259
|
+
if (!targetId) continue;
|
|
260
|
+
if (!indexed[targetId]) indexed[targetId] = [];
|
|
261
|
+
indexed[targetId].push(rev);
|
|
262
|
+
}
|
|
263
|
+
return indexed;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @param {any} att
|
|
268
|
+
* @param {Record<string, string>} tags
|
|
269
|
+
* @param {any[]} revocations
|
|
270
|
+
*/
|
|
271
|
+
/**
|
|
272
|
+
* @param {any} att
|
|
273
|
+
* @param {Record<string, string>} tags
|
|
274
|
+
* @param {any[]} [revocations]
|
|
275
|
+
*/
|
|
276
|
+
_evaluateAttestation(att, tags, revocations = []) {
|
|
277
|
+
for (const rev of revocations) {
|
|
278
|
+
if (rev.pubkey === att.pubkey) {
|
|
279
|
+
return { valid: false, revoked: true };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!verifyEvent(att)) return { valid: false, revoked: false };
|
|
218
284
|
|
|
219
285
|
const now = Math.floor(Date.now() / 1000);
|
|
220
|
-
|
|
221
|
-
// 2. NBF validation
|
|
286
|
+
|
|
222
287
|
if (tags.nbf) {
|
|
223
|
-
const nbf = parseInt(tags.nbf);
|
|
224
|
-
if (isNaN(nbf) || nbf > now) return false;
|
|
288
|
+
const nbf = parseInt(tags.nbf, 10);
|
|
289
|
+
if (isNaN(nbf) || nbf > now) return { valid: false, revoked: false };
|
|
225
290
|
}
|
|
226
291
|
|
|
227
|
-
// 3. EXP validation
|
|
228
292
|
if (tags.exp) {
|
|
229
|
-
const exp = parseInt(tags.exp);
|
|
230
|
-
if (isNaN(exp) || exp < now) return false;
|
|
293
|
+
const exp = parseInt(tags.exp, 10);
|
|
294
|
+
if (isNaN(exp) || exp < now) return { valid: false, revoked: false };
|
|
231
295
|
}
|
|
232
296
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
for (const rev of revocations) {
|
|
236
|
-
const revTags = Object.fromEntries(rev.tags);
|
|
237
|
-
if (revTags.e === att.id && rev.pubkey === att.pubkey) {
|
|
238
|
-
if (verifyEvent(rev)) {
|
|
239
|
-
return false; // Valid revocation found
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
297
|
+
return { valid: true, revoked: false };
|
|
298
|
+
}
|
|
243
299
|
|
|
244
|
-
|
|
300
|
+
/**
|
|
301
|
+
* @param {string | undefined} actual
|
|
302
|
+
* @param {string} required
|
|
303
|
+
*/
|
|
304
|
+
_isLevelSufficient(actual, required) {
|
|
305
|
+
/** @type {Record<string, number>} */
|
|
306
|
+
const levels = { 'self': 0, 'verified': 1, 'hardened': 2 };
|
|
307
|
+
const actualVal = actual ? (levels[actual] ?? -1) : -1;
|
|
308
|
+
const requiredVal = levels[required] ?? 0;
|
|
309
|
+
return actualVal >= requiredVal;
|
|
245
310
|
}
|
|
246
311
|
|
|
247
312
|
/**
|
|
248
313
|
* Verifies that the actual fingerprint found during transport-level connection
|
|
249
314
|
* matches the one declared in the signed service record.
|
|
250
315
|
*
|
|
251
|
-
* @param {
|
|
316
|
+
* @param {ServiceStatus} resolved - The object returned by resolve().
|
|
252
317
|
* @param {string} actualFingerprint - The fingerprint obtained from the service.
|
|
253
318
|
* @returns {boolean}
|
|
254
319
|
*/
|