mirror of
https://github.com/marcogll/passkit-generator.git
synced 2026-03-15 16:25:21 +00:00
v2.0.0 merge commit
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ passModels/
|
||||
certificates/
|
||||
*.code-workspace
|
||||
.vscode/
|
||||
*.js
|
||||
|
||||
@@ -3,5 +3,5 @@ certificates/
|
||||
*.code-workspace
|
||||
examples/
|
||||
.vscode/
|
||||
abstract.ts
|
||||
abstract.js
|
||||
*.ts
|
||||
!*.d.ts
|
||||
|
||||
445
API.md
445
API.md
@@ -11,75 +11,79 @@ Some details:
|
||||
|
||||
* 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).
|
||||
|
||||
* Here below are listed all the available methods that library will let you use.
|
||||
|
||||
* In case of troubleshooting, you can start your project with "debug flag" as follows:
|
||||
|
||||
```sh
|
||||
$ DEBUG=* node index.js
|
||||
```
|
||||
|
||||
For other OSs, see [Debug Documentation](https://www.npmjs.com/package/debug).
|
||||
* In case of troubleshooting, you can refer to the [Self-help](https://github.com/alexandercerutti/passkit-generator/wiki/Self-help) guide in Wiki or open an issue.
|
||||
|
||||
* Keep this as always valid for the reference:
|
||||
|
||||
```javascript
|
||||
const { Pass } = require("passkit-generator");
|
||||
const { createPass } = require("passkit-generator");
|
||||
```
|
||||
___
|
||||
|
||||
### Index:
|
||||
|
||||
* [Instance](#method_constructor)
|
||||
* [Create a Pass](#pass_class_constructor)
|
||||
* [Localizing Passes](#localizing_passes)
|
||||
* [.localize()](#method_localize)
|
||||
* Setting barcode
|
||||
* [.barcodes()](#method_barcodes)
|
||||
* [.barcode()](#method_barcode)
|
||||
* [.backward()](#method_bBackward)
|
||||
* [.autocomplete()](#method_bAutocomplete)
|
||||
* Setting expiration / voiding the pass
|
||||
* [.expiration()](#method_expiration)
|
||||
* [.void()](#method_void)
|
||||
* Setting relevance
|
||||
* [.relevance()](#method_relevance)
|
||||
* [.beacons()](#method_beacons)
|
||||
* [.locations()](#method_locations)
|
||||
* [.relevantDate()](#method_revdate)
|
||||
* Setting NFC
|
||||
* [.nfc()](#method_nfc)
|
||||
* Getting remote resources
|
||||
* [.load()](#method_load)
|
||||
* [Personalization](#personalization)
|
||||
* Getting the current information
|
||||
* [.props](#getter_props)
|
||||
* [Setting Pass Structure Keys (primaryFields, secondaryFields, ...)](#prop_fields)
|
||||
* [TransitType](#prop_transitType)
|
||||
* Generating the compiled pass.
|
||||
* [.generate()](#method_generate)
|
||||
* [Create an Abstract Models](#abs_class_constructor)
|
||||
* [.bundle](#getter_abmbundle)
|
||||
* [.certificates](#getter_abmcertificates)
|
||||
* [.overrides](#getter_abmoverrides)
|
||||
|
||||
<br><br>
|
||||
___
|
||||
|
||||
<a name="method_constructor"></a>
|
||||
## Create a Pass
|
||||
___
|
||||
|
||||
<a name="pass_class_constructor"></a>
|
||||
|
||||
#### constructor()
|
||||
|
||||
```javascript
|
||||
var pass = new Pass(options);
|
||||
```typescript
|
||||
const pass = await createPass({ ... }, Buffer.from([ ... ], { ... }));
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
|
||||
`Object<Pass>`
|
||||
`Promise<Pass>`
|
||||
|
||||
**Arguments**:
|
||||
|
||||
| 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 | Object \| [Abstract Model](#abs_class_constructor) | The options to create the pass. It can also be an instance of an [Abstract Model](#abs_class_constructor). If instance, below `options` keys are not valid (obv.) and both `abstractMissingData` and `additionalBuffers` can be used. `additionalBuffers` usage is **NOT** restricted to Abstract Models. | false | -
|
||||
| options.model | String \| Path \| Buffer Object | The model path or a Buffer Object with path as key and Buffer as content | false | -
|
||||
| options.certificates | Object | The certificate object containing the paths to certs files. | false | -
|
||||
| options.certificates.wwdr | String/Path | The path to Apple WWDR certificate or its content. | false | -
|
||||
| options.certificates.signerCert | String/Path | The path to Developer certificate file or its content. | false | -
|
||||
| options.certificates.wwdr | String \| Path | The path to Apple WWDR certificate or its content. | false | -
|
||||
| options.certificates.signerCert | String \| Path | The path to Developer certificate file or its content. | false | -
|
||||
| options.certificates.signerKey | Object/String | The object containing developer certificate's key and passphrase. If string, it can be the path to the key file or its content. If object, must have `keyFile` key and might need `passphrase`. | false | -
|
||||
| options.certificates.signerKey.keyFile | String/Path | The path to developer certificate key or its content. | false | -
|
||||
| options.certificates.signerKey.keyFile | String \| Path | The path to developer certificate key or its content. | false | -
|
||||
| options.certificates.signerKey.passphrase | String \| Number | The passphrase to use to unlock the key. | false | -
|
||||
| options.overrides | Object | Dictionary containing all the keys you can override in the pass.json file and does not have a method to get overridden. | true | { }
|
||||
| options.shouldOverwrite | Boolean | Setting this property to false, will make properties in `overrides` and fields to be pushed along with the ones added through methods to the existing ones in pass.json. | true | true
|
||||
| options.overrides | Object | Dictionary containing all the keys you can override in the pass.json file and does not have a method to get overridden. | true | `{ }`
|
||||
| additionalBuffers | Buffer Object | Dictionary with path as key and Buffer as a content. Each will represent a file to be added to the final model. These will have priority on model ones | true | -
|
||||
| abstractMissingData | Object | An object containing missing datas. It will be used only if `options` is an [Abstract Model](#abs_class_constructor). | true | -
|
||||
| abstractMissingData.certificates | Object | The same as `options.certificates` and its keys. | true | -
|
||||
| abstractMissingData.overrides | Object | The same as `options.overrides`. | true | -
|
||||
|
||||
<br><br>
|
||||
<a name="localizing_passes"></a>
|
||||
@@ -92,6 +96,7 @@ Following Apple Developer Documentation, localization (L10N) is done by creating
|
||||
|
||||
In this library, localization can be done in three ways: **media-only** (images), **translations-only** or both.
|
||||
The only differences stands in the way the only method below is used and how the model is designed.
|
||||
If this method is used for translations and the model already contains a `pass.strings` for the specified language, the translations will be appended to that file.
|
||||
|
||||
> If you are designing your pass for a language only, you can directly replace the placeholders in `pass.json` with translation.
|
||||
|
||||
@@ -100,8 +105,8 @@ The only differences stands in the way the only method below is used and how the
|
||||
|
||||
#### .localize()
|
||||
|
||||
```javascript
|
||||
pass.localize(lang, options);
|
||||
```typescript
|
||||
pass.localize(lang: string, options = {});
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
@@ -121,7 +126,7 @@ In the other two cases, you'll need to specify also the second argument (the tra
|
||||
| Key | Type | Description | Optional | Default Value |
|
||||
|-----|------|-------------|----------|:-------------:|
|
||||
| lang | String | The ISO-3166-1 language code | false | -
|
||||
| options | Object | Translations in format PLACEHOLDER : TRANSLATED-VALUE. | true | undefined \| { }
|
||||
| options | Object | Translations in format `{ <PLACEHOLDER>: "TRANSLATED-VALUE"}`. | true | undefined \| { }
|
||||
|
||||
**Example**:
|
||||
|
||||
@@ -144,50 +149,43 @@ ___
|
||||
**Setting barcodes**:
|
||||
___
|
||||
|
||||
<a name="method_barcode"></a>
|
||||
<a name="method_barcodes"></a>
|
||||
|
||||
#### .barcode()
|
||||
#### .barcodes()
|
||||
|
||||
```javascript
|
||||
pass.barcode(data);
|
||||
```typescript
|
||||
pass.barcodes(first: string | schema.Barcode, ...data: schema.Barcodes[]) : this;
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
|
||||
`Improved Object<Pass> (this with some "private" methods available to be called under aliases, as below)`
|
||||
`Object<Pass> (this)`
|
||||
|
||||
**Description**:
|
||||
|
||||
Each object in `data` will be filtered against a schema ([Apple reference](https://apple.co/2myAbst)) validation and used if correctly formed.
|
||||
Setting barcodes can happen in two ways: `controlled` and `uncontrolled` (autogenerated), which mean how many [barcode structures](https://apple.co/2myAbst) you will have in your pass.
|
||||
|
||||
If the argument is an Object, it will be treated as one-element Array.
|
||||
Passing a `string` to the method, will lead to an `uncontrolled` way: starting from the message (content), all the structures will be generated. Any further parameter will be ignored.
|
||||
|
||||
If the argument is a String or an Object with `format` parameter missing, but `message` available, the structure will be **autogenerated** complete of all the fallbacks (4 dictionaries).
|
||||
Passing *N* barcode structures (see below), will only validate them and push only the valid ones.
|
||||
|
||||
To support versions prior to iOS 9, `barcode` key is automatically supported as the first valid value of the provided (or generated) barcode. To change the key, see below.
|
||||
This method will not take take of setting retro-compatibility, of which responsability is assigned to `.barcode()`.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
| Key | Type | Description | Optional | Default Value |
|
||||
|-----|------|-------------|----------|:-------------:|
|
||||
| data | String \| Array\<Object> \| Object | Data to be used in the barcode | false | -
|
||||
| Key | Type | Description | Optional |
|
||||
|-------|------|-------------|----------|
|
||||
| first | `String` \| `schema.Barcode` | first value of barcodes | false
|
||||
| ...data | `schema.Barcode[]` | the other barcode values | true
|
||||
|
||||
**Examples**:
|
||||
|
||||
```javascript
|
||||
pass.barcode("11424771526");
|
||||
```typescript
|
||||
pass.barcodes("11424771526");
|
||||
|
||||
// or
|
||||
|
||||
pass.barcode({
|
||||
message: "11424771526",
|
||||
format: "PKBarcodeFormatCode128"
|
||||
altText: "11424771526"
|
||||
});
|
||||
|
||||
// or
|
||||
|
||||
pass.barcode([{
|
||||
pass.barcodes({
|
||||
message: "11424771526",
|
||||
format: "PKBarcodeFormatCode128"
|
||||
altText: "11424771526"
|
||||
@@ -199,17 +197,18 @@ To support versions prior to iOS 9, `barcode` key is automatically supported as
|
||||
message: "11424771526",
|
||||
format: "PKBarcodeFormatPDF417"
|
||||
altText: "11424771526"
|
||||
}]);
|
||||
});
|
||||
```
|
||||
|
||||
**See**: [PassKit Package Format Reference # Barcode Dictionary](https://apple.co/2myAbst)
|
||||
<br>
|
||||
<a name="method_bBackward"></a>
|
||||
<br>
|
||||
<a name="method_barcode"></a>
|
||||
|
||||
#### .barcode().backward()
|
||||
#### .barcode()
|
||||
|
||||
```javascript
|
||||
pass.barcode(data).backward(format);
|
||||
pass.barcode(chosenFormat: string): this;
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
@@ -219,44 +218,30 @@ pass.barcode(data).backward(format);
|
||||
**Description**:
|
||||
|
||||
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.
|
||||
Also it will work only if `barcodes()` method has already been called or if the current properties already have at least one barcode structure in it and if it matches with the specified one.
|
||||
|
||||
`PKBarcodeFormatCode128` is not supported in barcode. Therefore any attempt to set it, will fail.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
| Key | Type | Description | Optional | Default Value |
|
||||
|-----|------|-------------|----------|:-------------:|
|
||||
| format | String | Format to be used. Must be one of these types: *PKBarcodeFormatQR*, *PKBarcodeFormatPDF417*, *PKBarcodeFormatAztec* | false | -
|
||||
| format | String | Format to be used. Must be one of these types: `PKBarcodeFormatQR`, `PKBarcodeFormatPDF417`, `PKBarcodeFormatAztec` | false | -
|
||||
|
||||
**Example**:
|
||||
|
||||
```javascript
|
||||
// Based on the previous example
|
||||
// Based on the previous (barcodes) example
|
||||
pass
|
||||
.barcode(...)
|
||||
.backward("PKBarcodeFormatQR");
|
||||
.barcodes(...)
|
||||
.barcode("PKBarcodeFormatQR");
|
||||
|
||||
// This won't set the property since not found.
|
||||
pass
|
||||
.barcode(...)
|
||||
.backward("PKBarcodeFormatAztec");
|
||||
.barcodes(...)
|
||||
.barcode("PKBarcodeFormatAztec");
|
||||
```
|
||||
|
||||
<br>
|
||||
<a name="method_bAutocomplete"></a>
|
||||
|
||||
#### .barcode().autocomplete()
|
||||
|
||||
```javascript
|
||||
pass.barcode(data).autocomplete();
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
|
||||
`Improved Object<Pass> ("this" with backward() support and length prop. reporting how many structs have been added).`
|
||||
|
||||
**Description**:
|
||||
|
||||
It will generate all the barcodes fallback starting from the first dictionary in `barcodes`.
|
||||
|
||||
<br><br>
|
||||
___
|
||||
@@ -268,8 +253,8 @@ ___
|
||||
|
||||
#### .expiration()
|
||||
|
||||
```javascript
|
||||
pass.expiration(date [, format]);
|
||||
```typescript
|
||||
pass.expiration(date: Date) : this;
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
@@ -278,22 +263,18 @@ pass.expiration(date [, format]);
|
||||
|
||||
**Description**:
|
||||
|
||||
It sets the date of expiration to the passed argument. The date will be automatically parsed in order in the following formats:
|
||||
|
||||
* **MM-DD-YYYY hh:mm:ss**,
|
||||
* **DD-MM-YYYY hh:mm:ss**.
|
||||
|
||||
Otherwise you can specify a personal format to use.
|
||||
|
||||
Seconds are not optionals.
|
||||
It sets the date of expiration to the passed argument.
|
||||
If the parsing fails, the error will be emitted only in debug mode and the property won't be set.
|
||||
Passing `null` as the parameter, will remove the value.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
| Key | Type | Description | Optional | Default Value |
|
||||
|-----|------|-------------|----------|:-------------:|
|
||||
| date | String/date | The date on which the pass will expire | false | -
|
||||
| format | String | A custom format to be used to parse the date | true | undefined
|
||||
| Key | Type | Description | Optional |
|
||||
|-----|------|-------------|----------|
|
||||
| date | String/date | The date on which the pass will expire | false
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
|
||||
<a name="method_void"></a>
|
||||
|
||||
@@ -309,7 +290,7 @@ pass.void();
|
||||
|
||||
**Description**:
|
||||
|
||||
It sets directly the pass as voided (void: true).
|
||||
It sets directly the pass as voided.
|
||||
|
||||
<br><br>
|
||||
___
|
||||
@@ -317,53 +298,107 @@ ___
|
||||
**Setting relevance**:
|
||||
___
|
||||
|
||||
<a name="method_relevance"></a>
|
||||
<a name="method_beacons"></a>
|
||||
|
||||
#### .relevance()
|
||||
#### .beacons()
|
||||
|
||||
```javascript
|
||||
pass.relevance(key, value [, relevanceDateFormat]);
|
||||
```typescript
|
||||
pass.beacons(...data: schema.Beacons[]): this;
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
|
||||
`Improved Object<Pass> (this with length property)`
|
||||
`Object<Pass> (this)`
|
||||
|
||||
**Description**:
|
||||
|
||||
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.
|
||||
|
||||
For the first two keys, the argument 'value' (which will be of type **Array\<Object>**) will be checked and filtered against dedicated schema.
|
||||
|
||||
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.
|
||||
|
||||
Sets the beacons information in the passes.
|
||||
If other beacons structures are available in the structure, they will be overwritten.
|
||||
Passing `null` as parameter, will remove the content.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
| Key | Type | Description | Optional | Default Value |
|
||||
|-----|------|-------------|----------|:-------------:|
|
||||
| key | String | The relevance key to be set, among **beacons**, **locations**, **relevantDate** and **maxDistance** | false | -
|
||||
| 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
|
||||
| ...data | [schema.Beacons[]](https://apple.co/2XPDoYX) \| `null` | The beacons structures | false | -
|
||||
|
||||
**Example**:
|
||||
|
||||
```javascript
|
||||
pass.relevance("locations", [{
|
||||
longitude: "73.2943532945212",
|
||||
latitude: "-42.3088613015625",
|
||||
}]);
|
||||
|
||||
pass.relevance("maxDistance", 150);
|
||||
|
||||
// DD-MM-YYYY -> April, 10th 2021
|
||||
pass.relevance("relevantDate", "10/04/2021", "DD-MM-YYYY");
|
||||
|
||||
// MM-DD-YYYY -> October, 4th 2021
|
||||
pass.relevance("relevantDate", "10/04/2021");
|
||||
```typescript
|
||||
pass.beacons({
|
||||
"major": 55,
|
||||
"minor": 0,
|
||||
"proximityUUID": "59da0f96-3fb5-43aa-9028-2bc796c3d0c5"
|
||||
}, {
|
||||
"major": 65,
|
||||
"minor": 46,
|
||||
"proximityUUID": "fdcbbf48-a4ae-4ffb-9200-f8a373c5c18e",
|
||||
});
|
||||
```
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
|
||||
<a name="method_locations"></a>
|
||||
|
||||
#### .locations()
|
||||
|
||||
```typescript
|
||||
pass.locations(...data: schema.Locations[]): this;
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
|
||||
`Object<Pass> (this)`
|
||||
|
||||
**Description**:
|
||||
|
||||
Sets the location-relevance information in the passes.
|
||||
If other location structures are available in the structure, they will be overwritten.
|
||||
Passing `null` as parameter, will remove its content;
|
||||
|
||||
**Arguments**:
|
||||
|
||||
| Key | Type | Description | Optional | Default Value |
|
||||
|-----|------|-------------|----------|:-------------:|
|
||||
| ...data | [schema.Locations[]](https://apple.co/2LE00VZ) \| `null` | The location structures | false | -
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
pass.locations({
|
||||
"latitude": 66.45725212,
|
||||
"longitude": 33.010004420
|
||||
}, {
|
||||
"longitude": 4.42634523,
|
||||
"latitude": 5.344233323352
|
||||
});
|
||||
```
|
||||
<br>
|
||||
<hr>
|
||||
|
||||
<a name="method_relevantDate"></a>
|
||||
|
||||
#### .relevantDate()
|
||||
|
||||
```typescript
|
||||
pass.relevantDate(date: Date): this;
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
|
||||
`Object<Pass> (this)`
|
||||
|
||||
**Description**:
|
||||
|
||||
Sets the relevant date for the current pass. Passing `null` to the parameter, will remove its content.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
| Key | Type | Description | Optional | Default Value |
|
||||
|-----|------|-------------|----------|:-------------:|
|
||||
| date | Date \| `null` | The relevant date | false | -
|
||||
|
||||
<br><br>
|
||||
___
|
||||
|
||||
@@ -374,8 +409,8 @@ ___
|
||||
|
||||
#### .nfc()
|
||||
|
||||
```javascript
|
||||
pass.nfc([data, ...])
|
||||
```typescript
|
||||
pass.nfc(data: schema.NFC): this
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
@@ -384,61 +419,64 @@ pass.nfc([data, ...])
|
||||
|
||||
**Description**:
|
||||
|
||||
It sets the property for nfc dictionary.
|
||||
An Object as argument will be treated as one-element array.
|
||||
|
||||
>*Notice*: **I had the possibility to test in no way this pass feature and, therefore, the implementation. If you need it and this won't work, feel free to contact me and we will investigate together 😄**
|
||||
It sets NFC info for the current pass. Passing `null` as parameter, will remove its value.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
| Key | Type | Description | Optional | Default Value |
|
||||
|-----|------|-------------|----------|:-------------:|
|
||||
| data | Array\<Object> \| Object | The data regarding to be used for nfc | false | -
|
||||
| Key | Type | Description | Optional |
|
||||
|-----|------|-------------|----------|
|
||||
| data | [schema.NFC](https://apple.co/2XrXwMr) \| `null` | NFC structure | false
|
||||
|
||||
**See**: [PassKit Package Format Reference # NFC](https://apple.co/2wTxiaC)
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
|
||||
<a name="personalization"></a>
|
||||
|
||||
#### Personalization / Reward Enrollment passes
|
||||
|
||||
Personalization (or [Reward Enrollment](https://apple.co/2YkS12N) passes) is supported only if `personalization.json` is available and it's a valid json file (checked against a schema), `personalizationLogo@XX.png` (with 'XX' => x2, x3) is available and NFC is setted.
|
||||
If these conditions are not met, the personalization gets removed from the output pass.
|
||||
|
||||
>*Notice*: **I had the possibility to test in no way this feature on any real pass. If you need it and this won't work, feel free to contact me and we will investigate together 😄**
|
||||
|
||||
<br><br>
|
||||
___
|
||||
<hr>
|
||||
|
||||
**Getting remote resources**:
|
||||
___
|
||||
<a name="getter_props"></a>
|
||||
|
||||
<a name="method_load"></a>
|
||||
#### [Getter] .props()
|
||||
|
||||
#### .load()
|
||||
|
||||
```javascript
|
||||
pass.load(resource, name);
|
||||
```typescript
|
||||
pass.props;
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
|
||||
`Object<Pass> (this)`
|
||||
An object containing all the current props;
|
||||
|
||||
**Description**:
|
||||
|
||||
Sets the resources to be downloaded in runtime to be pushed in the pass.
|
||||
Use `name` param to give your downloaded file a name or to provide the folder path it will be pushed into (with the name, _obv._).
|
||||
This is a getter: a way to access to the current props before generating a pass. In here are available the props set both from pass.json reading and this package methods usage, along with the valid overrides passed to `createPass`. The keys are the same used in pass.json.
|
||||
|
||||
Requests are not cached and load method can only load pictures right now (no other types should be required). In case of conflict between downloaded files and model files, downloaded files will have the priority and will be putted in the zip file.
|
||||
|
||||
When in debug mode, file header is shown.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
| Key | Type | Description | Optional | Default Value |
|
||||
|-----|------|-------------|----------|:-------------:|
|
||||
| resource | String | The URL where to fetch the picture | false | -
|
||||
| name | String | The name / path to be used to call this | false | -
|
||||
It does not contain fields content (`primaryFields`, `secondaryFields`...) and `transitType`, which are still accessible through their own props.
|
||||
|
||||
**Example**:
|
||||
|
||||
```javascript
|
||||
pass.load("http://...", "icon.png");
|
||||
pass.load("http://...", "en.lproj/icon.png");
|
||||
```typescript
|
||||
const currentLocations = pass.props["locations"];
|
||||
pass.locations({
|
||||
"latitude": 66.45725212,
|
||||
"longitude": 33.010004420
|
||||
}, {
|
||||
"longitude": 4.42634523,
|
||||
"latitude": 5.344233323352
|
||||
},
|
||||
...currentLocations);
|
||||
```
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<br><br>
|
||||
|
||||
<a name="prop_fields"></a>
|
||||
___
|
||||
@@ -474,7 +512,7 @@ pass.primaryFields.pop();
|
||||
|
||||
#### .transitType
|
||||
|
||||
```javascript
|
||||
```typescript
|
||||
pass.transitType = "PKTransitTypeAir";
|
||||
```
|
||||
|
||||
@@ -498,27 +536,92 @@ As you can see in [examples folder](/examples), to send a .pkpass file, a basic
|
||||
|
||||
#### .generate()
|
||||
|
||||
```javascript
|
||||
pass.generate();
|
||||
```typescript
|
||||
pass.generate(): Stream;
|
||||
```
|
||||
|
||||
**Returns**: `Promise`
|
||||
**Returns**: `Stream`
|
||||
|
||||
**Description**:
|
||||
|
||||
The returned Promise will contain a stream or an error.
|
||||
Creates a pass zip as Stream.
|
||||
|
||||
**Examples**:
|
||||
|
||||
```javascript
|
||||
pass.generate()
|
||||
.then(stream => {
|
||||
doSomethingWithPassStream();
|
||||
})
|
||||
.catch(error => {
|
||||
doSomethingWithThrownError();
|
||||
});
|
||||
```typescript
|
||||
const passStream = pass.generate();
|
||||
doSomethingWithPassStream(stream);
|
||||
```
|
||||
|
||||
<br><br>
|
||||
|
||||
___
|
||||
|
||||
## Create an Abstract Model
|
||||
___
|
||||
|
||||
<a name="#abs_class_constructor"></a>
|
||||
|
||||
#### constructor()
|
||||
|
||||
```typescript
|
||||
const abstractModel = await createAbstractModel({ ... });
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
|
||||
`Promise<AbstractModel>`
|
||||
|
||||
**Description**:
|
||||
|
||||
The purpose of this class, is to create a model to be kept in memory during the application runtime. It contains a processed version of the passed `model` (already read and splitted) and, if passed, a processed version of the `certificates`, along with the chosen overrides.
|
||||
Since `certificates` and `overrides` might differ time to time or not available at the moment of the abstract model creation, an additional attribute has been added to [`createPass`](#pass_class_constructor) function. It is an object that accepts `overrides` and raw `certificates`
|
||||
|
||||
**Arguments**:
|
||||
|
||||
It accepts only one argument: an `options` object, which is identical to the first parameter of [`createPass`](#pass_class_constructor). You can refer to that method to compile it correctly.
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
|
||||
<a name="getter_abmbundle"></a>
|
||||
|
||||
#### [Getter] .bundle()
|
||||
|
||||
```typescript
|
||||
abstractModel.bundle
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
|
||||
An object containing processed model.
|
||||
|
||||
<hr>
|
||||
<a name="getter_abmcertificates"></a>
|
||||
|
||||
#### [Getter] .certificates()
|
||||
|
||||
```typescript
|
||||
abstractModel.certificates
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
|
||||
An object containing processed certificates.
|
||||
|
||||
<hr>
|
||||
<a name="getter_abmoverrides"></a>
|
||||
|
||||
#### [Getter] .overrides()
|
||||
|
||||
```typescript
|
||||
abstractModel.overrides
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
|
||||
An object containing passed overrides.
|
||||
|
||||
___
|
||||
|
||||
Thanks for using this library. ❤️ Every contribution is welcome.
|
||||
|
||||
51
README.md
51
README.md
@@ -16,16 +16,18 @@
|
||||
|
||||
### Architecture
|
||||
|
||||
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, ...).
|
||||
This package was created with a specific architecture in mind: **application** and **model** (as preprocessed entity), to split as much as possible static objects (such as logo, background, icon, etc.) from dynamic ones (translations, barcodes, serialNumber, ...).
|
||||
|
||||
Actually, pass creation and population doesn't fully happen within the application in runtime. Pass template is a folder in, for example, _your application directory_ (but nothing will stop you from putting it outside), that will contain all the objects needed (static medias) and structure to make a pass work.
|
||||
Pass creation and population doesn't fully happen in runtime. Pass template (model) can be one of a set of buffers or a folder, that will contain all the objects needed (static medias) and structure to make a pass work.
|
||||
|
||||
Pass template will be read and pushed as is in the resulting .zip file along with web-fetched medias (also considered dynamic objects), while dynamic objects will be patched against `pass.json` or generated in runtime (`manifest.json`, `signature` and translation files).
|
||||
Both Pass template will be read and pushed as they are in the resulting .zip file, while dynamic objects will be patched against `pass.json` or generated in runtime (`manifest.json`, `signature` and translation files).
|
||||
All the static medias from both sources, will be read and pushed as they are in the resulting .zip file; dynamic object will be patched against `pass.json`, generated on runtime (`manifest.json`, `signature`) or merged if already existing (translation files).
|
||||
|
||||
This package comes with an [API documentation](./API.md), that makes available a series of methods to customize passes.
|
||||
|
||||
> ⚠ Do not rely on branches outside "master", as might not be stable and will be removed once merged.
|
||||
|
||||
|
||||
### Install
|
||||
```sh
|
||||
$ npm install passkit-generator --save
|
||||
@@ -37,14 +39,18 @@ ___
|
||||
|
||||
##### 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.
|
||||
The first thing you'll have to do, is to start creating a model. A model will contain all 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.
|
||||
|
||||
If starting from scratch, the preferred solution is to use the folder as model, as it will allow you to access easily all the files. Also, a buffer model is mainly designed for models that are ready to be used in your application.
|
||||
|
||||
Let's suppose you have a file `model.zip` stored somewhere: you unzip it in runtime and then get the access to its files as buffers. Those buffers should be available for the rest of your application run-time and you shouldn't be in need to read them every time you are going to create a pass.
|
||||
___
|
||||
|
||||
> Using the .pass extension is a best practice, showing that the directory is a pass package.
|
||||
> ([Build your first pass - Apple Developer Portal](https://apple.co/2LYXWo3)).
|
||||
|
||||
Following to this best practice, the package is set to require each model to have a **_.pass_** extension.
|
||||
If the extension is not specified in the configuration (as in [Usage Example](#usage_example), at "model" key), it will be added forcefully.
|
||||
Following to this best practice, the package is set to require each folder-model to have a **_.pass_** extension.
|
||||
If omitted in the configuration (as in [Usage Example](#usage_example), at "model" key), it will be forcefully added.
|
||||
|
||||
___
|
||||
|
||||
@@ -57,9 +63,11 @@ Follow the [Apple Developer documentation](https://apple.co/2wuJLC1) (_Package S
|
||||
|
||||
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.
|
||||
|
||||
To include a file that belongs to an `.lproj` folder in buffers, you'll just have to name a key like `en.lproj/thumbnail.png`.
|
||||
|
||||
##### Pass.json
|
||||
|
||||
Create a `pass.json` by taking example from examples folder models or the one provided by Apple for the [first tutorial](https://apple.co/2NA2nus) and fill it with the basic informations, that is `teamIdentifier`, `passTypeIdentifier` and all the other basic keys like pass type. Please refer to [Top-Level Keys/Standard Keys](https://apple.co/2PRfSnu) and [Top-Level Keys/Style Keys](https://apple.co/2wzyL5J).
|
||||
Create a `pass.json` by taking example from examples folder models or the one provided by Apple for the [first tutorial](https://apple.co/2NA2nus) and fill it with the basic informations, that are `teamIdentifier`, `passTypeIdentifier` and all the other basic keys like pass type. Please refer to [Top-Level Keys/Standard Keys](https://apple.co/2PRfSnu) and [Top-Level Keys/Style Keys](https://apple.co/2wzyL5J).
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -115,10 +123,15 @@ ___
|
||||
|
||||
## Usage example
|
||||
|
||||
```javascript
|
||||
const { Pass } = require("passkit-generator");
|
||||
```typescript
|
||||
const { createPass, Pass } = require("passkit-generator");
|
||||
// or, for typescript
|
||||
import { createPass, Pass } from "passkit-generator";
|
||||
|
||||
let examplePass = new Pass({
|
||||
let examplePass: Pass;
|
||||
|
||||
try {
|
||||
examplePass = await createPass({
|
||||
model: "./passModels/myFirstModel",
|
||||
certificates: {
|
||||
wwdr: "./certs/wwdr.pem",
|
||||
@@ -131,10 +144,7 @@ let examplePass = new Pass({
|
||||
overrides: {
|
||||
// keys to be added or overridden
|
||||
serialNumber: "AAGH44625236dddaffbda"
|
||||
},
|
||||
// if true, existing keys added through methods get overwritten
|
||||
// pushed in queue otherwise.
|
||||
shouldOverwrite: true
|
||||
}
|
||||
});
|
||||
|
||||
// Adding some settings to be written inside pass.json
|
||||
@@ -142,15 +152,16 @@ examplePass.localize("en", { ... });
|
||||
examplePass.barcode("36478105430"); // Random value
|
||||
|
||||
// Generate the stream, which gets returned through a Promise
|
||||
examplePass.generate()
|
||||
.then(stream => {
|
||||
const stream: Stream = examplePass.generate();
|
||||
|
||||
doSomethingWithTheStream(stream);
|
||||
})
|
||||
.catch(err => {
|
||||
} catch (err) {
|
||||
doSomethingWithTheError(err);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
For more complex usage examples, please refer to [examples](https://github.com/alexandercerutti/passkit-generator/tree/master/examples) folder.
|
||||
|
||||
___
|
||||
|
||||
## Other
|
||||
@@ -159,7 +170,7 @@ If you used this package in any of your projects, feel free to open a topic in i
|
||||
|
||||
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.
|
||||
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 and to all the contributors.
|
||||
|
||||
Any contribution, is welcome.
|
||||
Made with ❤️ in Italy.
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# Examples
|
||||
|
||||
This is examples folder. Each example is linked to webserver.js, which *requires* express.js to run.
|
||||
Express.js **was not** inserted as dipendency.
|
||||
This is examples folder. These examples are used to test new features and as sample showcases.
|
||||
|
||||
Each example is linked to webserver.js, which *requires* express.js to run.
|
||||
Express.js has been inserted as "example package" dipendency.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/alexandercerutti/passkit-generator.git;
|
||||
cd passkit-generator;
|
||||
npm install;
|
||||
npm install -g express;
|
||||
cd examples;
|
||||
node <the-example-you-want-to-execute>.js
|
||||
$ git clone https://github.com/alexandercerutti/passkit-generator.git;
|
||||
$ cd passkit-generator && npm install;
|
||||
$ cd examples && npm install;
|
||||
$ npm run build;
|
||||
$ node <the-example-you-want-to-execute>.js
|
||||
```
|
||||
|
||||
Certificates paths in examples are linked to a folder `certificates` in the root of this project which is not provided.
|
||||
|
||||
160
examples/abstractModel.ts
Normal file
160
examples/abstractModel.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import genRoute, { app } from "./webserver";
|
||||
import { createPass, createAbstractModel, AbstractModel } from "..";
|
||||
|
||||
let abstractModel: AbstractModel;
|
||||
|
||||
(async () => {
|
||||
abstractModel = await createAbstractModel({
|
||||
model: `./models/exampleBooking.pass`,
|
||||
certificates: {
|
||||
wwdr: "../certificates/WWDR.pem",
|
||||
signerCert: "../certificates/signerCert.pem",
|
||||
signerKey: {
|
||||
keyFile: "../certificates/signerKey.pem",
|
||||
passphrase: "123456"
|
||||
}
|
||||
},
|
||||
// overrides: request.body || request.params || request.query,
|
||||
});
|
||||
})();
|
||||
|
||||
genRoute.all(async function manageRequest(request, response) {
|
||||
const passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, "");
|
||||
|
||||
try {
|
||||
const pass = await createPass(abstractModel);
|
||||
|
||||
pass.transitType = "PKTransitTypeAir";
|
||||
|
||||
pass.headerFields.push({
|
||||
"key": "header1",
|
||||
"label": "Data",
|
||||
"value": "25 mag",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
}, {
|
||||
"key": "header2",
|
||||
"label": "Volo",
|
||||
"value": "EZY997",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
});
|
||||
|
||||
pass.primaryFields.push({
|
||||
key: "IATA-source",
|
||||
value: "NAP",
|
||||
label: "Napoli",
|
||||
textAlignment: "PKTextAlignmentLeft"
|
||||
}, {
|
||||
key: "IATA-destination",
|
||||
value: "VCE",
|
||||
label: "Venezia Marco Polo",
|
||||
textAlignment: "PKTextAlignmentRight"
|
||||
});
|
||||
|
||||
pass.secondaryFields.push({
|
||||
"key": "secondary1",
|
||||
"label": "Imbarco chiuso",
|
||||
"value": "18:40",
|
||||
"textAlignment": "PKTextAlignmentCenter",
|
||||
}, {
|
||||
"key": "sec2",
|
||||
"label": "Partenze",
|
||||
"value": "19:10",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
}, {
|
||||
"key": "sec3",
|
||||
"label": "SB",
|
||||
"value": "Sì",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
}, {
|
||||
"key": "sec4",
|
||||
"label": "Imbarco",
|
||||
"value": "Anteriore",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
});
|
||||
|
||||
pass.auxiliaryFields.push({
|
||||
"key": "aux1",
|
||||
"label": "Passeggero",
|
||||
"value": "MR. WHO KNOWS",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "aux2",
|
||||
"label": "Posto",
|
||||
"value": "1A*",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
});
|
||||
|
||||
pass.backFields.push({
|
||||
"key": "document number",
|
||||
"label": "Numero documento:",
|
||||
"value": "- -",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "You're checked in, what next",
|
||||
"label": "Hai effettuato il check-in, Quali sono le prospettive",
|
||||
"value": "",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "Check In",
|
||||
"label": "1. check-in✓",
|
||||
"value": "",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "checkIn",
|
||||
"label": "",
|
||||
"value": "Le uscite d'imbarco chiudono 30 minuti prima della partenza, quindi sii puntuale. In questo aeroporto puoi utilizzare la corsia Fast Track ai varchi di sicurezza.",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "2. Bags",
|
||||
"label": "2. Bagaglio",
|
||||
"value": "",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "Require special assistance",
|
||||
"label": "Assistenza speciale",
|
||||
"value": "Se hai richiesto assistenza speciale, presentati a un membro del personale nell'area di Consegna bagagli almeno 90 minuti prima del volo.",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "3. Departures",
|
||||
"label": "3. Partenze",
|
||||
"value": "",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "photoId",
|
||||
"label": "Un documento d’identità corredato di fotografia",
|
||||
"value": "è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta d’identità.",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "yourSeat",
|
||||
"label": "Il tuo posto:",
|
||||
"value": "verifica il tuo numero di posto nella parte superiore. Durante l’imbarco utilizza le scale anteriori e posteriori: per le file 1-10 imbarcati dalla parte anteriore; per le file 11-31 imbarcati dalla parte posteriore. Colloca le borse di dimensioni ridotte sotto il sedile davanti a te.",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "Pack safely",
|
||||
"label": "Bagaglio sicuro",
|
||||
"value": "Fai clic http://easyjet.com/it/articoli-pericolosi per maggiori informazioni sulle merci pericolose oppure visita il sito CAA http://www.caa.co.uk/default.aspx?catid=2200",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "Thank you for travelling easyJet",
|
||||
"label": "Grazie per aver viaggiato con easyJet",
|
||||
"value": "",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
});
|
||||
|
||||
const stream = pass.generate();
|
||||
response.set({
|
||||
"Content-type": "application/vnd.apple.pkpass",
|
||||
"Content-disposition": `attachment; filename=${passName}.pkpass`
|
||||
});
|
||||
|
||||
stream.pipe(response);
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
|
||||
response.set({
|
||||
"Content-type": "text/html"
|
||||
});
|
||||
|
||||
response.send(err.message);
|
||||
}
|
||||
});
|
||||
@@ -1,91 +0,0 @@
|
||||
/**
|
||||
* .barcode(), autocomplete and backward() methods example
|
||||
* Here we set the barcode. To see all the results, you can
|
||||
* both unzip .pkpass file or check the properties before
|
||||
* generating the whole bundle
|
||||
*
|
||||
* Pass ?alt=true as querystring to test a barcode generate
|
||||
* by a string
|
||||
*/
|
||||
|
||||
const app = require("./webserver");
|
||||
const { Pass } = require("..");
|
||||
|
||||
app.all(function manageRequest(request, response) {
|
||||
|
||||
let passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, "");
|
||||
|
||||
let pass = new Pass({
|
||||
model: `./models/${request.params.modelName}`,
|
||||
certificates: {
|
||||
wwdr: "../certificates/WWDR.pem",
|
||||
signerCert: "../certificates/signerCert.pem",
|
||||
signerKey: {
|
||||
keyFile: "../certificates/signerKey.pem",
|
||||
passphrase: "123456"
|
||||
}
|
||||
},
|
||||
overrides: request.body || request.params || request.query,
|
||||
});
|
||||
|
||||
let bc;
|
||||
|
||||
if (request.query.alt === true) {
|
||||
// After this, pass.props["barcodes"] will have support for all the formats
|
||||
// while pass.props["barcode"] will be the first of barcodes.
|
||||
|
||||
bc = pass.barcode("Thank you for using this package <3");
|
||||
} else {
|
||||
// After this, pass.props["barcodes"] will have support for just two of three
|
||||
// of the passed format (the valid ones) and pass.props["barcode"] the first of barcodes.
|
||||
// if not specified, altText is automatically the message
|
||||
|
||||
bc = pass.barcode([{
|
||||
message: "Thank you for using this package <3",
|
||||
format: "PKBarcodeFormatCode128"
|
||||
}, {
|
||||
message: "Thank you for using this package <3",
|
||||
format: "PKBarcodeFormatPDF417"
|
||||
}, {
|
||||
message: "Thank you for using this package <3",
|
||||
format: "PKBarcodeFormatMock44617"
|
||||
}]);
|
||||
}
|
||||
|
||||
// You can change the format chosen for barcode prop support by calling .backward()
|
||||
// or cancel the support by calling empty .backward
|
||||
// like bc.backward().
|
||||
// If the property passed does not exists, things does not change.
|
||||
|
||||
bc.backward("PKBarcodeFormatPDF417");
|
||||
|
||||
// If your barcode structures got not autogenerated yet (as happens with string
|
||||
// parameter of barcode) you can call .autocomplete() to generate the support
|
||||
// to all the structures. Please beware that this will overwrite ONLY barcodes and not barcode.
|
||||
|
||||
if (!request.query.alt) {
|
||||
// String generated barcode returns autocomplete as empty function
|
||||
bc.autocomplete();
|
||||
}
|
||||
|
||||
console.log("Barcode property is now:", pass._props["barcode"]);
|
||||
console.log("Barcodes support is autocompleted:", pass._props["barcodes"]);
|
||||
|
||||
pass.generate().then(function (stream) {
|
||||
response.set({
|
||||
"Content-type": "application/vnd.apple.pkpass",
|
||||
"Content-disposition": `attachment; filename=${passName}.pkpass`
|
||||
});
|
||||
|
||||
stream.pipe(response);
|
||||
}).catch(err => {
|
||||
|
||||
console.log(err);
|
||||
|
||||
response.set({
|
||||
"Content-type": "text/html",
|
||||
});
|
||||
|
||||
response.send(err.message);
|
||||
});
|
||||
});
|
||||
77
examples/barcode.ts
Normal file
77
examples/barcode.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* .barcode() and .barcodes() methods example
|
||||
* Here we set the barcode. To see all the results, you can
|
||||
* both unzip .pkpass file or check the properties before
|
||||
* generating the whole bundle
|
||||
*
|
||||
* Pass ?alt=true as querystring to test a barcode generate
|
||||
* by a string
|
||||
*/
|
||||
|
||||
import app from "./webserver";
|
||||
import { createPass } from "..";
|
||||
|
||||
app.all(async function manageRequest(request, response) {
|
||||
const passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, "");
|
||||
|
||||
try {
|
||||
const pass = await createPass({
|
||||
model: `./models/${request.params.modelName}`,
|
||||
certificates: {
|
||||
wwdr: "../certificates/WWDR.pem",
|
||||
signerCert: "../certificates/signerCert.pem",
|
||||
signerKey: {
|
||||
keyFile: "../certificates/signerKey.pem",
|
||||
passphrase: "123456"
|
||||
}
|
||||
},
|
||||
overrides: request.body || request.params || request.query,
|
||||
});
|
||||
|
||||
if (request.query.alt === true) {
|
||||
// After this, pass.props["barcodes"] will have support for all the formats
|
||||
// while pass.props["barcode"] will be the first of barcodes.
|
||||
|
||||
pass.barcodes("Thank you for using this package <3");
|
||||
} else {
|
||||
// After this, pass.props["barcodes"] will have support for just two of three
|
||||
// of the passed format (the valid ones);
|
||||
|
||||
pass.barcodes({
|
||||
message: "Thank you for using this package <3",
|
||||
format: "PKBarcodeFormatCode128"
|
||||
}, {
|
||||
message: "Thank you for using this package <3",
|
||||
format: "PKBarcodeFormatPDF417"
|
||||
}, {
|
||||
message: "Thank you for using this package <3",
|
||||
format: "PKBarcodeFormatMock44617"
|
||||
});
|
||||
}
|
||||
|
||||
// You can change the format chosen for barcode prop support by calling .barcode()
|
||||
// or cancel the support by calling empty .barcode
|
||||
// like pass.barcode().
|
||||
|
||||
pass.barcode("PKBarcodeFormatPDF417");
|
||||
|
||||
console.log("Barcode property is now:", pass.props["barcode"]);
|
||||
console.log("Barcodes support is autocompleted:", pass.props["barcodes"]);
|
||||
|
||||
const stream = pass.generate();
|
||||
response.set({
|
||||
"Content-type": "application/vnd.apple.pkpass",
|
||||
"Content-disposition": `attachment; filename=${passName}.pkpass`
|
||||
});
|
||||
|
||||
stream.pipe(response);
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
|
||||
response.set({
|
||||
"Content-type": "text/html",
|
||||
});
|
||||
|
||||
response.send(err.message);
|
||||
}
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* .void() and .expiration() methods example
|
||||
* To check if a ticket is void, look at the barcode;
|
||||
* If it is grayed, the ticket is voided. May not be showed on macOS.
|
||||
*
|
||||
* To check if a ticket has an expiration date, you'll
|
||||
* have to wait two minutes.
|
||||
*/
|
||||
|
||||
const app = require("./webserver");
|
||||
const { Pass } = require("..");
|
||||
|
||||
app.all(function manageRequest(request, response) {
|
||||
let passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, "");
|
||||
|
||||
let pass = new Pass({
|
||||
model: `./models/${request.params.modelName}`,
|
||||
certificates: {
|
||||
wwdr: "../certificates/WWDR.pem",
|
||||
signerCert: "../certificates/signerCert.pem",
|
||||
signerKey: {
|
||||
keyFile: "../certificates/signerKey.pem",
|
||||
passphrase: "123456"
|
||||
}
|
||||
},
|
||||
overrides: request.body || request.params || request.query,
|
||||
});
|
||||
|
||||
pass.load("https://s.gravatar.com/avatar/83cd11399b7ea79977bc302f3931ee52?size=32&default=retro", "icon.png");
|
||||
pass.load("https://s.gravatar.com/avatar/83cd11399b7ea79977bc302f3931ee52?size=64&default=retro", "icon@2x.png");
|
||||
|
||||
// This to import them directly in the localization folder
|
||||
/*
|
||||
pass.load("https://s.gravatar.com/avatar/83cd11399b7ea79977bc302f3931ee52?size=32&default=retro", "en.lproj/icon.png");
|
||||
pass.load("https://s.gravatar.com/avatar/83cd11399b7ea79977bc302f3931ee52?size=64&default=retro", "en.lproj/icon@2x.png");
|
||||
|
||||
pass.localize("en", {
|
||||
"EVENT": "Event",
|
||||
"LOCATION": "Location"
|
||||
});
|
||||
*/
|
||||
|
||||
pass.generate().then(function (stream) {
|
||||
response.set({
|
||||
"Content-type": "application/vnd.apple.pkpass",
|
||||
"Content-disposition": `attachment; filename=${passName}.pkpass`
|
||||
});
|
||||
|
||||
stream.pipe(response);
|
||||
}).catch(err => {
|
||||
|
||||
console.log(err);
|
||||
|
||||
response.set({
|
||||
"Content-type": "text/html",
|
||||
});
|
||||
|
||||
response.send(err.message);
|
||||
});
|
||||
});
|
||||
@@ -5,12 +5,13 @@
|
||||
*
|
||||
* To check if a ticket has an expiration date, you'll
|
||||
* have to wait two minutes.
|
||||
*
|
||||
*/
|
||||
|
||||
const app = require("./webserver");
|
||||
const { Pass } = require("..");
|
||||
import app from "./webserver";
|
||||
import { createPass } from "..";
|
||||
|
||||
app.all(function manageRequest(request, response) {
|
||||
app.all(async function manageRequest(request, response) {
|
||||
if (!request.query.fn) {
|
||||
response.send("<a href='?fn=void'>Generate a voided pass.</a><br><a href='?fn=expiration'>Generate a pass with expiration date</a>");
|
||||
return;
|
||||
@@ -18,7 +19,8 @@ app.all(function manageRequest(request, response) {
|
||||
|
||||
let passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, "");
|
||||
|
||||
let pass = new Pass({
|
||||
try {
|
||||
let pass = await createPass({
|
||||
model: `./models/${request.params.modelName}`,
|
||||
certificates: {
|
||||
wwdr: "../certificates/WWDR.pem",
|
||||
@@ -35,22 +37,21 @@ app.all(function manageRequest(request, response) {
|
||||
pass.void();
|
||||
} else if (request.query.fn === "expiration") {
|
||||
// 2 minutes later...
|
||||
let d = new Date();
|
||||
const d = new Date();
|
||||
d.setMinutes(d.getMinutes() + 2);
|
||||
|
||||
// setting the expiration
|
||||
pass.expiration(d.toLocaleString());
|
||||
pass.expiration(d);
|
||||
}
|
||||
|
||||
pass.generate().then(function (stream) {
|
||||
const stream = pass.generate();
|
||||
response.set({
|
||||
"Content-type": "application/vnd.apple.pkpass",
|
||||
"Content-disposition": `attachment; filename=${passName}.pkpass`
|
||||
});
|
||||
|
||||
stream.pipe(response);
|
||||
}).catch(err => {
|
||||
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
|
||||
response.set({
|
||||
@@ -58,5 +59,5 @@ app.all(function manageRequest(request, response) {
|
||||
});
|
||||
|
||||
response.send(err.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,164 +0,0 @@
|
||||
/**
|
||||
* Fields pushing dimostration
|
||||
* To see all the included Fields, just open the pass
|
||||
* Refer to https://apple.co/2Nvshvn to see how passes
|
||||
* have their fields disposed.
|
||||
*
|
||||
* In this example we are going to imitate an EasyJet boarding pass
|
||||
*
|
||||
* @Author: Alexander P. Cerutti
|
||||
*/
|
||||
|
||||
const app = require("./webserver");
|
||||
const { Pass } = require("..");
|
||||
|
||||
app.all(function manageRequest(request, response) {
|
||||
let passName = "exampleBooking" + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, "");
|
||||
|
||||
let pass = new Pass({
|
||||
model: `./models/exampleBooking`,
|
||||
certificates: {
|
||||
wwdr: "../certificates/WWDR.pem",
|
||||
signerCert: "../certificates/signerCert.pem",
|
||||
signerKey: {
|
||||
keyFile: "../certificates/signerKey.pem",
|
||||
passphrase: "123456"
|
||||
}
|
||||
},
|
||||
overrides: request.body || request.params || request.query,
|
||||
});
|
||||
|
||||
pass.transitType = "PKTransitTypeAir";
|
||||
|
||||
pass.headerFields.push({
|
||||
"key": "header1",
|
||||
"label": "Data",
|
||||
"value": "25 mag",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
}, {
|
||||
"key": "header2",
|
||||
"label": "Volo",
|
||||
"value": "EZY997",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
});
|
||||
|
||||
pass.primaryFields.push({
|
||||
key: "IATA-source",
|
||||
value: "NAP",
|
||||
label: "Napoli",
|
||||
textAlignment: "PKTextAlignmentLeft"
|
||||
}, {
|
||||
key: "IATA-destination",
|
||||
value: "VCE",
|
||||
label: "Venezia Marco Polo",
|
||||
textAlignment: "PKTextAlignmentRight"
|
||||
});
|
||||
|
||||
pass.secondaryFields.push({
|
||||
"key": "secondary1",
|
||||
"label": "Imbarco chiuso",
|
||||
"value": "18:40",
|
||||
"textAlignment": "PKTextAlignmentCenter",
|
||||
}, {
|
||||
"key": "sec2",
|
||||
"label": "Partenze",
|
||||
"value": "19:10",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
}, {
|
||||
"key": "sec3",
|
||||
"label": "SB",
|
||||
"value": "Sì",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
}, {
|
||||
"key": "sec4",
|
||||
"label": "Imbarco",
|
||||
"value": "Anteriore",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
});
|
||||
|
||||
pass.auxiliaryFields.push({
|
||||
"key": "aux1",
|
||||
"label": "Passeggero",
|
||||
"value": "MR. WHO KNOWS",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "aux2",
|
||||
"label": "Posto",
|
||||
"value": "1A*",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
});
|
||||
|
||||
pass.backFields.push({
|
||||
"key": "document number",
|
||||
"label": "Numero documento:",
|
||||
"value": "- -",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "You're checked in, what next",
|
||||
"label": "Hai effettuato il check-in, Quali sono le prospettive",
|
||||
"value": "",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "Check In",
|
||||
"label": "1. check-in✓",
|
||||
"value": "",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "checkIn",
|
||||
"label": "",
|
||||
"value": "Le uscite d'imbarco chiudono 30 minuti prima della partenza, quindi sii puntuale. In questo aeroporto puoi utilizzare la corsia Fast Track ai varchi di sicurezza.",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "2. Bags",
|
||||
"label": "2. Bagaglio",
|
||||
"value": "",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "Require special assistance",
|
||||
"label": "Assistenza speciale",
|
||||
"value": "Se hai richiesto assistenza speciale, presentati a un membro del personale nell'area di Consegna bagagli almeno 90 minuti prima del volo.",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "3. Departures",
|
||||
"label": "3. Partenze",
|
||||
"value": "",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "photoId",
|
||||
"label": "Un documento d’identità corredato di fotografia",
|
||||
"value": "è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta d’identità.",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "yourSeat",
|
||||
"label": "Il tuo posto:",
|
||||
"value": "verifica il tuo numero di posto nella parte superiore. Durante l’imbarco utilizza le scale anteriori e posteriori: per le file 1-10 imbarcati dalla parte anteriore; per le file 11-31 imbarcati dalla parte posteriore. Colloca le borse di dimensioni ridotte sotto il sedile davanti a te.",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "Pack safely",
|
||||
"label": "Bagaglio sicuro",
|
||||
"value": "Fai clic http://easyjet.com/it/articoli-pericolosi per maggiori informazioni sulle merci pericolose oppure visita il sito CAA http://www.caa.co.uk/default.aspx?catid=2200",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "Thank you for travelling easyJet",
|
||||
"label": "Grazie per aver viaggiato con easyJet",
|
||||
"value": "",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
});
|
||||
|
||||
pass.generate().then(function (stream) {
|
||||
response.set({
|
||||
"Content-type": "application/vnd.apple.pkpass",
|
||||
"Content-disposition": `attachment; filename=${passName}.pkpass`
|
||||
});
|
||||
|
||||
stream.pipe(response);
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
|
||||
response.set({
|
||||
"Content-type": "text/html"
|
||||
});
|
||||
|
||||
response.send(err.message);
|
||||
});
|
||||
});
|
||||
164
examples/fields.ts
Normal file
164
examples/fields.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Fields pushing dimostration
|
||||
* To see all the included Fields, just open the pass
|
||||
* Refer to https://apple.co/2Nvshvn to see how passes
|
||||
* have their fields disposed.
|
||||
*
|
||||
* In this example we are going to imitate an EasyJet boarding pass
|
||||
*
|
||||
* @Author: Alexander P. Cerutti
|
||||
*/
|
||||
|
||||
import app from "./webserver";
|
||||
import { createPass } from "..";
|
||||
|
||||
app.all(async function manageRequest(request, response) {
|
||||
let passName = "exampleBooking" + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, "");
|
||||
try {
|
||||
let pass = await createPass({
|
||||
model: `./models/exampleBooking`,
|
||||
certificates: {
|
||||
wwdr: "../certificates/WWDR.pem",
|
||||
signerCert: "../certificates/signerCert.pem",
|
||||
signerKey: {
|
||||
keyFile: "../certificates/signerKey.pem",
|
||||
passphrase: "123456"
|
||||
}
|
||||
},
|
||||
overrides: request.body || request.params || request.query,
|
||||
});
|
||||
|
||||
pass.transitType = "PKTransitTypeAir";
|
||||
|
||||
pass.headerFields.push({
|
||||
"key": "header1",
|
||||
"label": "Data",
|
||||
"value": "25 mag",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
}, {
|
||||
"key": "header2",
|
||||
"label": "Volo",
|
||||
"value": "EZY997",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
});
|
||||
|
||||
pass.primaryFields.push({
|
||||
key: "IATA-source",
|
||||
value: "NAP",
|
||||
label: "Napoli",
|
||||
textAlignment: "PKTextAlignmentLeft"
|
||||
}, {
|
||||
key: "IATA-destination",
|
||||
value: "VCE",
|
||||
label: "Venezia Marco Polo",
|
||||
textAlignment: "PKTextAlignmentRight"
|
||||
});
|
||||
|
||||
pass.secondaryFields.push({
|
||||
"key": "secondary1",
|
||||
"label": "Imbarco chiuso",
|
||||
"value": "18:40",
|
||||
"textAlignment": "PKTextAlignmentCenter",
|
||||
}, {
|
||||
"key": "sec2",
|
||||
"label": "Partenze",
|
||||
"value": "19:10",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
}, {
|
||||
"key": "sec3",
|
||||
"label": "SB",
|
||||
"value": "Sì",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
}, {
|
||||
"key": "sec4",
|
||||
"label": "Imbarco",
|
||||
"value": "Anteriore",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
});
|
||||
|
||||
pass.auxiliaryFields.push({
|
||||
"key": "aux1",
|
||||
"label": "Passeggero",
|
||||
"value": "MR. WHO KNOWS",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "aux2",
|
||||
"label": "Posto",
|
||||
"value": "1A*",
|
||||
"textAlignment": "PKTextAlignmentCenter"
|
||||
});
|
||||
|
||||
pass.backFields.push({
|
||||
"key": "document number",
|
||||
"label": "Numero documento:",
|
||||
"value": "- -",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "You're checked in, what next",
|
||||
"label": "Hai effettuato il check-in, Quali sono le prospettive",
|
||||
"value": "",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "Check In",
|
||||
"label": "1. check-in✓",
|
||||
"value": "",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "checkIn",
|
||||
"label": "",
|
||||
"value": "Le uscite d'imbarco chiudono 30 minuti prima della partenza, quindi sii puntuale. In questo aeroporto puoi utilizzare la corsia Fast Track ai varchi di sicurezza.",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "2. Bags",
|
||||
"label": "2. Bagaglio",
|
||||
"value": "",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "Require special assistance",
|
||||
"label": "Assistenza speciale",
|
||||
"value": "Se hai richiesto assistenza speciale, presentati a un membro del personale nell'area di Consegna bagagli almeno 90 minuti prima del volo.",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "3. Departures",
|
||||
"label": "3. Partenze",
|
||||
"value": "",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "photoId",
|
||||
"label": "Un documento d’identità corredato di fotografia",
|
||||
"value": "è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta d’identità.",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "yourSeat",
|
||||
"label": "Il tuo posto:",
|
||||
"value": "verifica il tuo numero di posto nella parte superiore. Durante l’imbarco utilizza le scale anteriori e posteriori: per le file 1-10 imbarcati dalla parte anteriore; per le file 11-31 imbarcati dalla parte posteriore. Colloca le borse di dimensioni ridotte sotto il sedile davanti a te.",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "Pack safely",
|
||||
"label": "Bagaglio sicuro",
|
||||
"value": "Fai clic http://easyjet.com/it/articoli-pericolosi per maggiori informazioni sulle merci pericolose oppure visita il sito CAA http://www.caa.co.uk/default.aspx?catid=2200",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
}, {
|
||||
"key": "Thank you for travelling easyJet",
|
||||
"label": "Grazie per aver viaggiato con easyJet",
|
||||
"value": "",
|
||||
"textAlignment": "PKTextAlignmentLeft"
|
||||
});
|
||||
|
||||
const stream = pass.generate();
|
||||
response.set({
|
||||
"Content-type": "application/vnd.apple.pkpass",
|
||||
"Content-disposition": `attachment; filename=${passName}.pkpass`
|
||||
});
|
||||
|
||||
stream.pipe(response);
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
|
||||
response.set({
|
||||
"Content-type": "text/html"
|
||||
});
|
||||
|
||||
response.send(err.message);
|
||||
}
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
/**
|
||||
* .localize() methods example
|
||||
* To see all the included languages, you have to unzip the
|
||||
* .pkpass file and check for .lproj folders
|
||||
*/
|
||||
|
||||
const app = require("./webserver");
|
||||
const { Pass } = require("..");
|
||||
|
||||
app.all(function manageRequest(request, response) {
|
||||
|
||||
let passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, "");
|
||||
|
||||
let pass = new Pass({
|
||||
model: `./models/${request.params.modelName}`,
|
||||
certificates: {
|
||||
wwdr: "../certificates/WWDR.pem",
|
||||
signerCert: "../certificates/signerCert.pem",
|
||||
signerKey: {
|
||||
keyFile: "../certificates/signerKey.pem",
|
||||
passphrase: "123456"
|
||||
}
|
||||
},
|
||||
overrides: request.body || request.params || request.query,
|
||||
});
|
||||
|
||||
// For each language you include, an .lproj folder in pass bundle
|
||||
// is created or included. You may not want to add translations but
|
||||
// only images for a specific language. So you create manually
|
||||
// an .lproj folder in your pass model then add the language here below.
|
||||
// If no translations were added, the folder
|
||||
// is included or created but without pass.strings file
|
||||
|
||||
// English, does not has an .lproj folder and no translation
|
||||
// Text placeholders may not be showed for the english language
|
||||
// (e.g. "Event" and "Location" as literal) and another language may be used instead
|
||||
pass.localize("en");
|
||||
|
||||
// Italian, already has an .lproj which gets included
|
||||
pass.localize("it", {
|
||||
"EVENT": "Evento",
|
||||
"LOCATION": "Dove"
|
||||
});
|
||||
|
||||
// German, doesn't, so is created
|
||||
pass.localize("de", {
|
||||
"EVENT": "Ereignis",
|
||||
"LOCATION": "Ort"
|
||||
});
|
||||
|
||||
// This language does not exist but is still added as .lproj folder
|
||||
pass.localize("zu", {});
|
||||
|
||||
console.log("Added languages", Object.keys(pass.l10n).join(", "))
|
||||
|
||||
pass.generate().then(function (stream) {
|
||||
response.set({
|
||||
"Content-type": "application/vnd.apple.pkpass",
|
||||
"Content-disposition": `attachment; filename=${passName}.pkpass`
|
||||
});
|
||||
|
||||
stream.pipe(response);
|
||||
}).catch(err => {
|
||||
|
||||
console.log(err);
|
||||
|
||||
response.set({
|
||||
"Content-type": "text/html",
|
||||
});
|
||||
|
||||
response.send(err.message);
|
||||
});
|
||||
});
|
||||
73
examples/localization.ts
Normal file
73
examples/localization.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* .localize() methods example
|
||||
* To see all the included languages, you have to unzip the
|
||||
* .pkpass file and check for .lproj folders
|
||||
*/
|
||||
|
||||
import app from "./webserver";
|
||||
import { createPass } from "..";
|
||||
|
||||
app.all(async function manageRequest(request, response) {
|
||||
const passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, "");
|
||||
|
||||
try {
|
||||
const pass = await createPass({
|
||||
model: `./models/${request.params.modelName}`,
|
||||
certificates: {
|
||||
wwdr: "../certificates/WWDR.pem",
|
||||
signerCert: "../certificates/signerCert.pem",
|
||||
signerKey: {
|
||||
keyFile: "../certificates/signerKey.pem",
|
||||
passphrase: "123456"
|
||||
}
|
||||
},
|
||||
overrides: request.body || request.params || request.query
|
||||
});
|
||||
|
||||
// For each language you include, an .lproj folder in pass bundle
|
||||
// is created or included. You may not want to add translations but
|
||||
// only images for a specific language. So you create manually
|
||||
// an .lproj folder in your pass model then add the language here below.
|
||||
// If no translations were added, the folder
|
||||
// is included or created but without pass.strings file
|
||||
|
||||
// English, does not has an .lproj folder and no translation
|
||||
// Text placeholders may not be showed for the english language
|
||||
// (e.g. "Event" and "Location" as literal) and another language may be used instead
|
||||
pass.localize("en");
|
||||
|
||||
// Italian, already has an .lproj which gets included
|
||||
pass.localize("it", {
|
||||
"EVENT": "Evento",
|
||||
"LOCATION": "Dove"
|
||||
});
|
||||
|
||||
// German, doesn't, so is created
|
||||
pass.localize("de", {
|
||||
"EVENT": "Ereignis",
|
||||
"LOCATION": "Ort"
|
||||
});
|
||||
|
||||
// This language does not exist but is still added as .lproj folder
|
||||
pass.localize("zu", {});
|
||||
|
||||
// @ts-ignore - ignoring for logging purposes. Do not replicate
|
||||
console.log("Added languages", Object.keys(pass.l10nTranslations).join(", "))
|
||||
|
||||
const stream = pass.generate();
|
||||
response.set({
|
||||
"Content-type": "application/vnd.apple.pkpass",
|
||||
"Content-disposition": `attachment; filename=${passName}.pkpass`
|
||||
});
|
||||
|
||||
stream.pipe(response);
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
|
||||
response.set({
|
||||
"Content-type": "text/html",
|
||||
});
|
||||
|
||||
response.send(err.message);
|
||||
}
|
||||
});
|
||||
BIN
examples/models/examplePass.pass/de.lproj/icon.png
Normal file
BIN
examples/models/examplePass.pass/de.lproj/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
BIN
examples/models/examplePass.pass/de.lproj/icon@2x.png
Normal file
BIN
examples/models/examplePass.pass/de.lproj/icon@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
BIN
examples/models/examplePass.pass/de.lproj/thumbnail.png
Normal file
BIN
examples/models/examplePass.pass/de.lproj/thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
examples/models/examplePass.pass/de.lproj/thumbnail@2x.png
Normal file
BIN
examples/models/examplePass.pass/de.lproj/thumbnail@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
434
examples/package-lock.json
generated
Normal file
434
examples/package-lock.json
generated
Normal file
@@ -0,0 +1,434 @@
|
||||
{
|
||||
"name": "examples",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@types/body-parser": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz",
|
||||
"integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==",
|
||||
"requires": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/connect": {
|
||||
"version": "3.4.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz",
|
||||
"integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/express": {
|
||||
"version": "4.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.0.tgz",
|
||||
"integrity": "sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw==",
|
||||
"requires": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "*",
|
||||
"@types/serve-static": "*"
|
||||
}
|
||||
},
|
||||
"@types/express-serve-static-core": {
|
||||
"version": "4.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.7.tgz",
|
||||
"integrity": "sha512-847KvL8Q1y3TtFLRTXcVakErLJQgdpFSaq+k043xefz9raEf0C7HalpSY7OW5PyjCnY8P7bPW5t/Co9qqp+USg==",
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"@types/range-parser": "*"
|
||||
}
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
|
||||
"integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "12.6.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.8.tgz",
|
||||
"integrity": "sha512-aX+gFgA5GHcDi89KG5keey2zf0WfZk/HAQotEamsK2kbey+8yGKcson0hbK8E+v0NArlCJQCqMP161YhV6ZXLg=="
|
||||
},
|
||||
"@types/range-parser": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
|
||||
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA=="
|
||||
},
|
||||
"@types/serve-static": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz",
|
||||
"integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==",
|
||||
"requires": {
|
||||
"@types/express-serve-static-core": "*",
|
||||
"@types/mime": "*"
|
||||
}
|
||||
},
|
||||
"accepts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
||||
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
|
||||
"requires": {
|
||||
"mime-types": "~2.1.24",
|
||||
"negotiator": "0.6.2"
|
||||
}
|
||||
},
|
||||
"array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
|
||||
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
|
||||
"requires": {
|
||||
"bytes": "3.1.0",
|
||||
"content-type": "~1.0.4",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~1.1.2",
|
||||
"http-errors": "1.7.2",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "~2.3.0",
|
||||
"qs": "6.7.0",
|
||||
"raw-body": "2.4.0",
|
||||
"type-is": "~1.6.17"
|
||||
}
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
|
||||
},
|
||||
"content-disposition": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
|
||||
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.2"
|
||||
}
|
||||
},
|
||||
"content-type": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
|
||||
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
|
||||
},
|
||||
"cookie": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
|
||||
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
|
||||
},
|
||||
"cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
|
||||
},
|
||||
"destroy": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
||||
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
|
||||
},
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
||||
},
|
||||
"encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
|
||||
},
|
||||
"escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
|
||||
},
|
||||
"etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
|
||||
},
|
||||
"express": {
|
||||
"version": "4.17.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
|
||||
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
|
||||
"requires": {
|
||||
"accepts": "~1.3.7",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.19.0",
|
||||
"content-disposition": "0.5.3",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.4.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~1.1.2",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "~1.1.2",
|
||||
"fresh": "0.5.2",
|
||||
"merge-descriptors": "1.0.1",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "~2.3.0",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"proxy-addr": "~2.0.5",
|
||||
"qs": "6.7.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.1.2",
|
||||
"send": "0.17.1",
|
||||
"serve-static": "1.14.1",
|
||||
"setprototypeof": "1.1.1",
|
||||
"statuses": "~1.5.0",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
}
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
|
||||
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "~2.3.0",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "~1.5.0",
|
||||
"unpipe": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"forwarded": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
||||
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
|
||||
},
|
||||
"fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
|
||||
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
|
||||
"requires": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.3",
|
||||
"setprototypeof": "1.1.1",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"ipaddr.js": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
|
||||
"integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA=="
|
||||
},
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
|
||||
},
|
||||
"merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.40.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
|
||||
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.24",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
|
||||
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
|
||||
"requires": {
|
||||
"mime-db": "1.40.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
|
||||
"requires": {
|
||||
"ee-first": "1.1.1"
|
||||
}
|
||||
},
|
||||
"parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
},
|
||||
"proxy-addr": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz",
|
||||
"integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==",
|
||||
"requires": {
|
||||
"forwarded": "~0.1.2",
|
||||
"ipaddr.js": "1.9.0"
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
|
||||
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
|
||||
},
|
||||
"range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
||||
},
|
||||
"raw-body": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
|
||||
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
|
||||
"requires": {
|
||||
"bytes": "3.1.0",
|
||||
"http-errors": "1.7.2",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"send": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
|
||||
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "~1.1.2",
|
||||
"destroy": "~1.0.4",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "~1.7.2",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.1",
|
||||
"on-finished": "~2.3.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "~1.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve-static": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
|
||||
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
|
||||
"requires": {
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.17.1"
|
||||
}
|
||||
},
|
||||
"setprototypeof": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
|
||||
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
|
||||
},
|
||||
"statuses": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
|
||||
},
|
||||
"toidentifier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
|
||||
},
|
||||
"type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"requires": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
}
|
||||
},
|
||||
"unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
|
||||
},
|
||||
"utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
|
||||
},
|
||||
"vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
|
||||
}
|
||||
}
|
||||
}
|
||||
14
examples/package.json
Normal file
14
examples/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "examples",
|
||||
"version": "0.0.0",
|
||||
"description": "Passkit-generator examples",
|
||||
"author": "Alexander P. Cerutti <cerutti.alexander@gmail.com>",
|
||||
"license": "ISC",
|
||||
"scripts": {
|
||||
"build": "cd ..; npm run build; cd examples"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/express": "^4.17.0",
|
||||
"express": "^4.17.1"
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@
|
||||
* Requires express to run
|
||||
*/
|
||||
|
||||
const express = require("express");
|
||||
const app = express();
|
||||
import express from "express";
|
||||
export const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
@@ -23,4 +23,4 @@ app.route("/gen")
|
||||
res.send("Cannot generate a pass. Specify a modelName in the url to continue. <br/>Usage: /gen/<i>modelName</i>")
|
||||
});
|
||||
|
||||
module.exports = app.route("/gen/:modelName");
|
||||
export default app.route("/gen/:modelName");
|
||||
236
index.d.ts
vendored
236
index.d.ts
vendored
@@ -1,9 +1,19 @@
|
||||
import { Stream } from "stream";
|
||||
|
||||
export declare class Pass {
|
||||
constructor(options: Schema.Instance);
|
||||
/**
|
||||
* Creates a new Pass instance.
|
||||
*
|
||||
* @param options Options to be used to create the instance or an Abstract Model reference
|
||||
* @param additionalBuffers More buffers (with file name) to be added on runtime (if you are downloading some files from the web)
|
||||
* @param abstractMissingData Additional data for abstract models, that might vary from pass to pass.
|
||||
*/
|
||||
export declare function createPass(options: Schema.FactoryOptions | AbstractModel, additionalBuffers?: Schema.BundleUnit, abstractMissingData?: Omit<Schema.AbstractFactoryOptions, "model">): Promise<Pass>;
|
||||
|
||||
public transitType: "PKTransitTypeAir" | "PKTransitTypeBoat" | "PKTransitTypeBus" | "PKTransitTypeGeneric" | "PKTransitTypeTrain";
|
||||
|
||||
export declare class Pass {
|
||||
constructor(options: Schema.PassInstance);
|
||||
|
||||
public transitType: Schema.TransitType;
|
||||
public headerFields: Schema.Field[];
|
||||
public primaryFields: Schema.Field[];
|
||||
public secondaryFields: Schema.Field[];
|
||||
@@ -11,72 +21,120 @@ export declare class Pass {
|
||||
public backFields: Schema.Field[];
|
||||
|
||||
/**
|
||||
* Generates a Stream of a zip file using the infos passed through overrides or methods.
|
||||
* (MIME: `application/vnd.apple.pkpass`)
|
||||
* Generates the pass Stream
|
||||
*
|
||||
* @method generate
|
||||
* @return A Stream of the generated pass.
|
||||
*/
|
||||
generate(): Promise<Stream>;
|
||||
generate(): Stream;
|
||||
|
||||
/**
|
||||
* Generates pass.strings translation files in the specified language
|
||||
* @param lang - lang in ISO 3166 alpha-2 format (e.g. `en` or `en-US`);
|
||||
* @param translations - Object in format `{ "placeholder" : "translated-text" }`
|
||||
* @see https://apple.co/2KOv0OW
|
||||
* Adds traslated strings object to the list of translation to be inserted into the pass
|
||||
*
|
||||
* @method localize
|
||||
* @params lang - the ISO 3166 alpha-2 code for the language
|
||||
* @params translations - key/value pairs where key is the
|
||||
* placeholder in pass.json localizable strings
|
||||
* and value the real translated string.
|
||||
* @returns {this}
|
||||
*
|
||||
* @see https://apple.co/2KOv0OW - Passes support localization
|
||||
*/
|
||||
localize(lang: string, translations: Object): this;
|
||||
localize(lang: string, translations?: { [key: string]: string }): this;
|
||||
|
||||
/**
|
||||
* Sets pass expiration date
|
||||
* @param date - A date in the format you want (see "format")
|
||||
* @param format - A custom date format. If `undefined`, the date will be parsed in the following formats: `MM-DD-YYYY`, `MM-DD-YYYY hh:mm:ss`, `DD-MM-YYYY`, `DD-MM-YYYY hh:mm:ss`.
|
||||
* Sets expirationDate property to a W3C-formatted date
|
||||
*
|
||||
* @method expiration
|
||||
* @params date
|
||||
* @returns {this}
|
||||
*/
|
||||
expiration(date: string, format?: string | string[]): this;
|
||||
expiration(date: Date | null): this;
|
||||
|
||||
/** Generates a voided pass. Useful for backend pass updates. */
|
||||
/**
|
||||
* Sets voided property to true
|
||||
*
|
||||
* @method void
|
||||
* @return {this}
|
||||
*/
|
||||
void(): this;
|
||||
|
||||
/**
|
||||
* Sets relevance for pass (conditions to appear in the lockscren).
|
||||
* @param type - must be `beacons`, `locations`, `maxDistance` or `relevantDate`
|
||||
* @param data - if object, will be treated as one-element array
|
||||
* @param relevanceDateFormat - custom format to be used in case of "relevatDate" as type. Otherwise the date will be parsed in the following formats: `MM-DD-YYYY`, `MM-DD-YYYY hh:mm:ss`, `DD-MM-YYYY`, `DD-MM-YYYY hh:mm:ss`.
|
||||
* Sets current pass' relevancy through beacons
|
||||
* @param data
|
||||
* @returns {Pass}
|
||||
*/
|
||||
relevance(type: Schema.RelevanceType, data: string | Schema.Location | Schema.Location[] | Schema.Beacon | Schema.Beacon[], relevanceDateFormat?: string): SuccessfulOperations;
|
||||
beacons(...data: Schema.Beacon[] | null): this;
|
||||
|
||||
/**
|
||||
* Adds barcode to the pass. If data is an Object, will be treated as one-element array.
|
||||
* @param data - data to be used to generate a barcode. If string, Barcode will contain structures for all the supported types and `data` will be used message and altText.
|
||||
* @see https://apple.co/2C74kbm
|
||||
* Sets current pass' relevancy through locations
|
||||
* @param data
|
||||
* @returns {Pass}
|
||||
*/
|
||||
barcode(data: Schema.Barcode | Schema.Barcode[] | string): BarcodeInterfaces;
|
||||
locations(...data: Schema.Location[] | null): this;
|
||||
|
||||
/**
|
||||
* Sets nfc infos for the pass
|
||||
* @param data - NFC data
|
||||
* Sets current pass' relevancy through a date
|
||||
* @param data
|
||||
* @returns {Pass}
|
||||
*/
|
||||
relevantDate(date: Date | null): this;
|
||||
|
||||
/**
|
||||
* Adds barcodes "barcodes" property.
|
||||
* It allows to pass a string to autogenerate all the structures.
|
||||
*
|
||||
* @method barcode
|
||||
* @params first - a structure or the string (message) that will generate
|
||||
* all the barcodes
|
||||
* @params data - other barcodes support
|
||||
* @return {this} Improved this with length property and other methods
|
||||
*/
|
||||
barcodes(first: null | string | Schema.Barcode, ...data: Schema.Barcode[]): this;
|
||||
|
||||
/**
|
||||
* Given an index <= the amount of already set "barcodes",
|
||||
* this let you choose which structure to use for retrocompatibility
|
||||
* property "barcode".
|
||||
*
|
||||
* @method barcode
|
||||
* @params format - the format to be used
|
||||
* @return {this}
|
||||
*/
|
||||
barcode(chosenFormat: Schema.BarcodeFormat | null): this;
|
||||
|
||||
/**
|
||||
* Sets nfc fields in properties
|
||||
*
|
||||
* @method nfc
|
||||
* @params data - the data to be pushed in the pass
|
||||
* @returns {this}
|
||||
* @see https://apple.co/2wTxiaC
|
||||
*/
|
||||
nfc(...data: Schema.NFC[]): this;
|
||||
nfc(data: Schema.NFC | null): this;
|
||||
|
||||
/**
|
||||
* Sets resources to be downloaded right inside
|
||||
* the pass archive.
|
||||
* @param resource - url
|
||||
* @param name - name (or path) to be used inside the archive
|
||||
* @returns this;
|
||||
* Allows to get the current inserted props;
|
||||
* will return all props from valid overrides,
|
||||
* template's pass.json and methods-inserted ones;
|
||||
*
|
||||
* @returns The properties will be inserted in the pass.
|
||||
*/
|
||||
|
||||
load(resource: string, name: string): this;
|
||||
readonly props: Readonly<Schema.ValidPass>;
|
||||
}
|
||||
|
||||
interface BarcodeInterfaces extends BarcodeSuccessfulOperations {
|
||||
autocomplete: () => void | BarcodeSuccessfulOperations
|
||||
}
|
||||
/**
|
||||
* Creates an abstract model to keep data
|
||||
* in memory for future passes creation
|
||||
* @param options
|
||||
*/
|
||||
export declare function createAbstractModel(options: Schema.AbstractFactoryOptions): Promise<AbstractModel>;
|
||||
|
||||
interface BarcodeSuccessfulOperations extends SuccessfulOperations {
|
||||
backward: (format: null | string) => void | ThisType<Pass>
|
||||
}
|
||||
|
||||
interface SuccessfulOperations extends ThisType<Pass> {
|
||||
length: number
|
||||
export declare class AbstractModel {
|
||||
constructor(options: Schema.AbstractModelOptions);
|
||||
readonly certificates: Schema.FinalCertificates;
|
||||
readonly bundle: Schema.PartitionedBundle;
|
||||
readonly overrides: Schema.OverridesSupportedOptions;
|
||||
}
|
||||
|
||||
declare namespace Schema {
|
||||
@@ -85,34 +143,70 @@ declare namespace Schema {
|
||||
type DateTimeStyle = "PKDateStyleNone" | "PKDateStyleShort" | "PKDateStyleMedium" | "PKDateStyleLong" | "PKDateStyleFull";
|
||||
type NumberStyle = "PKNumberStyleDecimal" | "PKNumberStylePercent" | "PKNumberStyleScientific" | "PKNumberStyleSpellOut";
|
||||
type BarcodeFormat = "PKBarcodeFormatQR" | "PKBarcodeFormatPDF417" | "PKBarcodeFormatAztec" | "PKBarcodeFormatCode128";
|
||||
type RelevanceType = "beacons" | "locations" | "maxDistance" | "relevantDate";
|
||||
type SemanticsEventType = "PKEventTypeGeneric" | "PKEventTypeLivePerformance" | "PKEventTypeMovie" | "PKEventTypeSports" | "PKEventTypeConference" | "PKEventTypeConvention" | "PKEventTypeWorkshop" | "PKEventTypeSocialGathering";
|
||||
type TransitType = "PKTransitTypeAir" | "PKTransitTypeBoat" | "PKTransitTypeBus" | "PKTransitTypeGeneric" | "PKTransitTypeTrain";
|
||||
|
||||
interface Instance {
|
||||
model: string;
|
||||
certificates: {
|
||||
interface Certificates {
|
||||
wwdr?: string;
|
||||
signerCert?: string;
|
||||
signerKey?: {
|
||||
keyFile: string;
|
||||
passphrase?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface FactoryOptions {
|
||||
model: BundleUnit | string;
|
||||
certificates: Certificates;
|
||||
overrides?: Object;
|
||||
}
|
||||
|
||||
interface BundleUnit {
|
||||
[key: string]: Buffer;
|
||||
}
|
||||
|
||||
interface PartitionedBundle {
|
||||
bundle: BundleUnit;
|
||||
l10nBundle: {
|
||||
[key: string]: BundleUnit
|
||||
};
|
||||
}
|
||||
|
||||
interface FinalCertificates {
|
||||
wwdr: string;
|
||||
signerCert: string;
|
||||
signerKey: {
|
||||
keyFile: string;
|
||||
passphrase: string;
|
||||
}
|
||||
};
|
||||
overrides: SupportedOptions;
|
||||
shouldOverwrite?: boolean;
|
||||
signerKey: string;
|
||||
}
|
||||
|
||||
interface SupportedOptions {
|
||||
interface AbstractFactoryOptions extends Omit<FactoryOptions, "certificates"> {
|
||||
certificates?: Certificates;
|
||||
}
|
||||
|
||||
interface AbstractModelOptions {
|
||||
bundle: PartitionedBundle;
|
||||
certificates: FinalCertificates;
|
||||
overrides?: OverridesSupportedOptions;
|
||||
}
|
||||
|
||||
interface PassInstance {
|
||||
model: PartitionedBundle;
|
||||
certificates: FinalCertificates;
|
||||
overrides?: OverridesSupportedOptions;
|
||||
}
|
||||
|
||||
interface OverridesSupportedOptions {
|
||||
serialNumber?: string;
|
||||
description?: string;
|
||||
userInfo?: Object | any[];
|
||||
userInfo?: Object | Array<any>;
|
||||
webServiceURL?: string;
|
||||
authenticationToken?: string;
|
||||
sharingProhibited?: boolean;
|
||||
backgroundColor?: string;
|
||||
foregroundColor?: string;
|
||||
labelColor?: string;
|
||||
groupingIdentifier?: string;
|
||||
suppressStripShine?: boolean;
|
||||
maxDistance?: number;
|
||||
}
|
||||
|
||||
interface Field {
|
||||
@@ -132,6 +226,34 @@ declare namespace Schema {
|
||||
semantics?: Semantics;
|
||||
}
|
||||
|
||||
interface PassFields {
|
||||
auxiliaryFields: Field[];
|
||||
backFields: Field[];
|
||||
headerFields: Field[];
|
||||
primaryFields: Field[];
|
||||
secondaryFields: Field[];
|
||||
}
|
||||
|
||||
interface ValidPassType {
|
||||
boardingPass?: PassFields & { transitType: TransitType };
|
||||
eventTicket?: PassFields;
|
||||
coupon?: PassFields;
|
||||
generic?: PassFields;
|
||||
storeCard?: PassFields;
|
||||
}
|
||||
|
||||
interface ValidPass extends OverridesSupportedOptions, ValidPassType {
|
||||
barcode?: Barcode;
|
||||
barcodes?: Barcode[];
|
||||
beacons?: Beacon[];
|
||||
locations?: Location[];
|
||||
maxDistance?: number;
|
||||
relevantDate?: string;
|
||||
nfc?: NFC;
|
||||
expirationDate?: string;
|
||||
voided?: boolean;
|
||||
}
|
||||
|
||||
interface Beacon {
|
||||
major?: number;
|
||||
minor?: number;
|
||||
|
||||
7
index.ts
Normal file
7
index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Pass } from "./src/pass";
|
||||
import { AbstractModel } from "./src/abstract";
|
||||
|
||||
export { createPass } from "./src/factory";
|
||||
export { createAbstractModel } from "./src/abstract";
|
||||
export type Pass = InstanceType<typeof Pass>
|
||||
export type AbstractModel = InstanceType<typeof AbstractModel>
|
||||
604
package-lock.json
generated
604
package-lock.json
generated
@@ -1,301 +1,134 @@
|
||||
{
|
||||
"name": "passkit-generator",
|
||||
"version": "1.6.8",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
|
||||
"integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ=="
|
||||
},
|
||||
"@szmarczak/http-timer": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
|
||||
"integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
|
||||
"requires": {
|
||||
"defer-to-connect": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"archiver": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/archiver/-/archiver-3.0.0.tgz",
|
||||
"integrity": "sha512-5QeR6Xc5hSA9X1rbQfcuQ6VZuUXOaEdB65Dhmk9duuRJHYif/ZyJfuyJqsQrj34PFjU5emv5/MmfgA8un06onw==",
|
||||
"requires": {
|
||||
"archiver-utils": "^2.0.0",
|
||||
"async": "^2.0.0",
|
||||
"buffer-crc32": "^0.2.1",
|
||||
"glob": "^7.0.0",
|
||||
"readable-stream": "^2.0.0",
|
||||
"tar-stream": "^1.5.0",
|
||||
"zip-stream": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"archiver-utils": {
|
||||
"@hapi/address": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-JRBgcVvDX4Mwu2RBF8bBaHcQCSxab7afsxAPYDQ5W+19quIPP5CfKE7Ql+UHs9wYvwsaNR8oDuhtf5iqrKmzww==",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.0.0.tgz",
|
||||
"integrity": "sha512-mV6T0IYqb0xL1UALPFplXYQmR0twnXG0M6jUswpquqT2sD12BOiCiLy3EvMp/Fy7s3DZElC4/aPjEjo2jeZpvw=="
|
||||
},
|
||||
"@hapi/hoek": {
|
||||
"version": "6.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-6.2.4.tgz",
|
||||
"integrity": "sha512-HOJ20Kc93DkDVvjwHyHawPwPkX44sIrbXazAUDiUXaY2R9JwQGo2PhFfnQtdrsIe4igjG2fPgMra7NYw7qhy0A=="
|
||||
},
|
||||
"@hapi/joi": {
|
||||
"version": "15.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.0.tgz",
|
||||
"integrity": "sha512-n6kaRQO8S+kepUTbXL9O/UOL788Odqs38/VOfoCrATDtTvyfiO3fgjlSRaNkHabpTLgM7qru9ifqXlXbXk8SeQ==",
|
||||
"requires": {
|
||||
"glob": "^7.0.0",
|
||||
"graceful-fs": "^4.1.0",
|
||||
"lazystream": "^1.0.0",
|
||||
"lodash.assign": "^4.2.0",
|
||||
"lodash.defaults": "^4.2.0",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"lodash.flatten": "^4.4.0",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.toarray": "^4.4.0",
|
||||
"lodash.union": "^4.6.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"readable-stream": "^2.0.0"
|
||||
"@hapi/address": "2.x.x",
|
||||
"@hapi/hoek": "6.x.x",
|
||||
"@hapi/marker": "1.x.x",
|
||||
"@hapi/topo": "3.x.x"
|
||||
}
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
|
||||
"integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
|
||||
"@hapi/marker": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/marker/-/marker-1.0.0.tgz",
|
||||
"integrity": "sha512-JOfdekTXnJexfE8PyhZFyHvHjt81rBFSAbTIRAhF2vv/2Y1JzoKsGqxH/GpZJoF7aEfYok8JVcAHmSz1gkBieA=="
|
||||
},
|
||||
"@hapi/topo": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.0.tgz",
|
||||
"integrity": "sha512-gZDI/eXOIk8kP2PkUKjWu9RW8GGVd2Hkgjxyr/S7Z+JF+0mr7bAlbw+DkTRxnD580o8Kqxlnba9wvqp5aOHBww==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.10"
|
||||
"@hapi/hoek": "6.x.x"
|
||||
}
|
||||
},
|
||||
"@types/debug": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.4.tgz",
|
||||
"integrity": "sha512-D9MyoQFI7iP5VdpEyPZyjjqIJ8Y8EDNQFIFVLOmeg1rI1xiHOChyUPMPRUVfqFCerxfE+yS3vMyj37F6IdtOoQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/hapi__joi": {
|
||||
"version": "15.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/hapi__joi/-/hapi__joi-15.0.2.tgz",
|
||||
"integrity": "sha512-EsOuX8cbAdSgp/9mo5NoI4vMnZ68c8Jk1fl3tyA07zd9aOq4q4udsJ2/YjhaFw0u2Zp6hBonUBrKEWotZg7PDQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/hapi__joi": "*"
|
||||
}
|
||||
},
|
||||
"@types/jasmine": {
|
||||
"version": "3.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.3.13.tgz",
|
||||
"integrity": "sha512-iczmLoIiVymaD1TIr2UctxjFkNEslVE/QtNAUmpDsD71cZfZBAsPCUv1Y+8AwsfA8bLx2ccr7d95T9w/UAirlQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "12.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.10.tgz",
|
||||
"integrity": "sha512-LcsGbPomWsad6wmMNv7nBLw7YYYyfdYcz6xryKYQhx89c3XXan+8Q6AJ43G5XDIaklaVkK3mE4fCb0SBvMiPSQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node-forge": {
|
||||
"version": "0.8.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.8.4.tgz",
|
||||
"integrity": "sha512-bueB7eD1EUkWaz7SW57QYor6nZSQtH0gJwfcp9kuXhdnypYy6Frnqa73LNXqX41E71ANffBK9EJX+aQH2/eTrQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/yazl": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/yazl/-/yazl-2.4.1.tgz",
|
||||
"integrity": "sha512-uTgQOl6gCKZ6ys5x2BmnNCd/Em8TqCltjPtyHFc1mz8Q6/+Na7yWnoPgCPhsl44M7S6MfaL6spL6pUM1c7NcDg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
|
||||
"integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw=="
|
||||
},
|
||||
"bl": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
|
||||
"integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==",
|
||||
"requires": {
|
||||
"readable-stream": "^2.3.5",
|
||||
"safe-buffer": "^5.1.1"
|
||||
}
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==",
|
||||
"requires": {
|
||||
"base64-js": "^1.0.2",
|
||||
"ieee754": "^1.1.4"
|
||||
}
|
||||
},
|
||||
"buffer-alloc": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
|
||||
"integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
|
||||
"requires": {
|
||||
"buffer-alloc-unsafe": "^1.1.0",
|
||||
"buffer-fill": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"buffer-alloc-unsafe": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
|
||||
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
|
||||
},
|
||||
"buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
|
||||
},
|
||||
"buffer-fill": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
|
||||
"integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw="
|
||||
},
|
||||
"cacheable-request": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.0.0.tgz",
|
||||
"integrity": "sha512-2N7AmszH/WPPpl5Z3XMw1HAP+8d+xugnKQAeKvxFZ/04dbT/CAznqwbl+7eSr3HkwdepNwtb2yx3CAMQWvG01Q==",
|
||||
"requires": {
|
||||
"clone-response": "^1.0.2",
|
||||
"get-stream": "^4.0.0",
|
||||
"http-cache-semantics": "^4.0.0",
|
||||
"keyv": "^3.0.0",
|
||||
"lowercase-keys": "^1.0.1",
|
||||
"normalize-url": "^3.1.0",
|
||||
"responselike": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"clone-response": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
|
||||
"integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
|
||||
"requires": {
|
||||
"mimic-response": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"compress-commons": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz",
|
||||
"integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=",
|
||||
"requires": {
|
||||
"buffer-crc32": "^0.2.1",
|
||||
"crc32-stream": "^2.0.0",
|
||||
"normalize-path": "^2.0.0",
|
||||
"readable-stream": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"normalize-path": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
|
||||
"integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
|
||||
"requires": {
|
||||
"remove-trailing-separator": "^1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"crc": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
|
||||
"integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
|
||||
"requires": {
|
||||
"buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"crc32-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz",
|
||||
"integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=",
|
||||
"requires": {
|
||||
"crc": "^3.4.4",
|
||||
"readable-stream": "^2.0.0"
|
||||
}
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"decompress-response": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
|
||||
"integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
|
||||
"requires": {
|
||||
"mimic-response": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"defer-to-connect": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.0.2.tgz",
|
||||
"integrity": "sha512-k09hcQcTDY+cwgiwa6PYKLm3jlagNzQ+RSvhjzESOGOx+MNOuXkxTfEvPrO1IOQ81tArCFYQgi631clB70RpQw=="
|
||||
},
|
||||
"duplexer3": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
|
||||
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
|
||||
},
|
||||
"end-of-stream": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
|
||||
"integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
|
||||
"requires": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
|
||||
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
|
||||
"requires": {
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"got": {
|
||||
"version": "9.6.0",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
|
||||
"integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
|
||||
"requires": {
|
||||
"@sindresorhus/is": "^0.14.0",
|
||||
"@szmarczak/http-timer": "^1.1.2",
|
||||
"cacheable-request": "^6.0.0",
|
||||
"decompress-response": "^3.3.0",
|
||||
"duplexer3": "^0.1.4",
|
||||
"get-stream": "^4.1.0",
|
||||
"lowercase-keys": "^1.0.1",
|
||||
"mimic-response": "^1.0.1",
|
||||
"p-cancelable": "^1.0.0",
|
||||
"to-readable-stream": "^1.0.0",
|
||||
"url-parse-lax": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
|
||||
"integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA=="
|
||||
},
|
||||
"hoek": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz",
|
||||
"integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w=="
|
||||
},
|
||||
"http-cache-semantics": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz",
|
||||
"integrity": "sha512-TcIMG3qeVLgDr1TEd2XvHaTnMPwYQUQMIBLy+5pLSDKYFc7UIqj39w8EGzZkaxoLv/l2K8HaI0t5AVA+YYgUew=="
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz",
|
||||
"integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA=="
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||
"dev": true
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
@@ -304,20 +137,8 @@
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"isemail": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz",
|
||||
"integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==",
|
||||
"requires": {
|
||||
"punycode": "2.x.x"
|
||||
}
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
|
||||
"dev": true
|
||||
},
|
||||
"jasmine": {
|
||||
"version": "3.4.0",
|
||||
@@ -351,91 +172,11 @@
|
||||
"integrity": "sha512-HU/YxV4i6GcmiH4duATwAbJQMlE0MsDIR5XmSVxURxKHn3aGAdbY1/ZJFmVRbKtnLwIxxMJD7gYaPsypcbYimg==",
|
||||
"dev": true
|
||||
},
|
||||
"joi": {
|
||||
"version": "13.7.0",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz",
|
||||
"integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==",
|
||||
"requires": {
|
||||
"hoek": "5.x.x",
|
||||
"isemail": "3.x.x",
|
||||
"topo": "3.x.x"
|
||||
}
|
||||
},
|
||||
"json-buffer": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
|
||||
"integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg="
|
||||
},
|
||||
"keyv": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
|
||||
"integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
|
||||
"requires": {
|
||||
"json-buffer": "3.0.0"
|
||||
}
|
||||
},
|
||||
"lazystream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz",
|
||||
"integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=",
|
||||
"requires": {
|
||||
"readable-stream": "^2.0.5"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.11",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
|
||||
},
|
||||
"lodash.assign": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
|
||||
"integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc="
|
||||
},
|
||||
"lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
|
||||
},
|
||||
"lodash.difference": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
||||
"integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw="
|
||||
},
|
||||
"lodash.flatten": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
|
||||
},
|
||||
"lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
|
||||
},
|
||||
"lodash.toarray": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
|
||||
"integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE="
|
||||
},
|
||||
"lodash.union": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||
"integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg="
|
||||
},
|
||||
"lowercase-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
|
||||
"integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA=="
|
||||
},
|
||||
"mimic-response": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@@ -446,177 +187,48 @@
|
||||
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "0.7.6",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz",
|
||||
"integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw=="
|
||||
},
|
||||
"normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
|
||||
},
|
||||
"normalize-url": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz",
|
||||
"integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg=="
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz",
|
||||
"integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q=="
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"p-cancelable": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
|
||||
"integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw=="
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||
"dev": true
|
||||
},
|
||||
"prepend-http": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
|
||||
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
|
||||
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"remove-trailing-separator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
|
||||
"integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8="
|
||||
},
|
||||
"responselike": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
|
||||
"integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=",
|
||||
"requires": {
|
||||
"lowercase-keys": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"tar-stream": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz",
|
||||
"integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==",
|
||||
"requires": {
|
||||
"bl": "^1.0.0",
|
||||
"buffer-alloc": "^1.2.0",
|
||||
"end-of-stream": "^1.0.0",
|
||||
"fs-constants": "^1.0.0",
|
||||
"readable-stream": "^2.3.0",
|
||||
"to-buffer": "^1.1.1",
|
||||
"xtend": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"to-buffer": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz",
|
||||
"integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg=="
|
||||
},
|
||||
"to-readable-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q=="
|
||||
},
|
||||
"topo": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz",
|
||||
"integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==",
|
||||
"requires": {
|
||||
"hoek": "6.x.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"hoek": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hoek/-/hoek-6.0.3.tgz",
|
||||
"integrity": "sha512-TU6RyZ/XaQCTWRLrdqZZtZqwxUVr6PDMfi6MlWNURZ7A6czanQqX4pFE1mdOUQR9FdPCsZ0UzL8jI/izZ+eBSQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"url-parse-lax": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
|
||||
"integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
|
||||
"requires": {
|
||||
"prepend-http": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
"typescript": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz",
|
||||
"integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==",
|
||||
"dev": true
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
|
||||
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
|
||||
},
|
||||
"zip-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-c+eUhhkDpaK87G/py74wvWLtz2kzMPNCCkUApkun50ssE0oQliIQzWpTnwjB+MTKVIf2tGzIgHyqW/Y+W77ecQ==",
|
||||
"yazl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz",
|
||||
"integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==",
|
||||
"requires": {
|
||||
"archiver-utils": "^2.0.0",
|
||||
"compress-commons": "^1.2.0",
|
||||
"readable-stream": "^2.0.0"
|
||||
"buffer-crc32": "~0.2.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
package.json
24
package.json
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"name": "passkit-generator",
|
||||
"version": "1.6.8",
|
||||
"version": "2.0.0",
|
||||
"description": "The easiest way to generate custom Apple Wallet passes in Node.js",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "jasmine spec/index.js"
|
||||
"build": "tsc",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "npm run build && jasmine spec/index.js"
|
||||
},
|
||||
"author": "Alexander Patrick Cerutti",
|
||||
"license": "MIT",
|
||||
@@ -17,17 +19,23 @@
|
||||
"Pass"
|
||||
],
|
||||
"dependencies": {
|
||||
"archiver": "^3.0.0",
|
||||
"debug": "^3.2.6",
|
||||
"got": "^9.6.0",
|
||||
"joi": "^13.7.0",
|
||||
"@hapi/joi": "^15.1.0",
|
||||
"debug": "^4.1.1",
|
||||
"moment": "^2.24.0",
|
||||
"node-forge": "^0.7.6"
|
||||
"node-forge": "^0.8.5",
|
||||
"yazl": "^2.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jasmine": "^3.4.0"
|
||||
"@types/debug": "^4.1.4",
|
||||
"@types/hapi__joi": "^15.0.2",
|
||||
"@types/jasmine": "^3.3.13",
|
||||
"@types/node": "^12.0.10",
|
||||
"@types/node-forge": "^0.8.4",
|
||||
"@types/yazl": "^2.4.1",
|
||||
"jasmine": "^3.4.0",
|
||||
"typescript": "^3.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
299
spec/index.js
299
spec/index.js
@@ -1,299 +0,0 @@
|
||||
const Passkit = require("..");
|
||||
|
||||
/*
|
||||
* Yes, I know that I'm checking against "private" properties
|
||||
* and that I shouldn't do that, but there's no other way to check
|
||||
* the final results for each test. The only possible way is to
|
||||
* read the generated stream of the zip file, unzip it
|
||||
* (hopefully in memory) and check each property in pass.json file
|
||||
* and .lproj directories. I hope who is reading this, will understand.
|
||||
*
|
||||
* Tests created upon Jasmine testing suite.
|
||||
*/
|
||||
|
||||
describe("Node-Passkit-generator", function () {
|
||||
let pass;
|
||||
beforeEach(() => {
|
||||
pass = new Passkit.Pass({
|
||||
model: "../examples/examplePass.pass",
|
||||
certificates: {
|
||||
wwdr: "certificates/WWDR.pem",
|
||||
signerCert: "certificates/signerCert.pem",
|
||||
signerKey: {
|
||||
keyFile: "certificates/signerKey.pem",
|
||||
passphrase: "123456"
|
||||
}
|
||||
},
|
||||
overrides: {}
|
||||
});
|
||||
});
|
||||
|
||||
describe("localize()", () => {
|
||||
it("Won't apply changes without at least one parameter", () => {
|
||||
pass.localize();
|
||||
expect(Object.keys(pass.l10n).length).toBe(0);
|
||||
});
|
||||
|
||||
it("Passing first argument not a string, won't apply changes", () => {
|
||||
pass.localize(5);
|
||||
expect(Object.keys(pass.l10n).length).toBe(0);
|
||||
});
|
||||
|
||||
it("Not passing the second argument, will apply changes (.lproj folder inclusion)", () => {
|
||||
pass.localize("en");
|
||||
expect(Object.keys(pass.l10n).length).toBe(1);
|
||||
});
|
||||
|
||||
it("Second argument of type different from object or undefined, won't apply changes.", () => {
|
||||
pass.localize("en", 42);
|
||||
expect(Object.keys(pass.l10n).length).toBe(0);
|
||||
});
|
||||
|
||||
it("A second argument of type object will apply changes", () => {
|
||||
pass.localize("it", {
|
||||
"Test": "Prova"
|
||||
});
|
||||
|
||||
expect(typeof pass.l10n["it"]).toBe("object");
|
||||
expect(pass.l10n["it"]["Test"]).toBe("Prova");
|
||||
});
|
||||
});
|
||||
|
||||
describe("expiration()", () => {
|
||||
it("Missing first argument or not a string won't apply changes", () => {
|
||||
pass.expiration();
|
||||
expect(pass._props["expirationDate"]).toBe(undefined);
|
||||
pass.expiration(42);
|
||||
expect(pass._props["expirationDate"]).toBe(undefined);
|
||||
});
|
||||
|
||||
it("A date with defined format DD-MM-YYYY will apply changes", () => {
|
||||
pass.expiration("10-04-2021", "DD-MM-YYYY");
|
||||
// this is made to avoid problems with winter and summer time:
|
||||
// we focus only on the date and time for the tests.
|
||||
let noTimeZoneDateTime = pass._props["expirationDate"].split("+")[0];
|
||||
expect(noTimeZoneDateTime).toBe("2021-04-10T00:00:00");
|
||||
});
|
||||
|
||||
it("A date with undefined custom format, will apply changes", () => {
|
||||
pass.expiration("10-04-2021");
|
||||
// this is made to avoid problems with winter and summer time:
|
||||
// we focus only on the date and time for the tests.
|
||||
let noTimeZoneDateTime = pass._props["expirationDate"].split("+")[0];
|
||||
expect(noTimeZoneDateTime).toBe("2021-10-04T00:00:00");
|
||||
});
|
||||
|
||||
it("A date with defined format but with slashes will apply changes", () => {
|
||||
pass.expiration("10/04/2021", "DD-MM-YYYY");
|
||||
// this is made to avoid problems with winter and summer time:
|
||||
// we focus only on the date and time for the tests.
|
||||
let noTimeZoneDateTime = pass._props["expirationDate"].split("+")[0];
|
||||
expect(noTimeZoneDateTime).toBe("2021-04-10T00:00:00");
|
||||
});
|
||||
|
||||
it("A date as a Date object will apply changes", () => {
|
||||
pass.expiration(new Date(2020,5,1,0,0,0));
|
||||
// this is made to avoid problems with winter and summer time:
|
||||
// we focus only on the date and time for the tests.
|
||||
let noTimeZoneDateTime = pass._props["expirationDate"].split("+")[0];
|
||||
expect(noTimeZoneDateTime).toBe("2020-06-01T00:00:00");
|
||||
});
|
||||
|
||||
it("An invalid date, will not apply changes", () => {
|
||||
pass.expiration("32/18/228317");
|
||||
expect(pass._props["expirationDate"]).toBe(undefined);
|
||||
|
||||
pass.expiration("32/18/228317", "DD-MM-YYYY");
|
||||
expect(pass._props["expirationDate"]).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("relevance()", () => {
|
||||
describe("relevance('relevantDate')", () => {
|
||||
it("A date with defined format DD-MM-YYYY will apply changes", () => {
|
||||
pass.relevance("relevantDate", "10-04-2021", "DD-MM-YYYY");
|
||||
// this is made to avoid problems with winter and summer time:
|
||||
// we focus only on the date and time for the tests.
|
||||
let noTimeZoneDateTime = pass._props["relevantDate"].split("+")[0];
|
||||
expect(noTimeZoneDateTime).toBe("2021-04-10T00:00:00");
|
||||
});
|
||||
|
||||
it("A date with undefined custom format, will apply changes", () => {
|
||||
pass.relevance("relevantDate", "10-04-2021");
|
||||
// this is made to avoid problems with winter and summer time:
|
||||
// we focus only on the date and time for the tests.
|
||||
let noTimeZoneDateTime = pass._props["relevantDate"].split("+")[0];
|
||||
expect(noTimeZoneDateTime).toBe("2021-10-04T00:00:00");
|
||||
});
|
||||
|
||||
it("A date with defined format but with slashes will apply changes", () => {
|
||||
pass.relevance("relevantDate", "10/04/2021", "DD-MM-YYYY");
|
||||
// this is made to avoid problems with winter and summer time:
|
||||
// we focus only on the date and time for the tests.
|
||||
let noTimeZoneDateTime = pass._props["relevantDate"].split("+")[0];
|
||||
expect(noTimeZoneDateTime).toBe("2021-04-10T00:00:00");
|
||||
});
|
||||
|
||||
it("A date as a Date object will apply changes", () => {
|
||||
pass.relevance("relevantDate",new Date(2020,5,1,0,0,0));
|
||||
// this is made to avoid problems with winter and summer time:
|
||||
// we focus only on the date and time for the tests.
|
||||
let noTimeZoneDateTime = pass._props["relevantDate"].split("+")[0];
|
||||
expect(noTimeZoneDateTime).toBe("2020-06-01T00:00:00");
|
||||
});
|
||||
});
|
||||
|
||||
describe("relevance('maxDistance')", () => {
|
||||
it("A string is accepted and converted to Number", () => {
|
||||
pass.relevance("maxDistance", "150");
|
||||
expect(pass._props["maxDistance"]).toBe(150);
|
||||
});
|
||||
|
||||
it("A number is accepeted and will apply changes", () => {
|
||||
pass.relevance("maxDistance", 150);
|
||||
expect(pass._props["maxDistance"]).toBe(150);
|
||||
});
|
||||
|
||||
it("Passing NaN value won't apply changes", () => {
|
||||
pass.relevance("maxDistance", NaN);
|
||||
expect(pass._props["maxDistance"]).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("relevance('locations') && relevance('beacons')", () => {
|
||||
it("A one-Invalid-schema location won't apply changes", () => {
|
||||
pass.relevance("locations", [{
|
||||
"ibrupofene": "no",
|
||||
"longitude": 0.00000000
|
||||
}]);
|
||||
|
||||
expect(pass._props["locations"]).toBe(undefined);
|
||||
});
|
||||
|
||||
it("A two locations, with one invalid, will be filtered", () => {
|
||||
pass.relevance("locations", [{
|
||||
"ibrupofene": "no",
|
||||
"longitude": 0.00000000
|
||||
}, {
|
||||
"longitude": 4.42634523,
|
||||
"latitude": 5.344233323352
|
||||
}]);
|
||||
|
||||
expect(pass._props["locations"].length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("barcode()", () => {
|
||||
it("Missing data will won't apply changes", () => {
|
||||
pass.barcode();
|
||||
|
||||
expect(pass._props["barcode"]).toBe(undefined);
|
||||
expect(pass._props["barcodes"]).toBe(undefined);
|
||||
});
|
||||
|
||||
it("Boolean parameter won't apply changes", () => {
|
||||
pass.barcode(true);
|
||||
|
||||
expect(pass._props["barcode"]).toBe(undefined);
|
||||
expect(pass._props["barcodes"]).toBe(undefined);
|
||||
});
|
||||
|
||||
it("Numeric parameter won't apply changes", () => {
|
||||
pass.barcode(42);
|
||||
|
||||
expect(pass._props["barcode"]).toBe(undefined);
|
||||
expect(pass._props["barcodes"]).toBe(undefined);
|
||||
});
|
||||
|
||||
it("String parameter will autogenerate all the objects", () => {
|
||||
pass.barcode("28363516282");
|
||||
|
||||
expect(pass._props["barcode"] instanceof Object).toBe(true);
|
||||
expect(pass._props["barcode"].message).toBe("28363516282");
|
||||
expect(pass._props["barcodes"].length).toBe(4);
|
||||
});
|
||||
|
||||
it("Object parameter will be automatically converted to one-element Array", () => {
|
||||
pass.barcode({
|
||||
message: "28363516282",
|
||||
format: "PKBarcodeFormatPDF417",
|
||||
messageEncoding: "utf8"
|
||||
});
|
||||
|
||||
expect(pass._props["barcode"] instanceof Object).toBe(true);
|
||||
expect(pass._props["barcode"].format).toBe("PKBarcodeFormatPDF417");
|
||||
expect(pass._props["barcodes"].length).toBe(1);
|
||||
});
|
||||
|
||||
it("Array parameter will apply changes", () => {
|
||||
pass.barcode({
|
||||
message: "28363516282",
|
||||
format: "PKBarcodeFormatPDF417",
|
||||
messageEncoding: "utf8"
|
||||
});
|
||||
|
||||
expect(pass._props["barcode"] instanceof Object).toBe(true);
|
||||
expect(pass._props["barcode"].format).toBe("PKBarcodeFormatPDF417");
|
||||
expect(pass._props["barcodes"].length).toBe(1);
|
||||
});
|
||||
|
||||
it("Missing messageEncoding gets automatically added.", () => {
|
||||
pass.barcode([{
|
||||
message: "28363516282",
|
||||
format: "PKBarcodeFormatPDF417",
|
||||
}]);
|
||||
|
||||
expect(pass._props["barcode"] instanceof Object).toBe(true);
|
||||
expect(pass._props["barcode"].messageEncoding).toBe("iso-8859-1");
|
||||
expect(pass._props["barcodes"][0].messageEncoding).toBe("iso-8859-1");
|
||||
});
|
||||
|
||||
it("Object without message property, will be filtered out", () => {
|
||||
pass.barcode([{
|
||||
format: "PKBarcodeFormatPDF417",
|
||||
}]);
|
||||
|
||||
expect(pass._props["barcode"]).toBe(undefined);
|
||||
expect(pass._props["barcodes"]).toBe(undefined);
|
||||
});
|
||||
|
||||
it("Array containing non-object elements will be filtered out", () => {
|
||||
pass.barcode([5, 10, 15, {
|
||||
message: "28363516282",
|
||||
format: "PKBarcodeFormatPDF417"
|
||||
}, 7, 1]);
|
||||
|
||||
expect(pass._props["barcode"] instanceof Object).toBe(true);
|
||||
expect(pass._props["barcodes"].length).toBe(1);
|
||||
expect(pass._props["barcodes"][0] instanceof Object).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("barcode().backward()", () => {
|
||||
it("Passing argument of type different from string or null, won't apply changes", function () {
|
||||
pass
|
||||
.barcode("Message-22645272183")
|
||||
.backward(5);
|
||||
|
||||
// unchanged
|
||||
expect(pass._props["barcode"].format).toBe("PKBarcodeFormatQR");
|
||||
});
|
||||
|
||||
it("Null will delete backward support", () => {
|
||||
pass
|
||||
.barcode("Message-22645272183")
|
||||
.backward(null);
|
||||
|
||||
expect(pass._props["barcode"]).toBe(undefined);
|
||||
});
|
||||
|
||||
it("Unknown format won't apply changes", () => {
|
||||
pass
|
||||
.barcode("Message-22645272183")
|
||||
.backward("PKBingoBongoFormat");
|
||||
|
||||
expect(pass._props["barcode"].format).toBe("PKBarcodeFormatQR");
|
||||
});
|
||||
});
|
||||
});
|
||||
298
spec/index.ts
Normal file
298
spec/index.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
import { createPass } from "..";
|
||||
|
||||
// This is used to extract the type of a Promise (like Promise<Pass> => Pass)
|
||||
// found here: https://medium.com/@curtistatewilkinson/this-can-be-done-using-conditional-types-like-so-633cf9787c8b
|
||||
type Unpacked<T> = T extends Promise<infer U> ? U : T;
|
||||
|
||||
/**
|
||||
* Tests created upon Jasmine testing suite.
|
||||
*/
|
||||
|
||||
describe("Node-Passkit-generator", function () {
|
||||
let pass: Unpacked<ReturnType<typeof createPass>>;
|
||||
beforeEach(async () => {
|
||||
pass = await createPass({
|
||||
model: "examples/models/examplePass.pass",
|
||||
certificates: {
|
||||
wwdr: "certificates/WWDR.pem",
|
||||
signerCert: "certificates/signerCert.pem",
|
||||
signerKey: {
|
||||
keyFile: "certificates/signerKey.pem",
|
||||
passphrase: "123456"
|
||||
}
|
||||
},
|
||||
overrides: {}
|
||||
});
|
||||
});
|
||||
|
||||
describe("localize()", () => {
|
||||
it("Won't apply changes without at least one parameter", () => {
|
||||
// @ts-ignore -- Ignoring for test purposes
|
||||
pass.localize();
|
||||
expect(Object.keys(pass.l10nTranslations).length).toBe(0);
|
||||
});
|
||||
|
||||
it("Passing first argument not a string, won't apply changes", () => {
|
||||
// @ts-ignore -- Ignoring for test purposes
|
||||
pass.localize(5);
|
||||
expect(Object.keys(pass.l10nTranslations).length).toBe(0);
|
||||
});
|
||||
|
||||
it("Not passing the second argument, will apply changes (.lproj folder inclusion)", () => {
|
||||
pass.localize("en");
|
||||
expect(Object.keys(pass.l10nTranslations).length).toBe(1);
|
||||
});
|
||||
|
||||
it("Second argument of type different from object or undefined, won't apply changes.", () => {
|
||||
// @ts-ignore -- Ignoring for test purposes
|
||||
pass.localize("en", 42);
|
||||
expect(Object.keys(pass.l10nTranslations).length).toBe(0);
|
||||
});
|
||||
|
||||
it("A second argument of type object will apply changes", () => {
|
||||
pass.localize("it", {
|
||||
"Test": "Prova"
|
||||
});
|
||||
|
||||
expect(typeof pass.l10nTranslations["it"]).toBe("object");
|
||||
expect(pass.l10nTranslations["it"]["Test"]).toBe("Prova");
|
||||
});
|
||||
});
|
||||
|
||||
describe("expiration()", () => {
|
||||
it("Missing first argument or not a string won't apply changes", () => {
|
||||
// @ts-ignore -- Ignoring for test purposes
|
||||
pass.expiration();
|
||||
expect(pass.props["expirationDate"]).toBe(undefined);
|
||||
// @ts-ignore -- Ignoring for test purposes
|
||||
pass.expiration(42);
|
||||
expect(pass.props["expirationDate"]).toBe(undefined);
|
||||
});
|
||||
|
||||
it("A date as a Date object will apply changes", () => {
|
||||
pass.expiration(new Date(2020,5,1,0,0,0));
|
||||
// this is made to avoid problems with winter and summer time:
|
||||
// we focus only on the date and time for the tests.
|
||||
let noTimeZoneDateTime = pass.props["expirationDate"].split("+")[0];
|
||||
expect(noTimeZoneDateTime).toBe("2020-06-01T00:00:00");
|
||||
});
|
||||
|
||||
it("An invalid date, will not apply changes", () => {
|
||||
// @ts-ignore -- Ignoring for test purposes
|
||||
pass.expiration("32/18/228317");
|
||||
expect(pass.props["expirationDate"]).toBe(undefined);
|
||||
|
||||
// @ts-ignore -- Ignoring for test purposes
|
||||
pass.expiration("32/18/228317");
|
||||
expect(pass.props["expirationDate"]).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Relevancy:", () => {
|
||||
describe("Relevant Date", () => {
|
||||
it("A date object will apply changes", () => {
|
||||
pass.relevantDate(new Date("10-04-2021"));
|
||||
// this is made to avoid problems with winter and summer time:
|
||||
// we focus only on the date and time for the tests.
|
||||
let noTimeZoneDateTime = pass.props["relevantDate"].split("+")[0];
|
||||
expect(noTimeZoneDateTime).toBe("2021-10-04T00:00:00");
|
||||
});
|
||||
});
|
||||
|
||||
describe("locations :: ", () => {
|
||||
it("One-Invalid-schema location won't apply changes", () => {
|
||||
const props = pass.props["locations"] || [];
|
||||
const oldAmountOfLocations = props && props.length || 0;
|
||||
|
||||
pass.locations({
|
||||
// @ts-ignore
|
||||
"ibrupofene": "no",
|
||||
"longitude": 0.00000000
|
||||
}, ...props);
|
||||
|
||||
if (oldAmountOfLocations) {
|
||||
expect(pass.props["locations"].length).toBe(oldAmountOfLocations);
|
||||
} else {
|
||||
expect(pass.props["locations"]).toBe(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
it("Two locations, with one invalid, will be filtered", () => {
|
||||
const props = pass.props["locations"] || [];
|
||||
const oldAmountOfLocations = props && props.length || 0;
|
||||
|
||||
pass.locations({
|
||||
//@ts-ignore
|
||||
"ibrupofene": "no",
|
||||
"longitude": 0.00000000
|
||||
}, {
|
||||
"longitude": 4.42634523,
|
||||
"latitude": 5.344233323352
|
||||
}, ...(pass.props["locations"] || []));
|
||||
|
||||
expect(pass.props["locations"].length).toBe((oldAmountOfLocations || 0) + 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Beacons :: ", () => {
|
||||
it("One-Invalid-schema beacon data won't apply changes", () => {
|
||||
const props = pass.props["beacons"] || [];
|
||||
const oldAmountOfBeacons = props && props.length || 0;
|
||||
|
||||
pass.beacons({
|
||||
// @ts-ignore
|
||||
"ibrupofene": "no",
|
||||
"major": 55,
|
||||
"minor": 0,
|
||||
"proximityUUID": "2707c5f4-deb9-48ff-b760-671bc885b6a7"
|
||||
}, ...props);
|
||||
|
||||
if (oldAmountOfBeacons) {
|
||||
expect(pass.props["beacons"].length).toBe(oldAmountOfBeacons);
|
||||
} else {
|
||||
expect(pass.props["beacons"]).toBe(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
it("Two beacons sets, with one invalid, will be filtered out", () => {
|
||||
const props = pass.props["beacons"] || [];
|
||||
const oldAmountOfBeacons = props && props.length || 0;
|
||||
|
||||
pass.beacons({
|
||||
"major": 55,
|
||||
"minor": 0,
|
||||
"proximityUUID": "59da0f96-3fb5-43aa-9028-2bc796c3d0c5"
|
||||
}, {
|
||||
"major": 55,
|
||||
"minor": 0,
|
||||
"proximityUUID": "fdcbbf48-a4ae-4ffb-9200-f8a373c5c18e",
|
||||
// @ts-ignore
|
||||
"animal": "Monkey"
|
||||
}, ...props);
|
||||
|
||||
|
||||
expect(pass.props["beacons"].length).toBe(oldAmountOfBeacons + 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("barcodes()", () => {
|
||||
it("Missing data will left situation unchanged", () => {
|
||||
const props = pass.props["barcodes"] || [];
|
||||
const oldAmountOfBarcodes = props && props.length || 0;
|
||||
|
||||
// @ts-ignore - Ignoring for test purposes
|
||||
pass.barcodes();
|
||||
expect(pass.props["barcodes"].length).toBe(oldAmountOfBarcodes);
|
||||
});
|
||||
|
||||
it("Boolean parameter won't apply changes", () => {
|
||||
const props = pass.props["barcodes"] || [];
|
||||
const oldAmountOfBarcodes = props && props.length || 0;
|
||||
|
||||
// @ts-ignore -- Ignoring for test purposes
|
||||
pass.barcode(true);
|
||||
expect(props.length).toBe(oldAmountOfBarcodes);
|
||||
});
|
||||
|
||||
it("Numeric parameter won't apply changes", () => {
|
||||
const props = pass.props["barcodes"] || [];
|
||||
const oldAmountOfBarcodes = props && props.length || 0;
|
||||
|
||||
// @ts-ignore -- Ignoring for test purposes
|
||||
pass.barcodes(42);
|
||||
expect(pass.props["barcodes"].length).toBe(oldAmountOfBarcodes);
|
||||
});
|
||||
|
||||
it("String parameter will autogenerate all the objects", () => {
|
||||
pass.barcodes("28363516282");
|
||||
expect(pass.props["barcodes"].length).toBe(4);
|
||||
});
|
||||
|
||||
it("Object parameter will be accepted", () => {
|
||||
pass.barcodes({
|
||||
message: "28363516282",
|
||||
format: "PKBarcodeFormatPDF417",
|
||||
messageEncoding: "utf8"
|
||||
});
|
||||
|
||||
expect(pass.props["barcodes"].length).toBe(1);
|
||||
});
|
||||
|
||||
it("Array parameter will apply changes", () => {
|
||||
pass.barcodes({
|
||||
message: "28363516282",
|
||||
format: "PKBarcodeFormatPDF417",
|
||||
messageEncoding: "utf8"
|
||||
});
|
||||
|
||||
expect(pass.props["barcodes"].length).toBe(1);
|
||||
});
|
||||
|
||||
it("Missing messageEncoding gets automatically added.", () => {
|
||||
pass.barcodes({
|
||||
message: "28363516282",
|
||||
format: "PKBarcodeFormatPDF417",
|
||||
});
|
||||
|
||||
expect(pass.props["barcodes"][0].messageEncoding).toBe("iso-8859-1");
|
||||
});
|
||||
|
||||
it("Object without message property, will be ignored", () => {
|
||||
const props = pass.props["barcodes"] || [];
|
||||
const oldAmountOfBarcodes = props && props.length || 0;
|
||||
|
||||
// @ts-ignore -- Ignoring for test purposes
|
||||
pass.barcodes({
|
||||
format: "PKBarcodeFormatPDF417",
|
||||
});
|
||||
|
||||
expect(pass.props["barcodes"].length).toBe(oldAmountOfBarcodes);
|
||||
});
|
||||
|
||||
it("Array containing non-object elements will be rejected", () => {
|
||||
// @ts-ignore -- Ignoring for test purposes
|
||||
pass.barcodes(5, 10, 15, {
|
||||
message: "28363516282",
|
||||
format: "PKBarcodeFormatPDF417"
|
||||
}, 7, 1);
|
||||
|
||||
expect(pass.props["barcodes"].length).toBe(1)
|
||||
});
|
||||
});
|
||||
|
||||
describe("barcode retrocompatibility", () => {
|
||||
it("Passing argument of type different from string or null, won't apply changes", function () {
|
||||
const oldBarcode = pass.props["barcode"] || undefined;
|
||||
|
||||
pass
|
||||
.barcodes("Message-22645272183")
|
||||
// @ts-ignore -- Ignoring for test purposes
|
||||
.barcode(55)
|
||||
|
||||
// unchanged
|
||||
expect(pass.props["barcode"]).toEqual(oldBarcode);
|
||||
});
|
||||
|
||||
it("Null will delete backward support", () => {
|
||||
pass.barcodes("Message-22645272183")
|
||||
.barcode("PKBarcodeFormatAztec");
|
||||
|
||||
expect(pass.props["barcode"].format).toBe("PKBarcodeFormatAztec");
|
||||
|
||||
pass.barcode(null);
|
||||
expect(pass.props["barcode"]).toBe(undefined);
|
||||
});
|
||||
|
||||
it("Unknown format won't apply changes", () => {
|
||||
const oldBarcode = pass.props["barcode"] || undefined;
|
||||
|
||||
pass
|
||||
.barcodes("Message-22645272183")
|
||||
// @ts-ignore -- Ignoring for test purposes
|
||||
.barcode("PKBingoBongoFormat");
|
||||
|
||||
expect(pass.props["barcode"]).toEqual(oldBarcode);
|
||||
});
|
||||
});
|
||||
});
|
||||
69
src/abstract.ts
Normal file
69
src/abstract.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
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) {
|
||||
console.log(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];
|
||||
}
|
||||
}
|
||||
73
src/factory.ts
Normal file
73
src/factory.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Pass } from "./pass";
|
||||
import { FactoryOptions, BundleUnit, FinalCertificates } from "./schema";
|
||||
import formatMessage from "./messages";
|
||||
import { getModelContents, readCertificatesFromOptions } from "./parser";
|
||||
import { splitBufferBundle } from "./utils";
|
||||
import { AbstractModel, AbstractFactoryOptions } from "./abstract";
|
||||
|
||||
/**
|
||||
* Creates a new Pass instance.
|
||||
*
|
||||
* @param options Options to be used to create the instance or an Abstract Model reference
|
||||
* @param additionalBuffers More buffers (with file name) to be added on runtime (if you are downloading some files from the web)
|
||||
* @param abstractMissingData Additional data for abstract models, that might vary from pass to pass.
|
||||
*/
|
||||
|
||||
export async function createPass(
|
||||
options: FactoryOptions | AbstractModel,
|
||||
additionalBuffers?: BundleUnit,
|
||||
abstractMissingData?: Omit<AbstractFactoryOptions, "model">
|
||||
): Promise<Pass> {
|
||||
if (!(options && (options instanceof AbstractModel || Object.keys(options).length))) {
|
||||
throw new Error(formatMessage("CP_NO_OPTS"));
|
||||
}
|
||||
|
||||
try {
|
||||
if (options instanceof AbstractModel) {
|
||||
let certificates: FinalCertificates;
|
||||
|
||||
if (!(options.certificates && options.certificates.signerCert && options.certificates.signerKey) && abstractMissingData.certificates) {
|
||||
certificates = Object.assign(
|
||||
options.certificates,
|
||||
await readCertificatesFromOptions(abstractMissingData.certificates)
|
||||
);
|
||||
} else {
|
||||
certificates = options.certificates;
|
||||
}
|
||||
|
||||
if (additionalBuffers) {
|
||||
const [ additionalL10n, additionalBundle ] = splitBufferBundle(additionalBuffers);
|
||||
Object.assign(options.bundle["l10nBundle"], additionalL10n);
|
||||
Object.assign(options.bundle["bundle"], additionalBundle);
|
||||
}
|
||||
|
||||
return new Pass({
|
||||
model: options.bundle,
|
||||
certificates: certificates,
|
||||
overrides: {
|
||||
...(options.overrides || {}),
|
||||
...(abstractMissingData && abstractMissingData.overrides || {})
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const [bundle, certificates] = await Promise.all([
|
||||
getModelContents(options.model),
|
||||
readCertificatesFromOptions(options.certificates)
|
||||
]);
|
||||
|
||||
if (additionalBuffers) {
|
||||
const [ additionalL10n, additionalBundle ] = splitBufferBundle(additionalBuffers);
|
||||
Object.assign(bundle["l10nBundle"], additionalL10n);
|
||||
Object.assign(bundle["bundle"], additionalBundle);
|
||||
}
|
||||
|
||||
return new Pass({
|
||||
model: bundle,
|
||||
certificates,
|
||||
overrides: options.overrides
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(formatMessage("CP_INIT_ERROR", "pass", err));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
const schema = require("./schema");
|
||||
const debug = require("debug")("passkit:fields");
|
||||
import * as schema from "./schema";
|
||||
import debug from "debug";
|
||||
|
||||
const fieldsDebug = debug("passkit:fields");
|
||||
|
||||
/**
|
||||
* Class to represent lower-level keys pass fields
|
||||
@@ -8,9 +10,8 @@ const debug = require("debug")("passkit:fields");
|
||||
|
||||
const poolSymbol = Symbol("pool");
|
||||
|
||||
class FieldsArray extends Array {
|
||||
|
||||
constructor(pool,...args) {
|
||||
export default class FieldsArray extends Array {
|
||||
constructor(pool: Set<string>, ...args: any[]) {
|
||||
super(...args);
|
||||
this[poolSymbol] = pool;
|
||||
}
|
||||
@@ -20,14 +21,14 @@ class FieldsArray extends Array {
|
||||
* also uniqueKeys set.
|
||||
*/
|
||||
|
||||
push(...fieldsData) {
|
||||
const validFields = fieldsData.reduce((acc, current) => {
|
||||
push(...fieldsData: schema.Field[]): number {
|
||||
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)) {
|
||||
debug(`Field with key "${current.key}" discarded: fields must be unique in pass scope.`);
|
||||
fieldsDebug(`Field with key "${current.key}" discarded: fields must be unique in pass scope.`);
|
||||
} else {
|
||||
this[poolSymbol].add(current.key);
|
||||
acc.push(current);
|
||||
@@ -44,9 +45,9 @@ class FieldsArray extends Array {
|
||||
* also uniqueKeys set
|
||||
*/
|
||||
|
||||
pop() {
|
||||
const element = Array.prototype.pop.call(this);
|
||||
this[poolSymbol].delete(element.key)
|
||||
pop(): schema.Field {
|
||||
const element: schema.Field = Array.prototype.pop.call(this);
|
||||
this[poolSymbol].delete(element.key);
|
||||
return element;
|
||||
}
|
||||
|
||||
@@ -55,16 +56,14 @@ class FieldsArray extends Array {
|
||||
* also uniqueKeys set
|
||||
*/
|
||||
|
||||
splice(start, deleteCount, ...items) {
|
||||
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));
|
||||
|
||||
return Array.prototype.splice.call(this, start, deleteCount, items);
|
||||
}
|
||||
|
||||
get length() {
|
||||
get length(): number {
|
||||
return this.length;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FieldsArray;
|
||||
@@ -1,9 +1,17 @@
|
||||
const errors = {
|
||||
interface MessageGroup {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const errors: MessageGroup = {
|
||||
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.\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_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_CERT_PATH: "Invalid certificate loaded. %s does not exist.",
|
||||
TRSTYPE_REQUIRED: "Cannot proceed with pass creation. transitType field is required for boardingPasses.",
|
||||
@@ -11,16 +19,17 @@ const errors = {
|
||||
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 = {
|
||||
const debugMessages: MessageGroup = {
|
||||
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 or an object with no message field",
|
||||
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.",
|
||||
LOAD_TYPES_UNMATCH: "Resource and name are not valid strings. No action will be taken for the specified medias.",
|
||||
LOAD_MIME: "Picture MIME-type: %s",
|
||||
LOAD_NORES: "Was not able to fetch resource %s. Error: %s"
|
||||
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."
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -29,7 +38,7 @@ const debugMessages = {
|
||||
* @param {any[]} values
|
||||
*/
|
||||
|
||||
function format(messageName, ...values) {
|
||||
export default function format(messageName: string, ...values: any[]) {
|
||||
// reversing because it is better popping than shifting.
|
||||
let replaceValues = values.reverse();
|
||||
return resolveMessageName(messageName).replace(/%s/g, () => {
|
||||
@@ -43,12 +52,10 @@ function format(messageName, ...values) {
|
||||
* @param {string} name
|
||||
*/
|
||||
|
||||
function resolveMessageName(name) {
|
||||
function resolveMessageName(name: string): string {
|
||||
if (!errors[name] && !debugMessages[name]) {
|
||||
return `<ErrorName "${name}" is not linked to any error messages>`;
|
||||
}
|
||||
|
||||
return errors[name] || debugMessages[name];
|
||||
}
|
||||
|
||||
module.exports = format;
|
||||
306
src/parser.ts
Normal file
306
src/parser.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
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 { promisify } from "util";
|
||||
import { readFile as _readFile, readdir as _readdir } from "fs";
|
||||
import debug from "debug";
|
||||
|
||||
const prsDebug = debug("Personalization");
|
||||
const readDir = promisify(_readdir);
|
||||
const readFile = promisify(_readFile);
|
||||
|
||||
/**
|
||||
* Performs checks on the passed model to
|
||||
* determine how to parse it
|
||||
* @param model
|
||||
*/
|
||||
|
||||
export async function getModelContents(model: FactoryOptions["model"]) {
|
||||
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;
|
||||
|
||||
if (typeof model === "string") {
|
||||
modelContents = await getModelFolderContents(model);
|
||||
} else {
|
||||
modelContents = getModelBufferContents(model);
|
||||
}
|
||||
|
||||
const modelFiles = Object.keys(modelContents.bundle);
|
||||
const isModelInitialized = (
|
||||
modelFiles.includes("pass.json") &&
|
||||
hasFilesWithName("icon", modelFiles, "startsWith")
|
||||
);
|
||||
|
||||
if (!isModelInitialized) {
|
||||
throw new Error(formatMessage("MODEL_UNINITIALIZED", "parse result"));
|
||||
}
|
||||
|
||||
// ======================= //
|
||||
// *** Personalization *** //
|
||||
// ======================= //
|
||||
|
||||
const personalizationJsonFile = "personalization.json";
|
||||
|
||||
if (!modelFiles.includes(personalizationJsonFile)) {
|
||||
return modelContents;
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
if (!isPersonalizationValid) {
|
||||
[...logoFullNames, personalizationJsonFile]
|
||||
.forEach(file => delete modelContents.bundle[file]);
|
||||
|
||||
return modelContents;
|
||||
}
|
||||
} catch (err) {
|
||||
prsDebug(formatMessage("PRS_INVALID", err));
|
||||
deletePersonalization(modelContents.bundle, logoFullNames);
|
||||
}
|
||||
|
||||
return modelContents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and model contents and creates a splitted
|
||||
* bundles-object.
|
||||
* @param model
|
||||
*/
|
||||
|
||||
export async function getModelFolderContents(model: string): Promise<PartitionedBundle> {
|
||||
try {
|
||||
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")
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
const fileComponents = file.split(path.sep);
|
||||
const fileName = fileComponents[fileComponents.length-1];
|
||||
|
||||
return { ...acc, [fileName]: 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes the passed buffer model and splits it to
|
||||
* return buffers and localization files buffers.
|
||||
* @param model
|
||||
*/
|
||||
|
||||
export function getModelBufferContents(model: BundleUnit): PartitionedBundle {
|
||||
const rawBundle = removeHidden(Object.keys(model)).reduce<BundleUnit>((acc, current) => {
|
||||
// Checking if current file is one of the autogenerated ones or if its
|
||||
// content is not available
|
||||
|
||||
if (/(manifest|signature)/.test(current) || !model[current]) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return { ...acc, [current]: model[current] };
|
||||
}, {});
|
||||
|
||||
const bundleKeys = Object.keys(rawBundle);
|
||||
|
||||
const isModelInitialized = (
|
||||
bundleKeys.length &&
|
||||
hasFilesWithName("icon", bundleKeys, "startsWith")
|
||||
);
|
||||
|
||||
// Icon is required to proceed
|
||||
if (!isModelInitialized) {
|
||||
throw new Error(formatMessage("MODEL_UNINITIALIZED", "Buffers"))
|
||||
}
|
||||
|
||||
// separing localization folders from bundle files
|
||||
const [ l10nBundle, bundle ] = splitBufferBundle(rawBundle);
|
||||
|
||||
return {
|
||||
bundle,
|
||||
l10nBundle
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads certificate contents, if the passed content is a path,
|
||||
* and parses them as a PEM.
|
||||
* @param options
|
||||
*/
|
||||
|
||||
export async function readCertificatesFromOptions(options: Certificates): Promise<FinalCertificates> {
|
||||
if (!(options && Object.keys(options).length && isValid(options, "certificatesSchema"))) {
|
||||
throw new Error(formatMessage("CP_NO_CERTS"));
|
||||
}
|
||||
|
||||
// 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: (
|
||||
typeof options.signerKey === "string"
|
||||
? options.signerKey
|
||||
: options.signerKey.keyFile
|
||||
)
|
||||
});
|
||||
|
||||
// We read the contents
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const parsedContents = await Promise.all(rawContentsPromises);
|
||||
const pemParsedContents = parsedContents.map((file, index) => {
|
||||
const certName = Object.keys(options)[index];
|
||||
const pem = parsePEM(
|
||||
certName,
|
||||
file,
|
||||
typeof options.signerKey === "object"
|
||||
? options.signerKey.passphrase
|
||||
: undefined
|
||||
);
|
||||
|
||||
if (!pem) {
|
||||
throw new Error(formatMessage("INVALID_CERTS", certName));
|
||||
}
|
||||
|
||||
return { [certName]: pem };
|
||||
});
|
||||
|
||||
return Object.assign({}, ...pemParsedContents);
|
||||
} catch (err) {
|
||||
if (!err.path) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw new Error(formatMessage("INVALID_CERT_PATH", path.parse(err.path).base));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the PEM-formatted passed text (certificates)
|
||||
*
|
||||
* @param element - Text content of .pem files
|
||||
* @param passphrase - passphrase for the key
|
||||
* @returns The parsed certificate or key in node forge format
|
||||
*/
|
||||
|
||||
function parsePEM(pemName: string, element: string, passphrase?: string) {
|
||||
if (pemName === "signerKey" && passphrase) {
|
||||
return forge.pki.decryptRsaPrivateKey(element, String(passphrase));
|
||||
} else {
|
||||
return forge.pki.certificateFromPem(element);
|
||||
}
|
||||
}
|
||||
829
src/pass.js
829
src/pass.js
@@ -1,829 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { promisify } = require("util");
|
||||
const stream = require("stream");
|
||||
const forge = require("node-forge");
|
||||
const archiver = require("archiver");
|
||||
const debug = require("debug");
|
||||
const got = require("got");
|
||||
|
||||
const barcodeDebug = debug("passkit:barcode");
|
||||
const genericDebug = debug("passkit:generic");
|
||||
const loadDebug = debug("passkit:load");
|
||||
|
||||
const schema = require("./schema");
|
||||
const formatMessage = require("./messages");
|
||||
const FieldsArray = require("./fieldsArray");
|
||||
const {
|
||||
assignLength, generateStringFile,
|
||||
removeHidden, dateToW3CString,
|
||||
isValidRGB
|
||||
} = require("./utils");
|
||||
|
||||
const readdir = promisify(fs.readdir);
|
||||
const readFile = promisify(fs.readFile);
|
||||
|
||||
const noop = () => {};
|
||||
const transitType = Symbol("transitType");
|
||||
const barcodesFillMissing = Symbol("bfm");
|
||||
const barcodesSetBackward = Symbol("bsb");
|
||||
|
||||
class Pass {
|
||||
constructor(options) {
|
||||
this.Certificates = {
|
||||
// Even if this assigning will fail, it will be captured below
|
||||
// in _parseSettings, since this won't match with the schema.
|
||||
_raw: options.certificates || {},
|
||||
};
|
||||
|
||||
options.overrides = options.overrides || {};
|
||||
|
||||
this.l10n = {};
|
||||
this._remoteResources = [];
|
||||
this.shouldOverwrite = !(options.hasOwnProperty("shouldOverwrite") && !options.shouldOverwrite);
|
||||
|
||||
this._fields = ["primaryFields", "secondaryFields", "auxiliaryFields", "backFields", "headerFields"];
|
||||
|
||||
this.fieldsKeys = new Set();
|
||||
|
||||
this._fields.forEach(name => {
|
||||
this[name] = new FieldsArray(this.fieldsKeys);
|
||||
});
|
||||
|
||||
this[transitType] = "";
|
||||
|
||||
// Assigning model and _props to this
|
||||
Object.assign(this, this._parseSettings(options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the pass Stream
|
||||
*
|
||||
* @async
|
||||
* @method generate
|
||||
* @return {Promise<Stream>} A Promise containing the stream of the generated pass.
|
||||
*/
|
||||
|
||||
async generate() {
|
||||
try {
|
||||
// Reading the model
|
||||
const modelFilesList = await readdir(this.model);
|
||||
|
||||
/**
|
||||
* Getting the buffers for remote files
|
||||
*/
|
||||
|
||||
const buffersPromise = await this._remoteResources.reduce(async (acc, current) => {
|
||||
try {
|
||||
const response = await got(current[0], { encoding: null });
|
||||
loadDebug(formatMessage("LOAD_MIME", response.headers["content-type"]));
|
||||
|
||||
if (!Buffer.isBuffer(response.body)) {
|
||||
throw "LOADED_RESOURCE_NOT_A_BUFFER";
|
||||
}
|
||||
|
||||
if (!response.headers["content-type"].includes("image/")) {
|
||||
throw "LOADED_RESOURCE_NOT_A_PICTURE";
|
||||
}
|
||||
|
||||
return [...acc, response.body];
|
||||
} catch (err) {
|
||||
loadDebug(formatMessage("LOAD_NORES", current[1], err));
|
||||
return acc;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const remoteFilesList = buffersPromise.length ? this._remoteResources.map(r => r[1]): [];
|
||||
|
||||
// list without dynamic components like manifest, signature or pass files (will be added later in the flow) and hidden files.
|
||||
const noDynList = removeHidden(modelFilesList).filter(f => !/(manifest|signature|pass)/i.test(f));
|
||||
const hasAssets = noDynList.length || remoteFilesList.length;
|
||||
|
||||
if (!hasAssets || ![...noDynList, ...remoteFilesList].some(f => f.toLowerCase().includes("icon"))) {
|
||||
let eMessage = formatMessage("MODEL_UNINITIALIZED", path.parse(this.model).name);
|
||||
throw new Error(eMessage);
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
||||
/*
|
||||
* Reading all the localization selected folders and removing hidden files (the ones that starts with ".")
|
||||
* from the list.
|
||||
*/
|
||||
|
||||
const L10N_FilesListByFolder = await Promise.all(
|
||||
L10N.map(async folderPath => {
|
||||
const list = await readdir(path.join(this.model, folderPath))
|
||||
return removeHidden(list);
|
||||
})
|
||||
);
|
||||
|
||||
// Pushing into the bundle the composed paths for localization files
|
||||
|
||||
L10N_FilesListByFolder.forEach((filesList, index) =>
|
||||
bundle.push(
|
||||
...filesList.map(file => path.join(L10N[index], file))
|
||||
)
|
||||
);
|
||||
|
||||
/* Getting all bundle file buffers, pass.json included, and appending path */
|
||||
|
||||
if (remoteFilesList.length) {
|
||||
// Removing files in bundle that also exist in remoteFilesList
|
||||
// I'm giving priority to downloaded files
|
||||
bundle = bundle.filter(file => !remoteFilesList.includes(file));
|
||||
}
|
||||
|
||||
// Reading bundle files to buffers without pass.json - it gets read below
|
||||
// to use a different parsing process
|
||||
|
||||
const bundleBuffers = bundle.map(file => readFile(path.resolve(this.model, file)));
|
||||
const passBuffer = this._extractPassDefinition();
|
||||
bundle.push("pass.json");
|
||||
|
||||
const buffers = await Promise.all([...bundleBuffers, passBuffer, ...buffersPromise]);
|
||||
|
||||
Object.keys(this.l10n).forEach(lang => {
|
||||
const strings = generateStringFile(this.l10n[lang]);
|
||||
|
||||
/**
|
||||
* if .string file buffer is empty, no translations were added
|
||||
* but still wanted to include the language
|
||||
*/
|
||||
|
||||
if (!strings.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* if there's already a buffer of the same folder and called
|
||||
* `pass.strings`, we'll merge the two buffers. We'll create
|
||||
* it otherwise.
|
||||
*
|
||||
* We are replacing the slashes to avoid Windows slashes
|
||||
* composition.
|
||||
*/
|
||||
|
||||
const stringFilePath = path.join(`${lang}.lproj`, "pass.strings").replace(/\\/, "/");
|
||||
|
||||
const stringFileIndex = bundle.findIndex(file => file === stringFilePath);
|
||||
|
||||
if (stringFileIndex > -1) {
|
||||
buffers[stringFileIndex] = Buffer.concat([
|
||||
buffers[stringFileIndex],
|
||||
strings
|
||||
]);
|
||||
} else {
|
||||
buffers.push(strings);
|
||||
bundle.push(stringFilePath);
|
||||
}
|
||||
});
|
||||
|
||||
// Pushing the remote files into the bundle
|
||||
bundle.push(...remoteFilesList);
|
||||
|
||||
/*
|
||||
* Parsing the buffers, pushing them into the archive
|
||||
* and returning the compiled manifest
|
||||
*/
|
||||
const archive = archiver("zip");
|
||||
const manifest = 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;
|
||||
}, {});
|
||||
|
||||
// Reading the certificates,
|
||||
// signing the manifest, appending signature an manifest to the archive
|
||||
// and returning the generated pass stream.
|
||||
|
||||
Object.assign(this.Certificates, await readCertificates(this.Certificates));
|
||||
|
||||
const signatureBuffer = this._sign(manifest);
|
||||
|
||||
archive.append(signatureBuffer, { name: "signature" });
|
||||
archive.append(JSON.stringify(manifest), { name: "manifest.json" });
|
||||
|
||||
const passStream = new stream.PassThrough();
|
||||
|
||||
archive.pipe(passStream);
|
||||
|
||||
return archive.finalize().then(() => passStream);
|
||||
} catch (err) {
|
||||
if (err.code && err.code === "ENOENT") {
|
||||
// No model available at this path - renaming the error
|
||||
throw new Error(formatMessage("MODEL_NOT_FOUND", this.model));
|
||||
}
|
||||
|
||||
throw new Error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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" && !(date instanceof Date)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
let dateParse = dateToW3CString(date, format);
|
||||
|
||||
if (!dateParse) {
|
||||
genericDebug(formatMessage("DATE_FORMAT_UNMATCH", "Expiration date"));
|
||||
return this;
|
||||
}
|
||||
|
||||
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 assignLength(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 assignLength(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 assignLength(Number(!cond), this);
|
||||
} else if (type === "relevantDate") {
|
||||
if (typeof data !== "string" && !(data instanceof Date)) {
|
||||
genericDebug(formatMessage("DATE_FORMAT_UNMATCH", "Relevant Date"));
|
||||
return this;
|
||||
}
|
||||
|
||||
let dateParse = dateToW3CString(data, relevanceDateFormat);
|
||||
|
||||
if (!dateParse) {
|
||||
genericDebug(formatMessage("DATE_FORMAT_UNMATCH", "Relevant Date"));
|
||||
} else {
|
||||
this._props[type] = dateParse;
|
||||
}
|
||||
|
||||
return assignLength(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 assignLength(0, this, {
|
||||
autocomplete: noop,
|
||||
backward: noop,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof data === "string" || (data instanceof Object && !Array.isArray(data) && !data.format && data.message)) {
|
||||
const autogen = barcodesFromUncompleteData(data instanceof Object ? data : { message: data });
|
||||
|
||||
if (!autogen.length) {
|
||||
return assignLength(0, this, {
|
||||
autocomplete: noop,
|
||||
backward: noop
|
||||
});
|
||||
}
|
||||
|
||||
this._props["barcode"] = autogen[0];
|
||||
this._props["barcodes"] = autogen;
|
||||
|
||||
return assignLength(autogen.length, this, {
|
||||
autocomplete: noop,
|
||||
backward: (format) => this[barcodesSetBackward](format)
|
||||
});
|
||||
}
|
||||
|
||||
if (!(data instanceof Array)) {
|
||||
data = [data];
|
||||
}
|
||||
|
||||
// Stripping from the array not-object elements, objects with no message
|
||||
// and the ones that does not pass validation.
|
||||
// Validation assign default value to missing parameters (if any).
|
||||
|
||||
const valid = data.reduce((acc, current) => {
|
||||
if (!(current && current instanceof Object && current.hasOwnProperty("message"))) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const validated = schema.getValidated(current, "barcode");
|
||||
|
||||
if (!(validated && validated instanceof Object && Object.keys(validated).length)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [...acc, validated];
|
||||
}, []);
|
||||
|
||||
if (valid.length) {
|
||||
// With this check, we want to avoid that
|
||||
// PKBarcodeFormatCode128 gets chosen automatically
|
||||
// if it is the first. If true, we'll get 1
|
||||
// (so not the first index)
|
||||
const barcodeFirstValidIndex = Number(valid[0].format === "PKBarcodeFormatCode128");
|
||||
|
||||
if (valid.length > 0) {
|
||||
this._props["barcode"] = valid[barcodeFirstValidIndex];
|
||||
}
|
||||
|
||||
this._props["barcodes"] = valid;
|
||||
}
|
||||
|
||||
return assignLength(valid.length, this, {
|
||||
autocomplete: () => this[barcodesFillMissing](),
|
||||
backward: (format) => this[barcodesSetBackward](format),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Symbol/barcodesFillMissing
|
||||
* @returns {this} Improved this, with length property and retroCompatibility method.
|
||||
*/
|
||||
|
||||
[barcodesFillMissing]() {
|
||||
let props = this._props["barcodes"];
|
||||
|
||||
if (props.length === 4 || !props.length) {
|
||||
return assignLength(0, this, {
|
||||
autocomplete: noop,
|
||||
backward: (format) => this[barcodesSetBackward](format)
|
||||
});
|
||||
}
|
||||
|
||||
this._props["barcodes"] = barcodesFromUncompleteData(props[0]);
|
||||
|
||||
return assignLength(4 - props.length, this, {
|
||||
autocomplete: noop,
|
||||
backward: (format) => this[barcodesSetBackward](format)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an index <= the amount of already set "barcodes",
|
||||
* this let you choose which structure to use for retrocompatibility
|
||||
* property "barcode".
|
||||
*
|
||||
* @method Symbol/barcodesSetBackward
|
||||
* @params {String} format - the format, or part of it, to be used
|
||||
* @return {this}
|
||||
*/
|
||||
|
||||
[barcodesSetBackward](format) {
|
||||
if (format === null) {
|
||||
this._props["barcode"] = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
if (typeof format !== "string") {
|
||||
barcodeDebug(formatMessage("BRC_FORMAT_UNMATCH"));
|
||||
return this;
|
||||
}
|
||||
|
||||
if (format === "PKBarcodeFormatCode128") {
|
||||
barcodeDebug(formatMessage("BRC_BW_FORMAT_UNSUPPORTED"));
|
||||
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(formatMessage("BRC_NOT_SUPPORTED"));
|
||||
return this;
|
||||
}
|
||||
|
||||
this._props["barcode"] = this._props["barcodes"][index];
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets nfc fields in properties
|
||||
*
|
||||
* @method nfc
|
||||
* @params {Object} data - the data to be pushed in the pass
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
nfc(data) {
|
||||
if (!(typeof data === "object" && !Array.isArray(data) && schema.isValid(data, "nfcDict"))) {
|
||||
genericDebug("Invalid NFC data provided");
|
||||
return this;
|
||||
}
|
||||
|
||||
this._props["nfc"] = data;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a web resource (image)
|
||||
* @param {string} resource
|
||||
* @param {string} name
|
||||
*/
|
||||
|
||||
load(resource, name) {
|
||||
if (typeof resource !== "string" && typeof name !== "string") {
|
||||
loadDebug(formatMessage("LOAD_TYPES_UNMATCH"));
|
||||
return;
|
||||
}
|
||||
|
||||
this._remoteResources.push([resource, name]);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if pass model type is one of the supported ones
|
||||
*
|
||||
* @method _hasValidType
|
||||
* @params {string} passFile - parsed pass structure content
|
||||
* @returns {Boolean} true if type is supported, false otherwise.
|
||||
*/
|
||||
|
||||
_hasValidType(passFile) {
|
||||
let passTypes = ["boardingPass", "eventTicket", "coupon", "generic", "storeCard"];
|
||||
|
||||
this.type = passTypes.find(type => passFile.hasOwnProperty(type));
|
||||
|
||||
if (!this.type) {
|
||||
genericDebug(formatMessage("NO_PASS_TYPE"));
|
||||
return false;
|
||||
}
|
||||
|
||||
return schema.isValid(passFile[this.type], "passDict");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads pass.json file and returns the patched version
|
||||
* @function
|
||||
* @name passExtractor
|
||||
* @return {Promise<Buffer>} The patched pass.json buffer
|
||||
*/
|
||||
|
||||
async _extractPassDefinition() {
|
||||
const passStructBuffer = await readFile(path.resolve(this.model, "pass.json"))
|
||||
const parsedPassDefinition = JSON.parse(passStructBuffer.toString("utf8"));
|
||||
|
||||
if (!this._hasValidType(parsedPassDefinition)) {
|
||||
const eMessage = formatMessage("PASSFILE_VALIDATION_FAILED");
|
||||
throw new Error(eMessage);
|
||||
}
|
||||
|
||||
return this._patch(parsedPassDefinition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the PKCS #7 cryptografic signature for the manifest file.
|
||||
*
|
||||
* @method _sign
|
||||
* @params {Object} manifest - Manifest content.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
_sign(manifest) {
|
||||
let signature = forge.pkcs7.createSignedData();
|
||||
|
||||
signature.content = forge.util.createBuffer(JSON.stringify(manifest), "utf8");
|
||||
|
||||
signature.addCertificate(this.Certificates.wwdr);
|
||||
signature.addCertificate(this.Certificates.signerCert);
|
||||
|
||||
/**
|
||||
* 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,
|
||||
authenticatedAttributes: [{
|
||||
type: forge.pki.oids.contentType,
|
||||
value: forge.pki.oids.data
|
||||
}, {
|
||||
type: forge.pki.oids.messageDigest,
|
||||
}, {
|
||||
type: forge.pki.oids.signingTime,
|
||||
}]
|
||||
});
|
||||
|
||||
/**
|
||||
* We are creating a detached signature because we don't need the signed content.
|
||||
* Detached signature is a property of PKCS#7 cryptography standard.
|
||||
*/
|
||||
|
||||
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 content as value.
|
||||
*
|
||||
* In fact the previous approach was to generating a detached signature and the pull away the generated
|
||||
* content.
|
||||
*
|
||||
* 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. ¯\_(ツ)_/¯
|
||||
*/
|
||||
|
||||
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(passFile) {
|
||||
if (Object.keys(this._props).length) {
|
||||
// We filter the existing (in passFile) and non-valid keys from
|
||||
// the below array keys that accept rgb values
|
||||
// and then delete it from the passFile.
|
||||
["backgroundColor", "foregroundColor", "labelColor"]
|
||||
.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];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._fields.forEach(area => {
|
||||
if (this[area].length) {
|
||||
if (this.shouldOverwrite) {
|
||||
passFile[this.type][area] = [...this[area]];
|
||||
} else {
|
||||
passFile[this.type][area].push(...this[area]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.type === "boardingPass" && !this.transitType) {
|
||||
throw new Error(formatMessage("TRSTYPE_REQUIRED"));
|
||||
}
|
||||
|
||||
passFile[this.type]["transitType"] = this.transitType;
|
||||
|
||||
return Buffer.from(JSON.stringify(passFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the contents of the passed options and handle them
|
||||
*
|
||||
* @method _parseSettings
|
||||
* @params {Object} options - the options passed to be parsed
|
||||
* @returns {Object} - model path and filtered options
|
||||
*/
|
||||
|
||||
_parseSettings(options) {
|
||||
if (!schema.isValid(options, "instance")) {
|
||||
throw new Error(formatMessage("REQUIR_VALID_FAILED"));
|
||||
}
|
||||
|
||||
if (!options.model || typeof options.model !== "string") {
|
||||
throw new Error(formatMessage("MODEL_NOT_STRING"));
|
||||
}
|
||||
|
||||
const modelPath = path.resolve(options.model) + (!!options.model && !path.extname(options.model) ? ".pass" : "");
|
||||
const filteredOpts = schema.getValidated(options.overrides, "supportedOptions");
|
||||
|
||||
if (!filteredOpts) {
|
||||
throw new Error(formatMessage("OVV_KEYS_BADFORMAT"))
|
||||
}
|
||||
|
||||
return {
|
||||
model: modelPath,
|
||||
_props: filteredOpts
|
||||
};
|
||||
}
|
||||
|
||||
set transitType(v) {
|
||||
if (schema.isValid(v, "transitType")) {
|
||||
this[transitType] = v;
|
||||
} else {
|
||||
genericDebug(formatMessage("TRSTYPE_NOT_VALID", v));
|
||||
this[transitType] = this[transitType] || "";
|
||||
}
|
||||
}
|
||||
|
||||
get transitType() {
|
||||
return this[transitType];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the contents of the passed options and handle them
|
||||
*
|
||||
* @function readCertificates
|
||||
* @params {Object} certificates - certificates object with raw content
|
||||
* and, optionally, the already parsed certificates
|
||||
* @returns {Object} - parsed certificates to be pushed to Pass.Certificates.
|
||||
*/
|
||||
|
||||
function readCertificates(certificates) {
|
||||
if (certificates.wwdr && certificates.signerCert && typeof certificates.signerKey === "object") {
|
||||
// Nothing must be added. Void object is returned.
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
const raw = certificates._raw;
|
||||
const optCertsNames = Object.keys(raw);
|
||||
const certPaths = optCertsNames.map((val) => {
|
||||
const cert = raw[val];
|
||||
// realRawValue exists as signerKey might be an object
|
||||
const realRawValue = !(cert instanceof Object) ? cert : cert["keyFile"];
|
||||
|
||||
// We are checking if the string is a path or a content
|
||||
if (!!path.parse(realRawValue).ext) {
|
||||
const resolvedPath = path.resolve(realRawValue);
|
||||
return readFile(resolvedPath);
|
||||
} else {
|
||||
return Promise.resolve(realRawValue);
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(certPaths)
|
||||
.then(contents => {
|
||||
// Mapping each file content to a PEM structure, returned in form of one-key-object
|
||||
// which is conjoint later with the other pems
|
||||
|
||||
return Object.assign(
|
||||
...contents.map((file, index) => {
|
||||
const certName = optCertsNames[index];
|
||||
const pem = parsePEM(certName, file, raw[certName].passphrase);
|
||||
|
||||
if (!pem) {
|
||||
throw new Error(formatMessage("INVALID_CERTS", certName));
|
||||
}
|
||||
|
||||
return { [certName]: pem };
|
||||
})
|
||||
);
|
||||
}).catch(err => {
|
||||
if (!err.path) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw new Error(formatMessage("INVALID_CERT_PATH", path.parse(err.path).base));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(pemName, element, passphrase) {
|
||||
if (pemName === "signerKey" && passphrase) {
|
||||
return forge.pki.decryptRsaPrivateKey(element, String(passphrase));
|
||||
} else {
|
||||
return forge.pki.certificateFromPem(element);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically generates barcodes for all the types given common info
|
||||
*
|
||||
* @method barcodesFromMessage
|
||||
* @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
|
||||
*/
|
||||
|
||||
function barcodesFromUncompleteData(origin) {
|
||||
if (!(origin.message && typeof origin.message === "string")) {
|
||||
barcodeDebug(formatMessage("BRC_AUTC_MISSING_DATA"));
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
"PKBarcodeFormatQR",
|
||||
"PKBarcodeFormatPDF417",
|
||||
"PKBarcodeFormatAztec",
|
||||
"PKBarcodeFormatCode128"
|
||||
].map(format =>
|
||||
schema.getValidated(
|
||||
Object.assign(origin, { format }),
|
||||
"barcode"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { Pass };
|
||||
657
src/pass.ts
Normal file
657
src/pass.ts
Normal file
@@ -0,0 +1,657 @@
|
||||
import path from "path";
|
||||
import forge from "node-forge";
|
||||
import debug from "debug";
|
||||
import { Stream } from "stream";
|
||||
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";
|
||||
|
||||
const barcodeDebug = debug("passkit:barcode");
|
||||
const genericDebug = debug("passkit:generic");
|
||||
|
||||
const transitType = Symbol("transitType");
|
||||
const passProps = Symbol("_props");
|
||||
|
||||
const propsSchemaMap = new Map([
|
||||
["barcodes", "barcode"],
|
||||
["barcode", "barcode"],
|
||||
["beacons", "beaconsDict"],
|
||||
["locations", "locationsDict"],
|
||||
["nfc", "nfcDict"]
|
||||
]);
|
||||
|
||||
export class Pass {
|
||||
private bundle: schema.BundleUnit;
|
||||
private l10nBundles: schema.PartitionedBundle["l10nBundle"];
|
||||
private _fields: (keyof schema.PassFields)[];
|
||||
private [passProps]: schema.ValidPass = {};
|
||||
private type: keyof schema.ValidPassType;
|
||||
private fieldsKeys: Set<string> = new Set<string>();
|
||||
private passCore: schema.ValidPass = {};
|
||||
|
||||
public headerFields: FieldsArray;
|
||||
public primaryFields: FieldsArray;
|
||||
public secondaryFields: FieldsArray;
|
||||
public auxiliaryFields: FieldsArray;
|
||||
public backFields: FieldsArray;
|
||||
|
||||
Certificates: schema.FinalCertificates;
|
||||
l10nTranslations: { [key: string]: { [key: string]: string } } = {};
|
||||
[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 };
|
||||
|
||||
try {
|
||||
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;
|
||||
|
||||
if (validOverrides === null) {
|
||||
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;
|
||||
|
||||
if (!this.type) {
|
||||
throw new Error(formatMessage("NO_PASS_TYPE"));
|
||||
}
|
||||
|
||||
// Parsing and validating pass.json keys
|
||||
const validatedPassKeys = Object.keys(this.passCore).reduce((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] };
|
||||
}
|
||||
|
||||
const currentSchema = propsSchemaMap.get(current);
|
||||
|
||||
if (Array.isArray(this.passCore[current])) {
|
||||
const valid = getValidInArray(currentSchema, this.passCore[current]);
|
||||
return { ...acc, [current]: valid };
|
||||
} else {
|
||||
return {
|
||||
...acc,
|
||||
[current]: schema.isValid(
|
||||
this.passCore[current],
|
||||
currentSchema
|
||||
) && this.passCore[current] || undefined
|
||||
};
|
||||
}
|
||||
}, {});
|
||||
|
||||
this[passProps] = {
|
||||
...(validatedPassKeys || {}),
|
||||
...(validOverrides || {})
|
||||
};
|
||||
|
||||
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[fieldName] = new FieldsArray(
|
||||
this.fieldsKeys,
|
||||
...(this.passCore[this.type][fieldName] || [])
|
||||
.filter(field => schema.isValid(field, "field"))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the pass Stream
|
||||
*
|
||||
* @method generate
|
||||
* @return A Stream of the generated pass.
|
||||
*/
|
||||
|
||||
generate(): Stream {
|
||||
// Editing Pass.json
|
||||
this.bundle["pass.json"] = this._patch(this.bundle["pass.json"]);
|
||||
|
||||
/**
|
||||
* Checking Personalization, as this is available only with NFC
|
||||
* @see https://apple.co/2SHfb22
|
||||
*/
|
||||
const currentBundleFiles = Object.keys(this.bundle);
|
||||
|
||||
if (!this[passProps].nfc && currentBundleFiles.includes("personalization.json")) {
|
||||
genericDebug(formatMessage("PRS_REMOVED"));
|
||||
deletePersonalization(this.bundle, getAllFilesWithName(
|
||||
"personalizationLogo",
|
||||
currentBundleFiles,
|
||||
"startsWith"
|
||||
));
|
||||
}
|
||||
|
||||
const finalBundle = { ...this.bundle } as schema.BundleUnit;
|
||||
|
||||
/**
|
||||
* Iterating through languages and generating pass.string file
|
||||
*/
|
||||
|
||||
Object.keys(this.l10nTranslations).forEach(lang => {
|
||||
const strings = generateStringFile(this.l10nTranslations[lang]);
|
||||
const langInBundles = `${lang}.lproj`;
|
||||
|
||||
if (strings.length) {
|
||||
/**
|
||||
* if there's already a buffer of the same folder and called
|
||||
* `pass.strings`, we'll merge the two buffers. We'll create
|
||||
* it otherwise.
|
||||
*/
|
||||
|
||||
if (!this.l10nBundles[langInBundles]) {
|
||||
this.l10nBundles[langInBundles] = {};
|
||||
}
|
||||
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigning all the localization files to the final bundle
|
||||
* by mapping the buffer to the pass-relative file path;
|
||||
*
|
||||
* We are replacing the slashes to avoid Windows slashes
|
||||
* composition.
|
||||
*/
|
||||
|
||||
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
|
||||
*/
|
||||
const archive = new ZipFile();
|
||||
const manifest = Object.keys(finalBundle).reduce((acc, current) => {
|
||||
let hashFlow = forge.md.sha1.create();
|
||||
|
||||
hashFlow.update(finalBundle[current].toString("binary"));
|
||||
archive.addBuffer(finalBundle[current], current);
|
||||
acc[current] = hashFlow.digest().toHex();
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const signatureBuffer = this._sign(manifest);
|
||||
|
||||
archive.addBuffer(signatureBuffer, "signature");
|
||||
archive.addBuffer(Buffer.from(JSON.stringify(manifest)), "manifest.json");
|
||||
const passStream = new Stream.PassThrough();
|
||||
|
||||
archive.outputStream.pipe(passStream);
|
||||
archive.end();
|
||||
|
||||
return passStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds traslated strings object to the list of translation to be inserted into the pass
|
||||
*
|
||||
* @method localize
|
||||
* @params lang - the ISO 3166 alpha-2 code for the language
|
||||
* @params translations - key/value pairs where key is the
|
||||
* placeholder in pass.json localizable strings
|
||||
* and value the real translated string.
|
||||
* @returns {this}
|
||||
*
|
||||
* @see https://apple.co/2KOv0OW - Passes support localization
|
||||
*/
|
||||
|
||||
localize(lang: string, translations?: { [key: string]: string }): this {
|
||||
if (lang && typeof lang === "string" && (typeof translations === "object" || translations === undefined)) {
|
||||
this.l10nTranslations[lang] = translations || {};
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets expirationDate property to a W3C-formatted date
|
||||
*
|
||||
* @method expiration
|
||||
* @params date
|
||||
* @returns {this}
|
||||
*/
|
||||
|
||||
expiration(date: Date | null): this {
|
||||
if (date === null) {
|
||||
delete this[passProps]["expirationDate"];
|
||||
return this;
|
||||
}
|
||||
|
||||
const parsedDate = processDate("expirationDate", date);
|
||||
|
||||
if (parsedDate) {
|
||||
this[passProps]["expirationDate"] = parsedDate;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets voided property to true
|
||||
*
|
||||
* @method void
|
||||
* @return {this}
|
||||
*/
|
||||
|
||||
void(): this {
|
||||
this[passProps]["voided"] = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets current pass' relevancy through beacons
|
||||
* @param data
|
||||
* @returns {Pass}
|
||||
*/
|
||||
|
||||
beacons(...data: schema.Beacon[] | null): this {
|
||||
if (data === null) {
|
||||
delete this[passProps]["beacons"];
|
||||
return this;
|
||||
}
|
||||
|
||||
const valid = processRelevancySet("beacons", data);
|
||||
|
||||
if (valid.length) {
|
||||
this[passProps]["beacons"] = valid;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets current pass' relevancy through locations
|
||||
* @param data
|
||||
* @returns {Pass}
|
||||
*/
|
||||
|
||||
locations(...data: schema.Location[] | null): this {
|
||||
if (data === null) {
|
||||
delete this[passProps]["locations"];
|
||||
return this;
|
||||
}
|
||||
|
||||
const valid = processRelevancySet("locations", data);
|
||||
|
||||
if (valid.length) {
|
||||
this[passProps]["locations"] = valid;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets current pass' relevancy through a date
|
||||
* @param data
|
||||
* @returns {Pass}
|
||||
*/
|
||||
|
||||
relevantDate(date: Date | null): this {
|
||||
if (date === null) {
|
||||
delete this[passProps]["relevantDate"];
|
||||
return this;
|
||||
}
|
||||
|
||||
const parsedDate = processDate("relevantDate", date);
|
||||
|
||||
if (parsedDate) {
|
||||
this[passProps]["relevantDate"] = parsedDate;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds barcodes "barcodes" property.
|
||||
* It allows to pass a string to autogenerate all the structures.
|
||||
*
|
||||
* @method barcode
|
||||
* @params first - a structure or the string (message) that will generate
|
||||
* all the barcodes
|
||||
* @params data - other barcodes support
|
||||
* @return {this} Improved this with length property and other methods
|
||||
*/
|
||||
|
||||
barcodes(first: null | string | schema.Barcode, ...data: schema.Barcode[]): this {
|
||||
if (first === null) {
|
||||
delete this[passProps]["barcodes"];
|
||||
return this;
|
||||
}
|
||||
|
||||
const isFirstParameterValid = (
|
||||
first && (
|
||||
typeof first === "string" || (
|
||||
typeof first === "object" &&
|
||||
first.hasOwnProperty("message")
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (!isFirstParameterValid) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (typeof first === "string") {
|
||||
const autogen = barcodesFromUncompleteData(first);
|
||||
|
||||
if (!autogen.length) {
|
||||
barcodeDebug(formatMessage("BRC_AUTC_MISSING_DATA"));
|
||||
return this;
|
||||
}
|
||||
|
||||
this[passProps]["barcodes"] = autogen;
|
||||
|
||||
return this;
|
||||
} else {
|
||||
const barcodes = [first, ...(data || [])];
|
||||
|
||||
/**
|
||||
* Stripping from the array not-object elements
|
||||
* and the ones that does not pass validation.
|
||||
* Validation assign default value to missing parameters (if any).
|
||||
*/
|
||||
|
||||
const valid = barcodes.reduce<schema.Barcode[]>((acc, current) => {
|
||||
if (!(current && current instanceof Object)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const validated = schema.getValidated(current, "barcode");
|
||||
|
||||
if (!(validated && validated instanceof Object && Object.keys(validated).length)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [...acc, validated] as schema.Barcode[];
|
||||
}, []);
|
||||
|
||||
if (valid.length) {
|
||||
this[passProps]["barcodes"] = valid;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an index <= the amount of already set "barcodes",
|
||||
* this let you choose which structure to use for retrocompatibility
|
||||
* property "barcode".
|
||||
*
|
||||
* @method barcode
|
||||
* @params format - the format to be used
|
||||
* @return {this}
|
||||
*/
|
||||
|
||||
barcode(chosenFormat: schema.BarcodeFormat | null): this {
|
||||
let { barcodes } = this[passProps];
|
||||
|
||||
if (chosenFormat === null) {
|
||||
delete this[passProps]["barcode"];
|
||||
return this;
|
||||
}
|
||||
|
||||
if (typeof chosenFormat !== "string") {
|
||||
barcodeDebug(formatMessage("BRC_FORMATTYPE_UNMATCH"));
|
||||
return this;
|
||||
}
|
||||
|
||||
if (chosenFormat === "PKBarcodeFormatCode128") {
|
||||
barcodeDebug(formatMessage("BRC_BW_FORMAT_UNSUPPORTED"));
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!(barcodes && barcodes.length)) {
|
||||
barcodeDebug(formatMessage("BRC_NO_POOL"));
|
||||
return this;
|
||||
}
|
||||
|
||||
// Checking which object among barcodes has the same format of the specified one.
|
||||
const index = barcodes.findIndex(b => b.format.toLowerCase().includes(chosenFormat.toLowerCase()));
|
||||
|
||||
if (index === -1) {
|
||||
barcodeDebug(formatMessage("BRC_NOT_SUPPORTED"));
|
||||
return this;
|
||||
}
|
||||
|
||||
this[passProps]["barcode"] = barcodes[index];
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets nfc fields in properties
|
||||
*
|
||||
* @method nfc
|
||||
* @params data - the data to be pushed in the pass
|
||||
* @returns {this}
|
||||
* @see https://apple.co/2wTxiaC
|
||||
*/
|
||||
|
||||
nfc(data: schema.NFC | null): this {
|
||||
if (data === null) {
|
||||
delete this[passProps]["nfc"];
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!(data && typeof data === "object" && !Array.isArray(data) && schema.isValid(data, "nfcDict"))) {
|
||||
genericDebug(formatMessage("NFC_INVALID"));
|
||||
return this;
|
||||
}
|
||||
|
||||
this[passProps]["nfc"] = data;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to get the current inserted props;
|
||||
* will return all props from valid overrides,
|
||||
* template's pass.json and methods-inserted ones;
|
||||
*
|
||||
* @returns The properties will be inserted in the pass.
|
||||
*/
|
||||
|
||||
get props(): Readonly<schema.ValidPass> {
|
||||
return this[passProps];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the PKCS #7 cryptografic signature for the manifest file.
|
||||
*
|
||||
* @method _sign
|
||||
* @params {Object} manifest - Manifest content.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
private _sign(manifest: { [key: string]: string }): Buffer {
|
||||
const signature = forge.pkcs7.createSignedData();
|
||||
|
||||
signature.content = forge.util.createBuffer(JSON.stringify(manifest), "utf8");
|
||||
|
||||
signature.addCertificate(this.Certificates.wwdr);
|
||||
signature.addCertificate(this.Certificates.signerCert);
|
||||
|
||||
/**
|
||||
* 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: [{
|
||||
type: forge.pki.oids.contentType,
|
||||
value: forge.pki.oids.data
|
||||
}, {
|
||||
type: forge.pki.oids.messageDigest,
|
||||
}, {
|
||||
type: forge.pki.oids.signingTime,
|
||||
}]
|
||||
});
|
||||
|
||||
/**
|
||||
* We are creating a detached signature because we don't need the signed content.
|
||||
* Detached signature is a property of PKCS#7 cryptography standard.
|
||||
*/
|
||||
|
||||
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 content as value.
|
||||
*
|
||||
* In fact the previous approach was to generating a detached signature and the pull away the generated
|
||||
* content.
|
||||
*
|
||||
* 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. ¯\_(ツ)_/¯
|
||||
*/
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
private _patch(passCoreBuffer: Buffer): Buffer {
|
||||
let passFile = JSON.parse(passCoreBuffer.toString());
|
||||
|
||||
if (Object.keys(this[passProps]).length) {
|
||||
/*
|
||||
* We filter the existing (in passFile) and non-valid keys from
|
||||
* the below array keys that accept rgb values
|
||||
* and then delete it from the passFile.
|
||||
*/
|
||||
|
||||
["backgroundColor", "foregroundColor", "labelColor"]
|
||||
.filter(v => this[passProps][v] && !isValidRGB(this[passProps][v]))
|
||||
.forEach(v => delete this[passProps][v]);
|
||||
|
||||
Object.assign(passFile, this[passProps]);
|
||||
}
|
||||
|
||||
this._fields.forEach(field => {
|
||||
passFile[this.type][field] = this[field];
|
||||
});
|
||||
|
||||
if (this.type === "boardingPass" && !this[transitType]) {
|
||||
throw new Error(formatMessage("TRSTYPE_REQUIRED"));
|
||||
}
|
||||
|
||||
passFile[this.type]["transitType"] = this[transitType];
|
||||
|
||||
return Buffer.from(JSON.stringify(passFile));
|
||||
}
|
||||
|
||||
set transitType(value: string) {
|
||||
if (!schema.isValid(value, "transitType")) {
|
||||
genericDebug(formatMessage("TRSTYPE_NOT_VALID", value));
|
||||
this[transitType] = this[transitType] || "";
|
||||
return;
|
||||
}
|
||||
|
||||
this[transitType] = value;
|
||||
}
|
||||
|
||||
get transitType(): string {
|
||||
return this[transitType];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically generates barcodes for all the types given common info
|
||||
*
|
||||
* @method barcodesFromUncompleteData
|
||||
* @params message - the content to be placed inside "message" field
|
||||
* @return Array of barcodeDict compliant
|
||||
*/
|
||||
|
||||
function barcodesFromUncompleteData(message: string): schema.Barcode[] {
|
||||
if (!(message && typeof message === "string")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
"PKBarcodeFormatQR",
|
||||
"PKBarcodeFormatPDF417",
|
||||
"PKBarcodeFormatAztec",
|
||||
"PKBarcodeFormatCode128"
|
||||
].map(format => schema.getValidated({ format, message }, "barcode"));
|
||||
}
|
||||
|
||||
function processRelevancySet<T>(key: string, data: T[]): T[] {
|
||||
return getValidInArray(`${key}Dict`, data);
|
||||
}
|
||||
|
||||
function getValidInArray<T>(schemaName: string, contents: T[]): T[] {
|
||||
return contents.filter(current => Object.keys(current).length && schema.isValid(current, schemaName));
|
||||
}
|
||||
|
||||
function processDate(key: string, date: Date): string | null {
|
||||
if (!(date instanceof Date)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dateParse = dateToW3CString(date);
|
||||
|
||||
if (!dateParse) {
|
||||
genericDebug(formatMessage("DATE_FORMAT_UNMATCH", key));
|
||||
return null;
|
||||
}
|
||||
|
||||
return dateParse;
|
||||
}
|
||||
@@ -1,20 +1,84 @@
|
||||
const Joi = require("joi");
|
||||
const debug = require("debug")("Schema");
|
||||
import Joi from "@hapi/joi";
|
||||
import debug from "debug";
|
||||
|
||||
const schemaDebug = debug("Schema");
|
||||
|
||||
export interface Certificates {
|
||||
wwdr?: string;
|
||||
signerCert?: string;
|
||||
signerKey?: {
|
||||
keyFile: string;
|
||||
passphrase?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FactoryOptions {
|
||||
model: BundleUnit | string;
|
||||
certificates: Certificates;
|
||||
overrides?: OverridesSupportedOptions;
|
||||
}
|
||||
|
||||
export interface BundleUnit {
|
||||
[key: string]: Buffer;
|
||||
}
|
||||
|
||||
export interface PartitionedBundle {
|
||||
bundle: BundleUnit;
|
||||
l10nBundle: {
|
||||
[key: string]: BundleUnit
|
||||
};
|
||||
}
|
||||
|
||||
export interface FinalCertificates {
|
||||
wwdr: string;
|
||||
signerCert: string;
|
||||
signerKey: string;
|
||||
}
|
||||
|
||||
export interface PassInstance {
|
||||
model: PartitionedBundle;
|
||||
certificates: FinalCertificates;
|
||||
overrides?: OverridesSupportedOptions;
|
||||
}
|
||||
|
||||
// ************************************ //
|
||||
// * JOI Schemas + Related Interfaces * //
|
||||
// ************************************ //
|
||||
|
||||
const certificatesSchema = Joi.object().keys({
|
||||
wwdr: Joi.alternatives(Joi.binary(), Joi.string()).required(),
|
||||
signerCert: Joi.alternatives(Joi.binary(), Joi.string()).required(),
|
||||
signerKey: Joi.alternatives().try(
|
||||
Joi.object().keys({
|
||||
keyFile: Joi.alternatives(Joi.binary(), Joi.string()).required(),
|
||||
passphrase: Joi.string().required(),
|
||||
}),
|
||||
Joi.alternatives(Joi.binary(), Joi.string())
|
||||
).required()
|
||||
}).required();
|
||||
|
||||
const instance = Joi.object().keys({
|
||||
model: Joi.string().required(),
|
||||
certificates: Joi.object().keys({
|
||||
wwdr: Joi.string().required(),
|
||||
signerCert: Joi.string().required(),
|
||||
signerKey: Joi.object().keys({
|
||||
keyFile: Joi.string().required(),
|
||||
passphrase: Joi.string().required(),
|
||||
}).required()
|
||||
}).required(),
|
||||
model: Joi.alternatives(Joi.object(), Joi.string()).required(),
|
||||
certificates: Joi.object(),
|
||||
overrides: Joi.object(),
|
||||
shouldOverwrite: Joi.boolean()
|
||||
});
|
||||
|
||||
export interface OverridesSupportedOptions {
|
||||
serialNumber?: string;
|
||||
description?: string;
|
||||
userInfo?: Object | Array<any>;
|
||||
webServiceURL?: string;
|
||||
authenticationToken?: string;
|
||||
sharingProhibited?: boolean;
|
||||
backgroundColor?: string;
|
||||
foregroundColor?: string;
|
||||
labelColor?: string;
|
||||
groupingIdentifier?: string;
|
||||
suppressStripShine?: boolean;
|
||||
logoText?: string;
|
||||
maxDistance?: number;
|
||||
}
|
||||
|
||||
const supportedOptions = Joi.object().keys({
|
||||
serialNumber: Joi.string(),
|
||||
description: Joi.string(),
|
||||
@@ -29,21 +93,41 @@ const supportedOptions = Joi.object().keys({
|
||||
groupingIdentifier: Joi.string(),
|
||||
suppressStripShine: Joi.boolean(),
|
||||
logoText: Joi.string(),
|
||||
maxDistance: Joi.number().positive(),
|
||||
}).with("webServiceURL", "authenticationToken");
|
||||
|
||||
|
||||
/* For a correct usage of semantics, please refer to https://apple.co/2I66Phk */
|
||||
|
||||
interface CurrencyAmount {
|
||||
currencyCode: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
const currencyAmount = Joi.object().keys({
|
||||
currencyCode: Joi.string().required(),
|
||||
amount: Joi.string().required(),
|
||||
});
|
||||
|
||||
interface PersonNameComponent {
|
||||
givenName: string;
|
||||
familyName: string;
|
||||
}
|
||||
|
||||
const personNameComponents = Joi.object().keys({
|
||||
givenName: Joi.string().required(),
|
||||
familyName: Joi.string().required()
|
||||
});
|
||||
|
||||
interface Seat {
|
||||
seatSection?: string;
|
||||
seatRow?: string;
|
||||
seatNumber?: string;
|
||||
seatIdentifier?: string;
|
||||
seatType?: string;
|
||||
seatDescription?: string;
|
||||
}
|
||||
|
||||
const seat = Joi.object().keys({
|
||||
seatSection: Joi.string(),
|
||||
seatRow: Joi.string(),
|
||||
@@ -58,6 +142,69 @@ const location = Joi.object().keys({
|
||||
longitude: Joi.number().required()
|
||||
});
|
||||
|
||||
interface Semantics {
|
||||
totalPrice?: CurrencyAmount;
|
||||
duration?: number;
|
||||
seats?: Seat[];
|
||||
silenceRequested?: boolean;
|
||||
departureLocation?: Location;
|
||||
destinationLocation?: Location;
|
||||
destinationLocationDescription?: Location;
|
||||
transitProvider?: string;
|
||||
vehicleName?: string;
|
||||
vehicleType?: string;
|
||||
originalDepartureDate?: string;
|
||||
currentDepartureDate?: string;
|
||||
originalArrivalDate?: string;
|
||||
currentArrivalDate?: string;
|
||||
originalBoardingDate?: string;
|
||||
currentBoardingDate?: string;
|
||||
boardingGroup?: string;
|
||||
boardingSequenceNumber?: string;
|
||||
confirmationNumber?: string;
|
||||
transitStatus?: string;
|
||||
transitStatuReason?: string;
|
||||
passengetName?: PersonNameComponent;
|
||||
membershipProgramName?: string;
|
||||
membershipProgramNumber?: string;
|
||||
priorityStatus?: string;
|
||||
securityScreening?: string;
|
||||
flightCode?: string;
|
||||
airlineCode?: string;
|
||||
flightNumber?: number;
|
||||
departureAirportCode?: string;
|
||||
departureAirportName?: string;
|
||||
destinationTerminal?: string;
|
||||
destinationGate?: string;
|
||||
departurePlatform?: string;
|
||||
departureStationName?: string;
|
||||
destinationPlatform?: string;
|
||||
destinationStationName?: string;
|
||||
carNumber?: string;
|
||||
eventName?: string;
|
||||
venueName?: string;
|
||||
venueLocation?: Location;
|
||||
venueEntrance?: string;
|
||||
venuePhoneNumber?: string;
|
||||
venueRoom?: string;
|
||||
eventType?: "PKEventTypeGeneric" | "PKEventTypeLivePerformance" | "PKEventTypeMovie" | "PKEventTypeSports" | "PKEventTypeConference" | "PKEventTypeConvention" | "PKEventTypeWorkshop" | "PKEventTypeSocialGathering";
|
||||
eventStartDate?: string;
|
||||
eventEndDate?: string;
|
||||
artistIDs?: string;
|
||||
performerNames?: string[];
|
||||
genre?: string;
|
||||
leagueName?: string;
|
||||
leagueAbbreviation?: string;
|
||||
homeTeamLocation?: string;
|
||||
homeTeamName?: string;
|
||||
homeTeamAbbreviation?: string;
|
||||
awayTeamLocation?: string;
|
||||
awayTeamName?: string;
|
||||
awayTeamAbbreviation?: string;
|
||||
sportName?: string;
|
||||
balance?: CurrencyAmount;
|
||||
}
|
||||
|
||||
const semantics = Joi.object().keys({
|
||||
// All
|
||||
totalPrice: currencyAmount,
|
||||
@@ -129,6 +276,35 @@ const semantics = Joi.object().keys({
|
||||
balance: currencyAmount
|
||||
});
|
||||
|
||||
export interface ValidPassType {
|
||||
boardingPass?: PassFields & { transitType: TransitType };
|
||||
eventTicket?: PassFields;
|
||||
coupon?: PassFields;
|
||||
generic?: PassFields;
|
||||
storeCard?: PassFields;
|
||||
}
|
||||
|
||||
export interface ValidPass extends OverridesSupportedOptions, ValidPassType {
|
||||
barcode?: Barcode;
|
||||
barcodes?: Barcode[];
|
||||
beacons?: Beacon[];
|
||||
locations?: Location[];
|
||||
maxDistance?: number;
|
||||
relevantDate?: string;
|
||||
nfc?: NFC;
|
||||
expirationDate?: string;
|
||||
voided?: boolean;
|
||||
}
|
||||
|
||||
export interface Barcode {
|
||||
altText?: string;
|
||||
messageEncoding?: string;
|
||||
format: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type BarcodeFormat = "PKBarcodeFormatQR" | "PKBarcodeFormatPDF417" | "PKBarcodeFormatAztec" | "PKBarcodeFormatCode128";
|
||||
|
||||
const barcode = Joi.object().keys({
|
||||
altText: Joi.string(),
|
||||
messageEncoding: Joi.string().default("iso-8859-1"),
|
||||
@@ -136,6 +312,23 @@ const barcode = Joi.object().keys({
|
||||
message: Joi.string().required()
|
||||
});
|
||||
|
||||
export interface Field {
|
||||
attributedValue?: string | number | Date;
|
||||
changeMessage?: string;
|
||||
dataDetectorType?: string[];
|
||||
label?: string;
|
||||
textAlignment?: string;
|
||||
key: string;
|
||||
value: string | number | Date;
|
||||
semantics?: Semantics;
|
||||
dateStyle?: string;
|
||||
ignoreTimeZone?: boolean;
|
||||
isRelative?: boolean;
|
||||
timeStyle?: string;
|
||||
currencyCode?: string;
|
||||
numberStyle?: string;
|
||||
}
|
||||
|
||||
const field = Joi.object().keys({
|
||||
attributedValue: Joi.alternatives(Joi.string().allow(""), Joi.number(), Joi.date().iso()),
|
||||
changeMessage: Joi.string(),
|
||||
@@ -164,13 +357,27 @@ const field = Joi.object().keys({
|
||||
}),
|
||||
});
|
||||
|
||||
export interface Beacon {
|
||||
major?: number;
|
||||
minor?: number;
|
||||
relevantText?: string;
|
||||
proximityUUID: string;
|
||||
}
|
||||
|
||||
const beaconsDict = Joi.object().keys({
|
||||
major: Joi.number().integer().positive().max(65535).greater(Joi.ref("minor")),
|
||||
minor: Joi.number().integer().positive().max(65535).less(Joi.ref("major")),
|
||||
minor: Joi.number().integer().min(0).max(65535).less(Joi.ref("major")),
|
||||
proximityUUID: Joi.string().required(),
|
||||
relevantText: Joi.string()
|
||||
});
|
||||
|
||||
export interface Location {
|
||||
relevantText?: string;
|
||||
altitude?: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
const locationsDict = Joi.object().keys({
|
||||
altitude: Joi.number(),
|
||||
latitude: Joi.number().required(),
|
||||
@@ -178,6 +385,14 @@ const locationsDict = Joi.object().keys({
|
||||
relevantText: Joi.string()
|
||||
});
|
||||
|
||||
export interface PassFields {
|
||||
auxiliaryFields: (Field & { row?: number })[];
|
||||
backFields: Field[];
|
||||
headerFields: Field[];
|
||||
primaryFields: Field[];
|
||||
secondaryFields: Field[];
|
||||
}
|
||||
|
||||
const passDict = Joi.object().keys({
|
||||
auxiliaryFields: Joi.array().items(Joi.object().keys({
|
||||
row: Joi.number().max(1).min(0)
|
||||
@@ -188,17 +403,48 @@ const passDict = Joi.object().keys({
|
||||
secondaryFields: Joi.array().items(field)
|
||||
});
|
||||
|
||||
export type TransitType = "PKTransitTypeAir" | "PKTransitTypeBoat" | "PKTransitTypeBus" | "PKTransitTypeGeneric" | "PKTransitTypeTrain";
|
||||
|
||||
const transitType = Joi.string().regex(/(PKTransitTypeAir|PKTransitTypeBoat|PKTransitTypeBus|PKTransitTypeGeneric|PKTransitTypeTrain)/);
|
||||
|
||||
export interface NFC {
|
||||
message: string;
|
||||
encryptionPublicKey?: string;
|
||||
}
|
||||
|
||||
const nfcDict = Joi.object().keys({
|
||||
message: Joi.string().required().max(64),
|
||||
encryptionPublicKey: Joi.string()
|
||||
});
|
||||
|
||||
// ************************************* //
|
||||
// *** Personalizable Passes Schemas *** //
|
||||
// ************************************* //
|
||||
|
||||
export interface Personalization {
|
||||
requiredPersonalizationFields: PRSField[];
|
||||
description: string;
|
||||
termsAndConditions?: string;
|
||||
}
|
||||
|
||||
type PRSField = "PKPassPersonalizationFieldName" | "PKPassPersonalizationFieldPostalCode" | "PKPassPersonalizationFieldEmailAddress" | "PKPassPersonalizationFieldPhoneNumber";
|
||||
|
||||
const personalizationDict = Joi.object().keys({
|
||||
requiredPersonalizationFields: Joi.array()
|
||||
.items("PKPassPersonalizationFieldName", "PKPassPersonalizationFieldPostalCode", "PKPassPersonalizationFieldEmailAddress", "PKPassPersonalizationFieldPhoneNumber")
|
||||
.required(),
|
||||
description: Joi.string().required(),
|
||||
termsAndConditions: Joi.string(),
|
||||
});
|
||||
|
||||
// --------- UTILITIES ---------- //
|
||||
|
||||
const schemas = {
|
||||
type Schemas = {
|
||||
[index: string]: Joi.ObjectSchema | Joi.StringSchema;
|
||||
};
|
||||
const schemas: Schemas = {
|
||||
instance,
|
||||
certificatesSchema,
|
||||
barcode,
|
||||
field,
|
||||
passDict,
|
||||
@@ -206,11 +452,12 @@ const schemas = {
|
||||
locationsDict,
|
||||
transitType,
|
||||
nfcDict,
|
||||
supportedOptions
|
||||
supportedOptions,
|
||||
personalizationDict
|
||||
};
|
||||
|
||||
function resolveSchemaName(name) {
|
||||
return schemas[name] || "";
|
||||
function resolveSchemaName(name: keyof Schemas) {
|
||||
return schemas[name] || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,18 +467,18 @@ function resolveSchemaName(name) {
|
||||
* @returns {boolean} - result of the check
|
||||
*/
|
||||
|
||||
function isValid(opts, schemaName) {
|
||||
let resolvedSchema = resolveSchemaName(schemaName);
|
||||
export function isValid(opts: any, schemaName: keyof Schemas): boolean {
|
||||
const resolvedSchema = resolveSchemaName(schemaName);
|
||||
|
||||
if (!resolvedSchema) {
|
||||
debug(`validation failed due to missing or mispelled schema name`);
|
||||
schemaDebug(`validation failed due to missing or mispelled schema name`);
|
||||
return false;
|
||||
}
|
||||
|
||||
let validation = Joi.validate(opts, resolvedSchema);
|
||||
const validation = Joi.validate(opts, resolvedSchema);
|
||||
|
||||
if (validation.error) {
|
||||
debug(`validation failed due to error: ${validation.error.message}`);
|
||||
schemaDebug(`validation failed due to error: ${validation.error.message}`);
|
||||
}
|
||||
|
||||
return !validation.error;
|
||||
@@ -244,19 +491,14 @@ function isValid(opts, schemaName) {
|
||||
* @returns {object} the filtered value or empty object
|
||||
*/
|
||||
|
||||
function getValidated(opts, schemaName) {
|
||||
export function getValidated<T extends Object>(opts: any, schemaName: keyof Schemas): T {
|
||||
let resolvedSchema = resolveSchemaName(schemaName);
|
||||
let validation = Joi.validate(opts, resolvedSchema, { stripUnknown: true });
|
||||
|
||||
if (validation.error) {
|
||||
debug(`Validation failed in getValidated due to error: ${validation.error.message}`);
|
||||
schemaDebug(`Validation failed in getValidated due to error: ${validation.error.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return validation.value;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isValid,
|
||||
getValidated
|
||||
};
|
||||
101
src/utils.js
101
src/utils.js
@@ -1,101 +0,0 @@
|
||||
const moment = require("moment");
|
||||
const { EOL } = require("os");
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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" && !(date instanceof Date)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const parsedDate = date instanceof Date ? moment(date).format() : 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";
|
||||
|
||||
const strings = Object.keys(lang)
|
||||
.map(key => `"${key}" = "${lang[key].replace(/"/g, /\\"/)}";`);
|
||||
|
||||
return Buffer.from(strings.join(EOL), "utf8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new object with custom length property
|
||||
* @param {number} value - the length
|
||||
* @param {Array<Object<string, any>>} source - the main sources of properties
|
||||
*/
|
||||
|
||||
function assignLength(length, ...sources) {
|
||||
return Object.assign({ length }, ...sources);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
assignLength,
|
||||
generateStringFile,
|
||||
removeHidden,
|
||||
dateToW3CString,
|
||||
isValidRGB
|
||||
};
|
||||
122
src/utils.ts
Normal file
122
src/utils.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import moment from "moment";
|
||||
import { EOL } from "os";
|
||||
import { PartitionedBundle, BundleUnit } from "./schema";
|
||||
import { sep } from "path";
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export function isValidRGB(value: string): boolean {
|
||||
if (!value || typeof value !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a date to W3C Standard format
|
||||
*
|
||||
* @function dateToW3Cstring
|
||||
* @params date - The date to be parsed
|
||||
* @returns - The parsed string if the parameter is valid,
|
||||
* undefined otherwise
|
||||
*/
|
||||
|
||||
export function dateToW3CString(date: Date) {
|
||||
if (!(date instanceof Date)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const parsedDate = moment(date).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[]}
|
||||
*/
|
||||
|
||||
export function removeHidden(from: Array<string>): Array<string> {
|
||||
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
|
||||
*/
|
||||
|
||||
export function generateStringFile(lang: { [index: string]: string }): Buffer {
|
||||
if (!Object.keys(lang).length) {
|
||||
return Buffer.from("", "utf8");
|
||||
}
|
||||
|
||||
// Pass.strings format is the following one for each row:
|
||||
// "key" = "value";
|
||||
|
||||
const strings = Object.keys(lang)
|
||||
.map(key => `"${key}" = "${lang[key].replace(/"/g, '\"')}";`);
|
||||
|
||||
return Buffer.from(strings.join(EOL), "utf8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a partition to split one bundle
|
||||
* to two
|
||||
* @param origin
|
||||
*/
|
||||
|
||||
export function splitBufferBundle(origin: Object): [PartitionedBundle["l10nBundle"], PartitionedBundle["bundle"]] {
|
||||
const keys = Object.keys(origin);
|
||||
return keys.reduce(([ l10n, bundle ], current) => {
|
||||
if (current.includes(".lproj")) {
|
||||
const pathComponents = current.split(sep);
|
||||
const lang = pathComponents[0];
|
||||
const file = pathComponents.slice(1).join("/");
|
||||
|
||||
(l10n[lang] || (l10n[lang] = {}))[file] = origin[current];
|
||||
|
||||
return [ l10n, bundle ];
|
||||
} else {
|
||||
return [ l10n, { ...bundle, [current]: origin[current] }];
|
||||
}
|
||||
}, [{},{}]);
|
||||
}
|
||||
|
||||
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 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]);
|
||||
}
|
||||
11
tsconfig.json
Normal file
11
tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"esModuleInterop": true,
|
||||
"newLine": "LF",
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules/"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user