@govuk-one-login/mobile-wallet-mdoc-builder 0.0.0 → 0.2.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 GOV.UK One Login
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # Mobile Wallet mdoc Builder
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
4
+
5
+ ## Overview
6
+
7
+ A TypeScript library for building mdoc (ISO 18013-5) documents for use with GOV.UK Wallet. It provides a type-safe API
8
+ for constructing and encoding mdoc-based credentials.
9
+
10
+ ## Tech stack
11
+
12
+ Built with TypeScript and Node.js, published as a dual-format (ESM and CJS) npm package.
13
+
14
+ ## Prerequisites
15
+
16
+ - [Node.js](https://nodejs.org/en) — we recommend managing versions with [nvm](https://github.com/nvm-sh/nvm)
17
+ - [Pre-commit](https://pre-commit.com/)
18
+
19
+ ## Set up locally
20
+
21
+ ### Install
22
+
23
+ ```bash
24
+ nvm use
25
+ npm install
26
+ ```
27
+
28
+ ### Lint and format
29
+
30
+ ```bash
31
+ npm run lint
32
+ npm run format
33
+ ```
34
+
35
+ ### Type check
36
+
37
+ ```bash
38
+ npm run typecheck
39
+ ```
40
+
41
+ ### Build
42
+
43
+ ```bash
44
+ npm run build
45
+ ```
46
+
47
+ ## Contributing
48
+
49
+ This project uses [pre-commit](https://pre-commit.com/) to enforce code quality and validate commit messages against [Conventional Commits](https://www.conventionalcommits.org/) standards. Non-conforming messages will be rejected.
50
+
51
+ Ensure your branch is up to date and all hooks pass before opening a pull request. Avoid using the git `--no-verify` flag to skip these checks unless absolutely necessary.
52
+
53
+ ### Installing pre-commit hooks
54
+
55
+ ```bash
56
+ brew install pre-commit
57
+ ```
58
+
59
+ ```bash
60
+ pre-commit install --hook-type pre-commit --hook-type commit-msg
61
+ ```
62
+
63
+ ## Documentation
64
+
65
+ - [Release process](docs/release-process.md)
66
+ - [Component architecture](docs/component-architecture.md)
67
+ - [CBOR test guide](docs/cbor-test-guide.md)
68
+
69
+ ## Licence
70
+
71
+ [MIT License](LICENSE)
package/dist/index.cjs ADDED
@@ -0,0 +1,602 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let zod = require("zod");
25
+ let node_crypto = require("node:crypto");
26
+ node_crypto = __toESM(node_crypto, 1);
27
+ let cbor2 = require("cbor2");
28
+ //#region src/types/dateFormat.ts
29
+ /**
30
+ * Specifies the date encoding format for a data element.
31
+ *
32
+ * Used to indicate whether a Date value should be encoded as a full-date
33
+ * (YYYY-MM-DD) or a date-time (ISO 8601 with time component) in the
34
+ * resulting CBOR structure.
35
+ */
36
+ let DateFormat = /* @__PURE__ */ function(DateFormat) {
37
+ /** Encode as a full-date (YYYY-MM-DD) per RFC 3339. */
38
+ DateFormat[DateFormat["FullDate"] = 0] = "FullDate";
39
+ /** Encode as a date-time (YYYY-MM-DDTHH:mm:ssZ) per RFC 3339. */
40
+ DateFormat[DateFormat["DateTime"] = 1] = "DateTime";
41
+ return DateFormat;
42
+ }({});
43
+ //#endregion
44
+ //#region src/types/mdocBuilderError.ts
45
+ /**
46
+ * Error thrown by the mdoc builder when construction or signing fails.
47
+ *
48
+ * When the failure is caused by input validation, `violations` holds the
49
+ * complete set of {@link ValidationError} entries which callers can inspect.
50
+ */
51
+ var MdocBuilderError = class extends Error {
52
+ /** The validation violations that caused the failure, if any. */
53
+ violations;
54
+ constructor(message, options) {
55
+ super(message, options);
56
+ this.name = "MdocBuilderError";
57
+ if (options?.violations !== void 0) this.violations = options.violations;
58
+ }
59
+ };
60
+ //#endregion
61
+ //#region src/validation/constants.ts
62
+ const ELEMENT_VALUE_MAX_BYTES = 1572864;
63
+ const DEVICE_KEY_MAX_BYTES = 2048;
64
+ const CERTIFICATE_MAX_BYTES = 8192;
65
+ const STATUS_LIST_IDX_MAX = 4294967295;
66
+ const DOCUMENT_TYPE_MAX_LENGTH = 128;
67
+ const IDENTIFIER_MAX_LENGTH = 256;
68
+ const STRING_VALUE_MAX_LENGTH = 150;
69
+ const COLLECTION_MAX_ENTRIES = 256;
70
+ const VALIDATION_LIMITS = {
71
+ documentType: {
72
+ minLength: 1,
73
+ maxLength: DOCUMENT_TYPE_MAX_LENGTH
74
+ },
75
+ nameSpaces: {
76
+ namespaceKey: {
77
+ minLength: 1,
78
+ maxLength: IDENTIFIER_MAX_LENGTH
79
+ },
80
+ minDataElements: 1,
81
+ maxDataElements: COLLECTION_MAX_ENTRIES,
82
+ elementIdentifier: {
83
+ minLength: 1,
84
+ maxLength: IDENTIFIER_MAX_LENGTH
85
+ }
86
+ },
87
+ elementValue: {
88
+ string: {
89
+ minLength: 1,
90
+ maxLength: STRING_VALUE_MAX_LENGTH
91
+ },
92
+ number: {
93
+ min: Number.MIN_SAFE_INTEGER,
94
+ max: Number.MAX_SAFE_INTEGER
95
+ },
96
+ uint8Array: {
97
+ minByteLength: 1,
98
+ maxByteLength: ELEMENT_VALUE_MAX_BYTES
99
+ }
100
+ },
101
+ collections: {
102
+ minLength: 1,
103
+ maxLength: COLLECTION_MAX_ENTRIES
104
+ },
105
+ deviceKey: {
106
+ minByteLength: 1,
107
+ maxByteLength: DEVICE_KEY_MAX_BYTES
108
+ },
109
+ statusList: {
110
+ idx: {
111
+ min: 0,
112
+ max: STATUS_LIST_IDX_MAX
113
+ },
114
+ uri: { maxLength: 2048 }
115
+ },
116
+ certificateChain: {
117
+ minLength: 1,
118
+ entry: {
119
+ minByteLength: 1,
120
+ maxByteLength: CERTIFICATE_MAX_BYTES
121
+ }
122
+ }
123
+ };
124
+ //#endregion
125
+ //#region src/validation/helpers/latin1.ts
126
+ const LATIN1_PATTERN = /^[\u0000-\u00FF]*$/;
127
+ const LATIN1_MESSAGE = "must contain only Latin1 (ISO/IEC 8859-1) characters";
128
+ function isLatin1(value) {
129
+ return LATIN1_PATTERN.test(value);
130
+ }
131
+ //#endregion
132
+ //#region src/validation/topLevelCredentialFields.ts
133
+ const { documentType, deviceKey, statusList, certificateChain } = VALIDATION_LIMITS;
134
+ function byteLengthSchema(minByteLength, maxByteLength) {
135
+ return zod.z.instanceof(Uint8Array).refine((value) => value.length >= minByteLength, { message: "must not be empty" }).refine((value) => value.length <= maxByteLength, { message: `must not exceed ${maxByteLength.toString()} bytes` });
136
+ }
137
+ const documentTypeSchema = zod.z.string().min(documentType.minLength).max(documentType.maxLength).refine(isLatin1, { message: LATIN1_MESSAGE });
138
+ const deviceKeySchema = byteLengthSchema(deviceKey.minByteLength, deviceKey.maxByteLength);
139
+ const statusListSchema = zod.z.object({
140
+ idx: zod.z.number().int().min(statusList.idx.min).max(statusList.idx.max),
141
+ uri: zod.z.url().max(statusList.uri.maxLength)
142
+ }).strict();
143
+ const certificateChainSchema = zod.z.array(byteLengthSchema(certificateChain.entry.minByteLength, certificateChain.entry.maxByteLength)).min(certificateChain.minLength);
144
+ //#endregion
145
+ //#region src/validation/primitives.ts
146
+ const { string, number, uint8Array } = VALIDATION_LIMITS.elementValue;
147
+ const stringValueSchema = zod.z.string().min(string.minLength).max(string.maxLength).refine(isLatin1, { message: LATIN1_MESSAGE });
148
+ const numberValueSchema = zod.z.number().min(number.min).max(number.max);
149
+ const booleanValueSchema = zod.z.boolean();
150
+ const dateValueSchema = zod.z.date();
151
+ const uint8ArrayValueSchema = zod.z.instanceof(Uint8Array).refine((value) => value.length >= uint8Array.minByteLength, { message: "must not be empty" }).refine((value) => value.length <= uint8Array.maxByteLength, { message: `must not exceed ${uint8Array.maxByteLength.toString()} bytes` });
152
+ //#endregion
153
+ //#region src/validation/dataElementValue.ts
154
+ const { minLength, maxLength } = VALIDATION_LIMITS.collections;
155
+ const latin1KeySchema = zod.z.string().refine(isLatin1, { message: LATIN1_MESSAGE });
156
+ const primitiveValueSchema = zod.z.union([
157
+ stringValueSchema,
158
+ numberValueSchema,
159
+ booleanValueSchema,
160
+ dateValueSchema,
161
+ uint8ArrayValueSchema
162
+ ]);
163
+ function primitiveTypeTag(value) {
164
+ if (value instanceof Uint8Array) return "uint8Array";
165
+ if (value instanceof Date) return "date";
166
+ if (typeof value === "string") return "string";
167
+ if (typeof value === "number") return "number";
168
+ return "boolean";
169
+ }
170
+ function isHomogeneous(values) {
171
+ let tag;
172
+ for (const value of values) {
173
+ const current = primitiveTypeTag(value);
174
+ tag ??= current;
175
+ if (current !== tag) return false;
176
+ }
177
+ return true;
178
+ }
179
+ const HOMOGENEITY_MESSAGE = "all values must be the same primitive type";
180
+ const primitiveArraySchema = zod.z.array(primitiveValueSchema).min(minLength).max(maxLength).refine(isHomogeneous, { message: HOMOGENEITY_MESSAGE });
181
+ const primitiveMapSchema = zod.z.map(latin1KeySchema, primitiveValueSchema).refine((map) => map.size >= minLength, { message: "must not be empty" }).refine((map) => map.size <= maxLength, { message: `must not exceed ${maxLength.toString()} entries` }).refine((map) => isHomogeneous(map.values()), { message: HOMOGENEITY_MESSAGE });
182
+ const primitiveMapArraySchema = zod.z.array(zod.z.map(latin1KeySchema, primitiveValueSchema)).min(minLength).max(maxLength).refine((maps) => maps.every((map) => map.size >= minLength), { message: "each map must not be empty" }).refine((maps) => maps.every((map) => map.size <= maxLength), { message: `each map must not exceed ${maxLength.toString()} entries` }).refine((maps) => isHomogeneous(maps.flatMap((map) => [...map.values()])), { message: HOMOGENEITY_MESSAGE });
183
+ const dataElementValueSchema = zod.z.union([
184
+ primitiveMapArraySchema,
185
+ primitiveMapSchema,
186
+ primitiveArraySchema,
187
+ primitiveValueSchema
188
+ ]);
189
+ //#endregion
190
+ //#region src/validation/nameSpaces.ts
191
+ const { namespaceKey, elementIdentifier, minDataElements, maxDataElements } = VALIDATION_LIMITS.nameSpaces;
192
+ function isDateTyped(value) {
193
+ if (value instanceof Date) return true;
194
+ if (value instanceof Map) return [...value.values()].some((v) => v instanceof Date);
195
+ if (Array.isArray(value)) return value.some(isDateTyped);
196
+ return false;
197
+ }
198
+ const namespaceKeySchema = zod.z.string().min(namespaceKey.minLength).max(namespaceKey.maxLength).refine(isLatin1, { message: LATIN1_MESSAGE });
199
+ const dataElementSchema = zod.z.object({
200
+ elementIdentifier: zod.z.string().min(elementIdentifier.minLength).max(elementIdentifier.maxLength).refine(isLatin1, { message: LATIN1_MESSAGE }),
201
+ elementValue: dataElementValueSchema,
202
+ dateFormat: zod.z.enum(DateFormat).optional()
203
+ }).superRefine((element, ctx) => {
204
+ if (!isDateTyped(element.elementValue) && element.dateFormat !== void 0) ctx.addIssue({
205
+ code: "custom",
206
+ message: "dateFormat must not be provided when elementValue is not date-typed",
207
+ path: ["dateFormat"]
208
+ });
209
+ });
210
+ const nameSpacesSchema = zod.z.map(namespaceKeySchema, zod.z.array(dataElementSchema).min(minDataElements).max(maxDataElements)).refine((nameSpaces) => nameSpaces.size >= 1, { message: "must not be empty" });
211
+ //#endregion
212
+ //#region src/validation/credentialValidity.ts
213
+ function credentialValiditySchema(now) {
214
+ return zod.z.object({
215
+ earliestValidFrom: zod.z.date().optional(),
216
+ validUntil: zod.z.date(),
217
+ expectedUpdate: zod.z.date().optional()
218
+ }).superRefine((validity, ctx) => {
219
+ if (validity.validUntil.getTime() <= now.getTime()) ctx.addIssue({
220
+ code: "custom",
221
+ message: "must be after the current time",
222
+ path: ["validUntil"]
223
+ });
224
+ if (validity.earliestValidFrom !== void 0 && validity.earliestValidFrom.getTime() >= validity.validUntil.getTime()) ctx.addIssue({
225
+ code: "custom",
226
+ message: "must be before validUntil",
227
+ path: ["earliestValidFrom"]
228
+ });
229
+ if (validity.expectedUpdate !== void 0 && validity.expectedUpdate.getTime() > validity.validUntil.getTime()) ctx.addIssue({
230
+ code: "custom",
231
+ message: "must be before or equal to validUntil",
232
+ path: ["expectedUpdate"]
233
+ });
234
+ });
235
+ }
236
+ //#endregion
237
+ //#region src/validation/errors.ts
238
+ function formatFieldPath(path) {
239
+ return path.reduce((acc, segment) => {
240
+ if (typeof segment === "number") return `${acc}[${segment.toString()}]`;
241
+ const key = String(segment);
242
+ return acc === "" ? key : `${acc}.${key}`;
243
+ }, "");
244
+ }
245
+ function mapZodErrorToValidationErrors(error) {
246
+ return error.issues.flatMap((issue) => {
247
+ if (issue.code === "unrecognized_keys") return issue.keys.map((key) => ({
248
+ field: formatFieldPath([...issue.path, key]),
249
+ message: issue.message
250
+ }));
251
+ return {
252
+ field: formatFieldPath(issue.path),
253
+ message: issue.message
254
+ };
255
+ });
256
+ }
257
+ //#endregion
258
+ //#region src/validation/validateMdocBuilderInput.ts
259
+ function mdocBuilderInputSchema(now) {
260
+ return zod.z.object({
261
+ documentType: documentTypeSchema,
262
+ nameSpaces: nameSpacesSchema,
263
+ deviceKey: deviceKeySchema,
264
+ credentialValidity: credentialValiditySchema(now),
265
+ statusList: statusListSchema,
266
+ certificateChain: certificateChainSchema
267
+ }).strict();
268
+ }
269
+ function validateMdocBuilderInput(input) {
270
+ const result = mdocBuilderInputSchema(/* @__PURE__ */ new Date()).safeParse(input);
271
+ return result.success ? [] : mapZodErrorToValidationErrors(result.error);
272
+ }
273
+ //#endregion
274
+ //#region src/deviceKey/helpers/padCoordinate.ts
275
+ const P256_COORDINATE_LENGTH = 32;
276
+ function decodeAndPadCoordinate(base64url) {
277
+ return padCoordinate(new Uint8Array(Buffer.from(base64url, "base64url")));
278
+ }
279
+ function padCoordinate(coordinate) {
280
+ if (coordinate.length === P256_COORDINATE_LENGTH) return new Uint8Array(coordinate);
281
+ const padded = new Uint8Array(P256_COORDINATE_LENGTH);
282
+ padded.set(coordinate, P256_COORDINATE_LENGTH - coordinate.length);
283
+ return padded;
284
+ }
285
+ //#endregion
286
+ //#region src/deviceKey/deviceKey.ts
287
+ const COSE_KEY_LABEL = {
288
+ /** Key Type (kty) */
289
+ KTY: 1,
290
+ /** EC Curve (crv) */
291
+ CRV: -1,
292
+ /** x-coordinate */
293
+ X: -2,
294
+ /** y-coordinate */
295
+ Y: -3
296
+ };
297
+ const COSE_KEY_VALUE = {
298
+ /** Key type: Elliptic Curve (EC2) */
299
+ KTY_EC2: 2,
300
+ /** Curve: P-256 */
301
+ CRV_P256: 1
302
+ };
303
+ const DEVICE_KEY_INFO_KEY = {
304
+ DEVICE_KEY: "deviceKey",
305
+ KEY_AUTHORIZATIONS: "keyAuthorizations",
306
+ NAME_SPACES: "nameSpaces"
307
+ };
308
+ function buildDeviceKeyInfo(spkiBytes, namespaceNames) {
309
+ const jwk = importSpki(spkiBytes).export({ format: "jwk" });
310
+ if (jwk.kty !== "EC") throw new MdocBuilderError(`Unsupported key type: ${String(jwk.kty)}. Only EC keys are supported.`);
311
+ if (jwk.crv !== "P-256") throw new MdocBuilderError(`Unsupported curve: ${String(jwk.crv)}. Only P-256 is supported.`);
312
+ const coseKey = buildCoseKey(jwk);
313
+ const deviceKeyInfo = /* @__PURE__ */ new Map();
314
+ deviceKeyInfo.set(DEVICE_KEY_INFO_KEY.DEVICE_KEY, coseKey);
315
+ if (namespaceNames.length > 0) {
316
+ const keyAuthorizations = /* @__PURE__ */ new Map();
317
+ keyAuthorizations.set(DEVICE_KEY_INFO_KEY.NAME_SPACES, namespaceNames);
318
+ deviceKeyInfo.set(DEVICE_KEY_INFO_KEY.KEY_AUTHORIZATIONS, keyAuthorizations);
319
+ }
320
+ return deviceKeyInfo;
321
+ }
322
+ function buildCoseKey(jwk) {
323
+ const x = decodeAndPadCoordinate(jwk.x);
324
+ const y = decodeAndPadCoordinate(jwk.y);
325
+ const coseKey = /* @__PURE__ */ new Map();
326
+ coseKey.set(COSE_KEY_LABEL.KTY, COSE_KEY_VALUE.KTY_EC2);
327
+ coseKey.set(COSE_KEY_LABEL.CRV, COSE_KEY_VALUE.CRV_P256);
328
+ coseKey.set(COSE_KEY_LABEL.X, x);
329
+ coseKey.set(COSE_KEY_LABEL.Y, y);
330
+ return coseKey;
331
+ }
332
+ function importSpki(spkiBytes) {
333
+ try {
334
+ return node_crypto.default.createPublicKey({
335
+ key: Buffer.from(spkiBytes),
336
+ format: "der",
337
+ type: "spki"
338
+ });
339
+ } catch (error) {
340
+ const message = error.message;
341
+ throw new MdocBuilderError(`Failed to import SPKI key: ${message}`, { cause: error });
342
+ }
343
+ }
344
+ //#endregion
345
+ //#region src/cbor/cbor.ts
346
+ var TaggedValue = class {
347
+ tagNumber;
348
+ contents;
349
+ constructor(tagNumber, contents) {
350
+ this.tagNumber = tagNumber;
351
+ this.contents = contents;
352
+ }
353
+ };
354
+ const CBOR_TAG = {
355
+ /** Tag 0: Standard date/time string () */
356
+ TDATE: 0,
357
+ /** Tag 24: Encoded CBOR data item (bstr-wrapped CBOR) */
358
+ EMBEDDED_CBOR: 24,
359
+ /** Tag 1004: Full-date string (YYYY-MM-DD) */
360
+ FULL_DATE: 1004
361
+ };
362
+ function tdate(date) {
363
+ const iso = date.toISOString().replace(/\.\d{3}/, "");
364
+ return new TaggedValue(CBOR_TAG.TDATE, iso);
365
+ }
366
+ function fullDate(date) {
367
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) throw new MdocBuilderError(`fullDate requires a "YYYY-MM-DD" string, received: "${date}"`);
368
+ return new TaggedValue(CBOR_TAG.FULL_DATE, date);
369
+ }
370
+ function embeddedCbor(bytes) {
371
+ return new TaggedValue(CBOR_TAG.EMBEDDED_CBOR, bytes);
372
+ }
373
+ function encode(value) {
374
+ const prepared = convertTags(value);
375
+ return (0, cbor2.encode)(prepared);
376
+ }
377
+ function convertTags(value) {
378
+ if (value instanceof TaggedValue) return new cbor2.Tag(value.tagNumber, convertTags(value.contents));
379
+ if (value instanceof Map) {
380
+ const prepared = /* @__PURE__ */ new Map();
381
+ for (const [k, v] of value) prepared.set(convertTags(k), convertTags(v));
382
+ return prepared;
383
+ }
384
+ if (Array.isArray(value)) return value.map(convertTags);
385
+ return value;
386
+ }
387
+ //#endregion
388
+ //#region src/issuerSigned/buildSingleItem.ts
389
+ const SALT_LENGTH_BYTES = 16;
390
+ function generateDigestId(existingIds) {
391
+ let id;
392
+ do
393
+ id = (0, node_crypto.randomBytes)(4).readUInt32BE(0) >>> 1;
394
+ while (existingIds.has(id));
395
+ return id;
396
+ }
397
+ function encodeElementValue(element) {
398
+ const { elementValue, dateFormat } = element;
399
+ if (elementValue instanceof Date) return encodeDateValue(elementValue, dateFormat);
400
+ if (elementValue instanceof Map) return encodeDateMap(elementValue, dateFormat);
401
+ if (Array.isArray(elementValue)) return elementValue.map((item) => {
402
+ if (item instanceof Date) return encodeDateValue(item, dateFormat);
403
+ if (item instanceof Map) return encodeDateMap(item, dateFormat);
404
+ return item;
405
+ });
406
+ return elementValue;
407
+ }
408
+ function encodeDateMap(map, dateFormat) {
409
+ const result = /* @__PURE__ */ new Map();
410
+ for (const [key, value] of map) result.set(key, value instanceof Date ? encodeDateValue(value, dateFormat) : value);
411
+ return result;
412
+ }
413
+ function encodeDateValue(date, dateFormat) {
414
+ if (dateFormat === 0) return fullDate(`${date.getUTCFullYear().toString().padStart(4, "0")}-${(date.getUTCMonth() + 1).toString().padStart(2, "0")}-${date.getUTCDate().toString().padStart(2, "0")}`);
415
+ return tdate(date);
416
+ }
417
+ function buildSingleItem(element, usedIds) {
418
+ const digestId = generateDigestId(usedIds);
419
+ usedIds.add(digestId);
420
+ const salt = new Uint8Array((0, node_crypto.randomBytes)(SALT_LENGTH_BYTES));
421
+ return {
422
+ digestId,
423
+ tag24Bytes: encode(embeddedCbor(encode(/* @__PURE__ */ new Map([
424
+ ["digestID", digestId],
425
+ ["random", salt],
426
+ ["elementIdentifier", element.elementIdentifier],
427
+ ["elementValue", encodeElementValue(element)]
428
+ ]))))
429
+ };
430
+ }
431
+ //#endregion
432
+ //#region src/issuerSigned/helpers/digest.ts
433
+ async function digestItem(tag24Bytes) {
434
+ const buffer = await crypto.subtle.digest("SHA-256", tag24Bytes);
435
+ return new Uint8Array(buffer);
436
+ }
437
+ //#endregion
438
+ //#region src/issuerSigned/issuerSignedItem.ts
439
+ async function buildIssuerSignedItems(nameSpaces) {
440
+ const issuerSignedItemBytes = /* @__PURE__ */ new Map();
441
+ const valueDigests = /* @__PURE__ */ new Map();
442
+ const namespaceResults = await Promise.all([...nameSpaces].map(async ([namespace, elements]) => {
443
+ const usedIds = /* @__PURE__ */ new Set();
444
+ const items = elements.map((element) => buildSingleItem(element, usedIds));
445
+ const digestEntries = await Promise.all(items.map(async ({ digestId, tag24Bytes }) => [digestId, await digestItem(tag24Bytes)]));
446
+ return {
447
+ namespace,
448
+ itemBytes: items.map(({ tag24Bytes }) => tag24Bytes),
449
+ digests: new Map(digestEntries)
450
+ };
451
+ }));
452
+ for (const { namespace, itemBytes, digests } of namespaceResults) {
453
+ issuerSignedItemBytes.set(namespace, itemBytes);
454
+ valueDigests.set(namespace, digests);
455
+ }
456
+ return {
457
+ issuerSignedItemBytes,
458
+ valueDigests
459
+ };
460
+ }
461
+ //#endregion
462
+ //#region src/issuerSigned/assembleIssuerSigned.ts
463
+ function assembleIssuerSigned(nameSpaces, issuerAuth) {
464
+ const nameSpacesMap = /* @__PURE__ */ new Map();
465
+ for (const [namespace, items] of nameSpaces) nameSpacesMap.set(namespace, items.map((itemBytes) => embeddedCbor(itemBytes)));
466
+ const issuerSigned = /* @__PURE__ */ new Map();
467
+ issuerSigned.set("nameSpaces", nameSpacesMap);
468
+ issuerSigned.set("issuerAuth", issuerAuth);
469
+ return encode(issuerSigned);
470
+ }
471
+ //#endregion
472
+ //#region src/validityInfo/buildValidityInfo.ts
473
+ function buildValidityInfo(input) {
474
+ const signed = /* @__PURE__ */ new Date();
475
+ const validityInfo = {
476
+ signed,
477
+ validFrom: input.earliestValidFrom && input.earliestValidFrom > signed ? input.earliestValidFrom : signed,
478
+ validUntil: input.validUntil
479
+ };
480
+ if (input.expectedUpdate !== void 0) validityInfo.expectedUpdate = input.expectedUpdate;
481
+ return validityInfo;
482
+ }
483
+ //#endregion
484
+ //#region src/mso/buildMso.ts
485
+ function buildMso(input) {
486
+ const validityInfoMap = /* @__PURE__ */ new Map();
487
+ validityInfoMap.set("signed", tdate(input.validityInfo.signed));
488
+ validityInfoMap.set("validFrom", tdate(input.validityInfo.validFrom));
489
+ validityInfoMap.set("validUntil", tdate(input.validityInfo.validUntil));
490
+ if (input.validityInfo.expectedUpdate !== void 0) validityInfoMap.set("expectedUpdate", tdate(input.validityInfo.expectedUpdate));
491
+ const statusListMap = /* @__PURE__ */ new Map();
492
+ statusListMap.set("idx", input.statusList.idx);
493
+ statusListMap.set("uri", input.statusList.uri);
494
+ const statusMap = /* @__PURE__ */ new Map();
495
+ statusMap.set("status_list", statusListMap);
496
+ const mso = /* @__PURE__ */ new Map();
497
+ mso.set("version", "1.0");
498
+ mso.set("digestAlgorithm", "SHA-256");
499
+ mso.set("valueDigests", input.valueDigests);
500
+ mso.set("deviceKeyInfo", input.deviceKeyInfo);
501
+ mso.set("validityInfo", validityInfoMap);
502
+ mso.set("status", statusMap);
503
+ mso.set("docType", input.docType);
504
+ return encode(embeddedCbor(encode(mso)));
505
+ }
506
+ //#endregion
507
+ //#region src/issuerAuth/buildProtectedHeader.ts
508
+ const COSE_HEADER_ALG = 1;
509
+ const COSE_ALG_ES256 = -7;
510
+ function buildProtectedHeader() {
511
+ return encode(/* @__PURE__ */ new Map([[COSE_HEADER_ALG, COSE_ALG_ES256]]));
512
+ }
513
+ //#endregion
514
+ //#region src/issuerAuth/buildUnprotectedHeader.ts
515
+ const COSE_HEADER_X5CHAIN = 33;
516
+ function buildUnprotectedHeader(certificateChain) {
517
+ const [signingCertificate] = certificateChain;
518
+ return /* @__PURE__ */ new Map([[COSE_HEADER_X5CHAIN, signingCertificate]]);
519
+ }
520
+ //#endregion
521
+ //#region src/issuerAuth/buildToBeSigned.ts
522
+ const SIG_STRUCTURE_CONTEXT = "Signature1";
523
+ const EXTERNAL_AAD = /* @__PURE__ */ new Uint8Array(0);
524
+ function buildToBeSigned(protectedHeaderBytes, payloadBytes) {
525
+ return encode([
526
+ SIG_STRUCTURE_CONTEXT,
527
+ protectedHeaderBytes,
528
+ EXTERNAL_AAD,
529
+ payloadBytes
530
+ ]);
531
+ }
532
+ //#endregion
533
+ //#region src/issuerAuth/assembleIssuerAuth.ts
534
+ const EXPECTED_SIGNATURE_LENGTH = 64;
535
+ async function assembleIssuerAuth(msoBytes, certificateChain, sign) {
536
+ const protectedHeader = buildProtectedHeader();
537
+ const unprotectedHeader = buildUnprotectedHeader(certificateChain);
538
+ const toBeSigned = buildToBeSigned(protectedHeader, msoBytes);
539
+ let signature;
540
+ try {
541
+ signature = await sign(toBeSigned);
542
+ } catch (error) {
543
+ throw new MdocBuilderError("Signing function threw an error", { cause: error });
544
+ }
545
+ if (!(signature instanceof Uint8Array)) throw new MdocBuilderError("Signing function must return a Uint8Array signature");
546
+ if (signature.length !== EXPECTED_SIGNATURE_LENGTH) throw new MdocBuilderError(`Invalid signature length: expected ${String(EXPECTED_SIGNATURE_LENGTH)} bytes, got ${String(signature.length)}`);
547
+ return [
548
+ protectedHeader,
549
+ unprotectedHeader,
550
+ msoBytes,
551
+ signature
552
+ ];
553
+ }
554
+ //#endregion
555
+ //#region src/mdoc/mdoc.ts
556
+ var MdocOutput = class {
557
+ bytes;
558
+ constructor(bytes) {
559
+ this.bytes = Uint8Array.from(bytes);
560
+ }
561
+ asBase64Url() {
562
+ return Buffer.from(this.bytes).toString("base64url");
563
+ }
564
+ asHex() {
565
+ return Buffer.from(this.bytes).toString("hex");
566
+ }
567
+ asBytes() {
568
+ return Uint8Array.from(this.bytes);
569
+ }
570
+ };
571
+ //#endregion
572
+ //#region src/index.ts
573
+ /**
574
+ * Builds an mdoc (ISO 18013-5) document from the provided input.
575
+ *
576
+ * @param input - The mdoc builder input containing document data and metadata.
577
+ * @param sign - A signing function called with the `toBeSigned` bytes.
578
+ * @returns A promise resolving to the built Mdoc document.
579
+ * @throws {MdocBuilderError} If input validation fails, with the aggregated
580
+ * violations in the message. Errors from any other component propagate to the
581
+ * caller unchanged.
582
+ */
583
+ async function buildMdoc(input, sign) {
584
+ const violations = validateMdocBuilderInput(input);
585
+ if (violations.length > 0) throw new MdocBuilderError("Input validation failed", { violations });
586
+ const deviceKeyInfo = buildDeviceKeyInfo(input.deviceKey, [...input.nameSpaces.keys()]);
587
+ const { issuerSignedItemBytes, valueDigests } = await buildIssuerSignedItems(input.nameSpaces);
588
+ const validityInfo = buildValidityInfo(input.credentialValidity);
589
+ return new MdocOutput(assembleIssuerSigned(issuerSignedItemBytes, await assembleIssuerAuth(buildMso({
590
+ docType: input.documentType,
591
+ valueDigests,
592
+ deviceKeyInfo,
593
+ validityInfo,
594
+ statusList: input.statusList
595
+ }), input.certificateChain, sign)));
596
+ }
597
+ //#endregion
598
+ exports.DateFormat = DateFormat;
599
+ exports.MdocBuilderError = MdocBuilderError;
600
+ exports.buildMdoc = buildMdoc;
601
+
602
+ //# sourceMappingURL=index.cjs.map