@govuk-one-login/mobile-wallet-mdoc-builder 0.1.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/dist/index.cjs CHANGED
@@ -1,4 +1,30 @@
1
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");
2
28
  //#region src/types/dateFormat.ts
3
29
  /**
4
30
  * Specifies the date encoding format for a data element.
@@ -19,13 +45,527 @@ let DateFormat = /* @__PURE__ */ function(DateFormat) {
19
45
  /**
20
46
  * Error thrown by the mdoc builder when construction or signing fails.
21
47
  *
22
- * The error structure will be refined as implementation progresses
23
- * (e.g., aggregated validation errors may be added later).
48
+ * When the failure is caused by input validation, `violations` holds the
49
+ * complete set of {@link ValidationError} entries which callers can inspect.
24
50
  */
25
51
  var MdocBuilderError = class extends Error {
52
+ /** The validation violations that caused the failure, if any. */
53
+ violations;
26
54
  constructor(message, options) {
27
55
  super(message, options);
28
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);
29
569
  }
30
570
  };
31
571
  //#endregion
@@ -34,16 +574,25 @@ var MdocBuilderError = class extends Error {
34
574
  * Builds an mdoc (ISO 18013-5) document from the provided input.
35
575
  *
36
576
  * @param input - The mdoc builder input containing document data and metadata.
37
- * @param sign - A signing function that will be called with the data to sign.
577
+ * @param sign - A signing function called with the `toBeSigned` bytes.
38
578
  * @returns A promise resolving to the built Mdoc document.
39
- * @throws {MdocBuilderError} Always throws until implementation is complete.
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.
40
582
  */
41
- function buildMdoc(input, sign) {
42
- console.log("buildMdoc not implemented", {
43
- input,
44
- sign
45
- });
46
- return Promise.reject(new MdocBuilderError("not implemented"));
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)));
47
596
  }
48
597
  //#endregion
49
598
  exports.DateFormat = DateFormat;