@kehto/services 0.11.0 → 0.11.1

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 CHANGED
@@ -36,9 +36,9 @@ import {
36
36
  runtime.registerService(
37
37
  'identity',
38
38
  createIdentityService({
39
- getPublicKey: () => signer.getPublicKey(),
40
- getRelays: () => signer.getRelays(),
39
+ getSigner: () => signer,
41
40
  getProfile: (pk) => nostrClient.fetchProfile(pk),
41
+ getFollows: (pk) => contactListCache.getFollows(pk),
42
42
  }),
43
43
  );
44
44
 
@@ -353,6 +353,8 @@ Each factory returns a `ServiceHandler` registrable via `runtime.registerService
353
353
  ### Identity NAP
354
354
  - `createIdentityService` — `identity.*` reads (`identity:read`). No signing surface; shell mediates signing internally.
355
355
 
356
+ `createIdentityService` uses `getSigner()` for `identity.getPublicKey` and `identity.getRelays`. Hosts may also pass optional read-only provider hooks for `getProfile`, `getFollows`, `getList`, `getZaps`, `getMutes`, `getBlocked`, and `getBadges`. These hooks receive the current signer pubkey (or `""` when no signer is connected) and return the payload portion of the corresponding `.result` envelope. Kehto does not query relays itself; the hooks are for hosts that already maintain profile, contact-list, list, zap, moderation, or badge data.
357
+
356
358
  ### Notify NAP
357
359
  - `createNotifyService` — canonical `notify.*` envelopes (`notify:send` / `notify:channel`).
358
360
  - `createNotificationService` — legacy inc-emit `notifications:*` channel; coexists with `createNotifyService` until retired.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ServiceHandler, Signer } from '@kehto/runtime';
2
+ import { IdentityGetProfileMessage, IdentityGetProfileResultMessage, IdentityGetFollowsMessage, IdentityGetFollowsResultMessage, IdentityGetListMessage, IdentityGetListResultMessage, IdentityGetZapsMessage, IdentityGetZapsResultMessage, IdentityGetMutesMessage, IdentityGetMutesResultMessage, IdentityGetBlockedMessage, IdentityGetBlockedResultMessage, IdentityGetBadgesMessage, IdentityGetBadgesResultMessage } from '@napplet/nap/identity/types';
2
3
  import { NostrFilter, NostrEvent, EventTemplate } from '@napplet/core';
3
4
  import { MediaMetadata, MediaAction } from '@napplet/nap/media/types';
4
5
  export { MediaAction } from '@napplet/nap/media/types';
@@ -189,11 +190,12 @@ declare function createNotificationService(options?: NotificationServiceOptions)
189
190
  *
190
191
  * Handles 9 identity.* request types from @napplet/nap/identity. getPublicKey
191
192
  * and getRelays return real values sourced from hooks.auth.getSigner(); the
192
- * remaining 7 (getProfile/getFollows/getList/getZaps/getMutes/getBlocked/
193
- * getBadges) are stub-level. Host apps plug real backends via
194
- * runtime.registerService('identity', realHandler).
193
+ * remaining read-only queries can be backed by optional host providers. When a
194
+ * provider is absent, the service keeps returning spec-shaped empty results.
195
195
  */
196
196
 
197
+ /** A value that may be returned synchronously or through a Promise. */
198
+ type MaybePromise<T> = T | Promise<T>;
197
199
  /**
198
200
  * Options for creating the identity service.
199
201
  *
@@ -212,14 +214,75 @@ interface IdentityServiceOptions {
212
214
  * availability can change dynamically.
213
215
  */
214
216
  getSigner: () => Signer | null;
217
+ /**
218
+ * Optional host-backed profile lookup for the current user.
219
+ *
220
+ * Kehto does not query relays itself; hosts that already maintain kind-0
221
+ * metadata can provide it here without replacing the whole identity service.
222
+ *
223
+ * @param pubkey - Current signer pubkey, or "" when no signer is connected.
224
+ * @param message - Original identity.getProfile request envelope.
225
+ * @returns Profile metadata, or null when unavailable.
226
+ */
227
+ getProfile?: (pubkey: string, message: IdentityGetProfileMessage) => MaybePromise<IdentityGetProfileResultMessage['profile']>;
228
+ /**
229
+ * Optional host-backed follow-list lookup for the current user.
230
+ *
231
+ * @param pubkey - Current signer pubkey, or "" when no signer is connected.
232
+ * @param message - Original identity.getFollows request envelope.
233
+ * @returns Hex-encoded followed pubkeys.
234
+ */
235
+ getFollows?: (pubkey: string, message: IdentityGetFollowsMessage) => MaybePromise<IdentityGetFollowsResultMessage['pubkeys']>;
236
+ /**
237
+ * Optional host-backed categorized-list lookup for the current user.
238
+ *
239
+ * @param listType - Requested list category from the wire envelope.
240
+ * @param pubkey - Current signer pubkey, or "" when no signer is connected.
241
+ * @param message - Original identity.getList request envelope.
242
+ * @returns List entries.
243
+ */
244
+ getList?: (listType: string, pubkey: string, message: IdentityGetListMessage) => MaybePromise<IdentityGetListResultMessage['entries']>;
245
+ /**
246
+ * Optional host-backed zap receipt lookup for the current user.
247
+ *
248
+ * @param pubkey - Current signer pubkey, or "" when no signer is connected.
249
+ * @param message - Original identity.getZaps request envelope.
250
+ * @returns Zap receipts.
251
+ */
252
+ getZaps?: (pubkey: string, message: IdentityGetZapsMessage) => MaybePromise<IdentityGetZapsResultMessage['zaps']>;
253
+ /**
254
+ * Optional host-backed mute-list lookup for the current user.
255
+ *
256
+ * @param pubkey - Current signer pubkey, or "" when no signer is connected.
257
+ * @param message - Original identity.getMutes request envelope.
258
+ * @returns Hex-encoded muted pubkeys.
259
+ */
260
+ getMutes?: (pubkey: string, message: IdentityGetMutesMessage) => MaybePromise<IdentityGetMutesResultMessage['pubkeys']>;
261
+ /**
262
+ * Optional host-backed block-list lookup for the current user.
263
+ *
264
+ * @param pubkey - Current signer pubkey, or "" when no signer is connected.
265
+ * @param message - Original identity.getBlocked request envelope.
266
+ * @returns Hex-encoded blocked pubkeys.
267
+ */
268
+ getBlocked?: (pubkey: string, message: IdentityGetBlockedMessage) => MaybePromise<IdentityGetBlockedResultMessage['pubkeys']>;
269
+ /**
270
+ * Optional host-backed badge lookup for the current user.
271
+ *
272
+ * @param pubkey - Current signer pubkey, or "" when no signer is connected.
273
+ * @param message - Original identity.getBadges request envelope.
274
+ * @returns Badges awarded to the user.
275
+ */
276
+ getBadges?: (pubkey: string, message: IdentityGetBadgesMessage) => MaybePromise<IdentityGetBadgesResultMessage['badges']>;
215
277
  }
216
278
  /**
217
279
  * Create an identity service that handles NIP-5D identity.* envelope messages.
218
280
  *
219
281
  * Supports the 9 read-only identity.* request types from @napplet/nap/identity.
220
282
  * The two nostr-info queries (getPublicKey, getRelays) resolve through the
221
- * caller-supplied signer; the remaining 7 return default/empty payloads with
222
- * spec-correct envelope shapes so napplets always receive a result envelope.
283
+ * caller-supplied signer; the remaining read-only queries resolve through
284
+ * optional host providers or return default/empty payloads with spec-correct
285
+ * envelope shapes so napplets always receive a result envelope.
223
286
  *
224
287
  * @param options - Identity service configuration (getSigner)
225
288
  * @returns A ServiceHandler ready for runtime.registerService('identity', handler)
@@ -2338,4 +2401,4 @@ interface ManifestArchetypeInput {
2338
2401
  */
2339
2402
  declare function manifestToIntentCatalogEntry(manifest: ManifestArchetypeInput): IntentCatalogEntry;
2340
2403
 
2341
- export { type AudioServiceOptions, type AudioSource, type CacheServiceOptions, type CatalogIntentResolver, type CatalogIntentResolverOptions, type ConfigSchemaValidation, type ConfigService, type ConfigServiceOptions, type CoordinatedRelayOptions, type HostCacheBridge, type HostKeyEvent, type HostKeysBridge, type HostMediaBridge, type HttpUploaderOptions, type HttpUploaderRails, type IdentityServiceOptions, type IntentArchetypeSupport, type IntentAvailability, type IntentBehavior, type IntentCandidate, type IntentCatalogEntry, type IntentHandlerPreference, type IntentOpenParams, type IntentRequest, type IntentResolver, type IntentResolverContext, type IntentResult, type IntentServiceOptions, type IntentWindowController, type KeysServiceOptions, type ManifestArchetypeInput, type MediaMetadataLike, type MediaPlaybackOwner, type MediaServiceOptions, type MediaSessionCreateOptions, type MediaSessionTarget, type MediaSourceRef, type NostrTag, type Notification, type NotificationServiceOptions, type NotifyServiceOptions, type OutboxPublishOptions, type OutboxPublishResult, type OutboxQueryOptions, type OutboxRelayPlan, type OutboxRelayPool, type OutboxResult, type OutboxRouter, type OutboxRouterSubscription, type OutboxServiceOptions, type OutboxStrategy, type OutboxSubscribeOptions, type OutboxSubscriptionSink, type OutboxTarget, type RailServerConfig, type RelayListEntry, type RelayPoolOutboxRouterOptions, type RelayPoolServiceOptions, type ResourceService, type ResourceServiceOptions, type SignEvent, type ThemeService, type ThemeServiceOptions, type UploadDimensions, type UploadRail, type UploadRequest, type UploadResult, type UploadServiceOptions, type UploadState, type UploadStatus, type Uploader, type UploaderContext, createAudioService, createBrowserMediaBridge, createCacheService, createCatalogIntentResolver, createConfigService, createCoordinatedRelay, createHttpUploader, createIdentityService, createIntentService, createKeysService, createMediaService, createNotificationService, createNotifyService, createOutboxService, createRelayPoolOutboxRouter, createRelayPoolService, createResourceService, createThemeService, createUploadService, manifestToIntentCatalogEntry };
2404
+ export { type AudioServiceOptions, type AudioSource, type CacheServiceOptions, type CatalogIntentResolver, type CatalogIntentResolverOptions, type ConfigSchemaValidation, type ConfigService, type ConfigServiceOptions, type CoordinatedRelayOptions, type HostCacheBridge, type HostKeyEvent, type HostKeysBridge, type HostMediaBridge, type HttpUploaderOptions, type HttpUploaderRails, type IdentityServiceOptions, type IntentArchetypeSupport, type IntentAvailability, type IntentBehavior, type IntentCandidate, type IntentCatalogEntry, type IntentHandlerPreference, type IntentOpenParams, type IntentRequest, type IntentResolver, type IntentResolverContext, type IntentResult, type IntentServiceOptions, type IntentWindowController, type KeysServiceOptions, type ManifestArchetypeInput, type MaybePromise, type MediaMetadataLike, type MediaPlaybackOwner, type MediaServiceOptions, type MediaSessionCreateOptions, type MediaSessionTarget, type MediaSourceRef, type NostrTag, type Notification, type NotificationServiceOptions, type NotifyServiceOptions, type OutboxPublishOptions, type OutboxPublishResult, type OutboxQueryOptions, type OutboxRelayPlan, type OutboxRelayPool, type OutboxResult, type OutboxRouter, type OutboxRouterSubscription, type OutboxServiceOptions, type OutboxStrategy, type OutboxSubscribeOptions, type OutboxSubscriptionSink, type OutboxTarget, type RailServerConfig, type RelayListEntry, type RelayPoolOutboxRouterOptions, type RelayPoolServiceOptions, type ResourceService, type ResourceServiceOptions, type SignEvent, type ThemeService, type ThemeServiceOptions, type UploadDimensions, type UploadRail, type UploadRequest, type UploadResult, type UploadServiceOptions, type UploadState, type UploadStatus, type Uploader, type UploaderContext, createAudioService, createBrowserMediaBridge, createCacheService, createCatalogIntentResolver, createConfigService, createCoordinatedRelay, createHttpUploader, createIdentityService, createIntentService, createKeysService, createMediaService, createNotificationService, createNotifyService, createOutboxService, createRelayPoolOutboxRouter, createRelayPoolService, createResourceService, createThemeService, createUploadService, manifestToIntentCatalogEntry };
package/dist/index.js CHANGED
@@ -231,6 +231,184 @@ function createNotificationService(options) {
231
231
 
232
232
  // src/identity-service.ts
233
233
  var IDENTITY_SERVICE_VERSION = "1.0.0";
234
+ function sendProviderError(send, result, fallback, err) {
235
+ send({
236
+ ...result,
237
+ error: err?.message ?? fallback
238
+ });
239
+ }
240
+ async function getCurrentPubkey(options) {
241
+ const currentSigner = options.getSigner();
242
+ if (!currentSigner?.getPublicKey) return "";
243
+ try {
244
+ return await currentSigner.getPublicKey() ?? "";
245
+ } catch {
246
+ return "";
247
+ }
248
+ }
249
+ function sendOptionalProviderResult(options, send, fallbackResult, errorFallback, buildResult) {
250
+ if (!buildResult) {
251
+ send(fallbackResult);
252
+ return;
253
+ }
254
+ Promise.resolve(getCurrentPubkey(options)).then((pubkey) => buildResult(pubkey)).then((result) => send(result)).catch((err) => sendProviderError(send, fallbackResult, errorFallback, err));
255
+ }
256
+ function sendIdentityError(send, id, typeBase, error) {
257
+ send({ type: `${typeBase}.error`, id, error });
258
+ }
259
+ function sendSignerError(send, id, typeBase, fallback, err) {
260
+ sendIdentityError(send, id, typeBase, err?.message ?? fallback);
261
+ }
262
+ function handleGetPublicKey(options, id, send) {
263
+ const signer = options.getSigner();
264
+ if (!signer) {
265
+ const result = {
266
+ type: "identity.getPublicKey.result",
267
+ id,
268
+ pubkey: ""
269
+ };
270
+ send(result);
271
+ return;
272
+ }
273
+ Promise.resolve(signer.getPublicKey?.()).then((pubkey) => {
274
+ const result = {
275
+ type: "identity.getPublicKey.result",
276
+ id,
277
+ pubkey: pubkey ?? ""
278
+ };
279
+ send(result);
280
+ }).catch((err) => sendSignerError(send, id, "identity.getPublicKey", "getPublicKey failed", err));
281
+ }
282
+ function handleGetRelays(options, id, send) {
283
+ const signer = options.getSigner();
284
+ if (!signer) {
285
+ sendIdentityError(send, id, "identity.getRelays", "no signer configured");
286
+ return;
287
+ }
288
+ Promise.resolve(signer.getRelays?.() ?? {}).then((relays) => {
289
+ const result = {
290
+ type: "identity.getRelays.result",
291
+ id,
292
+ relays
293
+ };
294
+ send(result);
295
+ }).catch((err) => sendSignerError(send, id, "identity.getRelays", "getRelays failed", err));
296
+ }
297
+ function handleReadProvider(options, id, message, send) {
298
+ switch (message.type) {
299
+ case "identity.getProfile":
300
+ sendOptionalProviderResult(
301
+ options,
302
+ send,
303
+ { type: "identity.getProfile.result", id, profile: null },
304
+ "getProfile failed",
305
+ options.getProfile ? async (pubkey) => ({
306
+ type: "identity.getProfile.result",
307
+ id,
308
+ profile: await options.getProfile(pubkey, message)
309
+ }) : void 0
310
+ );
311
+ return true;
312
+ case "identity.getFollows":
313
+ sendOptionalProviderResult(
314
+ options,
315
+ send,
316
+ { type: "identity.getFollows.result", id, pubkeys: [] },
317
+ "getFollows failed",
318
+ options.getFollows ? async (pubkey) => ({
319
+ type: "identity.getFollows.result",
320
+ id,
321
+ pubkeys: await options.getFollows(pubkey, message)
322
+ }) : void 0
323
+ );
324
+ return true;
325
+ case "identity.getList":
326
+ handleGetList(options, id, message, send);
327
+ return true;
328
+ case "identity.getZaps":
329
+ sendOptionalProviderResult(
330
+ options,
331
+ send,
332
+ { type: "identity.getZaps.result", id, zaps: [] },
333
+ "getZaps failed",
334
+ options.getZaps ? async (pubkey) => ({
335
+ type: "identity.getZaps.result",
336
+ id,
337
+ zaps: await options.getZaps(pubkey, message)
338
+ }) : void 0
339
+ );
340
+ return true;
341
+ case "identity.getMutes":
342
+ sendOptionalProviderResult(
343
+ options,
344
+ send,
345
+ { type: "identity.getMutes.result", id, pubkeys: [] },
346
+ "getMutes failed",
347
+ options.getMutes ? async (pubkey) => ({
348
+ type: "identity.getMutes.result",
349
+ id,
350
+ pubkeys: await options.getMutes(pubkey, message)
351
+ }) : void 0
352
+ );
353
+ return true;
354
+ case "identity.getBlocked":
355
+ sendOptionalProviderResult(
356
+ options,
357
+ send,
358
+ { type: "identity.getBlocked.result", id, pubkeys: [] },
359
+ "getBlocked failed",
360
+ options.getBlocked ? async (pubkey) => ({
361
+ type: "identity.getBlocked.result",
362
+ id,
363
+ pubkeys: await options.getBlocked(pubkey, message)
364
+ }) : void 0
365
+ );
366
+ return true;
367
+ case "identity.getBadges":
368
+ sendOptionalProviderResult(
369
+ options,
370
+ send,
371
+ { type: "identity.getBadges.result", id, badges: [] },
372
+ "getBadges failed",
373
+ options.getBadges ? async (pubkey) => ({
374
+ type: "identity.getBadges.result",
375
+ id,
376
+ badges: await options.getBadges(pubkey, message)
377
+ }) : void 0
378
+ );
379
+ return true;
380
+ default:
381
+ return false;
382
+ }
383
+ }
384
+ function handleGetList(options, id, message, send) {
385
+ sendOptionalProviderResult(
386
+ options,
387
+ send,
388
+ { type: "identity.getList.result", id, entries: [] },
389
+ "getList failed",
390
+ options.getList ? async (pubkey) => ({
391
+ type: "identity.getList.result",
392
+ id,
393
+ entries: await options.getList(message.listType, pubkey, message)
394
+ }) : void 0
395
+ );
396
+ }
397
+ function handleIdentityServiceMessage(options, message, send) {
398
+ const id = message.id ?? "";
399
+ switch (message.type) {
400
+ case "identity.getPublicKey":
401
+ handleGetPublicKey(options, id, send);
402
+ return;
403
+ case "identity.getRelays":
404
+ handleGetRelays(options, id, send);
405
+ return;
406
+ default:
407
+ if (!handleReadProvider(options, id, message, send)) {
408
+ sendIdentityError(send, id, message.type, `Unknown identity method: ${message.type}`);
409
+ }
410
+ }
411
+ }
234
412
  function createIdentityService(options) {
235
413
  return {
236
414
  descriptor: {
@@ -239,116 +417,7 @@ function createIdentityService(options) {
239
417
  description: "NIP-5D identity NAP reference handler (9 read-only identity queries)"
240
418
  },
241
419
  handleMessage(_windowId, message, send) {
242
- const id = message.id ?? "";
243
- function sendError(typeBase, error) {
244
- send({ type: `${typeBase}.error`, id, error });
245
- }
246
- function sendSignerError(typeBase, fallback, err) {
247
- sendError(typeBase, err?.message ?? fallback);
248
- }
249
- const signer = options.getSigner();
250
- switch (message.type) {
251
- case "identity.getPublicKey": {
252
- if (!signer) {
253
- const result = {
254
- type: "identity.getPublicKey.result",
255
- id,
256
- pubkey: ""
257
- };
258
- send(result);
259
- return;
260
- }
261
- Promise.resolve(signer.getPublicKey?.()).then((pubkey) => {
262
- const result = {
263
- type: "identity.getPublicKey.result",
264
- id,
265
- pubkey: pubkey ?? ""
266
- };
267
- send(result);
268
- }).catch((err) => sendSignerError("identity.getPublicKey", "getPublicKey failed", err));
269
- return;
270
- }
271
- case "identity.getRelays": {
272
- if (!signer) {
273
- sendError("identity.getRelays", "no signer configured");
274
- return;
275
- }
276
- Promise.resolve(signer.getRelays?.() ?? {}).then((relays) => {
277
- const result = {
278
- type: "identity.getRelays.result",
279
- id,
280
- relays
281
- };
282
- send(result);
283
- }).catch((err) => sendSignerError("identity.getRelays", "getRelays failed", err));
284
- return;
285
- }
286
- case "identity.getProfile": {
287
- const result = {
288
- type: "identity.getProfile.result",
289
- id,
290
- profile: null
291
- };
292
- send(result);
293
- return;
294
- }
295
- case "identity.getFollows": {
296
- const result = {
297
- type: "identity.getFollows.result",
298
- id,
299
- pubkeys: []
300
- };
301
- send(result);
302
- return;
303
- }
304
- case "identity.getList": {
305
- const result = {
306
- type: "identity.getList.result",
307
- id,
308
- entries: []
309
- };
310
- send(result);
311
- return;
312
- }
313
- case "identity.getZaps": {
314
- const result = {
315
- type: "identity.getZaps.result",
316
- id,
317
- zaps: []
318
- };
319
- send(result);
320
- return;
321
- }
322
- case "identity.getMutes": {
323
- const result = {
324
- type: "identity.getMutes.result",
325
- id,
326
- pubkeys: []
327
- };
328
- send(result);
329
- return;
330
- }
331
- case "identity.getBlocked": {
332
- const result = {
333
- type: "identity.getBlocked.result",
334
- id,
335
- pubkeys: []
336
- };
337
- send(result);
338
- return;
339
- }
340
- case "identity.getBadges": {
341
- const result = {
342
- type: "identity.getBadges.result",
343
- id,
344
- badges: []
345
- };
346
- send(result);
347
- return;
348
- }
349
- default:
350
- sendError(message.type, `Unknown identity method: ${message.type}`);
351
- }
420
+ handleIdentityServiceMessage(options, message, send);
352
421
  },
353
422
  // Identity service has no per-window state to clean up.
354
423
  onWindowDestroyed(_windowId) {
@@ -383,7 +452,8 @@ function createRelayPoolService(options) {
383
452
  send({ type: "relay.eose", subId });
384
453
  return;
385
454
  }
386
- const relayUrls = options.selectRelayTier(filters);
455
+ const relayHint = typeof relayMessage.relay === "string" && relayMessage.relay.length > 0 ? relayMessage.relay : void 0;
456
+ const relayUrls = relayHint ? [relayHint] : options.selectRelayTier(filters);
387
457
  let eoseSent = false;
388
458
  const eoseTimer = setTimeout(() => {
389
459
  if (!eoseSent) {
@@ -563,7 +633,8 @@ function createCoordinatedRelay(options) {
563
633
  maybeSendEose(subKey, subId, send);
564
634
  }
565
635
  }, timeoutMs);
566
- const relayUrls = options.relayPool.selectRelayTier(filters);
636
+ const relayHint = typeof relayMessage.relay === "string" && relayMessage.relay.length > 0 ? relayMessage.relay : void 0;
637
+ const relayUrls = relayHint ? [relayHint] : options.relayPool.selectRelayTier(filters);
567
638
  tracked.relayHandle = options.relayPool.subscribe(filters, (item) => {
568
639
  if (item === "EOSE") {
569
640
  clearTimeout(tracked.eoseTimer);