mirror of
https://github.com/marcogll/passkit-generator.git
synced 2026-03-15 18:25:24 +00:00
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:
@@ -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<FactoryOptions, "certificates"> {
|
||||
certificates?: Certificates;
|
||||
extends Omit<Schemas.FactoryOptions, "certificates"> {
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<typeof AbstractModel>,
|
||||
additionalBuffers?: BundleUnit,
|
||||
options: Schemas.FactoryOptions | InstanceType<typeof AbstractModel>,
|
||||
additionalBuffers?: Schemas.BundleUnit,
|
||||
abstractMissingData?: Omit<AbstractFactoryOptions, "model">,
|
||||
): Promise<Pass> {
|
||||
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(
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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<PartitionedBundle> {
|
||||
): Promise<Schemas.PartitionedBundle> {
|
||||
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<BundleUnit> = await Promise.all(
|
||||
const L10N_FilesListByFolder: Array<Schemas.BundleUnit> = 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<BundleUnit>((acc, file, index) => {
|
||||
if (!buffers[index].length) {
|
||||
return acc;
|
||||
}
|
||||
return validFiles.reduce<Schemas.BundleUnit>(
|
||||
(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<BundleUnit>(
|
||||
(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<Schemas.BundleUnit>((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<Certificates, "signerKey"> & {
|
||||
type flatCertificates = Omit<Schemas.Certificates, "signerKey"> & {
|
||||
signerKey: string;
|
||||
};
|
||||
|
||||
export async function readCertificatesFromOptions(
|
||||
options: Certificates,
|
||||
): Promise<FinalCertificates> {
|
||||
options: Schemas.Certificates,
|
||||
): Promise<Schemas.CertificatesSchema> {
|
||||
if (
|
||||
!(
|
||||
options &&
|
||||
Object.keys(options).length &&
|
||||
isValid(options, "certificatesSchema")
|
||||
Schemas.isValid(options, Schemas.CertificatesSchema)
|
||||
)
|
||||
) {
|
||||
throw new Error(formatMessage("CP_NO_CERTS"));
|
||||
|
||||
120
src/pass.ts
120
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<string, schema.Schema>([
|
||||
["barcodes", "barcode"],
|
||||
["barcode", "barcode"],
|
||||
["beacons", "beaconsDict"],
|
||||
["locations", "locationsDict"],
|
||||
["nfc", "nfcDict"],
|
||||
const propsSchemaMap = new Map<string, Joi.ObjectSchema<any>>([
|
||||
["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<string> = new Set<string>();
|
||||
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<schema.ValidPass>(
|
||||
) as (keyof Schemas.ValidPass)[];
|
||||
const validatedPassKeys = passCoreKeys.reduce<Schemas.ValidPass>(
|
||||
(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<schema.ArrayPassSchema>(
|
||||
const valid = getValidInArray<Schemas.ArrayPassSchema>(
|
||||
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<schema.Manifest>(
|
||||
const manifest = Object.keys(finalBundle).reduce<Schemas.Manifest>(
|
||||
(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<schema.Barcode[]>(
|
||||
const validBarcodes = data.reduce<Schemas.Barcode[]>(
|
||||
(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<schema.ValidPass> {
|
||||
get props(): Readonly<Schemas.ValidPass> {
|
||||
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<keyof schema.PassColors>;
|
||||
] as Array<keyof Schemas.PassColors>;
|
||||
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<T>(key: string, data: T[]): T[] {
|
||||
return getValidInArray(`${key}Dict` as schema.Schema, data);
|
||||
function processRelevancySet<T>(schema: Joi.ObjectSchema<T>, data: T[]): T[] {
|
||||
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(
|
||||
(current) =>
|
||||
Object.keys(current).length && schema.isValid(current, schemaName),
|
||||
Object.keys(current).length && Schemas.isValid(current, schemaName),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
624
src/schema.ts
624
src/schema.ts
@@ -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
26
src/schemas/barcode.ts
Normal 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
19
src/schemas/beacon.ts
Normal 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
70
src/schemas/field.ts
Normal 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
244
src/schemas/index.ts
Normal 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
15
src/schemas/location.ts
Normal 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
11
src/schemas/nfc.ts
Normal 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
35
src/schemas/passFields.ts
Normal 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)/,
|
||||
);
|
||||
26
src/schemas/personalization.ts
Normal file
26
src/schemas/personalization.ts
Normal 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
267
src/schemas/semantics.ts
Normal 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),
|
||||
});
|
||||
10
src/utils.ts
10
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(
|
||||
|
||||
Reference in New Issue
Block a user