Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

passkit-generator

Package Overview
Dependencies
Maintainers
1
Versions
53
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

passkit-generator - npm Package Compare versions

Comparing version 1.3.1 to 1.3.2

examples/README.md

26

API.md

@@ -12,3 +12,3 @@ # API Reference

* Every schema the properties will be checked against, is built to reflect Apple's specifications, that can be found on Apple Developer Portal, at [PassKit Package Format Reference](https://apple.co/2MUHsm0).
* Properties will be checked against schemas, which are built to reflect Apple's specifications, that can be found on Apple Developer Portal, at [PassKit Package Format Reference](https://apple.co/2MUHsm0).

@@ -73,6 +73,6 @@ * Here below are listed all the available methods that library will let you use.

| Key | Type | Description | Optional | Default Value |
| Key | Type | Description | Optional | Default Value |
|-----|------|---------------|:-------------:|:-----------:|
| options | Object | The options to create the pass | false | -
| options.model | String/Path | The model path to be used to generate a new model. | false | -
| options.model | String/Path | The model path to be used to generate a new model. | false | -
| options.certificates | Object | The certificate object containing the paths to certs files. | false | -

@@ -181,5 +181,5 @@ | options.certificates.wwdr | String/Path | The path to Apple WWDR certificate. | false | -

pass.barcode("11424771526");
// or
pass.barcode({

@@ -190,5 +190,5 @@ message: "11424771526",

});
// or
pass.barcode([{

@@ -225,4 +225,4 @@ message: "11424771526",

Will let you choose the format to be used in barcode property as backward compatibility.
Also will only work if `data` is provided to `barcode()` method and will fail if the selected format is not found among barcodes dictionaries array.
It will let you choose the format to be used in barcode property as backward compatibility.
Also it will work only if `data` is provided to `barcode()` method and will fail if the selected format is not found among barcodes dictionaries array.

@@ -264,3 +264,3 @@ **Arguments**:

Will generate all the barcodes fallback starting from the first dictionary in `barcodes`.
It will generate all the barcodes fallback starting from the first dictionary in `barcodes`.

@@ -318,3 +318,3 @@ <br><br>

Sets directly the pass as voided (void: true).
It sets directly the pass as voided (void: true).

@@ -341,3 +341,3 @@ <br><br>

It sets the relevance key in the pass among four: **beacons**, **locations**, **relevantDate** and **maxDistance**.
It sets the relevance key in the pass among four: **beacons**, **locations**, **relevantDate** and **maxDistance**.
See [Apple Documentation dedicated page](https://apple.co/2QiE9Ds) for more.

@@ -356,3 +356,3 @@

| value | String \| Number \| Array\<Object> | Type depends on the key. Please refer to the description above for more details | false | -
| relevanceDateFormat | String | Custom date format. Will be only used when using `relevanceDate` key | true | undefined
| relevanceDateFormat | String | Custom date format. Will be only used when using `relevanceDate` key | true | undefined

@@ -359,0 +359,0 @@ **Example**:

@@ -71,4 +71,4 @@ /**

console.log("Barcode property is now:", pass.props["barcode"]);
console.log("Barcodes support is autocompleted:", pass.props["barcodes"]);
console.log("Barcode property is now:", pass._props["barcode"]);
console.log("Barcodes support is autocompleted:", pass._props["barcodes"]);

@@ -75,0 +75,0 @@ pass.generate().then(function(stream) {

@@ -1,743 +0,1 @@

const fs = require("fs");
const path = require("path");
const util = require("util");
const stream = require("stream");
const moment = require("moment");
const forge = require("node-forge");
const archiver = require("archiver");
const barcodeDebug = require("debug")("passkit:barcode");
const genericDebug = require("debug")("passkit:generic");
const schema = require("./schema");
const { areas: fieldsName, FieldsContainer } = require("./fields");
const errors = require("./messages");
const readdir = util.promisify(fs.readdir);
const readFile = util.promisify(fs.readFile);
class Pass {
constructor(options) {
this.options = options;
this.Certificates = {};
this.model = "";
this.l10n = {};
this._props = {};
this.shouldOverwrite = !(this.options.hasOwnProperty("shouldOverwrite") && !this.options.shouldOverwrite);
fieldsName.forEach(a => this[a] = new FieldsContainer());
this._transitType = "";
}
/**
* Generates the pass Stream
*
* @async
* @method generate
* @return {Promise<Stream>} A Promise containing the stream of the generated pass.
*/
generate() {
let archive = archiver("zip");
return this._parseSettings(this.options)
.then(() => readdir(this.model))
.catch((err) => {
// May have not used this catch but ENOENT error is not enough self-explanatory in the case of external usage
if (err.code && err.code === "ENOENT") {
throw new Error(errors.MODEL_NOT_FOUND.replace("%s", (this.model ? this.model+" " : "")));
}
throw new Error(err);
})
.then(files => {
// list without dynamic components like manifest, signature or pass files (will be added later in the flow) and hidden files.
let noDynList = removeHidden(files).filter(f => !/(manifest|signature|pass)/i.test(f));
if (!noDynList.length || !noDynList.some(f => f.toLowerCase().includes("icon"))) {
throw new Error(errors.UNINITIALIZED.replace("%s", path.parse(this.model).name));
}
// list without localization files (they will be added later in the flow)
let bundle = noDynList.filter(f => !f.includes(".lproj"));
// Localization folders only
const L10N = noDynList.filter(f => f.includes(".lproj") && Object.keys(this.l10n).includes(path.parse(f).name));
/**
* Reads pass.json file and apply patches on it
* @function
* @name passExtractor
* @return {Promise<Buffer>} The patched pass.json buffer
*/
let passExtractor = (() => {
return readFile(path.resolve(this.model, "pass.json"))
.then(passStructBuffer => {
if (!this._validateType(passStructBuffer)) {
throw new Error(errors.VALIDATION_FAILED)
}
bundle.push("pass.json");
return this._patch(passStructBuffer);
});
});
/*
* Reading all the localization selected folders and removing hidden files (the ones that starts with ".")
* from the list. Returning a Promise containing all those files
*/
return Promise.all(L10N.map(f => readdir(path.join(this.model, f)).then(removeHidden)))
.then(listByFolder => {
/* Each file name is joined with its own path and pushed to the bundle files array. */
listByFolder.forEach((folder, index) => bundle.push(...folder.map(f => path.join(L10N[index], f))));
/* Getting all bundle file buffers, pass.json included, and appending */
let bundleBuffers = bundle.map(f => readFile(path.resolve(this.model, f)));
let passBuffer = passExtractor();
return Promise.all([...bundleBuffers, passBuffer])
.then(buffers => {
Object.keys(this.l10n).forEach(l => {
const strings = generateStringFile(this.l10n[l]);
/*
* if .string file buffer is empty, no translations were added
* but still wanted to include the language
*/
if (strings.length) {
buffers.push(strings);
bundle.push(path.join(`${l}.lproj`, `pass.strings`));
}
});
return [buffers, bundle];
});
});
})
.then(([buffers, bundle]) => {
/*
* Parsing the buffers, pushing them into the archive
* and returning the compiled manifest
*/
return buffers.reduce((acc, current, index) => {
let filename = bundle[index];
let hashFlow = forge.md.sha1.create();
hashFlow.update(current.toString("binary"));
archive.append(current, { name: filename });
acc[filename] = hashFlow.digest().toHex();
return acc;
}, {});
})
.then((manifest) => {
let signatureBuffer = this._sign(manifest);
archive.append(signatureBuffer, { name: "signature" });
archive.append(JSON.stringify(manifest), { name: "manifest.json" });
let passStream = new stream.PassThrough();
archive.pipe(passStream);
return archive.finalize().then(() => passStream);
});
}
/**
* Adds traslated strings object to the list of translation to be inserted into the pass
*
* @method localize
* @params {String} lang - the ISO 3166 alpha-2 code for the language
* @params {Object} translations - key/value pairs where key is the
* string appearing in pass.json and value the translated string
* @returns {this}
*
* @see https://apple.co/2KOv0OW - Passes support localization
*/
localize(lang, translations) {
if (lang && typeof lang === "string" && (typeof translations === "object" || translations === undefined)) {
this.l10n[lang] = translations || {};
}
return this;
}
/**
* Sets expirationDate property to the W3C date
*
* @method expiration
* @params {String} date - the date in string
* @params {String} format - a custom format for the date
* @returns {this}
*/
expiration(date, format) {
if (typeof date !== "string") {
return this;
}
let dateParse = dateToW3CString(date, format);
if (!dateParse) {
genericDebug("Expiration Date was not set due to invalid format.");
} else {
this._props.expirationDate = dateParse;
}
return this;
}
/**
* Sets voided property to true
*
* @method void
* @return {this}
*/
void() {
this._props.voided = true;
return this;
}
/**
* Checks and sets data for "beacons", "locations", "maxDistance" and "relevantDate" keys
*
* @method relevance
* @params {String} type - one of the key above
* @params {Any[]} data - the data to be pushed to the property
* @params {String} [relevanceDateFormat] - A custom format for the date
* @return {Number} The quantity of data pushed
*/
relevance(type, data, relevanceDateFormat) {
let types = ["beacons", "locations", "maxDistance", "relevantDate"];
if (!type || !data || !types.includes(type)) {
return Object.assign({
length: 0
}, this);
}
if (type === "beacons" || type === "locations") {
if (!(data instanceof Array)) {
data = [data];
}
let valid = data.filter(d => schema.isValid(d, type+"Dict"));
this._props[type] = valid.length ? valid : undefined;
return Object.assign({
length: valid.length
}, this);
}
if (type === "maxDistance" && (typeof data === "string" || typeof data === "number")) {
let conv = Number(data);
// condition to proceed
let cond = isNaN(conv);
if (!cond) {
this._props[type] = conv;
}
return Object.assign({
length: Number(!cond)
}, this);
} else if (type === "relevantDate") {
let dateParse = dateToW3CString(data, relevanceDateFormat);
if (!dateParse) {
genericDebug("Relevant Date was not set due to incorrect date format.");
} else {
this._props[type] = dateParse;
}
return Object.assign({
length: Number(!!dateParse)
}, this);
}
}
/**
* Adds barcodes to "barcode" and "barcodes" properties.
* It will let later to add the missing versions
*
* @method barcode
* @params {Object|String} data - the data to be added
* @return {this} Improved this with length property and other methods
*/
barcode(data) {
if (!data) {
return Object.assign({
length: 0,
autocomplete: () => {},
backward: () => {}
}, this);
}
if (typeof data === "string" || (data instanceof Object && !data.format && data.message)) {
let autogen = this.__barcodeAutogen(data instanceof Object ? data : { message: data });
this._props["barcode"] = autogen[0] || {};
this._props["barcodes"] = autogen || [];
return Object.assign({
length: 4,
autocomplete: () => {},
backward: this.__barcodeChooseBackward.bind(this)
}, this);
}
if (!(data instanceof Array)) {
data = [data];
}
// messageEncoding is required but has a default value.
// Therefore I assign a validated version of the object with the default value
// to the ones that doesn't have messageEncoding.
// if o is not a valid object, false is returned and then filtered later
let valid = data
.map(o => schema.getValidated(o, "barcode"))
.filter(o => o instanceof Object);
if (valid.length) {
this._props["barcode"] = valid[0];
this._props["barcodes"] = valid;
}
// I bind "this" to get a clean context (without these two methods) when returning from the methods
return Object.assign({
length: valid.length,
autocomplete: this.__barcodeAutocomplete.bind(this),
backward: this.__barcodeChooseBackward.bind(this)
}, this);
}
/**
* Automatically generates barcodes for all the types given common info
*
* @method __barcodeAutogen
* @params {Object} data - common info, may be object or the message itself
* @params {String} data.message - the content to be placed inside "message" field
* @params {String} [data.altText=data.message] - alternativeText, is message content if not overwritten
* @params {String} [data.messageEncoding=iso-8859-1] - the encoding
* @return {Object[]} Object array barcodeDict compliant
*/
__barcodeAutogen(data) {
if (!data || !(data instanceof Object) || !data.message) {
barcodeDebug("Unable to autogenerate barcodes. Data is not an object or has not message field.");
return [];
}
let types = ["PKBarcodeFormatQR", "PKBarcodeFormatPDF417", "PKBarcodeFormatAztec", "PKBarcodeFormatCode128"];
data.altText = data.altText || data.message;
data.messageEncoding = data.messageEncoding || "iso-8859-1";
delete data.format;
return types.map(T => Object.assign({ format: T }, data));
}
/**
* Given an already compiled props["barcodes"] with missing objects
* (less than 4), takes infos from the first object and replicate them
* in the missing structures.
*
* @method __barcodeAutocomplete
* @returns {this} Improved this, with length property and retroCompatibility method.
*/
__barcodeAutocomplete() {
let props = this._props["barcodes"];
if (props.length === 4 || !props.length) {
return Object.assign({
length: 0,
backward: this.__barcodeChooseBackward.bind(this)
}, this);
}
this._props["barcodes"] = this.__barcodeAutogen(props[0]);
return Object.assign({
length: 4 - props.length,
backward: this.__barcodeChooseBackward.bind(this)
}, this);
}
/**
* Given an index <= the amount of already set "barcodes",
* this let you choose which structure to use for retrocompatibility
* property "barcode".
*
* @method __barcodeChooseBackward
* @params {String} format - the format, or part of it, to be used
* @return {this}
*/
__barcodeChooseBackward(format) {
if (format === null) {
this._props["barcode"] = undefined;
return this;
}
if (typeof format !== "string") {
barcodeDebug("format must be a string or null. Cannot set backward compatibility.");
return this;
}
// Checking which object among barcodes has the same format of the specified one.
let index = this._props["barcodes"].findIndex(b => b.format.toLowerCase().includes(format.toLowerCase()));
if (index === -1) {
barcodeDebug("format not found among barcodes. Cannot set backward compatibility.");
return this;
}
this._props["barcode"] = this._props["barcodes"][index];
return this;
}
/**
* Sets nfc fields in properties
*
* @method nfc
* @params {Array<Object>} data - the data to be pushed in the pass
* @returns {this}
*/
nfc(...data) {
if (data.length === 1 && data[0] instanceof Array) {
data = data[0];
}
let valid = data.filter(d => d instanceof Object && schema.isValid(d, "nfcDict"));
if (valid.length) {
this._props["nfc"] = valid;
}
return this;
}
/**
* Checks if pass model type is one of the supported ones
*
* @method _validateType
* @params {Buffer} passBuffer - buffer of the pass structure content
* @returns {Boolean} true if type is supported, false otherwise.
*/
_validateType(passBuffer) {
let passTypes = ["boardingPass", "eventTicket", "coupon", "generic", "storeCard"];
let passFile = JSON.parse(passBuffer.toString("utf8"));
let index = passTypes.findIndex(passType => passFile.hasOwnProperty(passType));
if (index == -1) {
return false;
}
let type = passTypes[index];
this.type = type;
return schema.isValid(passFile[type], "passDict");
}
/**
* Generates the PKCS #7 cryptografic signature for the manifest file.
*
* @method _sign
* @params {String|Object} manifest - Manifest content.
* @returns {Buffer}
*/
_sign(manifest) {
let signature = forge.pkcs7.createSignedData();
if (typeof manifest === "object") {
signature.content = forge.util.createBuffer(JSON.stringify(manifest), "utf8");
} else if (typeof manifest === "string") {
signature.content = manifest;
} else {
throw new Error(errors.MANIFEST_TYPE.replace("%s", typeof manifest));
}
signature.addCertificate(this.Certificates.wwdr);
signature.addCertificate(this.Certificates.signerCert);
signature.addSigner({
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,
}, {
// the value is autogenerated
type: forge.pki.oids.signingTime,
}]
});
signature.sign();
/*
* Signing creates in contentInfo a JSON object nested BER/TLV (X.690 standard) structure.
* Each object represents a component of ASN.1 (Abstract Syntax Notation)
* For a more complete reference, refer to: https://en.wikipedia.org/wiki/X.690#BER_encoding
*
* signature.contentInfo.type => SEQUENCE OF (16)
* signature.contentInfo.value[0].type => OBJECT IDENTIFIER (6)
* signature.contantInfo.value[1].type => END OF CONTENT (EOC - 0)
*
* EOC are only present only in constructed indefinite-length methods
* Since `signature.contentInfo.value[1].value` contains an object whose value contains the content we passed,
* we have to pop the whole object away to avoid signature content invalidation.
*
*/
signature.contentInfo.value.pop();
// Converting the JSON Structure into a DER (which is a subset of BER), ASN.1 valid structure
// Returning the buffer of the signature
return Buffer.from(forge.asn1.toDer(signature.toAsn1()).getBytes(), "binary");
}
/**
* Edits the buffer of pass.json based on the passed options.
*
* @method _patch
* @params {Buffer} passBuffer - Buffer of the contents of pass.json
* @returns {Promise<Buffer>} Edited pass.json buffer or Object containing error.
*/
_patch(passBuffer) {
let passFile = JSON.parse(passBuffer.toString("utf8"));
if (Object.keys(this._props).length) {
const rgbValues = ["backgroundColor", "foregroundColor", "labelColor"];
rgbValues.filter(v => this._props[v] && !isValidRGB(this._props[v])).forEach(v => delete this._props[v]);
if (this.shouldOverwrite) {
Object.assign(passFile, this._props);
} else {
Object.keys(this._props).forEach(prop => {
if (passFile[prop]) {
if (passFile[prop] instanceof Array) {
passFile[prop].push(...this._props[prop]);
} else if (passFile[prop] instanceof Object) {
Object.assign(passFile[prop], this._props[prop]);
}
} else {
passFile[prop] = this._props[prop];
}
});
}
}
fieldsName.forEach(area => {
if (this[area].fields.length) {
if (this.shouldOverwrite) {
passFile[this.type][area] = this[area].fields;
} else {
passFile[this.type][area].push(...this[area].fields);
}
}
});
if (!this.transitType && this.type === "boardingPass") {
throw new Error("Cannot proceed with pass creation. transitType field is required for boardingPasses.");
}
passFile[this.type]["transitType"] = this.transitType;
return Buffer.from(JSON.stringify(passFile));
}
/**
* Validates the contents of the passed options and assigns the contents to the right properties
*
* @async
* @method _parseSettings
* @params {Object} options - the options passed to be parsed
* @returns {Promise}
*/
_parseSettings(options) {
if (!schema.isValid(options, "instance")) {
throw new Error(errors.REQS_NOT_MET);
}
if (!options.model || typeof options.model !== "string") {
throw new Error(errors.MODEL_NOT_STRING);
}
this.model = path.resolve(options.model) + (!!options.model && !path.extname(options.model) ? ".pass" : "");
const filteredOpts = schema.filter(options.overrides, "supportedOptions");
Object.assign(this._props, filteredOpts);
let optCertsNames = Object.keys(options.certificates);
let certPaths = optCertsNames.map((val) => {
const cert = options.certificates[val];
const filePath = !(cert instanceof Object) ? cert : cert["keyFile"];
const resolvedPath = path.resolve(filePath);
return readFile(resolvedPath);
});
return Promise.all(certPaths)
.then(contents => {
contents.forEach((file, index) => {
let certName = optCertsNames[index];
let pem = parsePEM(file, options.certificates[certName].passphrase);
if (!pem) {
throw new Error(errors.INVALID_CERTS.replace("%s", optCertsNames[index]));
}
this.Certificates[certName] = pem;
});
}).catch(err => {
if (!err.path) {
throw err;
} else {
throw new Error(errors.INVALID_CERT_PATH.replace("%s", path.parse(err.path).base));
}
});
}
set transitType(v) {
if (schema.isValid(v, "transitType")) {
this._transitType = v;
} else {
this._transitType = this._transitType || "";
}
}
get transitType() {
return this._transitType;
}
}
/**
* Parses the PEM-formatted passed text (certificates)
*
* @function parsePEM
* @params {String} element - Text content of .pem files
* @params {String=} passphrase - passphrase for the key
* @returns {Object} The parsed certificate or key in node forge format
*/
function parsePEM(element, passphrase) {
if (element.includes("PRIVATE KEY") && passphrase) {
return forge.pki.decryptRsaPrivateKey(element, String(passphrase));
} else if (element.includes("CERTIFICATE")) {
return forge.pki.certificateFromPem(element);
} else {
return null;
}
}
/**
* Checks if an rgb value is compliant with CSS-like syntax
*
* @function isValidRGB
* @params {String} value - string to analyze
* @returns {Boolean} True if valid rgb, false otherwise
*/
function isValidRGB(value) {
if (!value || typeof value !== "string") {
return false;
}
let 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);
}
/**
* Converts a date to W3C Standard format
*
* @function dateToW3Cstring
* @params {String} date - The date to be parsed
* @params {String} [format] - a custom format
* @returns {String|undefined} The parsed string if the parameter is valid,
* undefined otherwise
*/
function dateToW3CString(date, format) {
if (typeof date !== "string") {
return "";
}
let parsedDate = moment(date.replace(/\//g, "-"), format || ["MM-DD-YYYY hh:mm:ss", "DD-MM-YYYY hh:mm:ss"]).format();
if (parsedDate === "Invalid date") {
return undefined;
}
return parsedDate;
}
/**
* Apply a filter to arg0 to remove hidden files names (starting with dot)
*
* @function removeHidden
* @params {String[]} from - list of file names
* @return {String[]}
*/
function removeHidden(from) {
return from.filter(e => e.charAt(0) !== ".");
}
/**
* Creates a buffer of translations in Apple .strings format
*
* @function generateStringFile
* @params {Object} lang - structure containing related to ISO 3166 alpha-2 code for the language
* @returns {Buffer} Buffer to be written in pass.strings for language in lang
* @see https://apple.co/2M9LWVu - String Resources
*/
function generateStringFile(lang) {
if (!Object.keys(lang).length) {
return Buffer.from("", "utf8");
}
// Pass.strings format is the following one for each row:
// "key" = "value";
let strings = Object.keys(lang)
.map(key => `"${key}" = "${lang[key].replace(/"/g, /\\"/)}";`);
return Buffer.from(strings.join("\n"), "utf8");
}
module.exports = { Pass };
module.exports = require("./src/pass");
{
"name": "passkit-generator",
"version": "1.3.1",
"description": "The easiest way to generate custom Apple Wallet passes in Node.js",
"main": "index.js",
"scripts": {
"test": "jasmine tests/index.js"
},
"author": "Alexander Patrick Cerutti",
"license": "MIT",
"repository": "https://github.com/alexandercerutti/passkit-generator",
"bugs": "https://github.com/alexandercerutti/passkit-generator/issues",
"keywords": [
"Apple",
"Passkit",
"Wallet",
"Pass"
],
"dependencies": {
"archiver": "^2.1.1",
"debug": "^3.1.0",
"joi": "^13.6.0",
"moment": "^2.22.2",
"node-forge": "^0.7.6"
},
"devDependencies": {
"jasmine": "^3.2.0"
}
"name": "passkit-generator",
"version": "1.3.2",
"description": "The easiest way to generate custom Apple Wallet passes in Node.js",
"main": "index.js",
"scripts": {
"test": "jasmine tests/index.js"
},
"author": "Alexander Patrick Cerutti",
"license": "MIT",
"repository": "https://github.com/alexandercerutti/passkit-generator",
"bugs": "https://github.com/alexandercerutti/passkit-generator/issues",
"keywords": [
"Apple",
"Passkit",
"Wallet",
"Pass"
],
"dependencies": {
"archiver": "^2.1.1",
"debug": "^3.1.0",
"joi": "^13.6.0",
"moment": "^2.22.2",
"node-forge": "^0.7.6"
},
"devDependencies": {
"express": "^4.16.3",
"jasmine": "^3.2.0"
}
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc