@pagopa/io-react-native-wallet 2.0.0-next.8 → 2.0.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 (54) hide show
  1. package/README.md +3 -3
  2. package/lib/commonjs/credential/issuance/07-verify-and-parse-credential.js +148 -86
  3. package/lib/commonjs/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
  4. package/lib/commonjs/credential/presentation/07-evaluate-dcql-query.js +3 -2
  5. package/lib/commonjs/credential/presentation/07-evaluate-dcql-query.js.map +1 -1
  6. package/lib/commonjs/credential/status/02-status-assertion.js.map +1 -1
  7. package/lib/commonjs/credential/status/03-verify-and-parse-status-assertion.js.map +1 -1
  8. package/lib/commonjs/sd-jwt/types.js +11 -4
  9. package/lib/commonjs/sd-jwt/types.js.map +1 -1
  10. package/lib/commonjs/trust/types.js +6 -0
  11. package/lib/commonjs/trust/types.js.map +1 -1
  12. package/lib/commonjs/utils/credentials.js +2 -1
  13. package/lib/commonjs/utils/credentials.js.map +1 -1
  14. package/lib/commonjs/utils/misc.js +3 -1
  15. package/lib/commonjs/utils/misc.js.map +1 -1
  16. package/lib/module/credential/issuance/07-verify-and-parse-credential.js +148 -86
  17. package/lib/module/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
  18. package/lib/module/credential/presentation/07-evaluate-dcql-query.js +3 -2
  19. package/lib/module/credential/presentation/07-evaluate-dcql-query.js.map +1 -1
  20. package/lib/module/credential/status/02-status-assertion.js.map +1 -1
  21. package/lib/module/credential/status/03-verify-and-parse-status-assertion.js.map +1 -1
  22. package/lib/module/sd-jwt/types.js +9 -2
  23. package/lib/module/sd-jwt/types.js.map +1 -1
  24. package/lib/module/trust/types.js +6 -0
  25. package/lib/module/trust/types.js.map +1 -1
  26. package/lib/module/utils/credentials.js +2 -1
  27. package/lib/module/utils/credentials.js.map +1 -1
  28. package/lib/module/utils/misc.js +1 -0
  29. package/lib/module/utils/misc.js.map +1 -1
  30. package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts +1 -1
  31. package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts.map +1 -1
  32. package/lib/typescript/credential/presentation/07-evaluate-dcql-query.d.ts +2 -2
  33. package/lib/typescript/credential/presentation/07-evaluate-dcql-query.d.ts.map +1 -1
  34. package/lib/typescript/credential/status/02-status-assertion.d.ts +2 -1
  35. package/lib/typescript/credential/status/02-status-assertion.d.ts.map +1 -1
  36. package/lib/typescript/credential/status/03-verify-and-parse-status-assertion.d.ts +2 -1
  37. package/lib/typescript/credential/status/03-verify-and-parse-status-assertion.d.ts.map +1 -1
  38. package/lib/typescript/sd-jwt/types.d.ts +7 -1
  39. package/lib/typescript/sd-jwt/types.d.ts.map +1 -1
  40. package/lib/typescript/trust/types.d.ts +4 -0
  41. package/lib/typescript/trust/types.d.ts.map +1 -1
  42. package/lib/typescript/utils/credentials.d.ts +2 -1
  43. package/lib/typescript/utils/credentials.d.ts.map +1 -1
  44. package/lib/typescript/utils/misc.d.ts +1 -0
  45. package/lib/typescript/utils/misc.d.ts.map +1 -1
  46. package/package.json +1 -1
  47. package/src/credential/issuance/07-verify-and-parse-credential.ts +142 -44
  48. package/src/credential/presentation/07-evaluate-dcql-query.ts +4 -4
  49. package/src/credential/status/02-status-assertion.ts +2 -1
  50. package/src/credential/status/03-verify-and-parse-status-assertion.ts +2 -1
  51. package/src/sd-jwt/types.ts +9 -2
  52. package/src/trust/types.ts +4 -0
  53. package/src/utils/credentials.ts +6 -2
  54. package/src/utils/misc.ts +3 -0
@@ -1,5 +1,5 @@
1
1
  import type { CryptoContext } from "@pagopa/io-react-native-jwt";
2
- import type { Out } from "../../utils/misc";
2
+ import { isObject, type Out } from "../../utils/misc";
3
3
  import type { EvaluateIssuerTrust } from "./02-evaluate-issuer-trust";
4
4
  import { IoWalletError } from "../../utils/errors";
5
5
  import { SdJwt4VC, verify as verifySdJwt } from "../../sd-jwt";
@@ -64,6 +64,118 @@ type DecodedSdJwtCredential = Out<typeof verifySdJwt> & {
64
64
  sdJwt: SdJwt4VC;
65
65
  };
66
66
 
67
+ // The data used to create localized names
68
+ type DisplayData = { locale: string; name: string }[];
69
+
70
+ // The resulting object of localized names { en: "Name", it: "Nome" }
71
+ type LocalizedNames = Record<string, string>;
72
+
73
+ // The core structure being built: a node containing the actual value and its localized names
74
+ type PropertyNode<T> = {
75
+ value: T;
76
+ name: LocalizedNames;
77
+ };
78
+
79
+ // A path can consist of object keys, array indices, or null for mapping
80
+ type Path = (string | number | null)[];
81
+
82
+ // A union of all possible shapes. It can be a custom PropertyNode or a standard object/array structure
83
+ type NodeOrStructure = Partial<PropertyNode<any>> | Record<string, any> | any[];
84
+
85
+ // Helper to build localized names from the display data.
86
+ const buildName = (display: DisplayData): LocalizedNames =>
87
+ display.reduce(
88
+ (names, { locale, name }) => ({ ...names, [locale]: name }),
89
+ {}
90
+ );
91
+
92
+ /**
93
+ * Recursively constructs a nested object with descriptive properties from a path.
94
+ *
95
+ * @param currentObject - The object or array being built upon.
96
+ * @param path - The path segments to follow.
97
+ * @param sourceValue - The raw value to place at the end of the path.
98
+ * @param displayData - The data for generating localized names.
99
+ * @returns The new object or array structure.
100
+ */
101
+ const createNestedProperty = (
102
+ currentObject: NodeOrStructure,
103
+ path: Path,
104
+ sourceValue: unknown, // Use `unknown` for type-safe input
105
+ displayData: DisplayData
106
+ ): NodeOrStructure => {
107
+ const [key, ...rest] = path;
108
+
109
+ // Case 1: Map over an array (key is null)
110
+ if (key === null) {
111
+ if (!Array.isArray(sourceValue)) return currentObject;
112
+
113
+ // We assert the type here because we know this branch handles PropertyNodes
114
+ const node = currentObject as Partial<PropertyNode<unknown[]>>;
115
+ const existingValue = Array.isArray(node.value) ? node.value : [];
116
+
117
+ const mappedArray = sourceValue.map((item, idx) =>
118
+ createNestedProperty(existingValue[idx] || {}, rest, item, displayData)
119
+ );
120
+
121
+ return {
122
+ ...node,
123
+ value: mappedArray,
124
+ name: node.name ?? buildName(displayData),
125
+ };
126
+ }
127
+
128
+ // Case 2: Handle an object key (key is a string)
129
+ if (typeof key === "string") {
130
+ let nextSourceValue = sourceValue;
131
+
132
+ if (isObject(sourceValue)) {
133
+ // Skip processing when the key is not found within the claim object
134
+ if (!(key in sourceValue)) return currentObject;
135
+
136
+ nextSourceValue = sourceValue[key];
137
+ }
138
+
139
+ // base case
140
+ if (rest.length === 0) {
141
+ return {
142
+ ...currentObject,
143
+ [key]: { value: nextSourceValue, name: buildName(displayData) },
144
+ };
145
+ }
146
+
147
+ // recursive step
148
+ const nextObject =
149
+ (currentObject as Record<string, NodeOrStructure>)[key] || {};
150
+
151
+ return {
152
+ ...currentObject,
153
+ [key]: createNestedProperty(
154
+ nextObject,
155
+ rest,
156
+ nextSourceValue,
157
+ displayData
158
+ ),
159
+ };
160
+ }
161
+
162
+ // Case 3: Handle a specific array index (key is a number)
163
+ if (typeof key === "number") {
164
+ const newArray = Array.isArray(currentObject) ? [...currentObject] : [];
165
+ const nextValue = Array.isArray(sourceValue) ? sourceValue[key] : undefined;
166
+
167
+ newArray[key] = createNestedProperty(
168
+ newArray[key] || {},
169
+ rest,
170
+ nextValue,
171
+ displayData
172
+ );
173
+ return newArray;
174
+ }
175
+
176
+ return currentObject;
177
+ };
178
+
67
179
  const parseCredentialSdJwt = (
68
180
  // The credential configuration to use to parse the provided credential
69
181
  credentialConfig: CredentialConf,
@@ -72,7 +184,7 @@ const parseCredentialSdJwt = (
72
184
  includeUndefinedAttributes: boolean = false
73
185
  ): ParsedCredential => {
74
186
  if (credentialConfig.format !== sdJwt.header.typ) {
75
- const message = `Received credential is of an unknwown type. Expected one of [${credentialConfig.format}], received '${sdJwt.header.typ}'`;
187
+ const message = `Received credential is of an unknown type. Expected one of [${credentialConfig.format}], received '${sdJwt.header.typ}'`;
76
188
  Logger.log(LogLevel.ERROR, message);
77
189
  throw new IoWalletError(message);
78
190
  }
@@ -81,55 +193,40 @@ const parseCredentialSdJwt = (
81
193
  Logger.log(LogLevel.ERROR, "Missing claims in the credential subject");
82
194
  throw new IoWalletError("Missing claims in the credential subject"); // TODO [SIW-1268]: should not be optional
83
195
  }
196
+
84
197
  const attrDefinitions = credentialConfig.claims;
85
198
 
86
- // the key of the attribute defintion must match the disclosure's name
199
+ // Validate that all attributes from the config exist in the disclosures
87
200
  const attrsNotInDisclosures = attrDefinitions.filter(
88
- (definition) => !disclosures.some(([, name]) => name === definition.path[0]) // Ignore nested paths for now, see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-15.html#name-claims-path-pointer
201
+ (definition) => !disclosures.some(([, name]) => name === definition.path[0])
89
202
  );
90
- if (attrsNotInDisclosures.length > 0) {
203
+
204
+ if (attrsNotInDisclosures.length > 0 && !ignoreMissingAttributes) {
91
205
  const missing = attrsNotInDisclosures.map((_) => _.path[0]).join(", ");
92
- const received = disclosures.map((_) => _[1 /* name */]).join(", ");
93
- if (!ignoreMissingAttributes) {
94
- const message = `Some attributes are missing in the credential. Missing: [${missing}], received: [${received}]`;
95
- Logger.log(LogLevel.ERROR, message);
96
- throw new IoWalletError(message);
97
- }
206
+ const received = disclosures.map((_) => _[1]).join(", ");
207
+ const message = `Some attributes are missing in the credential. Missing: [${missing}], received: [${received}]`;
208
+ Logger.log(LogLevel.ERROR, message);
209
+ throw new IoWalletError(message);
98
210
  }
99
211
 
100
- // attributes that are defined in the issuer configuration
101
- // and are present in the disclosure set
102
- const definedValues = Object.fromEntries(
103
- attrDefinitions
104
- // retrieve the value from the disclosure set
105
- .map(
106
- ({ path, ...definition }) =>
107
- [
108
- path[0],
109
- {
110
- ...definition,
111
- value: disclosures.find(
112
- (_) => _[1 /* name */] === path[0]
113
- )?.[2 /* value */],
114
- },
115
- ] as const
116
- )
117
- // add a human readable attribute name, with i18n, in the form { locale: name }
118
- // example: { "it-IT": "Nome", "en-EN": "Name", "es-ES": "Nombre" }
119
- .map(
120
- ([attrKey, { display, ...definition }]) =>
121
- [
122
- attrKey,
123
- {
124
- ...definition,
125
- name: display.reduce(
126
- (names, { locale, name }) => ({ ...names, [locale]: name }),
127
- {} as Record<string, string>
128
- ),
129
- },
130
- ] as const
131
- )
132
- );
212
+ const definedValues: ParsedCredential = {};
213
+
214
+ for (const { path, display } of attrDefinitions) {
215
+ const attrKey = path[0];
216
+ const disclosureValue = disclosures.find(
217
+ ([, name]) => name === attrKey
218
+ )?.[2];
219
+
220
+ if (disclosureValue !== undefined) {
221
+ const enriched = createNestedProperty(
222
+ definedValues,
223
+ path,
224
+ disclosureValue,
225
+ display
226
+ );
227
+ Object.assign(definedValues, enriched);
228
+ }
229
+ }
133
230
 
134
231
  if (includeUndefinedAttributes) {
135
232
  // attributes that are in the disclosure set
@@ -139,6 +236,7 @@ const parseCredentialSdJwt = (
139
236
  .filter((_) => !Object.keys(definedValues).includes(_[1]))
140
237
  .map(([, key, value]) => [key, { value, name: key }])
141
238
  );
239
+
142
240
  return {
143
241
  ...definedValues,
144
242
  ...undefinedValues,
@@ -1,10 +1,10 @@
1
1
  import { DcqlQuery, DcqlError, DcqlQueryResult } from "dcql";
2
2
  import { isValiError } from "valibot";
3
+ import type { CryptoContext } from "@pagopa/io-react-native-jwt";
3
4
  import { decode, prepareVpToken } from "../../sd-jwt";
4
- import type { Disclosure } from "../../sd-jwt/types";
5
+ import { LEGACY_SD_JWT, type Disclosure } from "../../sd-jwt/types";
5
6
  import type { RemotePresentation } from "./types";
6
7
  import { CredentialsNotFoundError, type NotFoundDetail } from "./errors";
7
- import type { CryptoContext } from "@pagopa/io-react-native-jwt";
8
8
 
9
9
  /**
10
10
  * The purpose for the credential request by the RP.
@@ -97,7 +97,7 @@ const extractMissingCredentials = (
97
97
  const credential = originalQuery.credentials.find((c) => c.id === id);
98
98
  if (
99
99
  credential?.format !== "dc+sd-jwt" &&
100
- credential?.format !== "vc+sd-jwt"
100
+ credential?.format !== LEGACY_SD_JWT
101
101
  ) {
102
102
  throw new Error("Unsupported format"); // TODO [SIW-2082]: support MDOC credentials
103
103
  }
@@ -134,7 +134,7 @@ export const evaluateDcqlQuery: EvaluateDcqlQuery = (
134
134
  return getDcqlQueryMatches(queryResult).map(([id, match]) => {
135
135
  if (
136
136
  match.output.credential_format !== "dc+sd-jwt" &&
137
- match.output.credential_format !== "vc+sd-jwt"
137
+ match.output.credential_format !== LEGACY_SD_JWT
138
138
  ) {
139
139
  throw new Error("Unsupported format"); // TODO [SIW-2082]: support MDOC credentials
140
140
  }
@@ -15,11 +15,12 @@ import {
15
15
  } from "../../utils/errors";
16
16
  import { Logger, LogLevel } from "../../utils/logging";
17
17
  import { extractJwkFromCredential } from "../../utils/credentials";
18
+ import type { SupportedSdJwtLegacyFormat } from "../../sd-jwt/types";
18
19
 
19
20
  export type StatusAssertion = (
20
21
  issuerConf: Out<EvaluateIssuerTrust>["issuerConf"],
21
22
  credential: Out<ObtainCredential>["credential"],
22
- format: Out<ObtainCredential>["format"],
23
+ format: Out<ObtainCredential>["format"] | SupportedSdJwtLegacyFormat,
23
24
  context: {
24
25
  credentialCryptoContext: CryptoContext;
25
26
  wiaCryptoContext: CryptoContext;
@@ -17,12 +17,13 @@ import { Logger, LogLevel } from "../../utils/logging";
17
17
  import type { ObtainCredential } from "../issuance";
18
18
  import { extractJwkFromCredential } from "../../utils/credentials";
19
19
  import { isSameThumbprint } from "../../utils/jwk";
20
+ import type { SupportedSdJwtLegacyFormat } from "../../sd-jwt/types";
20
21
 
21
22
  export type VerifyAndParseStatusAssertion = (
22
23
  issuerConf: Out<EvaluateIssuerTrust>["issuerConf"],
23
24
  statusAssertion: Out<StatusAssertion>,
24
25
  credential: Out<ObtainCredential>["credential"],
25
- format: Out<ObtainCredential>["format"]
26
+ format: Out<ObtainCredential>["format"] | SupportedSdJwtLegacyFormat
26
27
  ) => Promise<{ parsedStatusAssertion: ParsedStatusAssertion }>;
27
28
 
28
29
  /**
@@ -20,6 +20,13 @@ export const Disclosure = z.tuple([
20
20
  /* claim value */ z.unknown(),
21
21
  ]);
22
22
 
23
+ /**
24
+ * For backward compatibility reasons it is still necessary to support the legacy SD-JWT
25
+ * in a few flows (for instance status assertion and presentation of the old eID).
26
+ */
27
+ export type SupportedSdJwtLegacyFormat = typeof LEGACY_SD_JWT;
28
+ export const LEGACY_SD_JWT = "vc+sd-jwt";
29
+
23
30
  /**
24
31
  * Encoding depends on the serialization algorithm used when generating the disclosure tokens.
25
32
  * The SD-JWT reference itself take no decision about how to handle whitespaces in serialized objects.
@@ -44,7 +51,7 @@ const StatusAssertion = z.object({
44
51
  export type SdJwt4VC = z.infer<typeof SdJwt4VC>;
45
52
  export const SdJwt4VC = z.object({
46
53
  header: z.object({
47
- typ: z.enum(["vc+sd-jwt", "dc+sd-jwt"]),
54
+ typ: z.enum(["dc+sd-jwt", LEGACY_SD_JWT]),
48
55
  alg: z.string(),
49
56
  kid: z.string(),
50
57
  trust_chain: z.array(z.string()).optional(),
@@ -62,7 +69,7 @@ export const SdJwt4VC = z.object({
62
69
  .union([
63
70
  // Credentials v1.0
64
71
  z.object({ status_assertion: StatusAssertion }),
65
- // Credentials v0.7.1
72
+ // Legacy credentials v0.7.1
66
73
  z.object({ status_attestation: StatusAssertion }),
67
74
  ])
68
75
  .optional(),
@@ -71,6 +71,10 @@ const SupportedCredentialMetadata = z.intersection(
71
71
  })
72
72
  );
73
73
 
74
+ /**
75
+ * Supported formats for credentials issued by the Issuer API 1.0,
76
+ * compliant with IT-Wallet technical specifications 1.0.
77
+ */
74
78
  export type SupportedCredentialFormat = z.infer<
75
79
  typeof SupportedCredentialMetadata
76
80
  >["format"];
@@ -4,8 +4,12 @@ import type { Out } from "./misc";
4
4
  import type { ObtainCredential } from "../credential/issuance";
5
5
  import type { JWK } from "./jwk";
6
6
  import { IoWalletError } from "./errors";
7
+ import {
8
+ LEGACY_SD_JWT,
9
+ type SupportedSdJwtLegacyFormat,
10
+ } from "../sd-jwt/types";
7
11
 
8
- const SD_JWT = ["vc+sd-jwt", "dc+sd-jwt"];
12
+ const SD_JWT = ["dc+sd-jwt", LEGACY_SD_JWT];
9
13
 
10
14
  /**
11
15
  * Extracts a JWK from a credential.
@@ -15,7 +19,7 @@ const SD_JWT = ["vc+sd-jwt", "dc+sd-jwt"];
15
19
  */
16
20
  export const extractJwkFromCredential = async (
17
21
  credential: Out<ObtainCredential>["credential"],
18
- format: Out<ObtainCredential>["format"]
22
+ format: Out<ObtainCredential>["format"] | SupportedSdJwtLegacyFormat
19
23
  ): Promise<JWK> => {
20
24
  if (SD_JWT.includes(format)) {
21
25
  // 1. SD-JWT case
package/src/utils/misc.ts CHANGED
@@ -78,3 +78,6 @@ export const safeJsonParse = <T>(text: string, withDefault?: T): T | null => {
78
78
  return withDefault ?? null;
79
79
  }
80
80
  };
81
+
82
+ export const isObject = (value: unknown): value is Record<string, unknown> =>
83
+ typeof value === "object" && value !== null && !Array.isArray(value);