diff --git a/src/abstract.ts b/src/abstract.ts index 3235d55..96d2982 100644 --- a/src/abstract.ts +++ b/src/abstract.ts @@ -1,10 +1,4 @@ -import { - Certificates, - FinalCertificates, - PartitionedBundle, - OverridesSupportedOptions, - FactoryOptions, -} from "./schema"; +import * as Schemas from "./schemas"; import { getModelContents, readCertificatesFromOptions } from "./parser"; import formatMessage from "./messages"; @@ -13,14 +7,14 @@ const abmModel = Symbol("model"); const abmOverrides = Symbol("overrides"); export interface AbstractFactoryOptions - extends Omit { - certificates?: Certificates; + extends Omit { + certificates?: Schemas.Certificates; } interface AbstractModelOptions { - bundle: PartitionedBundle; - certificates: FinalCertificates; - overrides?: OverridesSupportedOptions; + bundle: Schemas.PartitionedBundle; + certificates: Schemas.CertificatesSchema; + overrides?: Schemas.OverridesSupportedOptions; } /** @@ -51,9 +45,9 @@ export async function createAbstractModel(options: AbstractFactoryOptions) { } export class AbstractModel { - private [abmCertificates]: FinalCertificates; - private [abmModel]: PartitionedBundle; - private [abmOverrides]: OverridesSupportedOptions; + private [abmCertificates]: Schemas.CertificatesSchema; + private [abmModel]: Schemas.PartitionedBundle; + private [abmOverrides]: Schemas.OverridesSupportedOptions; constructor(options: AbstractModelOptions) { this[abmModel] = options.bundle; @@ -61,15 +55,15 @@ export class AbstractModel { this[abmOverrides] = options.overrides; } - get certificates(): FinalCertificates { + get certificates(): Schemas.CertificatesSchema { return this[abmCertificates]; } - get bundle(): PartitionedBundle { + get bundle(): Schemas.PartitionedBundle { return this[abmModel]; } - get overrides(): OverridesSupportedOptions { + get overrides(): Schemas.OverridesSupportedOptions { return this[abmOverrides]; } } diff --git a/src/factory.ts b/src/factory.ts index 52e185e..2646806 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -1,11 +1,5 @@ import { Pass } from "./pass"; -import { - FactoryOptions, - BundleUnit, - FinalCertificates, - PartitionedBundle, - OverridesSupportedOptions, -} from "./schema"; +import * as Schemas from "./schemas"; import formatMessage from "./messages"; import { getModelContents, readCertificatesFromOptions } from "./parser"; import { splitBufferBundle } from "./utils"; @@ -20,8 +14,8 @@ import { AbstractModel, AbstractFactoryOptions } from "./abstract"; */ export async function createPass( - options: FactoryOptions | InstanceType, - additionalBuffers?: BundleUnit, + options: Schemas.FactoryOptions | InstanceType, + additionalBuffers?: Schemas.BundleUnit, abstractMissingData?: Omit, ): Promise { if ( @@ -35,8 +29,8 @@ export async function createPass( try { if (options instanceof AbstractModel) { - let certificates: FinalCertificates; - let overrides: OverridesSupportedOptions = { + let certificates: Schemas.CertificatesSchema; + let overrides: Schemas.OverridesSupportedOptions = { ...(options.overrides || {}), ...((abstractMissingData && abstractMissingData.overrides) || {}), @@ -85,10 +79,10 @@ export async function createPass( } function createPassInstance( - model: PartitionedBundle, - certificates: FinalCertificates, - overrides: OverridesSupportedOptions, - additionalBuffers?: BundleUnit, + model: Schemas.PartitionedBundle, + certificates: Schemas.CertificatesSchema, + overrides: Schemas.OverridesSupportedOptions, + additionalBuffers?: Schemas.BundleUnit, ) { if (additionalBuffers) { const [additionalL10n, additionalBundle] = splitBufferBundle( diff --git a/src/fieldsArray.ts b/src/fieldsArray.ts index b2543bf..d8d4a5f 100644 --- a/src/fieldsArray.ts +++ b/src/fieldsArray.ts @@ -1,4 +1,4 @@ -import * as schema from "./schema"; +import * as Schemas from "./schemas"; import debug from "debug"; const fieldsDebug = debug("passkit:fields"); @@ -23,12 +23,12 @@ export default class FieldsArray extends Array { * also uniqueKeys set. */ - push(...fieldsData: schema.Field[]): number { + push(...fieldsData: Schemas.Field[]): number { const validFields = fieldsData.reduce( - (acc: schema.Field[], current: schema.Field) => { + (acc: Schemas.Field[], current: Schemas.Field) => { if ( !(typeof current === "object") || - !schema.isValid(current, "field") + !Schemas.isValid(current, Schemas.Field) ) { return acc; } @@ -55,8 +55,8 @@ export default class FieldsArray extends Array { * also uniqueKeys set */ - pop(): schema.Field { - const element: schema.Field = Array.prototype.pop.call(this); + pop(): Schemas.Field { + const element: Schemas.Field = Array.prototype.pop.call(this); this[poolSymbol].delete(element.key); return element; } @@ -69,8 +69,8 @@ export default class FieldsArray extends Array { splice( start: number, deleteCount: number, - ...items: schema.Field[] - ): schema.Field[] { + ...items: Schemas.Field[] + ): Schemas.Field[] { const removeList = this.slice(start, deleteCount + start); removeList.forEach((item) => this[poolSymbol].delete(item.key)); diff --git a/src/parser.ts b/src/parser.ts index 08c69ab..e979472 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,14 +1,7 @@ import * as path from "path"; import forge from "node-forge"; import formatMessage from "./messages"; -import { - FactoryOptions, - PartitionedBundle, - BundleUnit, - Certificates, - FinalCertificates, - isValid, -} from "./schema"; +import * as Schemas from "./schemas"; import { removeHidden, splitBufferBundle, @@ -28,8 +21,8 @@ const { readdir: readDir, readFile } = fs.promises; * @param model */ -export async function getModelContents(model: FactoryOptions["model"]) { - let modelContents: PartitionedBundle; +export async function getModelContents(model: Schemas.FactoryOptions["model"]) { + let modelContents: Schemas.PartitionedBundle; if (typeof model === "string") { modelContents = await getModelFolderContents(model); @@ -77,9 +70,9 @@ export async function getModelContents(model: FactoryOptions["model"]) { const parsedPersonalization = JSON.parse( modelContents.bundle[personalizationJsonFile].toString("utf8"), ); - const isPersonalizationValid = isValid( + const isPersonalizationValid = Schemas.isValid( parsedPersonalization, - "personalizationDict", + Schemas.Personalization, ); if (!isPersonalizationValid) { @@ -105,7 +98,7 @@ export async function getModelContents(model: FactoryOptions["model"]) { export async function getModelFolderContents( model: string, -): Promise { +): Promise { try { const modelPath = `${model}${(!path.extname(model) && ".pass") || ""}`; const modelFilesList = await readDir(modelPath); @@ -142,7 +135,7 @@ export async function getModelFolderContents( ), ); - const bundle: BundleUnit = Object.assign( + const bundle: Schemas.BundleUnit = Object.assign( {}, ...rawBundleFiles.map((fileName, index) => ({ [fileName]: rawBundleBuffers[index], @@ -151,7 +144,7 @@ export async function getModelFolderContents( // Reading concurrently localizations folder // and their files and their buffers - const L10N_FilesListByFolder: Array = await Promise.all( + const L10N_FilesListByFolder: Array = await Promise.all( l10nFolders.map(async (folderPath) => { // Reading current folder const currentLangPath = path.join(modelPath, folderPath); @@ -172,23 +165,27 @@ export async function getModelFolderContents( // Assigning each file path to its buffer // and discarding the empty ones - return validFiles.reduce((acc, file, index) => { - if (!buffers[index].length) { - return acc; - } + return validFiles.reduce( + (acc, file, index) => { + if (!buffers[index].length) { + return acc; + } - const fileComponents = file.split(path.sep); - const fileName = fileComponents[fileComponents.length - 1]; + const fileComponents = file.split(path.sep); + const fileName = + fileComponents[fileComponents.length - 1]; - return { - ...acc, - [fileName]: buffers[index], - }; - }, {}); + return { + ...acc, + [fileName]: buffers[index], + }; + }, + {}, + ); }), ); - const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign( + const l10nBundle: Schemas.PartitionedBundle["l10nBundle"] = Object.assign( {}, ...L10N_FilesListByFolder.map((folder, index) => ({ [l10nFolders[index]]: folder, @@ -226,20 +223,21 @@ export async function getModelFolderContents( * @param model */ -export function getModelBufferContents(model: BundleUnit): PartitionedBundle { - const rawBundle = removeHidden(Object.keys(model)).reduce( - (acc, current) => { - // Checking if current file is one of the autogenerated ones or if its - // content is not available +export function getModelBufferContents( + model: Schemas.BundleUnit, +): Schemas.PartitionedBundle { + const rawBundle = removeHidden( + Object.keys(model), + ).reduce((acc, current) => { + // Checking if current file is one of the autogenerated ones or if its + // content is not available - if (/(manifest|signature)/.test(current) || !model[current]) { - return acc; - } + if (/(manifest|signature)/.test(current) || !model[current]) { + return acc; + } - return { ...acc, [current]: model[current] }; - }, - {}, - ); + return { ...acc, [current]: model[current] }; + }, {}); const bundleKeys = Object.keys(rawBundle); @@ -266,18 +264,18 @@ export function getModelBufferContents(model: BundleUnit): PartitionedBundle { * @param options */ -type flatCertificates = Omit & { +type flatCertificates = Omit & { signerKey: string; }; export async function readCertificatesFromOptions( - options: Certificates, -): Promise { + options: Schemas.Certificates, +): Promise { if ( !( options && Object.keys(options).length && - isValid(options, "certificatesSchema") + Schemas.isValid(options, Schemas.CertificatesSchema) ) ) { throw new Error(formatMessage("CP_NO_CERTS")); diff --git a/src/pass.ts b/src/pass.ts index 15dd629..94f659c 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -4,7 +4,7 @@ import debug from "debug"; import { Stream } from "stream"; import { ZipFile } from "yazl"; -import * as schema from "./schema"; +import * as Schemas from "./schemas"; import formatMessage from "./messages"; import FieldsArray from "./fieldsArray"; import { @@ -14,6 +14,7 @@ import { deletePersonalization, getAllFilesWithName, } from "./utils"; +import type Joi from "joi"; const barcodeDebug = debug("passkit:barcode"); const genericDebug = debug("passkit:generic"); @@ -21,22 +22,22 @@ const genericDebug = debug("passkit:generic"); const transitType = Symbol("transitType"); const passProps = Symbol("_props"); -const propsSchemaMap = new Map([ - ["barcodes", "barcode"], - ["barcode", "barcode"], - ["beacons", "beaconsDict"], - ["locations", "locationsDict"], - ["nfc", "nfcDict"], +const propsSchemaMap = new Map>([ + ["barcodes", Schemas.Barcode], + ["barcode", Schemas.Barcode], + ["beacons", Schemas.Beacon], + ["locations", Schemas.Location], + ["nfc", Schemas.NFC], ]); export class Pass { - private bundle: schema.BundleUnit; - private l10nBundles: schema.PartitionedBundle["l10nBundle"]; - private _fields: (keyof schema.PassFields)[]; - private [passProps]: schema.ValidPass = {}; - private type: keyof schema.ValidPassType; + private bundle: Schemas.BundleUnit; + private l10nBundles: Schemas.PartitionedBundle["l10nBundle"]; + private _fields: (keyof Schemas.PassFields)[]; + private [passProps]: Schemas.ValidPass = {}; + private type: keyof Schemas.ValidPassType; private fieldsKeys: Set = new Set(); - private passCore: schema.ValidPass; + private passCore: Schemas.ValidPass; // Setting these as possibly undefined because we set // them all in an loop later @@ -46,14 +47,14 @@ export class Pass { public auxiliaryFields: FieldsArray | undefined; public backFields: FieldsArray | undefined; - private Certificates: schema.FinalCertificates; + private Certificates: Schemas.CertificatesSchema; private [transitType]: string = ""; private l10nTranslations: { [languageCode: string]: { [placeholder: string]: string }; } = {}; - constructor(options: schema.PassInstance) { - if (!schema.isValid(options, "instance")) { + constructor(options: Schemas.PassInstance) { + if (!Schemas.isValid(options, Schemas.PassInstance)) { throw new Error(formatMessage("REQUIR_VALID_FAILED")); } @@ -70,10 +71,10 @@ export class Pass { } // Parsing the options and extracting only the valid ones. - const validOverrides = schema.getValidated( + const validOverrides = Schemas.getValidated( options.overrides || {}, - "supportedOptions", - ) as schema.OverridesSupportedOptions; + Schemas.OverridesSupportedOptions, + ); if (validOverrides === null) { throw new Error(formatMessage("OVV_KEYS_BADFORMAT")); @@ -81,7 +82,7 @@ export class Pass { this.type = Object.keys(this.passCore).find((key) => /(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key), - ) as keyof schema.ValidPassType; + ) as keyof Schemas.ValidPassType; if (!this.type) { throw new Error(formatMessage("NO_PASS_TYPE")); @@ -90,8 +91,8 @@ export class Pass { // Parsing and validating pass.json keys const passCoreKeys = Object.keys( this.passCore, - ) as (keyof schema.ValidPass)[]; - const validatedPassKeys = passCoreKeys.reduce( + ) as (keyof Schemas.ValidPass)[]; + const validatedPassKeys = passCoreKeys.reduce( (acc, current) => { if (this.type === current) { // We want to exclude type keys (eventTicket, @@ -109,16 +110,16 @@ export class Pass { const currentSchema = propsSchemaMap.get(current)!; if (Array.isArray(this.passCore[current])) { - const valid = getValidInArray( + const valid = getValidInArray( currentSchema, - this.passCore[current] as schema.ArrayPassSchema[], + this.passCore[current] as Schemas.ArrayPassSchema[], ); return { ...acc, [current]: valid }; } else { return { ...acc, [current]: - (schema.isValid( + (Schemas.isValid( this.passCore[current], currentSchema, ) && @@ -155,7 +156,7 @@ export class Pass { this[fieldName] = new FieldsArray( this.fieldsKeys, ...(this.passCore[this.type][fieldName] || []).filter((field) => - schema.isValid(field, "field"), + Schemas.isValid(field, Schemas.Field), ), ); }); @@ -193,7 +194,7 @@ export class Pass { ); } - const finalBundle = { ...this.bundle } as schema.BundleUnit; + const finalBundle = { ...this.bundle } as Schemas.BundleUnit; /** * Iterating through languages and generating pass.string file @@ -262,7 +263,7 @@ export class Pass { * and returning the compiled manifest */ const archive = new ZipFile(); - const manifest = Object.keys(finalBundle).reduce( + const manifest = Object.keys(finalBundle).reduce( (acc, current) => { let hashFlow = forge.md.sha1.create(); @@ -360,14 +361,14 @@ export class Pass { */ beacons(resetFlag: null): this; - beacons(...data: schema.Beacon[]): this; - beacons(...data: (schema.Beacon | null)[]): this { + beacons(...data: Schemas.Beacon[]): this; + beacons(...data: (Schemas.Beacon | null)[]): this { if (data[0] === null) { delete this[passProps]["beacons"]; return this; } - const valid = processRelevancySet("beacons", data as schema.Beacon[]); + const valid = processRelevancySet(Schemas.Beacon, data); if (valid.length) { this[passProps]["beacons"] = valid; @@ -383,17 +384,14 @@ export class Pass { */ locations(resetFlag: null): this; - locations(...data: schema.Location[]): this; - locations(...data: (schema.Location | null)[]): this { + locations(...data: Schemas.Location[]): this; + locations(...data: (Schemas.Location | null)[]): this { if (data[0] === null) { delete this[passProps]["locations"]; return this; } - const valid = processRelevancySet( - "locations", - data as schema.Location[], - ); + const valid = processRelevancySet(Schemas.Location, data); if (valid.length) { this[passProps]["locations"] = valid; @@ -436,8 +434,8 @@ export class Pass { barcodes(resetFlag: null): this; barcodes(message: string): this; - barcodes(...data: schema.Barcode[]): this; - barcodes(...data: (schema.Barcode | null | string)[]): this { + barcodes(...data: Schemas.Barcode[]): this; + barcodes(...data: (Schemas.Barcode | null | string)[]): this { if (data[0] === null) { delete this[passProps]["barcodes"]; return this; @@ -461,13 +459,16 @@ export class Pass { * Validation assign default value to missing parameters (if any). */ - const validBarcodes = data.reduce( + const validBarcodes = data.reduce( (acc, current) => { if (!(current && current instanceof Object)) { return acc; } - const validated = schema.getValidated(current, "barcode"); + const validated = Schemas.getValidated( + current, + Schemas.Barcode, + ); if ( !( @@ -479,7 +480,7 @@ export class Pass { return acc; } - return [...acc, validated] as schema.Barcode[]; + return [...acc, validated] as Schemas.Barcode[]; }, [], ); @@ -502,7 +503,7 @@ export class Pass { * @return {this} */ - barcode(chosenFormat: schema.BarcodeFormat | null): this { + barcode(chosenFormat: Schemas.BarcodeFormat | null): this { const { barcodes } = this[passProps]; if (chosenFormat === null) { @@ -548,7 +549,7 @@ export class Pass { * @see https://apple.co/2wTxiaC */ - nfc(data: schema.NFC | null): this { + nfc(data: Schemas.NFC | null): this { if (data === null) { delete this[passProps]["nfc"]; return this; @@ -559,7 +560,7 @@ export class Pass { data && typeof data === "object" && !Array.isArray(data) && - schema.isValid(data, "nfcDict") + Schemas.isValid(data, Schemas.NFC) ) ) { genericDebug(formatMessage("NFC_INVALID")); @@ -579,7 +580,7 @@ export class Pass { * @returns The properties will be inserted in the pass. */ - get props(): Readonly { + get props(): Readonly { return this[passProps]; } @@ -591,7 +592,7 @@ export class Pass { * @returns {Buffer} */ - private _sign(manifest: schema.Manifest): Buffer { + private _sign(manifest: Schemas.Manifest): Buffer { const signature = forge.pkcs7.createSignedData(); signature.content = forge.util.createBuffer( @@ -667,7 +668,7 @@ export class Pass { private _patch(passCoreBuffer: Buffer): Buffer { const passFile = JSON.parse( passCoreBuffer.toString(), - ) as schema.ValidPass; + ) as Schemas.ValidPass; if (Object.keys(this[passProps]).length) { /* @@ -680,7 +681,7 @@ export class Pass { "backgroundColor", "foregroundColor", "labelColor", - ] as Array; + ] as Array; passColors .filter( (v) => @@ -705,7 +706,7 @@ export class Pass { } set transitType(value: string) { - if (!schema.isValid(value, "transitType")) { + if (!Schemas.isValid(value, Schemas.TransitType)) { genericDebug(formatMessage("TRSTYPE_NOT_VALID", value)); this[transitType] = this[transitType] || ""; return; @@ -727,7 +728,7 @@ export class Pass { * @return Array of barcodeDict compliant */ -function barcodesFromUncompleteData(message: string): schema.Barcode[] { +function barcodesFromUncompleteData(message: string): Schemas.Barcode[] { if (!(message && typeof message === "string")) { return []; } @@ -739,21 +740,24 @@ function barcodesFromUncompleteData(message: string): schema.Barcode[] { "PKBarcodeFormatCode128", ].map( (format) => - schema.getValidated( + Schemas.getValidated( { format, message }, - "barcode", - ) as schema.Barcode, + Schemas.Barcode, + ) as Schemas.Barcode, ); } -function processRelevancySet(key: string, data: T[]): T[] { - return getValidInArray(`${key}Dict` as schema.Schema, data); +function processRelevancySet(schema: Joi.ObjectSchema, data: T[]): T[] { + return getValidInArray(schema, data); } -function getValidInArray(schemaName: schema.Schema, contents: T[]): T[] { +function getValidInArray( + schemaName: Joi.ObjectSchema, + contents: T[], +): T[] { return contents.filter( (current) => - Object.keys(current).length && schema.isValid(current, schemaName), + Object.keys(current).length && Schemas.isValid(current, schemaName), ); } diff --git a/src/schema.ts b/src/schema.ts deleted file mode 100644 index 368cf22..0000000 --- a/src/schema.ts +++ /dev/null @@ -1,624 +0,0 @@ -import Joi from "joi"; -import debug from "debug"; - -const schemaDebug = debug("Schema"); - -export interface Manifest { - [key: string]: string; -} - -export interface Certificates { - wwdr?: string; - signerCert?: string; - signerKey?: - | { - keyFile: string; - passphrase?: string; - } - | string; -} - -export interface FactoryOptions { - model: BundleUnit | string; - certificates: Certificates; - overrides?: OverridesSupportedOptions; -} - -export interface BundleUnit { - [key: string]: Buffer; -} - -export interface PartitionedBundle { - bundle: BundleUnit; - l10nBundle: { - [key: string]: BundleUnit; - }; -} - -export interface FinalCertificates { - wwdr: string; - signerCert: string; - signerKey: string; -} - -export interface PassInstance { - model: PartitionedBundle; - certificates: FinalCertificates; - overrides?: OverridesSupportedOptions; -} - -// ************************************ // -// * JOI Schemas + Related Interfaces * // -// ************************************ // - -const certificatesSchema = Joi.object() - .keys({ - wwdr: Joi.alternatives(Joi.binary(), Joi.string()).required(), - signerCert: Joi.alternatives(Joi.binary(), Joi.string()).required(), - signerKey: Joi.alternatives() - .try( - Joi.object().keys({ - keyFile: Joi.alternatives( - Joi.binary(), - Joi.string(), - ).required(), - passphrase: Joi.string().required(), - }), - Joi.alternatives(Joi.binary(), Joi.string()), - ) - .required(), - }) - .required(); - -const instance = Joi.object().keys({ - model: Joi.alternatives(Joi.object(), Joi.string()).required(), - certificates: Joi.object(), - overrides: Joi.object(), -}); - -export interface OverridesSupportedOptions { - serialNumber?: string; - description?: string; - organizationName?: string; - passTypeIdentifier?: string; - teamIdentifier?: string; - appLaunchURL?: string; - associatedStoreIdentifiers?: Array; - userInfo?: { [key: string]: any }; - webServiceURL?: string; - authenticationToken?: string; - sharingProhibited?: boolean; - backgroundColor?: string; - foregroundColor?: string; - labelColor?: string; - groupingIdentifier?: string; - suppressStripShine?: boolean; - logoText?: string; - maxDistance?: number; -} - -const supportedOptions = Joi.object() - .keys({ - serialNumber: Joi.string(), - description: Joi.string(), - organizationName: Joi.string(), - passTypeIdentifier: Joi.string(), - teamIdentifier: Joi.string(), - appLaunchURL: Joi.string(), - associatedStoreIdentifiers: Joi.array().items(Joi.number()), - userInfo: Joi.alternatives(Joi.object().unknown(), Joi.array()), - // parsing url as set of words and nums followed by dots, optional port and any possible path after - webServiceURL: Joi.string().regex( - /https?:\/\/(?:[a-z0-9]+\.?)+(?::\d{2,})?(?:\/[\S]+)*/, - ), - authenticationToken: Joi.string().min(16), - sharingProhibited: Joi.boolean(), - backgroundColor: Joi.string().min(10).max(16), - foregroundColor: Joi.string().min(10).max(16), - labelColor: Joi.string().min(10).max(16), - groupingIdentifier: Joi.string(), - suppressStripShine: Joi.boolean(), - logoText: Joi.string(), - maxDistance: Joi.number().positive(), - }) - .with("webServiceURL", "authenticationToken"); - -/* For a correct usage of semantics, please refer to https://apple.co/2I66Phk */ - -interface CurrencyAmount { - currencyCode: string; - amount: string; -} - -const currencyAmount = Joi.object().keys({ - currencyCode: Joi.string().required(), - amount: Joi.string().required(), -}); - -interface PersonNameComponent { - givenName: string; - familyName: string; -} - -const personNameComponents = Joi.object().keys({ - givenName: Joi.string().required(), - familyName: Joi.string().required(), -}); - -interface Seat { - seatSection?: string; - seatRow?: string; - seatNumber?: string; - seatIdentifier?: string; - seatType?: string; - seatDescription?: string; -} - -const seat = Joi.object().keys({ - seatSection: Joi.string(), - seatRow: Joi.string(), - seatNumber: Joi.string(), - seatIdentifier: Joi.string(), - seatType: Joi.string(), - seatDescription: Joi.string(), -}); - -const location = Joi.object().keys({ - latitude: Joi.number().required(), - longitude: Joi.number().required(), -}); - -interface Semantics { - totalPrice?: CurrencyAmount; - duration?: number; - seats?: Seat[]; - silenceRequested?: boolean; - departureLocation?: Location; - destinationLocation?: Location; - destinationLocationDescription?: Location; - transitProvider?: string; - vehicleName?: string; - vehicleType?: string; - originalDepartureDate?: string; - currentDepartureDate?: string; - originalArrivalDate?: string; - currentArrivalDate?: string; - originalBoardingDate?: string; - currentBoardingDate?: string; - boardingGroup?: string; - boardingSequenceNumber?: string; - confirmationNumber?: string; - transitStatus?: string; - transitStatuReason?: string; - passengetName?: PersonNameComponent; - membershipProgramName?: string; - membershipProgramNumber?: string; - priorityStatus?: string; - securityScreening?: string; - flightCode?: string; - airlineCode?: string; - flightNumber?: number; - departureAirportCode?: string; - departureAirportName?: string; - destinationTerminal?: string; - destinationGate?: string; - departurePlatform?: string; - departureStationName?: string; - destinationPlatform?: string; - destinationStationName?: string; - carNumber?: string; - eventName?: string; - venueName?: string; - venueLocation?: Location; - venueEntrance?: string; - venuePhoneNumber?: string; - venueRoom?: string; - eventType?: - | "PKEventTypeGeneric" - | "PKEventTypeLivePerformance" - | "PKEventTypeMovie" - | "PKEventTypeSports" - | "PKEventTypeConference" - | "PKEventTypeConvention" - | "PKEventTypeWorkshop" - | "PKEventTypeSocialGathering"; - eventStartDate?: string; - eventEndDate?: string; - artistIDs?: string; - performerNames?: string[]; - genre?: string; - leagueName?: string; - leagueAbbreviation?: string; - homeTeamLocation?: string; - homeTeamName?: string; - homeTeamAbbreviation?: string; - awayTeamLocation?: string; - awayTeamName?: string; - awayTeamAbbreviation?: string; - sportName?: string; - balance?: CurrencyAmount; -} - -const semantics = Joi.object().keys({ - // All - totalPrice: currencyAmount, - // boarding Passes and Events - duration: Joi.number(), - seats: Joi.array().items(seat), - silenceRequested: Joi.boolean(), - // all boarding passes - departureLocation: location, - destinationLocation: location, - destinationLocationDescription: location, - transitProvider: Joi.string(), - vehicleName: Joi.string(), - vehicleType: Joi.string(), - originalDepartureDate: Joi.string(), - currentDepartureDate: Joi.string(), - originalArrivalDate: Joi.string(), - currentArrivalDate: Joi.string(), - originalBoardingDate: Joi.string(), - currentBoardingDate: Joi.string(), - boardingGroup: Joi.string(), - boardingSequenceNumber: Joi.string(), - confirmationNumber: Joi.string(), - transitStatus: Joi.string(), - transitStatuReason: Joi.string(), - passengetName: personNameComponents, - membershipProgramName: Joi.string(), - membershipProgramNumber: Joi.string(), - priorityStatus: Joi.string(), - securityScreening: Joi.string(), - // Airline Boarding Passes - flightCode: Joi.string(), - airlineCode: Joi.string(), - flightNumber: Joi.number(), - departureAirportCode: Joi.string(), - departureAirportName: Joi.string(), - destinationTerminal: Joi.string(), - destinationGate: Joi.string(), - // Train and Other Rail Boarding Passes - departurePlatform: Joi.string(), - departureStationName: Joi.string(), - destinationPlatform: Joi.string(), - destinationStationName: Joi.string(), - carNumber: Joi.string(), - // All Event Tickets - eventName: Joi.string(), - venueName: Joi.string(), - venueLocation: location, - venueEntrance: Joi.string(), - venuePhoneNumber: Joi.string(), - venueRoom: Joi.string(), - eventType: Joi.string().regex( - /(PKEventTypeGeneric|PKEventTypeLivePerformance|PKEventTypeMovie|PKEventTypeSports|PKEventTypeConference|PKEventTypeConvention|PKEventTypeWorkshop|PKEventTypeSocialGathering)/, - ), - eventStartDate: Joi.string(), - eventEndDate: Joi.string(), - artistIDs: Joi.string(), - performerNames: Joi.array().items(Joi.string()), - genre: Joi.string(), - // Sport Event Tickets - leagueName: Joi.string(), - leagueAbbreviation: Joi.string(), - homeTeamLocation: Joi.string(), - homeTeamName: Joi.string(), - homeTeamAbbreviation: Joi.string(), - awayTeamLocation: Joi.string(), - awayTeamName: Joi.string(), - awayTeamAbbreviation: Joi.string(), - sportName: Joi.string(), - // Store Card Passes - balance: currencyAmount, -}); - -export interface ValidPassType { - boardingPass?: PassFields & { transitType: TransitType }; - eventTicket?: PassFields; - coupon?: PassFields; - generic?: PassFields; - storeCard?: PassFields; -} - -interface PassInterfacesProps { - barcode?: Barcode; - barcodes?: Barcode[]; - beacons?: Beacon[]; - locations?: Location[]; - maxDistance?: number; - relevantDate?: string; - nfc?: NFC; - expirationDate?: string; - voided?: boolean; -} - -type AllPassProps = PassInterfacesProps & - ValidPassType & - OverridesSupportedOptions; -export type ValidPass = { - [K in keyof AllPassProps]: AllPassProps[K]; -}; -export type PassColors = Pick< - OverridesSupportedOptions, - "backgroundColor" | "foregroundColor" | "labelColor" ->; - -export interface Barcode { - altText?: string; - messageEncoding?: string; - format: string; - message: string; -} - -export type BarcodeFormat = - | "PKBarcodeFormatQR" - | "PKBarcodeFormatPDF417" - | "PKBarcodeFormatAztec" - | "PKBarcodeFormatCode128"; - -const barcode = Joi.object().keys({ - altText: Joi.string(), - messageEncoding: Joi.string().default("iso-8859-1"), - format: Joi.string() - .required() - .regex( - /(PKBarcodeFormatQR|PKBarcodeFormatPDF417|PKBarcodeFormatAztec|PKBarcodeFormatCode128)/, - "barcodeType", - ), - message: Joi.string().required(), -}); - -export interface Field { - attributedValue?: string | number | Date; - changeMessage?: string; - dataDetectorTypes?: string[]; - label?: string; - textAlignment?: string; - key: string; - value: string | number | Date; - semantics?: Semantics; - dateStyle?: string; - ignoresTimeZone?: boolean; - isRelative?: boolean; - timeStyle?: string; - currencyCode?: string; - numberStyle?: string; -} - -const field = Joi.object().keys({ - attributedValue: Joi.alternatives( - Joi.string().allow(""), - Joi.number(), - Joi.date().iso(), - ), - changeMessage: Joi.string(), - dataDetectorTypes: Joi.array().items( - Joi.string().regex( - /(PKDataDetectorTypePhoneNumber|PKDataDetectorTypeLink|PKDataDetectorTypeAddress|PKDataDetectorTypeCalendarEvent)/, - "dataDetectorType", - ), - ), - label: Joi.string().allow(""), - textAlignment: Joi.string().regex( - /(PKTextAlignmentLeft|PKTextAlignmentCenter|PKTextAlignmentRight|PKTextAlignmentNatural)/, - "graphic-alignment", - ), - key: Joi.string().required(), - value: Joi.alternatives( - Joi.string().allow(""), - Joi.number(), - Joi.date().iso(), - ).required(), - semantics, - // date fields formatters, all optionals - dateStyle: Joi.string().regex( - /(PKDateStyleNone|PKDateStyleShort|PKDateStyleMedium|PKDateStyleLong|PKDateStyleFull)/, - "date style", - ), - ignoresTimeZone: Joi.boolean(), - isRelative: Joi.boolean(), - timeStyle: Joi.string().regex( - /(PKDateStyleNone|PKDateStyleShort|PKDateStyleMedium|PKDateStyleLong|PKDateStyleFull)/, - "date style", - ), - // number fields formatters, all optionals - currencyCode: Joi.string().when("value", { - is: Joi.number(), - otherwise: Joi.string().forbidden(), - }), - numberStyle: Joi.string() - .regex( - /(PKNumberStyleDecimal|PKNumberStylePercent|PKNumberStyleScientific|PKNumberStyleSpellOut)/, - ) - .when("value", { - is: Joi.number(), - otherwise: Joi.string().forbidden(), - }), -}); - -export interface Beacon { - major?: number; - minor?: number; - relevantText?: string; - proximityUUID: string; -} - -const beaconsDict = Joi.object().keys({ - major: Joi.number() - .integer() - .positive() - .max(65535) - .greater(Joi.ref("minor")), - minor: Joi.number().integer().min(0).max(65535), - proximityUUID: Joi.string().required(), - relevantText: Joi.string(), -}); - -export interface Location { - relevantText?: string; - altitude?: number; - latitude: number; - longitude: number; -} - -const locationsDict = Joi.object().keys({ - altitude: Joi.number(), - latitude: Joi.number().required(), - longitude: Joi.number().required(), - relevantText: Joi.string(), -}); - -export interface PassFields { - auxiliaryFields: (Field & { row?: number })[]; - backFields: Field[]; - headerFields: Field[]; - primaryFields: Field[]; - secondaryFields: Field[]; -} - -const passDict = Joi.object().keys({ - auxiliaryFields: Joi.array().items( - Joi.object() - .keys({ - row: Joi.number().max(1).min(0), - }) - .concat(field), - ), - backFields: Joi.array().items(field), - headerFields: Joi.array().items(field), - primaryFields: Joi.array().items(field), - secondaryFields: Joi.array().items(field), -}); - -export type TransitType = - | "PKTransitTypeAir" - | "PKTransitTypeBoat" - | "PKTransitTypeBus" - | "PKTransitTypeGeneric" - | "PKTransitTypeTrain"; - -const transitType = Joi.string().regex( - /(PKTransitTypeAir|PKTransitTypeBoat|PKTransitTypeBus|PKTransitTypeGeneric|PKTransitTypeTrain)/, -); - -export interface NFC { - message: string; - encryptionPublicKey?: string; -} - -const nfcDict = Joi.object().keys({ - message: Joi.string().required().max(64), - encryptionPublicKey: Joi.string(), -}); - -// ************************************* // -// *** Personalizable Passes Schemas *** // -// ************************************* // - -export interface Personalization { - requiredPersonalizationFields: PRSField[]; - description: string; - termsAndConditions?: string; -} - -type PRSField = - | "PKPassPersonalizationFieldName" - | "PKPassPersonalizationFieldPostalCode" - | "PKPassPersonalizationFieldEmailAddress" - | "PKPassPersonalizationFieldPhoneNumber"; - -const personalizationDict = Joi.object().keys({ - requiredPersonalizationFields: Joi.array() - .items( - "PKPassPersonalizationFieldName", - "PKPassPersonalizationFieldPostalCode", - "PKPassPersonalizationFieldEmailAddress", - "PKPassPersonalizationFieldPhoneNumber", - ) - .required(), - description: Joi.string().required(), - termsAndConditions: Joi.string(), -}); - -// --------- UTILITIES ---------- // - -const schemas = { - instance, - certificatesSchema, - barcode, - field, - passDict, - beaconsDict, - locationsDict, - transitType, - nfcDict, - supportedOptions, - personalizationDict, -}; - -export type Schema = keyof typeof schemas; -export type ArrayPassSchema = Beacon | Location | Barcode; - -function resolveSchemaName(name: Schema) { - return schemas[name] || undefined; -} - -/** - * Checks if the passed options are compliant with the indicated schema - * @param {any} opts - options to be checks - * @param {string} schemaName - the indicated schema (will be converted) - * @returns {boolean} - result of the check - */ - -export function isValid(opts: any, schemaName: Schema): boolean { - const resolvedSchema = resolveSchemaName(schemaName); - - if (!resolvedSchema) { - schemaDebug( - `validation failed due to missing or mispelled schema name`, - ); - return false; - } - - const validation = resolvedSchema.validate(opts); - - if (validation.error) { - schemaDebug( - `validation failed due to error: ${validation.error.message}`, - ); - } - - return !validation.error; -} - -/** - * Executes the validation in verbose mode, exposing the value or an empty object - * @param {object} opts - to be validated - * @param {*} schemaName - selected schema - * @returns {object} the filtered value or empty object - */ - -export function getValidated( - opts: any, - schemaName: Schema, -): T | null { - const resolvedSchema = resolveSchemaName(schemaName); - - if (!resolvedSchema) { - schemaDebug( - `validation failed due to missing or mispelled schema name`, - ); - return null; - } - - const validation = resolvedSchema.validate(opts, { stripUnknown: true }); - - if (validation.error) { - schemaDebug( - `Validation failed in getValidated due to error: ${validation.error.message}`, - ); - return null; - } - - return validation.value; -} diff --git a/src/schemas/barcode.ts b/src/schemas/barcode.ts new file mode 100644 index 0000000..bbcba0b --- /dev/null +++ b/src/schemas/barcode.ts @@ -0,0 +1,26 @@ +import Joi from "joi"; + +export type BarcodeFormat = + | "PKBarcodeFormatQR" + | "PKBarcodeFormatPDF417" + | "PKBarcodeFormatAztec" + | "PKBarcodeFormatCode128"; + +export interface Barcode { + altText?: string; + messageEncoding?: string; + format: BarcodeFormat; + message: string; +} + +export const Barcode = Joi.object().keys({ + altText: Joi.string(), + messageEncoding: Joi.string().default("iso-8859-1"), + format: Joi.string() + .required() + .regex( + /(PKBarcodeFormatQR|PKBarcodeFormatPDF417|PKBarcodeFormatAztec|PKBarcodeFormatCode128)/, + "barcodeType", + ), + message: Joi.string().required(), +}); diff --git a/src/schemas/beacon.ts b/src/schemas/beacon.ts new file mode 100644 index 0000000..d946b3b --- /dev/null +++ b/src/schemas/beacon.ts @@ -0,0 +1,19 @@ +import Joi from "joi"; + +export interface Beacon { + major?: number; + minor?: number; + relevantText?: string; + proximityUUID: string; +} + +export const Beacon = Joi.object().keys({ + major: Joi.number() + .integer() + .positive() + .max(65535) + .greater(Joi.ref("minor")), + minor: Joi.number().integer().min(0).max(65535), + proximityUUID: Joi.string().required(), + relevantText: Joi.string(), +}); diff --git a/src/schemas/field.ts b/src/schemas/field.ts new file mode 100644 index 0000000..42f71bf --- /dev/null +++ b/src/schemas/field.ts @@ -0,0 +1,70 @@ +import Joi from "joi"; +import { Semantics } from "./semantics"; + +export interface Field { + attributedValue?: string | number | Date; + changeMessage?: string; + dataDetectorTypes?: string[]; + label?: string; + textAlignment?: string; + key: string; + value: string | number | Date; + semantics?: Semantics; + dateStyle?: string; + ignoresTimeZone?: boolean; + isRelative?: boolean; + timeStyle?: string; + currencyCode?: string; + numberStyle?: string; +} + +export const Field = Joi.object().keys({ + attributedValue: Joi.alternatives( + Joi.string().allow(""), + Joi.number(), + Joi.date().iso(), + ), + changeMessage: Joi.string(), + dataDetectorTypes: Joi.array().items( + Joi.string().regex( + /(PKDataDetectorTypePhoneNumber|PKDataDetectorTypeLink|PKDataDetectorTypeAddress|PKDataDetectorTypeCalendarEvent)/, + "dataDetectorType", + ), + ), + label: Joi.string().allow(""), + textAlignment: Joi.string().regex( + /(PKTextAlignmentLeft|PKTextAlignmentCenter|PKTextAlignmentRight|PKTextAlignmentNatural)/, + "graphic-alignment", + ), + key: Joi.string().required(), + value: Joi.alternatives( + Joi.string().allow(""), + Joi.number(), + Joi.date().iso(), + ).required(), + semantics: Semantics, + // date fields formatters, all optionals + dateStyle: Joi.string().regex( + /(PKDateStyleNone|PKDateStyleShort|PKDateStyleMedium|PKDateStyleLong|PKDateStyleFull)/, + "date style", + ), + ignoresTimeZone: Joi.boolean(), + isRelative: Joi.boolean(), + timeStyle: Joi.string().regex( + /(PKDateStyleNone|PKDateStyleShort|PKDateStyleMedium|PKDateStyleLong|PKDateStyleFull)/, + "date style", + ), + // number fields formatters, all optionals + currencyCode: Joi.string().when("value", { + is: Joi.number(), + otherwise: Joi.string().forbidden(), + }), + numberStyle: Joi.string() + .regex( + /(PKNumberStyleDecimal|PKNumberStylePercent|PKNumberStyleScientific|PKNumberStyleSpellOut)/, + ) + .when("value", { + is: Joi.number(), + otherwise: Joi.string().forbidden(), + }), +}); diff --git a/src/schemas/index.ts b/src/schemas/index.ts new file mode 100644 index 0000000..bca6bff --- /dev/null +++ b/src/schemas/index.ts @@ -0,0 +1,244 @@ +export * from "./barcode"; +export * from "./beacon"; +export * from "./location"; +export * from "./field"; +export * from "./nfc"; +export * from "./semantics"; +export * from "./passFields"; +export * from "./personalization"; + +import Joi from "joi"; +import debug from "debug"; + +import { Barcode } from "./barcode"; +import { Location } from "./location"; +import { Beacon } from "./beacon"; +import { NFC } from "./nfc"; +import { Field } from "./field"; +import { PassFields, TransitType } from "./passFields"; +import { Personalization } from "./personalization"; + +const schemaDebug = debug("Schema"); + +export interface Manifest { + [key: string]: string; +} + +export interface Certificates { + wwdr?: string; + signerCert?: string; + signerKey?: + | { + keyFile: string; + passphrase?: string; + } + | string; +} + +export interface FactoryOptions { + model: BundleUnit | string; + certificates: Certificates; + overrides?: OverridesSupportedOptions; +} + +export interface BundleUnit { + [key: string]: Buffer; +} + +export interface PartitionedBundle { + bundle: BundleUnit; + l10nBundle: { + [key: string]: BundleUnit; + }; +} + +export interface CertificatesSchema { + wwdr: string; + signerCert: string; + signerKey: string; +} + +export const CertificatesSchema = Joi.object() + .keys({ + wwdr: Joi.alternatives(Joi.binary(), Joi.string()).required(), + signerCert: Joi.alternatives(Joi.binary(), Joi.string()).required(), + signerKey: Joi.alternatives() + .try( + Joi.object().keys({ + keyFile: Joi.alternatives( + Joi.binary(), + Joi.string(), + ).required(), + passphrase: Joi.string().required(), + }), + Joi.alternatives(Joi.binary(), Joi.string()), + ) + .required(), + }) + .required(); + +export interface PassInstance { + model: PartitionedBundle; + certificates: CertificatesSchema; + overrides?: OverridesSupportedOptions; +} + +export const PassInstance = Joi.object().keys({ + model: Joi.alternatives(Joi.object(), Joi.string()).required(), + certificates: Joi.object(), + overrides: Joi.object(), +}); + +export interface OverridesSupportedOptions { + serialNumber?: string; + description?: string; + organizationName?: string; + passTypeIdentifier?: string; + teamIdentifier?: string; + appLaunchURL?: string; + associatedStoreIdentifiers?: Array; + userInfo?: { [key: string]: any }; + webServiceURL?: string; + authenticationToken?: string; + sharingProhibited?: boolean; + backgroundColor?: string; + foregroundColor?: string; + labelColor?: string; + groupingIdentifier?: string; + suppressStripShine?: boolean; + logoText?: string; + maxDistance?: number; +} + +export const OverridesSupportedOptions = Joi.object() + .keys({ + serialNumber: Joi.string(), + description: Joi.string(), + organizationName: Joi.string(), + passTypeIdentifier: Joi.string(), + teamIdentifier: Joi.string(), + appLaunchURL: Joi.string(), + associatedStoreIdentifiers: Joi.array().items(Joi.number()), + userInfo: Joi.alternatives(Joi.object().unknown(), Joi.array()), + // parsing url as set of words and nums followed by dots, optional port and any possible path after + webServiceURL: Joi.string().regex( + /https?:\/\/(?:[a-z0-9]+\.?)+(?::\d{2,})?(?:\/[\S]+)*/, + ), + authenticationToken: Joi.string().min(16), + sharingProhibited: Joi.boolean(), + backgroundColor: Joi.string().min(10).max(16), + foregroundColor: Joi.string().min(10).max(16), + labelColor: Joi.string().min(10).max(16), + groupingIdentifier: Joi.string(), + suppressStripShine: Joi.boolean(), + logoText: Joi.string(), + maxDistance: Joi.number().positive(), + }) + .with("webServiceURL", "authenticationToken"); + +export interface ValidPassType { + boardingPass?: PassFields & { transitType: TransitType }; + eventTicket?: PassFields; + coupon?: PassFields; + generic?: PassFields; + storeCard?: PassFields; +} + +interface PassInterfacesProps { + barcode?: Barcode; + barcodes?: Barcode[]; + beacons?: Beacon[]; + locations?: Location[]; + maxDistance?: number; + relevantDate?: string; + nfc?: NFC; + expirationDate?: string; + voided?: boolean; +} + +type AllPassProps = PassInterfacesProps & + ValidPassType & + OverridesSupportedOptions; +export type ValidPass = { + [K in keyof AllPassProps]: AllPassProps[K]; +}; +export type PassColors = Pick< + OverridesSupportedOptions, + "backgroundColor" | "foregroundColor" | "labelColor" +>; + +// --------- UTILITIES ---------- // + +type AvailableSchemas = + | typeof Barcode + | typeof Location + | typeof Beacon + | typeof NFC + | typeof Field + | typeof PassFields + | typeof Personalization + | typeof TransitType + | typeof PassInstance + | typeof CertificatesSchema + | typeof OverridesSupportedOptions; + +export type ArrayPassSchema = Beacon | Location | Barcode; + +/* function resolveSchemaName(name: Schema) { + return schemas[name] || undefined; +} + */ +/** + * Checks if the passed options are compliant with the indicated schema + * @param {any} opts - options to be checks + * @param {string} schemaName - the indicated schema (will be converted) + * @returns {boolean} - result of the check + */ + +export function isValid(opts: any, schema: AvailableSchemas): boolean { + if (!schema) { + schemaDebug( + `validation failed due to missing or mispelled schema name`, + ); + return false; + } + + const validation = schema.validate(opts); + + if (validation.error) { + schemaDebug( + `validation failed due to error: ${validation.error.message}`, + ); + } + + return !validation.error; +} + +/** + * Executes the validation in verbose mode, exposing the value or an empty object + * @param {object} opts - to be validated + * @param {*} schemaName - selected schema + * @returns {object} the filtered value or empty object + */ + +export function getValidated( + opts: T, + schema: AvailableSchemas, +): T | null { + if (!schema) { + schemaDebug(`validation failed due to missing schema`); + + return null; + } + + const validation = schema.validate(opts, { stripUnknown: true }); + + if (validation.error) { + schemaDebug( + `Validation failed in getValidated due to error: ${validation.error.message}`, + ); + return null; + } + + return validation.value; +} diff --git a/src/schemas/location.ts b/src/schemas/location.ts new file mode 100644 index 0000000..f99cd82 --- /dev/null +++ b/src/schemas/location.ts @@ -0,0 +1,15 @@ +import Joi from "joi"; + +export interface Location { + relevantText?: string; + altitude?: number; + latitude: number; + longitude: number; +} + +export const Location = Joi.object().keys({ + altitude: Joi.number(), + latitude: Joi.number().required(), + longitude: Joi.number().required(), + relevantText: Joi.string(), +}); diff --git a/src/schemas/nfc.ts b/src/schemas/nfc.ts new file mode 100644 index 0000000..f46ef29 --- /dev/null +++ b/src/schemas/nfc.ts @@ -0,0 +1,11 @@ +import Joi from "joi"; + +export interface NFC { + message: string; + encryptionPublicKey?: string; +} + +export const NFC = Joi.object().keys({ + message: Joi.string().required().max(64), + encryptionPublicKey: Joi.string(), +}); diff --git a/src/schemas/passFields.ts b/src/schemas/passFields.ts new file mode 100644 index 0000000..9b4fb86 --- /dev/null +++ b/src/schemas/passFields.ts @@ -0,0 +1,35 @@ +import Joi from "joi"; +import { Field } from "./field"; + +export interface PassFields { + auxiliaryFields: (Field & { row?: number })[]; + backFields: Field[]; + headerFields: Field[]; + primaryFields: Field[]; + secondaryFields: Field[]; +} + +export const PassFields = Joi.object().keys({ + auxiliaryFields: Joi.array().items( + Joi.object() + .keys({ + row: Joi.number().max(1).min(0), + }) + .concat(Field), + ), + backFields: Joi.array().items(Field), + headerFields: Joi.array().items(Field), + primaryFields: Joi.array().items(Field), + secondaryFields: Joi.array().items(Field), +}); + +export type TransitType = + | "PKTransitTypeAir" + | "PKTransitTypeBoat" + | "PKTransitTypeBus" + | "PKTransitTypeGeneric" + | "PKTransitTypeTrain"; + +export const TransitType = Joi.string().regex( + /(PKTransitTypeAir|PKTransitTypeBoat|PKTransitTypeBus|PKTransitTypeGeneric|PKTransitTypeTrain)/, +); diff --git a/src/schemas/personalization.ts b/src/schemas/personalization.ts new file mode 100644 index 0000000..9cca8e9 --- /dev/null +++ b/src/schemas/personalization.ts @@ -0,0 +1,26 @@ +import Joi from "joi"; + +export interface Personalization { + description: string; + requiredPersonalizationFields: RequiredPersonalizationFields[]; + termsAndConditions?: string; +} + +type RequiredPersonalizationFields = + | "PKPassPersonalizationFieldName" + | "PKPassPersonalizationFieldPostalCode" + | "PKPassPersonalizationFieldEmailAddress" + | "PKPassPersonalizationFieldPhoneNumber"; + +export const Personalization = Joi.object().keys({ + description: Joi.string().required(), + requiredPersonalizationFields: Joi.array() + .items( + "PKPassPersonalizationFieldName", + "PKPassPersonalizationFieldPostalCode", + "PKPassPersonalizationFieldEmailAddress", + "PKPassPersonalizationFieldPhoneNumber", + ) + .required(), + termsAndConditions: Joi.string(), +}); diff --git a/src/schemas/semantics.ts b/src/schemas/semantics.ts new file mode 100644 index 0000000..423a037 --- /dev/null +++ b/src/schemas/semantics.ts @@ -0,0 +1,267 @@ +import Joi from "joi"; + +/** + * For a better description of every single field, + * please refer to Apple official documentation. + * + * @see https://developer.apple.com/documentation/walletpasses/semantictags + */ + +/** + * @see https://developer.apple.com/documentation/walletpasses/semantictagtype + */ + +declare namespace SemanticTagType { + interface PersonNameComponents { + familyName?: string; + givenName?: string; + middleName?: string; + namePrefix?: string; + nameSuffix?: string; + nickname?: string; + phoneticRepresentation?: string; + } + + interface CurrencyAmount { + currencyCode?: string; // ISO 4217 currency code + amount?: string; + } + + interface Location { + latitude: number; + longitude: number; + } + + interface Seat { + seatSection?: string; + seatRow?: string; + seatNumber?: string; + seatIdentifier?: string; + seatType?: string; + seatDescription?: string; + } + + interface WifiNetwork { + password: string; + ssid: string; + } +} + +const CurrencyAmount = Joi.object().keys({ + currencyCode: Joi.string(), + amount: Joi.string(), +}); + +const PersonNameComponent = Joi.object().keys( + { + givenName: Joi.string(), + familyName: Joi.string(), + middleName: Joi.string(), + namePrefix: Joi.string(), + nameSuffix: Joi.string(), + nickname: Joi.string(), + phoneticRepresentation: Joi.string(), + }, +); + +const seat = Joi.object().keys({ + seatSection: Joi.string(), + seatRow: Joi.string(), + seatNumber: Joi.string(), + seatIdentifier: Joi.string(), + seatType: Joi.string(), + seatDescription: Joi.string(), +}); + +const location = Joi.object().keys({ + latitude: Joi.number().required(), + longitude: Joi.number().required(), +}); + +const WifiNetwork = Joi.object().keys({ + password: Joi.string().required(), + ssid: Joi.string().required(), +}); + +/** + * Alphabetical order + * @see https://developer.apple.com/documentation/walletpasses/semantictags + */ + +export interface Semantics { + airlineCode?: string; + artistIDs?: string[]; + awayTeamAbbreviation?: string; + awayTeamLocation?: string; + awayTeamName?: string; + + balance?: SemanticTagType.CurrencyAmount; + boardingGroup?: string; + boardingSequenceNumber?: string; + + carNumber?: string; + confirmationNumber?: string; + currentArrivalDate?: string; + currentBoardingDate?: string; + currentDepartureDate?: string; + + departureAirportCode?: string; + departureAirportName?: string; + departureGate?: string; + departureLocation?: SemanticTagType.Location; + departureLocationDescription?: string; + departurePlatform?: string; + departureStationName?: string; + departureTerminal?: string; + destinationAirportCode?: string; + destinationAirportName?: string; + destinationGate?: string; + destinationLocation?: SemanticTagType.Location; + destinationLocationDescription?: string; + destinationPlatform?: string; + destinationStationName?: string; + destinationTerminal?: string; + duration?: number; + + eventEndDate?: string; + eventName?: string; + eventStartDate?: string; + eventType?: + | "PKEventTypeGeneric" + | "PKEventTypeLivePerformance" + | "PKEventTypeMovie" + | "PKEventTypeSports" + | "PKEventTypeConference" + | "PKEventTypeConvention" + | "PKEventTypeWorkshop" + | "PKEventTypeSocialGathering"; + + flightCode?: string; + flightNumber?: number; + + genre?: string; + + homeTeamAbbreviation?: string; + homeTeamLocation?: string; + homeTeamName?: string; + leagueAbbreviation?: string; + leagueName?: string; + + membershipProgramName?: string; + membershipProgramNumber?: string; + + originalArrivalDate?: string; + originalBoardingDate?: string; + originalDepartureDate?: string; + + passengerName?: SemanticTagType.PersonNameComponents; + performerNames?: string[]; + priorityStatus?: string; + + seats?: SemanticTagType.Seat[]; + securityScreening?: string; + silenceRequested?: boolean; + sportName?: string; + + totalPrice?: SemanticTagType.CurrencyAmount; + transitProvider?: string; + transitStatus?: string; + transitStatusReason?: string; + + vehicleName?: string; + vehicleNumber?: string; + vehicleType?: string; + venueEntrance?: string; + venueLocation?: SemanticTagType.Location; + venueName?: string; + venuePhoneNumber?: string; + venueRoom?: string; + + wifiAccess?: SemanticTagType.WifiNetwork; +} + +export const Semantics = Joi.object().keys({ + airlineCode: Joi.string(), + artistIDs: Joi.array().items(Joi.string()), + awayTeamAbbreviation: Joi.string(), + awayTeamLocation: Joi.string(), + awayTeamName: Joi.string(), + + balance: CurrencyAmount, + boardingGroup: Joi.string(), + boardingSequenceNumber: Joi.string(), + + carNumber: Joi.string(), + confirmationNumber: Joi.string(), + currentArrivalDate: Joi.string(), + currentBoardingDate: Joi.string(), + currentDepartureDate: Joi.string(), + + departureAirportCode: Joi.string(), + departureAirportName: Joi.string(), + departureGate: Joi.string(), + departureLocation: location, + departureLocationDescription: Joi.string(), + departurePlatform: Joi.string(), + departureStationName: Joi.string(), + departureTerminal: Joi.string(), + destinationAirportCode: Joi.string(), + destinationAirportName: Joi.string(), + destinationGate: Joi.string(), + destinationLocation: location, + destinationLocationDescription: Joi.string(), + destinationPlatform: Joi.string(), + destinationStationName: Joi.string(), + destinationTerminal: Joi.string(), + duration: Joi.number(), + + eventEndDate: Joi.string(), + eventName: Joi.string(), + eventStartDate: Joi.string(), + eventType: Joi.string().regex( + /(PKEventTypeGeneric|PKEventTypeLivePerformance|PKEventTypeMovie|PKEventTypeSports|PKEventTypeConference|PKEventTypeConvention|PKEventTypeWorkshop|PKEventTypeSocialGathering)/, + ), + + flightCode: Joi.string(), + flightNumber: Joi.number(), + + genre: Joi.string(), + + homeTeamAbbreviation: Joi.string(), + homeTeamLocation: Joi.string(), + homeTeamName: Joi.string(), + leagueAbbreviation: Joi.string(), + leagueName: Joi.string(), + + membershipProgramName: Joi.string(), + membershipProgramNumber: Joi.string(), + + originalArrivalDate: Joi.string(), + originalBoardingDate: Joi.string(), + originalDepartureDate: Joi.string(), + + passengerName: PersonNameComponent, + performerNames: Joi.array().items(Joi.string()), + priorityStatus: Joi.string(), + + seats: Joi.array().items(seat), + securityScreening: Joi.string(), + silenceRequested: Joi.boolean(), + sportName: Joi.string(), + + totalPrice: CurrencyAmount, + transitProvider: Joi.string(), + transitStatus: Joi.string(), + transitStatusReason: Joi.string(), + + vehicleName: Joi.string(), + vehicleNumber: Joi.string(), + vehicleType: Joi.string(), + venueEntrance: Joi.string(), + venueLocation: location, + venueName: Joi.string(), + venuePhoneNumber: Joi.string(), + venueRoom: Joi.string(), + + wifiAccess: Joi.array().items(WifiNetwork), +}); diff --git a/src/utils.ts b/src/utils.ts index 7e4a77d..7cced28 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import { EOL } from "os"; -import { PartitionedBundle, BundleUnit } from "./schema"; +import type * as Schemas from "./schemas"; import { sep } from "path"; /** @@ -118,12 +118,12 @@ export function generateStringFile(lang: { [index: string]: string }): Buffer { */ type PartitionedBundleElements = [ - PartitionedBundle["l10nBundle"], - PartitionedBundle["bundle"], + Schemas.PartitionedBundle["l10nBundle"], + Schemas.PartitionedBundle["bundle"], ]; export function splitBufferBundle( - origin: BundleUnit, + origin: Schemas.BundleUnit, ): PartitionedBundleElements { const initialValue: PartitionedBundleElements = [{}, {}]; @@ -180,7 +180,7 @@ export function hasFilesWithName( } export function deletePersonalization( - source: BundleUnit, + source: Schemas.BundleUnit, logosNames: string[] = [], ): void { [...logosNames, "personalization.json"].forEach(