• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hyperledger-identus / sdk-ts / 23159132029

16 Mar 2026 06:16PM UTC coverage: 71.673% (-0.03%) from 71.698%
23159132029

push

github

web-flow
fix: linter rules on e2e tests project (#510)

Signed-off-by: Francisco Javier Ribo Labrador <elribonazo@gmail.com>

1387 of 2155 branches covered (64.36%)

Branch coverage included in aggregate %.

3213 of 4263 relevant lines covered (75.37%)

45.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

30.72
/src/pluto/Pluto.ts
1
/* eslint-disable @typescript-eslint/unbound-method */
2

3

4

5

6
import * as Domain from "../domain";
7
import * as Models from "./models";
8
import { PeerDID } from "../peer-did/PeerDID";
9
import { BackupManager } from "./backup/BackupManager";
10
import { type PlutoRepositories, repositoryFactory } from "./repositories";
11
import { type Arrayable, asArray, notNil } from "../utils";
12
import { Startable } from "../domain/protocols/Startable";
13
import { type Version } from "../domain/backup";
14
import { type Query } from "./types";
15

16

17
/**
18
 * Pluto implementation
19
 * 
20
 * Structure:
21
 * - Pluto class is an orchestration layer
22
 * - Repositories handle mapping between Domain and Storable Models
23
 * - Models suggest db structure
24
 * - Store abstracts db implementation
25
 * 
26
 * Pluto:
27
 * - always handles Domain classes
28
 * - manage relationships
29
 * - handle logic and concepts
30
 * - throw known Errors
31
 * - return null
32
 * - naming convention
33
 *   - (get/store) (Domain name Pluralized) ie getCredentials
34
 * 
35
 * Models:
36
 * - naming convention
37
 *   - alias for optional names
38
 *   - name for required identifiers
39
 *   - dataJson for JSON.stringified objects
40
 * 
41
 * Store:
42
 * - simplified interface
43
 * - crud interactions
44
 * - only use Models
45
 * 
46
 */
47
export namespace Pluto {
48
  /**
49
   * Store interface for Pluto persistence layer
50
   * 
51
   * This interface defines the contract for database operations on Models.
52
   * Implementations must handle CRUD operations for all supported model types.
53
   * 
54
   * Supported Models:
55
   * - Models.Credential - Verifiable credentials
56
   * - Models.CredentialMetadata - Metadata for credential schemas
57
   * - Models.DID - Decentralized identifiers
58
   * - Models.Key - Private keys
59
   * - Models.Message - DIDComm messages
60
   * - Models.DIDKeyLink - Links between DIDs and keys
61
   * - Models.DIDLink - Links between DIDs (pairs, mediators, routing)
62
   * 
63
   * Supported Tables:
64
   * - "credentials" - Stores credential data
65
   * - "credential-metadata" - Stores credential metadata
66
   * - "dids" - Stores DID documents
67
   * - "keys" - Stores private keys
68
   * - "messages" - Stores DIDComm messages
69
   * - "didkey-link" - Stores DID-key relationships
70
   * - "did-link" - Stores DID-DID relationships
71
   */
72
  export interface Store {
73
    /**
74
     * Handle any necessary startup.
75
     * Will be called first before any usage, if provided.
76
     */
77
    start?(): Promise<void>;
78

79
    /**
80
     * Handle any necessary teardown.
81
     */
82
    stop?(): Promise<void>;
83

84
    /**
85
     * Run a query to fetch data from the Store
86
     * 
87
     * @template T - The model type that extends Models.Model (e.g., Models.Credential, Models.DID, Models.Key, etc.)
88
     * @param table - Valid table name. Must be one of: "credentials", "credential-metadata", "didkey-link", "did-link", "dids", "keys", "messages"
89
     * @param query - Optional Query object with selector conditions and operators for filtering results
90
     * 
91
     * Query behavior:
92
     * - Properties within an object will be AND'ed together
93
     * - Different objects in $or array will be OR'd together
94
     * - Omit query parameter to fetch all records from the table
95
     * 
96
     * @example
97
     * Search for credentials by uuid and issuer
98
     * ```ts
99
     *   store.query<Models.Credential>("credentials", { 
100
     *     selector: { uuid: "credential-123", issuer: "did:example:issuer" }
101
     *   })
102
     * ```
103
     * @example
104
     * Search for DIDs with method "prism" OR "peer"
105
     * ```ts
106
     *   store.query<Models.DID>("dids", { 
107
     *     selector: { $or: [{ method: "prism" }, { method: "peer" }] }
108
     *   })
109
     * ```
110
     * @example
111
     * Fetch all messages from the table
112
     * ```ts
113
     *   store.query<Models.Message>("messages")
114
     * ```
115
     * 
116
     * @returns Promise resolving to array of models matching the query criteria
117
     */
118
    query<T extends Models.Model>(table: string, query?: Query<T>): Promise<T[]>;
119

120
    /**
121
     * Persist new data in the Store.
122
     * 
123
     * @template T - The model type that extends Models.Model (e.g., Models.Credential, Models.DID, Models.Key, etc.)
124
     * @param table - Valid table name. Must be one of: "credentials", "credential-metadata", "didkey-link", "did-link", "dids", "keys", "messages"
125
     * @param model - The model instance to persist. Must include all required properties and should have a valid uuid
126
     * 
127
     * @example
128
     * Insert a new credential
129
     * ```ts
130
     *   const credential: Models.Credential = {
131
     *     uuid: "credential-123",
132
     *     recoveryId: "jwt",
133
     *     dataJson: JSON.stringify(credentialData),
134
     *     id: "credential-id",
135
     *     issuer: "did:example:issuer"
136
     *   };
137
     *   await store.insert<Models.Credential>("credentials", credential);
138
     * ```
139
     * 
140
     * @returns Promise that resolves when the model is successfully persisted
141
     * @throws Error if the model is invalid or table name is not recognized
142
     */
143
    insert<T extends Models.Model>(table: string, model: T): Promise<void>;
144

145
    /**
146
     * Update an existing row in the Store
147
     * 
148
     * @template T - The model type that extends Models.Model (e.g., Models.Credential, Models.DID, Models.Key, etc.)
149
     * @param table - Valid table name. Must be one of: "credentials", "credential-metadata", "didkey-link", "did-link", "dids", "keys", "messages"
150
     * @param model - The model instance with updated data. Must include the uuid to identify the record to update
151
     * 
152
     * @example
153
     * Update a credential to mark it as revoked
154
     * ```ts
155
     *   const updatedCredential: Models.Credential = {
156
     *     uuid: "credential-123",
157
     *     recoveryId: "jwt",
158
     *     dataJson: JSON.stringify(updatedCredentialData),
159
     *     id: "credential-id",
160
     *     issuer: "did:example:issuer",
161
     *     revoked: true
162
     *   };
163
     *   await store.update<Models.Credential>("credentials", updatedCredential);
164
     * ```
165
     * 
166
     * @returns Promise that resolves when the model is successfully updated
167
     * @throws Error if the model with the given uuid is not found or table name is not recognized
168
     */
169
    update<T extends Models.Model>(table: string, model: T): Promise<void>;
170

171
    /**
172
     * Delete a row from the Store
173
     * 
174
     * @param table - Valid table name. Must be one of: "credentials", "credential-metadata", "didkey-link", "did-link", "dids", "keys", "messages"
175
     * @param uuid - The unique identifier of the record to delete
176
     * 
177
     * @example
178
     * Delete a credential by its uuid
179
     * ```ts
180
     *   await store.delete("credentials", "credential-123");
181
     * ```
182
     * 
183
     * @example
184
     * Delete a DID by its uuid
185
     * ```ts
186
     *   await store.delete("dids", "did:example:123");
187
     * ```
188
     * 
189
     * @returns Promise that resolves when the record is successfully deleted
190
     * @throws Error if the record with the given uuid is not found or table name is not recognized
191
     */
192
    delete(table: string, uuid: string): Promise<void>;
193
  }
194
}
195

196
export class Pluto extends Startable.Controller implements Domain.Pluto {
197
  public BackupMgr: BackupManager;
198
  private Repositories: PlutoRepositories;
199

200
  constructor(
201
    private readonly store: Pluto.Store,
107✔
202
    private readonly keyRestoration: Domain.KeyRestoration
107✔
203
  ) {
204
    super();
107✔
205
    this.Repositories = repositoryFactory(store, keyRestoration);
107✔
206
    this.BackupMgr = new BackupManager(this, this.Repositories);
107✔
207
  }
208

209
  protected async _start() {
210
    if (notNil(this.store.start)) {
56!
211
      await this.store.start();
56✔
212
    }
213
  }
214

215
  protected async _stop() {
216
    if (notNil(this.store.stop)) {
42!
217
      await this.store.stop();
42✔
218
    }
219
  }
220

221
  /** Backups **/
222
  backup(version?: Version) {
223
    return this.BackupMgr.backup(version);
×
224
  }
225

226
  restore(backup: Domain.Backup.Schema) {
227
    return this.BackupMgr.restore(backup);
6✔
228
  }
229

230
  async deleteMessage(id: string): Promise<void> {
231
    const message = await this.Repositories.Messages.findOne({ id });
×
232
    //TODO: Improve error handling
233
    if (message) {
×
234
      await this.Repositories.Messages.delete(message.uuid);
×
235
    }
236
  }
237

238
  /** Credentials **/
239

240
  async storeCredential(credential: Domain.Credential): Promise<void> {
241
    await this.Repositories.Credentials.save(credential);
12✔
242
  }
243

244
  async getAllCredentials(): Promise<Domain.Credential[]> {
245
    return this.Repositories.Credentials.get();
×
246
  }
247

248

249
  async revokeCredential(credential: Domain.Credential): Promise<void> {
250
    if (!credential || !credential.isStorable()) {
×
251
      throw new Error("Credential not found or invalid");
×
252
    }
253
    credential.properties.set("revoked", true);
×
254
    const credentialModel = this.Repositories.Credentials.toModel(credential);
×
255
    await this.Repositories.Credentials.update(credentialModel);
×
256
  }
257

258

259
  /** Credential Metadata **/
260

261
  async storeCredentialMetadata(metadata: Domain.CredentialMetadata): Promise<void> {
262
    await this.Repositories.CredentialMetadata.save(metadata);
×
263
  }
264

265
  async getCredentialMetadata(name: string): Promise<Domain.CredentialMetadata | null> {
266
    return await this.Repositories.CredentialMetadata.findOne({ name });
×
267
  }
268

269

270
  /** LinkSecret **/
271

272
  async storeLinkSecret(linkSecret: Domain.LinkSecret): Promise<void> {
273
    return await this.Repositories.LinkSecrets.save(linkSecret);
4✔
274
  }
275

276
  async getLinkSecret(name: string = Domain.LinkSecret.defaultName): Promise<Domain.LinkSecret | null> {
×
277
    return await this.Repositories.LinkSecrets.findOne({ alias: name });
×
278
  }
279

280

281
  /** PrivateKeys **/
282

283
  async storePrivateKey(privateKey: Domain.PrivateKey): Promise<void> {
284
    await this.Repositories.Keys.save(privateKey);
×
285
  }
286

287
  async getDIDPrivateKeysByDID(did: Domain.DID): Promise<Domain.PrivateKey[]> {
288
    const links = await this.Repositories.DIDKeyLinks.getModels({ selector: { didId: did.uuid } });
3✔
289
    const $or = links.map(x => ({ uuid: x.keyId }));
4✔
290
    const keys = await this.Repositories.Keys.get({ selector: { $or } });
3✔
291

292
    return keys;
3✔
293
  }
294

295
  /** DIDs **/
296

297
  async storeDID(did: Domain.DID, keys?: Arrayable<Domain.PrivateKey>, alias?: string): Promise<void> {
298
    await this.Repositories.DIDs.save(did, alias);
25✔
299
    for (const key of asArray(keys)) {
25✔
300
      await this.Repositories.Keys.save(key);
19✔
301
      await this.Repositories.DIDKeyLinks.insert({
19✔
302
        alias,
303
        didId: did.uuid,
304
        keyId: key.uuid
305
      });
306
    }
307
  }
308

309
  /** Prism DIDs **/
310

311
  async storePrismDID(did: Domain.DID, privateKey: Domain.PrivateKey, alias?: string): Promise<void> {
312
    await this.Repositories.DIDs.save(did, alias);
×
313
    await this.Repositories.Keys.save(privateKey);
×
314

315
    await this.Repositories.DIDKeyLinks.insert({
×
316
      alias,
317
      didId: did.uuid,
318
      keyId: privateKey.uuid
319
    });
320
  }
321

322
  async getAllPrismDIDs(): Promise<Domain.PrismDID[]> {
323
    const dids = await this.Repositories.DIDs.find({ method: "prism" });
3✔
324
    const prismDIDS: Domain.PrismDID[] = [];
3✔
325
    for (const did of dids) {
3✔
326
      const dbDids = await this.getPrismDIDS(did.uuid);
1✔
327
      for (const prismDID of dbDids) {
1✔
328
        prismDIDS.push(prismDID);
1✔
329
      }
330
    }
331
    return prismDIDS;
3✔
332
  }
333

334
  private async getPrismDIDS(didId: string): Promise<Domain.PrismDID[]> {
335
    const links = await this.Repositories.DIDKeyLinks.getModels({ selector: { didId } });
1✔
336
    return Promise.all(
1✔
337
      links.map(async (link) => {
338
        const did = await this.Repositories.DIDs.byUUID(link.didId);
1✔
339
        const key = await this.Repositories.Keys.byUUID(link.keyId);
1✔
340
        if (!did || !key) {
1!
341
          throw new Error("PrismDID not found");
×
342
        }
343
        const prismDID = new Domain.PrismDID(did, key, link.alias);
1✔
344
        return prismDID;
1✔
345
      })
346
    );
347
  }
348

349

350
  /** Peer DIDs **/
351

352
  async storePeerDID(did: Domain.DID, privateKeys: Domain.PrivateKey[]): Promise<void> {
353
    await this.Repositories.DIDs.save(did);
×
354
    for (const key of privateKeys) {
×
355
      await this.Repositories.Keys.save(key);
×
356
      await this.Repositories.DIDKeyLinks.insert({ didId: did.uuid, keyId: key.uuid });
×
357
    }
358
  }
359

360
  async getAllPeerDIDs(): Promise<PeerDID[]> {
361
    const allDids = await this.Repositories.DIDs.find({ method: "peer" });
×
362
    const allLinks = await this.Repositories.DIDKeyLinks.getModels({
×
363
      selector: { $or: allDids.map(x => ({ didId: x.uuid })) }
×
364
    });
365
    const allKeys = await this.Repositories.Keys.get({
×
366
      selector: { $or: allLinks.map(x => ({ uuid: x.keyId })) }
×
367
    });
368

369
    const getKeyCurveByNameAndIndex = (name: string, index?: number): Domain.KeyCurve => {
×
370
      switch (name) {
×
371
        case Domain.Curve.X25519.toString():
372
          return { curve: Domain.Curve.X25519 };
×
373
        case Domain.Curve.ED25519.toString():
374
          return { curve: Domain.Curve.ED25519 };
×
375
        case Domain.Curve.SECP256K1.toString():
376
          return { curve: Domain.Curve.SECP256K1, index };
×
377
        default:
378
          throw new Domain.ApolloError.InvalidKeyCurve(name);
×
379
      }
380
    };
381

382
    const peerDids = allDids.map(did => {
×
383
      const keyIds = allLinks.filter(x => x.didId === did.uuid).map(x => x.keyId);
×
384
      const keys = allKeys.filter(x => keyIds.includes(x.uuid));
×
385

386
      const peerDid = new PeerDID(
×
387
        did,
388
        // TODO: remove this when PeerDIDs are updated to use PrivateKey
389
        keys.map(x => ({
×
390
          keyCurve: getKeyCurveByNameAndIndex(x.curve, x.index),
391
          value: x.getEncoded()
392
        }))
393
      );
394

395
      return peerDid;
×
396
    });
397

398
    return peerDids;
×
399
  }
400

401

402
  /** Messages **/
403

404
  async storeMessage(message: Domain.Message): Promise<void> {
405
    await this.Repositories.Messages.save(message);
6✔
406
  }
407

408
  async storeMessages(messages: Domain.Message[]): Promise<void> {
409
    for (const msg of messages) {
6✔
410
      await this.Repositories.Messages.save(msg);
4✔
411
    }
412
  }
413

414
  async getMessage(id: string): Promise<Domain.Message | null> {
415
    return await this.Repositories.Messages.findOne({ id });
×
416
  }
417

418
  async getAllMessages(): Promise<Domain.Message[]> {
419
    return this.Repositories.Messages.get();
2✔
420
  }
421

422

423
  /** DID Pairs **/
424

425
  async storeDIDPair(host: Domain.DID, receiver: Domain.DID, alias: string): Promise<void> {
426
    await this.Repositories.DIDs.save(host);
7✔
427
    await this.Repositories.DIDs.save(receiver);
7✔
428

429
    await this.Repositories.DIDLinks.insert({
7✔
430
      alias,
431
      role: Models.DIDLink.role.pair,
432
      hostId: host.uuid,
433
      targetId: receiver.uuid
434
    });
435
  }
436

437
  async getAllDidPairs(): Promise<Domain.DIDPair[]> {
438
    const links = await this.Repositories.DIDLinks.getModels({ selector: { role: Models.DIDLink.role.pair } });
×
439
    const didPairs = await Promise.all(links.map(x => this.mapDIDPairToDomain(x)));
×
440
    const filtered = didPairs.filter((x): x is Domain.DIDPair => x != null);
×
441

442
    return filtered;
×
443
  }
444

445
  async getPairByDID(did: Domain.DID): Promise<Domain.DIDPair | null> {
446
    const links = await this.Repositories.DIDLinks.getModels({
×
447
      selector: {
448
        $or: [
449
          { role: Models.DIDLink.role.pair, hostId: did.uuid },
450
          { role: Models.DIDLink.role.pair, targetId: did.uuid }
451
        ]
452
      }
453
    });
454

455
    // ?? this seems presumptuous? couldnt hostDID be re-used?
456
    const link = this.onlyOne(links);
×
457
    const didPair = this.mapDIDPairToDomain(link);
×
458

459
    return didPair;
×
460
  }
461

462
  async getPairByName(alias: string): Promise<Domain.DIDPair | null> {
463
    const links = await this.Repositories.DIDLinks.getModels(
×
464
      {
465
        selector: { alias, role: Models.DIDLink.role.pair }
466
      });
467
    const link = this.onlyOne(links);
×
468
    const didPair = this.mapDIDPairToDomain(link);
×
469

470
    return didPair;
×
471
  }
472

473
  private async mapDIDPairToDomain(link: Models.DIDLink): Promise<Domain.DIDPair | null> {
474
    const hostDID = await this.Repositories.DIDs.byUUID(link.hostId);
×
475
    const targetDID = await this.Repositories.DIDs.byUUID(link.targetId);
×
476
    const alias = link.alias ?? "";
×
477

478
    if (!hostDID || !targetDID) {
×
479
      return null;
×
480
    }
481

482
    const didPair = new Domain.DIDPair(hostDID, targetDID, alias);
×
483
    return didPair;
×
484
  }
485

486

487
  /** Mediators **/
488

489
  async getAllMediators(): Promise<Domain.Mediator[]> {
490
    const links = await this.Repositories.DIDLinks.getModels({
×
491
      selector: {
492
        $or: [
493
          { role: Models.DIDLink.role.mediator },
494
          { role: Models.DIDLink.role.routing },
495
        ]
496
      }
497
    });
498
    const hostIds = links.map(x => x.hostId).filter((x, i, s) => s.indexOf(x) === i);
×
499

500
    const result = await Promise.all(
×
501
      hostIds.map(async hostId => {
502
        const mediatorLink = links.find(x => x.hostId === hostId && x.role === Models.DIDLink.role.mediator.valueOf());
×
503
        const routingLink = links.find(x => x.hostId === hostId && x.role === Models.DIDLink.role.routing.valueOf());
×
504

505
        if (!mediatorLink || !routingLink) {
×
506
          throw new Error();
×
507
        }
508

509
        const hostDID = await this.Repositories.DIDs.byUUID(hostId);
×
510
        const mediatorDID = await this.Repositories.DIDs.byUUID(mediatorLink.targetId);
×
511
        const routingDID = await this.Repositories.DIDs.byUUID(routingLink.targetId);
×
512

513
        if (!hostDID || !mediatorDID || !routingDID) {
×
514
          throw new Error();
×
515
        }
516

517
        const domain: Domain.Mediator = { hostDID, mediatorDID, routingDID };
×
518
        return domain;
×
519
      })
520
    );
521

522
    return result;
×
523
  }
524

525
  async storeMediator(mediator: Domain.Mediator): Promise<void> {
526
    await this.Repositories.DIDs.save(mediator.hostDID);
4✔
527
    await this.Repositories.DIDs.save(mediator.mediatorDID);
4✔
528
    await this.Repositories.DIDs.save(mediator.routingDID);
4✔
529

530
    await this.Repositories.DIDLinks.insert({
4✔
531
      role: Models.DIDLink.role.mediator,
532
      hostId: mediator.hostDID.uuid,
533
      targetId: mediator.mediatorDID.uuid
534
    });
535

536
    await this.Repositories.DIDLinks.insert({
4✔
537
      role: Models.DIDLink.role.routing,
538
      hostId: mediator.hostDID.uuid,
539
      targetId: mediator.routingDID.uuid
540
    });
541
  }
542

543
  private onlyOne<T>(arr: T[]): T {
544
    const item = arr.at(0);
×
545
    if (!item || arr.length !== 1) throw new Error("something wrong");
×
546

547
    return item;
×
548
  }
549
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc