import { Stream } from "node:stream"; import { Buffer } from "node:buffer"; import path from "node:path"; import FieldsArray from "./FieldsArray"; import Bundle, { filesSymbol } from "./Bundle"; import getModelFolderContents from "./getModelFolderContents"; import * as Schemas from "./schemas"; import * as Signature from "./Signature"; import * as Strings from "./StringsUtils"; import * as Utils from "./utils"; import * as Messages from "./messages"; const propsSymbol = Symbol("props"); const localizationSymbol = Symbol("pass.l10n"); const importMetadataSymbol = Symbol("import.pass.metadata"); const createManifestSymbol = Symbol("pass.manifest"); const closePassSymbol = Symbol("pass.close"); const passTypeSymbol = Symbol("pass.type"); const certificatesSymbol = Symbol("pass.certificates"); const RegExps = { PASS_JSON: /pass\.json/, MANIFEST_OR_SIGNATURE: /manifest|signature/, PERSONALIZATION: { JSON: /personalization\.json/, LOGO: /personalizationLogo@(?:.{2})/, } as const, PASS_STRINGS: /(?[a-zA-Z-]{2,}).lproj\/pass\.strings/, PASS_ICON: /icon(?:@\d{1}x)?/, } as const; export default class PKPass extends Bundle { private [certificatesSymbol]: Schemas.CertificatesSchema; private [propsSymbol]: Schemas.PassProps = {}; private [localizationSymbol]: { [lang: string]: { [placeholder: string]: string; }; } = {}; private [passTypeSymbol]: Schemas.PassTypesProps | undefined = undefined; /** * Either create a pass from another one * or a disk path. * * @param source * @returns */ public static async from( source: S, props?: Schemas.OverridablePassProps, ): Promise { let certificates: Schemas.CertificatesSchema | undefined = undefined; let buffers: Schemas.FileBuffers | undefined = undefined; if (!source) { throw new TypeError( Messages.format(Messages.FROM.MISSING_SOURCE, source), ); } if (source instanceof PKPass) { /** Cloning is happening here */ certificates = source[certificatesSymbol]; buffers = {}; const buffersEntries = Object.entries(source[filesSymbol]); /** Cloning all the buffers to prevent unwanted edits */ for (let i = 0; i < buffersEntries.length; i++) { const [fileName, contentBuffer] = buffersEntries[i]; buffers[fileName] = Buffer.alloc(contentBuffer.length); contentBuffer.copy(buffers[fileName]); } /** * Moving props to pass.json instead of overrides * because many might get excluded when passing * through validation */ buffers["pass.json"] = Buffer.from( JSON.stringify(source[propsSymbol]), ); } else { Schemas.assertValidity( Schemas.Template, source, Messages.TEMPLATE.INVALID, ); buffers = await getModelFolderContents(source.model); certificates = source.certificates; } return new PKPass(buffers, certificates, props); } /** * Creates a Bundle made of PKPass to be distributed * as a `.pkpasses` zip file. Returns a Bundle instance * so it can be outputted both as stream or as a buffer. * * Using this will freeze all the instances passed as * parameter. * * Throws if not all the files are instance of PKPass. * * @param passes */ public static pack(...passes: PKPass[]): Bundle { const [bundle, freezeBundle] = Bundle.freezable( "application/vnd.apple.pkpasses", ); for (let i = 0; i < passes.length; i++) { const pass = passes[i]; if (!(pass instanceof PKPass)) { throw new Error(Messages.PACK.INVALID); } bundle.addBuffer(`packed-pass-${i + 1}.pkpass`, pass.getAsBuffer()); } freezeBundle(); return bundle; } // **************** // // *** INSTANCE *** // // **************** // public constructor( buffers?: Schemas.FileBuffers, certificates?: Schemas.CertificatesSchema, props?: Schemas.OverridablePassProps, ) { super("application/vnd.apple.pkpass"); if (buffers && typeof buffers === "object") { const buffersEntries = Object.entries(buffers); for ( let i = buffersEntries.length, buffer: [string, Buffer]; (buffer = buffersEntries[--i]); ) { const [fileName, contentBuffer] = buffer; this.addBuffer(fileName, contentBuffer); } } else { console.warn( Messages.format(Messages.INIT.INVALID_BUFFERS, typeof buffers), ); } if (props) { /** Overrides validation and pushing in props */ const overridesValidation = Schemas.validate( Schemas.OverridablePassProps, props, ); Object.assign(this[propsSymbol], overridesValidation); } if (certificates) { this.certificates = certificates; } } /** * Allows changing the certificates, if needed. * They are actually expected to be received in * the constructor, but they can get overridden * here for whatever purpose. * * When using this setter, all certificates are * expected to be received, or an exception will * be thrown. * * @param certs */ public set certificates(certs: Schemas.CertificatesSchema) { Utils.assertUnfrozen(this); Schemas.assertValidity( Schemas.CertificatesSchema, certs, Messages.CERTIFICATES.INVALID, ); this[certificatesSymbol] = certs; } /** * Allows retrieving current languages */ public get languages() { return Object.keys(this[localizationSymbol]); } /** * Allows getting an image of the props * that are composing your pass instance. */ public get props(): Schemas.PassProps { return Utils.cloneRecursive(this[propsSymbol]); } /** * Allows accessing to iOS 18 new Event Ticket * property `preferredStyleSchemes`. * * @throws if current type is not "eventTicket". */ public get preferredStyleSchemes(): Schemas.PreferredStyleSchemes { if (this.type !== "eventTicket") { throw new TypeError( Messages.PREFERRED_STYLE_SCHEMES.UNEXPECTED_PASS_TYPE_GET, ); } return this[propsSymbol].preferredStyleSchemes; } /** * Allows setting a preferredStyleSchemes property * for a eventTicket. * * @throws if current type is not "eventTicket". * @param value */ public set preferredStyleSchemes(value: Schemas.PreferredStyleSchemes) { Utils.assertUnfrozen(this); if (this.type !== "eventTicket") { throw new TypeError( Messages.PREFERRED_STYLE_SCHEMES.UNEXPECTED_PASS_TYPE_SET, ); } Schemas.assertValidity( Schemas.PreferredStyleSchemes, value, Messages.PREFERRED_STYLE_SCHEMES.INVALID, ); this[propsSymbol].preferredStyleSchemes = value; } /** * Allows setting a transitType property * for a boardingPass. * * @throws if current type is not "boardingPass". * @param value */ public set transitType(value: Schemas.TransitType) { Utils.assertUnfrozen(this); if (this.type !== "boardingPass") { throw new TypeError(Messages.TRANSIT_TYPE.UNEXPECTED_PASS_TYPE); } Schemas.assertValidity( Schemas.TransitType, value, Messages.TRANSIT_TYPE.INVALID, ); this[propsSymbol]["boardingPass"].transitType = value; } /** * Allows getting the current transitType * from pass props. * * @throws (automatically) if current type is not "boardingPass". */ public get transitType() { return this[propsSymbol]["boardingPass"].transitType; } /** * Allows accessing to primaryFields object. * * @throws (automatically) if no valid pass.json * has been parsed yet or, anyway, if current * instance has not a valid type set yet. */ public get primaryFields(): Schemas.Field[] { return this[propsSymbol][this.type].primaryFields; } /** * Allows accessing to secondaryFields object * * @throws (automatically) if no valid pass.json * has been parsed yet or, anyway, if current * instance has not a valid type set yet. */ public get secondaryFields(): Schemas.Field[] { return this[propsSymbol][this.type].secondaryFields; } /** * Allows accessing to auxiliaryFields object * * For Typescript users: this signature allows * in any case to add the 'row' field, but on * runtime they are only allowed on "eventTicket" * passes. * * @throws (automatically) if no valid pass.json * has been parsed yet or, anyway, if current * instance has not a valid type set yet. */ public get auxiliaryFields(): Schemas.FieldWithRow[] { return this[propsSymbol][this.type].auxiliaryFields; } /** * Allows accessing to headerFields object * * @throws (automatically) if no valid pass.json * has been parsed yet or, anyway, if current * instance has not a valid type set yet. */ public get headerFields(): Schemas.Field[] { return this[propsSymbol][this.type].headerFields; } /** * Allows accessing to backFields object * * @throws (automatically) if no valid pass.json * has been parsed yet or, anyway, if current * instance has not a valid type set yet. */ public get backFields(): Schemas.Field[] { return this[propsSymbol][this.type].backFields; } /** * Allows accessing to new iOS 18 * event ticket additional fields * * @throws (automatically) if no valid pass.json * has been parsed yet or, anyway, if current * type is not "eventTicket". */ public get additionalInfoFields(): Schemas.Field[] { return this[propsSymbol]["eventTicket"].additionalInfoFields; } /** * Allows setting a pass type. * * **Warning**: setting a type with this setter, * will reset all the fields (primaryFields, * secondaryFields, headerFields, auxiliaryFields, backFields), * both imported or manually set. */ public set type(nextType: Schemas.PassTypesProps | undefined) { Utils.assertUnfrozen(this); Schemas.assertValidity( Schemas.PassType, nextType, Messages.PASS_TYPE.INVALID, ); /** Shut up, typescript strict mode! */ const type = nextType as Schemas.PassTypesProps; if (this.type) { /** * Removing reference to previous type and its content because * we might have some differences between types. It is way easier * to reset everything instead of making checks. */ this[propsSymbol][this.type] = undefined; this[propsSymbol].preferredStyleSchemes = undefined; } const sharedKeysPool = new Set(); this[passTypeSymbol] = type; this[propsSymbol][type] = { headerFields /******/: new FieldsArray( this, sharedKeysPool, Schemas.Field, ), primaryFields /*****/: new FieldsArray( this, sharedKeysPool, Schemas.Field, ), secondaryFields /***/: new FieldsArray( this, sharedKeysPool, Schemas.Field, ), auxiliaryFields /***/: new FieldsArray( this, sharedKeysPool, type === "eventTicket" ? Schemas.FieldWithRow : Schemas.Field, ), backFields /********/: new FieldsArray( this, sharedKeysPool, Schemas.Field, ), additionalInfoFields: new FieldsArray( this, sharedKeysPool, Schemas.Field, ), transitType: undefined, }; } public get type(): Schemas.PassTypesProps | undefined { return this[passTypeSymbol] ?? undefined; } // **************************** // // *** ASSETS SETUP METHODS *** // // **************************** // /** * Allows adding a new asset inside the pass / bundle with * the following exceptions: * * - Empty buffers are ignored; * - `manifest.json` and `signature` files will be ignored; * - `pass.json` will be read validated and merged in the * current instance, if it wasn't added previously. * It's properties will overwrite the instance ones. * You might loose data; * - `pass.strings` files will be read, parsed and merged * with the current translations. Comments will be ignored; * - `personalization.json` will be read, validated and added. * They will be stripped out when exporting the pass if * it won't have NFC details or if any of the personalization * files is missing; * * @param pathName * @param buffer */ public addBuffer(pathName: string, buffer: Buffer): void { if (!buffer?.length) { return; } if (RegExps.MANIFEST_OR_SIGNATURE.test(pathName)) { return; } if (RegExps.PASS_JSON.test(pathName)) { if (this[filesSymbol]["pass.json"]) { /** * Ignoring any further addition. In a * future we might consider merging instead */ return; } try { this[importMetadataSymbol]( validateJSONBuffer(buffer, Schemas.PassProps), ); } catch (err) { console.warn( Messages.format(Messages.PASS_SOURCE.INVALID, err), ); return; } /** * Adding an empty buffer just for reference * that we received a valid pass.json file. * It will be reconciliated in export phase. */ return super.addBuffer(pathName, Buffer.alloc(0)); } if (RegExps.PERSONALIZATION.JSON.test(pathName)) { /** * We are still allowing `personalizationLogo@XX.png` * to be added to the bundle, but we'll delete it * once the pass is getting closed, if needed. */ try { validateJSONBuffer(buffer, Schemas.Personalize); } catch (err) { console.warn( Messages.format(Messages.PERSONALIZE.INVALID, err), ); return; } return super.addBuffer(pathName, buffer); } /** * Converting Windows path to Unix path * @example de.lproj\\icon.png => de.lproj/icon.png */ const normalizedPathName = pathName.replace(path.sep, "/"); /** * If a new pass.strings file is added, we want to * prevent it from being merged and, instead, save * its translations for later */ let match: RegExpMatchArray | null; if ((match = normalizedPathName.match(RegExps.PASS_STRINGS))) { const [, lang] = match; const parsedTranslations = Strings.parse(buffer).translations; if (!parsedTranslations.length) { return; } this.localize(lang, Object.fromEntries(parsedTranslations)); return; } return super.addBuffer(normalizedPathName, buffer); } /** * Given data from a pass.json, reads them to bring them * into the current pass instance. * * @param data */ private [importMetadataSymbol](data: Schemas.PassProps) { const possibleTypes = [ "boardingPass", "coupon", "eventTicket", "storeCard", "generic", ] as Schemas.PassTypesProps[]; const type = possibleTypes.find((type) => Boolean(data[type])); const { boardingPass, coupon, storeCard, generic, eventTicket, ...otherPassData } = data; if (Object.keys(this[propsSymbol]).length) { console.warn(Messages.PASS_SOURCE.JOIN); } Object.assign(this[propsSymbol], otherPassData); if (!type) { if (!this[passTypeSymbol]) { console.warn(Messages.PASS_SOURCE.UNKNOWN_TYPE); } } else { this.type = type; const { headerFields = [], primaryFields = [], secondaryFields = [], auxiliaryFields = [], backFields = [], transitType, additionalInfoFields = [], } = data[type] || {}; this.headerFields.push(...headerFields); this.primaryFields.push(...primaryFields); this.secondaryFields.push(...secondaryFields); this.auxiliaryFields.push(...auxiliaryFields); this.backFields.push(...backFields); if (this.type === "boardingPass") { this.transitType = transitType; } if (this.type === "eventTicket") { this.additionalInfoFields.push(...additionalInfoFields); } } } /** * Creates the manifest starting from files * added to the bundle */ private [createManifestSymbol](): Buffer { const manifest = Object.entries(this[filesSymbol]).reduce<{ [key: string]: string; }>( (acc, [fileName, buffer]) => ({ ...acc, [fileName]: Signature.createHash(buffer), }), {}, ); return Buffer.from(JSON.stringify(manifest)); } /** * Applies the last validation checks against props, * applies the props to pass.json and creates l10n * files and folders and creates manifest and * signature files */ private [closePassSymbol]() { if (!this.type) { throw new TypeError(Messages.CLOSE.MISSING_TYPE); } const fileNames = Object.keys(this[filesSymbol]); const passJson = Buffer.from(JSON.stringify(this[propsSymbol])); super.addBuffer("pass.json", passJson); if (!fileNames.some((fileName) => RegExps.PASS_ICON.test(fileName))) { console.warn(Messages.CLOSE.MISSING_ICON); } // *********************************** // // *** LOCALIZATION FILES CREATION *** // // *********************************** // const localizationEntries = Object.entries(this[localizationSymbol]); for (let i = localizationEntries.length - 1; i >= 0; i--) { const [lang, translations] = localizationEntries[i]; const stringsFile = Strings.create(translations); if (stringsFile.length) { super.addBuffer(`${lang}.lproj/pass.strings`, stringsFile); } } // *********************** // // *** PERSONALIZATION *** // // *********************** // const meetsPersonalizationRequirements = Boolean( this[propsSymbol]["nfc"] && this[filesSymbol]["personalization.json"] && fileNames.find((file) => RegExps.PERSONALIZATION.LOGO.test(file), ), ); if (!meetsPersonalizationRequirements) { /** * Looking for every personalization file * and removing it */ for (let i = 0; i < fileNames.length; i++) { if (fileNames[i].includes("personalization")) { console.warn( Messages.format( Messages.CLOSE.PERSONALIZATION_REMOVED, fileNames[i], ), ); delete this[filesSymbol][fileNames[i]]; } } } // ******************************** // // *** BOARDING PASS VALIDATION *** // // ******************************** // if (this.type === "boardingPass" && !this.transitType) { throw new TypeError(Messages.CLOSE.MISSING_TRANSIT_TYPE); } // ****************************** // // *** SIGNATURE AND MANIFEST *** // // ****************************** // const manifestBuffer = this[createManifestSymbol](); super.addBuffer("manifest.json", manifestBuffer); const signatureBuffer = Signature.create( manifestBuffer, this[certificatesSymbol], ); super.addBuffer("signature", signatureBuffer); } // ************************* // // *** EXPORTING METHODS *** // // ************************* // /** * Exports the pass as a zip buffer. When this method * is invoked, the bundle will get frozen and, thus, * no files will be allowed to be added any further. * * @returns */ public getAsBuffer(): Buffer { if (!this.isFrozen) { this[closePassSymbol](); } return super.getAsBuffer(); } /** * Exports the pass as a zip stream. When this method * is invoked, the bundle will get frozen and, thus, * no files will be allowed to be added any further. * * @returns */ public getAsStream(): Stream { if (!this.isFrozen) { this[closePassSymbol](); } return super.getAsStream(); } /** * Exports the pass as a list of file paths and buffers. * When this method is invoked, the bundle will get * frozen and, thus, no files will be allowed to be * added any further. * * This allows developers to choose a different way * of serving, analyzing or zipping the file, outside the * default compression system. * * @returns a frozen object containing files paths as key * and Buffers as content. */ public getAsRaw(): { [filePath: string]: Buffer } { if (!this.isFrozen) { this[closePassSymbol](); } return super.getAsRaw(); } // ************************** // // *** DATA SETUP METHODS *** // // ************************** // /** * Allows to add a localization details to the * final bundle with some translations. * * If the language already exists, translations will be * merged with the existing ones. * * Setting `translations` to `null` fully deletes a language, * its translations and its files. * * @see https://developer.apple.com/documentation/walletpasses/creating_the_source_for_a_pass#3736718 * @param lang * @param translations */ public localize( lang: string, translations: { [key: string]: string } | null, ) { Utils.assertUnfrozen(this); if (typeof lang !== "string") { throw new TypeError( Messages.format(Messages.LANGUAGES.INVALID_LANG, typeof lang), ); } if (translations === null) { delete this[localizationSymbol][lang]; const allFilesKeys = Object.keys(this[filesSymbol]); const langFolderIdentifier = `${lang}.lproj`; for (let i = allFilesKeys.length - 1; i >= 0; i--) { const filePath = allFilesKeys[i]; if (filePath.startsWith(langFolderIdentifier)) { delete this[filesSymbol][filePath]; } } return; } if (!translations || !Object.keys(translations).length) { console.warn( Messages.format(Messages.LANGUAGES.NO_TRANSLATIONS, lang), ); return; } this[localizationSymbol][lang] ??= {}; if (typeof translations === "object" && !Array.isArray(translations)) { Object.assign(this[localizationSymbol][lang], translations); } } /** * Allows to specify an expiration date for the pass. * * Pass `null` to remove the expiration date. * * @param date * @throws if pass is frozen due to previous export * @returns */ public setExpirationDate(date: Date | null) { Utils.assertUnfrozen(this); if (date === null) { delete this[propsSymbol]["expirationDate"]; return; } try { this[propsSymbol]["expirationDate"] = Utils.processDate(date); } catch (err) { throw new TypeError( Messages.format(Messages.DATE.INVALID, "expirationDate", date), ); } } /** * Allows setting some beacons the OS should * react to and show this pass. * * Pass `null` to remove them at all. * * @example * ```ts * PKPassInstance.setBeacons(null) * PKPassInstance.setBeacons({ * proximityUUID: "00000-000000-0000-00000000000", * }); * ``` * * @see https://developer.apple.com/documentation/walletpasses/pass/beacons * @param beacons * @throws if pass is frozen due to previous export * @returns */ public setBeacons(beacons: null): void; public setBeacons(...beacons: Schemas.Beacon[]): void; public setBeacons(...beacons: (Schemas.Beacon | null)[]) { Utils.assertUnfrozen(this); if (beacons[0] === null) { delete this[propsSymbol]["beacons"]; return; } this[propsSymbol]["beacons"] = Schemas.filterValid( Schemas.Beacon, beacons as Schemas.Beacon[], ); } /** * Allows setting some locations the OS should * react to and show this pass. * * Pass `null` to remove them at all. * * @example * ```ts * PKPassInstance.setLocations(null) * PKPassInstance.setLocations({ * latitude: 0.5333245342 * longitude: 0.2135332252 * }); * ``` * * @see https://developer.apple.com/documentation/walletpasses/pass/locations * @param locations * @throws if pass is frozen due to previous export * @returns */ public setLocations(locations: null): void; public setLocations(...locations: Schemas.Location[]): void; public setLocations(...locations: (Schemas.Location | null)[]): void { Utils.assertUnfrozen(this); if (locations[0] === null) { delete this[propsSymbol]["locations"]; return; } this[propsSymbol]["locations"] = Schemas.filterValid( Schemas.Location, locations as Schemas.Location[], ); } /** * Allows setting a series of relevancy intervals or * relevancy entries for the pass. * * @param {Schemas.RelevantDate[] | null} relevancyEntries * @returns {void} */ public setRelevantDates( relevancyEntries: Schemas.RelevantDate[] | null, ): void { Utils.assertUnfrozen(this); if (relevancyEntries === null) { this[propsSymbol]["relevantDates"] = undefined; return; } const processedDateEntries = relevancyEntries.reduce< Schemas.RelevantDate[] >((acc, entry) => { try { Schemas.validate(Schemas.RelevantDate, entry); if (isRelevantEntry(entry)) { acc.push({ relevantDate: Utils.processDate( new Date(entry.relevantDate), ), }); return acc; } acc.push({ startDate: Utils.processDate(new Date(entry.startDate)), endDate: Utils.processDate(new Date(entry.endDate)), }); } catch (err) { console.warn(new TypeError(Messages.RELEVANT_DATE.INVALID)); } return acc; }, []); this[propsSymbol]["relevantDates"] = processedDateEntries; } /** * Allows setting a relevant date in which the OS * should show this pass. * * Pass `null` to remove relevant date from this pass. * * @param {Date | null} date * @throws if pass is frozen due to previous export * * @warning `relevantDate` property has been deprecated in iOS 18 * in order to get replaced by `relevantDates` array of intervals * (`relevantDates[].startDate` + `relevantDates[].endDate`) * or single date (`relevantDates[].relevantDate`). This method will * set both the original, as the new one will get ignored in older * iOS versions. */ public setRelevantDate(date: Date | null): void { Utils.assertUnfrozen(this); if (date === null) { delete this[propsSymbol]["relevantDate"]; return; } try { this[propsSymbol]["relevantDate"] = Utils.processDate(date); } catch (err) { throw new TypeError( Messages.format(Messages.DATE.INVALID, "relevantDate", date), ); } } /** * Allows to specify some barcodes formats. * As per the current specifications, only the first * will be shown to the user, without any possibility * to change it. * * It accepts either a string from which all formats will * be generated or a specific set of barcodes objects * to be validated and used. * * Pass `null` to remove all the barcodes. * * @see https://developer.apple.com/documentation/walletpasses/pass/barcodes * @param barcodes * @throws if pass is frozen due to previous export * @returns */ public setBarcodes(barcodes: null): void; public setBarcodes(message: string): void; public setBarcodes(...barcodes: Schemas.Barcode[]): void; public setBarcodes(...barcodes: (Schemas.Barcode | string | null)[]): void { Utils.assertUnfrozen(this); if (!barcodes.length) { return; } if (barcodes[0] === null) { delete this[propsSymbol]["barcodes"]; return; } let finalBarcodes: Schemas.Barcode[]; if (typeof barcodes[0] === "string") { /** * A string has been received instead of objects. We can * only auto-fill them all with the same data. */ const supportedFormats: Array = [ "PKBarcodeFormatQR", "PKBarcodeFormatPDF417", "PKBarcodeFormatAztec", "PKBarcodeFormatCode128", ]; finalBarcodes = supportedFormats.map((format) => Schemas.validate(Schemas.Barcode, { format, message: barcodes[0], } as Schemas.Barcode), ); } else { finalBarcodes = Schemas.filterValid( Schemas.Barcode, barcodes as Schemas.Barcode[], ); } this[propsSymbol]["barcodes"] = finalBarcodes; } /** * Allows to specify details to make this, an * NFC-capable pass. * * Pass `null` as parameter to remove it at all. * * @see https://developer.apple.com/documentation/walletpasses/pass/nfc * @param data * @throws if pass is frozen due to previous export * @returns */ public setNFC(nfc: Schemas.NFC | null): void { Utils.assertUnfrozen(this); if (nfc === null) { delete this[propsSymbol]["nfc"]; return; } this[propsSymbol]["nfc"] = Schemas.validate(Schemas.NFC, nfc) ?? undefined; } } function validateJSONBuffer( buffer: Buffer, schema: Parameters[0], ): Schemas.PassProps { let contentAsJSON: Schemas.PassProps; try { contentAsJSON = JSON.parse(buffer.toString("utf8")); } catch (err) { throw new TypeError(Messages.JSON.INVALID); } return Schemas.validate(schema, contentAsJSON); } function isRelevantEntry( entry: Schemas.RelevantDate, ): entry is Schemas.RelevancyEntry { return Object.prototype.hasOwnProperty.call(entry, "relevantDate"); }