import { Stream } from "stream" ; import path from "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" ; /** Exporting for tests specs */ export const propsSymbol = Symbol ( "props" ) ; export const localizationSymbol = Symbol ( "pass.l10n" ) ; export const importMetadataSymbol = Symbol ( "import.pass.metadata" ) ; export const createManifestSymbol = Symbol ( "pass.manifest" ) ; export const closePassSymbol = Symbol ( "pass.close" ) ; export const passTypeSymbol = Symbol ( "pass.type" ) ; export const certificatesSymbol = Symbol ( "pass.certificates" ) ; export default class PKPass extends Bundle { private [ certificatesSymbol ] : Schemas . CertificatesSchema ; private [ propsSymbol ] : Schemas . PassProps = { } ; private [ localizationSymbol ] : { [ lang ] : { [ placeholder ] : string ; } ; } = { } ; private [ passTypeSymbol ] : Schemas . PassTypesProps | undefined = undefined ; param source returns static async from < S extends PKPass | Schemas . Template > ( source , props ? ) { let certificates = undefined ; let buffers = 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 ) ; } param passes static pack ( ... passes ) { if ( ! passes . every ( ( pass ) => pass instanceof PKPass ) ) { throw new Error ( Messages . PACK . INVALID ) ; } const buffers = passes . map ( ( pass ) => pass . getAsBuffer ( ) ) ; const [ bundle , freezeBundle ] = Bundle . freezable ( "application/vnd.apple.pkpasses" ) ; for ( let i = 0 ; i < buffers . length ; i ++ ) { bundle . addBuffer ( `packed-pass-${ i + 1 }.pkpass` , buffers [ i ] ) ; } freezeBundle ( ) ; return bundle ; } constructor ( buffers ? , certificates ? , props ? ) { super ( "application/vnd.apple.pkpass" ) ; if ( buffers && typeof buffers === "object" ) { const buffersEntries = Object . entries ( buffers ) ; for ( let i = buffersEntries . length , 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 ; } } param certs public set certificates ( certs ) { 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 ] ) ; } param value public set transitType ( value ) { 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.
     *
     * It will (automatically) throw an exception
     * if current type is not "boardingPass".
     */ public get transitType ( ) { return this [ propsSymbol ] [ "boardingPass" ] . transitType ; } /**
     * Allows accessing to primaryFields object.
     *
     * It will (automatically) throw an exception
     * if no valid pass.json has been parsed yet
     * or, anyway, if it has not a valid type.
     */ public get primaryFields ( ) : Schemas . Field [ ] { return this [ propsSymbol ] [ this . type ] . primaryFields ; } /**
     * Allows accessing to secondaryFields object
     *
     * It will (automatically) throw an exception
     * if no valid pass.json has been parsed yet
     * or, anyway, if a valid type has not been
     * set yet.
     */ public get secondaryFields ( ) : Schemas . Field [ ] { return this [ propsSymbol ] [ this . type ] . secondaryFields ; } /**
     * Allows accessing to auxiliaryFields object
     *
     * It will (automatically) throw an exception
     * if no valid pass.json has been parsed yet
     * or, anyway, if a valid type has not been
     * set yet.
     */ public get auxiliaryFields ( ) : Schemas . Field [ ] { return this [ propsSymbol ] [ this . type ] . auxiliaryFields ; } /**
     * Allows accessing to headerFields object
     *
     * It will (automatically) throw an exception
     * if no valid pass.json has been parsed yet
     * or, anyway, if a valid type has not been
     * set yet.
     */ public get headerFields ( ) : Schemas . Field [ ] { return this [ propsSymbol ] [ this . type ] . headerFields ; } /**
     * Allows accessing to backFields object
     *
     * It will (automatically) throw an exception
     * if no valid pass.json has been parsed yet
     * or, anyway, if a valid type has not been
     * set yet.
     */ public get backFields ( ) : Schemas . Field [ ] { return this [ propsSymbol ] [ this . type ] . backFields ; } /**
     * Allows setting a pass type.
     *
     * **Warning**: setting a type with this setter,
     * will reset all the imported or manually
     * setted fields (primaryFields, secondaryFields,
     * headerFields, auxiliaryFields, backFields)
     */ public set type ( nextType ) { 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 ; } const sharedKeysPool = new Set < string > ( ) ; this [ passTypeSymbol ] = type ; this [ propsSymbol ] [ this [ passTypeSymbol ] ] = { headerFields : new FieldsArray ( this , sharedKeysPool ) , primaryFields : new FieldsArray ( this , sharedKeysPool ) , secondaryFields : new FieldsArray ( this , sharedKeysPool ) , auxiliaryFields : new FieldsArray ( this , sharedKeysPool ) , backFields : new FieldsArray ( this , sharedKeysPool ) , transitType : undefined , } ; } public get type ( ) : Schemas . PassTypesProps | undefined { return this [ passTypeSymbol ] ?? undefined ; } param pathName param buffer public addBuffer ( pathName , buffer ) { if ( ! buffer ) { return ; } if ( /manifest|signature/ . test ( pathName ) ) { return ; } if ( /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 ( /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 ) ; } example 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
         */ const translationsFileRegexp = /(?<lang>[a-zA-Z-]{2,}).lproj\/pass\.strings/ ; let match ; if ( ( match = normalizedPathName . match ( translationsFileRegexp ) ) ) { 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 ) ; } param data private [ importMetadataSymbol ] ( data ) { 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 = [ ] , } = data [ type ] || { } ; this . headerFields . push ( ... headerFields ) ; this . primaryFields . push ( ... primaryFields ) ; this . secondaryFields . push ( ... secondaryFields ) ; this . auxiliaryFields . push ( ... auxiliaryFields ) ; this . backFields . push ( ... backFields ) ; } } /**
     * Creates the manifest starting from files
     * added to the bundle
     */ private [ createManifestSymbol ] ( ) { const manifest = Object . entries ( this [ filesSymbol ] ) . reduce < { [ key ] : 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 ] ( __test_disable_manifest_signature_generation__ = false ) { 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 ) ; const ICON_REGEX = /icon(?:@\d{1}x)?/ ; if ( ! fileNames . some ( ( fileName ) => ICON_REGEX . test ( fileName ) ) ) { console . warn ( Messages . CLOSE . MISSING_ICON ) ; } const localizationEntries = Object . entries ( this [ localizationSymbol ] ) ; for ( let i = localizationEntries . length , entry ; ( entry = localizationEntries [ -- i ] ) ; ) { const [ lang , translations ] = entry ; const stringsFile = Strings . create ( translations ) ; if ( stringsFile . length ) { super . addBuffer ( `${ lang }.lproj/pass.strings` , stringsFile ) ; } } const meetsPersonalizationRequirements = Boolean ( this [ propsSymbol ] [ "nfc" ] && this [ filesSymbol ] [ "personalization.json" ] && fileNames . find ( ( file ) => /personalizationLogo@(?:.{2})/ . test ( file ) ) ) ; if ( ! meetsPersonalizationRequirements ) { /**
             * Looking for every personalization file
             * and removing it
             */ for ( let i = 0 ; i < fileNames . length ; i ++ ) { if ( /personalization/ . test ( fileNames [ i ] ) ) { console . warn ( Messages . format ( Messages . CLOSE . PERSONALIZATION_REMOVED , fileNames [ i ] ) ) ; delete this [ filesSymbol ] [ fileNames [ i ] ] ; } } } if ( this . type === "boardingPass" && ! this . transitType ) { throw new TypeError ( Messages . CLOSE . MISSING_TRANSIT_TYPE ) ; } if ( __test_disable_manifest_signature_generation__ ) { return ; } const manifestBuffer = this [ createManifestSymbol ] ( ) ; super . addBuffer ( "manifest.json" , manifestBuffer ) ; const signatureBuffer = Signature . create ( manifestBuffer , this [ certificatesSymbol ] ) ; super . addBuffer ( "signature" , signatureBuffer ) ; } returns public getAsBuffer ( ) { if ( ! this . isFrozen ) { this [ closePassSymbol ] ( ) ; } return super . getAsBuffer ( ) ; } returns public getAsStream ( ) { if ( ! this . isFrozen ) { this [ closePassSymbol ] ( ) ; } return super . getAsStream ( ) ; } returns public getAsRaw ( ) { if ( ! this . isFrozen ) { this [ closePassSymbol ] ( ) ; } return super . getAsRaw ( ) ; } see https param lang param translations public localize ( lang , translations ) { 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 , filePath ; ( 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 ) ; } } param date returns public setExpirationDate ( date ) { 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 ) ) ; } } example see https param beacons returns public setBeacons ( beacons ) ; public setBeacons ( ... beacons ) ; public setBeacons ( ... beacons ) { Utils . assertUnfrozen ( this ) ; if ( beacons [ 0 ] === null ) { delete this [ propsSymbol ] [ "beacons" ] ; return ; } this [ propsSymbol ] [ "beacons" ] = Schemas . filterValid ( Schemas . Beacon , beacons as Schemas . Beacon [ ] ) ; } example see https param locations returns public setLocations ( locations ) ; public setLocations ( ... locations ) ; public setLocations ( ... locations ) { Utils . assertUnfrozen ( this ) ; if ( locations [ 0 ] === null ) { delete this [ propsSymbol ] [ "locations" ] ; return ; } this [ propsSymbol ] [ "locations" ] = Schemas . filterValid ( Schemas . Location , locations as Schemas . Location [ ] ) ; } param date public setRelevantDate ( date ) { 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 ) ) ; } } see https param barcodes returns public setBarcodes ( barcodes ) ; public setBarcodes ( message ) ; public setBarcodes ( ... barcodes ) ; public setBarcodes ( ... barcodes ) { Utils . assertUnfrozen ( this ) ; if ( ! barcodes . length ) { return ; } if ( barcodes [ 0 ] === null ) { delete this [ propsSymbol ] [ "barcodes" ] ; return ; } let finalBarcodes ; 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 = [ "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 ; } see https param data returns public setNFC ( nfc ) { Utils . assertUnfrozen ( this ) ; if ( nfc === null ) { delete this [ propsSymbol ] [ "nfc" ] ; return ; } this [ propsSymbol ] [ "nfc" ] = Schemas . validate ( Schemas . NFC , nfc ) ?? undefined ; } } function validateJSONBuffer ( buffer , schema ) { let contentAsJSON ; try { contentAsJSON = JSON . parse ( buffer . toString ( "utf8" ) ) ; } catch ( err ) { throw new TypeError ( Messages . JSON . INVALID ) ; } return Schemas . validate ( schema , contentAsJSON ) ; }