mirror of
https://github.com/marcogll/passkit-generator.git
synced 2026-03-15 14:25:17 +00:00
Improved security checks;
Added back the formatted messages and added new ones;
This commit is contained in:
183
src/factory.ts
183
src/factory.ts
@@ -13,14 +13,14 @@ const readFile = promisify(_readFile);
|
||||
|
||||
export async function createPass(options: FactoryOptions): Promise<Pass> {
|
||||
if (!(options && Object.keys(options).length)) {
|
||||
throw new Error("Unable to create Pass: no options were passed");
|
||||
throw new Error(formatMessage("CP_NO_OPTS"));
|
||||
}
|
||||
|
||||
try {
|
||||
const [bundle, certificates] = await Promise.all([
|
||||
getModelContents(options.model),
|
||||
readCertificatesFromOptions(options.certificates)
|
||||
]);
|
||||
readCertificatesFromOptions(options.certificates)
|
||||
]);
|
||||
|
||||
return new Pass({
|
||||
model: bundle,
|
||||
@@ -28,13 +28,23 @@ export async function createPass(options: FactoryOptions): Promise<Pass> {
|
||||
overrides: options.overrides
|
||||
});
|
||||
} catch (err) {
|
||||
// @TODO: analyze the error and stop the execution somehow
|
||||
console.log(err);
|
||||
throw new Error(formatMessage("CP_INIT_ERROR"));
|
||||
}
|
||||
}
|
||||
|
||||
async function getModelContents(model: FactoryOptions["model"]) {
|
||||
if (!(model && (typeof model === "string" || (typeof model === "object" && Object.keys(model).length)))) {
|
||||
throw new Error("Unable to create Pass: invalid model provided");
|
||||
const isModelValid = (
|
||||
model && (
|
||||
typeof model === "string" || (
|
||||
typeof model === "object" &&
|
||||
Object.keys(model).length
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (!isModelValid) {
|
||||
throw new Error(formatMessage("MODEL_NOT_VALID"));
|
||||
}
|
||||
|
||||
let modelContents: PartitionedBundle;
|
||||
@@ -61,71 +71,96 @@ async function getModelContents(model: FactoryOptions["model"]) {
|
||||
*/
|
||||
|
||||
async function getModelFolderContents(model: string): Promise<PartitionedBundle> {
|
||||
const modelPath = path.resolve(model) + (!!model && !path.extname(model) ? ".pass" : "");
|
||||
const modelFilesList = await readDir(modelPath);
|
||||
try {
|
||||
const modelPath = path.resolve(model) + (!!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));
|
||||
// No dot-starting files, manifest and signature
|
||||
const filteredFiles = removeHidden(modelFilesList).filter(f => !/(manifest|signature)/i.test(f));
|
||||
|
||||
// Icon is required to proceed
|
||||
if (!(filteredFiles.length && filteredFiles.some(file => file.toLowerCase().includes("icon")))) {
|
||||
const eMessage = formatMessage("MODEL_UNINITIALIZED", path.parse(this.model).name);
|
||||
throw new Error(eMessage);
|
||||
const isModelInitialized = (
|
||||
filteredFiles.length &&
|
||||
filteredFiles.some(file => file.toLowerCase().includes("icon"))
|
||||
);
|
||||
|
||||
// Icon is required to proceed
|
||||
if (!isModelInitialized) {
|
||||
throw new Error(formatMessage(
|
||||
"MODEL_UNINITIALIZED",
|
||||
path.parse(this.model).name
|
||||
));
|
||||
}
|
||||
|
||||
// Splitting files from localization folders
|
||||
const rawBundle = filteredFiles.filter(entry => !entry.includes(".lproj"));
|
||||
const l10nFolders = filteredFiles.filter(entry => entry.includes(".lproj"));
|
||||
|
||||
const bundleBuffers = rawBundle.map(file => readFile(path.resolve(modelPath, file)));
|
||||
const buffers = await Promise.all(bundleBuffers);
|
||||
|
||||
const bundle: BundleUnit = Object.assign({},
|
||||
...rawBundle.map((fileName, index) => ({ [fileName]: buffers[index] }))
|
||||
);
|
||||
|
||||
// Reading concurrently localizations folder
|
||||
// and their files and their buffers
|
||||
const L10N_FilesListByFolder: Array<BundleUnit> = await Promise.all(
|
||||
l10nFolders.map(folderPath => {
|
||||
// Reading current folder
|
||||
const currentLangPath = path.join(modelPath, folderPath);
|
||||
return readDir(currentLangPath)
|
||||
.then(files => {
|
||||
// Transforming files path to a model-relative path
|
||||
const validFiles = removeHidden(files)
|
||||
.map(file => path.join(currentLangPath, file));
|
||||
|
||||
// Getting all the buffers from file paths
|
||||
return Promise.all([
|
||||
...validFiles.map(file =>
|
||||
readFile(file).catch(() => Buffer.alloc(0))
|
||||
)
|
||||
]).then(buffers =>
|
||||
// Assigning each file path to its buffer
|
||||
// and discarding the empty ones
|
||||
validFiles.reduce<BundleUnit>((acc, file, index) => {
|
||||
if (!buffers[index].length) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return { ...acc, [file]: buffers[index] };
|
||||
}, {})
|
||||
);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign(
|
||||
{},
|
||||
...L10N_FilesListByFolder
|
||||
.map((folder, index) => ({ [l10nFolders[index]]: folder }))
|
||||
);
|
||||
|
||||
return {
|
||||
bundle,
|
||||
l10nBundle
|
||||
};
|
||||
} catch (err) {
|
||||
if (err.code && err.code === "ENOENT") {
|
||||
if (err.syscall === "open") {
|
||||
// file opening failed
|
||||
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 err;
|
||||
}
|
||||
|
||||
// Splitting files from localization folders
|
||||
const rawBundle = filteredFiles.filter(entry => !entry.includes(".lproj"));
|
||||
const l10nFolders = filteredFiles.filter(entry => entry.includes(".lproj"));
|
||||
|
||||
const bundleBuffers = rawBundle.map(file => readFile(path.resolve(modelPath, file)));
|
||||
const buffers = await Promise.all(bundleBuffers);
|
||||
|
||||
const bundle: BundleUnit = Object.assign({},
|
||||
...rawBundle.map((fileName, index) => ({ [fileName]: buffers[index] }))
|
||||
);
|
||||
|
||||
// Reading concurrently localizations folder
|
||||
// and their files and their buffers
|
||||
const L10N_FilesListByFolder: Array<BundleUnit> = await Promise.all(
|
||||
l10nFolders.map(folderPath => {
|
||||
// Reading current folder
|
||||
const currentLangPath = path.join(modelPath, folderPath);
|
||||
return readDir(currentLangPath)
|
||||
.then(files => {
|
||||
// Transforming files path to a model-relative path
|
||||
const validFiles = removeHidden(files)
|
||||
.map(file => path.join(currentLangPath, file));
|
||||
|
||||
// Getting all the buffers from file paths
|
||||
return Promise.all([
|
||||
...validFiles.map(file =>
|
||||
readFile(file).catch(() => Buffer.alloc(0))
|
||||
)
|
||||
]).then(buffers =>
|
||||
// Assigning each file path to its buffer
|
||||
// and discarding the empty ones
|
||||
validFiles.reduce<BundleUnit>((acc, file, index) => {
|
||||
if (!buffers[index].length) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return { ...acc, [file]: buffers[index] };
|
||||
}, {})
|
||||
);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign(
|
||||
{},
|
||||
...L10N_FilesListByFolder
|
||||
.map((folder, index) => ({ [l10nFolders[index]]: folder }))
|
||||
);
|
||||
|
||||
return {
|
||||
bundle,
|
||||
l10nBundle
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,8 +182,14 @@ function getModelBufferContents(model: BundleUnit): PartitionedBundle {
|
||||
|
||||
const bundleKeys = Object.keys(rawBundle);
|
||||
|
||||
if (!bundleKeys.length) {
|
||||
throw new Error("Cannot proceed with pass creation: bundle not initialized")
|
||||
const isModelInitialized = (
|
||||
bundleKeys.length &&
|
||||
bundleKeys.some(file => file.toLowerCase().includes("icon"))
|
||||
);
|
||||
|
||||
// Icon is required to proceed
|
||||
if (!isModelInitialized) {
|
||||
throw new Error(formatMessage("MODEL_UNINITIALIZED", "Buffers"))
|
||||
}
|
||||
|
||||
// separing localization folders
|
||||
@@ -179,7 +220,7 @@ function getModelBufferContents(model: BundleUnit): PartitionedBundle {
|
||||
|
||||
async function readCertificatesFromOptions(options: Certificates): Promise<FinalCertificates> {
|
||||
if (!(options && Object.keys(options).length && isValid(options, "certificatesSchema"))) {
|
||||
throw new Error("Unable to create Pass: certificates schema validation failed.");
|
||||
throw new Error(formatMessage("CP_NO_CERTS"));
|
||||
}
|
||||
|
||||
// if the signerKey is an object, we want to get
|
||||
|
||||
@@ -3,11 +3,15 @@ interface MessageGroup {
|
||||
}
|
||||
|
||||
const errors: MessageGroup = {
|
||||
CP_INIT_ERROR: "Something went really bad in the initialization, dude! Please look at the log above this message. It should contain all the infos about the problem.",
|
||||
CP_NO_OPTS: "Cannot initialize the pass 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.\nRefer to https://apple.co/2IhJr0Q, https://apple.co/2Nvshvn and documentation to fill the model correctly.",
|
||||
MODEL_NOT_STRING: "A string model name must be provided in order to continue.",
|
||||
MODEL_NOT_FOUND: "Model %s not found. Provide a valid one to continue.",
|
||||
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_CERT_PATH: "Invalid certificate loaded. %s does not exist.",
|
||||
TRSTYPE_REQUIRED: "Cannot proceed with pass creation. transitType field is required for boardingPasses.",
|
||||
|
||||
28
src/pass.ts
28
src/pass.ts
@@ -53,20 +53,38 @@ export class Pass implements PassIndexSignature {
|
||||
[transitType]: string = "";
|
||||
|
||||
constructor(options: schema.PassInstance) {
|
||||
if (!schema.isValid(options, "instance")) {
|
||||
throw new Error(formatMessage("REQUIR_VALID_FAILED"));
|
||||
}
|
||||
|
||||
this.Certificates = options.certificates;
|
||||
this.l10nBundles = options.model.l10nBundle;
|
||||
this.bundle = { ...options.model.bundle };
|
||||
|
||||
options.overrides = options.overrides || {};
|
||||
// Parsing the options and extracting only the valid ones.
|
||||
const validOvverrides = schema.getValidated(options.overrides || {}, "supportedOptions") as schema.OverridesSupportedOptions;
|
||||
|
||||
// getting pass.json
|
||||
this.passCore = JSON.parse(this.bundle["pass.json"].toString("utf8"));
|
||||
if (validOvverrides === null) {
|
||||
throw new Error(formatMessage("OVV_KEYS_BADFORMAT"))
|
||||
}
|
||||
|
||||
if (Object.keys(validOvverrides).length) {
|
||||
this._props = { ...validOvverrides };
|
||||
}
|
||||
|
||||
try {
|
||||
// getting pass.json
|
||||
this.passCore = JSON.parse(this.bundle["pass.json"].toString("utf8"));
|
||||
} catch (err) {
|
||||
throw new Error(formatMessage("PASSFILE_VALIDATION_FAILED"));
|
||||
}
|
||||
|
||||
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("Missing type in model");
|
||||
// @TODO: change error message to say it is invalid or missing
|
||||
throw new Error(formatMessage("NO_PASS_TYPE"));
|
||||
}
|
||||
|
||||
if (this.type === "boardingPass" && this.passCore[this.type]["transitType"]) {
|
||||
@@ -432,7 +450,7 @@ export class Pass implements PassIndexSignature {
|
||||
}
|
||||
|
||||
if (typeof format !== "string") {
|
||||
barcodeDebug(formatMessage("BRC_FORMAT_UNMATCH"));
|
||||
barcodeDebug(formatMessage("BRC_FORMATTYPE_UNMATCH"));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user