@pagopa/io-react-native-wallet 0.21.0 → 0.23.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.
Files changed (63) hide show
  1. package/lib/commonjs/credential/issuance/06-obtain-credential.js +5 -4
  2. package/lib/commonjs/credential/issuance/06-obtain-credential.js.map +1 -1
  3. package/lib/commonjs/credential/issuance/README.md +7 -5
  4. package/lib/commonjs/credential/issuance/types.js +5 -1
  5. package/lib/commonjs/credential/issuance/types.js.map +1 -1
  6. package/lib/commonjs/credential/status/02-status-attestation.js +4 -3
  7. package/lib/commonjs/credential/status/02-status-attestation.js.map +1 -1
  8. package/lib/commonjs/credential/status/README.md +1 -1
  9. package/lib/commonjs/credential/status/types.js +14 -1
  10. package/lib/commonjs/credential/status/types.js.map +1 -1
  11. package/lib/commonjs/sd-jwt/index.js +3 -1
  12. package/lib/commonjs/sd-jwt/index.js.map +1 -1
  13. package/lib/commonjs/trust/types.js +9 -1
  14. package/lib/commonjs/trust/types.js.map +1 -1
  15. package/lib/commonjs/utils/errors.js +57 -29
  16. package/lib/commonjs/utils/errors.js.map +1 -1
  17. package/lib/commonjs/utils/misc.js +11 -2
  18. package/lib/commonjs/utils/misc.js.map +1 -1
  19. package/lib/module/credential/issuance/06-obtain-credential.js +8 -7
  20. package/lib/module/credential/issuance/06-obtain-credential.js.map +1 -1
  21. package/lib/module/credential/issuance/README.md +7 -5
  22. package/lib/module/credential/issuance/types.js +3 -0
  23. package/lib/module/credential/issuance/types.js.map +1 -1
  24. package/lib/module/credential/status/02-status-attestation.js +7 -6
  25. package/lib/module/credential/status/02-status-attestation.js.map +1 -1
  26. package/lib/module/credential/status/README.md +1 -1
  27. package/lib/module/credential/status/types.js +12 -0
  28. package/lib/module/credential/status/types.js.map +1 -1
  29. package/lib/module/sd-jwt/index.js +3 -2
  30. package/lib/module/sd-jwt/index.js.map +1 -1
  31. package/lib/module/trust/types.js +9 -1
  32. package/lib/module/trust/types.js.map +1 -1
  33. package/lib/module/utils/errors.js +52 -25
  34. package/lib/module/utils/errors.js.map +1 -1
  35. package/lib/module/utils/misc.js +9 -1
  36. package/lib/module/utils/misc.js.map +1 -1
  37. package/lib/typescript/credential/issuance/06-obtain-credential.d.ts.map +1 -1
  38. package/lib/typescript/credential/issuance/types.d.ts +8 -0
  39. package/lib/typescript/credential/issuance/types.d.ts.map +1 -1
  40. package/lib/typescript/credential/status/02-status-attestation.d.ts +1 -1
  41. package/lib/typescript/credential/status/02-status-attestation.d.ts.map +1 -1
  42. package/lib/typescript/credential/status/types.d.ts +15 -0
  43. package/lib/typescript/credential/status/types.d.ts.map +1 -1
  44. package/lib/typescript/sd-jwt/index.d.ts.map +1 -1
  45. package/lib/typescript/trust/index.d.ts +14 -0
  46. package/lib/typescript/trust/index.d.ts.map +1 -1
  47. package/lib/typescript/trust/types.d.ts +194 -0
  48. package/lib/typescript/trust/types.d.ts.map +1 -1
  49. package/lib/typescript/utils/errors.d.ts +31 -14
  50. package/lib/typescript/utils/errors.d.ts.map +1 -1
  51. package/lib/typescript/utils/misc.d.ts +1 -0
  52. package/lib/typescript/utils/misc.d.ts.map +1 -1
  53. package/package.json +5 -4
  54. package/src/credential/issuance/06-obtain-credential.ts +11 -7
  55. package/src/credential/issuance/README.md +7 -5
  56. package/src/credential/issuance/types.ts +8 -0
  57. package/src/credential/status/02-status-attestation.ts +13 -5
  58. package/src/credential/status/README.md +1 -1
  59. package/src/credential/status/types.ts +15 -0
  60. package/src/sd-jwt/index.ts +3 -3
  61. package/src/trust/types.ts +12 -0
  62. package/src/utils/errors.ts +72 -26
  63. package/src/utils/misc.ts +12 -4
@@ -5,16 +5,16 @@ import {
5
5
  } from "@pagopa/io-react-native-jwt";
6
6
  import type { AuthorizeAccess } from "./05-authorize-access";
7
7
  import type { EvaluateIssuerTrust } from "./02-evaluate-issuer-trust";
8
- import { hasStatus, type Out } from "../../utils/misc";
8
+ import { hasStatus, safeJsonParse, type Out } from "../../utils/misc";
9
9
  import type { StartUserAuthorization } from "./03-start-user-authorization";
10
10
  import {
11
+ CredentialInvalidStatusError,
11
12
  CredentialIssuingNotSynchronousError,
12
- CredentialNotEntitledError,
13
13
  CredentialRequestError,
14
14
  UnexpectedStatusCodeError,
15
15
  ValidationFailed,
16
16
  } from "../../utils/errors";
17
- import { CredentialResponse } from "./types";
17
+ import { CredentialIssuanceFailureResponse, CredentialResponse } from "./types";
18
18
 
19
19
  import { createDPopToken } from "../../utils/dpop";
20
20
  import uuid from "react-native-uuid";
@@ -157,8 +157,8 @@ export const obtainCredential: ObtainCredential = async (
157
157
  * Handle the credential error by mapping it to a custom exception.
158
158
  * If the error is not an instance of {@link UnexpectedStatusCodeError}, it is thrown as is.
159
159
  * @param e - The error to be handled
160
- * @throws {@link StatusAttestationError} if the status code is different from 404
161
- * @throws {@link StatusAttestationInvalid} if the status code is 404 (meaning the credential is invalid)
160
+ * @throws {@link CredentialRequestError} if the status code is different from 404
161
+ * @throws {@link CredentialInvalidStatusError} if the status code is 404 (meaning the credential is invalid)
162
162
  */
163
163
  const handleObtainCredentialError = (e: unknown) => {
164
164
  if (!(e instanceof UnexpectedStatusCodeError)) {
@@ -174,9 +174,13 @@ const handleObtainCredentialError = (e: unknown) => {
174
174
  );
175
175
  }
176
176
 
177
- if (e.statusCode === 404) {
178
- throw new CredentialNotEntitledError(
177
+ if ([403, 404].includes(e.statusCode)) {
178
+ const maybeError = CredentialIssuanceFailureResponse.safeParse(
179
+ safeJsonParse(e.responseBody)
180
+ );
181
+ throw new CredentialInvalidStatusError(
179
182
  "Invalid status found for the given credential",
183
+ maybeError.success ? maybeError.data.error : "unknown",
180
184
  e.message
181
185
  );
182
186
  }
@@ -43,14 +43,16 @@ graph TD;
43
43
 
44
44
  A `201 Created` response is returned by the credential issuer when the request has been queued because the credential cannot be issued synchronously. The consumer should try to obtain the credential at a later time.
45
45
 
46
- ### 404 Not Found (CredentialNotEntitledError)
46
+ Although `201 Created` is not considered an error, it is mapped as an error in this context in order to handle the case where the credential issuance is not synchronous.
47
+ This allows keeping the flow consistent and handle the case where the credential is not immediately available.
47
48
 
48
- A `404 Not Found` response is returned by the credential issuer when the authenticated user is not entitled to receive the requested credential.
49
+ ### 403 Forbidden (CredentialInvalidStatusError)
49
50
 
50
- ### 201 Created (CredentialIssuingNotSynchronousError)
51
+ A `403 Forbidden` response is returned by the credential issuer when the requested credential has an invalid status. It might contain more details in the `errorCode` property.
51
52
 
52
- Although `201 Created` is not considered an error, it is mapped as an error in this context in order to handle the case where the credential issuance is not synchronous.
53
- This allows keeping the flow consistent and handle the case where the credential is not immediately available.
53
+ ### 404 Not Found (CredentialInvalidStatusError)
54
+
55
+ A `404 Not Found` response is returned by the credential issuer when the authenticated user is not entitled to receive the requested credential. It might contain more details in the `errorCode` property.
54
56
 
55
57
  ## Strong authentication for eID issuance (Query Mode)
56
58
 
@@ -30,3 +30,11 @@ export const ResponseUriResultShape = z.object({
30
30
  });
31
31
 
32
32
  export type ResponseMode = "query" | "form_post.jwt";
33
+
34
+ export const CredentialIssuanceFailureResponse = z.object({
35
+ error: z.string(),
36
+ });
37
+
38
+ export type CredentialIssuanceFailureResponse = z.infer<
39
+ typeof CredentialIssuanceFailureResponse
40
+ >;
@@ -1,15 +1,19 @@
1
1
  import {
2
2
  getCredentialHashWithouDiscloures,
3
3
  hasStatus,
4
+ safeJsonParse,
4
5
  type Out,
5
6
  } from "../../utils/misc";
6
7
  import type { EvaluateIssuerTrust, ObtainCredential } from "../issuance";
7
8
  import { SignJWT, type CryptoContext } from "@pagopa/io-react-native-jwt";
8
9
  import uuid from "react-native-uuid";
9
- import { StatusAttestationResponse } from "./types";
10
+ import {
11
+ InvalidStatusAttestationResponse,
12
+ StatusAttestationResponse,
13
+ } from "./types";
10
14
  import {
11
15
  StatusAttestationError,
12
- StatusAttestationInvalid,
16
+ CredentialInvalidStatusError,
13
17
  UnexpectedStatusCodeError,
14
18
  } from "../../utils/errors";
15
19
 
@@ -29,7 +33,7 @@ export type StatusAttestation = (
29
33
  * @param credential - The credential to be verified
30
34
  * @param credentialCryptoContext - The credential's crypto context
31
35
  * @param context.appFetch (optional) fetch api implementation. Default: built-in fetch
32
- * @throws {@link StatusAttestationInvalid} if the status attestation is invalid and thus the credential is not valid
36
+ * @throws {@link CredentialInvalidStatusError} if the status attestation is invalid and thus the credential is not valid
33
37
  * @throws {@link StatusAttestationError} if an error occurs during the status attestation
34
38
  * @returns The credential status attestation
35
39
  */
@@ -83,7 +87,7 @@ export const statusAttestation: StatusAttestation = async (
83
87
  * If the error is not an instance of {@link UnexpectedStatusCodeError}, it is thrown as is.
84
88
  * @param e - The error to be handled
85
89
  * @throws {@link StatusAttestationError} if the status code is different from 404
86
- * @throws {@link StatusAttestationInvalid} if the status code is 404 (meaning the credential is invalid)
90
+ * @throws {@link CredentialInvalidStatusError} if the status code is 404 (meaning the credential is invalid)
87
91
  */
88
92
  const handleStatusAttestationError = (e: unknown) => {
89
93
  if (!(e instanceof UnexpectedStatusCodeError)) {
@@ -91,8 +95,12 @@ const handleStatusAttestationError = (e: unknown) => {
91
95
  }
92
96
 
93
97
  if (e.statusCode === 404) {
94
- throw new StatusAttestationInvalid(
98
+ const maybeError = InvalidStatusAttestationResponse.safeParse(
99
+ safeJsonParse(e.responseBody)
100
+ );
101
+ throw new CredentialInvalidStatusError(
95
102
  "Invalid status found for the given credential",
103
+ maybeError.success ? maybeError.data.error : "unknown",
96
104
  e.message
97
105
  );
98
106
  }
@@ -18,7 +18,7 @@ graph TD;
18
18
 
19
19
  ## Mapped results
20
20
 
21
- ### 404 Not Found (StatusAttestationInvalid)
21
+ ### 404 Not Found (CredentialInvalidStatusError)
22
22
 
23
23
  A `404 Not Found` response is returned by the credential issuer when the status attestation is invalid.
24
24
 
@@ -41,3 +41,18 @@ export const ParsedStatusAttestation = z.object({
41
41
  iat: UnixTime,
42
42
  }),
43
43
  });
44
+
45
+ /**
46
+ * Shape from parsing a status attestation response in case of error.
47
+ */
48
+ export const InvalidStatusAttestationResponse = z.object({
49
+ error: z.string(),
50
+ });
51
+
52
+ /**
53
+ * Type from parsing a status attestation response in case of error.
54
+ * Inferred from {@link InvalidStatusAttestationResponse}.
55
+ */
56
+ export type InvalidStatusAttestationResponse = z.infer<
57
+ typeof InvalidStatusAttestationResponse
58
+ >;
@@ -3,8 +3,6 @@ import { z } from "zod";
3
3
  import { decode as decodeJwt } from "@pagopa/io-react-native-jwt";
4
4
  import { verify as verifyJwt } from "@pagopa/io-react-native-jwt";
5
5
  import { sha256ToBase64 } from "@pagopa/io-react-native-jwt";
6
-
7
- import { decodeBase64 } from "@pagopa/io-react-native-jwt";
8
6
  import { Disclosure, SdJwt4VC, type DisclosureWithEncoded } from "./types";
9
7
  import { verifyDisclosure } from "./verifier";
10
8
  import type { JWK } from "../utils/jwk";
@@ -12,9 +10,11 @@ import {
12
10
  ClaimsNotFoundBetweenDislosures,
13
11
  ClaimsNotFoundInToken,
14
12
  } from "../utils/errors";
13
+ import { Base64 } from "js-base64";
15
14
 
16
15
  const decodeDisclosure = (encoded: string): DisclosureWithEncoded => {
17
- const decoded = Disclosure.parse(JSON.parse(decodeBase64(encoded)));
16
+ const utf8String = Base64.decode(encoded); // Decode Base64 into UTF-8 string
17
+ const decoded = Disclosure.parse(JSON.parse(utf8String));
18
18
  return { decoded, encoded };
19
19
  };
20
20
 
@@ -53,6 +53,17 @@ const ClaimsMetadata = z.record(
53
53
  })
54
54
  );
55
55
 
56
+ type IssuanceErrorSupported = z.infer<typeof IssuanceErrorSupported>;
57
+ const IssuanceErrorSupported = z.object({
58
+ display: z.array(
59
+ z.object({
60
+ title: z.string(),
61
+ description: z.string(),
62
+ locale: z.string(),
63
+ })
64
+ ),
65
+ });
66
+
56
67
  // Metadata for a credentia which is supported by a Issuer
57
68
  type SupportedCredentialMetadata = z.infer<typeof SupportedCredentialMetadata>;
58
69
  const SupportedCredentialMetadata = z.object({
@@ -63,6 +74,7 @@ const SupportedCredentialMetadata = z.object({
63
74
  cryptographic_binding_methods_supported: z.array(z.string()),
64
75
  credential_signing_alg_values_supported: z.array(z.string()),
65
76
  authentic_source: z.string().optional(),
77
+ issuance_errors_supported: z.record(IssuanceErrorSupported).optional(),
66
78
  });
67
79
 
68
80
  export type EntityStatement = z.infer<typeof EntityStatement>;
@@ -1,3 +1,5 @@
1
+ import type { CredentialIssuerEntityConfiguration } from "../trust/types";
2
+
1
3
  /**
2
4
  * utility to format a set of attributes into an error message string
3
5
  *
@@ -56,8 +58,10 @@ export class UnexpectedStatusCodeError extends IoWalletError {
56
58
 
57
59
  /** HTTP status code */
58
60
  statusCode: number;
61
+ /** The stringified response body, useful to process the error response */
62
+ responseBody: string;
59
63
 
60
- constructor(message: string, statusCode: number) {
64
+ constructor(message: string, statusCode: number, responseBody: string) {
61
65
  super(
62
66
  serializeAttrs({
63
67
  message,
@@ -65,6 +69,7 @@ export class UnexpectedStatusCodeError extends IoWalletError {
65
69
  })
66
70
  );
67
71
  this.statusCode = statusCode;
72
+ this.responseBody = responseBody;
68
73
  }
69
74
  }
70
75
  /**
@@ -461,19 +466,28 @@ export class OperationAbortedError extends IoWalletError {
461
466
  }
462
467
 
463
468
  /**
464
- * Error subclass thrown when the status attestation for a credential is invalid.
469
+ * Error subclass thrown when a credential status is invalid, either during issuance or when requesting a status attestation.
465
470
  */
466
- export class StatusAttestationInvalid extends IoWalletError {
467
- static get code(): "ERR_STATUS_ATTESTATION_INVALID" {
468
- return "ERR_STATUS_ATTESTATION_INVALID";
471
+ export class CredentialInvalidStatusError extends IoWalletError {
472
+ static get code(): "ERR_CREDENTIAL_INVALID_STATUS" {
473
+ return "ERR_CREDENTIAL_INVALID_STATUS";
469
474
  }
470
475
 
471
- code = "ERR_STATUS_ATTESTATION_INVALID";
476
+ code = "ERR_CREDENTIAL_INVALID_STATUS";
472
477
 
478
+ /**
479
+ * The error code that should be mapped with one of the `issuance_errors_supported` in the EC.
480
+ */
481
+ errorCode: string;
473
482
  reason: string;
474
483
 
475
- constructor(message: string, reason: string = "unspecified") {
476
- super(serializeAttrs({ message, reason }));
484
+ constructor(
485
+ message: string,
486
+ errorCode: string,
487
+ reason: string = "unspecified"
488
+ ) {
489
+ super(serializeAttrs({ message, errorCode, reason }));
490
+ this.errorCode = errorCode;
477
491
  this.reason = reason;
478
492
  }
479
493
  }
@@ -496,24 +510,6 @@ export class StatusAttestationError extends IoWalletError {
496
510
  }
497
511
  }
498
512
 
499
- /**
500
- * Error subclass thrown when the the user is not entitled to receive the requested credential.
501
- */
502
- export class CredentialNotEntitledError extends IoWalletError {
503
- static get code(): "CREDENTIAL_NOT_ENTITLED_ERROR" {
504
- return "CREDENTIAL_NOT_ENTITLED_ERROR";
505
- }
506
-
507
- code = "CREDENTIAL_NOT_ENTITLED_ERROR";
508
-
509
- reason: string;
510
-
511
- constructor(message: string, reason: string = "unspecified") {
512
- super(serializeAttrs({ message, reason }));
513
- this.reason = reason;
514
- }
515
- }
516
-
517
513
  /**
518
514
  * Error subclass thrown when an error occurs while requesting a credential.
519
515
  */
@@ -549,3 +545,53 @@ export class CredentialIssuingNotSynchronousError extends IoWalletError {
549
545
  this.reason = reason;
550
546
  }
551
547
  }
548
+
549
+ type LocalizedIssuanceError = {
550
+ [locale: string]: {
551
+ title: string;
552
+ description: string;
553
+ };
554
+ };
555
+
556
+ /**
557
+ * Function to extract the error message from the Entity Configuration's supported error codes.
558
+ * @param errorCode The error code to map to a meaningful message
559
+ * @param params.issuerConf The entity configuration for credentials
560
+ * @param params.credentialType The type of credential the error belongs to
561
+ * @returns A localized error {@link LocalizedIssuanceError} or undefined
562
+ * @throws {Error} When no credential config is found
563
+ */
564
+ export function extractErrorMessageFromIssuerConf(
565
+ errorCode: string,
566
+ {
567
+ issuerConf,
568
+ credentialType,
569
+ }: {
570
+ issuerConf: CredentialIssuerEntityConfiguration["payload"]["metadata"];
571
+ credentialType: string;
572
+ }
573
+ ): LocalizedIssuanceError | undefined {
574
+ const credentialConfiguration =
575
+ issuerConf.openid_credential_issuer.credential_configurations_supported[
576
+ credentialType
577
+ ];
578
+
579
+ if (!credentialConfiguration) {
580
+ throw new Error(
581
+ `No configuration found for ${credentialType} in the provided EC`
582
+ );
583
+ }
584
+
585
+ const { issuance_errors_supported } = credentialConfiguration;
586
+
587
+ if (!issuance_errors_supported?.[errorCode]) {
588
+ return undefined;
589
+ }
590
+
591
+ const localesList = issuance_errors_supported[errorCode]!.display;
592
+
593
+ return localesList.reduce(
594
+ (acc, { locale, ...rest }) => ({ ...acc, [locale]: rest }),
595
+ {} as LocalizedIssuanceError
596
+ );
597
+ }
package/src/utils/misc.ts CHANGED
@@ -11,11 +11,11 @@ export const hasStatus =
11
11
  (status: number) =>
12
12
  async (res: Response): Promise<Response> => {
13
13
  if (res.status !== status) {
14
+ const responseBody = await res.text();
14
15
  throw new UnexpectedStatusCodeError(
15
- `Http request failed. Expected ${status}, got ${res.status}, url: ${
16
- res.url
17
- } with response: ${await res.text()}`,
18
- res.status
16
+ `Http request failed. Expected ${status}, got ${res.status}, url: ${res.url} with response: ${responseBody}`,
17
+ res.status,
18
+ responseBody
19
19
  );
20
20
  }
21
21
  return res;
@@ -109,3 +109,11 @@ export const createAbortPromiseFromSignal = (signal: AbortSignal) => {
109
109
 
110
110
  export const isDefined = <T>(x: T | undefined | null | ""): x is T =>
111
111
  Boolean(x);
112
+
113
+ export const safeJsonParse = <T>(text: string, withDefault?: T): T | null => {
114
+ try {
115
+ return JSON.parse(text);
116
+ } catch (_) {
117
+ return withDefault ?? null;
118
+ }
119
+ };