mirror of
https://github.com/marcogll/passkit-generator.git
synced 2026-03-15 19:25:23 +00:00
Merged branch 'v3.0'
This commit is contained in:
627
API.md
627
API.md
@@ -1,627 +0,0 @@
|
|||||||
# API Reference
|
|
||||||
|
|
||||||
The flow of execution is really easy (once everything is set up):
|
|
||||||
|
|
||||||
* You get your data from somewhere
|
|
||||||
* You set the needed data in the pass through methods, overrides and data in fields
|
|
||||||
* You generate the pass stream through `.generate()` method
|
|
||||||
* Hooray 😄🎉
|
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
* 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 it is always valid for the reference:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const { createPass } = require("passkit-generator");
|
|
||||||
```
|
|
||||||
___
|
|
||||||
|
|
||||||
### Index:
|
|
||||||
|
|
||||||
* [Create a Pass](#pass_class_constructor)
|
|
||||||
* [Localizing Passes](#localizing_passes)
|
|
||||||
* [.localize()](#method_localize)
|
|
||||||
* Setting barcode
|
|
||||||
* [.barcodes()](#method_barcodes)
|
|
||||||
* [.barcode()](#method_barcode)
|
|
||||||
* Setting expiration / voiding the pass
|
|
||||||
* [.expiration()](#method_expiration)
|
|
||||||
* [.void()](#method_void)
|
|
||||||
* Setting relevance
|
|
||||||
* [.beacons()](#method_beacons)
|
|
||||||
* [.locations()](#method_locations)
|
|
||||||
* [.relevantDate()](#method_revdate)
|
|
||||||
* Setting NFC
|
|
||||||
* [.nfc()](#method_nfc)
|
|
||||||
* [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>
|
|
||||||
___
|
|
||||||
|
|
||||||
## Create a Pass
|
|
||||||
___
|
|
||||||
|
|
||||||
<a name="pass_class_constructor"></a>
|
|
||||||
|
|
||||||
#### constructor()
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const pass = await createPass({ ... }, Buffer.from([ ... ], { ... }));
|
|
||||||
```
|
|
||||||
|
|
||||||
**Returns**:
|
|
||||||
|
|
||||||
`Promise<Pass>`
|
|
||||||
|
|
||||||
**Arguments**:
|
|
||||||
|
|
||||||
| Key | Type | Description | Optional | Default Value |
|
|
||||||
|-----|------|---------------|:-------------:|:-----------:|
|
|
||||||
| 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.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.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 | `{ }`
|
|
||||||
| 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>
|
|
||||||
___
|
|
||||||
|
|
||||||
**Localizing Passes**:
|
|
||||||
___
|
|
||||||
|
|
||||||
Following Apple Developer Documentation, localization (L10N) is done by creating a `.lproj` folder for each language you want to translate your pass, each named with the relative [ISO-3166-1 alpha-2](https://it.wikipedia.org/wiki/ISO_3166-1_alpha-2) code (e.g. `en.lproj`).
|
|
||||||
|
|
||||||
In this library, localization can be done in three ways: **media-only** (images), **translations-only** or both.
|
|
||||||
The only difference 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.
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<a name="method_localize"></a>
|
|
||||||
|
|
||||||
#### .localize()
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
pass.localize(lang: string, options = {});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Returns**:
|
|
||||||
|
|
||||||
`Object<Pass> (this)`
|
|
||||||
|
|
||||||
**Description**:
|
|
||||||
|
|
||||||
You may want to create the folder and add translated media with no translations; else you may want to add only translations without different media or maybe both.
|
|
||||||
|
|
||||||
In the first case, create the `.lproj` folder in the model root folder and add the translated medias inside. Then use the method by passing only the first parameters, the code.
|
|
||||||
|
|
||||||
In the other two cases, you'll need to specify also the second argument (the translations to be added to `pass.strings` file, which will be added later).
|
|
||||||
|
|
||||||
**Arguments**:
|
|
||||||
|
|
||||||
| 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 \| { }
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
pass
|
|
||||||
.localize("it", {
|
|
||||||
"EVENT": "Evento",
|
|
||||||
"LOCATION": "Posizione"
|
|
||||||
})
|
|
||||||
.localize("de", {
|
|
||||||
"EVENT": "Ereignis",
|
|
||||||
"LOCATION": "Ort"
|
|
||||||
})
|
|
||||||
.localize("en")
|
|
||||||
```
|
|
||||||
|
|
||||||
<br><br>
|
|
||||||
___
|
|
||||||
|
|
||||||
**Setting barcodes**:
|
|
||||||
___
|
|
||||||
|
|
||||||
<a name="method_barcodes"></a>
|
|
||||||
|
|
||||||
#### .barcodes()
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
pass.barcodes(first: string | schema.Barcode, ...data: schema.Barcodes[]) : this;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Returns**:
|
|
||||||
|
|
||||||
`Object<Pass> (this)`
|
|
||||||
|
|
||||||
**Description**:
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Passing *N* barcode structures (see below), will only validate them and push only the valid ones.
|
|
||||||
|
|
||||||
This method will not take take of setting retro-compatibility, of which responsability is assigned to `.barcode()`.
|
|
||||||
|
|
||||||
**Arguments**:
|
|
||||||
|
|
||||||
| Key | Type | Description | Optional |
|
|
||||||
|-------|------|-------------|----------|
|
|
||||||
| first | `String` \| `schema.Barcode` | first value of barcodes | false
|
|
||||||
| ...data | `schema.Barcode[]` | the other barcode values | true
|
|
||||||
|
|
||||||
**Examples**:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
pass.barcodes("11424771526");
|
|
||||||
|
|
||||||
// or
|
|
||||||
|
|
||||||
pass.barcodes({
|
|
||||||
message: "11424771526",
|
|
||||||
format: "PKBarcodeFormatCode128"
|
|
||||||
altText: "11424771526"
|
|
||||||
}, {
|
|
||||||
message: "11424771526",
|
|
||||||
format: "PKBarcodeFormatQR"
|
|
||||||
altText: "11424771526"
|
|
||||||
}, {
|
|
||||||
message: "11424771526",
|
|
||||||
format: "PKBarcodeFormatPDF417"
|
|
||||||
altText: "11424771526"
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**See**: [PassKit Package Format Reference # Barcode Dictionary](https://apple.co/2myAbst)
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<a name="method_barcode"></a>
|
|
||||||
|
|
||||||
#### .barcode()
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
pass.barcode(chosenFormat: string): this;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Returns**:
|
|
||||||
|
|
||||||
`Object<Pass> (this)`
|
|
||||||
|
|
||||||
**Description**:
|
|
||||||
|
|
||||||
It will let you choose the format to be used in barcode property as backward compatibility.
|
|
||||||
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 | -
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Based on the previous (barcodes) example
|
|
||||||
pass
|
|
||||||
.barcodes(...)
|
|
||||||
.barcode("PKBarcodeFormatQR");
|
|
||||||
|
|
||||||
// This won't set the property since not found.
|
|
||||||
pass
|
|
||||||
.barcodes(...)
|
|
||||||
.barcode("PKBarcodeFormatAztec");
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
<br><br>
|
|
||||||
___
|
|
||||||
|
|
||||||
**Setting expiration / void the pass**:
|
|
||||||
___
|
|
||||||
|
|
||||||
<a name="method_expiration"></a>
|
|
||||||
|
|
||||||
#### .expiration()
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
pass.expiration(date: Date) : this;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Returns**:
|
|
||||||
|
|
||||||
`Object<Pass> (this)`
|
|
||||||
|
|
||||||
**Description**:
|
|
||||||
|
|
||||||
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 |
|
|
||||||
|-----|------|-------------|----------|
|
|
||||||
| date | String/date | The date on which the pass will expire | false
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<a name="method_void"></a>
|
|
||||||
|
|
||||||
#### .void()
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
pass.void();
|
|
||||||
```
|
|
||||||
|
|
||||||
**Returns**:
|
|
||||||
|
|
||||||
`Object<Pass> (this)`
|
|
||||||
|
|
||||||
**Description**:
|
|
||||||
|
|
||||||
It sets directly the pass as voided.
|
|
||||||
|
|
||||||
<br><br>
|
|
||||||
___
|
|
||||||
|
|
||||||
**Setting relevance**:
|
|
||||||
___
|
|
||||||
|
|
||||||
<a name="method_beacons"></a>
|
|
||||||
|
|
||||||
#### .beacons()
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
pass.beacons(...data: schema.Beacons[]): this;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Returns**:
|
|
||||||
|
|
||||||
`Object<Pass> (this)`
|
|
||||||
|
|
||||||
**Description**:
|
|
||||||
|
|
||||||
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 |
|
|
||||||
|-----|------|-------------|----------|:-------------:|
|
|
||||||
| ...data | [schema.Beacons[]](https://apple.co/2XPDoYX) \| `null` | The beacons structures | false | -
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
|
|
||||||
```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>
|
|
||||||
___
|
|
||||||
|
|
||||||
**NFC Support**:
|
|
||||||
___
|
|
||||||
|
|
||||||
<a name="method_nfc"></a>
|
|
||||||
|
|
||||||
#### .nfc()
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
pass.nfc(data: schema.NFC): this
|
|
||||||
```
|
|
||||||
|
|
||||||
**Returns**:
|
|
||||||
|
|
||||||
`Object<Pass> (this)`
|
|
||||||
|
|
||||||
**Description**:
|
|
||||||
|
|
||||||
It sets NFC info for the current pass. Passing `null` as parameter, will remove its value.
|
|
||||||
|
|
||||||
**Arguments**:
|
|
||||||
|
|
||||||
| 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>
|
|
||||||
|
|
||||||
<a name="getter_props"></a>
|
|
||||||
|
|
||||||
#### [Getter] .props()
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
pass.props;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Returns**:
|
|
||||||
|
|
||||||
An object containing all the current props;
|
|
||||||
|
|
||||||
**Description**:
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
It does not contain fields content (`primaryFields`, `secondaryFields`...) and `transitType`, which are still accessible through their own props.
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const currentLocations = pass.props["locations"];
|
|
||||||
pass.locations({
|
|
||||||
"latitude": 66.45725212,
|
|
||||||
"longitude": 33.010004420
|
|
||||||
}, {
|
|
||||||
"longitude": 4.42634523,
|
|
||||||
"latitude": 5.344233323352
|
|
||||||
},
|
|
||||||
...currentLocations);
|
|
||||||
```
|
|
||||||
|
|
||||||
<br><br>
|
|
||||||
|
|
||||||
<a name="prop_fields"></a>
|
|
||||||
___
|
|
||||||
|
|
||||||
**Setting Pass Structure Keys (primaryFields, secondaryFields, ...)**:
|
|
||||||
___
|
|
||||||
|
|
||||||
Unlike method-set properties or overrides, to set fields inside _areas_ (*primaryFields*, *secondaryFields*, *auxiliaryFields*, *headerFields*, *backFields*), this library make available a dedicated interface that extends native Array, to let you perform all the operations you need on the fields. Fields already available in pass.json, will be automatically loaded in the library. Therefore, reading one of the _areas_, will also show those that were loaded.
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
pass.headerFields.push({
|
|
||||||
key: "header1",
|
|
||||||
label: "Data",
|
|
||||||
value: "25 mag",
|
|
||||||
textAlignment: "PKTextAlignmentCenter"
|
|
||||||
}, {
|
|
||||||
key: "header2",
|
|
||||||
label: "Volo",
|
|
||||||
value: "EZY997",
|
|
||||||
textAlignment: "PKTextAlignmentCenter"
|
|
||||||
});
|
|
||||||
|
|
||||||
pass.primaryFields.pop();
|
|
||||||
```
|
|
||||||
|
|
||||||
**See**: [Passkit Package Format Reference # Field Dictionary Keys](https://apple.co/2NuDrUM)
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<a name="prop_transitType"></a>
|
|
||||||
|
|
||||||
#### .transitType
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
pass.transitType = "PKTransitTypeAir";
|
|
||||||
```
|
|
||||||
|
|
||||||
**Description**:
|
|
||||||
|
|
||||||
Since this property belongs to the "Structure Keys" but is not an "array of field dictionaries" like the other keys on the same level, a setter (and obv. also a getter) got included in this package to check it against a schema (which is like, "is a string and contains one of the following values: **PKTransitTypeAir**, **PKTransitTypeBoat**, **PKTransitTypeBus**, **PKTransitTypeGeneric**, **PKTransitTypeTrain**", as described in Passkit Package Format Reference).
|
|
||||||
|
|
||||||
<br><br>
|
|
||||||
___
|
|
||||||
|
|
||||||
**Generating the compiled Pass**
|
|
||||||
___
|
|
||||||
|
|
||||||
Generating the pass is the last step of the process (before enjoying 🎉).
|
|
||||||
Since the file format is `.pkpass` (which is a `.zip` file with changed MIME), this method will return a `Stream`, which can be used to be piped to a webserver response or to be written in the server.
|
|
||||||
As you can see in [examples folder](/examples), to send a .pkpass file, a basic webserver uses MIME-type `application/vnd.apple.pkpass`.
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<a name="method_generate"></a>
|
|
||||||
|
|
||||||
#### .generate()
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
pass.generate(): Stream;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Returns**: `Stream`
|
|
||||||
|
|
||||||
**Description**:
|
|
||||||
|
|
||||||
Creates a pass zip as Stream.
|
|
||||||
|
|
||||||
**Examples**:
|
|
||||||
|
|
||||||
```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.
|
|
||||||
172
README.md
172
README.md
@@ -4,7 +4,7 @@
|
|||||||
<img width="600" src="https://github.com/alexandercerutti/passkit-generator/raw/master/assets/logo.svg?sanitize=true" alt="Node Passkit Generator logo">
|
<img width="600" src="https://github.com/alexandercerutti/passkit-generator/raw/master/assets/logo.svg?sanitize=true" alt="Node Passkit Generator logo">
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<p align="center">Simple Node.js interface to generate customized <a href="https://developer.apple.com/wallet/">Apple Wallet Passes</a> for iOS.</p>
|
<p align="center">Simple Node.js interface to generate customized <a href="https://developer.apple.com/wallet/">Apple Wallet Passes</a> for iOS and WatchOS.</p>
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
@@ -14,18 +14,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
### Architecture
|
## Architecture
|
||||||
|
|
||||||
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, ...).
|
This library 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, ...), while keeping an eye on the different possible execution contexts.
|
||||||
|
|
||||||
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 creation and population might not fully happen in runtime. This library allows to create a pass from scratch, specify a folder model (template) or specify a set of buffers. In the last two cases, both should contain all the objects needed (static medias) and structure to make a pass work.
|
||||||
|
|
||||||
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).
|
Whenever adding files, through scratch, template or buffer, these will be read and pushed as they are in the resulting .zip file, while dynamic data will be patched (`pass.json` with props) 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).
|
|
||||||
|
|
||||||
> ⚠ Do not rely on branches outside "master", as might not be stable and will be removed once merged.
|
### Installation
|
||||||
|
|
||||||
### Install
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ npm install passkit-generator --save
|
$ npm install passkit-generator --save
|
||||||
@@ -35,36 +32,34 @@ $ npm install passkit-generator --save
|
|||||||
|
|
||||||
### API Documentation
|
### API Documentation
|
||||||
|
|
||||||
This package comes with an [API documentation](./API.md), that makes available a series of methods to create and customize passes.
|
This package comes with an [API Documentation Reference](https://github.com/alexandercerutti/passkit-generator/wiki/API-Documentation-Reference), available in wiki, that makes available a series of methods to create and customize passes.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Looking for the previous major version?
|
### Looking for the previous major version?
|
||||||
|
|
||||||
Check the [v1 branch](https://github.com/alexandercerutti/passkit-generator/tree/v1.6.8). That branch is kept for reference only.
|
Check the [v2 tag](https://github.com/alexandercerutti/passkit-generator/tree/v2.0.8). That tag is kept for reference only.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Coming from the previous major version?
|
### Coming from the previous major version?
|
||||||
|
|
||||||
Look at the [Migration Guide](https://github.com/alexandercerutti/passkit-generator/wiki/Migrating-from-v1-to-v2).
|
Look at the [Migration Guide](https://github.com/alexandercerutti/passkit-generator/wiki/Migrating-from-v2-to-v3).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Get Started
|
## Getting Started
|
||||||
|
|
||||||
##### Model
|
##### Model
|
||||||
|
|
||||||
The first thing you'll have to do, is to start creating a model. A model contains all the basic pass data that compose the Pass identity.
|
Assuming that you don't have a model yet, the first thing you'll have to do, is creating one. A model contains all the basic pass data that compose the Pass identity.
|
||||||
These data can be files (icon, thumbnails, ...), or pieces of information to be written in `pass.json` (Pass type identifier, Team Identifier, colors, ...).
|
These data can be files (icon, thumbnails, ...), or pieces of information to be written in `pass.json` (Pass type identifier, Team Identifier, colors, ...) and whatever you know that likely won't be customized on runtime.
|
||||||
|
|
||||||
This package allows to use two kinds of models: **Folder Model** or **Buffer Model**. 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.
|
When starting from zero, the best suggested solution is to use a Template (folder) to start with, as it will allow an easier access to all the files and data. Nothing will prevent you using a buffer model or creating a pass from scratch, but they are meant for an advanced usage or different contexts (e.g. running a cloud function might require a scratch model for faster startup, without storing the model in a "data bucket").
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
> To keep a model in memory, the method [`createAbstractModel`](https://github.com/alexandercerutti/passkit-generator/blob/master/API.md#create-an-abstract-model) has been created.
|
**To maintain a pass model available during the run-time, a PKPass instance can be created from whatever source, and then used as a template through `PKPass.from`**.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> Using the .pass extension is a best practice, showing that the directory is a pass package.
|
> 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)).
|
> ([Build your first pass - Apple Developer Portal](https://apple.co/2LYXWo3)).
|
||||||
@@ -75,18 +70,13 @@ If omitted in the configuration (as in [Usage Example](#usage_example), at "mode
|
|||||||
---
|
---
|
||||||
|
|
||||||
Model creation can be performed both manually or with the auxiliary of a web tool I developed, [Passkit Visual Designer](https://pkvd.app), which will let you design your model through a neat user interface.
|
Model creation can be performed both manually or with the auxiliary of a web tool I developed, [Passkit Visual Designer](https://pkvd.app), which will let you design your model through a neat user interface.
|
||||||
It will output a .zip file that you can decompress and use it as both file model and buffer model.
|
It will output a .zip file that you can decompress and use as source.
|
||||||
|
|
||||||
Since `.pass` extension is required, **it will be up to you to unzip the generated model in a .pass folder**.
|
---
|
||||||
|
|
||||||
```bash
|
You can follow the [Apple Developer documentation](https://apple.co/2wuJLC1) (_Package Structure_) to build a correct pass model. The **icon is required** in order to make the pass work. Omitting an icon resolution, might make a pass work on a device (e.g. Mac) but not on another (e.g. iPhone). _Manifest.json_ and _signature_ will be automatically ignored from the model and generated in runtime.
|
||||||
$ cd yourProjectDir;
|
|
||||||
$ mkdir passModels && mkdir $_/myFirstModel.pass && cd $_;
|
|
||||||
```
|
|
||||||
|
|
||||||
Follow the [Apple Developer documentation](https://apple.co/2wuJLC1) (_Package Structure_) to build a correct pass model. The **icon is required** in order to make the pass work. _Manifest.json_ and _signature_ will be automatically ignored from the model and generated in runtime.
|
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#localizing-passes) in the API documentation.
|
||||||
|
|
||||||
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`.
|
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`.
|
||||||
|
|
||||||
@@ -98,7 +88,7 @@ Create a `pass.json` by taking example from examples folder models or the one pr
|
|||||||
{
|
{
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"passTypeIdentifier": "pass.<bundle id>",
|
"passTypeIdentifier": "pass.<bundle id>",
|
||||||
"teamIdentifier": "<here your team identifier>",
|
"teamIdentifier": "<your team identifier>",
|
||||||
"organizationName": "<your organization name>",
|
"organizationName": "<your organization name>",
|
||||||
"description": "A localizable description of your pass. To do so, put here a placeholder.",
|
"description": "A localizable description of your pass. To do so, put here a placeholder.",
|
||||||
"boardingPass": {}
|
"boardingPass": {}
|
||||||
@@ -112,37 +102,8 @@ Create a `pass.json` by taking example from examples folder models or the one pr
|
|||||||
The third step is about the developer and WWDR certificates. I suggest you to create a certificate-dedicated folder inside your working directory (e.g. `./certs`) to contain everything concerning the certificates.
|
The third step is about the developer and WWDR certificates. I suggest you to create a certificate-dedicated folder inside your working directory (e.g. `./certs`) to contain everything concerning the certificates.
|
||||||
|
|
||||||
This is a standard procedure: you would have to do it also without using this library. We'll use OpenSSL to complete our work (or to do it entirely, if only on terminal), so be sure to have it installed.
|
This is a standard procedure: you would have to do it also without using this library. We'll use OpenSSL to complete our work (or to do it entirely, if only on terminal), so be sure to have it installed.
|
||||||
You'll need the following three elements:
|
|
||||||
|
|
||||||
- Apple WWDR (_Worldwide Developer Relationship_) certificate
|
[Follow the **FULL GUIDE in wiki** to get all the files you need to proceed](https://github.com/alexandercerutti/passkit-generator/wiki/Generating-Certificates).
|
||||||
- Signer certificate
|
|
||||||
- Signer key
|
|
||||||
|
|
||||||
While WWDR can be obtained from [Apple PKI Portal](https://www.apple.com/certificateauthority/), to get the `signer key` and the `certificate`, you'll have to get first a `Certificate Signing Request` (`.certSigningRequest` file) and upload it to Apple Developers Portal, at [Pass Types Identifiers](https://developer.apple.com/account/ios/identifier/passTypeId) (open it, it's worth it 😜).
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
> **If you don't have access to macOS** (or you are a terminal enthusiast), **follow [these steps](./non-macOS-steps.md) instead.**
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
1. Create a new pass type identifier and provide it with a Name and a reverse-domain bundle id (starting with "pass."). You will put this identifier as value for `passTypeIdentifier` in `pass.json` file.
|
|
||||||
2. Confirm and register the new identifier.
|
|
||||||
3. Go back to the pass type identifiers, click on your new pass id and edit it.
|
|
||||||
4. Click "Create Certificate" button and follow the instructions until you won't download a certificate like `pass.cer`. (here you'll generate the `.certSigningRequest` file to be uploaded).
|
|
||||||
5. Open the downloaded certificate. Go in "Certificates" on left in macOS Keychain access and `right-click > Export "\<certname\>"`. Choose a password (and write it down) and you will get a PKCS#12 file (`.p12`).
|
|
||||||
6. Open terminal, place where you want to save the files and insert the following OpenSSL commands changing the contents between angular brackets. You'll have to choose a secret passphrase (and write it down) that you'll use also in the application.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Creating and changing dir
|
|
||||||
$ mkdir "certs" && cd $_
|
|
||||||
# Extracting key and cert from pkcs12
|
|
||||||
$ openssl pkcs12 -in <cert-name>.p12 -clcerts -nokeys -out signerCert.pem -passin pass:<your-password>
|
|
||||||
$ openssl pkcs12 -in <cert-name>.p12 -nocerts -out signerKey.pem -passin pass:<your-password> -passout pass:<secret-passphrase>
|
|
||||||
```
|
|
||||||
|
|
||||||
7. Execute step 5 also for the WWDR certificate (`.cer`) you downloaded from Apple PKI portal (default name: _AppleWWDRCA.cer_) but instead exporting it as PKCS#12 (`.p12` - you'll also be unable to do that), export it as PEM (`.pem`) file.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -154,37 +115,37 @@ While WWDR can be obtained from [Apple PKI Portal](https://www.apple.com/certifi
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
/**
|
/**
|
||||||
* Use `const { createPass } = require("passkit-generator");`
|
* Use `const { PKPass } = require("passkit-generator");`
|
||||||
* for usage in pure Node.js. Please note that `Pass` is only exported
|
* for usage in pure Node.js
|
||||||
* as Typescript type.
|
|
||||||
*/
|
*/
|
||||||
import { createPass, Pass } from "passkit-generator";
|
import { PKPass } from "passkit-generator";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const examplePass = await createPass({
|
const pass = PKPass.from({
|
||||||
model: "./passModels/myFirstModel",
|
model: "./passModels/myFirstModel",
|
||||||
certificates: {
|
certificates: {
|
||||||
wwdr: "./certs/wwdr.pem",
|
wwdr: "./certs/wwdr.pem",
|
||||||
signerCert: "./certs/signercert.pem",
|
signerCert: "./certs/signercert.pem",
|
||||||
signerKey: {
|
signerKey: "./certs/signerkey.pem",
|
||||||
keyFile: "./certs/signerkey.pem",
|
signerKeyPassphrase: "123456"
|
||||||
passphrase: "123456"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
overrides: {
|
}, {
|
||||||
// keys to be added or overridden
|
// keys to be added or overridden
|
||||||
serialNumber: "AAGH44625236dddaffbda"
|
serialNumber: "AAGH44625236dddaffbda"
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Adding some settings to be written inside pass.json
|
// Adding some settings to be written inside pass.json
|
||||||
examplePass.localize("en", { ... });
|
pass.localize("en", { ... });
|
||||||
examplePass.barcode("36478105430"); // Random value
|
pass.setBarcodes("36478105430"); // Random value
|
||||||
|
|
||||||
// Generate the stream .pkpass file stream
|
// Generate the stream .pkpass file stream
|
||||||
const stream: Stream = examplePass.generate();
|
const stream = pass.getAsStream();
|
||||||
|
|
||||||
doSomethingWithTheStream(stream);
|
doSomethingWithTheStream(stream);
|
||||||
|
|
||||||
|
// or
|
||||||
|
|
||||||
|
const buffer = pass.getAsBuffer();
|
||||||
|
doSomethingWithTheBuffer(buffer);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
doSomethingWithTheError(err);
|
doSomethingWithTheError(err);
|
||||||
}
|
}
|
||||||
@@ -194,42 +155,41 @@ try {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
/**
|
/**
|
||||||
* Use `const { createPass } = require("passkit-generator");`
|
* Use `const { PKPass } = require("passkit-generator");`
|
||||||
* for usage in pure Node.js. Please note that `Pass` is only exported
|
* for usage in pure Node.js
|
||||||
* as Typescript type.
|
|
||||||
*/
|
*/
|
||||||
import { createPass, Pass } from "passkit-generator";
|
import { PKPass } from "passkit-generator";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const examplePass = await createPass({
|
const examplePass = new PKPass({
|
||||||
model: {
|
"thumbnail": Buffer.from([ ... ]),
|
||||||
"thumbnail": Buffer.from([ ... ]),
|
"icon": Buffer.from([ ... ]),
|
||||||
"icon": Buffer.from([ ... ]),
|
"pass.json": Buffer.from([ ... ]),
|
||||||
"pass.json": Buffer.from([ ... ]),
|
"it.lproj/pass.strings": Buffer.from([ ... ])
|
||||||
"it.lproj/pass.strings": Buffer.from([ ... ])
|
},
|
||||||
},
|
{
|
||||||
certificates: {
|
wwdr: "./certs/wwdr.pem",
|
||||||
wwdr: "./certs/wwdr.pem",
|
signerCert: "./certs/signercert.pem",
|
||||||
signerCert: "./certs/signercert.pem",
|
signerKey: "./certs/signerkey.pem",
|
||||||
signerKey: {
|
signerKeyPassphrase: "123456",
|
||||||
keyFile: "./certs/signerkey.pem",
|
},
|
||||||
passphrase: "123456"
|
{
|
||||||
}
|
// keys to be added or overridden
|
||||||
},
|
serialNumber: "AAGH44625236dddaffbda"
|
||||||
overrides: {
|
|
||||||
// keys to be added or overridden
|
|
||||||
serialNumber: "AAGH44625236dddaffbda"
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Adding some settings to be written inside pass.json
|
// Adding some settings to be written inside pass.json
|
||||||
examplePass.localize("en", { ... });
|
pass.localize("en", { ... });
|
||||||
examplePass.barcode("36478105430"); // Random value
|
pass.setBarcodes("36478105430"); // Random value
|
||||||
|
|
||||||
// Generate the stream .pkpass file stream
|
// Generate the stream .pkpass file stream
|
||||||
const stream: Stream = examplePass.generate();
|
const stream = pass.getAsStream();
|
||||||
|
|
||||||
doSomethingWithTheStream(stream);
|
doSomethingWithTheStream(stream);
|
||||||
|
|
||||||
|
// or
|
||||||
|
|
||||||
|
const buffer = pass.getAsBuffer();
|
||||||
|
doSomethingWithTheBuffer(buffer);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
doSomethingWithTheError(err);
|
doSomethingWithTheError(err);
|
||||||
}
|
}
|
||||||
@@ -242,7 +202,7 @@ For more complex usage examples, please refer to [examples](https://github.com/a
|
|||||||
|
|
||||||
## Other
|
## Other
|
||||||
|
|
||||||
If you used this package in any of your projects, feel free to open a topic in issues to tell me and include a project description or link (for companies). 😊 You'll make me feel like my time hasn't been wasted, even if it had not anyway because I learnt a lot of things by creating this.
|
If you used this package in any of your projects, feel free to open a topic in issues to tell me and include a project description or link (for companies). 😊 You'll make me feel like my time hasn't been wasted, even if it had not anyway because I learnt and keep learning a lot of things by creating this.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -255,8 +215,8 @@ Made with ❤️ in Italy.
|
|||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
A big thanks to all the people that contributed to improve this package. Any contribution is welcome. Do you have an idea to make this improve or something to say? Open a topic in the issues and we'll discuss together! Thank you ❤
|
A big thanks to all the people that contributed to improve this package. Any contribution is welcome. Do you have an idea to make this improve or something to say? Open a topic in the issues and we'll discuss together! Thank you ❤️
|
||||||
Also a big big big big thank you to all the financial contributors!
|
Also a big big big big thank you to all the financial contributors, which help me maintain the development of this package ❤️!
|
||||||
|
|
||||||
### Code Contributors
|
### Code Contributors
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# Examples
|
|
||||||
|
|
||||||
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;
|
|
||||||
$ cd examples && npm install;
|
|
||||||
$ npm run build;
|
|
||||||
$ npm run example <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.
|
|
||||||
To make them work, you'll have to edit both certificates and model path.
|
|
||||||
|
|
||||||
Visit [http://localhost:8080/gen/examplePass](http://localhost:8080/gen/examplePass) to get the pass. Replace "examplePass" with the pass name in models folder.
|
|
||||||
Please note that `field.js` example will force you to download `exampleBooking.pass`, no matter what.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Every contribution is really appreciated. ❤️ Thank you!
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
import genRoute, { app } from "./webserver";
|
|
||||||
import {
|
|
||||||
createPass,
|
|
||||||
createAbstractModel,
|
|
||||||
AbstractModel,
|
|
||||||
} from "passkit-generator";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
let abstractModel: AbstractModel;
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
abstractModel = await createAbstractModel({
|
|
||||||
model: path.resolve(__dirname, `../models/exampleBooking.pass`),
|
|
||||||
certificates: {
|
|
||||||
wwdr: path.resolve(__dirname, "../../certificates/WWDR.pem"),
|
|
||||||
signerCert: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../../certificates/signerCert.pem",
|
|
||||||
),
|
|
||||||
signerKey: {
|
|
||||||
keyFile: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../../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(/-/gi, "");
|
|
||||||
|
|
||||||
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,69 +0,0 @@
|
|||||||
/**
|
|
||||||
* Example for adding additional buffers to the
|
|
||||||
* model. These buffers, represent contents that
|
|
||||||
* get fetched in runtime and that may vary
|
|
||||||
* at any time, for any reason.
|
|
||||||
* For the example purposes, we are using a static URL.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import app from "./webserver";
|
|
||||||
import fetch from "node-fetch";
|
|
||||||
import { createPass } from "passkit-generator";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
app.all(async function manageRequest(request, response) {
|
|
||||||
let passName =
|
|
||||||
request.params.modelName +
|
|
||||||
"_" +
|
|
||||||
new Date().toISOString().split("T")[0].replace(/-/gi, "");
|
|
||||||
|
|
||||||
const avatar = await fetch(
|
|
||||||
"https://s.gravatar.com/avatar/83cd11399b7ea79977bc302f3931ee52?size=32&default=retro",
|
|
||||||
).then((res) => res.buffer());
|
|
||||||
|
|
||||||
const passConfig = {
|
|
||||||
model: path.resolve(__dirname, `../models/${request.params.modelName}`),
|
|
||||||
certificates: {
|
|
||||||
wwdr: path.resolve(__dirname, "../../certificates/WWDR.pem"),
|
|
||||||
signerCert: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../../certificates/signerCert.pem",
|
|
||||||
),
|
|
||||||
signerKey: {
|
|
||||||
keyFile: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../../certificates/signerKey.pem",
|
|
||||||
),
|
|
||||||
passphrase: "123456",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
overrides: request.body || request.params || request.query,
|
|
||||||
};
|
|
||||||
|
|
||||||
const additionalBuffers = {
|
|
||||||
"thumbnail@2x.png": avatar,
|
|
||||||
// If we are using L10N folders, we can set the content like this
|
|
||||||
"en.lproj/thumbnail@2x.png": avatar,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pass = await createPass(passConfig, additionalBuffers);
|
|
||||||
|
|
||||||
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,97 +0,0 @@
|
|||||||
/**
|
|
||||||
* .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 "passkit-generator";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
app.all(async function manageRequest(request, response) {
|
|
||||||
const passName =
|
|
||||||
request.params.modelName +
|
|
||||||
"_" +
|
|
||||||
new Date().toISOString().split("T")[0].replace(/-/gi, "");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pass = await createPass({
|
|
||||||
model: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
`../models/${request.params.modelName}`,
|
|
||||||
),
|
|
||||||
certificates: {
|
|
||||||
wwdr: path.resolve(__dirname, "../../certificates/WWDR.pem"),
|
|
||||||
signerCert: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../../certificates/signerCert.pem",
|
|
||||||
),
|
|
||||||
signerKey: {
|
|
||||||
keyFile: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../../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,78 +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.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
import app from "./webserver";
|
|
||||||
import { createPass } from "passkit-generator";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
let passName =
|
|
||||||
request.params.modelName +
|
|
||||||
"_" +
|
|
||||||
new Date().toISOString().split("T")[0].replace(/-/gi, "");
|
|
||||||
|
|
||||||
try {
|
|
||||||
let pass = await createPass({
|
|
||||||
model: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
`../models/${request.params.modelName}`,
|
|
||||||
),
|
|
||||||
certificates: {
|
|
||||||
wwdr: path.resolve(__dirname, "../../certificates/WWDR.pem"),
|
|
||||||
signerCert: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../../certificates/signerCert.pem",
|
|
||||||
),
|
|
||||||
signerKey: {
|
|
||||||
keyFile: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../../certificates/signerKey.pem",
|
|
||||||
),
|
|
||||||
passphrase: "123456",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
overrides: request.body || request.params || request.query,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (request.query.fn === "void") {
|
|
||||||
pass.void();
|
|
||||||
} else if (request.query.fn === "expiration") {
|
|
||||||
// 2 minutes later...
|
|
||||||
const d = new Date();
|
|
||||||
d.setMinutes(d.getMinutes() + 2);
|
|
||||||
|
|
||||||
// setting the expiration
|
|
||||||
pass.expiration(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,89 +0,0 @@
|
|||||||
/**
|
|
||||||
* .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 "passkit-generator";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
app.all(async function manageRequest(request, response) {
|
|
||||||
const passName =
|
|
||||||
request.params.modelName +
|
|
||||||
"_" +
|
|
||||||
new Date().toISOString().split("T")[0].replace(/-/gi, "");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pass = await createPass({
|
|
||||||
model: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
`../models/${request.params.modelName}`,
|
|
||||||
),
|
|
||||||
certificates: {
|
|
||||||
wwdr: path.resolve(__dirname, "../../certificates/WWDR.pem"),
|
|
||||||
signerCert: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../../certificates/signerCert.pem",
|
|
||||||
),
|
|
||||||
signerKey: {
|
|
||||||
keyFile: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../../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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"passTypeIdentifier": "pass.com.example.myapp",
|
"passTypeIdentifier": "pass.com.passkitgenerator",
|
||||||
"serialNumber": "nmyuxofgna",
|
"serialNumber": "nmyuxofgna",
|
||||||
"teamIdentifier": "F53WB8AE67",
|
"teamIdentifier": "F53WB8AE67",
|
||||||
"webServiceURL": "https://192.168.1.254:80/",
|
"webServiceURL": "https://192.168.1.254:80/",
|
||||||
@@ -23,11 +23,6 @@
|
|||||||
"messageEncoding": "iso-8859-1"
|
"messageEncoding": "iso-8859-1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"barcode": {
|
|
||||||
"message": "123456789",
|
|
||||||
"format": "PKBarcodeFormatQR",
|
|
||||||
"messageEncoding": "iso-8859-1"
|
|
||||||
},
|
|
||||||
"organizationName": "Apple Inc.",
|
"organizationName": "Apple Inc.",
|
||||||
"description": "A Booking pass",
|
"description": "A Booking pass",
|
||||||
"foregroundColor": "rgb(255, 255, 255)",
|
"foregroundColor": "rgb(255, 255, 255)",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"passTypeIdentifier": "pass.com.example.myapp",
|
"passTypeIdentifier": "pass.com.passkitgenerator",
|
||||||
"serialNumber": "nmyuxofgna",
|
"serialNumber": "nmyuxofgna",
|
||||||
"teamIdentifier": "F53WB8AE67",
|
"teamIdentifier": "F53WB8AE67",
|
||||||
"webServiceURL": "https://192.168.1.254:80/",
|
"webServiceURL": "https://192.168.1.254:80/",
|
||||||
@@ -23,11 +23,6 @@
|
|||||||
"messageEncoding": "iso-8859-1"
|
"messageEncoding": "iso-8859-1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"barcode": {
|
|
||||||
"message": "123456789",
|
|
||||||
"format": "PKBarcodeFormatQR",
|
|
||||||
"messageEncoding": "iso-8859-1"
|
|
||||||
},
|
|
||||||
"organizationName": "Apple Inc.",
|
"organizationName": "Apple Inc.",
|
||||||
"description": "Apple Event Ticket",
|
"description": "Apple Event Ticket",
|
||||||
"foregroundColor": "rgb(255, 255, 255)",
|
"foregroundColor": "rgb(255, 255, 255)",
|
||||||
|
|||||||
467
examples/package-lock.json
generated
467
examples/package-lock.json
generated
@@ -1,467 +0,0 @@
|
|||||||
{
|
|
||||||
"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==",
|
|
||||||
"dev": true,
|
|
||||||
"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==",
|
|
||||||
"dev": true,
|
|
||||||
"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==",
|
|
||||||
"dev": true,
|
|
||||||
"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==",
|
|
||||||
"dev": true,
|
|
||||||
"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==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@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==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/node-fetch": {
|
|
||||||
"version": "2.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.0.tgz",
|
|
||||||
"integrity": "sha512-TLFRywthBgL68auWj+ziWu+vnmmcHCDFC/sqCOQf1xTz4hRq8cu79z8CtHU9lncExGBsB8fXA4TiLDLt6xvMzw==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@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==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@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==",
|
|
||||||
"dev": true,
|
|
||||||
"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=="
|
|
||||||
},
|
|
||||||
"node-fetch": {
|
|
||||||
"version": "2.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
|
||||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
|
|
||||||
},
|
|
||||||
"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=="
|
|
||||||
},
|
|
||||||
"tslib": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A=="
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"typescript": {
|
|
||||||
"version": "4.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
|
|
||||||
"integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"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="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "examples",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"private": true,
|
|
||||||
"description": "Passkit-generator examples",
|
|
||||||
"author": "Alexander P. Cerutti <cerutti.alexander@gmail.com>",
|
|
||||||
"license": "ISC",
|
|
||||||
"scripts": {
|
|
||||||
"preinstall": "npm run clear:deps && npm unlink --no-save passkit-generator",
|
|
||||||
"postinstall": "cd .. && npm run build && npm link && cd examples && npm link passkit-generator",
|
|
||||||
"example": "cd build && node",
|
|
||||||
"example:debug": "cd build && node --inspect-brk",
|
|
||||||
"build": "npm run build:clear && npm install && npx tsc",
|
|
||||||
"build:clear": "rm -rf build",
|
|
||||||
"clear:deps": "rm -rf node_modules"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"passkit-generator": "latest"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"express": "^4.17.1",
|
|
||||||
"node-fetch": "^2.6.1",
|
|
||||||
"tslib": "^2.1.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/express": "^4.17.0",
|
|
||||||
"@types/node-fetch": "^2.5.0",
|
|
||||||
"typescript": "^4.1.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
35
examples/self-hosted/README.md
Normal file
35
examples/self-hosted/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Examples
|
||||||
|
|
||||||
|
This is examples folder. These examples are used to test new features and as sample showcases.
|
||||||
|
|
||||||
|
Each example owns an endpoint where a pass can be reached. This project is build upon Express.js, which is required to be installed.
|
||||||
|
|
||||||
|
Typescript compilation is done automatically through `ts-node`.
|
||||||
|
|
||||||
|
Assuming you already have cloned this repository, installed its dependencies through `npm install` and moved to `examples/self-hosted`, run these commands:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ npm install;
|
||||||
|
$ npm run example;
|
||||||
|
```
|
||||||
|
|
||||||
|
Certificates paths in examples are linked to a folder `certificates` in the root of this project which is not provided.
|
||||||
|
To make them work, you'll have to edit both certificates and model path.
|
||||||
|
|
||||||
|
Every example runs on `0.0.0.0:8080`. Visit `http://localhost:8080/:example/:modelName`, by replacing `:example` with one of the following and `:modelName` with one inside models folder.
|
||||||
|
|
||||||
|
Please note that `field.js` example will force you to download `exampleBooking.pass`, no matter what.
|
||||||
|
|
||||||
|
| Example name | Endpoint name | Additional notes |
|
||||||
|
| -------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| localize | `/localize` | - |
|
||||||
|
| fields | `/fields` | - |
|
||||||
|
| expirationDate | `/expirationDate` | Accepts a required parameter in query string `fn`, which can be either `expiration` or `void`, to switch generated example. |
|
||||||
|
| scratch | `/scratch` | - |
|
||||||
|
| PKPass.from | pkpassfrom | - |
|
||||||
|
| barcodes | `/barcodes` | Using `?alt=true` query parameter, will lead to barcode string message usage instead of selected ones |
|
||||||
|
| pkpasses | `/pkpasses` | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Every contribution is really appreciated. ❤️ Thank you!
|
||||||
1590
examples/self-hosted/package-lock.json
generated
Normal file
1590
examples/self-hosted/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
examples/self-hosted/package.json
Normal file
28
examples/self-hosted/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "examples-self-hosted",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Passkit-generator self-hosted examples",
|
||||||
|
"author": "Alexander P. Cerutti <cerutti.alexander@gmail.com>",
|
||||||
|
"license": "ISC",
|
||||||
|
"scripts": {
|
||||||
|
"preinstall": "npm run clear:deps && npm unlink --no-save passkit-generator",
|
||||||
|
"postinstall": "npm --prefix ../.. run build && npm --prefix ../.. link && npm link passkit-generator",
|
||||||
|
"example": "npx ts-node src/index.ts",
|
||||||
|
"example:debug": "node -r ts-node/register --inspect-brk src/index.ts",
|
||||||
|
"clear:deps": "rm -rf node_modules"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"passkit-generator": "latest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"node-fetch": "^3.0.0",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "4.17.8",
|
||||||
|
"ts-node": "^10.4.0",
|
||||||
|
"typescript": "^4.4.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
148
examples/self-hosted/src/PKPass.from.ts
Normal file
148
examples/self-hosted/src/PKPass.from.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* PKPass.from static method example.
|
||||||
|
* Here it is showed manual model reading and
|
||||||
|
* creating through another PKPass because in the other
|
||||||
|
* examples, creation through templates is already shown
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { app } from "./webserver";
|
||||||
|
import { getCertificates } from "./shared";
|
||||||
|
import path from "path";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import { PKPass } from "passkit-generator";
|
||||||
|
|
||||||
|
import * as Utils from "passkit-generator/lib/utils";
|
||||||
|
|
||||||
|
// ******************************************** //
|
||||||
|
// *** CODE FROM GET MODEL FOLDER INTERNALS *** //
|
||||||
|
// ******************************************** //
|
||||||
|
|
||||||
|
async function readFileOrDirectory(filePath: string) {
|
||||||
|
if ((await fs.lstat(filePath)).isDirectory()) {
|
||||||
|
return Promise.all(await readDirectory(filePath));
|
||||||
|
} else {
|
||||||
|
return fs
|
||||||
|
.readFile(filePath)
|
||||||
|
.then((content) => getObjectFromModelFile(filePath, content, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an object containing the parsed fileName
|
||||||
|
* from a path along with its content.
|
||||||
|
*
|
||||||
|
* @param filePath
|
||||||
|
* @param content
|
||||||
|
* @param depthFromEnd - used to preserve localization lproj content
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getObjectFromModelFile(
|
||||||
|
filePath: string,
|
||||||
|
content: Buffer,
|
||||||
|
depthFromEnd: number,
|
||||||
|
) {
|
||||||
|
const fileComponents = filePath.split(path.sep);
|
||||||
|
const fileName = fileComponents
|
||||||
|
.slice(fileComponents.length - depthFromEnd)
|
||||||
|
.join(path.sep);
|
||||||
|
|
||||||
|
return { [fileName]: content };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a directory and returns all the files in it
|
||||||
|
* as an Array<Promise>
|
||||||
|
*
|
||||||
|
* @param filePath
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function readDirectory(filePath: string) {
|
||||||
|
const dirContent = await fs.readdir(filePath).then(Utils.removeHidden);
|
||||||
|
|
||||||
|
return dirContent.map(async (fileName) => {
|
||||||
|
const content = await fs.readFile(path.resolve(filePath, fileName));
|
||||||
|
return getObjectFromModelFile(
|
||||||
|
path.resolve(filePath, fileName),
|
||||||
|
content,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// *************************** //
|
||||||
|
// *** EXAMPLE FROM NOW ON *** //
|
||||||
|
// *************************** //
|
||||||
|
|
||||||
|
const passTemplate = new Promise<PKPass>(async (resolve) => {
|
||||||
|
const modelPath = path.resolve(__dirname, `../../models/examplePass.pass`);
|
||||||
|
const [modelFilesList, certificates] = await Promise.all([
|
||||||
|
fs.readdir(modelPath),
|
||||||
|
getCertificates(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const modelRecords = (
|
||||||
|
await Promise.all(
|
||||||
|
/**
|
||||||
|
* Obtaining flattened array of buffer records
|
||||||
|
* containing file name and the buffer itself.
|
||||||
|
*
|
||||||
|
* This goes also to read every nested l10n
|
||||||
|
* subfolder.
|
||||||
|
*/
|
||||||
|
|
||||||
|
modelFilesList.map((fileOrDirectoryPath) => {
|
||||||
|
const fullPath = path.resolve(modelPath, fileOrDirectoryPath);
|
||||||
|
|
||||||
|
return readFileOrDirectory(fullPath);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.flat(1)
|
||||||
|
.reduce((acc, current) => ({ ...acc, ...current }), {});
|
||||||
|
|
||||||
|
/** Creating a PKPass Template */
|
||||||
|
|
||||||
|
return resolve(
|
||||||
|
new PKPass(modelRecords, {
|
||||||
|
wwdr: certificates.wwdr,
|
||||||
|
signerCert: certificates.signerCert,
|
||||||
|
signerKey: certificates.signerKey,
|
||||||
|
signerKeyPassphrase: certificates.signerKeyPassphrase,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.route("/pkpassfrom/:modelName").get(async (request, response) => {
|
||||||
|
const passName =
|
||||||
|
request.params.modelName +
|
||||||
|
"_" +
|
||||||
|
new Date().toISOString().split("T")[0].replace(/-/gi, "");
|
||||||
|
|
||||||
|
const templatePass = await passTemplate;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pass = await PKPass.from(
|
||||||
|
templatePass,
|
||||||
|
request.body || request.params || request.query,
|
||||||
|
);
|
||||||
|
|
||||||
|
const stream = pass.getAsStream();
|
||||||
|
|
||||||
|
response.set({
|
||||||
|
"Content-type": pass.mimeType,
|
||||||
|
"Content-disposition": `attachment; filename=${passName}.pkpass`,
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.pipe(response);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
|
||||||
|
response.set({
|
||||||
|
"Content-type": "text/html",
|
||||||
|
});
|
||||||
|
|
||||||
|
response.send(err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
151
examples/self-hosted/src/PKPasses.ts
Normal file
151
examples/self-hosted/src/PKPasses.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
/**
|
||||||
|
* PKPasses generation through PKPass.pack static method
|
||||||
|
* example.
|
||||||
|
* Here it is showed manual model reading and
|
||||||
|
* creating through another PKPass because in the other
|
||||||
|
* examples, creation through templates is already shown
|
||||||
|
*
|
||||||
|
* PLEASE NOTE THAT, AT TIME OF WRITING, THIS EXAMPLE WORKS
|
||||||
|
* ONLY IF PASSES ARE DOWNLOADED FROM SAFARI, due to the
|
||||||
|
* support of PKPasses archives. To test this, you might
|
||||||
|
* need to open a tunnel through NGROK if you cannot access
|
||||||
|
* to your local machine (in my personal case, developing
|
||||||
|
* under WSL is a pretty big limitation sometimes).
|
||||||
|
*
|
||||||
|
* @TODO test again this example with next iOS 15 versions.
|
||||||
|
* Currently, pass viewer seems to be soooo bugged.
|
||||||
|
*
|
||||||
|
* https://imgur.com/bDTbcDg.jpg
|
||||||
|
* https://imgur.com/Y4GpuHT.jpg
|
||||||
|
* https://i.imgur.com/qbJMy1d.jpg
|
||||||
|
*
|
||||||
|
* Alberto, come to look at APPLE.
|
||||||
|
*
|
||||||
|
* MAMMA MIA!
|
||||||
|
*
|
||||||
|
* A feedback to Apple have been sent for this.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { app } from "./webserver";
|
||||||
|
import { getCertificates } from "./shared";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { PKPass } from "passkit-generator";
|
||||||
|
|
||||||
|
// *************************** //
|
||||||
|
// *** EXAMPLE FROM NOW ON *** //
|
||||||
|
// *************************** //
|
||||||
|
|
||||||
|
function getRandomColorPart() {
|
||||||
|
return Math.floor(Math.random() * 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generatePass(props: Object) {
|
||||||
|
const [iconFromModel, certificates] = await Promise.all([
|
||||||
|
fs.readFile(
|
||||||
|
path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../models/exampleBooking.pass/icon.png",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
getCertificates(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const pass = new PKPass(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
wwdr: certificates.wwdr,
|
||||||
|
signerCert: certificates.signerCert,
|
||||||
|
signerKey: certificates.signerKey,
|
||||||
|
signerKeyPassphrase: certificates.signerKeyPassphrase,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...props,
|
||||||
|
description: "Example Apple Wallet Pass",
|
||||||
|
passTypeIdentifier: "pass.com.passkitgenerator",
|
||||||
|
serialNumber: "nmyuxofgna",
|
||||||
|
organizationName: `Test Organization ${Math.random()}`,
|
||||||
|
teamIdentifier: "F53WB8AE67",
|
||||||
|
foregroundColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
|
||||||
|
labelColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
|
||||||
|
backgroundColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
pass.type = "boardingPass";
|
||||||
|
pass.transitType = "PKTransitTypeAir";
|
||||||
|
|
||||||
|
pass.setBarcodes({
|
||||||
|
message: "123456789",
|
||||||
|
format: "PKBarcodeFormatQR",
|
||||||
|
});
|
||||||
|
|
||||||
|
pass.headerFields.push(
|
||||||
|
{
|
||||||
|
key: "header-field-test-1",
|
||||||
|
value: "Unknown",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "header-field-test-2",
|
||||||
|
value: "unknown",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
pass.primaryFields.push(
|
||||||
|
{
|
||||||
|
key: "primaryField-1",
|
||||||
|
value: "NAP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "primaryField-2",
|
||||||
|
value: "VCE",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required by Apple. If one is not available, a
|
||||||
|
* pass might be openable on a Mac but not on a
|
||||||
|
* specific iPhone model
|
||||||
|
*/
|
||||||
|
|
||||||
|
pass.addBuffer("icon.png", iconFromModel);
|
||||||
|
pass.addBuffer("icon@2x.png", iconFromModel);
|
||||||
|
pass.addBuffer("icon@3x.png", iconFromModel);
|
||||||
|
|
||||||
|
return pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.route("/pkpasses/:modelName").get(async (request, response) => {
|
||||||
|
const passName =
|
||||||
|
request.params.modelName +
|
||||||
|
"_" +
|
||||||
|
new Date().toISOString().split("T")[0].replace(/-/gi, "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const passes = await Promise.all([
|
||||||
|
generatePass(request.body || request.params || request.query),
|
||||||
|
generatePass(request.body || request.params || request.query),
|
||||||
|
generatePass(request.body || request.params || request.query),
|
||||||
|
generatePass(request.body || request.params || request.query),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const pkpasses = PKPass.pack(...passes);
|
||||||
|
|
||||||
|
response.set({
|
||||||
|
"Content-type": pkpasses.mimeType,
|
||||||
|
"Content-disposition": `attachment; filename=${passName}.pkpasses`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const stream = pkpasses.getAsStream();
|
||||||
|
|
||||||
|
stream.pipe(response);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
|
||||||
|
response.set({
|
||||||
|
"Content-type": "text/html",
|
||||||
|
});
|
||||||
|
|
||||||
|
response.send(err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -9,34 +9,32 @@
|
|||||||
* @Author: Alexander P. Cerutti
|
* @Author: Alexander P. Cerutti
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import app from "./webserver";
|
import { app } from "./webserver";
|
||||||
import { createPass } from "passkit-generator";
|
import { getCertificates } from "./shared";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { PKPass } from "passkit-generator";
|
||||||
|
|
||||||
app.all(async function manageRequest(request, response) {
|
app.route("/fields/:modelName").get(async (request, response) => {
|
||||||
let passName =
|
const passName =
|
||||||
"exampleBooking" +
|
"exampleBooking" +
|
||||||
"_" +
|
"_" +
|
||||||
new Date().toISOString().split("T")[0].replace(/-/gi, "");
|
new Date().toISOString().split("T")[0].replace(/-/gi, "");
|
||||||
|
|
||||||
|
const certificates = await getCertificates();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let pass = await createPass({
|
const pass = await PKPass.from(
|
||||||
model: path.resolve(__dirname, "../models/exampleBooking"),
|
{
|
||||||
certificates: {
|
model: path.resolve(__dirname, "../../models/exampleBooking"),
|
||||||
wwdr: path.resolve(__dirname, "../../certificates/WWDR.pem"),
|
certificates: {
|
||||||
signerCert: path.resolve(
|
wwdr: certificates.wwdr,
|
||||||
__dirname,
|
signerCert: certificates.signerCert,
|
||||||
"../../certificates/signerCert.pem",
|
signerKey: certificates.signerKey,
|
||||||
),
|
signerKeyPassphrase: certificates.signerKeyPassphrase,
|
||||||
signerKey: {
|
|
||||||
keyFile: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../../certificates/signerKey.pem",
|
|
||||||
),
|
|
||||||
passphrase: "123456",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
overrides: request.body || request.params || request.query,
|
request.body || request.params || request.query,
|
||||||
});
|
);
|
||||||
|
|
||||||
pass.transitType = "PKTransitTypeAir";
|
pass.transitType = "PKTransitTypeAir";
|
||||||
|
|
||||||
@@ -134,8 +132,7 @@ app.all(async function manageRequest(request, response) {
|
|||||||
{
|
{
|
||||||
key: "checkIn",
|
key: "checkIn",
|
||||||
label: "",
|
label: "",
|
||||||
value:
|
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.",
|
||||||
"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",
|
textAlignment: "PKTextAlignmentLeft",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -147,8 +144,7 @@ app.all(async function manageRequest(request, response) {
|
|||||||
{
|
{
|
||||||
key: "Require special assistance",
|
key: "Require special assistance",
|
||||||
label: "Assistenza speciale",
|
label: "Assistenza speciale",
|
||||||
value:
|
value: "Se hai richiesto assistenza speciale, presentati a un membro del personale nell'area di Consegna bagagli almeno 90 minuti prima del volo.",
|
||||||
"Se hai richiesto assistenza speciale, presentati a un membro del personale nell'area di Consegna bagagli almeno 90 minuti prima del volo.",
|
|
||||||
textAlignment: "PKTextAlignmentLeft",
|
textAlignment: "PKTextAlignmentLeft",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -160,22 +156,19 @@ app.all(async function manageRequest(request, response) {
|
|||||||
{
|
{
|
||||||
key: "photoId",
|
key: "photoId",
|
||||||
label: "Un documento d’identità corredato di fotografia",
|
label: "Un documento d’identità corredato di fotografia",
|
||||||
value:
|
value: "è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta d’identità.",
|
||||||
"è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta d’identità.",
|
|
||||||
textAlignment: "PKTextAlignmentLeft",
|
textAlignment: "PKTextAlignmentLeft",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "yourSeat",
|
key: "yourSeat",
|
||||||
label: "Il tuo posto:",
|
label: "Il tuo posto:",
|
||||||
value:
|
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.",
|
||||||
"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",
|
textAlignment: "PKTextAlignmentLeft",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Pack safely",
|
key: "Pack safely",
|
||||||
label: "Bagaglio sicuro",
|
label: "Bagaglio sicuro",
|
||||||
value:
|
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",
|
||||||
"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",
|
textAlignment: "PKTextAlignmentLeft",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -186,9 +179,10 @@ app.all(async function manageRequest(request, response) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const stream = pass.generate();
|
const stream = pass.getAsStream();
|
||||||
|
|
||||||
response.set({
|
response.set({
|
||||||
"Content-type": "application/vnd.apple.pkpass",
|
"Content-type": pass.mimeType,
|
||||||
"Content-disposition": `attachment; filename=${passName}.pkpass`,
|
"Content-disposition": `attachment; filename=${passName}.pkpass`,
|
||||||
});
|
});
|
||||||
|
|
||||||
7
examples/self-hosted/src/index.ts
Normal file
7
examples/self-hosted/src/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import "./fields";
|
||||||
|
import "./localize";
|
||||||
|
import "./PKPass.from";
|
||||||
|
import "./PKPasses";
|
||||||
|
import "./scratch";
|
||||||
|
import "./setBarcodes";
|
||||||
|
import "./setExpirationDate";
|
||||||
71
examples/self-hosted/src/localize.ts
Normal file
71
examples/self-hosted/src/localize.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* .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 { getCertificates } from "./shared";
|
||||||
|
import path from "path";
|
||||||
|
import { PKPass } from "passkit-generator";
|
||||||
|
|
||||||
|
app.route("/localize/:modelName").get(async (request, response) => {
|
||||||
|
const passName =
|
||||||
|
request.params.modelName +
|
||||||
|
"_" +
|
||||||
|
new Date().toISOString().split("T")[0].replace(/-/gi, "");
|
||||||
|
|
||||||
|
const certificates = await getCertificates();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pass = await PKPass.from(
|
||||||
|
{
|
||||||
|
model: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
`../../models/${request.params.modelName}`,
|
||||||
|
),
|
||||||
|
certificates: {
|
||||||
|
wwdr: certificates.wwdr,
|
||||||
|
signerCert: certificates.signerCert,
|
||||||
|
signerKey: certificates.signerKey,
|
||||||
|
signerKeyPassphrase: certificates.signerKeyPassphrase,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
request.body || request.params || request.query,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Italian, already has an .lproj which gets included...
|
||||||
|
pass.localize("it", {
|
||||||
|
EVENT: "Evento",
|
||||||
|
LOCATION: "Dove",
|
||||||
|
});
|
||||||
|
|
||||||
|
// ...while German doesn't, so it gets 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.languages).join(", "));
|
||||||
|
|
||||||
|
const stream = pass.getAsStream();
|
||||||
|
|
||||||
|
response.set({
|
||||||
|
"Content-type": pass.mimeType,
|
||||||
|
"Content-disposition": `attachment; filename=${passName}.pkpass`,
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.pipe(response);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
|
||||||
|
response.set({
|
||||||
|
"Content-type": "text/html",
|
||||||
|
});
|
||||||
|
|
||||||
|
response.send(err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
106
examples/self-hosted/src/scratch.ts
Normal file
106
examples/self-hosted/src/scratch.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* This examples shows how you can create a PKPass from scratch,
|
||||||
|
* by adding files later and not adding pass.json
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { app } from "./webserver";
|
||||||
|
import { getCertificates } from "./shared";
|
||||||
|
import path from "path";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import { PKPass } from "passkit-generator";
|
||||||
|
|
||||||
|
function getRandomColorPart() {
|
||||||
|
return Math.floor(Math.random() * 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.route("/scratch/:modelName").get(async (request, response) => {
|
||||||
|
const passName =
|
||||||
|
request.params.modelName +
|
||||||
|
"_" +
|
||||||
|
new Date().toISOString().split("T")[0].replace(/-/gi, "");
|
||||||
|
|
||||||
|
const [iconFromModel, certificates] = await Promise.all([
|
||||||
|
fs.readFile(
|
||||||
|
path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../models/exampleBooking.pass/icon.png",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
await getCertificates(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pass = new PKPass(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
wwdr: certificates.wwdr,
|
||||||
|
signerCert: certificates.signerCert,
|
||||||
|
signerKey: certificates.signerKey,
|
||||||
|
signerKeyPassphrase: certificates.signerKeyPassphrase,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...(request.body || request.params || request.query),
|
||||||
|
description: "Example Apple Wallet Pass",
|
||||||
|
passTypeIdentifier: "pass.com.passkitgenerator",
|
||||||
|
serialNumber: "nmyuxofgna",
|
||||||
|
organizationName: `Test Organization ${Math.random()}`,
|
||||||
|
teamIdentifier: "F53WB8AE67",
|
||||||
|
foregroundColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
|
||||||
|
labelColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
|
||||||
|
backgroundColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
pass.type = "boardingPass";
|
||||||
|
pass.transitType = "PKTransitTypeAir";
|
||||||
|
|
||||||
|
pass.headerFields.push(
|
||||||
|
{
|
||||||
|
key: "header-field-test-1",
|
||||||
|
value: "Unknown",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "header-field-test-2",
|
||||||
|
value: "unknown",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
pass.primaryFields.push(
|
||||||
|
{
|
||||||
|
key: "primaryField-1",
|
||||||
|
value: "NAP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "primaryField-2",
|
||||||
|
value: "VCE",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required by Apple. If one is not available, a
|
||||||
|
* pass might be openable on a Mac but not on a
|
||||||
|
* specific iPhone model
|
||||||
|
*/
|
||||||
|
|
||||||
|
pass.addBuffer("icon.png", iconFromModel);
|
||||||
|
pass.addBuffer("icon@2x.png", iconFromModel);
|
||||||
|
pass.addBuffer("icon@3x.png", iconFromModel);
|
||||||
|
|
||||||
|
const stream = pass.getAsStream();
|
||||||
|
|
||||||
|
response.set({
|
||||||
|
"Content-type": pass.mimeType,
|
||||||
|
"Content-disposition": `attachment; filename=${passName}.pkpass`,
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.pipe(response);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
|
||||||
|
response.set({
|
||||||
|
"Content-type": "text/html",
|
||||||
|
});
|
||||||
|
|
||||||
|
response.send(err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
82
examples/self-hosted/src/setBarcodes.ts
Normal file
82
examples/self-hosted/src/setBarcodes.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* .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 { getCertificates } from "./shared";
|
||||||
|
import { PKPass } from "passkit-generator";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
app.route("/barcodes/:modelName").get(async (request, response) => {
|
||||||
|
const passName =
|
||||||
|
request.params.modelName +
|
||||||
|
"_" +
|
||||||
|
new Date().toISOString().split("T")[0].replace(/-/gi, "");
|
||||||
|
|
||||||
|
const certificates = await getCertificates();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pass = await PKPass.from(
|
||||||
|
{
|
||||||
|
model: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
`../../models/${request.params.modelName}`,
|
||||||
|
),
|
||||||
|
certificates: {
|
||||||
|
wwdr: certificates.wwdr,
|
||||||
|
signerCert: certificates.signerCert,
|
||||||
|
signerKey: certificates.signerKey,
|
||||||
|
signerKeyPassphrase: certificates.signerKeyPassphrase,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
request.body || request.params || request.query || {},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (request.query.alt === "true") {
|
||||||
|
// After this, pass.props["barcodes"] will have support for all the formats
|
||||||
|
pass.setBarcodes("Thank you for using this package <3");
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"Barcodes support is autocompleted:",
|
||||||
|
pass.props["barcodes"],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// After this, pass.props["barcodes"] will have support for just two of three
|
||||||
|
// of the passed format (the valid ones);
|
||||||
|
|
||||||
|
pass.setBarcodes(
|
||||||
|
{
|
||||||
|
message: "Thank you for using this package <3",
|
||||||
|
format: "PKBarcodeFormatCode128",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Thank you for using this package <3",
|
||||||
|
format: "PKBarcodeFormatPDF417",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = pass.getAsStream();
|
||||||
|
|
||||||
|
response.set({
|
||||||
|
"Content-type": pass.mimeType,
|
||||||
|
"Content-disposition": `attachment; filename=${passName}.pkpass`,
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.pipe(response);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
|
||||||
|
response.set({
|
||||||
|
"Content-type": "text/html",
|
||||||
|
});
|
||||||
|
|
||||||
|
response.send(err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
82
examples/self-hosted/src/setExpirationDate.ts
Normal file
82
examples/self-hosted/src/setExpirationDate.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* .expiration() method and voided prop 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { app } from "./webserver";
|
||||||
|
import { getCertificates } from "./shared";
|
||||||
|
import path from "path";
|
||||||
|
import { PKPass } from "passkit-generator";
|
||||||
|
|
||||||
|
app.route("/expirationDate/:modelName").get(async (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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificates = await getCertificates();
|
||||||
|
|
||||||
|
const passName =
|
||||||
|
request.params.modelName +
|
||||||
|
"_" +
|
||||||
|
new Date().toISOString().split("T")[0].replace(/-/gi, "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pass = await PKPass.from(
|
||||||
|
{
|
||||||
|
model: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
`../../models/${request.params.modelName}`,
|
||||||
|
),
|
||||||
|
certificates: {
|
||||||
|
wwdr: certificates.wwdr,
|
||||||
|
signerCert: certificates.signerCert,
|
||||||
|
signerKey: certificates.signerKey,
|
||||||
|
signerKeyPassphrase: certificates.signerKeyPassphrase,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
|
voided: request.query.fn === "void",
|
||||||
|
},
|
||||||
|
{ ...(request.body || request.params || request.query || {}) },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (request.query.fn === "expiration") {
|
||||||
|
// 2 minutes later...
|
||||||
|
const d = new Date();
|
||||||
|
d.setMinutes(d.getMinutes() + 2);
|
||||||
|
|
||||||
|
// setting the expiration
|
||||||
|
pass.setExpirationDate(d);
|
||||||
|
console.log(
|
||||||
|
"EXPIRATION DATE EXPECTED:",
|
||||||
|
pass.props["expirationDate"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = pass.getAsStream();
|
||||||
|
|
||||||
|
response.set({
|
||||||
|
"Content-type": pass.mimeType,
|
||||||
|
"Content-disposition": `attachment; filename=${passName}.pkpass`,
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.pipe(response);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
|
||||||
|
response.set({
|
||||||
|
"Content-type": "text/html",
|
||||||
|
});
|
||||||
|
|
||||||
|
response.send(err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
41
examples/self-hosted/src/shared.ts
Normal file
41
examples/self-hosted/src/shared.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { promises as fs } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const certificatesCache: Partial<{
|
||||||
|
signerCert: Buffer;
|
||||||
|
signerKey: Buffer;
|
||||||
|
wwdr: Buffer;
|
||||||
|
signerKeyPassphrase: string;
|
||||||
|
}> = {};
|
||||||
|
|
||||||
|
export async function getCertificates(): Promise<typeof certificatesCache> {
|
||||||
|
if (Object.keys(certificatesCache).length) {
|
||||||
|
return certificatesCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [signerCert, signerKey, wwdr, signerKeyPassphrase] =
|
||||||
|
await Promise.all([
|
||||||
|
fs.readFile(
|
||||||
|
path.resolve(__dirname, "../../../certificates/signerCert.pem"),
|
||||||
|
"utf-8",
|
||||||
|
),
|
||||||
|
fs.readFile(
|
||||||
|
path.resolve(__dirname, "../../../certificates/signerKey.pem"),
|
||||||
|
"utf-8",
|
||||||
|
),
|
||||||
|
fs.readFile(
|
||||||
|
path.resolve(__dirname, "../../../certificates/WWDR.pem"),
|
||||||
|
"utf-8",
|
||||||
|
),
|
||||||
|
Promise.resolve("123456"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Object.assign(certificatesCache, {
|
||||||
|
signerCert,
|
||||||
|
signerKey,
|
||||||
|
wwdr,
|
||||||
|
signerKeyPassphrase,
|
||||||
|
});
|
||||||
|
|
||||||
|
return certificatesCache;
|
||||||
|
}
|
||||||
14
examples/self-hosted/src/webserver.ts
Normal file
14
examples/self-hosted/src/webserver.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* Generic webserver instance for the examples
|
||||||
|
* @Author Alexander P. Cerutti
|
||||||
|
* Requires express to run
|
||||||
|
*/
|
||||||
|
|
||||||
|
import express from "express";
|
||||||
|
export const app = express();
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.listen(8080, "0.0.0.0", () => {
|
||||||
|
console.log("Webserver started.");
|
||||||
|
});
|
||||||
11
examples/self-hosted/tsconfig.json
Normal file
11
examples/self-hosted/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"outDir": "build",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
8
examples/serverless/.gitignore
vendored
Normal file
8
examples/serverless/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# package directories
|
||||||
|
node_modules
|
||||||
|
jspm_packages
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless
|
||||||
|
.build
|
||||||
|
!*.js
|
||||||
52
examples/serverless/README.md
Normal file
52
examples/serverless/README.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Serverless Examples
|
||||||
|
|
||||||
|
This is a sample project for showing passkit-generator being used on cloud functions.
|
||||||
|
|
||||||
|
Typescript compilation happens automatically through `serverless-plugin-typescript` when serverless is started.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
These examples are basically made for being executed locally. In the file `config.json`, some constants can be customized.
|
||||||
|
|
||||||
|
```json
|
||||||
|
/** Passkit signerKey passphrase **/
|
||||||
|
"SIGNER_KEY_PASSPHRASE": "123456",
|
||||||
|
|
||||||
|
/** Bucket name where a pass is saved before being served. */
|
||||||
|
"PASSES_S3_TEMP_BUCKET": "pkge-test",
|
||||||
|
|
||||||
|
/** S3 Access key ID - "S3RVER" is default for `serverless-s3-local`. If this example is run offline, "S3RVER" will always be used. */
|
||||||
|
"ACCESS_KEY_ID": "S3RVER",
|
||||||
|
|
||||||
|
/** S3 Secret - "S3RVER" is default for `serverless-s3-local` */
|
||||||
|
"SECRET_ACCESS_KEY": "S3RVER",
|
||||||
|
|
||||||
|
/** Bucket that contains pass models **/
|
||||||
|
"MODELS_S3_BUCKET": "pkge-mdbk"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run examples
|
||||||
|
|
||||||
|
Install the dependencies and run serverless. Installing the dependencies will link the latest version of passkit-generator in the parent workspace.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ npm install;
|
||||||
|
$ npm run run-offline;
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start `serverless offline` with an additional host option (mainly for WSL environment).
|
||||||
|
Serverless will start, by default, on `0.0.0.0:8080`.
|
||||||
|
|
||||||
|
### Available examples
|
||||||
|
|
||||||
|
All the examples, except fields ones, require a `modelName` to be passed in queryString. The name will be checked against local FS or S3 bucket if example is deployed.
|
||||||
|
Pass in queryString all the pass props you want to apply them to the final result.
|
||||||
|
|
||||||
|
| Example name | Endpoint name | Additional notes |
|
||||||
|
| -------------- | ----------------- | ----------------------------------------------------------------------------------------------------- |
|
||||||
|
| localize | `/localize` | - |
|
||||||
|
| fields | `/fields` | - |
|
||||||
|
| expirationDate | `/expirationDate` | - |
|
||||||
|
| scratch | `/scratch` | - |
|
||||||
|
| barcodes | `/barcodes` | Using `?alt=true` query parameter, will lead to barcode string message usage instead of selected ones |
|
||||||
|
| pkpasses | `/pkpasses` | - |
|
||||||
7
examples/serverless/config.json
Normal file
7
examples/serverless/config.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"SIGNER_KEY_PASSPHRASE": "123456",
|
||||||
|
"PASSES_S3_TEMP_BUCKET": "pkge-test",
|
||||||
|
"ACCESS_KEY_ID": "S3RVER",
|
||||||
|
"SECRET_ACCESS_KEY": "S3RVER",
|
||||||
|
"MODELS_S3_BUCKET": "pkge-mdbk"
|
||||||
|
}
|
||||||
21368
examples/serverless/package-lock.json
generated
Normal file
21368
examples/serverless/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
examples/serverless/package.json
Normal file
27
examples/serverless/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "examples-aws-lambda",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Passkit-generator examples for running in AWS Lambda",
|
||||||
|
"author": "Alexander P. Cerutti <cerutti.alexander@gmail.com>",
|
||||||
|
"license": "ISC",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"preinstall": "npm run clear:deps && npm unlink --no-save passkit-generator",
|
||||||
|
"postinstall": "npm --prefix ../.. run build && npm --prefix ../.. link && npm link passkit-generator",
|
||||||
|
"clear:deps": "rm -rf node_modules",
|
||||||
|
"run-offline": "npx serverless offline --host 0.0.0.0; :'specifying host due to WSL limits'"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"aws-sdk": "^2.1018.0",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/aws-lambda": "^8.10.84",
|
||||||
|
"@types/express": "4.17.8",
|
||||||
|
"serverless-offline": "^8.2.0",
|
||||||
|
"serverless-plugin-typescript": "^2.1.0",
|
||||||
|
"serverless-s3-local": "^0.6.20",
|
||||||
|
"typescript": "^4.4.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
56
examples/serverless/serverless.yml
Normal file
56
examples/serverless/serverless.yml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
service: passkit-generator-test-lambda
|
||||||
|
frameworkVersion: "2"
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- serverless-offline
|
||||||
|
- serverless-plugin-typescript
|
||||||
|
- serverless-s3-local
|
||||||
|
|
||||||
|
provider:
|
||||||
|
name: aws
|
||||||
|
runtime: nodejs14.x
|
||||||
|
lambdaHashingVersion: "20201221"
|
||||||
|
|
||||||
|
functions:
|
||||||
|
fields:
|
||||||
|
handler: src/index.fields
|
||||||
|
events:
|
||||||
|
- httpApi:
|
||||||
|
path: /fields
|
||||||
|
method: get
|
||||||
|
expiration:
|
||||||
|
handler: src/index.expirationDate
|
||||||
|
events:
|
||||||
|
- httpApi:
|
||||||
|
path: /expirationDate
|
||||||
|
method: get
|
||||||
|
localize:
|
||||||
|
handler: src/index.localize
|
||||||
|
events:
|
||||||
|
- httpApi:
|
||||||
|
path: /localize
|
||||||
|
method: get
|
||||||
|
barcodes:
|
||||||
|
handler: src/index.barcodes
|
||||||
|
events:
|
||||||
|
- httpApi:
|
||||||
|
path: /barcodes
|
||||||
|
method: get
|
||||||
|
scratch:
|
||||||
|
handler: src/index.scratch
|
||||||
|
events:
|
||||||
|
- httpApi:
|
||||||
|
path: /scratch
|
||||||
|
method: get
|
||||||
|
pkpasses:
|
||||||
|
handler: src/index.pkpasses
|
||||||
|
events:
|
||||||
|
- httpApi:
|
||||||
|
path: /pkpasses
|
||||||
|
method: get
|
||||||
|
|
||||||
|
custom:
|
||||||
|
serverless-offline:
|
||||||
|
httpPort: 8080
|
||||||
|
s3:
|
||||||
|
directory: /tmp
|
||||||
43
examples/serverless/src/functions/barcodes.ts
Normal file
43
examples/serverless/src/functions/barcodes.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { ALBEvent, ALBResult } from "aws-lambda";
|
||||||
|
import { PKPass } from "../../../../lib";
|
||||||
|
import { finish400WithoutModelName, createPassGenerator } from "../shared";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lambda for barcodes example
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function barcodes(event: ALBEvent) {
|
||||||
|
finish400WithoutModelName(event);
|
||||||
|
|
||||||
|
const { modelName, alt, ...passOptions } = event.queryStringParameters;
|
||||||
|
|
||||||
|
const passGenerator = createPassGenerator(modelName, passOptions);
|
||||||
|
|
||||||
|
const pass = (await passGenerator.next()).value as PKPass;
|
||||||
|
|
||||||
|
if (alt === "true") {
|
||||||
|
// After this, pass.props["barcodes"] will have support for all the formats
|
||||||
|
pass.setBarcodes("Thank you for using this package <3");
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"Barcodes support is autocompleted:",
|
||||||
|
pass.props["barcodes"],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// After this, pass.props["barcodes"] will have support for just two of three
|
||||||
|
// of the passed format (the valid ones);
|
||||||
|
|
||||||
|
pass.setBarcodes(
|
||||||
|
{
|
||||||
|
message: "Thank you for using this package <3",
|
||||||
|
format: "PKBarcodeFormatCode128",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Thank you for using this package <3",
|
||||||
|
format: "PKBarcodeFormatPDF417",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await passGenerator.next(pass as PKPass)).value as ALBResult;
|
||||||
|
}
|
||||||
31
examples/serverless/src/functions/expirationDate.ts
Normal file
31
examples/serverless/src/functions/expirationDate.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { ALBEvent, ALBResult } from "aws-lambda";
|
||||||
|
import { Context } from "vm";
|
||||||
|
import { PKPass } from "passkit-generator";
|
||||||
|
import { finish400WithoutModelName, createPassGenerator } from "../shared";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lambda for expirationDate example
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function expirationDate(event: ALBEvent, context: Context) {
|
||||||
|
finish400WithoutModelName(event);
|
||||||
|
|
||||||
|
const { modelName, ...passOptions } = event.queryStringParameters;
|
||||||
|
|
||||||
|
const passGenerator = createPassGenerator(modelName, passOptions);
|
||||||
|
|
||||||
|
const pass = (await passGenerator.next()).value as PKPass;
|
||||||
|
|
||||||
|
// 2 minutes later...
|
||||||
|
const d = new Date();
|
||||||
|
d.setMinutes(d.getMinutes() + 2);
|
||||||
|
|
||||||
|
// setting the expiration
|
||||||
|
(pass as PKPass).setExpirationDate(d);
|
||||||
|
console.log(
|
||||||
|
"EXPIRATION DATE EXPECTED:",
|
||||||
|
(pass as PKPass).props["expirationDate"],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (await passGenerator.next(pass as PKPass)).value as ALBResult;
|
||||||
|
}
|
||||||
160
examples/serverless/src/functions/fields.ts
Normal file
160
examples/serverless/src/functions/fields.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import { ALBEvent, ALBResult } from "aws-lambda";
|
||||||
|
import { PKPass } from "passkit-generator";
|
||||||
|
import { createPassGenerator } from "../shared";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lambda for fields example
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function fields(event: ALBEvent) {
|
||||||
|
const { modelName, ...passOptions } = event.queryStringParameters;
|
||||||
|
|
||||||
|
const passGenerator = createPassGenerator("exampleBooking", passOptions);
|
||||||
|
|
||||||
|
const pass = (await passGenerator.next()).value as PKPass;
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (await passGenerator.next(pass as PKPass)).value as ALBResult;
|
||||||
|
}
|
||||||
6
examples/serverless/src/functions/index.ts
Normal file
6
examples/serverless/src/functions/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export * from "./barcodes";
|
||||||
|
export * from "./expirationDate";
|
||||||
|
export * from "./fields";
|
||||||
|
export * from "./localize";
|
||||||
|
export * from "./pkpasses";
|
||||||
|
export * from "./scratch";
|
||||||
41
examples/serverless/src/functions/localize.ts
Normal file
41
examples/serverless/src/functions/localize.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { finish400WithoutModelName, createPassGenerator } from "../shared";
|
||||||
|
import type { ALBEvent, ALBResult } from "aws-lambda";
|
||||||
|
import type { PKPass } from "passkit-generator";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lambda for localize example
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function localize(event: ALBEvent) {
|
||||||
|
finish400WithoutModelName(event);
|
||||||
|
|
||||||
|
const { modelName, ...passOptions } = event.queryStringParameters;
|
||||||
|
|
||||||
|
const passGenerator = createPassGenerator(modelName, passOptions);
|
||||||
|
|
||||||
|
const pass = (await passGenerator.next()).value as PKPass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Italian and German already has an .lproj which gets included
|
||||||
|
* but it doesn't have translations
|
||||||
|
*/
|
||||||
|
pass.localize("it", {
|
||||||
|
EVENT: "Evento",
|
||||||
|
LOCATION: "Dove",
|
||||||
|
});
|
||||||
|
|
||||||
|
pass.localize("de", {
|
||||||
|
EVENT: "Ereignis",
|
||||||
|
LOCATION: "Ort",
|
||||||
|
});
|
||||||
|
|
||||||
|
// ...while Norwegian doesn't, so it gets created
|
||||||
|
pass.localize("nn", {
|
||||||
|
EVENT: "Begivenhet",
|
||||||
|
LOCATION: "plassering",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Added languages", Object.keys(pass.languages).join(", "));
|
||||||
|
|
||||||
|
return (await passGenerator.next(pass as PKPass)).value as ALBResult;
|
||||||
|
}
|
||||||
115
examples/serverless/src/functions/pkpasses.ts
Normal file
115
examples/serverless/src/functions/pkpasses.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { ALBEvent } from "aws-lambda";
|
||||||
|
import { PKPass } from "passkit-generator";
|
||||||
|
import {
|
||||||
|
getCertificates,
|
||||||
|
getSpecificFileInModel,
|
||||||
|
getS3Instance,
|
||||||
|
getRandomColorPart,
|
||||||
|
finish400WithoutModelName,
|
||||||
|
} from "../shared";
|
||||||
|
import config from "../../config.json";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lambda for PkPasses example
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function pkpasses(event: ALBEvent) {
|
||||||
|
finish400WithoutModelName(event);
|
||||||
|
|
||||||
|
const [certificates, iconFromModel, s3] = await Promise.all([
|
||||||
|
getCertificates(),
|
||||||
|
getSpecificFileInModel(
|
||||||
|
"icon.png",
|
||||||
|
event.queryStringParameters.modelName,
|
||||||
|
),
|
||||||
|
getS3Instance(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
function createPass() {
|
||||||
|
const pass = new PKPass({}, certificates, {
|
||||||
|
description: "Example Apple Wallet Pass",
|
||||||
|
passTypeIdentifier: "pass.com.passkitgenerator",
|
||||||
|
serialNumber: "nmyuxofgna",
|
||||||
|
organizationName: `Test Organization ${Math.random()}`,
|
||||||
|
teamIdentifier: "F53WB8AE67",
|
||||||
|
foregroundColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
|
||||||
|
labelColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
|
||||||
|
backgroundColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
|
||||||
|
});
|
||||||
|
|
||||||
|
pass.type = "boardingPass";
|
||||||
|
pass.transitType = "PKTransitTypeAir";
|
||||||
|
|
||||||
|
pass.headerFields.push(
|
||||||
|
{
|
||||||
|
key: "header-field-test-1",
|
||||||
|
value: "Unknown",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "header-field-test-2",
|
||||||
|
value: "unknown",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
pass.primaryFields.push(
|
||||||
|
{
|
||||||
|
key: "primaryField-1",
|
||||||
|
value: "NAP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "primaryField-2",
|
||||||
|
value: "VCE",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required by Apple. If one is not available, a
|
||||||
|
* pass might be openable on a Mac but not on a
|
||||||
|
* specific iPhone model
|
||||||
|
*/
|
||||||
|
|
||||||
|
pass.addBuffer("icon.png", iconFromModel);
|
||||||
|
pass.addBuffer("icon@2x.png", iconFromModel);
|
||||||
|
pass.addBuffer("icon@3x.png", iconFromModel);
|
||||||
|
|
||||||
|
return pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
const passes = await Promise.all([
|
||||||
|
Promise.resolve(createPass()),
|
||||||
|
Promise.resolve(createPass()),
|
||||||
|
Promise.resolve(createPass()),
|
||||||
|
Promise.resolve(createPass()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const pkpasses = PKPass.pack(...passes);
|
||||||
|
|
||||||
|
const buffer = pkpasses.getAsBuffer();
|
||||||
|
const passName = `GeneratedPass-${Math.random()}.pkpass`;
|
||||||
|
|
||||||
|
const { Location } = await s3
|
||||||
|
.upload({
|
||||||
|
Bucket: config.PASSES_S3_TEMP_BUCKET,
|
||||||
|
Key: passName,
|
||||||
|
ContentType: pkpasses.mimeType,
|
||||||
|
/** Marking it as expiring in 5 minutes, because passes should not be stored */
|
||||||
|
Expires: new Date(Date.now() + 5 * 60 * 1000),
|
||||||
|
Body: buffer,
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Please note that redirection to `Location` does not work
|
||||||
|
* if you open this code in another device if this is run
|
||||||
|
* offline. This because `Location` is on localhost. Didn't
|
||||||
|
* find yet a way to solve this.
|
||||||
|
*/
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 302,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/vnd.apple.pkpass",
|
||||||
|
Location: Location,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
71
examples/serverless/src/functions/scratch.ts
Normal file
71
examples/serverless/src/functions/scratch.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { ALBEvent, ALBResult } from "aws-lambda";
|
||||||
|
import { PKPass } from "passkit-generator";
|
||||||
|
import {
|
||||||
|
createPassGenerator,
|
||||||
|
getRandomColorPart,
|
||||||
|
getSpecificFileInModel,
|
||||||
|
} from "../shared";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lambda for scratch example
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function scratch(event: ALBEvent) {
|
||||||
|
const passGenerator = createPassGenerator(undefined, {
|
||||||
|
description: "Example Apple Wallet Pass",
|
||||||
|
passTypeIdentifier: "pass.com.passkitgenerator",
|
||||||
|
serialNumber: "nmyuxofgna",
|
||||||
|
organizationName: `Test Organization ${Math.random()}`,
|
||||||
|
teamIdentifier: "F53WB8AE67",
|
||||||
|
foregroundColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
|
||||||
|
labelColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
|
||||||
|
backgroundColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [{ value }, iconFromModel] = await Promise.all([
|
||||||
|
passGenerator.next(),
|
||||||
|
getSpecificFileInModel(
|
||||||
|
"icon.png",
|
||||||
|
event.queryStringParameters.modelName,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const pass = value as PKPass;
|
||||||
|
|
||||||
|
pass.type = "boardingPass";
|
||||||
|
pass.transitType = "PKTransitTypeAir";
|
||||||
|
|
||||||
|
pass.headerFields.push(
|
||||||
|
{
|
||||||
|
key: "header-field-test-1",
|
||||||
|
value: "Unknown",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "header-field-test-2",
|
||||||
|
value: "unknown",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
pass.primaryFields.push(
|
||||||
|
{
|
||||||
|
key: "primaryField-1",
|
||||||
|
value: "NAP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "primaryField-2",
|
||||||
|
value: "VCE",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required by Apple. If one is not available, a
|
||||||
|
* pass might be openable on a Mac but not on a
|
||||||
|
* specific iPhone model
|
||||||
|
*/
|
||||||
|
|
||||||
|
pass.addBuffer("icon.png", iconFromModel);
|
||||||
|
pass.addBuffer("icon@2x.png", iconFromModel);
|
||||||
|
pass.addBuffer("icon@3x.png", iconFromModel);
|
||||||
|
|
||||||
|
return (await passGenerator.next(pass as PKPass)).value as ALBResult;
|
||||||
|
}
|
||||||
1
examples/serverless/src/index.ts
Normal file
1
examples/serverless/src/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./functions";
|
||||||
176
examples/serverless/src/shared.ts
Normal file
176
examples/serverless/src/shared.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { ALBEvent, ALBResult } from "aws-lambda";
|
||||||
|
import AWS from "aws-sdk";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import config from "../config.json";
|
||||||
|
import { PKPass } from "passkit-generator";
|
||||||
|
|
||||||
|
const S3: { instance: AWS.S3 } = { instance: undefined };
|
||||||
|
|
||||||
|
export function finish400WithoutModelName(event: ALBEvent) {
|
||||||
|
if (event.queryStringParameters?.modelName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: "modelName is missing in query params",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRandomColorPart() {
|
||||||
|
return Math.floor(Math.random() * 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getModel(
|
||||||
|
modelName: string,
|
||||||
|
): Promise<string | { [key: string]: Buffer }> {
|
||||||
|
if (process.env.IS_OFFLINE === "true") {
|
||||||
|
console.log("model offline retrieving");
|
||||||
|
return path.resolve(__dirname, `../../models/${modelName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const s3 = await getS3Instance();
|
||||||
|
|
||||||
|
const result = await s3
|
||||||
|
.getObject({ Bucket: config.MODELS_S3_BUCKET, Key: modelName })
|
||||||
|
.promise();
|
||||||
|
|
||||||
|
return {}; // @TODO, like when it is run on s3
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCertificates(): Promise<{
|
||||||
|
signerCert: string | Buffer;
|
||||||
|
signerKey: string | Buffer;
|
||||||
|
wwdr: string | Buffer;
|
||||||
|
signerKeyPassphrase?: string;
|
||||||
|
}> {
|
||||||
|
let signerCert: string;
|
||||||
|
let signerKey: string;
|
||||||
|
let wwdr: string;
|
||||||
|
let signerKeyPassphrase: string;
|
||||||
|
|
||||||
|
if (process.env.IS_OFFLINE) {
|
||||||
|
console.log("Fetching Certificates locally");
|
||||||
|
|
||||||
|
[signerCert, signerKey, wwdr, signerKeyPassphrase] = await Promise.all([
|
||||||
|
fs.readFile(
|
||||||
|
path.resolve(__dirname, "../../../certificates/signerCert.pem"),
|
||||||
|
"utf-8",
|
||||||
|
),
|
||||||
|
fs.readFile(
|
||||||
|
path.resolve(__dirname, "../../../certificates/signerKey.pem"),
|
||||||
|
"utf-8",
|
||||||
|
),
|
||||||
|
fs.readFile(
|
||||||
|
path.resolve(__dirname, "../../../certificates/WWDR.pem"),
|
||||||
|
"utf-8",
|
||||||
|
),
|
||||||
|
Promise.resolve(config.SIGNER_KEY_PASSPHRASE),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// @TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
signerCert,
|
||||||
|
signerKey,
|
||||||
|
wwdr,
|
||||||
|
signerKeyPassphrase,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getS3Instance() {
|
||||||
|
if (S3.instance) {
|
||||||
|
return S3.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new AWS.S3({
|
||||||
|
s3ForcePathStyle: true,
|
||||||
|
accessKeyId: process.env.IS_OFFLINE ? "S3RVER" : config.ACCESS_KEY_ID, // This specific key is required when working offline
|
||||||
|
secretAccessKey: config.SECRET_ACCESS_KEY,
|
||||||
|
endpoint: new AWS.Endpoint("http://localhost:4569"),
|
||||||
|
});
|
||||||
|
|
||||||
|
S3.instance = instance;
|
||||||
|
|
||||||
|
try {
|
||||||
|
/** Trying to create a new bucket. If it fails, it already exists (at least in theory) */
|
||||||
|
await instance
|
||||||
|
.createBucket({ Bucket: config.PASSES_S3_TEMP_BUCKET })
|
||||||
|
.promise();
|
||||||
|
} catch (err) {}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSpecificFileInModel(
|
||||||
|
fileName: string,
|
||||||
|
modelName: string,
|
||||||
|
) {
|
||||||
|
const model = await getModel(modelName);
|
||||||
|
|
||||||
|
if (typeof model === "string") {
|
||||||
|
return fs.readFile(path.resolve(`${model}.pass`, fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
return model[fileName];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function* createPassGenerator(
|
||||||
|
modelName?: string,
|
||||||
|
passOptions?: Object,
|
||||||
|
): AsyncGenerator<PKPass, ALBResult, PKPass> {
|
||||||
|
const [template, certificates, s3] = await Promise.all([
|
||||||
|
modelName ? getModel(modelName) : Promise.resolve({}),
|
||||||
|
getCertificates(),
|
||||||
|
getS3Instance(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let pass: PKPass;
|
||||||
|
|
||||||
|
if (template instanceof Object) {
|
||||||
|
pass = new PKPass(template, certificates, passOptions);
|
||||||
|
} else if (typeof template === "string") {
|
||||||
|
pass = await PKPass.from(
|
||||||
|
{
|
||||||
|
model: template,
|
||||||
|
certificates,
|
||||||
|
},
|
||||||
|
passOptions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pass = yield pass;
|
||||||
|
|
||||||
|
const buffer = pass.getAsBuffer();
|
||||||
|
const passName = `GeneratedPass-${Math.random()}.pkpass`;
|
||||||
|
|
||||||
|
const { Location } = await s3
|
||||||
|
.upload({
|
||||||
|
Bucket: config.PASSES_S3_TEMP_BUCKET,
|
||||||
|
Key: passName,
|
||||||
|
ContentType: pass.mimeType,
|
||||||
|
/** Marking it as expiring in 5 minutes, because passes should not be stored */
|
||||||
|
Expires: new Date(Date.now() + 5 * 60 * 1000),
|
||||||
|
Body: buffer,
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Please note that redirection to `Location` does not work
|
||||||
|
* if you open this code in another device if this is run
|
||||||
|
* offline. This because `Location` is on localhost. Didn't
|
||||||
|
* find yet a way to solve this.
|
||||||
|
*/
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 302,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/vnd.apple.pkpass",
|
||||||
|
Location: Location,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
11
examples/serverless/tsconfig.json
Normal file
11
examples/serverless/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"outDir": "build",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "build"
|
|
||||||
},
|
|
||||||
"exclude": ["node_modules"]
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
* Generic webserver instance for the examples
|
|
||||||
* @Author Alexander P. Cerutti
|
|
||||||
* Requires express to run
|
|
||||||
*/
|
|
||||||
|
|
||||||
import express from "express";
|
|
||||||
export const app = express();
|
|
||||||
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
app.listen(8080, "0.0.0.0", function (request, response) {
|
|
||||||
console.log("Webserver started.");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.all("/", function (request, response) {
|
|
||||||
response.redirect("/gen/");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.route("/gen").all((req, res) => {
|
|
||||||
res.set("Content-Type", "text/html");
|
|
||||||
res.send(
|
|
||||||
"Cannot generate a pass. Specify a modelName in the url to continue. <br/>Usage: /gen/<i>modelName</i>",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default app.route("/gen/:modelName");
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
# Non-macOS-steps
|
|
||||||
|
|
||||||
This is a branch file that starts from the [Certificate paragraph](./README.md#certificates) and is made for developers that does not have access to macOS or are very very enthusiast of terminal (so its still valid for macOS).
|
|
||||||
|
|
||||||
I have to use these steps to work under WSL (Windows Subsystem for Linux), or Bash on Windows. The only thing you'll still need over the certificates, are an Wallet-ready iOS App (and so an iPhone) or a way to pass it.
|
|
||||||
To test it, I use my [Passkit-sample-client](https://github.com/alexandercerutti/passkit-sample-client), a really basic iOS App which requires just few informations and will show you the generated pass. Or you can use like Telegram, save your generated pass in your "Saved Messages" and then open it from the iPhone. Your choice.
|
|
||||||
|
|
||||||
But let's not talk anymore about testing and let's go with the steps to follow. **It will still require OpenSSL installed**. We are going to use it in a massive way.
|
|
||||||
|
|
||||||
|
|
||||||
1. Create a new pass type identifier and provide it with a Name and a reverse-domain bundle id (starting with "pass."). You will put this identifier as value for `passTypeIdentifier` in `pass.json` file.
|
|
||||||
|
|
||||||
2. Confirm and register the new identifier.
|
|
||||||
|
|
||||||
3. Go back to the pass type identifiers, click on your new pass id and Edit it.
|
|
||||||
|
|
||||||
4. Click "Create certificates" and then "Continue". You won't need to follow the written steps as they are for "Keychain Access" on macOS. Just to let you know, you are required to provide Apple a CSR (Certificate Signing Request). To provide them, you have first to generate a private key.
|
|
||||||
|
|
||||||
5. Open your terminal, and **place yourself in a good directory for you**. It may be the `certs/` folder in your application root.
|
|
||||||
|
|
||||||
6. Generate a private key with a name you like.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Generate a key:
|
|
||||||
$ openssl genrsa -out <your-key-name>.key 2048
|
|
||||||
```
|
|
||||||
|
|
||||||
7. Generate a CSR using your private key. Usually it should have a `.csr` extension, but there is no difference: .csr is a ASN.1 Base64 encoded text. Therefore it can have any extension you want.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Create a signing request
|
|
||||||
$ openssl req -new -key <your-key-name>.key -out csr.certSigningRequest
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Please note that to generate a Certificate Signing Request under OpenSSL for Windows, you'll need a configuration file or you'll fall into error.**
|
|
||||||
>
|
|
||||||
> You can load it by adding to the command above the flag `-config <path/to/.cnf>`
|
|
||||||
>
|
|
||||||
> Other functions might need configuration file.
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
You will be prompted to insert some informations. You'll have to insert Apple CA's informations, like below (**bold ones**). If none, press Enter to skip. After the email address, you won't need any further informations. So press Enter until you won't finish.
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
Country Name (2 letter code) [AU]: **US**
|
|
||||||
|
|
||||||
State or Province Name [Some-State]: **United States**
|
|
||||||
|
|
||||||
Locality Name []:
|
|
||||||
|
|
||||||
Organization Name [Internet Widgits Pty Ltd]: **Apple Inc.**
|
|
||||||
|
|
||||||
Organizational Unit Name []: **Apple Worldwide Developer Relations**
|
|
||||||
|
|
||||||
Common Name []: **Apple Worldwide Developer Relations Certification Authority**
|
|
||||||
|
|
||||||
Email Address []: **your-email**
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
If you are curious about how a CSR is composed, use this command:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Optional, just for curious people, like George, even if it's a monkey.
|
|
||||||
$ openssl asn1parse -i -in csr.certSigningRequest
|
|
||||||
```
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
8. Take your `csr.certSigningRequest` and upload it to APP (Apple Provisioning Portal) at step 4. Once processed, it will give you a certificate `.cer`.
|
|
||||||
|
|
||||||
9. Let's convert it to `.pem` (from a DER encoded to PEM Base64 encoded)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# .cer to .pem
|
|
||||||
$ openssl x509 -inform DER -outform PEM -in pass-test.cer -out signerCert.pem
|
|
||||||
```
|
|
||||||
|
|
||||||
10. Take `signerCert.pem` and save it. You'll use it in your application.
|
|
||||||
|
|
||||||
11. Convert your private key `.key` to a `.pem` base64 and save your key. You'll be using it in your application.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# .key to .pem
|
|
||||||
$ openssl rsa -in <your-key-name>.key -outform PEM -out passkey.pem
|
|
||||||
```
|
|
||||||
|
|
||||||
12. Execute Step 9 also for `AppleWWDRCA.cer` you've download from [Apple PKI](https://www.apple.com/certificateauthority/) and save it somewhere (over the rainbow... 🌈).
|
|
||||||
|
|
||||||
13. And you are done. 🎉 Now try to create your first pass! My suggestion is to keep anyway the `.key` file somewhere as backup (this time not over the rainbow). You can always download `.cer` file from APP, but you cannot generate back your private key.
|
|
||||||
421
package-lock.json
generated
421
package-lock.json
generated
@@ -1,312 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "passkit-generator",
|
"name": "passkit-generator",
|
||||||
"version": "2.0.8",
|
"version": "3.0.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"version": "2.0.8",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "^4.3.1",
|
|
||||||
"joi": "^17.4.0",
|
|
||||||
"node-forge": "^0.10.0",
|
|
||||||
"tslib": "^2.3.0",
|
|
||||||
"yazl": "^2.5.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/debug": "^4.1.5",
|
|
||||||
"@types/jasmine": "^3.7.7",
|
|
||||||
"@types/node": "^14.17.3",
|
|
||||||
"@types/node-forge": "^0.10.0",
|
|
||||||
"@types/yazl": "^2.4.2",
|
|
||||||
"jasmine": "^3.7.0",
|
|
||||||
"prettier": "^2.3.1",
|
|
||||||
"rimraf": "^3.0.2",
|
|
||||||
"typescript": "^4.3.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@hapi/hoek": {
|
|
||||||
"version": "9.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.1.1.tgz",
|
|
||||||
"integrity": "sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw=="
|
|
||||||
},
|
|
||||||
"node_modules/@hapi/topo": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@hapi/hoek": "^9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sideway/address": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-wAH/JYRXeIFQRsxerIuLjgUu2Xszam+O5xKeatJ4oudShOOirfmsQ1D6LL54XOU2tizpCYku+s1wmU0SYdpoSA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@hapi/hoek": "^9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sideway/formula": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg=="
|
|
||||||
},
|
|
||||||
"node_modules/@sideway/pinpoint": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
|
|
||||||
},
|
|
||||||
"node_modules/@types/debug": {
|
|
||||||
"version": "4.1.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
|
|
||||||
"integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/jasmine": {
|
|
||||||
"version": "3.7.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.7.7.tgz",
|
|
||||||
"integrity": "sha512-yZzGe1d1T0y+imXDZ79F030nn8qbmiwpWKCZKvKN0KbTzwXAVYShUxkIxu1ba+vhIdabTGVGCfbtZC0oOam8TQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
|
||||||
"version": "14.17.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.3.tgz",
|
|
||||||
"integrity": "sha512-e6ZowgGJmTuXa3GyaPbTGxX17tnThl2aSSizrFthQ7m9uLGZBXiGhgE55cjRZTF5kjZvYn9EOPOMljdjwbflxw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/node-forge": {
|
|
||||||
"version": "0.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.10.0.tgz",
|
|
||||||
"integrity": "sha512-qxCPxN/6s/kY4Xud/1T6gIQtJjRz09UWSA8fTEfUXu4rC9EkFt4KA3s8bYLvkNJmIEWlVeuhZ1CFl7F5rp3FCA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/yazl": {
|
|
||||||
"version": "2.4.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/yazl/-/yazl-2.4.2.tgz",
|
|
||||||
"integrity": "sha512-T+9JH8O2guEjXNxqmybzQ92mJUh2oCwDDMSSimZSe1P+pceZiFROZLYmcbqkzV5EUwz6VwcKXCO2S2yUpra6XQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/balanced-match": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/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,
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0",
|
|
||||||
"concat-map": "0.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/buffer-crc32": {
|
|
||||||
"version": "0.2.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
|
||||||
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/concat-map": {
|
|
||||||
"version": "0.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
|
||||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/debug": {
|
|
||||||
"version": "4.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
|
||||||
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "2.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fs.realpath": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/glob": {
|
|
||||||
"version": "7.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
|
||||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/inflight": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
|
||||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"once": "^1.3.0",
|
|
||||||
"wrappy": "1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/inherits": {
|
|
||||||
"version": "2.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/jasmine": {
|
|
||||||
"version": "3.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.7.0.tgz",
|
|
||||||
"integrity": "sha512-wlzGQ+cIFzMEsI+wDqmOwvnjTvolLFwlcpYLCqSPPH0prOQaW3P+IzMhHYn934l1imNvw07oCyX+vGUv3wmtSQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"glob": "^7.1.6",
|
|
||||||
"jasmine-core": "~3.7.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"jasmine": "bin/jasmine.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jasmine-core": {
|
|
||||||
"version": "3.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.7.1.tgz",
|
|
||||||
"integrity": "sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/joi": {
|
|
||||||
"version": "17.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.4.0.tgz",
|
|
||||||
"integrity": "sha512-F4WiW2xaV6wc1jxete70Rw4V/VuMd6IN+a5ilZsxG4uYtUXWu2kq9W5P2dz30e7Gmw8RCbY/u/uk+dMPma9tAg==",
|
|
||||||
"dependencies": {
|
|
||||||
"@hapi/hoek": "^9.0.0",
|
|
||||||
"@hapi/topo": "^5.0.0",
|
|
||||||
"@sideway/address": "^4.1.0",
|
|
||||||
"@sideway/formula": "^3.0.0",
|
|
||||||
"@sideway/pinpoint": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/minimatch": {
|
|
||||||
"version": "3.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
|
||||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^1.1.7"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ms": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
|
||||||
},
|
|
||||||
"node_modules/node-forge": {
|
|
||||||
"version": "0.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
|
|
||||||
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/once": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
|
||||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"wrappy": "1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/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=",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prettier": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"prettier": "bin-prettier.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.13.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/rimraf": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"glob": "^7.1.3"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"rimraf": "bin.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tslib": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
|
||||||
},
|
|
||||||
"node_modules/typescript": {
|
|
||||||
"version": "4.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz",
|
|
||||||
"integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"tsc": "bin/tsc",
|
|
||||||
"tsserver": "bin/tsserver"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/wrappy": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
|
||||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/yazl": {
|
|
||||||
"version": "2.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz",
|
|
||||||
"integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==",
|
|
||||||
"dependencies": {
|
|
||||||
"buffer-crc32": "~0.2.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/hoek": {
|
"@hapi/hoek": {
|
||||||
"version": "9.1.1",
|
"version": "9.1.1",
|
||||||
@@ -339,37 +35,31 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
|
||||||
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
|
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
|
||||||
},
|
},
|
||||||
"@types/debug": {
|
"@types/do-not-zip": {
|
||||||
"version": "4.1.5",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/do-not-zip/-/do-not-zip-1.0.0.tgz",
|
||||||
"integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==",
|
"integrity": "sha512-A/itOzxvHqp0dGAhbhDLm62Wmx0W6c9eYvKkb8VijDBnSoC9u0e8GwouyIxluFCJELOilTe48oCzeTrQK1kkjg==",
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/jasmine": {
|
|
||||||
"version": "3.7.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.7.7.tgz",
|
|
||||||
"integrity": "sha512-yZzGe1d1T0y+imXDZ79F030nn8qbmiwpWKCZKvKN0KbTzwXAVYShUxkIxu1ba+vhIdabTGVGCfbtZC0oOam8TQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/node": {
|
|
||||||
"version": "14.17.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.3.tgz",
|
|
||||||
"integrity": "sha512-e6ZowgGJmTuXa3GyaPbTGxX17tnThl2aSSizrFthQ7m9uLGZBXiGhgE55cjRZTF5kjZvYn9EOPOMljdjwbflxw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/node-forge": {
|
|
||||||
"version": "0.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.10.0.tgz",
|
|
||||||
"integrity": "sha512-qxCPxN/6s/kY4Xud/1T6gIQtJjRz09UWSA8fTEfUXu4rC9EkFt4KA3s8bYLvkNJmIEWlVeuhZ1CFl7F5rp3FCA==",
|
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/yazl": {
|
"@types/jasmine": {
|
||||||
"version": "2.4.2",
|
"version": "3.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yazl/-/yazl-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.10.1.tgz",
|
||||||
"integrity": "sha512-T+9JH8O2guEjXNxqmybzQ92mJUh2oCwDDMSSimZSe1P+pceZiFROZLYmcbqkzV5EUwz6VwcKXCO2S2yUpra6XQ==",
|
"integrity": "sha512-So26woGjM6F9b2julbJlXdcPdyhwteZzEX2EbFmreuJBamPVVdp6w4djywUG9TmcwjiC+ECAe+RSSBgYEOgEqQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"version": "16.11.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.4.tgz",
|
||||||
|
"integrity": "sha512-TMgXmy0v2xWyuCSCJM6NCna2snndD8yvQF67J29ipdzMcsPa9u+o0tjF5+EQNdhcuZplYuouYqpc4zcd5I6amQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/node-forge": {
|
||||||
|
"version": "0.10.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.10.8.tgz",
|
||||||
|
"integrity": "sha512-pS02y9i4oqhyijveLYrgb7C/tQVHxyaFg7cxZITHO4C9oHU5gjIvtCnZXQNP6K7adXl4VtxCnijnKeuWqT++nA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -391,24 +81,16 @@
|
|||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"buffer-crc32": {
|
|
||||||
"version": "0.2.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
|
||||||
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
|
|
||||||
},
|
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"debug": {
|
"do-not-zip": {
|
||||||
"version": "4.3.1",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/do-not-zip/-/do-not-zip-1.0.0.tgz",
|
||||||
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
|
"integrity": "sha512-Pgd81ET43bhAGaN2Hq1zluSX1FmD7kl7KcV9ER/lawiLsRUB9pRA5y8r6us29Xk6BrINZETO8TjhYwtwafWUww=="
|
||||||
"requires": {
|
|
||||||
"ms": "2.1.2"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"fs.realpath": {
|
"fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -447,25 +129,25 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"jasmine": {
|
"jasmine": {
|
||||||
"version": "3.7.0",
|
"version": "3.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.10.0.tgz",
|
||||||
"integrity": "sha512-wlzGQ+cIFzMEsI+wDqmOwvnjTvolLFwlcpYLCqSPPH0prOQaW3P+IzMhHYn934l1imNvw07oCyX+vGUv3wmtSQ==",
|
"integrity": "sha512-2Y42VsC+3CQCTzTwJezOvji4qLORmKIE0kwowWC+934Krn6ZXNQYljiwK5st9V3PVx96BSiDYXSB60VVah3IlQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"glob": "^7.1.6",
|
"glob": "^7.1.6",
|
||||||
"jasmine-core": "~3.7.0"
|
"jasmine-core": "~3.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"jasmine-core": {
|
"jasmine-core": {
|
||||||
"version": "3.7.1",
|
"version": "3.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.10.1.tgz",
|
||||||
"integrity": "sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ==",
|
"integrity": "sha512-ooZWSDVAdh79Rrj4/nnfklL3NQVra0BcuhcuWoAwwi+znLDoUeH87AFfeX8s+YeYi6xlv5nveRyaA1v7CintfA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"joi": {
|
"joi": {
|
||||||
"version": "17.4.0",
|
"version": "17.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/joi/-/joi-17.4.2.tgz",
|
||||||
"integrity": "sha512-F4WiW2xaV6wc1jxete70Rw4V/VuMd6IN+a5ilZsxG4uYtUXWu2kq9W5P2dz30e7Gmw8RCbY/u/uk+dMPma9tAg==",
|
"integrity": "sha512-Lm56PP+n0+Z2A2rfRvsfWVDXGEWjXxatPopkQ8qQ5mxCEhwHG+Ettgg5o98FFaxilOxozoa14cFhrE/hOzh/Nw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@hapi/hoek": "^9.0.0",
|
"@hapi/hoek": "^9.0.0",
|
||||||
"@hapi/topo": "^5.0.0",
|
"@hapi/topo": "^5.0.0",
|
||||||
@@ -483,11 +165,6 @@
|
|||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ms": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
|
||||||
},
|
|
||||||
"node-forge": {
|
"node-forge": {
|
||||||
"version": "0.10.0",
|
"version": "0.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
|
||||||
@@ -509,9 +186,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"version": "2.3.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz",
|
||||||
"integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==",
|
"integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"rimraf": {
|
"rimraf": {
|
||||||
@@ -524,14 +201,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "4.3.4",
|
"version": "4.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz",
|
||||||
"integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==",
|
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"wrappy": {
|
"wrappy": {
|
||||||
@@ -539,14 +216,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
|
||||||
"yazl": {
|
|
||||||
"version": "2.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz",
|
|
||||||
"integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==",
|
|
||||||
"requires": {
|
|
||||||
"buffer-crc32": "~0.2.3"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
package.json
31
package.json
@@ -1,18 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "passkit-generator",
|
"name": "passkit-generator",
|
||||||
"version": "2.0.8",
|
"version": "3.0.0",
|
||||||
"description": "The easiest way to generate custom Apple Wallet passes in Node.js",
|
"description": "The easiest way to generate custom Apple Wallet passes in Node.js",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:src",
|
"build": "npm run build:src",
|
||||||
"build:all": "npm run build:src && npm run build:examples && npm run build:spec",
|
"build:all": "npm run build:src && npm run build:examples && npm run build:spec",
|
||||||
"build:src": "rimraf lib && npx tsc -p tsconfig.dist.json",
|
"build:src": "rimraf lib && npx tsc -p tsconfig.dist.json",
|
||||||
"build:examples": "cd examples && npm run build",
|
|
||||||
"build:spec": "rimraf \"./spec/*.!(ts)\" && npx tsc -p tsconfig.spec.json",
|
"build:spec": "rimraf \"./spec/*.!(ts)\" && npx tsc -p tsconfig.spec.json",
|
||||||
"prepublishOnly": "npm run build",
|
"prepublishOnly": "npm run build",
|
||||||
"test": "npm run build:spec && npx jasmine",
|
"test": "npm run build:spec && npx jasmine"
|
||||||
"example": "npm run build:src && npm --prefix examples run example",
|
|
||||||
"example:debug": "npm run build:src && npm --prefix examples run example:debug"
|
|
||||||
},
|
},
|
||||||
"author": "Alexander Patrick Cerutti",
|
"author": "Alexander Patrick Cerutti",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -25,25 +22,23 @@
|
|||||||
"Pass"
|
"Pass"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.3.1",
|
"do-not-zip": "^1.0.0",
|
||||||
"joi": "^17.4.0",
|
"joi": "^17.4.2",
|
||||||
"node-forge": "^0.10.0",
|
"node-forge": "^0.10.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.1"
|
||||||
"yazl": "^2.5.1"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=14.18.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/debug": "^4.1.5",
|
"@types/jasmine": "^3.10.1",
|
||||||
"@types/jasmine": "^3.7.7",
|
"@types/node": "^16.11.4",
|
||||||
"@types/node": "^14.17.3",
|
"@types/node-forge": "^0.10.8",
|
||||||
"@types/node-forge": "^0.10.0",
|
"@types/do-not-zip": "^1.0.0",
|
||||||
"@types/yazl": "^2.4.2",
|
"jasmine": "^3.10.0",
|
||||||
"jasmine": "^3.7.0",
|
"prettier": "^2.4.1",
|
||||||
"prettier": "^2.3.1",
|
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"typescript": "^4.3.4"
|
"typescript": "^4.4.4"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"lib/**/*.+(js|d.ts)!(*.map)"
|
"lib/**/*.+(js|d.ts)!(*.map)"
|
||||||
|
|||||||
122
spec/Bundle.ts
Normal file
122
spec/Bundle.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { Stream } from "stream";
|
||||||
|
import * as Messages from "../lib/messages";
|
||||||
|
import { default as Bundle, filesSymbol } from "../lib/Bundle";
|
||||||
|
|
||||||
|
describe("Bundle", () => {
|
||||||
|
let bundle: InstanceType<typeof Bundle>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
bundle = new Bundle("application/vnd.apple.pkpass");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("freezable", () => {
|
||||||
|
it("should expose freeze method and bundle itself to be frozen", () => {
|
||||||
|
const [bundle, freeze] = Bundle.freezable("any/any");
|
||||||
|
freeze();
|
||||||
|
expect(bundle.isFrozen).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("mimeType", () => {
|
||||||
|
it("should throw an error if no mime-type is specified in the constructor", () => {
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(() => new Bundle()).toThrowError(
|
||||||
|
Error,
|
||||||
|
Messages.BUNDLE.MIME_TYPE_MISSING,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should expose the mime-type as public property", () => {
|
||||||
|
expect(bundle.mimeType).toBe("application/vnd.apple.pkpass");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("addBuffer", () => {
|
||||||
|
it("should allow to add buffers", () => {
|
||||||
|
const buffer = Buffer.alloc(0);
|
||||||
|
bundle.addBuffer("pass.json", buffer);
|
||||||
|
|
||||||
|
expect(bundle[filesSymbol]).toEqual({ "pass.json": buffer });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("exporting", () => {
|
||||||
|
describe("getAsStream", () => {
|
||||||
|
it("should return a stream", () => {
|
||||||
|
addEmptyFilesToBundle(bundle);
|
||||||
|
|
||||||
|
expect(bundle.getAsStream()).toBeInstanceOf(Stream);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should freeze the bundle", () => {
|
||||||
|
bundle.getAsStream();
|
||||||
|
expect(bundle.isFrozen).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error if a file is attempted to be added when bundle is frozen", () => {
|
||||||
|
addEmptyFilesToBundle(bundle);
|
||||||
|
|
||||||
|
bundle.getAsStream();
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
bundle.addBuffer("icon.png", Buffer.alloc(0)),
|
||||||
|
).toThrowError(Error, Messages.BUNDLE.CLOSED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAsBuffer", () => {
|
||||||
|
it("should return a buffer", () => {
|
||||||
|
addEmptyFilesToBundle(bundle);
|
||||||
|
|
||||||
|
expect(bundle.getAsBuffer()).toBeInstanceOf(Buffer);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should freeze the bundle", () => {
|
||||||
|
bundle.getAsBuffer();
|
||||||
|
expect(bundle.isFrozen).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error if a file is attempted to be added when bundle is frozen", () => {
|
||||||
|
addEmptyFilesToBundle(bundle);
|
||||||
|
|
||||||
|
bundle.getAsBuffer();
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
bundle.addBuffer("icon.png", Buffer.alloc(0)),
|
||||||
|
).toThrowError(Error, Messages.BUNDLE.CLOSED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAsRaw", () => {
|
||||||
|
it("should freeze the bundle", () => {
|
||||||
|
bundle.getAsRaw();
|
||||||
|
expect(bundle.isFrozen).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return an object with filePath as key and Buffer as value", () => {
|
||||||
|
bundle.addBuffer("pass.json", Buffer.alloc(0));
|
||||||
|
bundle.addBuffer("signature", Buffer.alloc(0));
|
||||||
|
bundle.addBuffer("en.lproj/pass.strings", Buffer.alloc(0));
|
||||||
|
bundle.addBuffer("en.lproj/icon.png", Buffer.alloc(0));
|
||||||
|
|
||||||
|
const list = bundle.getAsRaw();
|
||||||
|
|
||||||
|
expect(list["pass.json"]).not.toBeUndefined();
|
||||||
|
expect(list["pass.json"]).toBeInstanceOf(Buffer);
|
||||||
|
expect(list["signature"]).not.toBeUndefined();
|
||||||
|
expect(list["signature"]).toBeInstanceOf(Buffer);
|
||||||
|
expect(list["en.lproj/pass.strings"]).not.toBeUndefined();
|
||||||
|
expect(list["en.lproj/pass.strings"]).toBeInstanceOf(Buffer);
|
||||||
|
expect(list["en.lproj/icon.png"]).not.toBeUndefined();
|
||||||
|
expect(list["en.lproj/icon.png"]).toBeInstanceOf(Buffer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function addEmptyFilesToBundle(bundle: Bundle) {
|
||||||
|
const buffer = Buffer.alloc(0);
|
||||||
|
bundle.addBuffer("pass.json", buffer);
|
||||||
|
bundle.addBuffer("icon@2x.png", buffer);
|
||||||
|
bundle.addBuffer("icon@3x.png", buffer);
|
||||||
|
}
|
||||||
195
spec/FieldsArray.ts
Normal file
195
spec/FieldsArray.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import { PKPass } from "../lib";
|
||||||
|
import FieldsArray from "../lib/FieldsArray";
|
||||||
|
import * as Messages from "../lib/messages";
|
||||||
|
|
||||||
|
describe("FieldsArray", () => {
|
||||||
|
let fa: FieldsArray;
|
||||||
|
let frozen = false;
|
||||||
|
let pool: Set<string>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
frozen = false;
|
||||||
|
pool = new Set<string>();
|
||||||
|
fa = new FieldsArray(
|
||||||
|
{
|
||||||
|
get isFrozen() {
|
||||||
|
return frozen;
|
||||||
|
},
|
||||||
|
} as PKPass /** Fake pass. This is okay for testing */,
|
||||||
|
pool,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should extend an array", () => {
|
||||||
|
expect(fa instanceof Array).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("push", () => {
|
||||||
|
it("should prevent adding new fields if pass is frozen", () => {
|
||||||
|
frozen = true;
|
||||||
|
|
||||||
|
expect(() => fa.push({ key: "t1", value: "v1" })).toThrowError(
|
||||||
|
Error,
|
||||||
|
Messages.BUNDLE.CLOSED,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow adding fields", () => {
|
||||||
|
expect(fa.push({ key: "t1", value: "v1" })).toBe(1);
|
||||||
|
expect(fa.length).toBe(1);
|
||||||
|
expect(fa[0]).toEqual({ key: "t1", value: "v1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add the key to the pool", () => {
|
||||||
|
fa.push({ key: "t1", value: "v1" });
|
||||||
|
|
||||||
|
expect(pool.has("t1")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should log a warning if key already exists and omit that object", () => {
|
||||||
|
fa.push({ key: "t1", value: "v1" });
|
||||||
|
|
||||||
|
console.warn = jasmine.createSpy("log");
|
||||||
|
|
||||||
|
fa.push({ key: "t1", value: "v1" });
|
||||||
|
|
||||||
|
expect(console.warn).toHaveBeenCalledWith(
|
||||||
|
Messages.FIELDS.REPEATED_KEY.replace("%s", "t1"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fa.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("pop", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fa.push({ key: "t1", value: "v1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent popping out fields if pass is frozen", () => {
|
||||||
|
frozen = true;
|
||||||
|
|
||||||
|
expect(() => fa.pop()).toThrowError(Error, Messages.BUNDLE.CLOSED);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should popping out fields", () => {
|
||||||
|
expect(fa.pop()).toEqual({ key: "t1", value: "v1" });
|
||||||
|
expect(fa.length).toBe(0);
|
||||||
|
expect(fa[0]).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove the key from the pool", () => {
|
||||||
|
expect(pool.has("t1")).toBe(true);
|
||||||
|
|
||||||
|
fa.pop();
|
||||||
|
|
||||||
|
expect(pool.has("t1")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("splice", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fa.push({ key: "t1", value: "v1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent splicing fields if pass is frozen", () => {
|
||||||
|
frozen = true;
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
fa.splice(0, 1, { key: "k1", value: "v1" }),
|
||||||
|
).toThrowError(Error, Messages.BUNDLE.CLOSED);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove the key from the pool", () => {
|
||||||
|
expect(pool.has("t1")).toBe(true);
|
||||||
|
|
||||||
|
fa.splice(0, 1, { key: "k1", value: "v2" });
|
||||||
|
|
||||||
|
expect(pool.has("t1")).toBe(false);
|
||||||
|
expect(pool.has("k1")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should log a warning if key already exists and omit that object", () => {
|
||||||
|
fa.push({ key: "t2", value: "v2" });
|
||||||
|
fa.push({ key: "t3", value: "v3" });
|
||||||
|
fa.push({ key: "t4", value: "v4" });
|
||||||
|
|
||||||
|
console.warn = jasmine.createSpy("log");
|
||||||
|
|
||||||
|
fa.splice(0, 1, { key: "t2", value: "v2" });
|
||||||
|
|
||||||
|
expect(console.warn).toHaveBeenCalledWith(
|
||||||
|
Messages.FIELDS.REPEATED_KEY.replace("%s", "t2"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fa.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shift", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fa.push({ key: "t1", value: "v1" });
|
||||||
|
fa.push({ key: "t2", value: "v2" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent popping out fields if pass is frozen", () => {
|
||||||
|
frozen = true;
|
||||||
|
|
||||||
|
expect(() => fa.shift()).toThrowError(
|
||||||
|
Error,
|
||||||
|
Messages.BUNDLE.CLOSED,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should shift out fields", () => {
|
||||||
|
expect(fa.shift()).toEqual({ key: "t1", value: "v1" });
|
||||||
|
expect(fa.length).toBe(1);
|
||||||
|
expect(fa[0]).toEqual({ key: "t2", value: "v2" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove the key from the pool", () => {
|
||||||
|
expect(pool.has("t1")).toBe(true);
|
||||||
|
|
||||||
|
fa.shift();
|
||||||
|
|
||||||
|
expect(pool.has("t1")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("unshift", () => {
|
||||||
|
it("should prevent adding new fields if pass is frozen", () => {
|
||||||
|
frozen = true;
|
||||||
|
|
||||||
|
expect(() => fa.unshift({ key: "t1", value: "v1" })).toThrowError(
|
||||||
|
Error,
|
||||||
|
Messages.BUNDLE.CLOSED,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow adding fields", () => {
|
||||||
|
expect(fa.unshift({ key: "t1", value: "v1" })).toBe(1);
|
||||||
|
expect(fa.length).toBe(1);
|
||||||
|
expect(fa[0]).toEqual({ key: "t1", value: "v1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add the key to the pool", () => {
|
||||||
|
fa.unshift({ key: "t1", value: "v1" });
|
||||||
|
|
||||||
|
expect(pool.has("t1")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should log a warning if key already exists and omit that object", () => {
|
||||||
|
fa.push({ key: "t1", value: "v1" });
|
||||||
|
|
||||||
|
console.warn = jasmine.createSpy("log");
|
||||||
|
|
||||||
|
fa.unshift({ key: "t1", value: "v1" });
|
||||||
|
|
||||||
|
expect(console.warn).toHaveBeenCalledWith(
|
||||||
|
Messages.FIELDS.REPEATED_KEY.replace("%s", "t1"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fa.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
1191
spec/PKPass.ts
Normal file
1191
spec/PKPass.ts
Normal file
File diff suppressed because it is too large
Load Diff
170
spec/factory.ts
170
spec/factory.ts
@@ -1,170 +0,0 @@
|
|||||||
import { createPass } from "../lib/factory";
|
|
||||||
import formatMessage, { ERROR } from "../lib/messages";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
|
|
||||||
describe("createPass", () => {
|
|
||||||
it("should throw if first argument is not provided", async () => {
|
|
||||||
await expectAsync(createPass(undefined)).toBeRejectedWithError(
|
|
||||||
formatMessage(ERROR.CP_NO_OPTS),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
let certificatesPath = "../certificates";
|
|
||||||
|
|
||||||
try {
|
|
||||||
fs.accessSync(path.resolve(__dirname, certificatesPath));
|
|
||||||
} catch (err) {
|
|
||||||
certificatesPath = "../certs";
|
|
||||||
|
|
||||||
try {
|
|
||||||
fs.accessSync(path.resolve(__dirname, certificatesPath));
|
|
||||||
} catch (err) {
|
|
||||||
certificatesPath = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (certificatesPath) {
|
|
||||||
it("should return a pass instance", async () => {
|
|
||||||
await expectAsync(
|
|
||||||
createPass({
|
|
||||||
model: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../examples/models/exampleBooking.pass",
|
|
||||||
),
|
|
||||||
certificates: {
|
|
||||||
signerCert: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
certificatesPath,
|
|
||||||
"./signerCert.pem",
|
|
||||||
),
|
|
||||||
signerKey: {
|
|
||||||
keyFile: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
certificatesPath,
|
|
||||||
"./signerKey.pem",
|
|
||||||
),
|
|
||||||
passphrase: "password1234",
|
|
||||||
},
|
|
||||||
wwdr: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
certificatesPath,
|
|
||||||
"./WWDR.pem",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toBeResolved();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Model validation", () => {
|
|
||||||
it("Should reject with non valid model", async () => {
|
|
||||||
await expectAsync(
|
|
||||||
createPass({
|
|
||||||
// @ts-expect-error
|
|
||||||
model: 0,
|
|
||||||
certificates: {
|
|
||||||
signerCert: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
certificatesPath,
|
|
||||||
"./signerCert.pem",
|
|
||||||
),
|
|
||||||
signerKey: {
|
|
||||||
keyFile: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
certificatesPath,
|
|
||||||
"./signerKey.pem",
|
|
||||||
),
|
|
||||||
passphrase: "password1234",
|
|
||||||
},
|
|
||||||
wwdr: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
certificatesPath,
|
|
||||||
"./WWDR.pem",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toBeRejected();
|
|
||||||
|
|
||||||
await expectAsync(
|
|
||||||
createPass({
|
|
||||||
model: undefined,
|
|
||||||
certificates: {
|
|
||||||
signerCert: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
certificatesPath,
|
|
||||||
"./signerCert.pem",
|
|
||||||
),
|
|
||||||
signerKey: {
|
|
||||||
keyFile: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
certificatesPath,
|
|
||||||
"./signerKey.pem",
|
|
||||||
),
|
|
||||||
passphrase: "password1234",
|
|
||||||
},
|
|
||||||
wwdr: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
certificatesPath,
|
|
||||||
"./WWDR.pem",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toBeRejected();
|
|
||||||
|
|
||||||
await expectAsync(
|
|
||||||
createPass({
|
|
||||||
model: null,
|
|
||||||
certificates: {
|
|
||||||
signerCert: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
certificatesPath,
|
|
||||||
"./signerCert.pem",
|
|
||||||
),
|
|
||||||
signerKey: {
|
|
||||||
keyFile: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
certificatesPath,
|
|
||||||
"./signerKey.pem",
|
|
||||||
),
|
|
||||||
passphrase: "password1234",
|
|
||||||
},
|
|
||||||
wwdr: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
certificatesPath,
|
|
||||||
"./WWDR.pem",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toBeRejected();
|
|
||||||
|
|
||||||
await expectAsync(
|
|
||||||
createPass({
|
|
||||||
model: {},
|
|
||||||
certificates: {
|
|
||||||
signerCert: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
certificatesPath,
|
|
||||||
"./signerCert.pem",
|
|
||||||
),
|
|
||||||
signerKey: {
|
|
||||||
keyFile: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
certificatesPath,
|
|
||||||
"./signerKey.pem",
|
|
||||||
),
|
|
||||||
passphrase: "password1234",
|
|
||||||
},
|
|
||||||
wwdr: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
certificatesPath,
|
|
||||||
"./WWDR.pem",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toBeRejected();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {}
|
|
||||||
});
|
|
||||||
318
spec/index.ts
318
spec/index.ts
@@ -1,318 +0,0 @@
|
|||||||
import { createPass, Pass } from "..";
|
|
||||||
import * as path from "path";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests created upon Jasmine testing suite.
|
|
||||||
*/
|
|
||||||
|
|
||||||
describe("Passkit-generator", function () {
|
|
||||||
let pass: Pass;
|
|
||||||
beforeEach(async () => {
|
|
||||||
pass = await createPass({
|
|
||||||
model: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../examples/models/examplePass.pass",
|
|
||||||
),
|
|
||||||
certificates: {
|
|
||||||
wwdr: path.resolve(__dirname, "../certificates/WWDR.pem"),
|
|
||||||
signerCert: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../certificates/signerCert.pem",
|
|
||||||
),
|
|
||||||
signerKey: {
|
|
||||||
keyFile: path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../certificates/signerKey.pem",
|
|
||||||
),
|
|
||||||
passphrase: "123456",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
overrides: {},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("localize()", () => {
|
|
||||||
it("Won't apply changes without at least one parameter", () => {
|
|
||||||
// @ts-expect-error
|
|
||||||
pass.localize();
|
|
||||||
expect(Object.keys(pass["l10nTranslations"]).length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Won't apply changes with a non-string first argument", () => {
|
|
||||||
// @ts-expect-error
|
|
||||||
pass.localize(5);
|
|
||||||
expect(Object.keys(pass["l10nTranslations"]).length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Will include .lproj folder if only the first argument is passed", () => {
|
|
||||||
pass.localize("en");
|
|
||||||
expect(Object.keys(pass["l10nTranslations"]).length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Will ignore all the second argument is not object or undefined", () => {
|
|
||||||
// @ts-expect-error
|
|
||||||
pass.localize("en", 42);
|
|
||||||
expect(Object.keys(pass["l10nTranslations"]).length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Will apply changes if a second object argument with translations is passed", () => {
|
|
||||||
pass.localize("it", {
|
|
||||||
Test: "Prova",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(typeof pass["l10nTranslations"]["it"]).toBe("object");
|
|
||||||
expect(pass["l10nTranslations"]["it"]["Test"]).toBe("Prova");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("expiration()", () => {
|
|
||||||
it("Won't apply changes without a valid argument", () => {
|
|
||||||
// @ts-expect-error
|
|
||||||
pass.expiration();
|
|
||||||
expect(pass.props["expirationDate"]).toBe(undefined);
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
pass.expiration(42);
|
|
||||||
expect(pass.props["expirationDate"]).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("expects a Date object as the only argument", () => {
|
|
||||||
pass.expiration(new Date(2020, 6, 1, 0, 0, 0, 0));
|
|
||||||
// Month starts from 0 in Date Object when used this way, therefore
|
|
||||||
// we expect one month more
|
|
||||||
expect(pass.props["expirationDate"]).toBe("2020-07-01T00:00:00Z");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("An invalid date, will not apply changes", () => {
|
|
||||||
// @ts-expect-error
|
|
||||||
pass.expiration("32/18/228317");
|
|
||||||
expect(pass.props["expirationDate"]).toBe(undefined);
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
pass.expiration("32/18/228317");
|
|
||||||
expect(pass.props["expirationDate"]).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("relevantDate()", () => {
|
|
||||||
it("expects a Date object as the only argument", () => {
|
|
||||||
pass.relevantDate(new Date("10-04-2021"));
|
|
||||||
expect(pass.props["relevantDate"]).toBe("2021-10-04T00:00:00Z");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("locations()", () => {
|
|
||||||
it("Won't apply changes if invalid location objects are passed", () => {
|
|
||||||
const props = pass.props["locations"] || [];
|
|
||||||
const oldAmountOfLocations = (props && props.length) || 0;
|
|
||||||
|
|
||||||
pass.locations(
|
|
||||||
{
|
|
||||||
// @ts-expect-error
|
|
||||||
ibrupofene: "no",
|
|
||||||
longitude: 0.0,
|
|
||||||
},
|
|
||||||
...props,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (oldAmountOfLocations) {
|
|
||||||
expect(pass.props["locations"].length).toBe(
|
|
||||||
oldAmountOfLocations,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
expect(pass.props["locations"]).toBe(undefined);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Will filter out invalid location objects", () => {
|
|
||||||
const props = pass.props["locations"] || [];
|
|
||||||
const oldAmountOfLocations = (props && props.length) || 0;
|
|
||||||
|
|
||||||
pass.locations(
|
|
||||||
{
|
|
||||||
// @ts-expect-error
|
|
||||||
ibrupofene: "no",
|
|
||||||
longitude: 0.0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
longitude: 4.42634523,
|
|
||||||
latitude: 5.344233323352,
|
|
||||||
},
|
|
||||||
...(pass.props["locations"] || []),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(pass.props["locations"].length).toBe(
|
|
||||||
(oldAmountOfLocations || 0) + 1,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Beacons()", () => {
|
|
||||||
it("Won't apply changes if invalid beacon objects are passed", () => {
|
|
||||||
const props = pass.props["beacons"] || [];
|
|
||||||
const oldAmountOfBeacons = (props && props.length) || 0;
|
|
||||||
|
|
||||||
pass.beacons(
|
|
||||||
{
|
|
||||||
// @ts-expect-error
|
|
||||||
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("Will filter out invalid beacons objects", () => {
|
|
||||||
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-expect-error
|
|
||||||
animal: "Monkey",
|
|
||||||
},
|
|
||||||
...props,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(pass.props["beacons"].length).toBe(oldAmountOfBeacons + 1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("barcodes()", () => {
|
|
||||||
it("Won't apply changes if no data is passed", () => {
|
|
||||||
const props = pass.props["barcodes"] || [];
|
|
||||||
const oldAmountOfBarcodes = (props && props.length) || 0;
|
|
||||||
|
|
||||||
pass.barcodes();
|
|
||||||
expect(pass.props["barcodes"].length).toBe(oldAmountOfBarcodes);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Will ignore boolean parameter", () => {
|
|
||||||
const props = pass.props["barcodes"] || [];
|
|
||||||
const oldAmountOfBarcodes = (props && props.length) || 0;
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
pass.barcode(true);
|
|
||||||
expect(props.length).toBe(oldAmountOfBarcodes);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Will ignore numeric parameter", () => {
|
|
||||||
const props = pass.props["barcodes"] || [];
|
|
||||||
const oldAmountOfBarcodes = (props && props.length) || 0;
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
pass.barcodes(42);
|
|
||||||
expect(pass.props["barcodes"].length).toBe(oldAmountOfBarcodes);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Will autogenerate all the barcode objects with a string parameter (message)", () => {
|
|
||||||
pass.barcodes("28363516282");
|
|
||||||
expect(pass.props["barcodes"].length).toBe(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Will accept object parameters", () => {
|
|
||||||
pass.barcodes({
|
|
||||||
message: "28363516282",
|
|
||||||
format: "PKBarcodeFormatPDF417",
|
|
||||||
messageEncoding: "utf8",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(pass.props["barcodes"].length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Will automatically add messageEncoding if missing in valid Barcodes objects", () => {
|
|
||||||
pass.barcodes({
|
|
||||||
message: "28363516282",
|
|
||||||
format: "PKBarcodeFormatPDF417",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(pass.props["barcodes"][0].messageEncoding).toBe(
|
|
||||||
"iso-8859-1",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Will ignore objects without message property", () => {
|
|
||||||
const props = pass.props["barcodes"] || [];
|
|
||||||
const oldAmountOfBarcodes = (props && props.length) || 0;
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
pass.barcodes({
|
|
||||||
format: "PKBarcodeFormatPDF417",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(pass.props["barcodes"].length).toBe(oldAmountOfBarcodes);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Will ignore non-Barcodes schema compliant objects", () => {
|
|
||||||
pass.barcodes(
|
|
||||||
// @ts-expect-error
|
|
||||||
5,
|
|
||||||
10,
|
|
||||||
15,
|
|
||||||
{
|
|
||||||
message: "28363516282",
|
|
||||||
format: "PKBarcodeFormatPDF417",
|
|
||||||
},
|
|
||||||
7,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(pass.props["barcodes"].length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Will reset barcodes content if parameter is null", () => {
|
|
||||||
pass.barcodes(null);
|
|
||||||
expect(pass.props["barcodes"]).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("barcode retrocompatibility", () => {
|
|
||||||
it("Will ignore non string or null arguments", function () {
|
|
||||||
const oldBarcode = pass.props["barcode"] || undefined;
|
|
||||||
|
|
||||||
pass.barcodes("Message-22645272183")
|
|
||||||
// @ts-expect-error
|
|
||||||
.barcode(55);
|
|
||||||
|
|
||||||
// unchanged
|
|
||||||
expect(pass.props["barcode"]).toEqual(oldBarcode);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Will reset backward value on null", () => {
|
|
||||||
pass.barcodes("Message-22645272183").barcode(
|
|
||||||
"PKBarcodeFormatAztec",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(pass.props["barcode"].format).toBe("PKBarcodeFormatAztec");
|
|
||||||
|
|
||||||
pass.barcode(null);
|
|
||||||
expect(pass.props["barcode"]).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Won't apply changes if unknown format is passed", () => {
|
|
||||||
const oldBarcode = pass.props["barcode"] || undefined;
|
|
||||||
|
|
||||||
pass.barcodes("Message-22645272183")
|
|
||||||
// @ts-expect-error
|
|
||||||
.barcode("PKBingoBongoFormat");
|
|
||||||
|
|
||||||
expect(pass.props["barcode"]).toEqual(oldBarcode);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,59 +1,39 @@
|
|||||||
import { splitBufferBundle } from "../lib/utils";
|
import { processDate, removeHidden } from "../lib/utils";
|
||||||
import type { BundleUnit } from "../lib/schemas";
|
|
||||||
|
|
||||||
describe("splitBufferBundle", () => {
|
describe("Utils", () => {
|
||||||
it("should split the bundle in language-organized files buffers and normal files with valid bundleUnit passed", () => {
|
describe("removeHidden", () => {
|
||||||
const zeroBuffer = Buffer.alloc(0);
|
it("should remove files that start with dot", () => {
|
||||||
const payload: BundleUnit = {
|
const filesList = [
|
||||||
"en.lproj/thumbnail@2x.png": zeroBuffer,
|
"a.png",
|
||||||
"de.lproj/background@2x.png": zeroBuffer,
|
"b.png",
|
||||||
"it.lproj/thumbnail@2x.png": zeroBuffer,
|
".DS_Store",
|
||||||
"thumbnail@2x.png": zeroBuffer,
|
"not_the_droids_you_are_looking_for.txt",
|
||||||
"background.png": zeroBuffer,
|
];
|
||||||
};
|
|
||||||
|
|
||||||
const result = splitBufferBundle(payload);
|
expect(removeHidden(filesList)).toEqual([
|
||||||
|
"a.png",
|
||||||
expect(result).toBeDefined();
|
"b.png",
|
||||||
expect(result.length).toBe(2);
|
"not_the_droids_you_are_looking_for.txt",
|
||||||
expect(result[0]).toEqual({
|
]);
|
||||||
"en.lproj": {
|
|
||||||
"thumbnail@2x.png": zeroBuffer,
|
|
||||||
},
|
|
||||||
"de.lproj": {
|
|
||||||
"background@2x.png": zeroBuffer,
|
|
||||||
},
|
|
||||||
"it.lproj": {
|
|
||||||
"thumbnail@2x.png": zeroBuffer,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(result[1]).toEqual({
|
|
||||||
"thumbnail@2x.png": zeroBuffer,
|
|
||||||
"background.png": zeroBuffer,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return empty partitionedBufferBundle if BundleUnit is empty object", () => {
|
describe("processDate", () => {
|
||||||
const result = splitBufferBundle({});
|
it("should throw Invalid date if args[0] is not a date", () => {
|
||||||
|
//@ts-expect-error
|
||||||
|
expect(() => processDate(5)).toThrow("Invalid date");
|
||||||
|
//@ts-expect-error
|
||||||
|
expect(() => processDate({})).toThrow("Invalid date");
|
||||||
|
//@ts-expect-error
|
||||||
|
expect(() => processDate("ciao")).toThrow("Invalid date");
|
||||||
|
//@ts-expect-error
|
||||||
|
expect(() => processDate(true)).toThrow("Invalid date");
|
||||||
|
});
|
||||||
|
|
||||||
expect(result).toBeDefined();
|
it("should convert a Date object to a valid W3C date", () => {
|
||||||
expect(result.length).toBe(2);
|
expect(processDate(new Date(2020, 6, 1, 0, 0, 0, 0))).toBe(
|
||||||
expect(result[0]).toEqual({});
|
"2020-07-01T00:00:00Z",
|
||||||
expect(result[1]).toEqual({});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return empty partitionedBufferBundle if BundleUnit is undefined", () => {
|
|
||||||
const resultUndefined = splitBufferBundle(undefined);
|
|
||||||
const resultNull = splitBufferBundle(null);
|
|
||||||
|
|
||||||
expect(resultUndefined).toBeDefined();
|
|
||||||
expect(resultUndefined.length).toBe(2);
|
|
||||||
expect(resultUndefined[0]).toEqual({});
|
|
||||||
expect(resultUndefined[1]).toEqual({});
|
|
||||||
|
|
||||||
expect(resultNull).toBeDefined();
|
|
||||||
expect(resultNull.length).toBe(2);
|
|
||||||
expect(resultNull[0]).toEqual({});
|
|
||||||
expect(resultNull[1]).toEqual({});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
162
src/Bundle.ts
Normal file
162
src/Bundle.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { Readable, Stream } from "stream";
|
||||||
|
import * as Messages from "./messages";
|
||||||
|
import * as zip from "do-not-zip";
|
||||||
|
|
||||||
|
export const filesSymbol = Symbol("bundleFiles");
|
||||||
|
export const freezeSymbol = Symbol("bundleFreeze");
|
||||||
|
export const mimeTypeSymbol = Symbol("bundleMimeType");
|
||||||
|
|
||||||
|
namespace Mime {
|
||||||
|
export type type = string;
|
||||||
|
export type subtype = string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a container ready to be distributed.
|
||||||
|
* If no mimeType is passed to the constructor,
|
||||||
|
* it will throw an error.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class Bundle {
|
||||||
|
private [filesSymbol]: { [key: string]: Buffer } = {};
|
||||||
|
private [mimeTypeSymbol]: string;
|
||||||
|
|
||||||
|
constructor(mimeType: `${Mime.type}/${Mime.subtype}`) {
|
||||||
|
if (!mimeType) {
|
||||||
|
throw new Error(Messages.BUNDLE.MIME_TYPE_MISSING);
|
||||||
|
}
|
||||||
|
|
||||||
|
this[mimeTypeSymbol] = mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a bundle and exposes the
|
||||||
|
* function to freeze it manually once
|
||||||
|
* completed.
|
||||||
|
*
|
||||||
|
* This was made to not expose freeze
|
||||||
|
* function outside of Bundle class.
|
||||||
|
*
|
||||||
|
* Normally, a bundle would get freezed
|
||||||
|
* when using getAsBuffer or getAsStream
|
||||||
|
* but when creating a PKPasses archive,
|
||||||
|
* we need to freeze the bundle so the
|
||||||
|
* user cannot add more files (we want to
|
||||||
|
* allow them to only the selected files)
|
||||||
|
* but also letting them choose how to
|
||||||
|
* export it.
|
||||||
|
*
|
||||||
|
* @param mimeType
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
static freezable(
|
||||||
|
mimeType: `${Mime.type}/${Mime.subtype}`,
|
||||||
|
): [Bundle, Function] {
|
||||||
|
const bundle = new Bundle(mimeType);
|
||||||
|
return [bundle, () => bundle[freezeSymbol]()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves bundle's mimeType
|
||||||
|
*/
|
||||||
|
|
||||||
|
public get mimeType() {
|
||||||
|
return this[mimeTypeSymbol];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freezes the bundle so no more files
|
||||||
|
* can be added any further.
|
||||||
|
*/
|
||||||
|
|
||||||
|
protected [freezeSymbol]() {
|
||||||
|
if (this.isFrozen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.freeze(this[filesSymbol]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells if this bundle still allows files to be added.
|
||||||
|
* @returns false if files are allowed, true otherwise
|
||||||
|
*/
|
||||||
|
|
||||||
|
public get isFrozen() {
|
||||||
|
return Object.isFrozen(this[filesSymbol]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows files to be added to the bundle.
|
||||||
|
* If the bundle is closed, it will throw an error.
|
||||||
|
*
|
||||||
|
* @param fileName
|
||||||
|
* @param buffer
|
||||||
|
*/
|
||||||
|
|
||||||
|
public addBuffer(fileName: string, buffer: Buffer) {
|
||||||
|
if (this.isFrozen) {
|
||||||
|
throw new Error(Messages.BUNDLE.CLOSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
this[filesSymbol][fileName] = buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the bundle and returns it as a Buffer.
|
||||||
|
* Once closed, the bundle does not allow files
|
||||||
|
* to be added any further.
|
||||||
|
*
|
||||||
|
* @returns Buffer
|
||||||
|
*/
|
||||||
|
|
||||||
|
public getAsBuffer(): Buffer {
|
||||||
|
this[freezeSymbol]();
|
||||||
|
return zip.toBuffer(createZipFilesMap(this[filesSymbol]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the bundle and returns it as a stream.
|
||||||
|
* Once closed, the bundle does not allow files
|
||||||
|
* to be added any further.
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
public getAsStream(): Stream {
|
||||||
|
this[freezeSymbol]();
|
||||||
|
return Readable.from(
|
||||||
|
zip.toBuffer(createZipFilesMap(this[filesSymbol])),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the bundle and returns it as an object.
|
||||||
|
* This allows developers to choose a different way
|
||||||
|
* of serving, analyzing or zipping the file, outside the
|
||||||
|
* default compression system.
|
||||||
|
*
|
||||||
|
* @returns a frozen object containing files paths as key
|
||||||
|
* and Buffers as content.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public getAsRaw(): { [filePath: string]: Buffer } {
|
||||||
|
this[freezeSymbol]();
|
||||||
|
return Object.freeze({ ...this[filesSymbol] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a files map for do-not-zip
|
||||||
|
*
|
||||||
|
* @param files
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
function createZipFilesMap(files: { [key: string]: Buffer }) {
|
||||||
|
return Object.entries(files).map(([path, data]) => ({
|
||||||
|
path,
|
||||||
|
data,
|
||||||
|
}));
|
||||||
|
}
|
||||||
101
src/FieldsArray.ts
Normal file
101
src/FieldsArray.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import type PKPass from "./PKPass";
|
||||||
|
import * as Schemas from "./schemas";
|
||||||
|
import * as Utils from "./utils";
|
||||||
|
import * as Messages from "./messages";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to represent lower-level keys pass fields
|
||||||
|
* @see https://apple.co/2wkUBdh
|
||||||
|
*/
|
||||||
|
|
||||||
|
const passInstanceSymbol = Symbol("passInstance");
|
||||||
|
const sharedKeysPoolSymbol = Symbol("keysPool");
|
||||||
|
|
||||||
|
export default class FieldsArray extends Array<Schemas.Field> {
|
||||||
|
private [passInstanceSymbol]: InstanceType<typeof PKPass>;
|
||||||
|
private [sharedKeysPoolSymbol]: Set<string>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
passInstance: InstanceType<typeof PKPass>,
|
||||||
|
keysPool: Set<string>,
|
||||||
|
...args: Schemas.Field[]
|
||||||
|
) {
|
||||||
|
super(...args);
|
||||||
|
this[passInstanceSymbol] = passInstance;
|
||||||
|
this[sharedKeysPoolSymbol] = keysPool;
|
||||||
|
}
|
||||||
|
|
||||||
|
push(...items: Schemas.Field[]): number {
|
||||||
|
const validItems = registerWithValidation(this, ...items);
|
||||||
|
return super.push(...validItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
pop(): Schemas.Field {
|
||||||
|
return unregisterItems(this, () => super.pop());
|
||||||
|
}
|
||||||
|
|
||||||
|
splice(
|
||||||
|
start: number,
|
||||||
|
deleteCount: number,
|
||||||
|
...items: Schemas.Field[]
|
||||||
|
): Schemas.Field[] {
|
||||||
|
// Perfoming frozen check, validation and getting valid items
|
||||||
|
const validItems = registerWithValidation(this, ...items);
|
||||||
|
|
||||||
|
for (let i = start; i < start + deleteCount; i++) {
|
||||||
|
this[sharedKeysPoolSymbol].delete(this[i].key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.splice(start, deleteCount, ...validItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
shift() {
|
||||||
|
return unregisterItems(this, () => super.shift());
|
||||||
|
}
|
||||||
|
|
||||||
|
unshift(...items: Schemas.Field[]) {
|
||||||
|
const validItems = registerWithValidation(this, ...items);
|
||||||
|
return super.unshift(...validItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerWithValidation(
|
||||||
|
instance: InstanceType<typeof FieldsArray>,
|
||||||
|
...items: Schemas.Field[]
|
||||||
|
) {
|
||||||
|
Utils.assertUnfrozen(instance[passInstanceSymbol]);
|
||||||
|
|
||||||
|
let validItems: Schemas.Field[] = [];
|
||||||
|
|
||||||
|
for (let i = items.length, field: Schemas.Field; (field = items[--i]); ) {
|
||||||
|
try {
|
||||||
|
Schemas.assertValidity(
|
||||||
|
Schemas.Field,
|
||||||
|
field,
|
||||||
|
Messages.FIELDS.INVALID,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (instance[sharedKeysPoolSymbol].has(field.key)) {
|
||||||
|
throw Messages.format(Messages.FIELDS.REPEATED_KEY, field.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
instance[sharedKeysPoolSymbol].add(field.key);
|
||||||
|
validItems.push(field);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unregisterItems(
|
||||||
|
instance: InstanceType<typeof FieldsArray>,
|
||||||
|
removeFn: Function,
|
||||||
|
) {
|
||||||
|
Utils.assertUnfrozen(instance[passInstanceSymbol]);
|
||||||
|
|
||||||
|
const element: Schemas.Field = removeFn();
|
||||||
|
instance[sharedKeysPoolSymbol].delete(element.key);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
963
src/PKPass.ts
Normal file
963
src/PKPass.ts
Normal file
@@ -0,0 +1,963 @@
|
|||||||
|
import { Stream } from "stream";
|
||||||
|
import FieldsArray from "./FieldsArray";
|
||||||
|
import Bundle, { filesSymbol } from "./Bundle";
|
||||||
|
import getModelFolderContents from "./getModelFolderContents";
|
||||||
|
import * as Schemas from "./schemas";
|
||||||
|
import * as Signature from "./Signature";
|
||||||
|
import * as Strings from "./StringsUtils";
|
||||||
|
import * as Utils from "./utils";
|
||||||
|
import * as Messages from "./messages";
|
||||||
|
|
||||||
|
/** Exporting for tests specs */
|
||||||
|
export const propsSymbol = Symbol("props");
|
||||||
|
export const localizationSymbol = Symbol("pass.l10n");
|
||||||
|
export const importMetadataSymbol = Symbol("import.pass.metadata");
|
||||||
|
export const createManifestSymbol = Symbol("pass.manifest");
|
||||||
|
export const closePassSymbol = Symbol("pass.close");
|
||||||
|
export const passTypeSymbol = Symbol("pass.type");
|
||||||
|
export const certificatesSymbol = Symbol("pass.certificates");
|
||||||
|
|
||||||
|
export default class PKPass extends Bundle {
|
||||||
|
private [certificatesSymbol]: Schemas.CertificatesSchema;
|
||||||
|
private [propsSymbol]: Schemas.PassProps = {};
|
||||||
|
private [localizationSymbol]: {
|
||||||
|
[lang: string]: {
|
||||||
|
[placeholder: string]: string;
|
||||||
|
};
|
||||||
|
} = {};
|
||||||
|
private [passTypeSymbol]: Schemas.PassTypesProps = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Either create a pass from another one
|
||||||
|
* or a disk path.
|
||||||
|
*
|
||||||
|
* @param source
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
static async from<S extends PKPass | Schemas.Template>(
|
||||||
|
source: S,
|
||||||
|
props?: Schemas.OverridablePassProps,
|
||||||
|
): Promise<PKPass> {
|
||||||
|
let certificates: Schemas.CertificatesSchema = undefined;
|
||||||
|
let buffers: Schemas.FileBuffers = undefined;
|
||||||
|
|
||||||
|
if (!source) {
|
||||||
|
throw new TypeError(
|
||||||
|
Messages.format(Messages.FROM.MISSING_SOURCE, source),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source instanceof PKPass) {
|
||||||
|
/** Cloning is happening here */
|
||||||
|
certificates = source[certificatesSymbol];
|
||||||
|
buffers = {};
|
||||||
|
|
||||||
|
const buffersEntries = Object.entries(source[filesSymbol]);
|
||||||
|
|
||||||
|
/** Cloning all the buffers to prevent unwanted edits */
|
||||||
|
for (let i = 0; i < buffersEntries.length; i++) {
|
||||||
|
const [fileName, contentBuffer] = buffersEntries[i];
|
||||||
|
|
||||||
|
buffers[fileName] = Buffer.alloc(contentBuffer.length);
|
||||||
|
contentBuffer.copy(buffers[fileName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moving props to pass.json instead of overrides
|
||||||
|
* because many might get excluded when passing
|
||||||
|
* through validation
|
||||||
|
*/
|
||||||
|
|
||||||
|
buffers["pass.json"] = Buffer.from(
|
||||||
|
JSON.stringify(source[propsSymbol]),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Schemas.assertValidity(
|
||||||
|
Schemas.Template,
|
||||||
|
source,
|
||||||
|
Messages.TEMPLATE.INVALID,
|
||||||
|
);
|
||||||
|
|
||||||
|
buffers = await getModelFolderContents(source.model);
|
||||||
|
certificates = source.certificates;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PKPass(buffers, certificates, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Bundle made of PKPass to be distributed
|
||||||
|
* as a `.pkpasses` zip file. Returns a Bundle instance
|
||||||
|
* so it can be outputted both as stream or as a buffer.
|
||||||
|
*
|
||||||
|
* Using this will freeze all the instances passed as
|
||||||
|
* parameter.
|
||||||
|
*
|
||||||
|
* Throws if not all the files are instance of PKPass.
|
||||||
|
*
|
||||||
|
* @param passes
|
||||||
|
*/
|
||||||
|
|
||||||
|
static pack(...passes: PKPass[]): Bundle {
|
||||||
|
if (!passes.every((pass) => pass instanceof PKPass)) {
|
||||||
|
throw new Error(Messages.PACK.INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffers = passes.map((pass) => pass.getAsBuffer());
|
||||||
|
|
||||||
|
const [bundle, freezeBundle] = Bundle.freezable(
|
||||||
|
"application/vnd.apple.pkpasses",
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 0; i < buffers.length; i++) {
|
||||||
|
bundle.addBuffer(`packed-pass-${i + 1}.pkpass`, buffers[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
freezeBundle();
|
||||||
|
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************** //
|
||||||
|
// *** INSTANCE *** //
|
||||||
|
// **************** //
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
buffers: Schemas.FileBuffers,
|
||||||
|
certificates: Schemas.CertificatesSchema,
|
||||||
|
props?: Schemas.OverridablePassProps,
|
||||||
|
) {
|
||||||
|
super("application/vnd.apple.pkpass");
|
||||||
|
|
||||||
|
const buffersEntries = Object.entries(buffers);
|
||||||
|
|
||||||
|
for (
|
||||||
|
let i = buffersEntries.length, buffer: [string, Buffer];
|
||||||
|
(buffer = buffersEntries[--i]);
|
||||||
|
|
||||||
|
) {
|
||||||
|
const [fileName, contentBuffer] = buffer;
|
||||||
|
this.addBuffer(fileName, contentBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props) {
|
||||||
|
/** Overrides validation and pushing in props */
|
||||||
|
const overridesValidation = Schemas.validate(
|
||||||
|
Schemas.OverridablePassProps,
|
||||||
|
props,
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.assign(this[propsSymbol], overridesValidation);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.certificates = certificates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows changing the certificates, if needed.
|
||||||
|
* They are actually expected to be received in
|
||||||
|
* the constructor, but they can get overridden
|
||||||
|
* here for whatever purpose.
|
||||||
|
*
|
||||||
|
* When using this setter, all certificates are
|
||||||
|
* expected to be received, or an exception will
|
||||||
|
* be thrown.
|
||||||
|
*
|
||||||
|
* @param certs
|
||||||
|
*/
|
||||||
|
|
||||||
|
public set certificates(certs: Schemas.CertificatesSchema) {
|
||||||
|
Utils.assertUnfrozen(this);
|
||||||
|
|
||||||
|
Schemas.assertValidity(
|
||||||
|
Schemas.CertificatesSchema,
|
||||||
|
certs,
|
||||||
|
Messages.CERTIFICATES.INVALID,
|
||||||
|
);
|
||||||
|
|
||||||
|
this[certificatesSymbol] = certs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows retrieving current languages
|
||||||
|
*/
|
||||||
|
|
||||||
|
public get languages() {
|
||||||
|
return Object.keys(this[localizationSymbol]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows getting an image of the props
|
||||||
|
* that are composing your pass instance.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public get props(): Schemas.PassProps {
|
||||||
|
return Utils.cloneRecursive(this[propsSymbol]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows setting a transitType property
|
||||||
|
* for a boardingPass. Throws an error if
|
||||||
|
* the current type is not a boardingPass.
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
|
||||||
|
public set transitType(value: Schemas.TransitType) {
|
||||||
|
Utils.assertUnfrozen(this);
|
||||||
|
|
||||||
|
if (this.type !== "boardingPass") {
|
||||||
|
throw new TypeError(Messages.TRANSIT_TYPE.UNEXPECTED_PASS_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
Schemas.assertValidity(
|
||||||
|
Schemas.TransitType,
|
||||||
|
value,
|
||||||
|
Messages.TRANSIT_TYPE.INVALID,
|
||||||
|
);
|
||||||
|
this[propsSymbol]["boardingPass"].transitType = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows getting the current transitType
|
||||||
|
* from pass props
|
||||||
|
*/
|
||||||
|
|
||||||
|
public get transitType() {
|
||||||
|
return this[propsSymbol]["boardingPass"].transitType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows accessing to primaryFields object.
|
||||||
|
*
|
||||||
|
* It will (automatically) throw an error if
|
||||||
|
* no valid pass.json has been parsed yet or,
|
||||||
|
* anyway, if it has not a valid type.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public get primaryFields(): Schemas.Field[] {
|
||||||
|
return this[propsSymbol][this.type].primaryFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows accessing to secondaryFields object
|
||||||
|
*
|
||||||
|
* It will (automatically) throw an error if
|
||||||
|
* no valid pass.json has been parsed yet or,
|
||||||
|
* anyway, if it has not a valid type.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public get secondaryFields(): Schemas.Field[] {
|
||||||
|
return this[propsSymbol][this.type].secondaryFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows accessing to auxiliaryFields object
|
||||||
|
*
|
||||||
|
* It will (automatically) throw an error if
|
||||||
|
* no valid pass.json has been parsed yet or,
|
||||||
|
* anyway, if it has not a valid type.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public get auxiliaryFields(): Schemas.Field[] {
|
||||||
|
return this[propsSymbol][this.type].auxiliaryFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows accessing to headerFields object
|
||||||
|
*
|
||||||
|
* It will (automatically) throw an error if
|
||||||
|
* no valid pass.json has been parsed yet or,
|
||||||
|
* anyway, if it has not a valid type.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public get headerFields(): Schemas.Field[] {
|
||||||
|
return this[propsSymbol][this.type].headerFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows accessing to backFields object
|
||||||
|
*
|
||||||
|
* It will (automatically) throw an error if
|
||||||
|
* no valid pass.json has been parsed yet or,
|
||||||
|
* anyway, if it has not a valid type.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public get backFields(): Schemas.Field[] {
|
||||||
|
return this[propsSymbol][this.type].backFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows setting a pass type.
|
||||||
|
*
|
||||||
|
* **Warning**: setting a type with this setter,
|
||||||
|
* will reset all the imported or manually
|
||||||
|
* setted fields (primaryFields, secondaryFields,
|
||||||
|
* headerFields, auxiliaryFields, backFields)
|
||||||
|
*/
|
||||||
|
|
||||||
|
public set type(type: Schemas.PassTypesProps) {
|
||||||
|
Utils.assertUnfrozen(this);
|
||||||
|
|
||||||
|
Schemas.assertValidity(
|
||||||
|
Schemas.PassType,
|
||||||
|
type,
|
||||||
|
Messages.PASS_TYPE.INVALID,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.type) {
|
||||||
|
/**
|
||||||
|
* Removing reference to previous type and its content because
|
||||||
|
* we might have some differences between types. It is way easier
|
||||||
|
* to reset everything instead of making checks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
this[propsSymbol][this.type] = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sharedKeysPool = new Set<string>();
|
||||||
|
|
||||||
|
this[passTypeSymbol] = type;
|
||||||
|
this[propsSymbol][this[passTypeSymbol]] = {
|
||||||
|
headerFields /******/: new FieldsArray(this, sharedKeysPool),
|
||||||
|
primaryFields /*****/: new FieldsArray(this, sharedKeysPool),
|
||||||
|
secondaryFields /***/: new FieldsArray(this, sharedKeysPool),
|
||||||
|
auxiliaryFields /***/: new FieldsArray(this, sharedKeysPool),
|
||||||
|
backFields /********/: new FieldsArray(this, sharedKeysPool),
|
||||||
|
transitType: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public get type(): Schemas.PassTypesProps | undefined {
|
||||||
|
return this[passTypeSymbol] ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************************** //
|
||||||
|
// *** ASSETS SETUP METHODS *** //
|
||||||
|
// **************************** //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows adding a new asset inside the pass / bundle;
|
||||||
|
* If an empty buffer is passed, it won't be added to
|
||||||
|
* the bundle.
|
||||||
|
*
|
||||||
|
* `manifest.json` and `signature` files will be ignored.
|
||||||
|
*
|
||||||
|
* If a `pass.json` is passed to this method (and it has
|
||||||
|
* not been added previously), it will be read, validated
|
||||||
|
* and merged in the current instance. Its properties
|
||||||
|
* will overwrite the ones setted through methods.
|
||||||
|
*
|
||||||
|
* If a `pass.strings` file is passed, it will be read, parsed
|
||||||
|
* and merged with the translations added previously.
|
||||||
|
* Comments will be ignored.
|
||||||
|
*
|
||||||
|
* @param pathName
|
||||||
|
* @param buffer
|
||||||
|
*/
|
||||||
|
|
||||||
|
public addBuffer(pathName: string, buffer: Buffer): void {
|
||||||
|
if (!buffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/manifest|signature/.test(pathName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/pass\.json/.test(pathName)) {
|
||||||
|
if (this[filesSymbol]["pass.json"]) {
|
||||||
|
/**
|
||||||
|
* Ignoring any further addition. In a
|
||||||
|
* future we might consider merging instead
|
||||||
|
*/
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this[importMetadataSymbol](
|
||||||
|
validateJSONBuffer(buffer, Schemas.PassProps),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
Messages.format(Messages.PASS_SOURCE.INVALID, err),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adding an empty buffer just for reference
|
||||||
|
* that we received a valid pass.json file.
|
||||||
|
* It will be reconciliated in export phase.
|
||||||
|
*/
|
||||||
|
|
||||||
|
return super.addBuffer(pathName, Buffer.alloc(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/personalization\.json/.test(pathName)) {
|
||||||
|
/**
|
||||||
|
* We are still allowing `personalizationLogo@XX.png`
|
||||||
|
* to be added to the bundle, but we'll delete it
|
||||||
|
* once the pass is getting closed, if needed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateJSONBuffer(buffer, Schemas.Personalize);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
Messages.format(Messages.PERSONALIZE.INVALID, err),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.addBuffer(pathName, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a new pass.strings file is added, we want to
|
||||||
|
* prevent it from being merged and, instead, save
|
||||||
|
* its translations for later
|
||||||
|
*/
|
||||||
|
|
||||||
|
const translationsFileRegexp =
|
||||||
|
/(?<lang>[a-zA-Z-]{2,}).lproj\/pass\.strings/;
|
||||||
|
|
||||||
|
let match: RegExpMatchArray;
|
||||||
|
|
||||||
|
if ((match = pathName.match(translationsFileRegexp))) {
|
||||||
|
const [, lang] = match;
|
||||||
|
|
||||||
|
const parsedTranslations = Strings.parse(buffer).translations;
|
||||||
|
|
||||||
|
if (!parsedTranslations.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.localize(lang, Object.fromEntries(parsedTranslations));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.addBuffer(pathName, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given data from a pass.json, reads them to bring them
|
||||||
|
* into the current pass instance.
|
||||||
|
*
|
||||||
|
* **Warning**: if this file contains a type (boardingPass,
|
||||||
|
* coupon, and so on), it will replace the current one,
|
||||||
|
* causing, therefore, the destroy of the fields added
|
||||||
|
* previously.
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
|
||||||
|
private [importMetadataSymbol](data: Schemas.PassProps) {
|
||||||
|
const possibleTypes = [
|
||||||
|
"boardingPass",
|
||||||
|
"coupon",
|
||||||
|
"eventTicket",
|
||||||
|
"storeCard",
|
||||||
|
"generic",
|
||||||
|
] as Schemas.PassTypesProps[];
|
||||||
|
|
||||||
|
const type = possibleTypes.find((type) => Boolean(data[type]));
|
||||||
|
|
||||||
|
const {
|
||||||
|
boardingPass,
|
||||||
|
coupon,
|
||||||
|
storeCard,
|
||||||
|
generic,
|
||||||
|
eventTicket,
|
||||||
|
...otherPassData
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
if (Object.keys(this[propsSymbol]).length) {
|
||||||
|
console.warn(Messages.PASS_SOURCE.JOIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this[propsSymbol], otherPassData);
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
if (!this[passTypeSymbol]) {
|
||||||
|
console.warn(Messages.PASS_SOURCE.UNKNOWN_TYPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.type = type;
|
||||||
|
|
||||||
|
const {
|
||||||
|
headerFields = [],
|
||||||
|
primaryFields = [],
|
||||||
|
secondaryFields = [],
|
||||||
|
auxiliaryFields = [],
|
||||||
|
backFields = [],
|
||||||
|
} = data[type];
|
||||||
|
|
||||||
|
this.headerFields.push(...headerFields);
|
||||||
|
this.primaryFields.push(...primaryFields);
|
||||||
|
this.secondaryFields.push(...secondaryFields);
|
||||||
|
this.auxiliaryFields.push(...auxiliaryFields);
|
||||||
|
this.backFields.push(...backFields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the manifest starting from files
|
||||||
|
* added to the bundle
|
||||||
|
*/
|
||||||
|
|
||||||
|
private [createManifestSymbol](): Buffer {
|
||||||
|
const manifest = Object.entries(this[filesSymbol]).reduce<{
|
||||||
|
[key: string]: string;
|
||||||
|
}>(
|
||||||
|
(acc, [fileName, buffer]) => ({
|
||||||
|
...acc,
|
||||||
|
[fileName]: Signature.createHash(buffer),
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
return Buffer.from(JSON.stringify(manifest));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the last validation checks against props,
|
||||||
|
* applies the props to pass.json and creates l10n
|
||||||
|
* files and folders and creates manifest and
|
||||||
|
* signature files
|
||||||
|
*/
|
||||||
|
|
||||||
|
private [closePassSymbol](
|
||||||
|
__test_disable_manifest_signature_generation__: boolean = false,
|
||||||
|
) {
|
||||||
|
if (!this.type) {
|
||||||
|
throw new TypeError(Messages.CLOSE.MISSING_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileNames = Object.keys(this[filesSymbol]);
|
||||||
|
|
||||||
|
const passJson = Buffer.from(JSON.stringify(this[propsSymbol]));
|
||||||
|
super.addBuffer("pass.json", passJson);
|
||||||
|
|
||||||
|
const ICON_REGEX = /icon(?:@\d{1}x)?/;
|
||||||
|
if (!fileNames.some((fileName) => ICON_REGEX.test(fileName))) {
|
||||||
|
console.warn(Messages.CLOSE.MISSING_ICON);
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************************** //
|
||||||
|
// *** LOCALIZATION FILES CREATION *** //
|
||||||
|
// *********************************** //
|
||||||
|
|
||||||
|
const localizationEntries = Object.entries(this[localizationSymbol]);
|
||||||
|
|
||||||
|
for (
|
||||||
|
let i = localizationEntries.length,
|
||||||
|
entry: [string, { [key: string]: string }];
|
||||||
|
(entry = localizationEntries[--i]);
|
||||||
|
|
||||||
|
) {
|
||||||
|
const [lang, translations] = entry;
|
||||||
|
|
||||||
|
const stringsFile = Strings.create(translations);
|
||||||
|
|
||||||
|
if (stringsFile.length) {
|
||||||
|
super.addBuffer(`${lang}.lproj/pass.strings`, stringsFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// *********************** //
|
||||||
|
// *** PERSONALIZATION *** //
|
||||||
|
// *********************** //
|
||||||
|
|
||||||
|
const meetsPersonalizationRequirements = Boolean(
|
||||||
|
this[propsSymbol]["nfc"] &&
|
||||||
|
this[filesSymbol]["personalization.json"] &&
|
||||||
|
fileNames.find((file) =>
|
||||||
|
/personalizationLogo@(?:.{2})/.test(file),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!meetsPersonalizationRequirements) {
|
||||||
|
/**
|
||||||
|
* Looking for every personalization file
|
||||||
|
* and removing it
|
||||||
|
*/
|
||||||
|
|
||||||
|
for (let i = 0; i < fileNames.length; i++) {
|
||||||
|
if (/personalization/.test(fileNames[i])) {
|
||||||
|
console.warn(
|
||||||
|
Messages.format(
|
||||||
|
Messages.CLOSE.PERSONALIZATION_REMOVED,
|
||||||
|
fileNames[i],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
delete this[filesSymbol][fileNames[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ******************************** //
|
||||||
|
// *** BOARDING PASS VALIDATION *** //
|
||||||
|
// ******************************** //
|
||||||
|
|
||||||
|
if (this.type === "boardingPass" && !this.transitType) {
|
||||||
|
throw new TypeError(Messages.CLOSE.MISSING_TRANSIT_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ****************************** //
|
||||||
|
// *** SIGNATURE AND MANIFEST *** //
|
||||||
|
// ****************************** //
|
||||||
|
|
||||||
|
if (__test_disable_manifest_signature_generation__) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifestBuffer = this[createManifestSymbol]();
|
||||||
|
super.addBuffer("manifest.json", manifestBuffer);
|
||||||
|
|
||||||
|
const signatureBuffer = Signature.create(
|
||||||
|
manifestBuffer,
|
||||||
|
this[certificatesSymbol],
|
||||||
|
);
|
||||||
|
super.addBuffer("signature", signatureBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ************************* //
|
||||||
|
// *** EXPORTING METHODS *** //
|
||||||
|
// ************************* //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports the pass as a zip buffer. When this method
|
||||||
|
* is invoked, the bundle will get frozen and, thus,
|
||||||
|
* no files will be allowed to be added any further.
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
public getAsBuffer(): Buffer {
|
||||||
|
if (!this.isFrozen) {
|
||||||
|
this[closePassSymbol]();
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.getAsBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports the pass as a zip stream. When this method
|
||||||
|
* is invoked, the bundle will get frozen and, thus,
|
||||||
|
* no files will be allowed to be added any further.
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
public getAsStream(): Stream {
|
||||||
|
if (!this.isFrozen) {
|
||||||
|
this[closePassSymbol]();
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.getAsStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports the pass as a list of file paths and buffers.
|
||||||
|
* When this method is invoked, the bundle will get
|
||||||
|
* frozen and, thus, no files will be allowed to be
|
||||||
|
* added any further.
|
||||||
|
*
|
||||||
|
* This allows developers to choose a different way
|
||||||
|
* of serving, analyzing or zipping the file, outside the
|
||||||
|
* default compression system.
|
||||||
|
*
|
||||||
|
* @returns a frozen object containing files paths as key
|
||||||
|
* and Buffers as content.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public getAsRaw(): { [filePath: string]: Buffer } {
|
||||||
|
if (!this.isFrozen) {
|
||||||
|
this[closePassSymbol]();
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.getAsRaw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ************************** //
|
||||||
|
// *** DATA SETUP METHODS *** //
|
||||||
|
// ************************** //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to add a localization details to the
|
||||||
|
* final bundle with some translations.
|
||||||
|
*
|
||||||
|
* If the language already exists, translations will be
|
||||||
|
* merged with the existing ones.
|
||||||
|
*
|
||||||
|
* Setting `translations` to `null`, fully deletes a language,
|
||||||
|
* its translations and its files.
|
||||||
|
*
|
||||||
|
* @see https://developer.apple.com/documentation/walletpasses/creating_the_source_for_a_pass#3736718
|
||||||
|
* @param lang
|
||||||
|
* @param translations
|
||||||
|
*/
|
||||||
|
|
||||||
|
public localize(
|
||||||
|
lang: string,
|
||||||
|
translations: { [key: string]: string } | null,
|
||||||
|
) {
|
||||||
|
Utils.assertUnfrozen(this);
|
||||||
|
|
||||||
|
if (typeof lang !== "string") {
|
||||||
|
throw new TypeError(
|
||||||
|
Messages.format(Messages.LANGUAGES.INVALID_LANG, typeof lang),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (translations === null) {
|
||||||
|
delete this[localizationSymbol][lang];
|
||||||
|
|
||||||
|
const allFilesKeys = Object.keys(this[filesSymbol]);
|
||||||
|
const langFolderIdentifier = `${lang}.lproj`;
|
||||||
|
|
||||||
|
for (
|
||||||
|
let i = allFilesKeys.length, filePath: string;
|
||||||
|
(filePath = allFilesKeys[--i]);
|
||||||
|
|
||||||
|
) {
|
||||||
|
if (filePath.startsWith(langFolderIdentifier)) {
|
||||||
|
delete this[filesSymbol][filePath];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!translations || !Object.keys(translations).length) {
|
||||||
|
console.warn(
|
||||||
|
Messages.format(Messages.LANGUAGES.NO_TRANSLATIONS, lang),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this[localizationSymbol][lang] ??= {};
|
||||||
|
|
||||||
|
if (typeof translations === "object" && !Array.isArray(translations)) {
|
||||||
|
Object.assign(this[localizationSymbol][lang], translations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to specify an expiration date for the pass.
|
||||||
|
*
|
||||||
|
* @param date
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
public setExpirationDate(date: Date | null) {
|
||||||
|
Utils.assertUnfrozen(this);
|
||||||
|
|
||||||
|
if (date === null) {
|
||||||
|
delete this[propsSymbol]["expirationDate"];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this[propsSymbol]["expirationDate"] = Utils.processDate(date);
|
||||||
|
} catch (err) {
|
||||||
|
throw new TypeError(
|
||||||
|
Messages.format(Messages.DATE.INVALID, "expirationDate", date),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows setting some beacons the OS should
|
||||||
|
* react to and show this pass.
|
||||||
|
*
|
||||||
|
* Pass `null` to remove them at all.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* PKPassInstance.setBeacons(null)
|
||||||
|
* PKPassInstance.setBeacons({
|
||||||
|
* proximityUUID: "00000-000000-0000-00000000000",
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @see https://developer.apple.com/documentation/walletpasses/pass/beacons
|
||||||
|
* @param beacons
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
public setBeacons(beacons: null): void;
|
||||||
|
public setBeacons(...beacons: Schemas.Beacon[]): void;
|
||||||
|
public setBeacons(...beacons: (Schemas.Beacon | null)[]) {
|
||||||
|
Utils.assertUnfrozen(this);
|
||||||
|
|
||||||
|
if (beacons[0] === null) {
|
||||||
|
delete this[propsSymbol]["beacons"];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this[propsSymbol]["beacons"] = Schemas.filterValid(
|
||||||
|
Schemas.Beacon,
|
||||||
|
beacons,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows setting some locations the OS should
|
||||||
|
* react to and show this pass.
|
||||||
|
*
|
||||||
|
* Pass `null` to remove them at all.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* PKPassInstance.setLocations(null)
|
||||||
|
* PKPassInstance.setLocations({
|
||||||
|
* latitude: 0.5333245342
|
||||||
|
* longitude: 0.2135332252
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @see https://developer.apple.com/documentation/walletpasses/pass/locations
|
||||||
|
* @param locations
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
public setLocations(locations: null): void;
|
||||||
|
public setLocations(...locations: Schemas.Location[]): void;
|
||||||
|
public setLocations(...locations: (Schemas.Location | null)[]): void {
|
||||||
|
Utils.assertUnfrozen(this);
|
||||||
|
|
||||||
|
if (locations[0] === null) {
|
||||||
|
delete this[propsSymbol]["locations"];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this[propsSymbol]["locations"] = Schemas.filterValid(
|
||||||
|
Schemas.Location,
|
||||||
|
locations,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows setting a relevant date in which the OS
|
||||||
|
* should show this pass.
|
||||||
|
*
|
||||||
|
* @param date
|
||||||
|
*/
|
||||||
|
|
||||||
|
public setRelevantDate(date: Date): void {
|
||||||
|
Utils.assertUnfrozen(this);
|
||||||
|
|
||||||
|
if (date === null) {
|
||||||
|
delete this[propsSymbol]["relevantDate"];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this[propsSymbol]["relevantDate"] = Utils.processDate(date);
|
||||||
|
} catch (err) {
|
||||||
|
throw new TypeError(
|
||||||
|
Messages.format(Messages.DATE.INVALID, "relevantDate", date),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to specify some barcodes formats.
|
||||||
|
* As per the current specifications, only the first
|
||||||
|
* will be shown to the user, without any possibility
|
||||||
|
* to change it.
|
||||||
|
*
|
||||||
|
* @see https://developer.apple.com/documentation/walletpasses/pass/barcodes
|
||||||
|
* @param barcodes
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
public setBarcodes(barcodes: null): void;
|
||||||
|
public setBarcodes(message: string): void;
|
||||||
|
public setBarcodes(...barcodes: Schemas.Barcode[]): void;
|
||||||
|
public setBarcodes(...barcodes: (Schemas.Barcode | string | null)[]): void {
|
||||||
|
Utils.assertUnfrozen(this);
|
||||||
|
|
||||||
|
if (!barcodes.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (barcodes[0] === null) {
|
||||||
|
delete this[propsSymbol]["barcodes"];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalBarcodes: Schemas.Barcode[];
|
||||||
|
|
||||||
|
if (typeof barcodes[0] === "string") {
|
||||||
|
/**
|
||||||
|
* A string has been received instead of objects. We can
|
||||||
|
* only auto-fill them all with the same data.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const supportedFormats: Array<Schemas.BarcodeFormat> = [
|
||||||
|
"PKBarcodeFormatQR",
|
||||||
|
"PKBarcodeFormatPDF417",
|
||||||
|
"PKBarcodeFormatAztec",
|
||||||
|
"PKBarcodeFormatCode128",
|
||||||
|
];
|
||||||
|
|
||||||
|
finalBarcodes = supportedFormats.map((format) =>
|
||||||
|
Schemas.validate(Schemas.Barcode, {
|
||||||
|
format,
|
||||||
|
message: barcodes[0],
|
||||||
|
} as Schemas.Barcode),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
finalBarcodes = Schemas.filterValid(
|
||||||
|
Schemas.Barcode,
|
||||||
|
barcodes as Schemas.Barcode[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this[propsSymbol]["barcodes"] = finalBarcodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to specify details to make this, an
|
||||||
|
* NFC-capable pass.
|
||||||
|
*
|
||||||
|
* Pass `null` as parameter to remove it at all.
|
||||||
|
*
|
||||||
|
* @see https://developer.apple.com/documentation/walletpasses/pass/nfc
|
||||||
|
* @param data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
public setNFC(nfc: Schemas.NFC | null): void {
|
||||||
|
Utils.assertUnfrozen(this);
|
||||||
|
|
||||||
|
if (nfc === null) {
|
||||||
|
delete this[propsSymbol]["nfc"];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this[propsSymbol]["nfc"] =
|
||||||
|
Schemas.validate(Schemas.NFC, nfc) ?? undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateJSONBuffer(
|
||||||
|
buffer: Buffer,
|
||||||
|
schema: Parameters<typeof Schemas.validate>[0],
|
||||||
|
) {
|
||||||
|
let contentAsJSON: unknown;
|
||||||
|
|
||||||
|
try {
|
||||||
|
contentAsJSON = JSON.parse(buffer.toString("utf8"));
|
||||||
|
} catch (err) {
|
||||||
|
throw new TypeError(Messages.JSON.INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Schemas.validate(schema, contentAsJSON);
|
||||||
|
}
|
||||||
@@ -1,27 +1,43 @@
|
|||||||
import forge from "node-forge";
|
import forge from "node-forge";
|
||||||
import type * as Schemas from "./schemas";
|
import type * as Schemas from "./schemas";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an hash for a buffer. Used by manifest
|
||||||
|
*
|
||||||
|
* @param buffer
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function createHash(buffer: Buffer) {
|
||||||
|
const hashFlow = forge.md.sha1.create();
|
||||||
|
hashFlow.update(buffer.toString("binary"));
|
||||||
|
|
||||||
|
return hashFlow.digest().toHex();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the PKCS #7 cryptografic signature for the manifest file.
|
* Generates the PKCS #7 cryptografic signature for the manifest file.
|
||||||
*
|
*
|
||||||
* @method create
|
* @method create
|
||||||
* @params {Object} manifest - Manifest content.
|
* @params manifest
|
||||||
* @returns {Buffer}
|
* @params certificates
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function create(
|
export function create(
|
||||||
manifest: Schemas.Manifest,
|
manifestBuffer: Buffer,
|
||||||
certificates: Schemas.CertificatesSchema,
|
certificates: Schemas.CertificatesSchema,
|
||||||
): Buffer {
|
): Buffer {
|
||||||
const signature = forge.pkcs7.createSignedData();
|
const signature = forge.pkcs7.createSignedData();
|
||||||
|
|
||||||
signature.content = forge.util.createBuffer(
|
signature.content = new forge.util.ByteStringBuffer(manifestBuffer);
|
||||||
JSON.stringify(manifest),
|
|
||||||
"utf8",
|
const { wwdr, signerCert, signerKey } = parseCertificates(
|
||||||
|
getStringCertificates(certificates),
|
||||||
);
|
);
|
||||||
|
|
||||||
signature.addCertificate(certificates.wwdr);
|
signature.addCertificate(wwdr);
|
||||||
signature.addCertificate(certificates.signerCert);
|
signature.addCertificate(signerCert);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* authenticatedAttributes belong to PKCS#9 standard.
|
* authenticatedAttributes belong to PKCS#9 standard.
|
||||||
@@ -33,8 +49,8 @@ export function create(
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
signature.addSigner({
|
signature.addSigner({
|
||||||
key: certificates.signerKey,
|
key: signerKey,
|
||||||
certificate: certificates.signerCert,
|
certificate: signerCert,
|
||||||
digestAlgorithm: forge.pki.oids.sha1,
|
digestAlgorithm: forge.pki.oids.sha1,
|
||||||
authenticatedAttributes: [
|
authenticatedAttributes: [
|
||||||
{
|
{
|
||||||
@@ -76,3 +92,35 @@ export function create(
|
|||||||
"binary",
|
"binary",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 parseCertificates(certificates: Schemas.CertificatesSchema) {
|
||||||
|
const { signerCert, signerKey, wwdr, signerKeyPassphrase } = certificates;
|
||||||
|
|
||||||
|
return {
|
||||||
|
signerCert: forge.pki.certificateFromPem(signerCert.toString("utf-8")),
|
||||||
|
wwdr: forge.pki.certificateFromPem(wwdr.toString("utf-8")),
|
||||||
|
signerKey: forge.pki.decryptRsaPrivateKey(
|
||||||
|
signerKey.toString("utf-8"),
|
||||||
|
signerKeyPassphrase,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStringCertificates(
|
||||||
|
certificates: Schemas.CertificatesSchema,
|
||||||
|
): Record<keyof Schemas.CertificatesSchema, string> {
|
||||||
|
return {
|
||||||
|
signerKeyPassphrase: certificates.signerKeyPassphrase,
|
||||||
|
wwdr: Buffer.from(certificates.wwdr).toString("utf-8"),
|
||||||
|
signerCert: Buffer.from(certificates.signerCert).toString("utf-8"),
|
||||||
|
signerKey: Buffer.from(certificates.signerKey).toString("utf-8"),
|
||||||
|
};
|
||||||
|
}
|
||||||
85
src/StringsUtils.ts
Normal file
85
src/StringsUtils.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { EOL } from "os";
|
||||||
|
|
||||||
|
// ************************************ //
|
||||||
|
// *** UTILS FOR PASS.STRINGS FILES *** //
|
||||||
|
// ************************************ //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a string file to convert it to
|
||||||
|
* an object
|
||||||
|
*
|
||||||
|
* @param buffer
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function parse(buffer: Buffer) {
|
||||||
|
const fileAsString = buffer.toString("utf8");
|
||||||
|
const translationRowRegex = /"(?<key>.+)"\s+=\s+"(?<value>.+)";\n?/;
|
||||||
|
const commentRowRegex = /\/\*\s*(.+)\s*\*\//;
|
||||||
|
|
||||||
|
let translations: [placeholder: string, value: string][] = [];
|
||||||
|
let comments: string[] = [];
|
||||||
|
|
||||||
|
let blockStartPoint = 0;
|
||||||
|
let blockEndPoint = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if (
|
||||||
|
/** New Line, new life */
|
||||||
|
/\n/.test(fileAsString[blockEndPoint]) ||
|
||||||
|
/** EOF */
|
||||||
|
blockEndPoint === fileAsString.length
|
||||||
|
) {
|
||||||
|
let match: RegExpMatchArray;
|
||||||
|
|
||||||
|
const section = fileAsString.substring(
|
||||||
|
blockStartPoint,
|
||||||
|
blockEndPoint + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ((match = section.match(translationRowRegex))) {
|
||||||
|
const {
|
||||||
|
groups: { key, value },
|
||||||
|
} = match;
|
||||||
|
|
||||||
|
translations.push([key, value]);
|
||||||
|
} else if ((match = section.match(commentRowRegex))) {
|
||||||
|
const [, content] = match;
|
||||||
|
|
||||||
|
comments.push(content.trimEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Skipping \n and going to the next block. */
|
||||||
|
blockEndPoint += 2;
|
||||||
|
blockStartPoint = blockEndPoint - 1;
|
||||||
|
} else {
|
||||||
|
blockEndPoint += 1;
|
||||||
|
}
|
||||||
|
} while (blockEndPoint <= fileAsString.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
translations,
|
||||||
|
comments,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a strings file buffer
|
||||||
|
*
|
||||||
|
* @param translations
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function create(translations: { [key: string]: string }): Buffer {
|
||||||
|
const stringContents = [];
|
||||||
|
|
||||||
|
const translationsEntries = Object.entries(translations);
|
||||||
|
|
||||||
|
for (let i = 0; i < translationsEntries.length; i++) {
|
||||||
|
const [key, value] = translationsEntries[i];
|
||||||
|
|
||||||
|
stringContents.push(`"${key}" = "${value}";`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.from(stringContents.join(EOL));
|
||||||
|
}
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import * as Schemas from "./schemas";
|
|
||||||
import { getModelContents, readCertificatesFromOptions } from "./parser";
|
|
||||||
import formatMessage, { ERROR } from "./messages";
|
|
||||||
|
|
||||||
const abmCertificates = Symbol("certificates");
|
|
||||||
const abmModel = Symbol("model");
|
|
||||||
const abmOverrides = Symbol("overrides");
|
|
||||||
|
|
||||||
export interface AbstractFactoryOptions
|
|
||||||
extends Omit<Schemas.FactoryOptions, "certificates"> {
|
|
||||||
certificates?: Schemas.Certificates;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AbstractModelOptions {
|
|
||||||
bundle: Schemas.PartitionedBundle;
|
|
||||||
certificates: Schemas.CertificatesSchema;
|
|
||||||
overrides?: Schemas.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(ERROR.CP_NO_OPTS));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [bundle, certificates] = await Promise.all([
|
|
||||||
getModelContents(options.model),
|
|
||||||
readCertificatesFromOptions(options.certificates),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return new AbstractModel({
|
|
||||||
bundle,
|
|
||||||
certificates,
|
|
||||||
overrides: options.overrides,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(formatMessage(ERROR.CP_INIT, "abstract model", err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AbstractModel {
|
|
||||||
private [abmCertificates]: Schemas.CertificatesSchema;
|
|
||||||
private [abmModel]: Schemas.PartitionedBundle;
|
|
||||||
private [abmOverrides]: Schemas.OverridesSupportedOptions;
|
|
||||||
|
|
||||||
constructor(options: AbstractModelOptions) {
|
|
||||||
this[abmModel] = options.bundle;
|
|
||||||
this[abmCertificates] = options.certificates;
|
|
||||||
this[abmOverrides] = options.overrides;
|
|
||||||
}
|
|
||||||
|
|
||||||
get certificates(): Schemas.CertificatesSchema {
|
|
||||||
return this[abmCertificates];
|
|
||||||
}
|
|
||||||
|
|
||||||
get bundle(): Schemas.PartitionedBundle {
|
|
||||||
return this[abmModel];
|
|
||||||
}
|
|
||||||
|
|
||||||
get overrides(): Schemas.OverridesSupportedOptions {
|
|
||||||
return this[abmOverrides];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import { Pass } from "./pass";
|
|
||||||
import * as Schemas from "./schemas";
|
|
||||||
import formatMessage, { ERROR } 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: Schemas.FactoryOptions | InstanceType<typeof AbstractModel>,
|
|
||||||
additionalBuffers?: Schemas.BundleUnit,
|
|
||||||
abstractMissingData?: Omit<AbstractFactoryOptions, "model">,
|
|
||||||
): Promise<Pass> {
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
options &&
|
|
||||||
(options instanceof AbstractModel || Object.keys(options).length)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new Error(formatMessage(ERROR.CP_NO_OPTS));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (options instanceof AbstractModel) {
|
|
||||||
let certificates: Schemas.CertificatesSchema;
|
|
||||||
let overrides: Schemas.OverridesSupportedOptions = {
|
|
||||||
...(options.overrides || {}),
|
|
||||||
...((abstractMissingData && abstractMissingData.overrides) ||
|
|
||||||
{}),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
options.certificates &&
|
|
||||||
options.certificates.signerCert &&
|
|
||||||
options.certificates.signerKey
|
|
||||||
) &&
|
|
||||||
abstractMissingData.certificates
|
|
||||||
) {
|
|
||||||
certificates = Object.assign(
|
|
||||||
options.certificates,
|
|
||||||
await readCertificatesFromOptions(
|
|
||||||
abstractMissingData.certificates,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
certificates = options.certificates;
|
|
||||||
}
|
|
||||||
|
|
||||||
return createPassInstance(
|
|
||||||
options.bundle,
|
|
||||||
certificates,
|
|
||||||
overrides,
|
|
||||||
additionalBuffers,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const [bundle, certificates] = await Promise.all([
|
|
||||||
getModelContents(options.model),
|
|
||||||
readCertificatesFromOptions(options.certificates),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return createPassInstance(
|
|
||||||
bundle,
|
|
||||||
certificates,
|
|
||||||
options.overrides,
|
|
||||||
additionalBuffers,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(formatMessage(ERROR.CP_INIT, "pass", err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPassInstance(
|
|
||||||
model: Schemas.PartitionedBundle,
|
|
||||||
certificates: Schemas.CertificatesSchema,
|
|
||||||
overrides: Schemas.OverridesSupportedOptions,
|
|
||||||
additionalBuffers?: Schemas.BundleUnit,
|
|
||||||
) {
|
|
||||||
if (additionalBuffers) {
|
|
||||||
const [additionalL10n, additionalBundle] =
|
|
||||||
splitBufferBundle(additionalBuffers);
|
|
||||||
Object.assign(model["l10nBundle"], additionalL10n);
|
|
||||||
Object.assign(model["bundle"], additionalBundle);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Pass({
|
|
||||||
model,
|
|
||||||
certificates,
|
|
||||||
overrides,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import * as Schemas from "./schemas";
|
|
||||||
import debug from "debug";
|
|
||||||
|
|
||||||
const fieldsDebug = debug("passkit:fields");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class to represent lower-level keys pass fields
|
|
||||||
* @see https://apple.co/2wkUBdh
|
|
||||||
*/
|
|
||||||
|
|
||||||
const poolSymbol = Symbol("pool");
|
|
||||||
|
|
||||||
export default class FieldsArray extends Array {
|
|
||||||
private [poolSymbol]: Set<string>;
|
|
||||||
|
|
||||||
constructor(pool: Set<string>, ...args: any[]) {
|
|
||||||
super(...args);
|
|
||||||
this[poolSymbol] = pool;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Like `Array.prototype.push` but will alter
|
|
||||||
* also uniqueKeys set.
|
|
||||||
*/
|
|
||||||
|
|
||||||
push(...fieldsData: Schemas.Field[]): number {
|
|
||||||
const validFields = fieldsData.reduce(
|
|
||||||
(acc: Schemas.Field[], current: Schemas.Field) => {
|
|
||||||
if (
|
|
||||||
!(typeof current === "object") ||
|
|
||||||
!Schemas.isValid(current, Schemas.Field)
|
|
||||||
) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this[poolSymbol].has(current.key)) {
|
|
||||||
fieldsDebug(
|
|
||||||
`Field with key "${current.key}" discarded: fields must be unique in pass scope.`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this[poolSymbol].add(current.key);
|
|
||||||
acc.push(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return Array.prototype.push.call(this, ...validFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Like `Array.prototype.pop`, but will alter
|
|
||||||
* also uniqueKeys set
|
|
||||||
*/
|
|
||||||
|
|
||||||
pop(): Schemas.Field {
|
|
||||||
const element: Schemas.Field = Array.prototype.pop.call(this);
|
|
||||||
this[poolSymbol].delete(element.key);
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Like `Array.prototype.splice` but will alter
|
|
||||||
* also uniqueKeys set
|
|
||||||
*/
|
|
||||||
|
|
||||||
splice(
|
|
||||||
start: number,
|
|
||||||
deleteCount: number,
|
|
||||||
...items: Schemas.Field[]
|
|
||||||
): Schemas.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(): number {
|
|
||||||
return this.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
132
src/getModelFolderContents.ts
Normal file
132
src/getModelFolderContents.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import * as path from "path";
|
||||||
|
import * as Utils from "./utils";
|
||||||
|
import * as Messages from "./messages";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the model folder contents
|
||||||
|
*
|
||||||
|
* @param model
|
||||||
|
* @returns A promise of an object containing all
|
||||||
|
* filePaths and the relative buffer
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default async function getModelFolderContents(
|
||||||
|
model: string,
|
||||||
|
): Promise<{ [filePath: string]: Buffer }> {
|
||||||
|
try {
|
||||||
|
const modelPath = `${model}${(!path.extname(model) && ".pass") || ""}`;
|
||||||
|
const modelFilesList = await fs.readdir(modelPath);
|
||||||
|
|
||||||
|
// No dot-starting files, manifest and signature
|
||||||
|
const filteredModelRecords = Utils.removeHidden(modelFilesList).filter(
|
||||||
|
(f) =>
|
||||||
|
!/(manifest|signature)/i.test(f) &&
|
||||||
|
/.+$/.test(path.parse(f).ext),
|
||||||
|
);
|
||||||
|
|
||||||
|
const modelRecords = (
|
||||||
|
await Promise.all(
|
||||||
|
/**
|
||||||
|
* Obtaining flattened array of buffer records
|
||||||
|
* containing file name and the buffer itself.
|
||||||
|
*
|
||||||
|
* This goes also to read every nested l10n
|
||||||
|
* subfolder.
|
||||||
|
*/
|
||||||
|
|
||||||
|
filteredModelRecords.map((fileOrDirectoryPath) => {
|
||||||
|
const fullPath = path.resolve(
|
||||||
|
modelPath,
|
||||||
|
fileOrDirectoryPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
return readFileOrDirectory(fullPath);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.flat(1)
|
||||||
|
.reduce((acc, current) => ({ ...acc, ...current }), {});
|
||||||
|
|
||||||
|
return modelRecords;
|
||||||
|
} catch (err) {
|
||||||
|
if (err?.code === "ENOENT") {
|
||||||
|
if (err.syscall === "open") {
|
||||||
|
// file opening failed
|
||||||
|
throw new Error(
|
||||||
|
Messages.format(
|
||||||
|
Messages.MODELS.FILE_NO_OPEN,
|
||||||
|
JSON.stringify(err),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (err.syscall === "scandir") {
|
||||||
|
// directory reading failed
|
||||||
|
throw new Error(
|
||||||
|
Messages.format(Messages.MODELS.DIR_NOT_FOUND, err.path),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads sequentially
|
||||||
|
* @param filePath
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function readFileOrDirectory(filePath: string) {
|
||||||
|
if ((await fs.lstat(filePath)).isDirectory()) {
|
||||||
|
return Promise.all(await readDirectory(filePath));
|
||||||
|
} else {
|
||||||
|
return fs
|
||||||
|
.readFile(filePath)
|
||||||
|
.then((content) => getObjectFromModelFile(filePath, content, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an object containing the parsed fileName
|
||||||
|
* from a path along with its content.
|
||||||
|
*
|
||||||
|
* @param filePath
|
||||||
|
* @param content
|
||||||
|
* @param depthFromEnd - used to preserve localization lproj content
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getObjectFromModelFile(
|
||||||
|
filePath: string,
|
||||||
|
content: Buffer,
|
||||||
|
depthFromEnd: number,
|
||||||
|
) {
|
||||||
|
const fileComponents = filePath.split(path.sep);
|
||||||
|
const fileName = fileComponents
|
||||||
|
.slice(fileComponents.length - depthFromEnd)
|
||||||
|
.join(path.sep);
|
||||||
|
|
||||||
|
return { [fileName]: content };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a directory and returns all the files in it
|
||||||
|
* as an Array<Promise>
|
||||||
|
*
|
||||||
|
* @param filePath
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function readDirectory(filePath: string) {
|
||||||
|
const dirContent = await fs.readdir(filePath).then(Utils.removeHidden);
|
||||||
|
|
||||||
|
return dirContent.map(async (fileName) => {
|
||||||
|
const content = await fs.readFile(path.resolve(filePath, fileName));
|
||||||
|
return getObjectFromModelFile(
|
||||||
|
path.resolve(filePath, fileName),
|
||||||
|
content,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
21
src/index.ts
21
src/index.ts
@@ -1,5 +1,18 @@
|
|||||||
export type { Pass } from "./pass";
|
export { default as PKPass } from "./PKPass";
|
||||||
export type { AbstractModel } from "./abstract";
|
|
||||||
|
|
||||||
export { createPass } from "./factory";
|
// ***************************************** //
|
||||||
export { createAbstractModel } from "./abstract";
|
// *** Exporting only schemas interfaces *** //
|
||||||
|
// ***************************************** //
|
||||||
|
|
||||||
|
export type {
|
||||||
|
Barcode,
|
||||||
|
Beacon,
|
||||||
|
Field,
|
||||||
|
Location,
|
||||||
|
NFC,
|
||||||
|
PassProps,
|
||||||
|
Semantics,
|
||||||
|
TransitType,
|
||||||
|
Personalize,
|
||||||
|
OverridablePassProps,
|
||||||
|
} from "./schemas";
|
||||||
|
|||||||
150
src/messages.ts
150
src/messages.ts
@@ -1,71 +1,101 @@
|
|||||||
export const ERROR = {
|
export const CERTIFICATES = {
|
||||||
CP_INIT:
|
INVALID:
|
||||||
"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",
|
"Invalid certificate(s) loaded. %s. Please provide valid WWDR certificates and developer signer certificate and key (with passphrase).\nRefer to docs to obtain them",
|
||||||
CP_NO_OPTS:
|
|
||||||
"Cannot initialize the pass or abstract model creation: no options were passed.",
|
|
||||||
CP_NO_CERTS:
|
|
||||||
"Cannot initialize the pass creation: no valid certificates were passed.",
|
|
||||||
PASSFILE_VALIDATION_FAILED:
|
|
||||||
"Validation of pass type failed. Pass file is not a valid buffer or (more probably) does not respect the schema.\nRefer to https://apple.co/2Nvshvn to build a correct pass.",
|
|
||||||
REQUIR_VALID_FAILED:
|
|
||||||
"The options passed to Pass constructor does not meet the requirements.\nRefer to the documentation to compile them correctly.",
|
|
||||||
MODEL_UNINITIALIZED:
|
|
||||||
"Provided model ( %s ) matched but unitialized or may not contain icon or a valid pass.json.\nRefer to https://apple.co/2IhJr0Q, https://apple.co/2Nvshvn and documentation to fill the model correctly.",
|
|
||||||
MODEL_NOT_VALID:
|
|
||||||
"A model must be provided in form of path (string) or object { 'fileName': Buffer } in order to continue.",
|
|
||||||
MODELF_NOT_FOUND: "Model %s not found. Provide a valid one to continue.",
|
|
||||||
MODELF_FILE_NOT_FOUND: "File %s not found.",
|
|
||||||
INVALID_CERTS:
|
|
||||||
"Invalid certificate(s) loaded: %s. Please provide valid WWDR certificates and developer signer certificate and key (with passphrase).\nRefer to docs to obtain them.",
|
|
||||||
INVALID_CERT_PATH: "Invalid certificate loaded. %s does not exist.",
|
|
||||||
TRSTYPE_REQUIRED:
|
|
||||||
"Cannot proceed with pass creation. transitType field is required for boardingPasses.",
|
|
||||||
OVV_KEYS_BADFORMAT:
|
|
||||||
"Cannot proceed with pass creation due to bad keys format in overrides.",
|
|
||||||
NO_PASS_TYPE:
|
|
||||||
"Cannot proceed with pass creation. Model definition (pass.json) has no valid type in it.\nRefer to https://apple.co/2wzyL5J to choose a valid pass type.",
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const DEBUG = {
|
export const TRANSIT_TYPE = {
|
||||||
TRSTYPE_NOT_VALID:
|
UNEXPECTED_PASS_TYPE:
|
||||||
'Transit type changing rejected as not compliant with Apple Specifications. Transit type would become "%s" but should be in [PKTransitTypeAir, PKTransitTypeBoat, PKTransitTypeBus, PKTransitTypeGeneric, PKTransitTypeTrain]',
|
"Cannot set transitType on a pass with type different from boardingPass.",
|
||||||
BRC_NOT_SUPPORTED:
|
INVALID:
|
||||||
"Format not found among barcodes. Cannot set backward compatibility.",
|
"Cannot set transitType because not compliant with Apple specifications. Refer to https://apple.co/3DHuAG4 for more - %s",
|
||||||
BRC_FORMATTYPE_UNMATCH:
|
|
||||||
"Format must be a string or null. Cannot set backward compatibility.",
|
|
||||||
BRC_AUTC_MISSING_DATA:
|
|
||||||
"Unable to autogenerate barcodes. Data is not a string.",
|
|
||||||
BRC_BW_FORMAT_UNSUPPORTED:
|
|
||||||
"This format is not supported (by Apple) for backward support. Please choose another one.",
|
|
||||||
BRC_NO_POOL:
|
|
||||||
"Cannot set barcode: no barcodes found. Please set barcodes first. Barcode is for retrocompatibility only.",
|
|
||||||
DATE_FORMAT_UNMATCH: "%s was not set due to incorrect date format.",
|
|
||||||
NFC_INVALID:
|
|
||||||
"Unable to set NFC properties: data not compliant with schema.",
|
|
||||||
PRS_INVALID:
|
|
||||||
"Unable to parse Personalization.json. File is not a valid JSON. Error: %s",
|
|
||||||
PRS_REMOVED:
|
|
||||||
"Personalization has been removed as it requires an NFC-enabled pass to work.",
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type ERROR_OR_DEBUG_MESSAGE =
|
export const PASS_TYPE = {
|
||||||
| typeof ERROR[keyof typeof ERROR]
|
INVALID:
|
||||||
| typeof DEBUG[keyof typeof DEBUG];
|
"Cannot set type because not compliant with Apple specifications. Refer to https://apple.co/3aFpSfg for a list of valid props - %s",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const TEMPLATE = {
|
||||||
|
INVALID: "Cannot create pass from a template. %s",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const FILTER_VALID = {
|
||||||
|
INVALID: "Cannot validate property. %s",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const FIELDS = {
|
||||||
|
INVALID: "Cannot add field. %s",
|
||||||
|
REPEATED_KEY:
|
||||||
|
"Cannot add field with key '%s': another field already owns this key. Ignored.",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const DATE = {
|
||||||
|
INVALID: "Cannot set %s. Invalid date %s",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const LANGUAGES = {
|
||||||
|
INVALID_LANG:
|
||||||
|
"Cannot set localization. Expected a string for 'lang' but received %s",
|
||||||
|
NO_TRANSLATIONS:
|
||||||
|
"Cannot create or use language %s. If your itention was to just add a language (.lproj) folder to the bundle, both specify some translations or use .addBuffer to add some media.",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const BARCODES = {
|
||||||
|
INVALID_POST: "",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const PASS_SOURCE = {
|
||||||
|
INVALID: "Cannot add pass.json to bundle because it is invalid. %s",
|
||||||
|
UNKNOWN_TYPE:
|
||||||
|
"Cannot find a valid type in pass.json. You won't be able to set fields until you won't set explicitly one.",
|
||||||
|
JOIN: "The imported pass.json's properties will be joined with the current setted props. You might lose some data.",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const PERSONALIZE = {
|
||||||
|
INVALID:
|
||||||
|
"Cannot add personalization.json to bundle because it is invalid. %s",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const JSON = {
|
||||||
|
INVALID: "Cannot parse JSON. Invalid file",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const CLOSE = {
|
||||||
|
MISSING_TYPE: "Cannot proceed creating the pass because type is missing.",
|
||||||
|
MISSING_ICON:
|
||||||
|
"At least one icon file is missing in your bundle. Your pass won't be openable by any Apple Device.",
|
||||||
|
PERSONALIZATION_REMOVED:
|
||||||
|
"Personalization file '%s' have been removed from the bundle as the requirements for personalization are not met.",
|
||||||
|
MISSING_TRANSIT_TYPE:
|
||||||
|
"Cannot proceed creating the pass because transitType is missing on your boardingPass.",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const MODELS = {
|
||||||
|
DIR_NOT_FOUND: "Cannot import model: directory %s not found.",
|
||||||
|
FILE_NO_OPEN: "Cannot open model file. %s",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const BUNDLE = {
|
||||||
|
MIME_TYPE_MISSING: "Cannot build Bundle. MimeType is missing",
|
||||||
|
CLOSED: "Cannot add file or set property. Bundle is closed.",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const FROM = {
|
||||||
|
MISSING_SOURCE: "Cannot create PKPass from source: source is '%s'",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const PACK = {
|
||||||
|
INVALID: "Cannot pack passes. Only PKPass instances allowed",
|
||||||
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a message with replaced values
|
* Creates a message with replaced values
|
||||||
* @param {string} messageName
|
* @param messageName
|
||||||
* @param {any[]} values
|
* @param values
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default function format(
|
export function format(messageName: string, ...values: any[]) {
|
||||||
messageName: ERROR_OR_DEBUG_MESSAGE,
|
|
||||||
...values: any[]
|
|
||||||
) {
|
|
||||||
// reversing because it is better popping than shifting.
|
// reversing because it is better popping than shifting.
|
||||||
let replaceValues = values.reverse();
|
const replaceValues = values.reverse();
|
||||||
return messageName.replace(/%s/g, () => {
|
return messageName.replace(/%s/g, () => replaceValues.pop());
|
||||||
let next = replaceValues.pop();
|
|
||||||
return next !== undefined ? next : "<passedValueIsUndefined>";
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
364
src/parser.ts
364
src/parser.ts
@@ -1,364 +0,0 @@
|
|||||||
import * as path from "path";
|
|
||||||
import forge from "node-forge";
|
|
||||||
import formatMessage, { ERROR, DEBUG } from "./messages";
|
|
||||||
import * as Schemas from "./schemas";
|
|
||||||
import {
|
|
||||||
removeHidden,
|
|
||||||
splitBufferBundle,
|
|
||||||
getAllFilesWithName,
|
|
||||||
hasFilesWithName,
|
|
||||||
deletePersonalization,
|
|
||||||
} from "./utils";
|
|
||||||
import fs from "fs";
|
|
||||||
import debug from "debug";
|
|
||||||
|
|
||||||
const prsDebug = debug("Personalization");
|
|
||||||
const { readdir: readDir, readFile } = fs.promises;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs checks on the passed model to
|
|
||||||
* determine how to parse it
|
|
||||||
* @param model
|
|
||||||
*/
|
|
||||||
|
|
||||||
export async function getModelContents(model: Schemas.FactoryOptions["model"]) {
|
|
||||||
let modelContents: Schemas.PartitionedBundle;
|
|
||||||
|
|
||||||
if (typeof model === "string") {
|
|
||||||
modelContents = await getModelFolderContents(model);
|
|
||||||
} else if (typeof model === "object" && Object.keys(model).length) {
|
|
||||||
modelContents = getModelBufferContents(model);
|
|
||||||
} else {
|
|
||||||
throw new Error(formatMessage(ERROR.MODEL_NOT_VALID));
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelFiles = Object.keys(modelContents.bundle);
|
|
||||||
const isModelInitialized =
|
|
||||||
modelFiles.includes("pass.json") &&
|
|
||||||
hasFilesWithName("icon", modelFiles, "startsWith");
|
|
||||||
|
|
||||||
if (!isModelInitialized) {
|
|
||||||
throw new Error(
|
|
||||||
formatMessage(ERROR.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 = Schemas.isValid(
|
|
||||||
parsedPersonalization,
|
|
||||||
Schemas.Personalization,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isPersonalizationValid) {
|
|
||||||
[...logoFullNames, personalizationJsonFile].forEach(
|
|
||||||
(file) => delete modelContents.bundle[file],
|
|
||||||
);
|
|
||||||
|
|
||||||
return modelContents;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
prsDebug(formatMessage(DEBUG.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<Schemas.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(
|
|
||||||
ERROR.MODEL_UNINITIALIZED,
|
|
||||||
path.parse(model).name,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Splitting files from localization folders
|
|
||||||
const rawBundleFiles = filteredFiles.filter(
|
|
||||||
(entry) => !entry.includes(".lproj"),
|
|
||||||
);
|
|
||||||
const l10nFolders = filteredFiles.filter((entry) =>
|
|
||||||
entry.includes(".lproj"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const rawBundleBuffers = await Promise.all(
|
|
||||||
rawBundleFiles.map((file) =>
|
|
||||||
readFile(path.resolve(modelPath, file)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const bundle: Schemas.BundleUnit = Object.assign(
|
|
||||||
{},
|
|
||||||
...rawBundleFiles.map((fileName, index) => ({
|
|
||||||
[fileName]: rawBundleBuffers[index],
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reading concurrently localizations folder
|
|
||||||
// and their files and their buffers
|
|
||||||
const L10N_FilesListByFolder: Array<Schemas.BundleUnit> =
|
|
||||||
await Promise.all(
|
|
||||||
l10nFolders.map(async (folderPath) => {
|
|
||||||
// Reading current folder
|
|
||||||
const currentLangPath = path.join(modelPath, folderPath);
|
|
||||||
|
|
||||||
const files = await readDir(currentLangPath);
|
|
||||||
// Transforming files path to a model-relative path
|
|
||||||
const validFiles = removeHidden(files).map((file) =>
|
|
||||||
path.join(currentLangPath, file),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Getting all the buffers from file paths
|
|
||||||
const buffers = await Promise.all(
|
|
||||||
validFiles.map((file) =>
|
|
||||||
readFile(file).catch(() => Buffer.alloc(0)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assigning each file path to its buffer
|
|
||||||
// and discarding the empty ones
|
|
||||||
|
|
||||||
return validFiles.reduce<Schemas.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: Schemas.PartitionedBundle["l10nBundle"] =
|
|
||||||
Object.assign(
|
|
||||||
{},
|
|
||||||
...L10N_FilesListByFolder.map((folder, index) => ({
|
|
||||||
[l10nFolders[index]]: folder,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
bundle,
|
|
||||||
l10nBundle,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
if (err?.code === "ENOENT") {
|
|
||||||
if (err.syscall === "open") {
|
|
||||||
// file opening failed
|
|
||||||
throw new Error(
|
|
||||||
formatMessage(ERROR.MODELF_NOT_FOUND, err.path),
|
|
||||||
);
|
|
||||||
} else if (err.syscall === "scandir") {
|
|
||||||
// directory reading failed
|
|
||||||
const pathContents = (err.path as string).split(/(\/|\\\?)/);
|
|
||||||
throw new Error(
|
|
||||||
formatMessage(
|
|
||||||
ERROR.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: Schemas.BundleUnit,
|
|
||||||
): Schemas.PartitionedBundle {
|
|
||||||
const rawBundle = removeHidden(
|
|
||||||
Object.keys(model),
|
|
||||||
).reduce<Schemas.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(ERROR.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
|
|
||||||
*/
|
|
||||||
|
|
||||||
type flatCertificates = Omit<Schemas.Certificates, "signerKey"> & {
|
|
||||||
signerKey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function readCertificatesFromOptions(
|
|
||||||
options: Schemas.Certificates,
|
|
||||||
): Promise<Schemas.CertificatesSchema> {
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
options &&
|
|
||||||
Object.keys(options).length &&
|
|
||||||
Schemas.isValid(options, Schemas.CertificatesSchema)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new Error(formatMessage(ERROR.CP_NO_CERTS));
|
|
||||||
}
|
|
||||||
|
|
||||||
let signerKey: string;
|
|
||||||
|
|
||||||
if (typeof options.signerKey === "object") {
|
|
||||||
signerKey = options.signerKey?.keyFile;
|
|
||||||
} else {
|
|
||||||
signerKey = options.signerKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the signerKey is an object, we want to get
|
|
||||||
// all the real contents and don't care of passphrase
|
|
||||||
const flattenedDocs = Object.assign({}, options, {
|
|
||||||
signerKey,
|
|
||||||
}) as flatCertificates;
|
|
||||||
|
|
||||||
// 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 passphrase =
|
|
||||||
(typeof options.signerKey === "object" &&
|
|
||||||
options.signerKey?.passphrase) ||
|
|
||||||
undefined;
|
|
||||||
|
|
||||||
const pem = parsePEM(certName, file, passphrase);
|
|
||||||
|
|
||||||
if (!pem) {
|
|
||||||
throw new Error(formatMessage(ERROR.INVALID_CERTS, certName));
|
|
||||||
}
|
|
||||||
|
|
||||||
return { [certName]: pem };
|
|
||||||
});
|
|
||||||
|
|
||||||
return Object.assign({}, ...pemParsedContents);
|
|
||||||
} catch (err) {
|
|
||||||
if (!err.path) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
formatMessage(ERROR.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
704
src/pass.ts
704
src/pass.ts
@@ -1,704 +0,0 @@
|
|||||||
import path from "path";
|
|
||||||
import forge from "node-forge";
|
|
||||||
import debug from "debug";
|
|
||||||
import { Stream } from "stream";
|
|
||||||
import { ZipFile } from "yazl";
|
|
||||||
import type Joi from "joi";
|
|
||||||
|
|
||||||
import * as Schemas from "./schemas";
|
|
||||||
import formatMessage, { ERROR, DEBUG } from "./messages";
|
|
||||||
import FieldsArray from "./fieldsArray";
|
|
||||||
import {
|
|
||||||
generateStringFile,
|
|
||||||
dateToW3CString,
|
|
||||||
isValidRGB,
|
|
||||||
deletePersonalization,
|
|
||||||
getAllFilesWithName,
|
|
||||||
} from "./utils";
|
|
||||||
import * as Signature from "./signature";
|
|
||||||
|
|
||||||
const barcodeDebug = debug("passkit:barcode");
|
|
||||||
const genericDebug = debug("passkit:generic");
|
|
||||||
|
|
||||||
const transitType = Symbol("transitType");
|
|
||||||
const passProps = Symbol("_props");
|
|
||||||
|
|
||||||
const propsSchemaMap = new Map<string, Joi.ObjectSchema<any>>([
|
|
||||||
["barcodes", Schemas.Barcode],
|
|
||||||
["barcode", Schemas.Barcode],
|
|
||||||
["beacons", Schemas.Beacon],
|
|
||||||
["locations", Schemas.Location],
|
|
||||||
["nfc", Schemas.NFC],
|
|
||||||
]);
|
|
||||||
|
|
||||||
export class Pass {
|
|
||||||
private bundle: Schemas.BundleUnit;
|
|
||||||
private l10nBundles: Schemas.PartitionedBundle["l10nBundle"];
|
|
||||||
private _fields: (keyof Schemas.PassFields)[] = [
|
|
||||||
"primaryFields",
|
|
||||||
"secondaryFields",
|
|
||||||
"auxiliaryFields",
|
|
||||||
"backFields",
|
|
||||||
"headerFields",
|
|
||||||
];
|
|
||||||
private [passProps]: Schemas.ValidPass = {};
|
|
||||||
private type: keyof Schemas.ValidPassType;
|
|
||||||
private fieldsKeys: Set<string> = new Set<string>();
|
|
||||||
private passCore: Schemas.ValidPass;
|
|
||||||
|
|
||||||
public headerFields: FieldsArray;
|
|
||||||
public primaryFields: FieldsArray;
|
|
||||||
public secondaryFields: FieldsArray;
|
|
||||||
public auxiliaryFields: FieldsArray;
|
|
||||||
public backFields: FieldsArray;
|
|
||||||
|
|
||||||
private Certificates: Schemas.CertificatesSchema;
|
|
||||||
private [transitType]: string = "";
|
|
||||||
private l10nTranslations: {
|
|
||||||
[languageCode: string]: { [placeholder: string]: string };
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
constructor(options: Schemas.PassInstance) {
|
|
||||||
if (!Schemas.isValid(options, Schemas.PassInstance)) {
|
|
||||||
throw new Error(formatMessage(ERROR.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(ERROR.PASSFILE_VALIDATION_FAILED));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parsing the options and extracting only the valid ones.
|
|
||||||
const validOverrides = Schemas.getValidated(
|
|
||||||
options.overrides || {},
|
|
||||||
Schemas.OverridesSupportedOptions,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (validOverrides === null) {
|
|
||||||
throw new Error(formatMessage(ERROR.OVV_KEYS_BADFORMAT));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.type = Object.keys(this.passCore).find((key) =>
|
|
||||||
/(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key),
|
|
||||||
) as keyof Schemas.ValidPassType;
|
|
||||||
|
|
||||||
if (!this.type) {
|
|
||||||
throw new Error(formatMessage(ERROR.NO_PASS_TYPE));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parsing and validating pass.json keys
|
|
||||||
const passCoreKeys = Object.keys(
|
|
||||||
this.passCore,
|
|
||||||
) as (keyof Schemas.ValidPass)[];
|
|
||||||
|
|
||||||
const validatedPassKeys = passCoreKeys.reduce<Schemas.ValidPass>(
|
|
||||||
(acc, current) => {
|
|
||||||
if (this.type === current) {
|
|
||||||
// We want to exclude type keys (eventTicket,
|
|
||||||
// boardingPass, ecc.) and their content
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!propsSchemaMap.has(current)) {
|
|
||||||
// If the property is unknown (we don't care if
|
|
||||||
// it is valid or not for Wallet), we return
|
|
||||||
// directly the content
|
|
||||||
return { ...acc, [current]: this.passCore[current] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentSchema = propsSchemaMap.get(current)!;
|
|
||||||
|
|
||||||
if (Array.isArray(this.passCore[current])) {
|
|
||||||
const valid = getValidInArray<Schemas.ArrayPassSchema>(
|
|
||||||
currentSchema,
|
|
||||||
this.passCore[current] as Schemas.ArrayPassSchema[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[current]: valid,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[current]:
|
|
||||||
(Schemas.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.forEach((fieldName) => {
|
|
||||||
this[fieldName] = new FieldsArray(
|
|
||||||
this.fieldsKeys,
|
|
||||||
...(this.passCore[this.type][fieldName] || []).filter((field) =>
|
|
||||||
Schemas.isValid(field, Schemas.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(DEBUG.PRS_REMOVED));
|
|
||||||
deletePersonalization(
|
|
||||||
this.bundle,
|
|
||||||
getAllFilesWithName(
|
|
||||||
"personalizationLogo",
|
|
||||||
currentBundleFiles,
|
|
||||||
"startsWith",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalBundle: Schemas.BundleUnit = { ...this.bundle };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterating through languages and generating pass.string file
|
|
||||||
*/
|
|
||||||
|
|
||||||
const translationsLanguageCodes = Object.keys(this.l10nTranslations);
|
|
||||||
|
|
||||||
for (
|
|
||||||
let langs = translationsLanguageCodes.length, lang: string;
|
|
||||||
(lang = translationsLanguageCodes[--langs]);
|
|
||||||
|
|
||||||
) {
|
|
||||||
const strings = generateStringFile(this.l10nTranslations[lang]);
|
|
||||||
const languageBundleDirname = `${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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const languageBundleUnit = (this.l10nBundles[
|
|
||||||
languageBundleDirname
|
|
||||||
] ??= {});
|
|
||||||
|
|
||||||
languageBundleUnit["pass.strings"] = Buffer.concat([
|
|
||||||
languageBundleUnit["pass.strings"] || Buffer.alloc(0),
|
|
||||||
strings,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!this.l10nBundles[languageBundleDirname] ||
|
|
||||||
!Object.keys(this.l10nBundles[languageBundleDirname]).length
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const bundleRelativeL10NPaths = Object.entries(
|
|
||||||
this.l10nBundles[languageBundleDirname],
|
|
||||||
).reduce((acc, [fileName, fileContent]) => {
|
|
||||||
const fullPath = path
|
|
||||||
.join(languageBundleDirname, fileName)
|
|
||||||
.replace(/\\/, "/");
|
|
||||||
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[fullPath]: fileContent,
|
|
||||||
};
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
Object.assign(finalBundle, bundleRelativeL10NPaths);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Parsing the buffers, pushing them into the archive
|
|
||||||
* and returning the compiled manifest
|
|
||||||
*/
|
|
||||||
const archive = new ZipFile();
|
|
||||||
const manifest = Object.entries(finalBundle).reduce<Schemas.Manifest>(
|
|
||||||
(acc, [fileName, buffer]) => {
|
|
||||||
let hashFlow = forge.md.sha1.create();
|
|
||||||
|
|
||||||
hashFlow.update(buffer.toString("binary"));
|
|
||||||
archive.addBuffer(buffer, fileName);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[fileName]: hashFlow.digest().toHex(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
const signatureBuffer = Signature.create(manifest, this.Certificates);
|
|
||||||
|
|
||||||
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?: { [placeholder: 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 varargs with type schema.Beacon, or single arg null
|
|
||||||
* @returns {Pass}
|
|
||||||
*/
|
|
||||||
|
|
||||||
beacons(resetFlag: null): this;
|
|
||||||
beacons(...data: Schemas.Beacon[]): this;
|
|
||||||
beacons(...data: (Schemas.Beacon | null)[]): this {
|
|
||||||
if (data[0] === null) {
|
|
||||||
delete this[passProps]["beacons"];
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
const valid = getValidInArray(Schemas.Beacon, data);
|
|
||||||
|
|
||||||
if (valid.length) {
|
|
||||||
this[passProps]["beacons"] = valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets current pass' relevancy through locations
|
|
||||||
* @param data varargs with type schema.Location, or single arg null
|
|
||||||
* @returns {Pass}
|
|
||||||
*/
|
|
||||||
|
|
||||||
locations(resetFlag: null): this;
|
|
||||||
locations(...data: Schemas.Location[]): this;
|
|
||||||
locations(...data: (Schemas.Location | null)[]): this {
|
|
||||||
if (data[0] === null) {
|
|
||||||
delete this[passProps]["locations"];
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
const valid = getValidInArray(Schemas.Location, 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(resetFlag: null): this;
|
|
||||||
barcodes(message: string): this;
|
|
||||||
barcodes(...data: Schemas.Barcode[]): this;
|
|
||||||
barcodes(...data: (Schemas.Barcode | null | string)[]): this {
|
|
||||||
if (data[0] === null) {
|
|
||||||
delete this[passProps]["barcodes"];
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof data[0] === "string") {
|
|
||||||
const autogen = barcodesFromUncompleteData(data[0]);
|
|
||||||
|
|
||||||
if (!autogen.length) {
|
|
||||||
barcodeDebug(formatMessage(DEBUG.BRC_AUTC_MISSING_DATA));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
this[passProps]["barcodes"] = autogen;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
} else {
|
|
||||||
/**
|
|
||||||
* 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 validBarcodes = data.reduce<Schemas.Barcode[]>(
|
|
||||||
(acc, current) => {
|
|
||||||
if (!(current && current instanceof Object)) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validated = Schemas.getValidated(
|
|
||||||
current,
|
|
||||||
Schemas.Barcode,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
validated &&
|
|
||||||
validated instanceof Object &&
|
|
||||||
Object.keys(validated).length
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...acc, validated];
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (validBarcodes.length) {
|
|
||||||
this[passProps]["barcodes"] = validBarcodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
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: Schemas.BarcodeFormat | null): this {
|
|
||||||
const { barcodes } = this[passProps];
|
|
||||||
|
|
||||||
if (chosenFormat === null) {
|
|
||||||
delete this[passProps]["barcode"];
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof chosenFormat !== "string") {
|
|
||||||
barcodeDebug(formatMessage(DEBUG.BRC_FORMATTYPE_UNMATCH));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chosenFormat === "PKBarcodeFormatCode128") {
|
|
||||||
barcodeDebug(formatMessage(DEBUG.BRC_BW_FORMAT_UNSUPPORTED));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(barcodes && barcodes.length)) {
|
|
||||||
barcodeDebug(formatMessage(DEBUG.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(DEBUG.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: Schemas.NFC | null): this {
|
|
||||||
if (data === null) {
|
|
||||||
delete this[passProps]["nfc"];
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
data &&
|
|
||||||
typeof data === "object" &&
|
|
||||||
!Array.isArray(data) &&
|
|
||||||
Schemas.isValid(data, Schemas.NFC)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
genericDebug(formatMessage(DEBUG.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<Schemas.ValidPass> {
|
|
||||||
return this[passProps];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
const passFile = JSON.parse(
|
|
||||||
passCoreBuffer.toString(),
|
|
||||||
) as Schemas.ValidPass;
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const passColors = [
|
|
||||||
"backgroundColor",
|
|
||||||
"foregroundColor",
|
|
||||||
"labelColor",
|
|
||||||
] as Array<keyof Schemas.PassColors>;
|
|
||||||
|
|
||||||
passColors
|
|
||||||
.filter(
|
|
||||||
(v) =>
|
|
||||||
this[passProps][v] && !isValidRGB(this[passProps][v]),
|
|
||||||
)
|
|
||||||
.forEach((v) => delete this[passProps][v]);
|
|
||||||
|
|
||||||
Object.assign(passFile, this[passProps]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._fields.forEach((field) => {
|
|
||||||
passFile[this.type][field] = this[field];
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.type === "boardingPass" && !this[transitType]) {
|
|
||||||
throw new Error(formatMessage(ERROR.TRSTYPE_REQUIRED));
|
|
||||||
}
|
|
||||||
|
|
||||||
passFile[this.type]["transitType"] = this[transitType];
|
|
||||||
|
|
||||||
return Buffer.from(JSON.stringify(passFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
set transitType(value: string) {
|
|
||||||
if (!Schemas.isValid(value, Schemas.TransitType)) {
|
|
||||||
genericDebug(formatMessage(DEBUG.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): Schemas.Barcode[] {
|
|
||||||
if (!(message && typeof message === "string")) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
[
|
|
||||||
"PKBarcodeFormatQR",
|
|
||||||
"PKBarcodeFormatPDF417",
|
|
||||||
"PKBarcodeFormatAztec",
|
|
||||||
"PKBarcodeFormatCode128",
|
|
||||||
] as Array<Schemas.BarcodeFormat>
|
|
||||||
).map((format) =>
|
|
||||||
Schemas.getValidated({ format, message }, Schemas.Barcode),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getValidInArray<T>(
|
|
||||||
schemaName: Joi.ObjectSchema<T>,
|
|
||||||
contents: T[],
|
|
||||||
): T[] {
|
|
||||||
return contents.filter(
|
|
||||||
(current) =>
|
|
||||||
Object.keys(current).length && Schemas.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(DEBUG.DATE_FORMAT_UNMATCH, key));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dateParse;
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ import Joi from "joi";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://developer.apple.com/documentation/walletpasses/pass/barcodes
|
* @see https://developer.apple.com/documentation/walletpasses/pass/barcodes
|
||||||
* @TODO Rename "Barcode" in "Barcodes". It will be done in v3.0
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type BarcodeFormat =
|
export type BarcodeFormat =
|
||||||
@@ -2,7 +2,6 @@ import Joi from "joi";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://developer.apple.com/documentation/walletpasses/pass/beacons
|
* @see https://developer.apple.com/documentation/walletpasses/pass/beacons
|
||||||
* @TODO Rename "Beacon" in "Beacons". This will be done in v3.0
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface Beacon {
|
export interface Beacon {
|
||||||
18
src/schemas/Certificates.ts
Normal file
18
src/schemas/Certificates.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type forge from "node-forge";
|
||||||
|
import Joi from "joi";
|
||||||
|
|
||||||
|
export interface CertificatesSchema {
|
||||||
|
wwdr: string | Buffer;
|
||||||
|
signerCert: string | Buffer;
|
||||||
|
signerKey: string | Buffer;
|
||||||
|
signerKeyPassphrase?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CertificatesSchema = Joi.object<CertificatesSchema>()
|
||||||
|
.keys({
|
||||||
|
wwdr: Joi.alternatives(Joi.binary(), Joi.string()).required(),
|
||||||
|
signerCert: Joi.alternatives(Joi.binary(), Joi.string()).required(),
|
||||||
|
signerKey: Joi.alternatives(Joi.binary(), Joi.string()).required(),
|
||||||
|
signerKeyPassphrase: Joi.string(),
|
||||||
|
})
|
||||||
|
.required();
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
import { Semantics } from "./SemanticTags";
|
import { Semantics } from "./Semantics";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://developer.apple.com/documentation/walletpasses/passfieldcontent
|
* @see https://developer.apple.com/documentation/walletpasses/passfieldcontent
|
||||||
* @TODO Rename interface to PassFieldContent to conform to above. This will be done in v3.0
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface Field {
|
export interface Field {
|
||||||
@@ -2,7 +2,6 @@ import Joi from "joi";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://developer.apple.com/documentation/walletpasses/pass/locations
|
* @see https://developer.apple.com/documentation/walletpasses/pass/locations
|
||||||
* @TODO Rename "Location" in "Locations". This will be done in v3.0
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface Location {
|
export interface Location {
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
import { Field } from "./PassFieldContent";
|
import { Field } from "./Field";
|
||||||
|
|
||||||
|
export type TransitType =
|
||||||
|
| "PKTransitTypeAir"
|
||||||
|
| "PKTransitTypeBoat"
|
||||||
|
| "PKTransitTypeBus"
|
||||||
|
| "PKTransitTypeGeneric"
|
||||||
|
| "PKTransitTypeTrain";
|
||||||
|
|
||||||
|
export const TransitType = Joi.string().regex(
|
||||||
|
/(PKTransitTypeAir|PKTransitTypeBoat|PKTransitTypeBus|PKTransitTypeGeneric|PKTransitTypeTrain)/,
|
||||||
|
);
|
||||||
|
|
||||||
export interface PassFields {
|
export interface PassFields {
|
||||||
auxiliaryFields: (Field & { row?: number })[];
|
auxiliaryFields: (Field & { row?: number })[];
|
||||||
@@ -7,6 +18,7 @@ export interface PassFields {
|
|||||||
headerFields: Field[];
|
headerFields: Field[];
|
||||||
primaryFields: Field[];
|
primaryFields: Field[];
|
||||||
secondaryFields: Field[];
|
secondaryFields: Field[];
|
||||||
|
transitType?: TransitType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PassFields = Joi.object<PassFields>().keys({
|
export const PassFields = Joi.object<PassFields>().keys({
|
||||||
@@ -21,15 +33,5 @@ export const PassFields = Joi.object<PassFields>().keys({
|
|||||||
headerFields: Joi.array().items(Field),
|
headerFields: Joi.array().items(Field),
|
||||||
primaryFields: Joi.array().items(Field),
|
primaryFields: Joi.array().items(Field),
|
||||||
secondaryFields: Joi.array().items(Field),
|
secondaryFields: Joi.array().items(Field),
|
||||||
|
transitType: TransitType,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TransitType =
|
|
||||||
| "PKTransitTypeAir"
|
|
||||||
| "PKTransitTypeBoat"
|
|
||||||
| "PKTransitTypeBus"
|
|
||||||
| "PKTransitTypeGeneric"
|
|
||||||
| "PKTransitTypeTrain";
|
|
||||||
|
|
||||||
export const TransitType = Joi.string().regex(
|
|
||||||
/(PKTransitTypeAir|PKTransitTypeBoat|PKTransitTypeBus|PKTransitTypeGeneric|PKTransitTypeTrain)/,
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -2,22 +2,21 @@ import Joi from "joi";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://developer.apple.com/documentation/walletpasses/personalize
|
* @see https://developer.apple.com/documentation/walletpasses/personalize
|
||||||
* @TODO Rename "Personalization" in "Personalize". This will be done in v3.0
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface Personalization {
|
|
||||||
description: string;
|
|
||||||
requiredPersonalizationFields: RequiredPersonalizationFields[];
|
|
||||||
termsAndConditions?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type RequiredPersonalizationFields =
|
type RequiredPersonalizationFields =
|
||||||
| "PKPassPersonalizationFieldName"
|
| "PKPassPersonalizationFieldName"
|
||||||
| "PKPassPersonalizationFieldPostalCode"
|
| "PKPassPersonalizationFieldPostalCode"
|
||||||
| "PKPassPersonalizationFieldEmailAddress"
|
| "PKPassPersonalizationFieldEmailAddress"
|
||||||
| "PKPassPersonalizationFieldPhoneNumber";
|
| "PKPassPersonalizationFieldPhoneNumber";
|
||||||
|
|
||||||
export const Personalization = Joi.object<Personalization>().keys({
|
export interface Personalize {
|
||||||
|
description: string;
|
||||||
|
requiredPersonalizationFields: RequiredPersonalizationFields[];
|
||||||
|
termsAndConditions?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Personalize = Joi.object<Personalize>().keys({
|
||||||
description: Joi.string().required(),
|
description: Joi.string().required(),
|
||||||
requiredPersonalizationFields: Joi.array()
|
requiredPersonalizationFields: Joi.array()
|
||||||
.items(
|
.items(
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ const CurrencyAmount = Joi.object<SemanticTagType.CurrencyAmount>().keys({
|
|||||||
amount: Joi.string(),
|
amount: Joi.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const PersonNameComponent = Joi.object<SemanticTagType.PersonNameComponents>().keys(
|
const PersonNameComponent =
|
||||||
{
|
Joi.object<SemanticTagType.PersonNameComponents>().keys({
|
||||||
givenName: Joi.string(),
|
givenName: Joi.string(),
|
||||||
familyName: Joi.string(),
|
familyName: Joi.string(),
|
||||||
middleName: Joi.string(),
|
middleName: Joi.string(),
|
||||||
@@ -61,8 +61,7 @@ const PersonNameComponent = Joi.object<SemanticTagType.PersonNameComponents>().k
|
|||||||
nameSuffix: Joi.string(),
|
nameSuffix: Joi.string(),
|
||||||
nickname: Joi.string(),
|
nickname: Joi.string(),
|
||||||
phoneticRepresentation: Joi.string(),
|
phoneticRepresentation: Joi.string(),
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const seat = Joi.object<SemanticTagType.Seat>().keys({
|
const seat = Joi.object<SemanticTagType.Seat>().keys({
|
||||||
seatSection: Joi.string(),
|
seatSection: Joi.string(),
|
||||||
@@ -1,145 +1,64 @@
|
|||||||
export * from "./Barcodes";
|
export * from "./Barcode";
|
||||||
export * from "./Beacons";
|
export * from "./Beacon";
|
||||||
export * from "./Location";
|
export * from "./Location";
|
||||||
export * from "./PassFieldContent";
|
export * from "./Field";
|
||||||
export * from "./NFC";
|
export * from "./NFC";
|
||||||
export * from "./SemanticTags";
|
export * from "./Semantics";
|
||||||
export * from "./PassFields";
|
export * from "./PassFields";
|
||||||
export * from "./Personalize";
|
export * from "./Personalize";
|
||||||
|
export * from "./Certificates";
|
||||||
|
|
||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
import debug from "debug";
|
|
||||||
|
|
||||||
import { Barcode } from "./Barcodes";
|
import { Barcode } from "./Barcode";
|
||||||
import { Location } from "./Location";
|
import { Location } from "./Location";
|
||||||
import { Beacon } from "./Beacons";
|
import { Beacon } from "./Beacon";
|
||||||
import { NFC } from "./NFC";
|
import { NFC } from "./NFC";
|
||||||
import { Field } from "./PassFieldContent";
|
|
||||||
import { PassFields, TransitType } from "./PassFields";
|
import { PassFields, TransitType } from "./PassFields";
|
||||||
import { Personalization } from "./Personalize";
|
import { Semantics } from "./Semantics";
|
||||||
import { Semantics } from "./SemanticTags";
|
import { CertificatesSchema } from "./Certificates";
|
||||||
|
|
||||||
const schemaDebug = debug("Schema");
|
import * as Messages from "../messages";
|
||||||
|
|
||||||
export interface Manifest {
|
const RGB_COLOR_REGEX =
|
||||||
[key: string]: string;
|
/rgb\(\s*(?:[01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\s*,\s*(?:[01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\s*,\s*(?:[01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\s*\)/;
|
||||||
}
|
|
||||||
|
|
||||||
export interface Certificates {
|
export interface FileBuffers {
|
||||||
wwdr?: string;
|
|
||||||
signerCert?: string;
|
|
||||||
signerKey?:
|
|
||||||
| {
|
|
||||||
keyFile: string;
|
|
||||||
passphrase?: string;
|
|
||||||
}
|
|
||||||
| string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FactoryOptions {
|
|
||||||
model: BundleUnit | string;
|
|
||||||
certificates: Certificates;
|
|
||||||
overrides?: OverridesSupportedOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BundleUnit {
|
|
||||||
[key: string]: Buffer;
|
[key: string]: Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PartitionedBundle {
|
export interface PassProps {
|
||||||
bundle: BundleUnit;
|
formatVersion?: 1;
|
||||||
l10nBundle: {
|
|
||||||
[key: string]: BundleUnit;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CertificatesSchema {
|
|
||||||
wwdr: string;
|
|
||||||
signerCert: string;
|
|
||||||
signerKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CertificatesSchema = Joi.object<CertificatesSchema>()
|
|
||||||
.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();
|
|
||||||
|
|
||||||
export interface PassInstance {
|
|
||||||
model: PartitionedBundle;
|
|
||||||
certificates: CertificatesSchema;
|
|
||||||
overrides?: OverridesSupportedOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PassInstance = Joi.object<PassInstance>().keys({
|
|
||||||
model: Joi.alternatives(Joi.object(), Joi.string()).required(),
|
|
||||||
certificates: Joi.object(),
|
|
||||||
overrides: Joi.object(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface OverridesSupportedOptions {
|
|
||||||
serialNumber?: string;
|
serialNumber?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
organizationName?: string;
|
organizationName?: string;
|
||||||
passTypeIdentifier?: string;
|
passTypeIdentifier?: string;
|
||||||
teamIdentifier?: string;
|
teamIdentifier?: string;
|
||||||
appLaunchURL?: string;
|
appLaunchURL?: string;
|
||||||
associatedStoreIdentifiers?: Array<number>;
|
voided?: boolean;
|
||||||
userInfo?: { [key: string]: any };
|
userInfo?: { [key: string]: any };
|
||||||
webServiceURL?: string;
|
|
||||||
authenticationToken?: string;
|
|
||||||
sharingProhibited?: boolean;
|
sharingProhibited?: boolean;
|
||||||
backgroundColor?: string;
|
|
||||||
foregroundColor?: string;
|
|
||||||
labelColor?: string;
|
|
||||||
groupingIdentifier?: string;
|
groupingIdentifier?: string;
|
||||||
suppressStripShine?: boolean;
|
suppressStripShine?: boolean;
|
||||||
logoText?: string;
|
logoText?: string;
|
||||||
maxDistance?: number;
|
maxDistance?: number;
|
||||||
semantics?: Semantics;
|
semantics?: Semantics;
|
||||||
}
|
|
||||||
|
|
||||||
export const OverridesSupportedOptions = Joi.object<OverridesSupportedOptions>()
|
webServiceURL?: string;
|
||||||
.keys({
|
associatedStoreIdentifiers?: Array<number>;
|
||||||
serialNumber: Joi.string(),
|
authenticationToken?: string;
|
||||||
description: Joi.string(),
|
|
||||||
organizationName: Joi.string(),
|
backgroundColor?: string;
|
||||||
passTypeIdentifier: Joi.string(),
|
foregroundColor?: string;
|
||||||
teamIdentifier: Joi.string(),
|
labelColor?: string;
|
||||||
appLaunchURL: Joi.string(),
|
|
||||||
associatedStoreIdentifiers: Joi.array().items(Joi.number()),
|
nfc?: NFC;
|
||||||
userInfo: Joi.alternatives(Joi.object().unknown(), Joi.array()),
|
beacons?: Beacon[];
|
||||||
// parsing url as set of words and nums followed by dots, optional port and any possible path after
|
barcodes?: Barcode[];
|
||||||
webServiceURL: Joi.string().regex(
|
relevantDate?: string;
|
||||||
/https?:\/\/(?:[a-z0-9]+\.?)+(?::\d{2,})?(?:\/[\S]+)*/,
|
expirationDate?: string;
|
||||||
),
|
locations?: Location[];
|
||||||
authenticationToken: Joi.string().min(16),
|
|
||||||
sharingProhibited: Joi.boolean(),
|
|
||||||
backgroundColor: Joi.string().min(10).max(16),
|
|
||||||
foregroundColor: Joi.string().min(10).max(16),
|
|
||||||
labelColor: Joi.string().min(10).max(16),
|
|
||||||
groupingIdentifier: Joi.string(),
|
|
||||||
suppressStripShine: Joi.boolean(),
|
|
||||||
logoText: Joi.string(),
|
|
||||||
maxDistance: Joi.number().positive(),
|
|
||||||
semantics: Semantics,
|
|
||||||
})
|
|
||||||
.with("webServiceURL", "authenticationToken");
|
|
||||||
|
|
||||||
export interface ValidPassType {
|
|
||||||
boardingPass?: PassFields & { transitType: TransitType };
|
boardingPass?: PassFields & { transitType: TransitType };
|
||||||
eventTicket?: PassFields;
|
eventTicket?: PassFields;
|
||||||
coupon?: PassFields;
|
coupon?: PassFields;
|
||||||
@@ -147,101 +66,175 @@ export interface ValidPassType {
|
|||||||
storeCard?: PassFields;
|
storeCard?: PassFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PassInterfacesProps {
|
/**
|
||||||
barcode?: Barcode;
|
* These are the properties passkit-generator will
|
||||||
barcodes?: Barcode[];
|
* handle through its methods
|
||||||
beacons?: Beacon[];
|
*/
|
||||||
locations?: Location[];
|
|
||||||
maxDistance?: number;
|
type PassMethodsProps =
|
||||||
relevantDate?: string;
|
| "nfc"
|
||||||
nfc?: NFC;
|
| "beacons"
|
||||||
expirationDate?: string;
|
| "barcodes"
|
||||||
voided?: boolean;
|
| "relevantDate"
|
||||||
}
|
| "expirationDate"
|
||||||
|
| "locations";
|
||||||
|
|
||||||
|
export type PassTypesProps =
|
||||||
|
| "boardingPass"
|
||||||
|
| "eventTicket"
|
||||||
|
| "coupon"
|
||||||
|
| "generic"
|
||||||
|
| "storeCard";
|
||||||
|
|
||||||
|
export type OverridablePassProps = Omit<
|
||||||
|
PassProps,
|
||||||
|
PassMethodsProps | PassTypesProps
|
||||||
|
>;
|
||||||
|
export type PassPropsFromMethods = { [K in PassMethodsProps]: PassProps[K] };
|
||||||
|
export type PassKindsProps = { [K in PassTypesProps]: PassProps[K] };
|
||||||
|
|
||||||
type AllPassProps = PassInterfacesProps &
|
|
||||||
ValidPassType &
|
|
||||||
OverridesSupportedOptions;
|
|
||||||
export type ValidPass = {
|
|
||||||
[K in keyof AllPassProps]: AllPassProps[K];
|
|
||||||
};
|
|
||||||
export type PassColors = Pick<
|
export type PassColors = Pick<
|
||||||
OverridesSupportedOptions,
|
OverridablePassProps,
|
||||||
"backgroundColor" | "foregroundColor" | "labelColor"
|
"backgroundColor" | "foregroundColor" | "labelColor"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export const PassPropsFromMethods = Joi.object<PassPropsFromMethods>({
|
||||||
|
nfc: NFC,
|
||||||
|
beacons: Joi.array().items(Beacon),
|
||||||
|
barcodes: Joi.array().items(Barcode),
|
||||||
|
relevantDate: Joi.string().isoDate(),
|
||||||
|
expirationDate: Joi.string().isoDate(),
|
||||||
|
locations: Joi.array().items(Location),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PassKindsProps = Joi.object<PassKindsProps>({
|
||||||
|
coupon: PassFields.disallow("transitType"),
|
||||||
|
generic: PassFields.disallow("transitType"),
|
||||||
|
storeCard: PassFields.disallow("transitType"),
|
||||||
|
eventTicket: PassFields.disallow("transitType"),
|
||||||
|
boardingPass: PassFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PassType = Joi.string().regex(
|
||||||
|
/(boardingPass|coupon|eventTicket|storeCard|generic)/,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const OverridablePassProps = Joi.object<OverridablePassProps>({
|
||||||
|
formatVersion: Joi.number().default(1),
|
||||||
|
semantics: Semantics,
|
||||||
|
voided: Joi.boolean(),
|
||||||
|
logoText: Joi.string(),
|
||||||
|
description: Joi.string(),
|
||||||
|
serialNumber: Joi.string(),
|
||||||
|
appLaunchURL: Joi.string(),
|
||||||
|
teamIdentifier: Joi.string(),
|
||||||
|
organizationName: Joi.string(),
|
||||||
|
passTypeIdentifier: Joi.string(),
|
||||||
|
sharingProhibited: Joi.boolean(),
|
||||||
|
groupingIdentifier: Joi.string(),
|
||||||
|
suppressStripShine: Joi.boolean(),
|
||||||
|
maxDistance: Joi.number().positive(),
|
||||||
|
authenticationToken: Joi.string().min(16),
|
||||||
|
labelColor: Joi.string().regex(RGB_COLOR_REGEX),
|
||||||
|
backgroundColor: Joi.string().regex(RGB_COLOR_REGEX),
|
||||||
|
foregroundColor: Joi.string().regex(RGB_COLOR_REGEX),
|
||||||
|
associatedStoreIdentifiers: Joi.array().items(Joi.number()),
|
||||||
|
userInfo: Joi.alternatives(Joi.object().unknown(), Joi.array()),
|
||||||
|
// parsing url as set of words and nums followed by dots, optional port and any possible path after
|
||||||
|
webServiceURL: Joi.string().regex(
|
||||||
|
/https?:\/\/(?:[a-z0-9]+\.?)+(?::\d{2,})?(?:\/[\S]+)*/,
|
||||||
|
),
|
||||||
|
}).with("webServiceURL", "authenticationToken");
|
||||||
|
|
||||||
|
export const PassProps = Joi.object<
|
||||||
|
OverridablePassProps & PassKindsProps & PassPropsFromMethods
|
||||||
|
>()
|
||||||
|
.concat(OverridablePassProps)
|
||||||
|
.concat(PassKindsProps)
|
||||||
|
.concat(PassPropsFromMethods);
|
||||||
|
|
||||||
|
export interface Template {
|
||||||
|
model: string;
|
||||||
|
certificates: CertificatesSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Template = Joi.object<Template>({
|
||||||
|
model: Joi.string().required(),
|
||||||
|
certificates: Joi.object().required(),
|
||||||
|
});
|
||||||
|
|
||||||
// --------- UTILITIES ---------- //
|
// --------- UTILITIES ---------- //
|
||||||
|
|
||||||
type AvailableSchemas =
|
|
||||||
| typeof Barcode
|
|
||||||
| typeof Location
|
|
||||||
| typeof Beacon
|
|
||||||
| typeof NFC
|
|
||||||
| typeof Field
|
|
||||||
| typeof PassFields
|
|
||||||
| typeof Personalization
|
|
||||||
| typeof TransitType
|
|
||||||
| typeof PassInstance
|
|
||||||
| typeof CertificatesSchema
|
|
||||||
| typeof OverridesSupportedOptions;
|
|
||||||
|
|
||||||
export type ArrayPassSchema = Beacon | Location | Barcode;
|
|
||||||
|
|
||||||
/* function resolveSchemaName(name: Schema) {
|
|
||||||
return schemas[name] || undefined;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* Checks if the passed options are compliant with the indicated schema
|
* Performs validation of a schema on an object.
|
||||||
* @param {any} opts - options to be checks
|
* If it fails, will throw an error.
|
||||||
* @param {string} schemaName - the indicated schema (will be converted)
|
*
|
||||||
* @returns {boolean} - result of the check
|
* @param schema
|
||||||
|
* @param data
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function isValid(opts: any, schema: AvailableSchemas): boolean {
|
export function assertValidity<T>(
|
||||||
if (!schema) {
|
schema: Joi.ObjectSchema<T> | Joi.StringSchema,
|
||||||
schemaDebug(
|
data: T,
|
||||||
`validation failed due to missing or mispelled schema name`,
|
customErrorMessage?: string,
|
||||||
);
|
): void {
|
||||||
return false;
|
const validation = schema.validate(data);
|
||||||
}
|
|
||||||
|
|
||||||
const validation = schema.validate(opts);
|
|
||||||
|
|
||||||
if (validation.error) {
|
if (validation.error) {
|
||||||
schemaDebug(
|
if (customErrorMessage) {
|
||||||
`validation failed due to error: ${validation.error.message}`,
|
console.warn(validation.error);
|
||||||
);
|
throw new TypeError(
|
||||||
}
|
`${validation.error.name} happened. ${Messages.format(
|
||||||
|
customErrorMessage,
|
||||||
|
validation.error.message,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return !validation.error;
|
throw new TypeError(validation.error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the validation in verbose mode, exposing the value or an empty object
|
* Performs validation and throws the error if there's one.
|
||||||
* @param {object} opts - to be validated
|
* Otherwise returns a (possibly patched) version of the specified
|
||||||
* @param {*} schemaName - selected schema
|
* options (it depends on the schema)
|
||||||
* @returns {object} the filtered value or empty object
|
*
|
||||||
|
* @param schema
|
||||||
|
* @param options
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function getValidated<T extends Object>(
|
export function validate<T extends Object>(
|
||||||
opts: T,
|
schema: Joi.ObjectSchema<T> | Joi.StringSchema,
|
||||||
schema: AvailableSchemas,
|
options: T,
|
||||||
): T | null {
|
): T {
|
||||||
if (!schema) {
|
const validationResult = schema.validate(options, {
|
||||||
schemaDebug(`validation failed due to missing schema`);
|
stripUnknown: true,
|
||||||
|
abortEarly: true,
|
||||||
|
});
|
||||||
|
|
||||||
return null;
|
if (validationResult.error) {
|
||||||
|
throw validationResult.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validation = schema.validate(opts, { stripUnknown: true });
|
return validationResult.value;
|
||||||
|
}
|
||||||
if (validation.error) {
|
|
||||||
schemaDebug(
|
export function filterValid<T extends Object>(
|
||||||
`Validation failed in getValidated due to error: ${validation.error.message}`,
|
schema: Joi.ObjectSchema<T>,
|
||||||
);
|
source: T[],
|
||||||
return null;
|
): T[] {
|
||||||
}
|
if (!source) {
|
||||||
|
return [];
|
||||||
return validation.value;
|
}
|
||||||
|
|
||||||
|
return source.reduce((acc, current) => {
|
||||||
|
try {
|
||||||
|
return [...acc, validate(schema, current)];
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(Messages.format(Messages.FILTER_VALID.INVALID, err));
|
||||||
|
return [...acc];
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
}
|
}
|
||||||
|
|||||||
155
src/utils.ts
155
src/utils.ts
@@ -1,29 +1,24 @@
|
|||||||
import { EOL } from "os";
|
import * as Messages from "./messages";
|
||||||
import type * as Schemas from "./schemas";
|
import type Bundle from "./Bundle";
|
||||||
import { sep } from "path";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if an rgb value is compliant with CSS-like syntax
|
* Acts as a wrapper for converting date to W3C string
|
||||||
*
|
* @param date
|
||||||
* @function isValidRGB
|
* @returns
|
||||||
* @params {String} value - string to analyze
|
|
||||||
* @returns {Boolean} True if valid rgb, false otherwise
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function isValidRGB(value?: string): boolean {
|
export function processDate(date: Date): string | null {
|
||||||
if (!value || typeof value !== "string") {
|
if (!(date instanceof Date)) {
|
||||||
return false;
|
throw "Invalid date";
|
||||||
}
|
}
|
||||||
|
|
||||||
const rgb = value.match(
|
const dateParse = dateToW3CString(date);
|
||||||
/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!rgb) {
|
if (!dateParse) {
|
||||||
return false;
|
throw "Invalid date";
|
||||||
}
|
}
|
||||||
|
|
||||||
return rgb.slice(1, 4).every((v) => Math.abs(Number(v)) <= 255);
|
return dateParse;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,11 +30,7 @@ export function isValidRGB(value?: string): boolean {
|
|||||||
* undefined otherwise
|
* undefined otherwise
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function dateToW3CString(date: Date) {
|
function dateToW3CString(date: Date) {
|
||||||
if (!(date instanceof Date)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// if it is NaN, it is "Invalid Date"
|
// if it is NaN, it is "Invalid Date"
|
||||||
if (isNaN(Number(date))) {
|
if (isNaN(Number(date))) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -76,11 +67,10 @@ function padMeTwo(original: string | number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply a filter to arg0 to remove hidden files names (starting with dot)
|
* Removes hidden files from a list (those starting with dot)
|
||||||
*
|
*
|
||||||
* @function removeHidden
|
* @params from - list of file names
|
||||||
* @params {String[]} from - list of file names
|
* @return
|
||||||
* @return {String[]}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function removeHidden(from: Array<string>): Array<string> {
|
export function removeHidden(from: Array<string>): Array<string> {
|
||||||
@@ -88,102 +78,39 @@ export function removeHidden(from: Array<string>): Array<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a buffer of translations in Apple .strings format
|
* Clones recursively an object and all of its properties
|
||||||
*
|
*
|
||||||
* @function generateStringFile
|
* @param object
|
||||||
* @params {Object} lang - structure containing related to ISO 3166 alpha-2 code for the language
|
* @returns
|
||||||
* @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 {
|
export function cloneRecursive(object: Object) {
|
||||||
if (!Object.keys(lang).length) {
|
const objectCopy = {};
|
||||||
return Buffer.from("", "utf8");
|
const objectEntries = Object.entries(object);
|
||||||
}
|
|
||||||
|
|
||||||
// Pass.strings format is the following one for each row:
|
for (let i = 0; i < objectEntries.length; i++) {
|
||||||
// "key" = "value";
|
const [key, value] = objectEntries[i];
|
||||||
|
|
||||||
const strings = Object.keys(lang).map(
|
if (value && typeof value === "object") {
|
||||||
(key) => `"${key}" = "${lang[key].replace(/"/g, '"')}";`,
|
if (Array.isArray(value)) {
|
||||||
);
|
objectCopy[key] = value.slice();
|
||||||
|
|
||||||
return Buffer.from(strings.join(EOL), "utf8");
|
for (let j = 0; j < value.length; j++) {
|
||||||
}
|
objectCopy[key][j] = cloneRecursive(value[j]);
|
||||||
|
}
|
||||||
/**
|
} else {
|
||||||
* Applies a partition to split one bundle
|
objectCopy[key] = cloneRecursive(value);
|
||||||
* to two
|
|
||||||
* @param origin
|
|
||||||
*/
|
|
||||||
|
|
||||||
type PartitionedBundleElements = [
|
|
||||||
Schemas.PartitionedBundle["l10nBundle"],
|
|
||||||
Schemas.PartitionedBundle["bundle"],
|
|
||||||
];
|
|
||||||
|
|
||||||
export function splitBufferBundle(
|
|
||||||
origin: Schemas.BundleUnit,
|
|
||||||
): PartitionedBundleElements {
|
|
||||||
const initialValue: PartitionedBundleElements = [{}, {}];
|
|
||||||
|
|
||||||
if (!origin) {
|
|
||||||
return initialValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.entries(origin).reduce<PartitionedBundleElements>(
|
|
||||||
([l10n, bundle], [key, buffer]) => {
|
|
||||||
if (!key.includes(".lproj")) {
|
|
||||||
return [
|
|
||||||
l10n,
|
|
||||||
{
|
|
||||||
...bundle,
|
|
||||||
[key]: buffer,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
objectCopy[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const pathComponents = key.split(sep);
|
return objectCopy;
|
||||||
const lang = pathComponents[0];
|
|
||||||
const file = pathComponents.slice(1).join("/");
|
|
||||||
|
|
||||||
(l10n[lang] || (l10n[lang] = {}))[file] = buffer;
|
|
||||||
|
|
||||||
return [l10n, bundle];
|
|
||||||
},
|
|
||||||
initialValue,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type StringSearchMode = "includes" | "startsWith" | "endsWith";
|
export function assertUnfrozen(instance: InstanceType<typeof Bundle>) {
|
||||||
|
if (instance.isFrozen) {
|
||||||
export function getAllFilesWithName(
|
throw new Error(Messages.BUNDLE.CLOSED);
|
||||||
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: Schemas.BundleUnit,
|
|
||||||
logosNames: string[] = [],
|
|
||||||
): void {
|
|
||||||
[...logosNames, "personalization.json"].forEach(
|
|
||||||
(file) => delete source[file],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,5 @@
|
|||||||
"newLine": "LF",
|
"newLine": "LF",
|
||||||
"importHelpers": true
|
"importHelpers": true
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": ["node_modules/"]
|
||||||
"node_modules/"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user