Refactored all the schemas;

Splitted them in a folder and renamed them to have the same name of typescript type;
Added more SemanticKeys;
Removed Schema resolution via string;
This commit is contained in:
Alexander Cerutti
2021-06-17 23:34:22 +02:00
parent 87321b8dc9
commit 17de1c7d79
16 changed files with 850 additions and 771 deletions

View File

@@ -1,10 +1,4 @@
import { import * as Schemas from "./schemas";
Certificates,
FinalCertificates,
PartitionedBundle,
OverridesSupportedOptions,
FactoryOptions,
} from "./schema";
import { getModelContents, readCertificatesFromOptions } from "./parser"; import { getModelContents, readCertificatesFromOptions } from "./parser";
import formatMessage from "./messages"; import formatMessage from "./messages";
@@ -13,14 +7,14 @@ const abmModel = Symbol("model");
const abmOverrides = Symbol("overrides"); const abmOverrides = Symbol("overrides");
export interface AbstractFactoryOptions export interface AbstractFactoryOptions
extends Omit<FactoryOptions, "certificates"> { extends Omit<Schemas.FactoryOptions, "certificates"> {
certificates?: Certificates; certificates?: Schemas.Certificates;
} }
interface AbstractModelOptions { interface AbstractModelOptions {
bundle: PartitionedBundle; bundle: Schemas.PartitionedBundle;
certificates: FinalCertificates; certificates: Schemas.CertificatesSchema;
overrides?: OverridesSupportedOptions; overrides?: Schemas.OverridesSupportedOptions;
} }
/** /**
@@ -51,9 +45,9 @@ export async function createAbstractModel(options: AbstractFactoryOptions) {
} }
export class AbstractModel { export class AbstractModel {
private [abmCertificates]: FinalCertificates; private [abmCertificates]: Schemas.CertificatesSchema;
private [abmModel]: PartitionedBundle; private [abmModel]: Schemas.PartitionedBundle;
private [abmOverrides]: OverridesSupportedOptions; private [abmOverrides]: Schemas.OverridesSupportedOptions;
constructor(options: AbstractModelOptions) { constructor(options: AbstractModelOptions) {
this[abmModel] = options.bundle; this[abmModel] = options.bundle;
@@ -61,15 +55,15 @@ export class AbstractModel {
this[abmOverrides] = options.overrides; this[abmOverrides] = options.overrides;
} }
get certificates(): FinalCertificates { get certificates(): Schemas.CertificatesSchema {
return this[abmCertificates]; return this[abmCertificates];
} }
get bundle(): PartitionedBundle { get bundle(): Schemas.PartitionedBundle {
return this[abmModel]; return this[abmModel];
} }
get overrides(): OverridesSupportedOptions { get overrides(): Schemas.OverridesSupportedOptions {
return this[abmOverrides]; return this[abmOverrides];
} }
} }

View File

@@ -1,11 +1,5 @@
import { Pass } from "./pass"; import { Pass } from "./pass";
import { import * as Schemas from "./schemas";
FactoryOptions,
BundleUnit,
FinalCertificates,
PartitionedBundle,
OverridesSupportedOptions,
} from "./schema";
import formatMessage from "./messages"; import formatMessage from "./messages";
import { getModelContents, readCertificatesFromOptions } from "./parser"; import { getModelContents, readCertificatesFromOptions } from "./parser";
import { splitBufferBundle } from "./utils"; import { splitBufferBundle } from "./utils";
@@ -20,8 +14,8 @@ import { AbstractModel, AbstractFactoryOptions } from "./abstract";
*/ */
export async function createPass( export async function createPass(
options: FactoryOptions | InstanceType<typeof AbstractModel>, options: Schemas.FactoryOptions | InstanceType<typeof AbstractModel>,
additionalBuffers?: BundleUnit, additionalBuffers?: Schemas.BundleUnit,
abstractMissingData?: Omit<AbstractFactoryOptions, "model">, abstractMissingData?: Omit<AbstractFactoryOptions, "model">,
): Promise<Pass> { ): Promise<Pass> {
if ( if (
@@ -35,8 +29,8 @@ export async function createPass(
try { try {
if (options instanceof AbstractModel) { if (options instanceof AbstractModel) {
let certificates: FinalCertificates; let certificates: Schemas.CertificatesSchema;
let overrides: OverridesSupportedOptions = { let overrides: Schemas.OverridesSupportedOptions = {
...(options.overrides || {}), ...(options.overrides || {}),
...((abstractMissingData && abstractMissingData.overrides) || ...((abstractMissingData && abstractMissingData.overrides) ||
{}), {}),
@@ -85,10 +79,10 @@ export async function createPass(
} }
function createPassInstance( function createPassInstance(
model: PartitionedBundle, model: Schemas.PartitionedBundle,
certificates: FinalCertificates, certificates: Schemas.CertificatesSchema,
overrides: OverridesSupportedOptions, overrides: Schemas.OverridesSupportedOptions,
additionalBuffers?: BundleUnit, additionalBuffers?: Schemas.BundleUnit,
) { ) {
if (additionalBuffers) { if (additionalBuffers) {
const [additionalL10n, additionalBundle] = splitBufferBundle( const [additionalL10n, additionalBundle] = splitBufferBundle(

View File

@@ -1,4 +1,4 @@
import * as schema from "./schema"; import * as Schemas from "./schemas";
import debug from "debug"; import debug from "debug";
const fieldsDebug = debug("passkit:fields"); const fieldsDebug = debug("passkit:fields");
@@ -23,12 +23,12 @@ export default class FieldsArray extends Array {
* also uniqueKeys set. * also uniqueKeys set.
*/ */
push(...fieldsData: schema.Field[]): number { push(...fieldsData: Schemas.Field[]): number {
const validFields = fieldsData.reduce( const validFields = fieldsData.reduce(
(acc: schema.Field[], current: schema.Field) => { (acc: Schemas.Field[], current: Schemas.Field) => {
if ( if (
!(typeof current === "object") || !(typeof current === "object") ||
!schema.isValid(current, "field") !Schemas.isValid(current, Schemas.Field)
) { ) {
return acc; return acc;
} }
@@ -55,8 +55,8 @@ export default class FieldsArray extends Array {
* also uniqueKeys set * also uniqueKeys set
*/ */
pop(): schema.Field { pop(): Schemas.Field {
const element: schema.Field = Array.prototype.pop.call(this); const element: Schemas.Field = Array.prototype.pop.call(this);
this[poolSymbol].delete(element.key); this[poolSymbol].delete(element.key);
return element; return element;
} }
@@ -69,8 +69,8 @@ export default class FieldsArray extends Array {
splice( splice(
start: number, start: number,
deleteCount: number, deleteCount: number,
...items: schema.Field[] ...items: Schemas.Field[]
): schema.Field[] { ): Schemas.Field[] {
const removeList = this.slice(start, deleteCount + start); const removeList = this.slice(start, deleteCount + start);
removeList.forEach((item) => this[poolSymbol].delete(item.key)); removeList.forEach((item) => this[poolSymbol].delete(item.key));

View File

@@ -1,14 +1,7 @@
import * as path from "path"; import * as path from "path";
import forge from "node-forge"; import forge from "node-forge";
import formatMessage from "./messages"; import formatMessage from "./messages";
import { import * as Schemas from "./schemas";
FactoryOptions,
PartitionedBundle,
BundleUnit,
Certificates,
FinalCertificates,
isValid,
} from "./schema";
import { import {
removeHidden, removeHidden,
splitBufferBundle, splitBufferBundle,
@@ -28,8 +21,8 @@ const { readdir: readDir, readFile } = fs.promises;
* @param model * @param model
*/ */
export async function getModelContents(model: FactoryOptions["model"]) { export async function getModelContents(model: Schemas.FactoryOptions["model"]) {
let modelContents: PartitionedBundle; let modelContents: Schemas.PartitionedBundle;
if (typeof model === "string") { if (typeof model === "string") {
modelContents = await getModelFolderContents(model); modelContents = await getModelFolderContents(model);
@@ -77,9 +70,9 @@ export async function getModelContents(model: FactoryOptions["model"]) {
const parsedPersonalization = JSON.parse( const parsedPersonalization = JSON.parse(
modelContents.bundle[personalizationJsonFile].toString("utf8"), modelContents.bundle[personalizationJsonFile].toString("utf8"),
); );
const isPersonalizationValid = isValid( const isPersonalizationValid = Schemas.isValid(
parsedPersonalization, parsedPersonalization,
"personalizationDict", Schemas.Personalization,
); );
if (!isPersonalizationValid) { if (!isPersonalizationValid) {
@@ -105,7 +98,7 @@ export async function getModelContents(model: FactoryOptions["model"]) {
export async function getModelFolderContents( export async function getModelFolderContents(
model: string, model: string,
): Promise<PartitionedBundle> { ): Promise<Schemas.PartitionedBundle> {
try { try {
const modelPath = `${model}${(!path.extname(model) && ".pass") || ""}`; const modelPath = `${model}${(!path.extname(model) && ".pass") || ""}`;
const modelFilesList = await readDir(modelPath); 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) => ({ ...rawBundleFiles.map((fileName, index) => ({
[fileName]: rawBundleBuffers[index], [fileName]: rawBundleBuffers[index],
@@ -151,7 +144,7 @@ export async function getModelFolderContents(
// Reading concurrently localizations folder // Reading concurrently localizations folder
// and their files and their buffers // and their files and their buffers
const L10N_FilesListByFolder: Array<BundleUnit> = await Promise.all( const L10N_FilesListByFolder: Array<Schemas.BundleUnit> = await Promise.all(
l10nFolders.map(async (folderPath) => { l10nFolders.map(async (folderPath) => {
// Reading current folder // Reading current folder
const currentLangPath = path.join(modelPath, folderPath); const currentLangPath = path.join(modelPath, folderPath);
@@ -172,23 +165,27 @@ export async function getModelFolderContents(
// Assigning each file path to its buffer // Assigning each file path to its buffer
// and discarding the empty ones // and discarding the empty ones
return validFiles.reduce<BundleUnit>((acc, file, index) => { return validFiles.reduce<Schemas.BundleUnit>(
(acc, file, index) => {
if (!buffers[index].length) { if (!buffers[index].length) {
return acc; return acc;
} }
const fileComponents = file.split(path.sep); const fileComponents = file.split(path.sep);
const fileName = fileComponents[fileComponents.length - 1]; const fileName =
fileComponents[fileComponents.length - 1];
return { return {
...acc, ...acc,
[fileName]: buffers[index], [fileName]: buffers[index],
}; };
}, {}); },
{},
);
}), }),
); );
const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign( const l10nBundle: Schemas.PartitionedBundle["l10nBundle"] = Object.assign(
{}, {},
...L10N_FilesListByFolder.map((folder, index) => ({ ...L10N_FilesListByFolder.map((folder, index) => ({
[l10nFolders[index]]: folder, [l10nFolders[index]]: folder,
@@ -226,9 +223,12 @@ export async function getModelFolderContents(
* @param model * @param model
*/ */
export function getModelBufferContents(model: BundleUnit): PartitionedBundle { export function getModelBufferContents(
const rawBundle = removeHidden(Object.keys(model)).reduce<BundleUnit>( model: Schemas.BundleUnit,
(acc, current) => { ): Schemas.PartitionedBundle {
const rawBundle = removeHidden(
Object.keys(model),
).reduce<Schemas.BundleUnit>((acc, current) => {
// Checking if current file is one of the autogenerated ones or if its // Checking if current file is one of the autogenerated ones or if its
// content is not available // content is not available
@@ -237,9 +237,7 @@ export function getModelBufferContents(model: BundleUnit): PartitionedBundle {
} }
return { ...acc, [current]: model[current] }; return { ...acc, [current]: model[current] };
}, }, {});
{},
);
const bundleKeys = Object.keys(rawBundle); const bundleKeys = Object.keys(rawBundle);
@@ -266,18 +264,18 @@ export function getModelBufferContents(model: BundleUnit): PartitionedBundle {
* @param options * @param options
*/ */
type flatCertificates = Omit<Certificates, "signerKey"> & { type flatCertificates = Omit<Schemas.Certificates, "signerKey"> & {
signerKey: string; signerKey: string;
}; };
export async function readCertificatesFromOptions( export async function readCertificatesFromOptions(
options: Certificates, options: Schemas.Certificates,
): Promise<FinalCertificates> { ): Promise<Schemas.CertificatesSchema> {
if ( if (
!( !(
options && options &&
Object.keys(options).length && Object.keys(options).length &&
isValid(options, "certificatesSchema") Schemas.isValid(options, Schemas.CertificatesSchema)
) )
) { ) {
throw new Error(formatMessage("CP_NO_CERTS")); throw new Error(formatMessage("CP_NO_CERTS"));

View File

@@ -4,7 +4,7 @@ import debug from "debug";
import { Stream } from "stream"; import { Stream } from "stream";
import { ZipFile } from "yazl"; import { ZipFile } from "yazl";
import * as schema from "./schema"; import * as Schemas from "./schemas";
import formatMessage from "./messages"; import formatMessage from "./messages";
import FieldsArray from "./fieldsArray"; import FieldsArray from "./fieldsArray";
import { import {
@@ -14,6 +14,7 @@ import {
deletePersonalization, deletePersonalization,
getAllFilesWithName, getAllFilesWithName,
} from "./utils"; } from "./utils";
import type Joi from "joi";
const barcodeDebug = debug("passkit:barcode"); const barcodeDebug = debug("passkit:barcode");
const genericDebug = debug("passkit:generic"); const genericDebug = debug("passkit:generic");
@@ -21,22 +22,22 @@ const genericDebug = debug("passkit:generic");
const transitType = Symbol("transitType"); const transitType = Symbol("transitType");
const passProps = Symbol("_props"); const passProps = Symbol("_props");
const propsSchemaMap = new Map<string, schema.Schema>([ const propsSchemaMap = new Map<string, Joi.ObjectSchema<any>>([
["barcodes", "barcode"], ["barcodes", Schemas.Barcode],
["barcode", "barcode"], ["barcode", Schemas.Barcode],
["beacons", "beaconsDict"], ["beacons", Schemas.Beacon],
["locations", "locationsDict"], ["locations", Schemas.Location],
["nfc", "nfcDict"], ["nfc", Schemas.NFC],
]); ]);
export class Pass { export class Pass {
private bundle: schema.BundleUnit; private bundle: Schemas.BundleUnit;
private l10nBundles: schema.PartitionedBundle["l10nBundle"]; private l10nBundles: Schemas.PartitionedBundle["l10nBundle"];
private _fields: (keyof schema.PassFields)[]; private _fields: (keyof Schemas.PassFields)[];
private [passProps]: schema.ValidPass = {}; private [passProps]: Schemas.ValidPass = {};
private type: keyof schema.ValidPassType; private type: keyof Schemas.ValidPassType;
private fieldsKeys: Set<string> = new Set<string>(); private fieldsKeys: Set<string> = new Set<string>();
private passCore: schema.ValidPass; private passCore: Schemas.ValidPass;
// Setting these as possibly undefined because we set // Setting these as possibly undefined because we set
// them all in an loop later // them all in an loop later
@@ -46,14 +47,14 @@ export class Pass {
public auxiliaryFields: FieldsArray | undefined; public auxiliaryFields: FieldsArray | undefined;
public backFields: FieldsArray | undefined; public backFields: FieldsArray | undefined;
private Certificates: schema.FinalCertificates; private Certificates: Schemas.CertificatesSchema;
private [transitType]: string = ""; private [transitType]: string = "";
private l10nTranslations: { private l10nTranslations: {
[languageCode: string]: { [placeholder: string]: string }; [languageCode: string]: { [placeholder: string]: string };
} = {}; } = {};
constructor(options: schema.PassInstance) { constructor(options: Schemas.PassInstance) {
if (!schema.isValid(options, "instance")) { if (!Schemas.isValid(options, Schemas.PassInstance)) {
throw new Error(formatMessage("REQUIR_VALID_FAILED")); throw new Error(formatMessage("REQUIR_VALID_FAILED"));
} }
@@ -70,10 +71,10 @@ export class Pass {
} }
// Parsing the options and extracting only the valid ones. // Parsing the options and extracting only the valid ones.
const validOverrides = schema.getValidated( const validOverrides = Schemas.getValidated(
options.overrides || {}, options.overrides || {},
"supportedOptions", Schemas.OverridesSupportedOptions,
) as schema.OverridesSupportedOptions; );
if (validOverrides === null) { if (validOverrides === null) {
throw new Error(formatMessage("OVV_KEYS_BADFORMAT")); throw new Error(formatMessage("OVV_KEYS_BADFORMAT"));
@@ -81,7 +82,7 @@ export class Pass {
this.type = Object.keys(this.passCore).find((key) => this.type = Object.keys(this.passCore).find((key) =>
/(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key), /(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key),
) as keyof schema.ValidPassType; ) as keyof Schemas.ValidPassType;
if (!this.type) { if (!this.type) {
throw new Error(formatMessage("NO_PASS_TYPE")); throw new Error(formatMessage("NO_PASS_TYPE"));
@@ -90,8 +91,8 @@ export class Pass {
// Parsing and validating pass.json keys // Parsing and validating pass.json keys
const passCoreKeys = Object.keys( const passCoreKeys = Object.keys(
this.passCore, this.passCore,
) as (keyof schema.ValidPass)[]; ) as (keyof Schemas.ValidPass)[];
const validatedPassKeys = passCoreKeys.reduce<schema.ValidPass>( const validatedPassKeys = passCoreKeys.reduce<Schemas.ValidPass>(
(acc, current) => { (acc, current) => {
if (this.type === current) { if (this.type === current) {
// We want to exclude type keys (eventTicket, // We want to exclude type keys (eventTicket,
@@ -109,16 +110,16 @@ export class Pass {
const currentSchema = propsSchemaMap.get(current)!; const currentSchema = propsSchemaMap.get(current)!;
if (Array.isArray(this.passCore[current])) { if (Array.isArray(this.passCore[current])) {
const valid = getValidInArray<schema.ArrayPassSchema>( const valid = getValidInArray<Schemas.ArrayPassSchema>(
currentSchema, currentSchema,
this.passCore[current] as schema.ArrayPassSchema[], this.passCore[current] as Schemas.ArrayPassSchema[],
); );
return { ...acc, [current]: valid }; return { ...acc, [current]: valid };
} else { } else {
return { return {
...acc, ...acc,
[current]: [current]:
(schema.isValid( (Schemas.isValid(
this.passCore[current], this.passCore[current],
currentSchema, currentSchema,
) && ) &&
@@ -155,7 +156,7 @@ export class Pass {
this[fieldName] = new FieldsArray( this[fieldName] = new FieldsArray(
this.fieldsKeys, this.fieldsKeys,
...(this.passCore[this.type][fieldName] || []).filter((field) => ...(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 * Iterating through languages and generating pass.string file
@@ -262,7 +263,7 @@ export class Pass {
* and returning the compiled manifest * and returning the compiled manifest
*/ */
const archive = new ZipFile(); const archive = new ZipFile();
const manifest = Object.keys(finalBundle).reduce<schema.Manifest>( const manifest = Object.keys(finalBundle).reduce<Schemas.Manifest>(
(acc, current) => { (acc, current) => {
let hashFlow = forge.md.sha1.create(); let hashFlow = forge.md.sha1.create();
@@ -360,14 +361,14 @@ export class Pass {
*/ */
beacons(resetFlag: null): this; beacons(resetFlag: null): this;
beacons(...data: schema.Beacon[]): this; beacons(...data: Schemas.Beacon[]): this;
beacons(...data: (schema.Beacon | null)[]): this { beacons(...data: (Schemas.Beacon | null)[]): this {
if (data[0] === null) { if (data[0] === null) {
delete this[passProps]["beacons"]; delete this[passProps]["beacons"];
return this; return this;
} }
const valid = processRelevancySet("beacons", data as schema.Beacon[]); const valid = processRelevancySet(Schemas.Beacon, data);
if (valid.length) { if (valid.length) {
this[passProps]["beacons"] = valid; this[passProps]["beacons"] = valid;
@@ -383,17 +384,14 @@ export class Pass {
*/ */
locations(resetFlag: null): this; locations(resetFlag: null): this;
locations(...data: schema.Location[]): this; locations(...data: Schemas.Location[]): this;
locations(...data: (schema.Location | null)[]): this { locations(...data: (Schemas.Location | null)[]): this {
if (data[0] === null) { if (data[0] === null) {
delete this[passProps]["locations"]; delete this[passProps]["locations"];
return this; return this;
} }
const valid = processRelevancySet( const valid = processRelevancySet(Schemas.Location, data);
"locations",
data as schema.Location[],
);
if (valid.length) { if (valid.length) {
this[passProps]["locations"] = valid; this[passProps]["locations"] = valid;
@@ -436,8 +434,8 @@ export class Pass {
barcodes(resetFlag: null): this; barcodes(resetFlag: null): this;
barcodes(message: string): this; barcodes(message: string): this;
barcodes(...data: schema.Barcode[]): this; barcodes(...data: Schemas.Barcode[]): this;
barcodes(...data: (schema.Barcode | null | string)[]): this { barcodes(...data: (Schemas.Barcode | null | string)[]): this {
if (data[0] === null) { if (data[0] === null) {
delete this[passProps]["barcodes"]; delete this[passProps]["barcodes"];
return this; return this;
@@ -461,13 +459,16 @@ export class Pass {
* Validation assign default value to missing parameters (if any). * Validation assign default value to missing parameters (if any).
*/ */
const validBarcodes = data.reduce<schema.Barcode[]>( const validBarcodes = data.reduce<Schemas.Barcode[]>(
(acc, current) => { (acc, current) => {
if (!(current && current instanceof Object)) { if (!(current && current instanceof Object)) {
return acc; return acc;
} }
const validated = schema.getValidated(current, "barcode"); const validated = Schemas.getValidated(
current,
Schemas.Barcode,
);
if ( if (
!( !(
@@ -479,7 +480,7 @@ export class Pass {
return acc; return acc;
} }
return [...acc, validated] as schema.Barcode[]; return [...acc, validated] as Schemas.Barcode[];
}, },
[], [],
); );
@@ -502,7 +503,7 @@ export class Pass {
* @return {this} * @return {this}
*/ */
barcode(chosenFormat: schema.BarcodeFormat | null): this { barcode(chosenFormat: Schemas.BarcodeFormat | null): this {
const { barcodes } = this[passProps]; const { barcodes } = this[passProps];
if (chosenFormat === null) { if (chosenFormat === null) {
@@ -548,7 +549,7 @@ export class Pass {
* @see https://apple.co/2wTxiaC * @see https://apple.co/2wTxiaC
*/ */
nfc(data: schema.NFC | null): this { nfc(data: Schemas.NFC | null): this {
if (data === null) { if (data === null) {
delete this[passProps]["nfc"]; delete this[passProps]["nfc"];
return this; return this;
@@ -559,7 +560,7 @@ export class Pass {
data && data &&
typeof data === "object" && typeof data === "object" &&
!Array.isArray(data) && !Array.isArray(data) &&
schema.isValid(data, "nfcDict") Schemas.isValid(data, Schemas.NFC)
) )
) { ) {
genericDebug(formatMessage("NFC_INVALID")); genericDebug(formatMessage("NFC_INVALID"));
@@ -579,7 +580,7 @@ export class Pass {
* @returns The properties will be inserted in the pass. * @returns The properties will be inserted in the pass.
*/ */
get props(): Readonly<schema.ValidPass> { get props(): Readonly<Schemas.ValidPass> {
return this[passProps]; return this[passProps];
} }
@@ -591,7 +592,7 @@ export class Pass {
* @returns {Buffer} * @returns {Buffer}
*/ */
private _sign(manifest: schema.Manifest): Buffer { private _sign(manifest: Schemas.Manifest): Buffer {
const signature = forge.pkcs7.createSignedData(); const signature = forge.pkcs7.createSignedData();
signature.content = forge.util.createBuffer( signature.content = forge.util.createBuffer(
@@ -667,7 +668,7 @@ export class Pass {
private _patch(passCoreBuffer: Buffer): Buffer { private _patch(passCoreBuffer: Buffer): Buffer {
const passFile = JSON.parse( const passFile = JSON.parse(
passCoreBuffer.toString(), passCoreBuffer.toString(),
) as schema.ValidPass; ) as Schemas.ValidPass;
if (Object.keys(this[passProps]).length) { if (Object.keys(this[passProps]).length) {
/* /*
@@ -680,7 +681,7 @@ export class Pass {
"backgroundColor", "backgroundColor",
"foregroundColor", "foregroundColor",
"labelColor", "labelColor",
] as Array<keyof schema.PassColors>; ] as Array<keyof Schemas.PassColors>;
passColors passColors
.filter( .filter(
(v) => (v) =>
@@ -705,7 +706,7 @@ export class Pass {
} }
set transitType(value: string) { set transitType(value: string) {
if (!schema.isValid(value, "transitType")) { if (!Schemas.isValid(value, Schemas.TransitType)) {
genericDebug(formatMessage("TRSTYPE_NOT_VALID", value)); genericDebug(formatMessage("TRSTYPE_NOT_VALID", value));
this[transitType] = this[transitType] || ""; this[transitType] = this[transitType] || "";
return; return;
@@ -727,7 +728,7 @@ export class Pass {
* @return Array of barcodeDict compliant * @return Array of barcodeDict compliant
*/ */
function barcodesFromUncompleteData(message: string): schema.Barcode[] { function barcodesFromUncompleteData(message: string): Schemas.Barcode[] {
if (!(message && typeof message === "string")) { if (!(message && typeof message === "string")) {
return []; return [];
} }
@@ -739,21 +740,24 @@ function barcodesFromUncompleteData(message: string): schema.Barcode[] {
"PKBarcodeFormatCode128", "PKBarcodeFormatCode128",
].map( ].map(
(format) => (format) =>
schema.getValidated( Schemas.getValidated(
{ format, message }, { format, message },
"barcode", Schemas.Barcode,
) as schema.Barcode, ) as Schemas.Barcode,
); );
} }
function processRelevancySet<T>(key: string, data: T[]): T[] { function processRelevancySet<T>(schema: Joi.ObjectSchema<T>, data: T[]): T[] {
return getValidInArray(`${key}Dict` as schema.Schema, data); return getValidInArray(schema, data);
} }
function getValidInArray<T>(schemaName: schema.Schema, contents: T[]): T[] { function getValidInArray<T>(
schemaName: Joi.ObjectSchema<T>,
contents: T[],
): T[] {
return contents.filter( return contents.filter(
(current) => (current) =>
Object.keys(current).length && schema.isValid(current, schemaName), Object.keys(current).length && Schemas.isValid(current, schemaName),
); );
} }

View File

@@ -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<number>;
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<T extends Object>(
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;
}

26
src/schemas/barcode.ts Normal file
View File

@@ -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<Barcode>().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(),
});

19
src/schemas/beacon.ts Normal file
View File

@@ -0,0 +1,19 @@
import Joi from "joi";
export interface Beacon {
major?: number;
minor?: number;
relevantText?: string;
proximityUUID: string;
}
export const Beacon = Joi.object<Beacon>().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(),
});

70
src/schemas/field.ts Normal file
View File

@@ -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<Field>().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(),
}),
});

244
src/schemas/index.ts Normal file
View File

@@ -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<CertificatesSchema>()
.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<PassInstance>().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<number>;
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<OverridesSupportedOptions>()
.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<T extends Object>(
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;
}

15
src/schemas/location.ts Normal file
View File

@@ -0,0 +1,15 @@
import Joi from "joi";
export interface Location {
relevantText?: string;
altitude?: number;
latitude: number;
longitude: number;
}
export const Location = Joi.object<Location>().keys({
altitude: Joi.number(),
latitude: Joi.number().required(),
longitude: Joi.number().required(),
relevantText: Joi.string(),
});

11
src/schemas/nfc.ts Normal file
View File

@@ -0,0 +1,11 @@
import Joi from "joi";
export interface NFC {
message: string;
encryptionPublicKey?: string;
}
export const NFC = Joi.object<NFC>().keys({
message: Joi.string().required().max(64),
encryptionPublicKey: Joi.string(),
});

35
src/schemas/passFields.ts Normal file
View File

@@ -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<PassFields>().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)/,
);

View File

@@ -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<Personalization>().keys({
description: Joi.string().required(),
requiredPersonalizationFields: Joi.array()
.items(
"PKPassPersonalizationFieldName",
"PKPassPersonalizationFieldPostalCode",
"PKPassPersonalizationFieldEmailAddress",
"PKPassPersonalizationFieldPhoneNumber",
)
.required(),
termsAndConditions: Joi.string(),
});

267
src/schemas/semantics.ts Normal file
View File

@@ -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<SemanticTagType.CurrencyAmount>().keys({
currencyCode: Joi.string(),
amount: Joi.string(),
});
const PersonNameComponent = Joi.object<SemanticTagType.PersonNameComponents>().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<SemanticTagType.Seat>().keys({
seatSection: Joi.string(),
seatRow: Joi.string(),
seatNumber: Joi.string(),
seatIdentifier: Joi.string(),
seatType: Joi.string(),
seatDescription: Joi.string(),
});
const location = Joi.object<SemanticTagType.Location>().keys({
latitude: Joi.number().required(),
longitude: Joi.number().required(),
});
const WifiNetwork = Joi.object<SemanticTagType.WifiNetwork>().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<Semantics>().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),
});

View File

@@ -1,5 +1,5 @@
import { EOL } from "os"; import { EOL } from "os";
import { PartitionedBundle, BundleUnit } from "./schema"; import type * as Schemas from "./schemas";
import { sep } from "path"; import { sep } from "path";
/** /**
@@ -118,12 +118,12 @@ export function generateStringFile(lang: { [index: string]: string }): Buffer {
*/ */
type PartitionedBundleElements = [ type PartitionedBundleElements = [
PartitionedBundle["l10nBundle"], Schemas.PartitionedBundle["l10nBundle"],
PartitionedBundle["bundle"], Schemas.PartitionedBundle["bundle"],
]; ];
export function splitBufferBundle( export function splitBufferBundle(
origin: BundleUnit, origin: Schemas.BundleUnit,
): PartitionedBundleElements { ): PartitionedBundleElements {
const initialValue: PartitionedBundleElements = [{}, {}]; const initialValue: PartitionedBundleElements = [{}, {}];
@@ -180,7 +180,7 @@ export function hasFilesWithName(
} }
export function deletePersonalization( export function deletePersonalization(
source: BundleUnit, source: Schemas.BundleUnit,
logosNames: string[] = [], logosNames: string[] = [],
): void { ): void {
[...logosNames, "personalization.json"].forEach( [...logosNames, "personalization.json"].forEach(