@twin.org/identity-connector-entity-storage 0.0.3-next.2 → 0.0.3-next.21

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.
@@ -371,6 +445,9 @@ export class EntityStorageIdentityConnector {
371
445
  if (!Is.undefined(options?.revocationIndex)) {
372
446
  Guards.number(EntityStorageIdentityConnector.CLASS_NAME, "options.revocationIndex", options.revocationIndex);
373
447
  }
448
+ if (!Is.undefined(options?.expirationDate)) {
449
+ Guards.date(EntityStorageIdentityConnector.CLASS_NAME, "options.expirationDate", options.expirationDate);
450
+ }
374
451
  try {
375
452
  const idParts = DocumentHelper.parseId(verificationMethodId);
376
453
  if (Is.empty(idParts.fragment)) {
@@ -412,7 +489,8 @@ export class EntityStorageIdentityConnector {
412
489
  finalTypes.push(credType);
413
490
  }
414
491
  const verifiableCredential = {
415
- "@context": JsonLdProcessor.combineContexts(DidContexts.ContextVCv1, credContext),
492
+ "@context": (JsonLdProcessor.combineContexts(DidContexts.ContextVCv1, credContext) ??
493
+ DidContexts.ContextVCv1),
416
494
  id,
417
495
  type: finalTypes,
418
496
  credentialSubject: subjectClone,
@@ -434,7 +512,7 @@ export class EntityStorageIdentityConnector {
434
512
  const jwtHeader = {
435
513
  kid: verificationDidMethod.id,
436
514
  typ: "JWT",
437
- alg: "EdDSA"
515
+ alg: JwsAlgorithms.EdDSA
438
516
  };
439
517
  const jwtVc = ObjectHelper.pick(ObjectHelper.clone(verifiableCredential), [
440
518
  "@context",
@@ -442,6 +520,15 @@ export class EntityStorageIdentityConnector {
442
520
  "credentialSubject",
443
521
  "credentialStatus"
444
522
  ]);
523
+ // Add the proof to the VC after extracting the jwt data
524
+ // as the jwt does not include the proof
525
+ verifiableCredential.proof = await this.createProof(controller, verificationMethodId, ProofTypes.DataIntegrityProof, JsonLdHelper.toNodeObject(verifiableCredential));
526
+ // As we are adding the receipt to the data we update the JSON-LD context
527
+ const proofContext = verifiableCredential.proof["@context"];
528
+ if (!Is.empty(proofContext)) {
529
+ verifiableCredential["@context"] = (JsonLdProcessor.combineContexts(verifiableCredential["@context"], proofContext) ?? verifiableCredential["@context"]);
530
+ delete verifiableCredential.proof["@context"];
531
+ }
445
532
  if (Is.array(jwtVc.credentialSubject)) {
446
533
  jwtVc.credentialSubject = jwtVc.credentialSubject.map(c => {
447
534
  ObjectHelper.propertyDelete(c, "id");
@@ -458,6 +545,9 @@ export class EntityStorageIdentityConnector {
458
545
  sub: credId,
459
546
  vc: jwtVc
460
547
  };
548
+ if (Is.date(options?.expirationDate)) {
549
+ jwtPayload.exp = Math.floor(options.expirationDate.getTime() / 1000);
550
+ }
461
551
  const signature = await Jwt.encodeWithSigner(jwtHeader, jwtPayload, async (header, payload) => VaultConnectorHelper.jwtSigner(this._vaultConnector, EntityStorageIdentityConnector.buildVaultKey(idParts.id, idParts.fragment ?? ""), header, payload));
462
552
  return {
463
553
  verifiableCredential,
@@ -470,13 +560,23 @@ export class EntityStorageIdentityConnector {
470
560
  }
471
561
  /**
472
562
  * Check a verifiable credential is valid.
473
- * @param credentialJwt The credential to verify.
563
+ * @param credential The credential to verify.
474
564
  * @returns The credential stored in the jwt and the revocation status.
475
565
  */
476
- async checkVerifiableCredential(credentialJwt) {
477
- Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "credentialJwt", credentialJwt);
566
+ async checkVerifiableCredential(credential) {
567
+ if (Is.object(credential)) {
568
+ Guards.objectValue(EntityStorageIdentityConnector.CLASS_NAME, "credential", credential);
569
+ Guards.objectValue(EntityStorageIdentityConnector.CLASS_NAME, "credential.proof", credential.proof);
570
+ const { proof, ...doc } = credential;
571
+ await this.verifyProof(JsonLdHelper.toNodeObject(doc), ArrayHelper.fromObjectOrArray(proof)[0]);
572
+ return {
573
+ revoked: false,
574
+ verifiableCredential: doc
575
+ };
576
+ }
577
+ Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "credential", credential);
478
578
  try {
479
- const jwtDecoded = await Jwt.decode(credentialJwt);
579
+ const jwtDecoded = await Jwt.decode(credential);
480
580
  const jwtHeader = jwtDecoded.header;
481
581
  const jwtPayload = jwtDecoded.payload;
482
582
  const jwtSignature = jwtDecoded.signature;
@@ -511,7 +611,7 @@ export class EntityStorageIdentityConnector {
511
611
  method: jwtHeader.kid
512
612
  });
513
613
  }
514
- await Jwt.verifySignature(credentialJwt, await Jwk.toCryptoKey(didMethod.publicKeyJwk));
614
+ await Jwt.verifySignature(credential, await Jwk.toCryptoKey(didMethod.publicKeyJwk));
515
615
  const verifiableCredential = jwtPayload.vc;
516
616
  if (Is.object(verifiableCredential)) {
517
617
  if (Is.string(jwtPayload.jti)) {
@@ -641,11 +741,12 @@ export class EntityStorageIdentityConnector {
641
741
  * @param contexts The contexts for the data stored in the verifiable credential.
642
742
  * @param types The types for the data stored in the verifiable credential.
643
743
  * @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.
744
+ * @param options Additional options for creating the verifiable presentation.
745
+ * @param options.expirationDate The date the verifiable presentation is valid until.
645
746
  * @returns The created verifiable presentation and its token.
646
747
  * @throws NotFoundError if the id can not be resolved.
647
748
  */
648
- async createVerifiablePresentation(controller, verificationMethodId, presentationId, contexts, types, verifiableCredentials, expiresInMinutes) {
749
+ async createVerifiablePresentation(controller, verificationMethodId, presentationId, contexts, types, verifiableCredentials, options) {
649
750
  Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "controller", controller);
650
751
  Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "verificationMethodId", verificationMethodId);
651
752
  if (Is.array(types)) {
@@ -655,8 +756,8 @@ export class EntityStorageIdentityConnector {
655
756
  Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "types", types);
656
757
  }
657
758
  Guards.arrayValue(EntityStorageIdentityConnector.CLASS_NAME, "verifiableCredentials", verifiableCredentials);
658
- if (!Is.undefined(expiresInMinutes)) {
659
- Guards.integer(EntityStorageIdentityConnector.CLASS_NAME, "expiresInMinutes", expiresInMinutes);
759
+ if (!Is.undefined(options?.expirationDate)) {
760
+ Guards.date(EntityStorageIdentityConnector.CLASS_NAME, "options.expirationDate", options.expirationDate);
660
761
  }
661
762
  try {
662
763
  const idParts = DocumentHelper.parseId(verificationMethodId);
@@ -694,8 +795,10 @@ export class EntityStorageIdentityConnector {
694
795
  else if (Is.stringValue(types)) {
695
796
  finalTypes.push(types);
696
797
  }
798
+ const combinedContext = JsonLdProcessor.combineContexts(DidContexts.ContextVCv1, contexts) ??
799
+ DidContexts.ContextVCv1;
697
800
  const verifiablePresentation = {
698
- "@context": JsonLdProcessor.combineContexts(DidContexts.ContextVCv1, contexts),
801
+ "@context": combinedContext,
699
802
  id: presentationId,
700
803
  type: finalTypes,
701
804
  verifiableCredential: verifiableCredentials,
@@ -704,21 +807,23 @@ export class EntityStorageIdentityConnector {
704
807
  const jwtHeader = {
705
808
  kid: didMethod.id,
706
809
  typ: "JWT",
707
- alg: "EdDSA"
810
+ alg: JwsAlgorithms.EdDSA
708
811
  };
709
812
  const jwtVp = ObjectHelper.pick(ObjectHelper.clone(verifiablePresentation), [
710
813
  "@context",
711
814
  "type",
712
815
  "verifiableCredential"
713
816
  ]);
817
+ // Add the proof to the VP after extracting the jwt data
818
+ // as the jwt does not include the proof
819
+ verifiablePresentation.proof = await this.createProof(controller, verificationMethodId, ProofTypes.DataIntegrityProof, JsonLdHelper.toNodeObject(verifiablePresentation));
714
820
  const jwtPayload = {
715
- iss: idParts.id,
821
+ iss: verifiablePresentation.holder,
716
822
  nbf: Math.floor(Date.now() / 1000),
717
823
  vp: jwtVp
718
824
  };
719
- if (Is.integer(expiresInMinutes)) {
720
- const expiresInSeconds = expiresInMinutes * 60;
721
- jwtPayload.exp = Math.floor(Date.now() / 1000) + expiresInSeconds;
825
+ if (Is.date(options?.expirationDate)) {
826
+ jwtPayload.exp = Math.floor(options.expirationDate.getTime() / 1000);
722
827
  }
723
828
  const signature = await Jwt.encodeWithSigner(jwtHeader, jwtPayload, async (header, payload) => VaultConnectorHelper.jwtSigner(this._vaultConnector, EntityStorageIdentityConnector.buildVaultKey(idParts.id, idParts.fragment ?? ""), header, payload));
724
829
  return {
@@ -732,11 +837,19 @@ export class EntityStorageIdentityConnector {
732
837
  }
733
838
  /**
734
839
  * Check a verifiable presentation is valid.
735
- * @param presentationJwt The presentation to verify.
840
+ * @param presentation The presentation to verify.
736
841
  * @returns The presentation stored in the jwt and the revocation status.
737
842
  */
738
- async checkVerifiablePresentation(presentationJwt) {
739
- Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "presentationJwt", presentationJwt);
843
+ async checkVerifiablePresentation(presentation) {
844
+ if (Is.object(presentation)) {
845
+ const { proof, ...doc } = presentation;
846
+ const proofEntry = ArrayHelper.fromObjectOrArray(proof)[0];
847
+ Guards.objectValue(EntityStorageIdentityConnector.CLASS_NAME, "proofEntry", proofEntry);
848
+ await this.verifyProof(JsonLdHelper.toNodeObject(doc), proofEntry);
849
+ return { revoked: false, verifiablePresentation: doc };
850
+ }
851
+ Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "presentation", presentation);
852
+ const presentationJwt = presentation;
740
853
  try {
741
854
  const jwtDecoded = await Jwt.decode(presentationJwt);
742
855
  const jwtHeader = jwtDecoded.header;
@@ -821,11 +934,15 @@ export class EntityStorageIdentityConnector {
821
934
  }
822
935
  /**
823
936
  * Create a proof for arbitrary data with the specified verification method.
937
+ * This method uses async signing to ensure the private key never leaves the vault,
938
+ * with algorithm validation to ensure key type compatibility.
824
939
  * @param controller The controller of the identity who can make changes.
825
940
  * @param verificationMethodId The verification method id to use.
826
941
  * @param proofType The type of proof to create.
827
942
  * @param unsecureDocument The unsecure document to create the proof for.
828
943
  * @returns The proof.
944
+ * @throws NotFoundError if the identity or method is not found.
945
+ * @throws GeneralError if algorithm doesn't match key type or proof creation fails.
829
946
  */
830
947
  async createProof(controller, verificationMethodId, proofType, unsecureDocument) {
831
948
  Guards.stringValue(EntityStorageIdentityConnector.CLASS_NAME, "controller", controller);
@@ -862,8 +979,14 @@ export class EntityStorageIdentityConnector {
862
979
  });
863
980
  }
864
981
  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));
982
+ const keyType = await this._vaultConnector.getKeyType(vaultKey);
983
+ if (Is.undefined(keyType)) {
984
+ throw new GeneralError(EntityStorageIdentityConnector.CLASS_NAME, "privateKeyMissing", {
985
+ keyId: vaultKey
986
+ });
987
+ }
988
+ const unsignedProof = ProofHelper.createUnsignedProof(proofType, verificationMethodId);
989
+ const signedProof = await ProofHelper.createProofWithSigner(proofType, unsecureDocument, unsignedProof, async (data, algorithm) => this.signWithVault(vaultKey, keyType, data, algorithm));
867
990
  return signedProof;
868
991
  }
869
992
  catch (error) {
@@ -916,6 +1039,27 @@ export class EntityStorageIdentityConnector {
916
1039
  throw new GeneralError(EntityStorageIdentityConnector.CLASS_NAME, "verifyProofFailed", undefined, error);
917
1040
  }
918
1041
  }
1042
+ /**
1043
+ * Signs data using the vault connector with algorithm validation.
1044
+ * @param vaultKey The vault key identifier.
1045
+ * @param keyType The type of the key.
1046
+ * @param data The data to sign.
1047
+ * @param algorithm The signing algorithm.
1048
+ * @returns The signature bytes.
1049
+ * @throws GeneralError if algorithm doesn't match key type.
1050
+ * @internal
1051
+ */
1052
+ async signWithVault(vaultKey, keyType, data, algorithm) {
1053
+ if (algorithm === JwsAlgorithms.EdDSA && keyType !== VaultKeyType.Ed25519) {
1054
+ throw new GeneralError(EntityStorageIdentityConnector.CLASS_NAME, "algorithmKeyTypeMismatch", {
1055
+ algorithm,
1056
+ expectedKeyType: VaultKeyType.Ed25519,
1057
+ actualKeyType: keyType,
1058
+ keyId: vaultKey
1059
+ });
1060
+ }
1061
+ return this._vaultConnector.sign(vaultKey, data);
1062
+ }
919
1063
  /**
920
1064
  * Get all the methods from a document.
921
1065
  * @param document The document to get the methods from.