@pagopa/io-react-native-wallet 0.21.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
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 +4 -3
  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
+ };