mirror of
https://github.com/marcogll/passkit-generator.git
synced 2026-03-16 01:25:30 +00:00
Added prettier to project
This commit is contained in:
143
src/abstract.ts
143
src/abstract.ts
@@ -1,68 +1,75 @@
|
||||
import { Certificates, FinalCertificates, PartitionedBundle, OverridesSupportedOptions, FactoryOptions } from "./schema";
|
||||
import { getModelContents, readCertificatesFromOptions } from "./parser";
|
||||
import formatMessage from "./messages";
|
||||
|
||||
const abmCertificates = Symbol("certificates");
|
||||
const abmModel = Symbol("model");
|
||||
const abmOverrides = Symbol("overrides");
|
||||
|
||||
export interface AbstractFactoryOptions extends Omit<FactoryOptions, "certificates"> {
|
||||
certificates?: Certificates;
|
||||
}
|
||||
|
||||
interface AbstractModelOptions {
|
||||
bundle: PartitionedBundle;
|
||||
certificates: FinalCertificates;
|
||||
overrides?: OverridesSupportedOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an abstract model to keep data
|
||||
* in memory for future passes creation
|
||||
* @param options
|
||||
*/
|
||||
|
||||
export async function createAbstractModel(options: AbstractFactoryOptions) {
|
||||
if (!(options && Object.keys(options).length)) {
|
||||
throw new Error(formatMessage("CP_NO_OPTS"));
|
||||
}
|
||||
|
||||
try {
|
||||
const [bundle, certificates] = await Promise.all([
|
||||
getModelContents(options.model),
|
||||
readCertificatesFromOptions(options.certificates)
|
||||
]);
|
||||
|
||||
return new AbstractModel({
|
||||
bundle,
|
||||
certificates,
|
||||
overrides: options.overrides
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(formatMessage("CP_INIT_ERROR", "abstract model", err));
|
||||
}
|
||||
}
|
||||
|
||||
export class AbstractModel {
|
||||
private [abmCertificates]: FinalCertificates;
|
||||
private [abmModel]: PartitionedBundle;
|
||||
private [abmOverrides]: OverridesSupportedOptions;
|
||||
|
||||
constructor(options: AbstractModelOptions) {
|
||||
this[abmModel] = options.bundle;
|
||||
this[abmCertificates] = options.certificates;
|
||||
this[abmOverrides] = options.overrides
|
||||
}
|
||||
|
||||
get certificates(): FinalCertificates {
|
||||
return this[abmCertificates];
|
||||
}
|
||||
|
||||
get bundle(): PartitionedBundle {
|
||||
return this[abmModel];
|
||||
}
|
||||
|
||||
get overrides(): OverridesSupportedOptions {
|
||||
return this[abmOverrides];
|
||||
}
|
||||
}
|
||||
import {
|
||||
Certificates,
|
||||
FinalCertificates,
|
||||
PartitionedBundle,
|
||||
OverridesSupportedOptions,
|
||||
FactoryOptions,
|
||||
} from "./schema";
|
||||
import { getModelContents, readCertificatesFromOptions } from "./parser";
|
||||
import formatMessage from "./messages";
|
||||
|
||||
const abmCertificates = Symbol("certificates");
|
||||
const abmModel = Symbol("model");
|
||||
const abmOverrides = Symbol("overrides");
|
||||
|
||||
export interface AbstractFactoryOptions
|
||||
extends Omit<FactoryOptions, "certificates"> {
|
||||
certificates?: Certificates;
|
||||
}
|
||||
|
||||
interface AbstractModelOptions {
|
||||
bundle: PartitionedBundle;
|
||||
certificates: FinalCertificates;
|
||||
overrides?: OverridesSupportedOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an abstract model to keep data
|
||||
* in memory for future passes creation
|
||||
* @param options
|
||||
*/
|
||||
|
||||
export async function createAbstractModel(options: AbstractFactoryOptions) {
|
||||
if (!(options && Object.keys(options).length)) {
|
||||
throw new Error(formatMessage("CP_NO_OPTS"));
|
||||
}
|
||||
|
||||
try {
|
||||
const [bundle, certificates] = await Promise.all([
|
||||
getModelContents(options.model),
|
||||
readCertificatesFromOptions(options.certificates),
|
||||
]);
|
||||
|
||||
return new AbstractModel({
|
||||
bundle,
|
||||
certificates,
|
||||
overrides: options.overrides,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(formatMessage("CP_INIT_ERROR", "abstract model", err));
|
||||
}
|
||||
}
|
||||
|
||||
export class AbstractModel {
|
||||
private [abmCertificates]: FinalCertificates;
|
||||
private [abmModel]: PartitionedBundle;
|
||||
private [abmOverrides]: OverridesSupportedOptions;
|
||||
|
||||
constructor(options: AbstractModelOptions) {
|
||||
this[abmModel] = options.bundle;
|
||||
this[abmCertificates] = options.certificates;
|
||||
this[abmOverrides] = options.overrides;
|
||||
}
|
||||
|
||||
get certificates(): FinalCertificates {
|
||||
return this[abmCertificates];
|
||||
}
|
||||
|
||||
get bundle(): PartitionedBundle {
|
||||
return this[abmModel];
|
||||
}
|
||||
|
||||
get overrides(): OverridesSupportedOptions {
|
||||
return this[abmOverrides];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { Pass } from "./pass";
|
||||
import { FactoryOptions, BundleUnit, FinalCertificates, PartitionedBundle, OverridesSupportedOptions } from "./schema";
|
||||
import {
|
||||
FactoryOptions,
|
||||
BundleUnit,
|
||||
FinalCertificates,
|
||||
PartitionedBundle,
|
||||
OverridesSupportedOptions,
|
||||
} from "./schema";
|
||||
import formatMessage from "./messages";
|
||||
import { getModelContents, readCertificatesFromOptions } from "./parser";
|
||||
import { splitBufferBundle } from "./utils";
|
||||
@@ -16,9 +22,14 @@ import { AbstractModel, AbstractFactoryOptions } from "./abstract";
|
||||
export async function createPass(
|
||||
options: FactoryOptions | InstanceType<typeof AbstractModel>,
|
||||
additionalBuffers?: BundleUnit,
|
||||
abstractMissingData?: Omit<AbstractFactoryOptions, "model">
|
||||
abstractMissingData?: Omit<AbstractFactoryOptions, "model">,
|
||||
): Promise<Pass> {
|
||||
if (!(options && (options instanceof AbstractModel || Object.keys(options).length))) {
|
||||
if (
|
||||
!(
|
||||
options &&
|
||||
(options instanceof AbstractModel || Object.keys(options).length)
|
||||
)
|
||||
) {
|
||||
throw new Error(formatMessage("CP_NO_OPTS"));
|
||||
}
|
||||
|
||||
@@ -27,35 +38,62 @@ export async function createPass(
|
||||
let certificates: FinalCertificates;
|
||||
let overrides: OverridesSupportedOptions = {
|
||||
...(options.overrides || {}),
|
||||
...(abstractMissingData && abstractMissingData.overrides || {})
|
||||
...((abstractMissingData && abstractMissingData.overrides) ||
|
||||
{}),
|
||||
};
|
||||
|
||||
if (!(options.certificates && options.certificates.signerCert && options.certificates.signerKey) && abstractMissingData.certificates) {
|
||||
if (
|
||||
!(
|
||||
options.certificates &&
|
||||
options.certificates.signerCert &&
|
||||
options.certificates.signerKey
|
||||
) &&
|
||||
abstractMissingData.certificates
|
||||
) {
|
||||
certificates = Object.assign(
|
||||
options.certificates,
|
||||
await readCertificatesFromOptions(abstractMissingData.certificates)
|
||||
await readCertificatesFromOptions(
|
||||
abstractMissingData.certificates,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
certificates = options.certificates;
|
||||
}
|
||||
|
||||
return createPassInstance(options.bundle, certificates, overrides, additionalBuffers);
|
||||
return createPassInstance(
|
||||
options.bundle,
|
||||
certificates,
|
||||
overrides,
|
||||
additionalBuffers,
|
||||
);
|
||||
} else {
|
||||
const [bundle, certificates] = await Promise.all([
|
||||
getModelContents(options.model),
|
||||
readCertificatesFromOptions(options.certificates)
|
||||
readCertificatesFromOptions(options.certificates),
|
||||
]);
|
||||
|
||||
return createPassInstance(bundle, certificates, options.overrides, additionalBuffers);
|
||||
return createPassInstance(
|
||||
bundle,
|
||||
certificates,
|
||||
options.overrides,
|
||||
additionalBuffers,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(formatMessage("CP_INIT_ERROR", "pass", err));
|
||||
}
|
||||
}
|
||||
|
||||
function createPassInstance(model: PartitionedBundle, certificates: FinalCertificates, overrides: OverridesSupportedOptions, additionalBuffers?: BundleUnit) {
|
||||
function createPassInstance(
|
||||
model: PartitionedBundle,
|
||||
certificates: FinalCertificates,
|
||||
overrides: OverridesSupportedOptions,
|
||||
additionalBuffers?: BundleUnit,
|
||||
) {
|
||||
if (additionalBuffers) {
|
||||
const [additionalL10n, additionalBundle] = splitBufferBundle(additionalBuffers);
|
||||
const [additionalL10n, additionalBundle] = splitBufferBundle(
|
||||
additionalBuffers,
|
||||
);
|
||||
Object.assign(model["l10nBundle"], additionalL10n);
|
||||
Object.assign(model["bundle"], additionalBundle);
|
||||
}
|
||||
@@ -63,6 +101,6 @@ function createPassInstance(model: PartitionedBundle, certificates: FinalCertifi
|
||||
return new Pass({
|
||||
model,
|
||||
certificates,
|
||||
overrides
|
||||
overrides,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,20 +24,28 @@ export default class FieldsArray extends Array {
|
||||
*/
|
||||
|
||||
push(...fieldsData: schema.Field[]): number {
|
||||
const validFields = fieldsData.reduce((acc: schema.Field[], current: schema.Field) => {
|
||||
if (!(typeof current === "object") || !schema.isValid(current, "field")) {
|
||||
const validFields = fieldsData.reduce(
|
||||
(acc: schema.Field[], current: schema.Field) => {
|
||||
if (
|
||||
!(typeof current === "object") ||
|
||||
!schema.isValid(current, "field")
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (this[poolSymbol].has(current.key)) {
|
||||
fieldsDebug(
|
||||
`Field with key "${current.key}" discarded: fields must be unique in pass scope.`,
|
||||
);
|
||||
} else {
|
||||
this[poolSymbol].add(current.key);
|
||||
acc.push(current);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (this[poolSymbol].has(current.key)) {
|
||||
fieldsDebug(`Field with key "${current.key}" discarded: fields must be unique in pass scope.`);
|
||||
} else {
|
||||
this[poolSymbol].add(current.key);
|
||||
acc.push(current);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return Array.prototype.push.call(this, ...validFields);
|
||||
}
|
||||
@@ -58,9 +66,13 @@ export default class FieldsArray extends Array {
|
||||
* also uniqueKeys set
|
||||
*/
|
||||
|
||||
splice(start: number, deleteCount: number, ...items: schema.Field[]): schema.Field[] {
|
||||
splice(
|
||||
start: number,
|
||||
deleteCount: number,
|
||||
...items: schema.Field[]
|
||||
): schema.Field[] {
|
||||
const removeList = this.slice(start, deleteCount + start);
|
||||
removeList.forEach(item => this[poolSymbol].delete(item.key));
|
||||
removeList.forEach((item) => this[poolSymbol].delete(item.key));
|
||||
|
||||
return Array.prototype.splice.call(this, start, deleteCount, items);
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@ import { AbstractModel as AbstractModelClass } from "./abstract";
|
||||
|
||||
export { createPass } from "./factory";
|
||||
export { createAbstractModel } from "./abstract";
|
||||
export type Pass = InstanceType<typeof PassClass>
|
||||
export type AbstractModel = InstanceType<typeof AbstractModelClass>
|
||||
export type Pass = InstanceType<typeof PassClass>;
|
||||
export type AbstractModel = InstanceType<typeof AbstractModelClass>;
|
||||
|
||||
@@ -1,31 +1,51 @@
|
||||
const errors = {
|
||||
CP_INIT_ERROR: "Something went really bad in the %s initialization! Look at the log below this message. It should contain all the infos about the problem: \n%s",
|
||||
CP_NO_OPTS: "Cannot initialize the pass or abstract model creation: no options were passed.",
|
||||
CP_NO_CERTS: "Cannot initialize the pass creation: no valid certificates were passed.",
|
||||
PASSFILE_VALIDATION_FAILED: "Validation of pass type failed. Pass file is not a valid buffer or (more probably) does not respect the schema.\nRefer to https://apple.co/2Nvshvn to build a correct pass.",
|
||||
REQUIR_VALID_FAILED: "The options passed to Pass constructor does not meet the requirements.\nRefer to the documentation to compile them correctly.",
|
||||
MODEL_UNINITIALIZED: "Provided model ( %s ) matched but unitialized or may not contain icon or a valid pass.json.\nRefer to https://apple.co/2IhJr0Q, https://apple.co/2Nvshvn and documentation to fill the model correctly.",
|
||||
MODEL_NOT_VALID: "A model must be provided in form of path (string) or object { 'fileName': Buffer } in order to continue.",
|
||||
CP_INIT_ERROR:
|
||||
"Something went really bad in the %s initialization! Look at the log below this message. It should contain all the infos about the problem: \n%s",
|
||||
CP_NO_OPTS:
|
||||
"Cannot initialize the pass or abstract model creation: no options were passed.",
|
||||
CP_NO_CERTS:
|
||||
"Cannot initialize the pass creation: no valid certificates were passed.",
|
||||
PASSFILE_VALIDATION_FAILED:
|
||||
"Validation of pass type failed. Pass file is not a valid buffer or (more probably) does not respect the schema.\nRefer to https://apple.co/2Nvshvn to build a correct pass.",
|
||||
REQUIR_VALID_FAILED:
|
||||
"The options passed to Pass constructor does not meet the requirements.\nRefer to the documentation to compile them correctly.",
|
||||
MODEL_UNINITIALIZED:
|
||||
"Provided model ( %s ) matched but unitialized or may not contain icon or a valid pass.json.\nRefer to https://apple.co/2IhJr0Q, https://apple.co/2Nvshvn and documentation to fill the model correctly.",
|
||||
MODEL_NOT_VALID:
|
||||
"A model must be provided in form of path (string) or object { 'fileName': Buffer } in order to continue.",
|
||||
MODELF_NOT_FOUND: "Model %s not found. Provide a valid one to continue.",
|
||||
MODELF_FILE_NOT_FOUND: "File %s not found.",
|
||||
INVALID_CERTS: "Invalid certificate(s) loaded: %s. Please provide valid WWDR certificates and developer signer certificate and key (with passphrase).\nRefer to docs to obtain them.",
|
||||
INVALID_CERTS:
|
||||
"Invalid certificate(s) loaded: %s. Please provide valid WWDR certificates and developer signer certificate and key (with passphrase).\nRefer to docs to obtain them.",
|
||||
INVALID_CERT_PATH: "Invalid certificate loaded. %s does not exist.",
|
||||
TRSTYPE_REQUIRED: "Cannot proceed with pass creation. transitType field is required for boardingPasses.",
|
||||
OVV_KEYS_BADFORMAT: "Cannot proceed with pass creation due to bad keys format in overrides.",
|
||||
NO_PASS_TYPE: "Cannot proceed with pass creation. Model definition (pass.json) has no valid type in it.\nRefer to https://apple.co/2wzyL5J to choose a valid pass type."
|
||||
TRSTYPE_REQUIRED:
|
||||
"Cannot proceed with pass creation. transitType field is required for boardingPasses.",
|
||||
OVV_KEYS_BADFORMAT:
|
||||
"Cannot proceed with pass creation due to bad keys format in overrides.",
|
||||
NO_PASS_TYPE:
|
||||
"Cannot proceed with pass creation. Model definition (pass.json) has no valid type in it.\nRefer to https://apple.co/2wzyL5J to choose a valid pass type.",
|
||||
};
|
||||
|
||||
const debugMessages = {
|
||||
TRSTYPE_NOT_VALID: "Transit type changing rejected as not compliant with Apple Specifications. Transit type would become \"%s\" but should be in [PKTransitTypeAir, PKTransitTypeBoat, PKTransitTypeBus, PKTransitTypeGeneric, PKTransitTypeTrain]",
|
||||
BRC_NOT_SUPPORTED: "Format not found among barcodes. Cannot set backward compatibility.",
|
||||
BRC_FORMATTYPE_UNMATCH: "Format must be a string or null. Cannot set backward compatibility.",
|
||||
BRC_AUTC_MISSING_DATA: "Unable to autogenerate barcodes. Data is not a string.",
|
||||
BRC_BW_FORMAT_UNSUPPORTED: "This format is not supported (by Apple) for backward support. Please choose another one.",
|
||||
BRC_NO_POOL: "Cannot set barcode: no barcodes found. Please set barcodes first. Barcode is for retrocompatibility only.",
|
||||
TRSTYPE_NOT_VALID:
|
||||
'Transit type changing rejected as not compliant with Apple Specifications. Transit type would become "%s" but should be in [PKTransitTypeAir, PKTransitTypeBoat, PKTransitTypeBus, PKTransitTypeGeneric, PKTransitTypeTrain]',
|
||||
BRC_NOT_SUPPORTED:
|
||||
"Format not found among barcodes. Cannot set backward compatibility.",
|
||||
BRC_FORMATTYPE_UNMATCH:
|
||||
"Format must be a string or null. Cannot set backward compatibility.",
|
||||
BRC_AUTC_MISSING_DATA:
|
||||
"Unable to autogenerate barcodes. Data is not a string.",
|
||||
BRC_BW_FORMAT_UNSUPPORTED:
|
||||
"This format is not supported (by Apple) for backward support. Please choose another one.",
|
||||
BRC_NO_POOL:
|
||||
"Cannot set barcode: no barcodes found. Please set barcodes first. Barcode is for retrocompatibility only.",
|
||||
DATE_FORMAT_UNMATCH: "%s was not set due to incorrect date format.",
|
||||
NFC_INVALID: "Unable to set NFC properties: data not compliant with schema.",
|
||||
PRS_INVALID: "Unable to parse Personalization.json. File is not a valid JSON. Error: %s",
|
||||
PRS_REMOVED: "Personalization has been removed as it requires an NFC-enabled pass to work."
|
||||
NFC_INVALID:
|
||||
"Unable to set NFC properties: data not compliant with schema.",
|
||||
PRS_INVALID:
|
||||
"Unable to parse Personalization.json. File is not a valid JSON. Error: %s",
|
||||
PRS_REMOVED:
|
||||
"Personalization has been removed as it requires an NFC-enabled pass to work.",
|
||||
};
|
||||
|
||||
type AllMessages = keyof (typeof debugMessages & typeof errors);
|
||||
|
||||
211
src/parser.ts
211
src/parser.ts
@@ -1,8 +1,21 @@
|
||||
import * as path from "path";
|
||||
import forge from "node-forge";
|
||||
import formatMessage from "./messages";
|
||||
import { FactoryOptions, PartitionedBundle, BundleUnit, Certificates, FinalCertificates, isValid } from "./schema";
|
||||
import { removeHidden, splitBufferBundle, getAllFilesWithName, hasFilesWithName, deletePersonalization } from "./utils";
|
||||
import {
|
||||
FactoryOptions,
|
||||
PartitionedBundle,
|
||||
BundleUnit,
|
||||
Certificates,
|
||||
FinalCertificates,
|
||||
isValid,
|
||||
} from "./schema";
|
||||
import {
|
||||
removeHidden,
|
||||
splitBufferBundle,
|
||||
getAllFilesWithName,
|
||||
hasFilesWithName,
|
||||
deletePersonalization,
|
||||
} from "./utils";
|
||||
import fs from "fs";
|
||||
import debug from "debug";
|
||||
|
||||
@@ -27,10 +40,9 @@ export async function getModelContents(model: FactoryOptions["model"]) {
|
||||
}
|
||||
|
||||
const modelFiles = Object.keys(modelContents.bundle);
|
||||
const isModelInitialized = (
|
||||
const isModelInitialized =
|
||||
modelFiles.includes("pass.json") &&
|
||||
hasFilesWithName("icon", modelFiles, "startsWith")
|
||||
);
|
||||
hasFilesWithName("icon", modelFiles, "startsWith");
|
||||
|
||||
if (!isModelInitialized) {
|
||||
throw new Error(formatMessage("MODEL_UNINITIALIZED", "parse result"));
|
||||
@@ -46,19 +58,34 @@ export async function getModelContents(model: FactoryOptions["model"]) {
|
||||
return modelContents;
|
||||
}
|
||||
|
||||
const logoFullNames = getAllFilesWithName("personalizationLogo", modelFiles, "startsWith");
|
||||
if (!(logoFullNames.length && modelContents.bundle[personalizationJsonFile].length)) {
|
||||
const logoFullNames = getAllFilesWithName(
|
||||
"personalizationLogo",
|
||||
modelFiles,
|
||||
"startsWith",
|
||||
);
|
||||
if (
|
||||
!(
|
||||
logoFullNames.length &&
|
||||
modelContents.bundle[personalizationJsonFile].length
|
||||
)
|
||||
) {
|
||||
deletePersonalization(modelContents.bundle, logoFullNames);
|
||||
return modelContents;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedPersonalization = JSON.parse(modelContents.bundle[personalizationJsonFile].toString("utf8"));
|
||||
const isPersonalizationValid = isValid(parsedPersonalization, "personalizationDict");
|
||||
const parsedPersonalization = JSON.parse(
|
||||
modelContents.bundle[personalizationJsonFile].toString("utf8"),
|
||||
);
|
||||
const isPersonalizationValid = isValid(
|
||||
parsedPersonalization,
|
||||
"personalizationDict",
|
||||
);
|
||||
|
||||
if (!isPersonalizationValid) {
|
||||
[...logoFullNames, personalizationJsonFile]
|
||||
.forEach(file => delete modelContents.bundle[file]);
|
||||
[...logoFullNames, personalizationJsonFile].forEach(
|
||||
(file) => delete modelContents.bundle[file],
|
||||
);
|
||||
|
||||
return modelContents;
|
||||
}
|
||||
@@ -76,55 +103,70 @@ export async function getModelContents(model: FactoryOptions["model"]) {
|
||||
* @param model
|
||||
*/
|
||||
|
||||
export async function getModelFolderContents(model: string): Promise<PartitionedBundle> {
|
||||
export async function getModelFolderContents(
|
||||
model: string,
|
||||
): Promise<PartitionedBundle> {
|
||||
try {
|
||||
const modelPath = `${model}${!path.extname(model) && ".pass" || ""}`;
|
||||
const modelPath = `${model}${(!path.extname(model) && ".pass") || ""}`;
|
||||
const modelFilesList = await readDir(modelPath);
|
||||
|
||||
// No dot-starting files, manifest and signature
|
||||
const filteredFiles = removeHidden(modelFilesList)
|
||||
.filter(f => !/(manifest|signature)/i.test(f) && /.+$/.test(path.parse(f).ext));
|
||||
|
||||
const isModelInitialized = (
|
||||
filteredFiles.length &&
|
||||
hasFilesWithName("icon", filteredFiles, "startsWith")
|
||||
const filteredFiles = removeHidden(modelFilesList).filter(
|
||||
(f) =>
|
||||
!/(manifest|signature)/i.test(f) &&
|
||||
/.+$/.test(path.parse(f).ext),
|
||||
);
|
||||
|
||||
const isModelInitialized =
|
||||
filteredFiles.length &&
|
||||
hasFilesWithName("icon", filteredFiles, "startsWith");
|
||||
|
||||
// Icon is required to proceed
|
||||
if (!isModelInitialized) {
|
||||
throw new Error(formatMessage(
|
||||
"MODEL_UNINITIALIZED",
|
||||
path.parse(model).name
|
||||
));
|
||||
throw new Error(
|
||||
formatMessage("MODEL_UNINITIALIZED", path.parse(model).name),
|
||||
);
|
||||
}
|
||||
|
||||
// Splitting files from localization folders
|
||||
const rawBundleFiles = filteredFiles.filter(entry => !entry.includes(".lproj"));
|
||||
const l10nFolders = filteredFiles.filter(entry => entry.includes(".lproj"));
|
||||
|
||||
const rawBundleBuffers = await Promise.all(
|
||||
rawBundleFiles.map(file => readFile(path.resolve(modelPath, file)))
|
||||
const rawBundleFiles = filteredFiles.filter(
|
||||
(entry) => !entry.includes(".lproj"),
|
||||
);
|
||||
const l10nFolders = filteredFiles.filter((entry) =>
|
||||
entry.includes(".lproj"),
|
||||
);
|
||||
|
||||
const bundle: BundleUnit = Object.assign({},
|
||||
...rawBundleFiles.map((fileName, index) => ({ [fileName]: rawBundleBuffers[index] }))
|
||||
const rawBundleBuffers = await Promise.all(
|
||||
rawBundleFiles.map((file) =>
|
||||
readFile(path.resolve(modelPath, file)),
|
||||
),
|
||||
);
|
||||
|
||||
const bundle: BundleUnit = Object.assign(
|
||||
{},
|
||||
...rawBundleFiles.map((fileName, index) => ({
|
||||
[fileName]: rawBundleBuffers[index],
|
||||
})),
|
||||
);
|
||||
|
||||
// Reading concurrently localizations folder
|
||||
// and their files and their buffers
|
||||
const L10N_FilesListByFolder: Array<BundleUnit> = await Promise.all(
|
||||
l10nFolders.map(async folderPath => {
|
||||
l10nFolders.map(async (folderPath) => {
|
||||
// Reading current folder
|
||||
const currentLangPath = path.join(modelPath, folderPath);
|
||||
|
||||
const files = await readDir(currentLangPath);
|
||||
// Transforming files path to a model-relative path
|
||||
const validFiles = removeHidden(files)
|
||||
.map(file => path.join(currentLangPath, file));
|
||||
const validFiles = removeHidden(files).map((file) =>
|
||||
path.join(currentLangPath, file),
|
||||
);
|
||||
|
||||
// Getting all the buffers from file paths
|
||||
const buffers = await Promise.all(
|
||||
validFiles.map(file => readFile(file).catch(() => Buffer.alloc(0)))
|
||||
validFiles.map((file) =>
|
||||
readFile(file).catch(() => Buffer.alloc(0)),
|
||||
),
|
||||
);
|
||||
|
||||
// Assigning each file path to its buffer
|
||||
@@ -140,34 +182,37 @@ export async function getModelFolderContents(model: string): Promise<Partitioned
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[fileName]: buffers[index]
|
||||
[fileName]: buffers[index],
|
||||
};
|
||||
}, {});
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign(
|
||||
{},
|
||||
...L10N_FilesListByFolder
|
||||
.map((folder, index) => ({ [l10nFolders[index]]: folder }))
|
||||
...L10N_FilesListByFolder.map((folder, index) => ({
|
||||
[l10nFolders[index]]: folder,
|
||||
})),
|
||||
);
|
||||
|
||||
return {
|
||||
bundle,
|
||||
l10nBundle
|
||||
l10nBundle,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err?.code === "ENOENT") {
|
||||
if (err.syscall === "open") {
|
||||
// file opening failed
|
||||
throw new Error(formatMessage("MODELF_NOT_FOUND", err.path))
|
||||
throw new Error(formatMessage("MODELF_NOT_FOUND", err.path));
|
||||
} else if (err.syscall === "scandir") {
|
||||
// directory reading failed
|
||||
const pathContents = (err.path as string).split(/(\/|\\\?)/);
|
||||
throw new Error(formatMessage(
|
||||
"MODELF_FILE_NOT_FOUND",
|
||||
pathContents[pathContents.length - 1]
|
||||
))
|
||||
throw new Error(
|
||||
formatMessage(
|
||||
"MODELF_FILE_NOT_FOUND",
|
||||
pathContents[pathContents.length - 1],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,27 +227,28 @@ export async function getModelFolderContents(model: string): Promise<Partitioned
|
||||
*/
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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);
|
||||
|
||||
const isModelInitialized = (
|
||||
bundleKeys.length &&
|
||||
hasFilesWithName("icon", bundleKeys, "startsWith")
|
||||
);
|
||||
const isModelInitialized =
|
||||
bundleKeys.length && hasFilesWithName("icon", bundleKeys, "startsWith");
|
||||
|
||||
// Icon is required to proceed
|
||||
if (!isModelInitialized) {
|
||||
throw new Error(formatMessage("MODEL_UNINITIALIZED", "Buffers"))
|
||||
throw new Error(formatMessage("MODEL_UNINITIALIZED", "Buffers"));
|
||||
}
|
||||
|
||||
// separing localization folders from bundle files
|
||||
@@ -210,7 +256,7 @@ export function getModelBufferContents(model: BundleUnit): PartitionedBundle {
|
||||
|
||||
return {
|
||||
bundle,
|
||||
l10nBundle
|
||||
l10nBundle,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -224,8 +270,16 @@ type flatCertificates = Omit<Certificates, "signerKey"> & {
|
||||
signerKey: string;
|
||||
};
|
||||
|
||||
export async function readCertificatesFromOptions(options: Certificates): Promise<FinalCertificates> {
|
||||
if (!(options && Object.keys(options).length && isValid(options, "certificatesSchema"))) {
|
||||
export async function readCertificatesFromOptions(
|
||||
options: Certificates,
|
||||
): Promise<FinalCertificates> {
|
||||
if (
|
||||
!(
|
||||
options &&
|
||||
Object.keys(options).length &&
|
||||
isValid(options, "certificatesSchema")
|
||||
)
|
||||
) {
|
||||
throw new Error(formatMessage("CP_NO_CERTS"));
|
||||
}
|
||||
|
||||
@@ -239,30 +293,31 @@ export async function readCertificatesFromOptions(options: Certificates): Promis
|
||||
|
||||
// if the signerKey is an object, we want to get
|
||||
// all the real contents and don't care of passphrase
|
||||
const flattenedDocs = Object.assign({}, options, { signerKey }) as flatCertificates;
|
||||
const flattenedDocs = Object.assign({}, options, {
|
||||
signerKey,
|
||||
}) as flatCertificates;
|
||||
|
||||
// We read the contents
|
||||
const rawContentsPromises = Object.keys(flattenedDocs)
|
||||
.map(key => {
|
||||
const content = flattenedDocs[key];
|
||||
const rawContentsPromises = Object.keys(flattenedDocs).map((key) => {
|
||||
const content = flattenedDocs[key];
|
||||
|
||||
if (!!path.parse(content).ext) {
|
||||
// The content is a path to the document
|
||||
return readFile(path.resolve(content), { encoding: "utf8" });
|
||||
} else {
|
||||
// Content is the real document content
|
||||
return Promise.resolve(content);
|
||||
}
|
||||
});
|
||||
if (!!path.parse(content).ext) {
|
||||
// The content is a path to the document
|
||||
return readFile(path.resolve(content), { encoding: "utf8" });
|
||||
} else {
|
||||
// Content is the real document content
|
||||
return Promise.resolve(content);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const parsedContents = await Promise.all(rawContentsPromises);
|
||||
const pemParsedContents = parsedContents.map((file, index) => {
|
||||
const certName = Object.keys(options)[index];
|
||||
const passphrase = (
|
||||
typeof options.signerKey === "object" &&
|
||||
options.signerKey?.passphrase
|
||||
) || undefined;
|
||||
const passphrase =
|
||||
(typeof options.signerKey === "object" &&
|
||||
options.signerKey?.passphrase) ||
|
||||
undefined;
|
||||
|
||||
const pem = parsePEM(certName, file, passphrase);
|
||||
|
||||
@@ -279,7 +334,9 @@ export async function readCertificatesFromOptions(options: Certificates): Promis
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw new Error(formatMessage("INVALID_CERT_PATH", path.parse(err.path).base));
|
||||
throw new Error(
|
||||
formatMessage("INVALID_CERT_PATH", path.parse(err.path).base),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
328
src/pass.ts
328
src/pass.ts
@@ -7,7 +7,13 @@ import { ZipFile } from "yazl";
|
||||
import * as schema from "./schema";
|
||||
import formatMessage from "./messages";
|
||||
import FieldsArray from "./fieldsArray";
|
||||
import { generateStringFile, dateToW3CString, isValidRGB, deletePersonalization, getAllFilesWithName } from "./utils";
|
||||
import {
|
||||
generateStringFile,
|
||||
dateToW3CString,
|
||||
isValidRGB,
|
||||
deletePersonalization,
|
||||
getAllFilesWithName,
|
||||
} from "./utils";
|
||||
|
||||
const barcodeDebug = debug("passkit:barcode");
|
||||
const genericDebug = debug("passkit:generic");
|
||||
@@ -20,7 +26,7 @@ const propsSchemaMap = new Map<string, schema.Schema>([
|
||||
["barcode", "barcode"],
|
||||
["beacons", "beaconsDict"],
|
||||
["locations", "locationsDict"],
|
||||
["nfc", "nfcDict"]
|
||||
["nfc", "nfcDict"],
|
||||
]);
|
||||
|
||||
export class Pass {
|
||||
@@ -42,7 +48,9 @@ export class Pass {
|
||||
|
||||
private Certificates: schema.FinalCertificates;
|
||||
private [transitType]: string = "";
|
||||
private l10nTranslations: { [languageCode: string]: { [placeholder: string]: string } } = {};
|
||||
private l10nTranslations: {
|
||||
[languageCode: string]: { [placeholder: string]: string };
|
||||
} = {};
|
||||
|
||||
constructor(options: schema.PassInstance) {
|
||||
if (!schema.isValid(options, "instance")) {
|
||||
@@ -54,77 +62,101 @@ export class Pass {
|
||||
this.bundle = { ...options.model.bundle };
|
||||
|
||||
try {
|
||||
this.passCore = JSON.parse(this.bundle["pass.json"].toString("utf8"));
|
||||
this.passCore = JSON.parse(
|
||||
this.bundle["pass.json"].toString("utf8"),
|
||||
);
|
||||
} catch (err) {
|
||||
throw new Error(formatMessage("PASSFILE_VALIDATION_FAILED"));
|
||||
}
|
||||
|
||||
// Parsing the options and extracting only the valid ones.
|
||||
const validOverrides = schema.getValidated(options.overrides || {}, "supportedOptions") as schema.OverridesSupportedOptions;
|
||||
const validOverrides = schema.getValidated(
|
||||
options.overrides || {},
|
||||
"supportedOptions",
|
||||
) as schema.OverridesSupportedOptions;
|
||||
|
||||
if (validOverrides === null) {
|
||||
throw new Error(formatMessage("OVV_KEYS_BADFORMAT"))
|
||||
throw new Error(formatMessage("OVV_KEYS_BADFORMAT"));
|
||||
}
|
||||
|
||||
this.type = Object.keys(this.passCore)
|
||||
.find(key => /(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key)) as keyof schema.ValidPassType;
|
||||
this.type = Object.keys(this.passCore).find((key) =>
|
||||
/(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key),
|
||||
) as keyof schema.ValidPassType;
|
||||
|
||||
if (!this.type) {
|
||||
throw new Error(formatMessage("NO_PASS_TYPE"));
|
||||
}
|
||||
|
||||
// Parsing and validating pass.json keys
|
||||
const passCoreKeys = Object.keys(this.passCore) as (keyof schema.ValidPass)[];
|
||||
const validatedPassKeys = passCoreKeys.reduce<schema.ValidPass>((acc, current) => {
|
||||
if (this.type === current) {
|
||||
// We want to exclude type keys (eventTicket,
|
||||
// boardingPass, ecc.) and their content
|
||||
return acc;
|
||||
}
|
||||
const passCoreKeys = Object.keys(
|
||||
this.passCore,
|
||||
) as (keyof schema.ValidPass)[];
|
||||
const validatedPassKeys = passCoreKeys.reduce<schema.ValidPass>(
|
||||
(acc, current) => {
|
||||
if (this.type === current) {
|
||||
// We want to exclude type keys (eventTicket,
|
||||
// boardingPass, ecc.) and their content
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (!propsSchemaMap.has(current)) {
|
||||
// If the property is unknown (we don't care if
|
||||
// it is valid or not for Wallet), we return
|
||||
// directly the content
|
||||
return { ...acc, [current]: this.passCore[current] };
|
||||
}
|
||||
if (!propsSchemaMap.has(current)) {
|
||||
// If the property is unknown (we don't care if
|
||||
// it is valid or not for Wallet), we return
|
||||
// directly the content
|
||||
return { ...acc, [current]: this.passCore[current] };
|
||||
}
|
||||
|
||||
const currentSchema = propsSchemaMap.get(current)!;
|
||||
const currentSchema = propsSchemaMap.get(current)!;
|
||||
|
||||
if (Array.isArray(this.passCore[current])) {
|
||||
const valid = getValidInArray<schema.ArrayPassSchema>(
|
||||
currentSchema,
|
||||
this.passCore[current] as schema.ArrayPassSchema[]
|
||||
);
|
||||
return { ...acc, [current]: valid };
|
||||
} else {
|
||||
return {
|
||||
...acc,
|
||||
[current]: schema.isValid(
|
||||
this.passCore[current],
|
||||
currentSchema
|
||||
) && this.passCore[current] || undefined
|
||||
};
|
||||
}
|
||||
}, {});
|
||||
if (Array.isArray(this.passCore[current])) {
|
||||
const valid = getValidInArray<schema.ArrayPassSchema>(
|
||||
currentSchema,
|
||||
this.passCore[current] as schema.ArrayPassSchema[],
|
||||
);
|
||||
return { ...acc, [current]: valid };
|
||||
} else {
|
||||
return {
|
||||
...acc,
|
||||
[current]:
|
||||
(schema.isValid(
|
||||
this.passCore[current],
|
||||
currentSchema,
|
||||
) &&
|
||||
this.passCore[current]) ||
|
||||
undefined,
|
||||
};
|
||||
}
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
this[passProps] = {
|
||||
...(validatedPassKeys || {}),
|
||||
...(validOverrides || {})
|
||||
...(validOverrides || {}),
|
||||
};
|
||||
|
||||
if (this.type === "boardingPass" && this.passCore[this.type]["transitType"]) {
|
||||
if (
|
||||
this.type === "boardingPass" &&
|
||||
this.passCore[this.type]["transitType"]
|
||||
) {
|
||||
// We might want to generate a boarding pass without setting manually
|
||||
// in the code the transit type but right in the model;
|
||||
this[transitType] = this.passCore[this.type]["transitType"];
|
||||
}
|
||||
|
||||
this._fields = ["primaryFields", "secondaryFields", "auxiliaryFields", "backFields", "headerFields"];
|
||||
this._fields.forEach(fieldName => {
|
||||
this._fields = [
|
||||
"primaryFields",
|
||||
"secondaryFields",
|
||||
"auxiliaryFields",
|
||||
"backFields",
|
||||
"headerFields",
|
||||
];
|
||||
this._fields.forEach((fieldName) => {
|
||||
this[fieldName] = new FieldsArray(
|
||||
this.fieldsKeys,
|
||||
...(this.passCore[this.type][fieldName] || [])
|
||||
.filter(field => schema.isValid(field, "field"))
|
||||
...(this.passCore[this.type][fieldName] || []).filter((field) =>
|
||||
schema.isValid(field, "field"),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -146,13 +178,19 @@ export class Pass {
|
||||
*/
|
||||
const currentBundleFiles = Object.keys(this.bundle);
|
||||
|
||||
if (!this[passProps].nfc && currentBundleFiles.includes("personalization.json")) {
|
||||
if (
|
||||
!this[passProps].nfc &&
|
||||
currentBundleFiles.includes("personalization.json")
|
||||
) {
|
||||
genericDebug(formatMessage("PRS_REMOVED"));
|
||||
deletePersonalization(this.bundle, getAllFilesWithName(
|
||||
"personalizationLogo",
|
||||
currentBundleFiles,
|
||||
"startsWith"
|
||||
));
|
||||
deletePersonalization(
|
||||
this.bundle,
|
||||
getAllFilesWithName(
|
||||
"personalizationLogo",
|
||||
currentBundleFiles,
|
||||
"startsWith",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const finalBundle = { ...this.bundle } as schema.BundleUnit;
|
||||
@@ -161,7 +199,7 @@ export class Pass {
|
||||
* Iterating through languages and generating pass.string file
|
||||
*/
|
||||
|
||||
Object.keys(this.l10nTranslations).forEach(lang => {
|
||||
Object.keys(this.l10nTranslations).forEach((lang) => {
|
||||
const strings = generateStringFile(this.l10nTranslations[lang]);
|
||||
const langInBundles = `${lang}.lproj`;
|
||||
|
||||
@@ -176,13 +214,21 @@ export class Pass {
|
||||
this.l10nBundles[langInBundles] = {};
|
||||
}
|
||||
|
||||
this.l10nBundles[langInBundles]["pass.strings"] = Buffer.concat([
|
||||
this.l10nBundles[langInBundles]["pass.strings"] || Buffer.alloc(0),
|
||||
strings
|
||||
this.l10nBundles[langInBundles][
|
||||
"pass.strings"
|
||||
] = Buffer.concat([
|
||||
this.l10nBundles[langInBundles]["pass.strings"] ||
|
||||
Buffer.alloc(0),
|
||||
strings,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!(this.l10nBundles[langInBundles] && Object.keys(this.l10nBundles[langInBundles]).length)) {
|
||||
if (
|
||||
!(
|
||||
this.l10nBundles[langInBundles] &&
|
||||
Object.keys(this.l10nBundles[langInBundles]).length
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -194,33 +240,48 @@ export class Pass {
|
||||
* composition.
|
||||
*/
|
||||
|
||||
Object.assign(finalBundle, ...Object.keys(this.l10nBundles[langInBundles])
|
||||
.map(fileName => {
|
||||
const fullPath = path.join(langInBundles, fileName).replace(/\\/, "/");
|
||||
return { [fullPath]: this.l10nBundles[langInBundles][fileName] };
|
||||
})
|
||||
Object.assign(
|
||||
finalBundle,
|
||||
...Object.keys(this.l10nBundles[langInBundles]).map(
|
||||
(fileName) => {
|
||||
const fullPath = path
|
||||
.join(langInBundles, fileName)
|
||||
.replace(/\\/, "/");
|
||||
return {
|
||||
[fullPath]: this.l10nBundles[langInBundles][
|
||||
fileName
|
||||
],
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
* Parsing the buffers, pushing them into the archive
|
||||
* and returning the compiled manifest
|
||||
*/
|
||||
* Parsing the buffers, pushing them into the archive
|
||||
* and returning the compiled manifest
|
||||
*/
|
||||
const archive = new ZipFile();
|
||||
const manifest = Object.keys(finalBundle).reduce<schema.Manifest>((acc, current) => {
|
||||
let hashFlow = forge.md.sha1.create();
|
||||
const manifest = Object.keys(finalBundle).reduce<schema.Manifest>(
|
||||
(acc, current) => {
|
||||
let hashFlow = forge.md.sha1.create();
|
||||
|
||||
hashFlow.update(finalBundle[current].toString("binary"));
|
||||
archive.addBuffer(finalBundle[current], current);
|
||||
acc[current] = hashFlow.digest().toHex();
|
||||
hashFlow.update(finalBundle[current].toString("binary"));
|
||||
archive.addBuffer(finalBundle[current], current);
|
||||
acc[current] = hashFlow.digest().toHex();
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const signatureBuffer = this._sign(manifest);
|
||||
|
||||
archive.addBuffer(signatureBuffer, "signature");
|
||||
archive.addBuffer(Buffer.from(JSON.stringify(manifest)), "manifest.json");
|
||||
archive.addBuffer(
|
||||
Buffer.from(JSON.stringify(manifest)),
|
||||
"manifest.json",
|
||||
);
|
||||
const passStream = new Stream.PassThrough();
|
||||
|
||||
archive.outputStream.pipe(passStream);
|
||||
@@ -242,8 +303,15 @@ export class Pass {
|
||||
* @see https://apple.co/2KOv0OW - Passes support localization
|
||||
*/
|
||||
|
||||
localize(lang: string, translations?: { [placeholder: string]: string }): this {
|
||||
if (lang && typeof lang === "string" && (typeof translations === "object" || translations === undefined)) {
|
||||
localize(
|
||||
lang: string,
|
||||
translations?: { [placeholder: string]: string },
|
||||
): this {
|
||||
if (
|
||||
lang &&
|
||||
typeof lang === "string" &&
|
||||
(typeof translations === "object" || translations === undefined)
|
||||
) {
|
||||
this.l10nTranslations[lang] = translations || {};
|
||||
}
|
||||
|
||||
@@ -292,7 +360,7 @@ export class Pass {
|
||||
*/
|
||||
|
||||
beacons(resetFlag: null): this;
|
||||
beacons(...data: schema.Beacon[]): this
|
||||
beacons(...data: schema.Beacon[]): this;
|
||||
beacons(...data: (schema.Beacon | null)[]): this {
|
||||
if (data[0] === null) {
|
||||
delete this[passProps]["beacons"];
|
||||
@@ -322,7 +390,10 @@ export class Pass {
|
||||
return this;
|
||||
}
|
||||
|
||||
const valid = processRelevancySet("locations", data as schema.Location[]);
|
||||
const valid = processRelevancySet(
|
||||
"locations",
|
||||
data as schema.Location[],
|
||||
);
|
||||
|
||||
if (valid.length) {
|
||||
this[passProps]["locations"] = valid;
|
||||
@@ -390,19 +461,28 @@ export class Pass {
|
||||
* Validation assign default value to missing parameters (if any).
|
||||
*/
|
||||
|
||||
const validBarcodes = data.reduce<schema.Barcode[]>((acc, current) => {
|
||||
if (!(current && current instanceof Object)) {
|
||||
return acc;
|
||||
}
|
||||
const validBarcodes = data.reduce<schema.Barcode[]>(
|
||||
(acc, current) => {
|
||||
if (!(current && current instanceof Object)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const validated = schema.getValidated(current, "barcode");
|
||||
const validated = schema.getValidated(current, "barcode");
|
||||
|
||||
if (!(validated && validated instanceof Object && Object.keys(validated).length)) {
|
||||
return acc;
|
||||
}
|
||||
if (
|
||||
!(
|
||||
validated &&
|
||||
validated instanceof Object &&
|
||||
Object.keys(validated).length
|
||||
)
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [...acc, validated] as schema.Barcode[];
|
||||
}, []);
|
||||
return [...acc, validated] as schema.Barcode[];
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (validBarcodes.length) {
|
||||
this[passProps]["barcodes"] = validBarcodes;
|
||||
@@ -446,7 +526,9 @@ export class Pass {
|
||||
}
|
||||
|
||||
// Checking which object among barcodes has the same format of the specified one.
|
||||
const index = barcodes.findIndex(b => b.format.toLowerCase().includes(chosenFormat.toLowerCase()));
|
||||
const index = barcodes.findIndex((b) =>
|
||||
b.format.toLowerCase().includes(chosenFormat.toLowerCase()),
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
barcodeDebug(formatMessage("BRC_NOT_SUPPORTED"));
|
||||
@@ -472,7 +554,14 @@ export class Pass {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!(data && typeof data === "object" && !Array.isArray(data) && schema.isValid(data, "nfcDict"))) {
|
||||
if (
|
||||
!(
|
||||
data &&
|
||||
typeof data === "object" &&
|
||||
!Array.isArray(data) &&
|
||||
schema.isValid(data, "nfcDict")
|
||||
)
|
||||
) {
|
||||
genericDebug(formatMessage("NFC_INVALID"));
|
||||
return this;
|
||||
}
|
||||
@@ -505,7 +594,10 @@ export class Pass {
|
||||
private _sign(manifest: schema.Manifest): Buffer {
|
||||
const signature = forge.pkcs7.createSignedData();
|
||||
|
||||
signature.content = forge.util.createBuffer(JSON.stringify(manifest), "utf8");
|
||||
signature.content = forge.util.createBuffer(
|
||||
JSON.stringify(manifest),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
signature.addCertificate(this.Certificates.wwdr);
|
||||
signature.addCertificate(this.Certificates.signerCert);
|
||||
@@ -523,14 +615,18 @@ export class Pass {
|
||||
key: this.Certificates.signerKey,
|
||||
certificate: this.Certificates.signerCert,
|
||||
digestAlgorithm: forge.pki.oids.sha1,
|
||||
authenticatedAttributes: [{
|
||||
type: forge.pki.oids.contentType,
|
||||
value: forge.pki.oids.data
|
||||
}, {
|
||||
type: forge.pki.oids.messageDigest,
|
||||
}, {
|
||||
type: forge.pki.oids.signingTime,
|
||||
}]
|
||||
authenticatedAttributes: [
|
||||
{
|
||||
type: forge.pki.oids.contentType,
|
||||
value: forge.pki.oids.data,
|
||||
},
|
||||
{
|
||||
type: forge.pki.oids.messageDigest,
|
||||
},
|
||||
{
|
||||
type: forge.pki.oids.signingTime,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -554,7 +650,10 @@ export class Pass {
|
||||
* of beautiful things. ¯\_(ツ)_/¯
|
||||
*/
|
||||
|
||||
return Buffer.from(forge.asn1.toDer(signature.toAsn1()).getBytes(), "binary");
|
||||
return Buffer.from(
|
||||
forge.asn1.toDer(signature.toAsn1()).getBytes(),
|
||||
"binary",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -566,7 +665,9 @@ export class Pass {
|
||||
*/
|
||||
|
||||
private _patch(passCoreBuffer: Buffer): Buffer {
|
||||
const passFile = JSON.parse(passCoreBuffer.toString()) as schema.ValidPass;
|
||||
const passFile = JSON.parse(
|
||||
passCoreBuffer.toString(),
|
||||
) as schema.ValidPass;
|
||||
|
||||
if (Object.keys(this[passProps]).length) {
|
||||
/*
|
||||
@@ -575,14 +676,22 @@ export class Pass {
|
||||
* and then delete it from the passFile.
|
||||
*/
|
||||
|
||||
const passColors = ["backgroundColor", "foregroundColor", "labelColor"] as Array<keyof schema.PassColors>;
|
||||
passColors.filter(v => this[passProps][v] && !isValidRGB(this[passProps][v]))
|
||||
.forEach(v => delete this[passProps][v]);
|
||||
const passColors = [
|
||||
"backgroundColor",
|
||||
"foregroundColor",
|
||||
"labelColor",
|
||||
] as Array<keyof schema.PassColors>;
|
||||
passColors
|
||||
.filter(
|
||||
(v) =>
|
||||
this[passProps][v] && !isValidRGB(this[passProps][v]),
|
||||
)
|
||||
.forEach((v) => delete this[passProps][v]);
|
||||
|
||||
Object.assign(passFile, this[passProps]);
|
||||
}
|
||||
|
||||
this._fields.forEach(field => {
|
||||
this._fields.forEach((field) => {
|
||||
passFile[this.type][field] = this[field];
|
||||
});
|
||||
|
||||
@@ -627,8 +736,14 @@ function barcodesFromUncompleteData(message: string): schema.Barcode[] {
|
||||
"PKBarcodeFormatQR",
|
||||
"PKBarcodeFormatPDF417",
|
||||
"PKBarcodeFormatAztec",
|
||||
"PKBarcodeFormatCode128"
|
||||
].map(format => schema.getValidated({ format, message }, "barcode") as schema.Barcode);
|
||||
"PKBarcodeFormatCode128",
|
||||
].map(
|
||||
(format) =>
|
||||
schema.getValidated(
|
||||
{ format, message },
|
||||
"barcode",
|
||||
) as schema.Barcode,
|
||||
);
|
||||
}
|
||||
|
||||
function processRelevancySet<T>(key: string, data: T[]): T[] {
|
||||
@@ -636,7 +751,10 @@ function processRelevancySet<T>(key: string, data: T[]): T[] {
|
||||
}
|
||||
|
||||
function getValidInArray<T>(schemaName: schema.Schema, contents: T[]): T[] {
|
||||
return contents.filter(current => Object.keys(current).length && schema.isValid(current, schemaName));
|
||||
return contents.filter(
|
||||
(current) =>
|
||||
Object.keys(current).length && schema.isValid(current, schemaName),
|
||||
);
|
||||
}
|
||||
|
||||
function processDate(key: string, date: Date): string | null {
|
||||
|
||||
254
src/schema.ts
254
src/schema.ts
@@ -10,10 +10,12 @@ export interface Manifest {
|
||||
export interface Certificates {
|
||||
wwdr?: string;
|
||||
signerCert?: string;
|
||||
signerKey?: {
|
||||
keyFile: string;
|
||||
passphrase?: string;
|
||||
} | string;
|
||||
signerKey?:
|
||||
| {
|
||||
keyFile: string;
|
||||
passphrase?: string;
|
||||
}
|
||||
| string;
|
||||
}
|
||||
|
||||
export interface FactoryOptions {
|
||||
@@ -29,7 +31,7 @@ export interface BundleUnit {
|
||||
export interface PartitionedBundle {
|
||||
bundle: BundleUnit;
|
||||
l10nBundle: {
|
||||
[key: string]: BundleUnit
|
||||
[key: string]: BundleUnit;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,17 +51,24 @@ export interface PassInstance {
|
||||
// * 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 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(),
|
||||
@@ -88,28 +97,31 @@ export interface OverridesSupportedOptions {
|
||||
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");
|
||||
|
||||
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 */
|
||||
|
||||
@@ -130,7 +142,7 @@ interface PersonNameComponent {
|
||||
|
||||
const personNameComponents = Joi.object().keys({
|
||||
givenName: Joi.string().required(),
|
||||
familyName: Joi.string().required()
|
||||
familyName: Joi.string().required(),
|
||||
});
|
||||
|
||||
interface Seat {
|
||||
@@ -148,12 +160,12 @@ const seat = Joi.object().keys({
|
||||
seatNumber: Joi.string(),
|
||||
seatIdentifier: Joi.string(),
|
||||
seatType: Joi.string(),
|
||||
seatDescription: Joi.string()
|
||||
seatDescription: Joi.string(),
|
||||
});
|
||||
|
||||
const location = Joi.object().keys({
|
||||
latitude: Joi.number().required(),
|
||||
longitude: Joi.number().required()
|
||||
longitude: Joi.number().required(),
|
||||
});
|
||||
|
||||
interface Semantics {
|
||||
@@ -201,7 +213,15 @@ interface Semantics {
|
||||
venueEntrance?: string;
|
||||
venuePhoneNumber?: string;
|
||||
venueRoom?: string;
|
||||
eventType?: "PKEventTypeGeneric" | "PKEventTypeLivePerformance" | "PKEventTypeMovie" | "PKEventTypeSports" | "PKEventTypeConference" | "PKEventTypeConvention" | "PKEventTypeWorkshop" | "PKEventTypeSocialGathering";
|
||||
eventType?:
|
||||
| "PKEventTypeGeneric"
|
||||
| "PKEventTypeLivePerformance"
|
||||
| "PKEventTypeMovie"
|
||||
| "PKEventTypeSports"
|
||||
| "PKEventTypeConference"
|
||||
| "PKEventTypeConvention"
|
||||
| "PKEventTypeWorkshop"
|
||||
| "PKEventTypeSocialGathering";
|
||||
eventStartDate?: string;
|
||||
eventEndDate?: string;
|
||||
artistIDs?: string;
|
||||
@@ -270,7 +290,9 @@ const semantics = Joi.object().keys({
|
||||
venueEntrance: Joi.string(),
|
||||
venuePhoneNumber: Joi.string(),
|
||||
venueRoom: Joi.string(),
|
||||
eventType: Joi.string().regex(/(PKEventTypeGeneric|PKEventTypeLivePerformance|PKEventTypeMovie|PKEventTypeSports|PKEventTypeConference|PKEventTypeConvention|PKEventTypeWorkshop|PKEventTypeSocialGathering)/),
|
||||
eventType: Joi.string().regex(
|
||||
/(PKEventTypeGeneric|PKEventTypeLivePerformance|PKEventTypeMovie|PKEventTypeSports|PKEventTypeConference|PKEventTypeConvention|PKEventTypeWorkshop|PKEventTypeSocialGathering)/,
|
||||
),
|
||||
eventStartDate: Joi.string(),
|
||||
eventEndDate: Joi.string(),
|
||||
artistIDs: Joi.string(),
|
||||
@@ -287,7 +309,7 @@ const semantics = Joi.object().keys({
|
||||
awayTeamAbbreviation: Joi.string(),
|
||||
sportName: Joi.string(),
|
||||
// Store Card Passes
|
||||
balance: currencyAmount
|
||||
balance: currencyAmount,
|
||||
});
|
||||
|
||||
export interface ValidPassType {
|
||||
@@ -310,11 +332,16 @@ interface PassInterfacesProps {
|
||||
voided?: boolean;
|
||||
}
|
||||
|
||||
type AllPassProps = PassInterfacesProps & ValidPassType & OverridesSupportedOptions;
|
||||
type AllPassProps = PassInterfacesProps &
|
||||
ValidPassType &
|
||||
OverridesSupportedOptions;
|
||||
export type ValidPass = {
|
||||
[K in keyof AllPassProps]: AllPassProps[K];
|
||||
};
|
||||
export type PassColors = Pick<OverridesSupportedOptions, "backgroundColor" | "foregroundColor" | "labelColor">;
|
||||
export type PassColors = Pick<
|
||||
OverridesSupportedOptions,
|
||||
"backgroundColor" | "foregroundColor" | "labelColor"
|
||||
>;
|
||||
|
||||
export interface Barcode {
|
||||
altText?: string;
|
||||
@@ -323,13 +350,22 @@ export interface Barcode {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type BarcodeFormat = "PKBarcodeFormatQR" | "PKBarcodeFormatPDF417" | "PKBarcodeFormatAztec" | "PKBarcodeFormatCode128";
|
||||
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()
|
||||
format: Joi.string()
|
||||
.required()
|
||||
.regex(
|
||||
/(PKBarcodeFormatQR|PKBarcodeFormatPDF417|PKBarcodeFormatAztec|PKBarcodeFormatCode128)/,
|
||||
"barcodeType",
|
||||
),
|
||||
message: Joi.string().required(),
|
||||
});
|
||||
|
||||
export interface Field {
|
||||
@@ -350,30 +386,53 @@ export interface Field {
|
||||
}
|
||||
|
||||
const field = Joi.object().keys({
|
||||
attributedValue: Joi.alternatives(Joi.string().allow(""), Joi.number(), Joi.date().iso()),
|
||||
attributedValue: Joi.alternatives(
|
||||
Joi.string().allow(""),
|
||||
Joi.number(),
|
||||
Joi.date().iso(),
|
||||
),
|
||||
changeMessage: Joi.string(),
|
||||
dataDetectorType: Joi.array().items(Joi.string().regex(/(PKDataDetectorTypePhoneNumber|PKDataDetectorTypeLink|PKDataDetectorTypeAddress|PKDataDetectorTypeCalendarEvent)/, "dataDetectorType")),
|
||||
dataDetectorType: 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"),
|
||||
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(),
|
||||
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"),
|
||||
dateStyle: Joi.string().regex(
|
||||
/(PKDateStyleNone|PKDateStyleShort|PKDateStyleMedium|PKDateStyleLong|PKDateStyleFull)/,
|
||||
"date style",
|
||||
),
|
||||
ignoreTimeZone: Joi.boolean(),
|
||||
isRelative: Joi.boolean(),
|
||||
timeStyle: Joi.string().regex(/(PKDateStyleNone|PKDateStyleShort|PKDateStyleMedium|PKDateStyleLong|PKDateStyleFull)/, "date style"),
|
||||
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()
|
||||
}),
|
||||
currencyCode: Joi.string().when("value", {
|
||||
is: Joi.number(),
|
||||
otherwise: Joi.string().forbidden(),
|
||||
}),
|
||||
numberStyle: Joi.string()
|
||||
.regex(/(PKNumberStyleDecimal|PKNumberStylePercent|PKNumberStyleScientific|PKNumberStyleSpellOut)/)
|
||||
.regex(
|
||||
/(PKNumberStyleDecimal|PKNumberStylePercent|PKNumberStyleScientific|PKNumberStyleSpellOut)/,
|
||||
)
|
||||
.when("value", {
|
||||
is: Joi.number(),
|
||||
otherwise: Joi.string().forbidden()
|
||||
otherwise: Joi.string().forbidden(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -385,10 +444,14 @@ export interface Beacon {
|
||||
}
|
||||
|
||||
const beaconsDict = Joi.object().keys({
|
||||
major: Joi.number().integer().positive().max(65535).greater(Joi.ref("minor")),
|
||||
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()
|
||||
relevantText: Joi.string(),
|
||||
});
|
||||
|
||||
export interface Location {
|
||||
@@ -402,7 +465,7 @@ const locationsDict = Joi.object().keys({
|
||||
altitude: Joi.number(),
|
||||
latitude: Joi.number().required(),
|
||||
longitude: Joi.number().required(),
|
||||
relevantText: Joi.string()
|
||||
relevantText: Joi.string(),
|
||||
});
|
||||
|
||||
export interface PassFields {
|
||||
@@ -414,18 +477,29 @@ export interface PassFields {
|
||||
}
|
||||
|
||||
const passDict = Joi.object().keys({
|
||||
auxiliaryFields: Joi.array().items(Joi.object().keys({
|
||||
row: Joi.number().max(1).min(0)
|
||||
}).concat(field)),
|
||||
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)
|
||||
secondaryFields: Joi.array().items(field),
|
||||
});
|
||||
|
||||
export type TransitType = "PKTransitTypeAir" | "PKTransitTypeBoat" | "PKTransitTypeBus" | "PKTransitTypeGeneric" | "PKTransitTypeTrain";
|
||||
export type TransitType =
|
||||
| "PKTransitTypeAir"
|
||||
| "PKTransitTypeBoat"
|
||||
| "PKTransitTypeBus"
|
||||
| "PKTransitTypeGeneric"
|
||||
| "PKTransitTypeTrain";
|
||||
|
||||
const transitType = Joi.string().regex(/(PKTransitTypeAir|PKTransitTypeBoat|PKTransitTypeBus|PKTransitTypeGeneric|PKTransitTypeTrain)/);
|
||||
const transitType = Joi.string().regex(
|
||||
/(PKTransitTypeAir|PKTransitTypeBoat|PKTransitTypeBus|PKTransitTypeGeneric|PKTransitTypeTrain)/,
|
||||
);
|
||||
|
||||
export interface NFC {
|
||||
message: string;
|
||||
@@ -434,7 +508,7 @@ export interface NFC {
|
||||
|
||||
const nfcDict = Joi.object().keys({
|
||||
message: Joi.string().required().max(64),
|
||||
encryptionPublicKey: Joi.string()
|
||||
encryptionPublicKey: Joi.string(),
|
||||
});
|
||||
|
||||
// ************************************* //
|
||||
@@ -447,11 +521,20 @@ export interface Personalization {
|
||||
termsAndConditions?: string;
|
||||
}
|
||||
|
||||
type PRSField = "PKPassPersonalizationFieldName" | "PKPassPersonalizationFieldPostalCode" | "PKPassPersonalizationFieldEmailAddress" | "PKPassPersonalizationFieldPhoneNumber";
|
||||
type PRSField =
|
||||
| "PKPassPersonalizationFieldName"
|
||||
| "PKPassPersonalizationFieldPostalCode"
|
||||
| "PKPassPersonalizationFieldEmailAddress"
|
||||
| "PKPassPersonalizationFieldPhoneNumber";
|
||||
|
||||
const personalizationDict = Joi.object().keys({
|
||||
requiredPersonalizationFields: Joi.array()
|
||||
.items("PKPassPersonalizationFieldName", "PKPassPersonalizationFieldPostalCode", "PKPassPersonalizationFieldEmailAddress", "PKPassPersonalizationFieldPhoneNumber")
|
||||
.items(
|
||||
"PKPassPersonalizationFieldName",
|
||||
"PKPassPersonalizationFieldPostalCode",
|
||||
"PKPassPersonalizationFieldEmailAddress",
|
||||
"PKPassPersonalizationFieldPhoneNumber",
|
||||
)
|
||||
.required(),
|
||||
description: Joi.string().required(),
|
||||
termsAndConditions: Joi.string(),
|
||||
@@ -470,7 +553,7 @@ const schemas = {
|
||||
transitType,
|
||||
nfcDict,
|
||||
supportedOptions,
|
||||
personalizationDict
|
||||
personalizationDict,
|
||||
};
|
||||
|
||||
export type Schema = keyof typeof schemas;
|
||||
@@ -491,14 +574,18 @@ export function isValid(opts: any, schemaName: Schema): boolean {
|
||||
const resolvedSchema = resolveSchemaName(schemaName);
|
||||
|
||||
if (!resolvedSchema) {
|
||||
schemaDebug(`validation failed due to missing or mispelled schema name`);
|
||||
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}`);
|
||||
schemaDebug(
|
||||
`validation failed due to error: ${validation.error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
return !validation.error;
|
||||
@@ -511,18 +598,25 @@ export function isValid(opts: any, schemaName: Schema): boolean {
|
||||
* @returns {object} the filtered value or empty object
|
||||
*/
|
||||
|
||||
export function getValidated<T extends Object>(opts: any, schemaName: Schema): T | null {
|
||||
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`);
|
||||
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}`);
|
||||
schemaDebug(
|
||||
`Validation failed in getValidated due to error: ${validation.error.message}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
89
src/utils.ts
89
src/utils.ts
@@ -16,13 +16,15 @@ export function isValidRGB(value?: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rgb = value.match(/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/);
|
||||
const rgb = value.match(
|
||||
/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/,
|
||||
);
|
||||
|
||||
if (!rgb) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return rgb.slice(1, 4).every(v => Math.abs(Number(v)) <= 255);
|
||||
return rgb.slice(1, 4).every((v) => Math.abs(Number(v)) <= 255);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +59,7 @@ export function dateToW3CString(date: Date) {
|
||||
*/
|
||||
|
||||
export function removeHidden(from: Array<string>): Array<string> {
|
||||
return from.filter(e => e.charAt(0) !== ".");
|
||||
return from.filter((e) => e.charAt(0) !== ".");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,8 +79,9 @@ export function generateStringFile(lang: { [index: string]: string }): Buffer {
|
||||
// Pass.strings format is the following one for each row:
|
||||
// "key" = "value";
|
||||
|
||||
const strings = Object.keys(lang)
|
||||
.map(key => `"${key}" = "${lang[key].replace(/"/g, '\"')}";`);
|
||||
const strings = Object.keys(lang).map(
|
||||
(key) => `"${key}" = "${lang[key].replace(/"/g, '"')}";`,
|
||||
);
|
||||
|
||||
return Buffer.from(strings.join(EOL), "utf8");
|
||||
}
|
||||
@@ -89,47 +92,73 @@ export function generateStringFile(lang: { [index: string]: string }): Buffer {
|
||||
* @param origin
|
||||
*/
|
||||
|
||||
type PartitionedBundleElements = [PartitionedBundle["l10nBundle"], PartitionedBundle["bundle"]];
|
||||
type PartitionedBundleElements = [
|
||||
PartitionedBundle["l10nBundle"],
|
||||
PartitionedBundle["bundle"],
|
||||
];
|
||||
|
||||
export function splitBufferBundle(origin: BundleUnit): PartitionedBundleElements {
|
||||
export function splitBufferBundle(
|
||||
origin: BundleUnit,
|
||||
): PartitionedBundleElements {
|
||||
const initialValue: PartitionedBundleElements = [{}, {}];
|
||||
|
||||
if (!origin) {
|
||||
return initialValue;
|
||||
}
|
||||
|
||||
return Object.entries(origin).reduce<PartitionedBundleElements>(([l10n, bundle], [key, buffer]) => {
|
||||
if (!key.includes(".lproj")) {
|
||||
return [
|
||||
l10n,
|
||||
{
|
||||
...bundle,
|
||||
[key]: buffer
|
||||
}
|
||||
];
|
||||
}
|
||||
return Object.entries(origin).reduce<PartitionedBundleElements>(
|
||||
([l10n, bundle], [key, buffer]) => {
|
||||
if (!key.includes(".lproj")) {
|
||||
return [
|
||||
l10n,
|
||||
{
|
||||
...bundle,
|
||||
[key]: buffer,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const pathComponents = key.split(sep);
|
||||
const lang = pathComponents[0];
|
||||
const file = pathComponents.slice(1).join("/");
|
||||
const pathComponents = key.split(sep);
|
||||
const lang = pathComponents[0];
|
||||
const file = pathComponents.slice(1).join("/");
|
||||
|
||||
(l10n[lang] || (l10n[lang] = {}))[file] = buffer;
|
||||
(l10n[lang] || (l10n[lang] = {}))[file] = buffer;
|
||||
|
||||
return [l10n, bundle];
|
||||
}, initialValue);
|
||||
return [l10n, bundle];
|
||||
},
|
||||
initialValue,
|
||||
);
|
||||
}
|
||||
|
||||
type StringSearchMode = "includes" | "startsWith" | "endsWith";
|
||||
|
||||
export function getAllFilesWithName(name: string, source: string[], mode: StringSearchMode = "includes", forceLowerCase: boolean = false): string[] {
|
||||
return source.filter(file => (forceLowerCase && file.toLowerCase() || file)[mode](name));
|
||||
export function getAllFilesWithName(
|
||||
name: string,
|
||||
source: string[],
|
||||
mode: StringSearchMode = "includes",
|
||||
forceLowerCase: boolean = false,
|
||||
): string[] {
|
||||
return source.filter((file) =>
|
||||
((forceLowerCase && file.toLowerCase()) || file)[mode](name),
|
||||
);
|
||||
}
|
||||
|
||||
export function hasFilesWithName(name: string, source: string[], mode: StringSearchMode = "includes", forceLowerCase: boolean = false): boolean {
|
||||
return source.some(file => (forceLowerCase && file.toLowerCase() || file)[mode](name));
|
||||
export function hasFilesWithName(
|
||||
name: string,
|
||||
source: string[],
|
||||
mode: StringSearchMode = "includes",
|
||||
forceLowerCase: boolean = false,
|
||||
): boolean {
|
||||
return source.some((file) =>
|
||||
((forceLowerCase && file.toLowerCase()) || file)[mode](name),
|
||||
);
|
||||
}
|
||||
|
||||
export function deletePersonalization(source: BundleUnit, logosNames: string[] = []): void {
|
||||
[...logosNames, "personalization.json"]
|
||||
.forEach(file => delete source[file]);
|
||||
export function deletePersonalization(
|
||||
source: BundleUnit,
|
||||
logosNames: string[] = [],
|
||||
): void {
|
||||
[...logosNames, "personalization.json"].forEach(
|
||||
(file) => delete source[file],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user