ncc-02-js 0.3.0 → 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 +78 -44
- package/dist/index.cjs +220 -78
- package/dist/index.mjs +219 -78
- package/dist/models.d.ts +29 -7
- package/dist/resolver.d.ts +78 -14
- package/package.json +1 -1
- package/src/models.js +105 -21
- package/src/resolver.js +157 -79
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.
|
|
@@ -51,6 +53,15 @@ export class NCC02Resolver {
|
|
|
51
53
|
this.trustedCAPubkeys = new Set(options.trustedCAPubkeys || []);
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Closes the connection to the relays if the pool is owned by this resolver.
|
|
58
|
+
*/
|
|
59
|
+
close() {
|
|
60
|
+
if (this.ownsPool && this.pool) {
|
|
61
|
+
this.pool.close(this.relays);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
54
65
|
/**
|
|
55
66
|
* Internal query helper using SimplePool.subscribeMany (since list() is deprecated).
|
|
56
67
|
* @param {import('nostr-tools').Filter} filter
|
|
@@ -69,6 +80,29 @@ export class NCC02Resolver {
|
|
|
69
80
|
});
|
|
70
81
|
}
|
|
71
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
|
+
|
|
72
106
|
/**
|
|
73
107
|
* Resolves a service for a given pubkey and service identifier.
|
|
74
108
|
*
|
|
@@ -78,8 +112,8 @@ export class NCC02Resolver {
|
|
|
78
112
|
* @param {boolean} [options.requireAttestation=false] - If true, fails if no trusted attestation is found.
|
|
79
113
|
* @param {string} [options.minLevel=null] - Minimum trust level ('self', 'verified', 'hardened').
|
|
80
114
|
* @param {string} [options.standard='nostr-service-trust-v0.1'] - Expected trust standard.
|
|
81
|
-
|
|
82
|
-
|
|
115
|
+
* @throws {NCC02Error} If verification or policy checks fail.
|
|
116
|
+
* @returns {Promise<ServiceStatus>} The service status including trust metadata.
|
|
83
117
|
*/
|
|
84
118
|
async resolve(pubkey, serviceId, options = {}) {
|
|
85
119
|
const {
|
|
@@ -88,9 +122,9 @@ export class NCC02Resolver {
|
|
|
88
122
|
standard = 'nostr-service-trust-v0.1'
|
|
89
123
|
} = options;
|
|
90
124
|
|
|
91
|
-
let
|
|
125
|
+
let serviceEvent;
|
|
92
126
|
try {
|
|
93
|
-
|
|
127
|
+
serviceEvent = await this._queryFreshest({
|
|
94
128
|
kinds: [KINDS.SERVICE_RECORD],
|
|
95
129
|
authors: [pubkey],
|
|
96
130
|
'#d': [serviceId]
|
|
@@ -99,16 +133,10 @@ export class NCC02Resolver {
|
|
|
99
133
|
throw new NCC02Error('RELAY_ERROR', `Failed to query relay for ${serviceId}`, err);
|
|
100
134
|
}
|
|
101
135
|
|
|
102
|
-
if (!
|
|
136
|
+
if (!serviceEvent) {
|
|
103
137
|
throw new NCC02Error('NOT_FOUND', `No service record found for ${serviceId}`);
|
|
104
138
|
}
|
|
105
139
|
|
|
106
|
-
// Stable tie-breaking: Sort by created_at DESC, then ID ASC
|
|
107
|
-
const serviceEvent = serviceEvents.sort((/** @type {any} */ a, /** @type {any} */ b) => {
|
|
108
|
-
if (b.created_at !== a.created_at) return b.created_at - a.created_at;
|
|
109
|
-
return a.id.localeCompare(b.id);
|
|
110
|
-
})[0];
|
|
111
|
-
|
|
112
140
|
if (!verifyEvent(serviceEvent)) {
|
|
113
141
|
throw new NCC02Error('INVALID_SIGNATURE', 'Service record signature verification failed');
|
|
114
142
|
}
|
|
@@ -134,41 +162,14 @@ export class NCC02Resolver {
|
|
|
134
162
|
throw new NCC02Error('EXPIRED', 'Service record has expired');
|
|
135
163
|
}
|
|
136
164
|
|
|
137
|
-
let
|
|
138
|
-
let revocations;
|
|
165
|
+
let trustData;
|
|
139
166
|
try {
|
|
140
|
-
|
|
141
|
-
this._query({ kinds: [KINDS.ATTESTATION], '#e': [serviceEvent.id] }),
|
|
142
|
-
this._query({ kinds: [KINDS.REVOCATION] })
|
|
143
|
-
]);
|
|
167
|
+
trustData = await this._buildTrustData(serviceEvent, { pubkey, serviceId, standard, minLevel });
|
|
144
168
|
} catch (err) {
|
|
145
169
|
throw new NCC02Error('RELAY_ERROR', 'Failed to query relay for attestations/revocations', err);
|
|
146
170
|
}
|
|
147
171
|
|
|
148
|
-
|
|
149
|
-
for (const att of attestations) {
|
|
150
|
-
if (this.trustedCAPubkeys.has(att.pubkey)) {
|
|
151
|
-
const attTags = Object.fromEntries(att.tags);
|
|
152
|
-
|
|
153
|
-
// Cross-validate subject, service ID, and standard
|
|
154
|
-
if (attTags.subj !== pubkey) continue;
|
|
155
|
-
if (attTags.srv !== serviceId) continue;
|
|
156
|
-
if (standard && attTags.std !== standard) continue;
|
|
157
|
-
|
|
158
|
-
// Trust Level Filtering
|
|
159
|
-
if (minLevel && !this._isLevelSufficient(attTags.lvl, minLevel)) continue;
|
|
160
|
-
|
|
161
|
-
if (this._isAttestationValid(att, attTags, revocations)) {
|
|
162
|
-
validAttestations.push({
|
|
163
|
-
pubkey: att.pubkey,
|
|
164
|
-
level: attTags.lvl,
|
|
165
|
-
eventId: att.id
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (requireAttestation && validAttestations.length === 0) {
|
|
172
|
+
if (requireAttestation && trustData.validAttestations.length === 0) {
|
|
172
173
|
throw new NCC02Error('POLICY_FAILURE', `No trusted attestations meet the required policy for ${serviceId}`);
|
|
173
174
|
}
|
|
174
175
|
|
|
@@ -176,66 +177,143 @@ export class NCC02Resolver {
|
|
|
176
177
|
endpoint: serviceTags.u,
|
|
177
178
|
fingerprint: serviceTags.k,
|
|
178
179
|
expiry: exp,
|
|
179
|
-
attestations: validAttestations,
|
|
180
|
+
attestations: trustData.validAttestations,
|
|
181
|
+
attestationCount: trustData.validAttestations.length,
|
|
182
|
+
isRevoked: trustData.isRevoked,
|
|
180
183
|
eventId: serviceEvent.id,
|
|
181
|
-
pubkey: serviceEvent.pubkey
|
|
184
|
+
pubkey: serviceEvent.pubkey,
|
|
185
|
+
serviceEvent
|
|
182
186
|
};
|
|
183
187
|
}
|
|
184
188
|
|
|
185
189
|
/**
|
|
186
|
-
* @param {
|
|
187
|
-
* @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
|
|
188
196
|
*/
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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 };
|
|
195
242
|
}
|
|
196
243
|
|
|
197
244
|
/**
|
|
198
|
-
* @param {any}
|
|
199
|
-
* @
|
|
200
|
-
|
|
245
|
+
* @param {any[]} revocations
|
|
246
|
+
* @returns {Record<string, any[]>}
|
|
247
|
+
*/
|
|
248
|
+
/**
|
|
249
|
+
* @param {any[]} revocations
|
|
250
|
+
* @returns {Record<string, any[]>}
|
|
201
251
|
*/
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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 };
|
|
205
284
|
|
|
206
285
|
const now = Math.floor(Date.now() / 1000);
|
|
207
|
-
|
|
208
|
-
// 2. NBF validation
|
|
286
|
+
|
|
209
287
|
if (tags.nbf) {
|
|
210
|
-
const nbf = parseInt(tags.nbf);
|
|
211
|
-
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 };
|
|
212
290
|
}
|
|
213
291
|
|
|
214
|
-
// 3. EXP validation
|
|
215
292
|
if (tags.exp) {
|
|
216
|
-
const exp = parseInt(tags.exp);
|
|
217
|
-
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 };
|
|
218
295
|
}
|
|
219
296
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
for (const rev of revocations) {
|
|
223
|
-
const revTags = Object.fromEntries(rev.tags);
|
|
224
|
-
if (revTags.e === att.id && rev.pubkey === att.pubkey) {
|
|
225
|
-
if (verifyEvent(rev)) {
|
|
226
|
-
return false; // Valid revocation found
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
297
|
+
return { valid: true, revoked: false };
|
|
298
|
+
}
|
|
230
299
|
|
|
231
|
-
|
|
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;
|
|
232
310
|
}
|
|
233
311
|
|
|
234
312
|
/**
|
|
235
313
|
* Verifies that the actual fingerprint found during transport-level connection
|
|
236
314
|
* matches the one declared in the signed service record.
|
|
237
315
|
*
|
|
238
|
-
* @param {
|
|
316
|
+
* @param {ServiceStatus} resolved - The object returned by resolve().
|
|
239
317
|
* @param {string} actualFingerprint - The fingerprint obtained from the service.
|
|
240
318
|
* @returns {boolean}
|
|
241
319
|
*/
|