@twin.org/identity-connector-entity-storage 0.0.3-next.3 → 0.0.3-next.30

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
@@ -1,6 +1,6 @@
1
- # TWIN Identity Connector Entity Storage
1
+ # TWIN Identity
2
2
 
3
- Identity connector implementation using entity storage.
3
+ The identity-connector-entity-storage package provides an entity storage backed connector for identity workflows, enabling reliable persistence and retrieval of identity records. It supports implementations that need consistent storage semantics while staying aligned with shared contracts across the ecosystem.
4
4
 
5
5
  ## Installation
6
6
 
@@ -1,10 +1,10 @@
1
1
  // Copyright 2024 IOTA Stiftung.
2
2
  // SPDX-License-Identifier: Apache-2.0.
3
- import { BaseError, BitString, Coerce, Compression, CompressionType, Converter, GeneralError, Guards, Is, JsonHelper, NotFoundError, ObjectHelper, RandomHelper } from "@twin.org/core";
4
- import { JsonLdProcessor } from "@twin.org/data-json-ld";
3
+ import { ArrayHelper, BaseError, BitString, Coerce, Compression, CompressionType, Converter, GeneralError, Guards, Is, JsonHelper, NotFoundError, ObjectHelper, RandomHelper, Url, Urn } from "@twin.org/core";
4
+ import { JsonLdHelper, JsonLdProcessor } from "@twin.org/data-json-ld";
5
5
  import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
6
6
  import { DocumentHelper } from "@twin.org/identity-models";
7
- import { DidContexts, DidTypes, DidVerificationMethodType, ProofHelper, ProofTypes } from "@twin.org/standards-w3c-did";
7
+ import { DidContexts, DidTypes, DidVerificationMethodType, JwsAlgorithms, ProofHelper, ProofTypes } from "@twin.org/standards-w3c-did";
8
8
  import { VaultConnectorFactory, VaultConnectorHelper, VaultKeyType } from "@twin.org/vault-models";
9
9
  import { Jwk, Jwt } from "@twin.org/web";
10
10
  /**
@@ -147,9 +147,8 @@ export class EntityStorageIdentityConnector {
147
147
  if (Is.stringValue(verificationMethodId)) {
148
148
  // If there is a verification method id, we will try to get the key from the vault.
149
149
  try {
150
- const defaultMethodId = `${controller}/${verificationMethodId}`;
151
150
  // If there is an existing key, we will use it.
152
- const existingKey = await this._vaultConnector.getKey(defaultMethodId);
151
+ const existingKey = await this._vaultConnector.getKey(EntityStorageIdentityConnector.buildVaultKey(didDocument.id, verificationMethodId));
153
152
  methodKeyPublic = existingKey.publicKey;
154
153
  }
155
154
  catch { }
@@ -352,6 +351,81 @@ export class EntityStorageIdentityConnector {
352
351
  throw new GeneralError(EntityStorageIdentityConnector.CLASS_NAME, "removeServiceFailed", undefined, error);
353
352
  }
354
353
  }
354
+ /**
355
+ * Add an alias to the alsoKnownAs property on the document.
356
+ * If the alias is already present the operation is a no-op.
357
+ * @param controller The controller of the identity who can make changes.
358
+ * @param documentId The id of the document to update.
359
+ * @param alias The alias to add. Must be a Url or Urn (typically another DID).
360
+ * @returns Nothing.
361
+ * @throws GeneralError if the alias is not a Url or Urn.
362
+ * @throws NotFoundError if the id can not be resolved.
363
+ */
364
+ async addAlsoKnownAs(controller, documentId, alias) {
365
+ Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "controller", controller);
366
+ Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "documentId", documentId);
367
+ Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "alias", alias);
368
+ if (!Url.tryParseExact(alias) && !Urn.tryParseExact(alias)) {
369
+ throw new GeneralError(EntityStorageIdentityConnector.CLASS_NAME, "invalidAlias", { alias });
370
+ }
371
+ try {
372
+ const didIdentityDocument = await this._didDocumentEntityStorage.get(documentId);
373
+ if (Is.undefined(didIdentityDocument)) {
374
+ throw new NotFoundError(EntityStorageIdentityConnector.CLASS_NAME, "documentNotFound", documentId);
375
+ }
376
+ await EntityStorageIdentityConnector.verifyDocument(didIdentityDocument, this._vaultConnector);
377
+ const didDocument = didIdentityDocument.document;
378
+ const existing = Is.array(didDocument.alsoKnownAs) ? didDocument.alsoKnownAs : [];
379
+ if (existing.includes(alias)) {
380
+ return;
381
+ }
382
+ didDocument.alsoKnownAs = [...existing, alias];
383
+ await this.updateDocument(controller, didDocument);
384
+ }
385
+ catch (error) {
386
+ throw new GeneralError(EntityStorageIdentityConnector.CLASS_NAME, "addAlsoKnownAsFailed", undefined, error);
387
+ }
388
+ }
389
+ /**
390
+ * Remove an alias from the alsoKnownAs property on the document.
391
+ * If the alias is not present the operation is a no-op.
392
+ * @param controller The controller of the identity who can make changes.
393
+ * @param documentId The id of the document to update.
394
+ * @param alias The alias to remove. Must be a Url or Urn.
395
+ * @returns Nothing.
396
+ * @throws GeneralError if the alias is not a Url or Urn.
397
+ * @throws NotFoundError if the id can not be resolved.
398
+ */
399
+ async removeAlsoKnownAs(controller, documentId, alias) {
400
+ Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "controller", controller);
401
+ Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "documentId", documentId);
402
+ Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "alias", alias);
403
+ if (!Url.tryParseExact(alias) && !Urn.tryParseExact(alias)) {
404
+ throw new GeneralError(EntityStorageIdentityConnector.CLASS_NAME, "invalidAlias", { alias });
405
+ }
406
+ try {
407
+ const didIdentityDocument = await this._didDocumentEntityStorage.get(documentId);
408
+ if (Is.undefined(didIdentityDocument)) {
409
+ throw new NotFoundError(EntityStorageIdentityConnector.CLASS_NAME, "documentNotFound", documentId);
410
+ }
411
+ await EntityStorageIdentityConnector.verifyDocument(didIdentityDocument, this._vaultConnector);
412
+ const didDocument = didIdentityDocument.document;
413
+ if (!Is.array(didDocument.alsoKnownAs) || !didDocument.alsoKnownAs.includes(alias)) {
414
+ return;
415
+ }
416
+ const filtered = didDocument.alsoKnownAs.filter(a => a !== alias);
417
+ if (filtered.length === 0) {
418
+ delete didDocument.alsoKnownAs;
419
+ }
420
+ else {
421
+ didDocument.alsoKnownAs = filtered;
422
+ }
423
+ await this.updateDocument(controller, didDocument);
424
+ }
425
+ catch (error) {
426
+ throw new GeneralError(EntityStorageIdentityConnector.CLASS_NAME, "removeAlsoKnownAsFailed", undefined, error);
427
+ }
428
+ }
355
429
  /**
356
430
  * Create a verifiable credential for a verification method.
357
431
  * @param controller The controller of the identity who can make changes.
@@ -361,6 +435,8 @@ export class EntityStorageIdentityConnector {
361
435
  * @param options Additional options for creating the verifiable credential.
362
436
  * @param options.revocationIndex The bitmap revocation index of the credential, if undefined will not have revocation status.
363
437
  * @param options.expirationDate The date the verifiable credential is valid until.
438
+ * @param options.jwtHeaderFields Additional fields to add to the JWT header.
439
+ * @param options.jwtPayloadFields Additional fields to add to the JWT payload.
364
440
  * @returns The created verifiable credential and its token.
365
441
  * @throws NotFoundError if the id can not be resolved.
366
442
  */
@@ -371,6 +447,9 @@ export class EntityStorageIdentityConnector {
371
447
  if (!Is.undefined(options?.revocationIndex)) {
372
448
  Guards.number(EntityStorageIdentityConnector.CLASS_NAME, "options.revocationIndex", options.revocationIndex);
373
449
  }
450
+ if (!Is.undefined(options?.expirationDate)) {
451
+ Guards.date(EntityStorageIdentityConnector.CLASS_NAME, "options.expirationDate", options.expirationDate);
452
+ }
374
453
  try {
375
454
  const idParts = DocumentHelper.parseId(verificationMethodId);
376
455
  if (Is.empty(idParts.fragment)) {
@@ -412,7 +491,8 @@ export class EntityStorageIdentityConnector {
412
491
  finalTypes.push(credType);
413
492
  }
414
493
  const verifiableCredential = {
415
- "@context": JsonLdProcessor.combineContexts(DidContexts.ContextVCv1, credContext),
494
+ "@context": (JsonLdProcessor.combineContexts(DidContexts.ContextVCv1, credContext) ??
495
+ DidContexts.ContextVCv1),
416
496
  id,
417
497
  type: finalTypes,
418
498
  credentialSubject: subjectClone,
@@ -432,9 +512,10 @@ export class EntityStorageIdentityConnector {
432
512
  : undefined
433
513
  };
434
514
  const jwtHeader = {
515
+ ...options?.jwtHeaderFields,
435
516
  kid: verificationDidMethod.id,
436
517
  typ: "JWT",
437
- alg: "EdDSA"
518
+ alg: JwsAlgorithms.EdDSA
438
519
  };
439
520
  const jwtVc = ObjectHelper.pick(ObjectHelper.clone(verifiableCredential), [
440
521
  "@context",
@@ -442,6 +523,15 @@ export class EntityStorageIdentityConnector {
442
523
  "credentialSubject",
443
524
  "credentialStatus"
444
525
  ]);
526
+ // Add the proof to the VC after extracting the jwt data
527
+ // as the jwt does not include the proof
528
+ verifiableCredential.proof = await this.createProof(controller, verificationMethodId, ProofTypes.DataIntegrityProof, JsonLdHelper.toNodeObject(verifiableCredential));
529
+ // As we are adding the receipt to the data we update the JSON-LD context
530
+ const proofContext = verifiableCredential.proof["@context"];
531
+ if (!Is.empty(proofContext)) {
532
+ verifiableCredential["@context"] = (JsonLdProcessor.combineContexts(verifiableCredential["@context"], proofContext) ?? verifiableCredential["@context"]);
533
+ delete verifiableCredential.proof["@context"];
534
+ }
445
535
  if (Is.array(jwtVc.credentialSubject)) {
446
536
  jwtVc.credentialSubject = jwtVc.credentialSubject.map(c => {
447
537
  ObjectHelper.propertyDelete(c, "id");
@@ -452,12 +542,16 @@ export class EntityStorageIdentityConnector {
452
542
  ObjectHelper.propertyDelete(jwtVc.credentialSubject, "id");
453
543
  }
454
544
  const jwtPayload = {
545
+ ...options?.jwtPayloadFields,
455
546
  iss: idParts.id,
456
547
  nbf: Math.floor(Date.now() / 1000),
457
548
  jti: verifiableCredential.id,
458
549
  sub: credId,
459
550
  vc: jwtVc
460
551
  };
552
+ if (Is.date(options?.expirationDate)) {
553
+ jwtPayload.exp = Math.floor(options.expirationDate.getTime() / 1000);
554
+ }
461
555
  const signature = await Jwt.encodeWithSigner(jwtHeader, jwtPayload, async (header, payload) => VaultConnectorHelper.jwtSigner(this._vaultConnector, EntityStorageIdentityConnector.buildVaultKey(idParts.id, idParts.fragment ?? ""), header, payload));
462
556
  return {
463
557
  verifiableCredential,
@@ -470,13 +564,23 @@ export class EntityStorageIdentityConnector {
470
564
  }
471
565
  /**
472
566
  * Check a verifiable credential is valid.
473
- * @param credentialJwt The credential to verify.
567
+ * @param credential The credential to verify.
474
568
  * @returns The credential stored in the jwt and the revocation status.
475
569
  */
476
- async checkVerifiableCredential(credentialJwt) {
477
- Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "credentialJwt", credentialJwt);
570
+ async checkVerifiableCredential(credential) {
571
+ if (Is.object(credential)) {
572
+ Guards.objectValue(EntityStorageIdentityConnector.CLASS_NAME, "credential", credential);
573
+ Guards.objectValue(EntityStorageIdentityConnector.CLASS_NAME, "credential.proof", credential.proof);
574
+ const { proof, ...doc } = credential;
575
+ await this.verifyProof(JsonLdHelper.toNodeObject(doc), ArrayHelper.fromObjectOrArray(proof)[0]);
576
+ return {
577
+ revoked: false,
578
+ verifiableCredential: doc
579
+ };
580
+ }
581
+ Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "credential", credential);
478
582
  try {
479
- const jwtDecoded = await Jwt.decode(credentialJwt);
583
+ const jwtDecoded = await Jwt.decode(credential);
480
584
  const jwtHeader = jwtDecoded.header;
481
585
  const jwtPayload = jwtDecoded.payload;
482
586
  const jwtSignature = jwtDecoded.signature;
@@ -511,7 +615,7 @@ export class EntityStorageIdentityConnector {
511
615
  method: jwtHeader.kid
512
616
  });
513
617
  }
514
- await Jwt.verifySignature(credentialJwt, await Jwk.toCryptoKey(didMethod.publicKeyJwk));
618
+ await Jwt.verifySignature(credential, await Jwk.toCryptoKey(didMethod.publicKeyJwk));
515
619
  const verifiableCredential = jwtPayload.vc;
516
620
  if (Is.object(verifiableCredential)) {
517
621
  if (Is.string(jwtPayload.jti)) {
@@ -641,11 +745,14 @@ export class EntityStorageIdentityConnector {
641
745
  * @param contexts The contexts for the data stored in the verifiable credential.
642
746
  * @param types The types for the data stored in the verifiable credential.
643
747
  * @param verifiableCredentials The credentials to use for creating the presentation in jwt format.
644
- * @param expiresInMinutes The time in minutes for the presentation to expire.
748
+ * @param options Additional options for creating the verifiable presentation.
749
+ * @param options.expirationDate The date the verifiable presentation is valid until.
750
+ * @param options.jwtHeaderFields Additional fields to add to the JWT header.
751
+ * @param options.jwtPayloadFields Additional fields to add to the JWT payload.
645
752
  * @returns The created verifiable presentation and its token.
646
753
  * @throws NotFoundError if the id can not be resolved.
647
754
  */
648
- async createVerifiablePresentation(controller, verificationMethodId, presentationId, contexts, types, verifiableCredentials, expiresInMinutes) {
755
+ async createVerifiablePresentation(controller, verificationMethodId, presentationId, contexts, types, verifiableCredentials, options) {
649
756
  Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "controller", controller);
650
757
  Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "verificationMethodId", verificationMethodId);
651
758
  if (Is.array(types)) {
@@ -655,8 +762,8 @@ export class EntityStorageIdentityConnector {
655
762
  Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "types", types);
656
763
  }
657
764
  Guards.arrayValue(EntityStorageIdentityConnector.CLASS_NAME, "verifiableCredentials", verifiableCredentials);
658
- if (!Is.undefined(expiresInMinutes)) {
659
- Guards.integer(EntityStorageIdentityConnector.CLASS_NAME, "expiresInMinutes", expiresInMinutes);
765
+ if (!Is.undefined(options?.expirationDate)) {
766
+ Guards.date(EntityStorageIdentityConnector.CLASS_NAME, "options.expirationDate", options.expirationDate);
660
767
  }
661
768
  try {
662
769
  const idParts = DocumentHelper.parseId(verificationMethodId);
@@ -694,31 +801,37 @@ export class EntityStorageIdentityConnector {
694
801
  else if (Is.stringValue(types)) {
695
802
  finalTypes.push(types);
696
803
  }
804
+ const combinedContext = JsonLdProcessor.combineContexts(DidContexts.ContextVCv1, contexts) ??
805
+ DidContexts.ContextVCv1;
697
806
  const verifiablePresentation = {
698
- "@context": JsonLdProcessor.combineContexts(DidContexts.ContextVCv1, contexts),
807
+ "@context": combinedContext,
699
808
  id: presentationId,
700
809
  type: finalTypes,
701
810
  verifiableCredential: verifiableCredentials,
702
811
  holder: idParts.id
703
812
  };
704
813
  const jwtHeader = {
814
+ ...options?.jwtHeaderFields,
705
815
  kid: didMethod.id,
706
816
  typ: "JWT",
707
- alg: "EdDSA"
817
+ alg: JwsAlgorithms.EdDSA
708
818
  };
709
819
  const jwtVp = ObjectHelper.pick(ObjectHelper.clone(verifiablePresentation), [
710
820
  "@context",
711
821
  "type",
712
822
  "verifiableCredential"
713
823
  ]);
824
+ // Add the proof to the VP after extracting the jwt data
825
+ // as the jwt does not include the proof
826
+ verifiablePresentation.proof = await this.createProof(controller, verificationMethodId, ProofTypes.DataIntegrityProof, JsonLdHelper.toNodeObject(verifiablePresentation));
714
827
  const jwtPayload = {
715
- iss: idParts.id,
828
+ ...options?.jwtPayloadFields,
829
+ iss: verifiablePresentation.holder,
716
830
  nbf: Math.floor(Date.now() / 1000),
717
831
  vp: jwtVp
718
832
  };
719
- if (Is.integer(expiresInMinutes)) {
720
- const expiresInSeconds = expiresInMinutes * 60;
721
- jwtPayload.exp = Math.floor(Date.now() / 1000) + expiresInSeconds;
833
+ if (Is.date(options?.expirationDate)) {
834
+ jwtPayload.exp = Math.floor(options.expirationDate.getTime() / 1000);
722
835
  }
723
836
  const signature = await Jwt.encodeWithSigner(jwtHeader, jwtPayload, async (header, payload) => VaultConnectorHelper.jwtSigner(this._vaultConnector, EntityStorageIdentityConnector.buildVaultKey(idParts.id, idParts.fragment ?? ""), header, payload));
724
837
  return {
@@ -732,11 +845,19 @@ export class EntityStorageIdentityConnector {
732
845
  }
733
846
  /**
734
847
  * Check a verifiable presentation is valid.
735
- * @param presentationJwt The presentation to verify.
848
+ * @param presentation The presentation to verify.
736
849
  * @returns The presentation stored in the jwt and the revocation status.
737
850
  */
738
- async checkVerifiablePresentation(presentationJwt) {
739
- Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "presentationJwt", presentationJwt);
851
+ async checkVerifiablePresentation(presentation) {
852
+ if (Is.object(presentation)) {
853
+ const { proof, ...doc } = presentation;
854
+ const proofEntry = ArrayHelper.fromObjectOrArray(proof)[0];
855
+ Guards.objectValue(EntityStorageIdentityConnector.CLASS_NAME, "proofEntry", proofEntry);
856
+ await this.verifyProof(JsonLdHelper.toNodeObject(doc), proofEntry);
857
+ return { revoked: false, verifiablePresentation: doc };
858
+ }
859
+ Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "presentation", presentation);
860
+ const presentationJwt = presentation;
740
861
  try {
741
862
  const jwtDecoded = await Jwt.decode(presentationJwt);
742
863
  const jwtHeader = jwtDecoded.header;
@@ -821,11 +942,15 @@ export class EntityStorageIdentityConnector {
821
942
  }
822
943
  /**
823
944
  * Create a proof for arbitrary data with the specified verification method.
945
+ * This method uses async signing to ensure the private key never leaves the vault,
946
+ * with algorithm validation to ensure key type compatibility.
824
947
  * @param controller The controller of the identity who can make changes.
825
948
  * @param verificationMethodId The verification method id to use.
826
949
  * @param proofType The type of proof to create.
827
950
  * @param unsecureDocument The unsecure document to create the proof for.
828
951
  * @returns The proof.
952
+ * @throws NotFoundError if the identity or method is not found.
953
+ * @throws GeneralError if algorithm doesn't match key type or proof creation fails.
829
954
  */
830
955
  async createProof(controller, verificationMethodId, proofType, unsecureDocument) {
831
956
  Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "controller", controller);
@@ -862,8 +987,14 @@ export class EntityStorageIdentityConnector {
862
987
  });
863
988
  }
864
989
  const vaultKey = EntityStorageIdentityConnector.buildVaultKey(didDocument.id, idParts.fragment ?? "");
865
- const key = await this._vaultConnector.getKey(vaultKey);
866
- const signedProof = await ProofHelper.createProof(proofType, unsecureDocument, ProofHelper.createUnsignedProof(proofType, verificationMethodId), await Jwk.fromEd25519Private(key.privateKey));
990
+ const keyType = await this._vaultConnector.getKeyType(vaultKey);
991
+ if (Is.undefined(keyType)) {
992
+ throw new GeneralError(EntityStorageIdentityConnector.CLASS_NAME, "privateKeyMissing", {
993
+ keyId: vaultKey
994
+ });
995
+ }
996
+ const unsignedProof = ProofHelper.createUnsignedProof(proofType, verificationMethodId);
997
+ const signedProof = await ProofHelper.createProofWithSigner(proofType, unsecureDocument, unsignedProof, async (data, algorithm) => this.signWithVault(vaultKey, keyType, data, algorithm));
867
998
  return signedProof;
868
999
  }
869
1000
  catch (error) {
@@ -916,6 +1047,27 @@ export class EntityStorageIdentityConnector {
916
1047
  throw new GeneralError(EntityStorageIdentityConnector.CLASS_NAME, "verifyProofFailed", undefined, error);
917
1048
  }
918
1049
  }
1050
+ /**
1051
+ * Signs data using the vault connector with algorithm validation.
1052
+ * @param vaultKey The vault key identifier.
1053
+ * @param keyType The type of the key.
1054
+ * @param data The data to sign.
1055
+ * @param algorithm The signing algorithm.
1056
+ * @returns The signature bytes.
1057
+ * @throws GeneralError if algorithm doesn't match key type.
1058
+ * @internal
1059
+ */
1060
+ async signWithVault(vaultKey, keyType, data, algorithm) {
1061
+ if (algorithm === JwsAlgorithms.EdDSA && keyType !== VaultKeyType.Ed25519) {
1062
+ throw new GeneralError(EntityStorageIdentityConnector.CLASS_NAME, "algorithmKeyTypeMismatch", {
1063
+ algorithm,
1064
+ expectedKeyType: VaultKeyType.Ed25519,
1065
+ actualKeyType: keyType,
1066
+ keyId: vaultKey
1067
+ });
1068
+ }
1069
+ return this._vaultConnector.sign(vaultKey, data);
1070
+ }
919
1071
  /**
920
1072
  * Get all the methods from a document.
921
1073
  * @param document The document to get the methods from.