passkit-generator
Advanced tools
Comparing version 1.3.2 to 1.3.3
28
API.md
@@ -19,3 +19,2 @@ # API Reference | ||
```sh | ||
$ # Bash terminal | ||
$ DEBUG=* node index.js | ||
@@ -36,3 +35,3 @@ ``` | ||
* [Instance](#method_constructor) | ||
* Localize the pass | ||
* [Localizing Passes](#localizing_passes) | ||
* [.localize()](#method_localize) | ||
@@ -50,3 +49,3 @@ * Setting barcode | ||
* [.nfc()](#method_nfc) | ||
* Setting Pass Structure Keys (primaryFields, secondaryFields, ...) | ||
* [Setting Pass Structure Keys (primaryFields, secondaryFields, ...)](#prop_fields) | ||
* [<field>.push()](#prop_fields-push) | ||
@@ -89,5 +88,6 @@ * [<field>.pop()](#prop_fields-pop) | ||
<br><br> | ||
<a name="localizing_passes"></a> | ||
___ | ||
**Localize the pass**: | ||
**Localizing Passes**: | ||
___ | ||
@@ -341,3 +341,3 @@ | ||
For the first two keys, the argument 'value' (which will be of type **Array\<Object>**) will be checked and filtered against a schema (one for type). | ||
For the first two keys, the argument 'value' (which will be of type **Array\<Object>**) will be checked and filtered against dedicated schema. | ||
@@ -406,2 +406,3 @@ For *relevantDate*, the date is parsed in the same formats of [#expiration()](#method_expiration). For *maxDistance*, the value is simply converted as Number and pushed only with successful conversion. | ||
<br> | ||
<a name="prop_fields"></a> | ||
___ | ||
@@ -443,11 +444,11 @@ | ||
pass.headerFields.push({ | ||
"key": "header1", | ||
"label": "Data", | ||
"value": "25 mag", | ||
"textAlignment": "PKTextAlignmentCenter" | ||
key: "header1", | ||
label: "Data", | ||
value: "25 mag", | ||
textAlignment: "PKTextAlignmentCenter" | ||
}, { | ||
"key": "header2", | ||
"label": "Volo", | ||
"value": "EZY997", | ||
"textAlignment": "PKTextAlignmentCenter" | ||
key: "header2", | ||
label: "Volo", | ||
value: "EZY997", | ||
textAlignment: "PKTextAlignmentCenter" | ||
}); | ||
@@ -546,3 +547,2 @@ | ||
```javascript | ||
... | ||
pass.generate() | ||
@@ -549,0 +549,0 @@ .then(stream => { |
@@ -14,4 +14,8 @@ # Examples | ||
Certificates paths in examples are linked to a folder `certificates` in the root of this project which is not provided. | ||
To make them work, you'll have to edit both certificates and model path. | ||
Generates pass will be generated at address [http://localhost:3000/gen/examplePass](http://localhost:3000/gen/examplePass); | ||
___ | ||
Every contribution is really appreciated. ❤️ Thank you! |
{ | ||
"name": "passkit-generator", | ||
"version": "1.3.2", | ||
"version": "1.3.3", | ||
"description": "The easiest way to generate custom Apple Wallet passes in Node.js", | ||
@@ -26,2 +26,5 @@ "main": "index.js", | ||
}, | ||
"engines": { | ||
"node": ">=8.1.0" | ||
}, | ||
"devDependencies": { | ||
@@ -28,0 +31,0 @@ "express": "^4.16.3", |
# Node PassKit Generator | ||
This is my implementation of a generator for [Apple Wallet Passes](https://developer.apple.com/wallet/). The idea was born during the Apple Developer Academy 17/18, in Naples, driven by the need to develop an iOS app component regarding passes generation for events. | ||
Simple Node.js interface to generate customized [Apple Wallet Passes](https://developer.apple.com/wallet/) for iOS. | ||
### Architecture | ||
### Installation | ||
This generator has two sides: application and model creation. | ||
This package was created with a specific architecture in mind: **application** and **model**, to split as much as possible static objects (such as logo, background, icon, etc.) from dynamic ones (translations, barcodes, serialNumber, ...). | ||
In fact, the pass template creation does not happen in the application. Instead you'll first have to create a folder containing all the static medias for a pass (logo, background, icon, etc.) while the dynamic info (such translations, barcodes, serialNumber and so on) will be applied in runtime. | ||
In fact, pass template (model) creation and population doesn't happen within the application in runtime. Pass template is a folder in _your application directory_ (but nothing will stop you from putting it outside), that will contain all the needed objects (static medias) and structure to make a pass work. | ||
Pass template will be read and pushed as is in the resulting .zip file, while dynamic objects will be patched against `pass.json` or generated in runtime (`manifest.json`, `signature` and translation files). | ||
This package comes with an [API documentation](./API.md), that makes available a series of methods to customize passes. | ||
### Install | ||
```sh | ||
@@ -15,13 +20,17 @@ $ npm install passkit-generator --save | ||
To see the API Reference, please refer to [API document](./API.md). | ||
Created under Node.js v10.8.0. | ||
### Compatibility | ||
This package is compatible starting with Node v8.1.0+. | ||
___ | ||
## Get Started | ||
##### Model | ||
The first thing you'll have to do, is to start creating a model. A model is a folder in your project directory, with inside the basic pass infos, like the thumbnails, the icon, and the background and **pass.json** containing all the static infos about the pass, like Team identifier, Pass type identifier, colors, etc. | ||
> Using the .pass extension is a best practice, showing that the directory is a pass package. | ||
> (cit. [Build your first pass - Apple Developer Portal](https://apple.co/2LYXWo3)). | ||
> ([Build your first pass - Apple Developer Portal](https://apple.co/2LYXWo3)). | ||
Following to this suggestion, each model is required to have a **.pass** extension (as suggested by Apple). | ||
Following to this suggestion, each model is required to have a **.pass** extension. | ||
@@ -35,3 +44,3 @@ ```bash | ||
You can also create `.lproj` folders (e.g. *en.lproj* or *it.lproj*) containing localized media. To include a folder or translate texts inside the pass, please refer to the API, [.localize()](./API.md#method_localize) method. | ||
You can also create `.lproj` folders (e.g. *en.lproj* or *it.lproj*) containing localized media. To include a folder or translate texts inside the pass, please refer to [Localizing Passes](./API.md#method_localize) in the API documentation. | ||
@@ -42,10 +51,10 @@ ##### Pass.json | ||
```javascript | ||
```json | ||
{ | ||
"formatVersion": 1, | ||
"passTypeIdentifier": "pass.<bundle id>", | ||
"teamIdentifier": "<here your team identifier>", | ||
"organizationName": "<your organization name>", | ||
"description": "A localizable description of your pass. To do so, put here a placeholder.", | ||
"boardingPass": {} | ||
"formatVersion": 1, | ||
"passTypeIdentifier": "pass.<bundle id>", | ||
"teamIdentifier": "<here your team identifier>", | ||
"organizationName": "<your organization name>", | ||
"description": "A localizable description of your pass. To do so, put here a placeholder.", | ||
"boardingPass": {} | ||
} | ||
@@ -55,3 +64,3 @@ ``` | ||
##### Certificates | ||
> Requirements: OpenSSL, | ||
> Requirements: OpenSSL, | ||
@@ -81,2 +90,3 @@ The third step is about the developer and WWDR certificates. I suggest you to create a certificate-dedicated folder inside your working directory (e.g. `./certs`) to contain everything concerning the certificates. This is a standard procedure: you would have to do it also without using this library. | ||
___ | ||
@@ -121,8 +131,13 @@ ## Usage example | ||
___ | ||
## Other | ||
If you developed any projects using this library, open an issue topic and link it inside if open to all or just tell it. 😊 You'll make me feel like my time hasn't been wasted (it had not anyway, I learnt a lot of things by doing this). | ||
If you developed any public projects using this library, open a topic in issues and link it inside if open to all or just tell it. 😊 You'll make me feel like my time hasn't been wasted (it had not anyway, I learnt a lot of things by doing this). | ||
Be sure to not include the certificates if you publish your project open to everybody. | ||
Any contribution is welcome ❤️ | ||
A big thanks to all the people and friends in the Apple Developer Academy that pushed me and helped me into realizing something like this and a big thanks to the ones that helped me to make technical choices. | ||
Any contribution is welcome. ❤️ | ||
The idea to develop this package, was born during the Apple Developer Academy 17/18, in Naples, Italy, driven by the need to create an iOS app component regarding passes generation for events. | ||
A big thanks to all the people and friends in the Apple Developer Academy (and not) that pushed me and helped me into realizing something like this and a big thanks to the ones that helped me to make technical choices. |
const schema = require("./schema"); | ||
const debug = require("debug")("passkit:fields"); | ||
@@ -8,5 +9,6 @@ /** | ||
let uniqueKeys = []; | ||
class FieldsContainer { | ||
constructor() { | ||
this._uniqueKeys = []; | ||
this.fields = []; | ||
@@ -30,12 +32,17 @@ } | ||
let validFields = fieldsData.filter(f => { | ||
if (this._uniqueKeys.includes(f.key)) { | ||
return false; | ||
let validFields = fieldsData.reduce((acc, current) => { | ||
if (!(typeof current === "object") || !schema.isValid(current, "field")) { | ||
return acc; | ||
} | ||
this._uniqueKeys.push(f.key); | ||
if (acc.some(e => e.key === current.key) || uniqueKeys.includes(current.key)) { | ||
debug(`UNIQUE field key CONSTRAINT VIOLATED. Fields keys must be unique in pass scope. Field key: "${current.key}"`); | ||
return acc; | ||
} | ||
return typeof f === "object" && schema.isValid(f, "field"); | ||
}); | ||
acc.push(current); | ||
return acc; | ||
}, []); | ||
uniqueKeys.push(...validFields.map(v => v.key)); | ||
this.fields.push(...validFields); | ||
@@ -71,4 +78,8 @@ | ||
} | ||
static emptyUnique() { | ||
uniqueKeys = []; | ||
} | ||
} | ||
module.exports = FieldsContainer; |
let errors = { | ||
VALIDATION_FAILED: "Validation of pass type failed. Pass file is not a valid buffer or (more probabily) does not respect the schema. Refer to https://apple.co/2Nvshvn to build a correct pass.", | ||
UNINITIALIZED: "Provided model (%s) matched but unitialized or may not contain icon. Refer to https://apple.co/2IhJr0Q, https://apple.co/2Nvshvn and documentation to fill the model correctly.", | ||
MANIFEST_TYPE: "Manifest content must be a string or an object. Unable to accept manifest of type %s", | ||
REQS_NOT_MET: "The options passed to Pass constructor does not meet the requirements. Refer to the documentation to compile them correctly.", | ||
@@ -6,0 +5,0 @@ MODEL_NOT_STRING: "A string model name must be provided in order to continue.", |
@@ -152,2 +152,4 @@ const fs = require("fs"); | ||
FieldsContainer.emptyUnique(); | ||
return archive.finalize().then(() => passStream); | ||
@@ -469,3 +471,3 @@ }); | ||
* @method _sign | ||
* @params {String|Object} manifest - Manifest content. | ||
* @params {Object} manifest - Manifest content. | ||
* @returns {Buffer} | ||
@@ -477,9 +479,3 @@ */ | ||
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.content = forge.util.createBuffer(JSON.stringify(manifest), "utf8"); | ||
@@ -489,6 +485,14 @@ signature.addCertificate(this.Certificates.wwdr); | ||
/** | ||
* authenticatedAttributes belong to PKCS#9 standard. | ||
* It requires at least 2 values: | ||
* • content-type (which is a PKCS#7 oid) and | ||
* • message-digest oid. | ||
* | ||
* Wallet requires a signingTime. | ||
*/ | ||
signature.addSigner({ | ||
key: this.Certificates.signerKey, | ||
certificate: this.Certificates.signerCert, | ||
digestAlgorithm: forge.pki.oids.sha1, | ||
authenticatedAttributes: [{ | ||
@@ -500,3 +504,2 @@ type: forge.pki.oids.contentType, | ||
}, { | ||
// the value is autogenerated | ||
type: forge.pki.oids.signingTime, | ||
@@ -506,23 +509,23 @@ }] | ||
signature.sign(); | ||
/** | ||
* We are creating a detached signature because we don't need the signed content. | ||
* Detached signature is a property of PKCS#7 cryptography standard. | ||
*/ | ||
/* | ||
* 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.sign({ detached: true }); | ||
/** | ||
* Signature here is an ASN.1 valid structure (DER-compliant). | ||
* Generating a non-detached signature, would have pushed inside signature.contentInfo | ||
* (which has type 16, or "SEQUENCE", and is an array) a Context-Specific element, with the signed | ||
* signed content as value. | ||
* | ||
* signature.contentInfo.type => SEQUENCE OF (16) | ||
* signature.contentInfo.value[0].type => OBJECT IDENTIFIER (6) | ||
* signature.contantInfo.value[1].type => END OF CONTENT (EOC - 0) | ||
* In fact the previous approach was to generating a detached signature and the pull away the generated | ||
* content. | ||
* | ||
* 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. | ||
* | ||
* That's what happens when you copy a fu****g line without understanding what it does. | ||
* Well, nevermind, it was funny to study BER, DER, CER, ASN.1 and PKCS#7. You can learn a lot | ||
* of beautiful things. ¯\_(ツ)_/¯ | ||
*/ | ||
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"); | ||
@@ -529,0 +532,0 @@ } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
593816
1596
138
0