Added prettier to project

This commit is contained in:
Alexander Cerutti
2021-02-08 00:03:28 +01:00
parent d8983b8321
commit d5a487a609
29 changed files with 2018 additions and 1420 deletions

3
.gitignore vendored
View File

@@ -3,7 +3,8 @@ node_modules
passModels/
certificates/
*.code-workspace
.vscode/
.vscode/*
!.vscode/settings.json
*.js
lib/
examples/build

6
.prettierrc Normal file
View File

@@ -0,0 +1,6 @@
{
"bracketSpacing": true,
"trailingComma": "all",
"tabWidth": 4,
"useTabs": true
}

7
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"editor.tabSize": 4,
"editor.formatOnSave": true,
"editor.insertSpaces": false,
"editor.smoothScrolling": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}

View File

@@ -2,7 +2,7 @@
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.
Each example is linked to webserver.js, which _requires_ express.js to run.
Express.js has been inserted as "example package" dipendency.
```sh
@@ -18,6 +18,7 @@ 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!

View File

@@ -1,5 +1,9 @@
import genRoute, { app } from "./webserver";
import { createPass, createAbstractModel, AbstractModel } from "passkit-generator";
import {
createPass,
createAbstractModel,
AbstractModel,
} from "passkit-generator";
let abstractModel: AbstractModel;
@@ -11,148 +15,182 @@ let abstractModel: AbstractModel;
signerCert: "../certificates/signerCert.pem",
signerKey: {
keyFile: "../certificates/signerKey.pem",
passphrase: "123456"
}
passphrase: "123456",
},
},
// overrides: request.body || request.params || request.query,
});
})();
genRoute.all(async function manageRequest(request, response) {
const passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, "");
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.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.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.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: "",
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.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 didentità corredato di fotografia",
"value": "è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta didentità.",
"textAlignment": "PKTextAlignmentLeft"
}, {
"key": "yourSeat",
"label": "Il tuo posto:",
"value": "verifica il tuo numero di posto nella parte superiore. Durante limbarco utilizza le scale anteriori e posteriori: per le file 1-10 imbarcati dalla parte anteriore; per le file 11-31 imbarcati dalla parte posteriore. Colloca le borse di dimensioni ridotte sotto il sedile davanti a te.",
"textAlignment": "PKTextAlignmentLeft"
}, {
"key": "Pack safely",
"label": "Bagaglio sicuro",
"value": "Fai clic http://easyjet.com/it/articoli-pericolosi per maggiori informazioni sulle merci pericolose oppure visita il sito CAA http://www.caa.co.uk/default.aspx?catid=2200",
"textAlignment": "PKTextAlignmentLeft"
}, {
"key": "Thank you for travelling easyJet",
"label": "Grazie per aver viaggiato con easyJet",
"value": "",
"textAlignment": "PKTextAlignmentLeft"
});
pass.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 didentità corredato di fotografia",
value:
"è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta didentità.",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "yourSeat",
label: "Il tuo posto:",
value:
"verifica il tuo numero di posto nella parte superiore. Durante limbarco 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`
"Content-disposition": `attachment; filename=${passName}.pkpass`,
});
stream.pipe(response);
} catch(err) {
} catch (err) {
console.log(err);
response.set({
"Content-type": "text/html"
"Content-type": "text/html",
});
response.send(err.message);

View File

@@ -11,12 +11,14 @@ import fetch from "node-fetch";
import { createPass } from "passkit-generator";
app.all(async function manageRequest(request, response) {
let passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, "");
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 avatar = await fetch(
"https://s.gravatar.com/avatar/83cd11399b7ea79977bc302f3931ee52?size=32&default=retro",
).then((res) => res.buffer());
const passConfig = {
model: `./models/${request.params.modelName}`,
@@ -25,8 +27,8 @@ app.all(async function manageRequest(request, response) {
signerCert: "../certificates/signerCert.pem",
signerKey: {
keyFile: "../certificates/signerKey.pem",
passphrase: "123456"
}
passphrase: "123456",
},
},
overrides: request.body || request.params || request.query,
};
@@ -44,7 +46,7 @@ app.all(async function manageRequest(request, response) {
response.set({
"Content-type": "application/vnd.apple.pkpass",
"Content-disposition": `attachment; filename=${passName}.pkpass`
"Content-disposition": `attachment; filename=${passName}.pkpass`,
});
stream.pipe(response);

View File

@@ -12,7 +12,10 @@ import app from "./webserver";
import { createPass } from "passkit-generator";
app.all(async function manageRequest(request, response) {
const passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, "");
const passName =
request.params.modelName +
"_" +
new Date().toISOString().split("T")[0].replace(/-/gi, "");
try {
const pass = await createPass({
@@ -22,8 +25,8 @@ app.all(async function manageRequest(request, response) {
signerCert: "../certificates/signerCert.pem",
signerKey: {
keyFile: "../certificates/signerKey.pem",
passphrase: "123456"
}
passphrase: "123456",
},
},
overrides: request.body || request.params || request.query,
});
@@ -37,17 +40,21 @@ app.all(async function manageRequest(request, response) {
// 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",
// @ts-expect-error
format: "PKBarcodeFormatMock44617"
});
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",
// @ts-expect-error
format: "PKBarcodeFormatMock44617",
},
);
}
// You can change the format chosen for barcode prop support by calling .barcode()
@@ -57,12 +64,15 @@ app.all(async function manageRequest(request, response) {
pass.barcode("PKBarcodeFormatPDF417");
console.log("Barcode property is now:", pass.props["barcode"]);
console.log("Barcodes support is autocompleted:", pass.props["barcodes"]);
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`
"Content-disposition": `attachment; filename=${passName}.pkpass`,
});
stream.pipe(response);

View File

@@ -13,11 +13,16 @@ import { createPass } from "passkit-generator";
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>");
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(/-/ig, "");
let passName =
request.params.modelName +
"_" +
new Date().toISOString().split("T")[0].replace(/-/gi, "");
try {
let pass = await createPass({
@@ -27,8 +32,8 @@ app.all(async function manageRequest(request, response) {
signerCert: "../certificates/signerCert.pem",
signerKey: {
keyFile: "../certificates/signerKey.pem",
passphrase: "123456"
}
passphrase: "123456",
},
},
overrides: request.body || request.params || request.query,
});
@@ -47,7 +52,7 @@ app.all(async function manageRequest(request, response) {
const stream = pass.generate();
response.set({
"Content-type": "application/vnd.apple.pkpass",
"Content-disposition": `attachment; filename=${passName}.pkpass`
"Content-disposition": `attachment; filename=${passName}.pkpass`,
});
stream.pipe(response);

View File

@@ -13,7 +13,10 @@ import app from "./webserver";
import { createPass } from "passkit-generator";
app.all(async function manageRequest(request, response) {
let passName = "exampleBooking" + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, "");
let passName =
"exampleBooking" +
"_" +
new Date().toISOString().split("T")[0].replace(/-/gi, "");
try {
let pass = await createPass({
model: `./models/exampleBooking`,
@@ -22,133 +25,164 @@ app.all(async function manageRequest(request, response) {
signerCert: "../certificates/signerCert.pem",
signerKey: {
keyFile: "../certificates/signerKey.pem",
passphrase: "123456"
}
passphrase: "123456",
},
},
overrides: request.body || request.params || request.query,
});
pass.transitType = "PKTransitTypeAir";
pass.headerFields.push({
"key": "header1",
"label": "Data",
"value": "25 mag",
"textAlignment": "PKTextAlignmentCenter"
}, {
"key": "header2",
"label": "Volo",
"value": "EZY997",
"textAlignment": "PKTextAlignmentCenter"
});
pass.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.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.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: "",
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.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 didentità corredato di fotografia",
"value": "è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta didentità.",
"textAlignment": "PKTextAlignmentLeft"
}, {
"key": "yourSeat",
"label": "Il tuo posto:",
"value": "verifica il tuo numero di posto nella parte superiore. Durante limbarco utilizza le scale anteriori e posteriori: per le file 1-10 imbarcati dalla parte anteriore; per le file 11-31 imbarcati dalla parte posteriore. Colloca le borse di dimensioni ridotte sotto il sedile davanti a te.",
"textAlignment": "PKTextAlignmentLeft"
}, {
"key": "Pack safely",
"label": "Bagaglio sicuro",
"value": "Fai clic http://easyjet.com/it/articoli-pericolosi per maggiori informazioni sulle merci pericolose oppure visita il sito CAA http://www.caa.co.uk/default.aspx?catid=2200",
"textAlignment": "PKTextAlignmentLeft"
}, {
"key": "Thank you for travelling easyJet",
"label": "Grazie per aver viaggiato con easyJet",
"value": "",
"textAlignment": "PKTextAlignmentLeft"
});
pass.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 didentità corredato di fotografia",
value:
"è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta didentità.",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "yourSeat",
label: "Il tuo posto:",
value:
"verifica il tuo numero di posto nella parte superiore. Durante limbarco 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`
"Content-disposition": `attachment; filename=${passName}.pkpass`,
});
stream.pipe(response);
@@ -156,7 +190,7 @@ app.all(async function manageRequest(request, response) {
console.log(err);
response.set({
"Content-type": "text/html"
"Content-type": "text/html",
});
response.send(err.message);

View File

@@ -8,7 +8,10 @@ import app from "./webserver";
import { createPass } from "passkit-generator";
app.all(async function manageRequest(request, response) {
const passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, "");
const passName =
request.params.modelName +
"_" +
new Date().toISOString().split("T")[0].replace(/-/gi, "");
try {
const pass = await createPass({
@@ -18,10 +21,10 @@ app.all(async function manageRequest(request, response) {
signerCert: "../certificates/signerCert.pem",
signerKey: {
keyFile: "../certificates/signerKey.pem",
passphrase: "123456"
}
passphrase: "123456",
},
},
overrides: request.body || request.params || request.query
overrides: request.body || request.params || request.query,
});
// For each language you include, an .lproj folder in pass bundle
@@ -38,30 +41,33 @@ app.all(async function manageRequest(request, response) {
// Italian, already has an .lproj which gets included
pass.localize("it", {
"EVENT": "Evento",
"LOCATION": "Dove"
EVENT: "Evento",
LOCATION: "Dove",
});
// German, doesn't, so is created
pass.localize("de", {
"EVENT": "Ereignis",
"LOCATION": "Ort"
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(", "))
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`
"Content-disposition": `attachment; filename=${passName}.pkpass`,
});
stream.pipe(response);
} catch(err) {
} catch (err) {
console.log(err);
response.set({

View File

@@ -1,49 +1,51 @@
{
"formatVersion": 1,
"passTypeIdentifier": "pass.com.example.myapp",
"serialNumber": "nmyuxofgna",
"teamIdentifier": "F53WB8AE67",
"webServiceURL": "https://192.168.1.254:80/",
"authenticationToken": "vxwxd7J8AlNNFPS8k0a0FfUFtq0ewzFdc",
"relevantDate": "2011-12-08T13:00-08:00",
"locations": [
{
"longitude": -122.3748889,
"latitude": 37.6189722
},
{
"longitude": -122.03118,
"latitude": 37.33182
}
],
"barcodes": [{
"message": "123456789",
"format": "PKBarcodeFormatQR",
"messageEncoding": "iso-8859-1"
}],
"barcode": {
"message": "123456789",
"format": "PKBarcodeFormatQR",
"messageEncoding": "iso-8859-1"
},
"organizationName": "Apple Inc.",
"description": "Apple Event Ticket",
"foregroundColor": "rgb(255, 255, 255)",
"backgroundColor": "rgb(60, 65, 76)",
"eventTicket": {
"primaryFields": [
{
"key": "event",
"label": "EVENT",
"value": "The Beat Goes On"
}
],
"secondaryFields": [
{
"key": "loc",
"label": "LOCATION",
"value": "Moscone West"
}
]
}
"formatVersion": 1,
"passTypeIdentifier": "pass.com.example.myapp",
"serialNumber": "nmyuxofgna",
"teamIdentifier": "F53WB8AE67",
"webServiceURL": "https://192.168.1.254:80/",
"authenticationToken": "vxwxd7J8AlNNFPS8k0a0FfUFtq0ewzFdc",
"relevantDate": "2011-12-08T13:00-08:00",
"locations": [
{
"longitude": -122.3748889,
"latitude": 37.6189722
},
{
"longitude": -122.03118,
"latitude": 37.33182
}
],
"barcodes": [
{
"message": "123456789",
"format": "PKBarcodeFormatQR",
"messageEncoding": "iso-8859-1"
}
],
"barcode": {
"message": "123456789",
"format": "PKBarcodeFormatQR",
"messageEncoding": "iso-8859-1"
},
"organizationName": "Apple Inc.",
"description": "Apple Event Ticket",
"foregroundColor": "rgb(255, 255, 255)",
"backgroundColor": "rgb(60, 65, 76)",
"eventTicket": {
"primaryFields": [
{
"key": "event",
"label": "EVENT",
"value": "The Beat Goes On"
}
],
"secondaryFields": [
{
"key": "loc",
"label": "LOCATION",
"value": "Moscone West"
}
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,5 @@
"compilerOptions": {
"outDir": "build"
},
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}

View File

@@ -9,7 +9,7 @@ export const app = express();
app.use(express.json());
app.listen(8080, "0.0.0.0", function(request, response) {
app.listen(8080, "0.0.0.0", function (request, response) {
console.log("Webserver started.");
});
@@ -17,10 +17,11 @@ 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>")
});
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");

6
package-lock.json generated
View File

@@ -209,6 +209,12 @@
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"prettier": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz",
"integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==",
"dev": true
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",

View File

@@ -41,6 +41,7 @@
"@types/node-forge": "^0.9.7",
"@types/yazl": "^2.4.2",
"jasmine": "^3.6.4",
"prettier": "^2.2.1",
"rimraf": "^3.0.2",
"typescript": "^4.1.3"
},

View File

@@ -6,7 +6,7 @@ import * as path from "path";
describe("createPass", () => {
it("should throw if first argument is not provided", async () => {
await expectAsync(createPass(undefined)).toBeRejectedWithError(
formatMessage("CP_NO_OPTS")
formatMessage("CP_NO_OPTS"),
);
});
@@ -27,71 +27,144 @@ describe("createPass", () => {
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"
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",
),
},
wwdr: path.resolve(__dirname, certificatesPath, "./WWDR.pem"),
}
})).toBeResolved();
}),
).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"
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",
),
},
wwdr: path.resolve(__dirname, certificatesPath, "./WWDR.pem"),
}
})).toBeRejected();
}),
).toBeRejected();
await expectAsync(createPass({
model: undefined,
certificates: {
signerCert: path.resolve(__dirname, certificatesPath, "./signerCert.pem"),
signerKey: {
keyFile: path.resolve(__dirname, certificatesPath, "./signerKey.pem"),
passphrase: "password1234"
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",
),
},
wwdr: path.resolve(__dirname, certificatesPath, "./WWDR.pem"),
}
})).toBeRejected();
}),
).toBeRejected();
await expectAsync(createPass({
model: null,
certificates: {
signerCert: path.resolve(__dirname, certificatesPath, "./signerCert.pem"),
signerKey: {
keyFile: path.resolve(__dirname, certificatesPath, "./signerKey.pem"),
passphrase: "password1234"
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",
),
},
wwdr: path.resolve(__dirname, certificatesPath, "./WWDR.pem"),
}
})).toBeRejected();
}),
).toBeRejected();
await expectAsync(createPass({
model: {},
certificates: {
signerCert: path.resolve(__dirname, certificatesPath, "./signerCert.pem"),
signerKey: {
keyFile: path.resolve(__dirname, certificatesPath, "./signerKey.pem"),
passphrase: "password1234"
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",
),
},
wwdr: path.resolve(__dirname, certificatesPath, "./WWDR.pem"),
}
})).toBeRejected();
}),
).toBeRejected();
});
});
}
} catch (err) { }
} catch (err) {}
});

View File

@@ -9,16 +9,25 @@ describe("Passkit-generator", function () {
let pass: Pass;
beforeEach(async () => {
pass = await createPass({
model: path.resolve(__dirname, "../examples/models/examplePass.pass"),
model: path.resolve(
__dirname,
"../examples/models/examplePass.pass",
),
certificates: {
wwdr: path.resolve(__dirname, "../certificates/WWDR.pem"),
signerCert: path.resolve(__dirname, "../certificates/signerCert.pem"),
signerCert: path.resolve(
__dirname,
"../certificates/signerCert.pem",
),
signerKey: {
keyFile: path.resolve(__dirname, "../certificates/signerKey.pem"),
passphrase: "123456"
}
keyFile: path.resolve(
__dirname,
"../certificates/signerKey.pem",
),
passphrase: "123456",
},
},
overrides: {}
overrides: {},
});
});
@@ -48,7 +57,7 @@ describe("Passkit-generator", function () {
it("Will apply changes if a second object argument with translations is passed", () => {
pass.localize("it", {
"Test": "Prova"
Test: "Prova",
});
expect(typeof pass["l10nTranslations"]["it"]).toBe("object");
@@ -99,16 +108,21 @@ describe("Passkit-generator", function () {
describe("locations()", () => {
it("Won't apply changes if invalid location objects are passed", () => {
const props = pass.props["locations"] || [];
const oldAmountOfLocations = props && props.length || 0;
const oldAmountOfLocations = (props && props.length) || 0;
pass.locations({
// @ts-expect-error
"ibrupofene": "no",
"longitude": 0.00000000
}, ...props);
pass.locations(
{
// @ts-expect-error
ibrupofene: "no",
longitude: 0.0,
},
...props,
);
if (oldAmountOfLocations) {
expect(pass.props["locations"].length).toBe(oldAmountOfLocations);
expect(pass.props["locations"].length).toBe(
oldAmountOfLocations,
);
} else {
expect(pass.props["locations"]).toBe(undefined);
}
@@ -116,33 +130,42 @@ describe("Passkit-generator", function () {
it("Will filter out invalid location objects", () => {
const props = pass.props["locations"] || [];
const oldAmountOfLocations = props && props.length || 0;
const oldAmountOfLocations = (props && props.length) || 0;
pass.locations({
// @ts-expect-error
"ibrupofene": "no",
"longitude": 0.00000000
}, {
"longitude": 4.42634523,
"latitude": 5.344233323352
}, ...(pass.props["locations"] || []));
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);
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;
const oldAmountOfBeacons = (props && props.length) || 0;
pass.beacons({
// @ts-expect-error
"ibrupofene": "no",
"major": 55,
"minor": 0,
"proximityUUID": "2707c5f4-deb9-48ff-b760-671bc885b6a7"
}, ...props);
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);
@@ -153,20 +176,23 @@ describe("Passkit-generator", function () {
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);
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);
});
@@ -175,7 +201,7 @@ describe("Passkit-generator", function () {
describe("barcodes()", () => {
it("Won't apply changes if no data is passed", () => {
const props = pass.props["barcodes"] || [];
const oldAmountOfBarcodes = props && props.length || 0;
const oldAmountOfBarcodes = (props && props.length) || 0;
pass.barcodes();
expect(pass.props["barcodes"].length).toBe(oldAmountOfBarcodes);
@@ -183,7 +209,7 @@ describe("Passkit-generator", function () {
it("Will ignore boolean parameter", () => {
const props = pass.props["barcodes"] || [];
const oldAmountOfBarcodes = props && props.length || 0;
const oldAmountOfBarcodes = (props && props.length) || 0;
// @ts-expect-error
pass.barcode(true);
@@ -192,7 +218,7 @@ describe("Passkit-generator", function () {
it("Will ignore numeric parameter", () => {
const props = pass.props["barcodes"] || [];
const oldAmountOfBarcodes = props && props.length || 0;
const oldAmountOfBarcodes = (props && props.length) || 0;
// @ts-expect-error
pass.barcodes(42);
@@ -208,7 +234,7 @@ describe("Passkit-generator", function () {
pass.barcodes({
message: "28363516282",
format: "PKBarcodeFormatPDF417",
messageEncoding: "utf8"
messageEncoding: "utf8",
});
expect(pass.props["barcodes"].length).toBe(1);
@@ -220,12 +246,14 @@ describe("Passkit-generator", function () {
format: "PKBarcodeFormatPDF417",
});
expect(pass.props["barcodes"][0].messageEncoding).toBe("iso-8859-1");
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;
const oldAmountOfBarcodes = (props && props.length) || 0;
// @ts-expect-error
pass.barcodes({
@@ -237,12 +265,19 @@ describe("Passkit-generator", function () {
it("Will ignore non-Barcodes schema compliant objects", () => {
// @ts-expect-error
pass.barcodes(5, 10, 15, {
message: "28363516282",
format: "PKBarcodeFormatPDF417"
}, 7, 1);
pass.barcodes(
5,
10,
15,
{
message: "28363516282",
format: "PKBarcodeFormatPDF417",
},
7,
1,
);
expect(pass.props["barcodes"].length).toBe(1)
expect(pass.props["barcodes"].length).toBe(1);
});
it("Will reset barcodes content if parameter is null", () => {
@@ -255,18 +290,18 @@ describe("Passkit-generator", function () {
it("Will ignore non string or null arguments", function () {
const oldBarcode = pass.props["barcode"] || undefined;
pass
.barcodes("Message-22645272183")
pass.barcodes("Message-22645272183")
// @ts-expect-error
.barcode(55)
.barcode(55);
// unchanged
expect(pass.props["barcode"]).toEqual(oldBarcode);
});
it("Will reset backward value on null", () => {
pass.barcodes("Message-22645272183")
.barcode("PKBarcodeFormatAztec");
pass.barcodes("Message-22645272183").barcode(
"PKBarcodeFormatAztec",
);
expect(pass.props["barcode"].format).toBe("PKBarcodeFormatAztec");
@@ -277,8 +312,7 @@ describe("Passkit-generator", function () {
it("Won't apply changes if unknown format is passed", () => {
const oldBarcode = pass.props["barcode"] || undefined;
pass
.barcodes("Message-22645272183")
pass.barcodes("Message-22645272183")
// @ts-expect-error
.barcode("PKBingoBongoFormat");

View File

@@ -1,8 +1,6 @@
{
"spec_dir": "spec",
"spec_files": [
"**/*.js"
],
"spec_files": ["**/*.js"],
"stopSpecOnExpectationFailure": false,
"random": true
}

View File

@@ -9,7 +9,7 @@ describe("splitBufferBundle", () => {
"de.lproj/background@2x.png": zeroBuffer,
"it.lproj/thumbnail@2x.png": zeroBuffer,
"thumbnail@2x.png": zeroBuffer,
"background.png": zeroBuffer
"background.png": zeroBuffer,
};
const result = splitBufferBundle(payload);
@@ -25,11 +25,11 @@ describe("splitBufferBundle", () => {
},
"it.lproj": {
"thumbnail@2x.png": zeroBuffer,
}
},
});
expect(result[1]).toEqual({
"thumbnail@2x.png": zeroBuffer,
"background.png": zeroBuffer
"background.png": zeroBuffer,
});
});

View File

@@ -1,68 +1,75 @@
import { Certificates, FinalCertificates, PartitionedBundle, OverridesSupportedOptions, FactoryOptions } from "./schema";
import { getModelContents, readCertificatesFromOptions } from "./parser";
import formatMessage from "./messages";
const abmCertificates = Symbol("certificates");
const abmModel = Symbol("model");
const abmOverrides = Symbol("overrides");
export interface AbstractFactoryOptions extends Omit<FactoryOptions, "certificates"> {
certificates?: Certificates;
}
interface AbstractModelOptions {
bundle: PartitionedBundle;
certificates: FinalCertificates;
overrides?: OverridesSupportedOptions;
}
/**
* Creates an abstract model to keep data
* in memory for future passes creation
* @param options
*/
export async function createAbstractModel(options: AbstractFactoryOptions) {
if (!(options && Object.keys(options).length)) {
throw new Error(formatMessage("CP_NO_OPTS"));
}
try {
const [bundle, certificates] = await Promise.all([
getModelContents(options.model),
readCertificatesFromOptions(options.certificates)
]);
return new AbstractModel({
bundle,
certificates,
overrides: options.overrides
});
} catch (err) {
throw new Error(formatMessage("CP_INIT_ERROR", "abstract model", err));
}
}
export class AbstractModel {
private [abmCertificates]: FinalCertificates;
private [abmModel]: PartitionedBundle;
private [abmOverrides]: OverridesSupportedOptions;
constructor(options: AbstractModelOptions) {
this[abmModel] = options.bundle;
this[abmCertificates] = options.certificates;
this[abmOverrides] = options.overrides
}
get certificates(): FinalCertificates {
return this[abmCertificates];
}
get bundle(): PartitionedBundle {
return this[abmModel];
}
get overrides(): OverridesSupportedOptions {
return this[abmOverrides];
}
}
import {
Certificates,
FinalCertificates,
PartitionedBundle,
OverridesSupportedOptions,
FactoryOptions,
} from "./schema";
import { getModelContents, readCertificatesFromOptions } from "./parser";
import formatMessage from "./messages";
const abmCertificates = Symbol("certificates");
const abmModel = Symbol("model");
const abmOverrides = Symbol("overrides");
export interface AbstractFactoryOptions
extends Omit<FactoryOptions, "certificates"> {
certificates?: Certificates;
}
interface AbstractModelOptions {
bundle: PartitionedBundle;
certificates: FinalCertificates;
overrides?: OverridesSupportedOptions;
}
/**
* Creates an abstract model to keep data
* in memory for future passes creation
* @param options
*/
export async function createAbstractModel(options: AbstractFactoryOptions) {
if (!(options && Object.keys(options).length)) {
throw new Error(formatMessage("CP_NO_OPTS"));
}
try {
const [bundle, certificates] = await Promise.all([
getModelContents(options.model),
readCertificatesFromOptions(options.certificates),
]);
return new AbstractModel({
bundle,
certificates,
overrides: options.overrides,
});
} catch (err) {
throw new Error(formatMessage("CP_INIT_ERROR", "abstract model", err));
}
}
export class AbstractModel {
private [abmCertificates]: FinalCertificates;
private [abmModel]: PartitionedBundle;
private [abmOverrides]: OverridesSupportedOptions;
constructor(options: AbstractModelOptions) {
this[abmModel] = options.bundle;
this[abmCertificates] = options.certificates;
this[abmOverrides] = options.overrides;
}
get certificates(): FinalCertificates {
return this[abmCertificates];
}
get bundle(): PartitionedBundle {
return this[abmModel];
}
get overrides(): OverridesSupportedOptions {
return this[abmOverrides];
}
}

View File

@@ -1,5 +1,11 @@
import { Pass } from "./pass";
import { FactoryOptions, BundleUnit, FinalCertificates, PartitionedBundle, OverridesSupportedOptions } from "./schema";
import {
FactoryOptions,
BundleUnit,
FinalCertificates,
PartitionedBundle,
OverridesSupportedOptions,
} from "./schema";
import formatMessage from "./messages";
import { getModelContents, readCertificatesFromOptions } from "./parser";
import { splitBufferBundle } from "./utils";
@@ -16,9 +22,14 @@ import { AbstractModel, AbstractFactoryOptions } from "./abstract";
export async function createPass(
options: FactoryOptions | InstanceType<typeof AbstractModel>,
additionalBuffers?: BundleUnit,
abstractMissingData?: Omit<AbstractFactoryOptions, "model">
abstractMissingData?: Omit<AbstractFactoryOptions, "model">,
): Promise<Pass> {
if (!(options && (options instanceof AbstractModel || Object.keys(options).length))) {
if (
!(
options &&
(options instanceof AbstractModel || Object.keys(options).length)
)
) {
throw new Error(formatMessage("CP_NO_OPTS"));
}
@@ -27,35 +38,62 @@ export async function createPass(
let certificates: FinalCertificates;
let overrides: OverridesSupportedOptions = {
...(options.overrides || {}),
...(abstractMissingData && abstractMissingData.overrides || {})
...((abstractMissingData && abstractMissingData.overrides) ||
{}),
};
if (!(options.certificates && options.certificates.signerCert && options.certificates.signerKey) && abstractMissingData.certificates) {
if (
!(
options.certificates &&
options.certificates.signerCert &&
options.certificates.signerKey
) &&
abstractMissingData.certificates
) {
certificates = Object.assign(
options.certificates,
await readCertificatesFromOptions(abstractMissingData.certificates)
await readCertificatesFromOptions(
abstractMissingData.certificates,
),
);
} else {
certificates = options.certificates;
}
return createPassInstance(options.bundle, certificates, overrides, additionalBuffers);
return createPassInstance(
options.bundle,
certificates,
overrides,
additionalBuffers,
);
} else {
const [bundle, certificates] = await Promise.all([
getModelContents(options.model),
readCertificatesFromOptions(options.certificates)
readCertificatesFromOptions(options.certificates),
]);
return createPassInstance(bundle, certificates, options.overrides, additionalBuffers);
return createPassInstance(
bundle,
certificates,
options.overrides,
additionalBuffers,
);
}
} catch (err) {
throw new Error(formatMessage("CP_INIT_ERROR", "pass", err));
}
}
function createPassInstance(model: PartitionedBundle, certificates: FinalCertificates, overrides: OverridesSupportedOptions, additionalBuffers?: BundleUnit) {
function createPassInstance(
model: PartitionedBundle,
certificates: FinalCertificates,
overrides: OverridesSupportedOptions,
additionalBuffers?: BundleUnit,
) {
if (additionalBuffers) {
const [additionalL10n, additionalBundle] = splitBufferBundle(additionalBuffers);
const [additionalL10n, additionalBundle] = splitBufferBundle(
additionalBuffers,
);
Object.assign(model["l10nBundle"], additionalL10n);
Object.assign(model["bundle"], additionalBundle);
}
@@ -63,6 +101,6 @@ function createPassInstance(model: PartitionedBundle, certificates: FinalCertifi
return new Pass({
model,
certificates,
overrides
overrides,
});
}

View File

@@ -24,20 +24,28 @@ export default class FieldsArray extends Array {
*/
push(...fieldsData: schema.Field[]): number {
const validFields = fieldsData.reduce((acc: schema.Field[], current: schema.Field) => {
if (!(typeof current === "object") || !schema.isValid(current, "field")) {
const validFields = fieldsData.reduce(
(acc: schema.Field[], current: schema.Field) => {
if (
!(typeof current === "object") ||
!schema.isValid(current, "field")
) {
return acc;
}
if (this[poolSymbol].has(current.key)) {
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;
}
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);
}
@@ -58,9 +66,13 @@ export default class FieldsArray extends Array {
* also uniqueKeys set
*/
splice(start: number, deleteCount: number, ...items: schema.Field[]): schema.Field[] {
splice(
start: number,
deleteCount: number,
...items: schema.Field[]
): schema.Field[] {
const removeList = this.slice(start, deleteCount + start);
removeList.forEach(item => this[poolSymbol].delete(item.key));
removeList.forEach((item) => this[poolSymbol].delete(item.key));
return Array.prototype.splice.call(this, start, deleteCount, items);
}

View File

@@ -3,5 +3,5 @@ import { AbstractModel as AbstractModelClass } from "./abstract";
export { createPass } from "./factory";
export { createAbstractModel } from "./abstract";
export type Pass = InstanceType<typeof PassClass>
export type AbstractModel = InstanceType<typeof AbstractModelClass>
export type Pass = InstanceType<typeof PassClass>;
export type AbstractModel = InstanceType<typeof AbstractModelClass>;

View File

@@ -1,31 +1,51 @@
const errors = {
CP_INIT_ERROR: "Something went really bad in the %s initialization! Look at the log below this message. It should contain all the infos about the problem: \n%s",
CP_NO_OPTS: "Cannot initialize the pass or abstract model creation: no options were passed.",
CP_NO_CERTS: "Cannot initialize the pass creation: no valid certificates were passed.",
PASSFILE_VALIDATION_FAILED: "Validation of pass type failed. Pass file is not a valid buffer or (more probably) does not respect the schema.\nRefer to https://apple.co/2Nvshvn to build a correct pass.",
REQUIR_VALID_FAILED: "The options passed to Pass constructor does not meet the requirements.\nRefer to the documentation to compile them correctly.",
MODEL_UNINITIALIZED: "Provided model ( %s ) matched but unitialized or may not contain icon 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.",
CP_INIT_ERROR:
"Something went really bad in the %s initialization! Look at the log below this message. It should contain all the infos about the problem: \n%s",
CP_NO_OPTS:
"Cannot initialize the pass or abstract model creation: no options were passed.",
CP_NO_CERTS:
"Cannot initialize the pass creation: no valid certificates were passed.",
PASSFILE_VALIDATION_FAILED:
"Validation of pass type failed. Pass file is not a valid buffer or (more probably) does not respect the schema.\nRefer to https://apple.co/2Nvshvn to build a correct pass.",
REQUIR_VALID_FAILED:
"The options passed to Pass constructor does not meet the requirements.\nRefer to the documentation to compile them correctly.",
MODEL_UNINITIALIZED:
"Provided model ( %s ) matched but unitialized or may not contain icon 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_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."
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.",
};
const debugMessages = {
TRSTYPE_NOT_VALID: "Transit type changing rejected as not compliant with Apple Specifications. Transit type would become \"%s\" but should be in [PKTransitTypeAir, PKTransitTypeBoat, PKTransitTypeBus, PKTransitTypeGeneric, PKTransitTypeTrain]",
BRC_NOT_SUPPORTED: "Format not found among barcodes. Cannot set backward compatibility.",
BRC_FORMATTYPE_UNMATCH: "Format must be a string or null. Cannot set backward compatibility.",
BRC_AUTC_MISSING_DATA: "Unable to autogenerate barcodes. Data is not a string.",
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.",
TRSTYPE_NOT_VALID:
'Transit type changing rejected as not compliant with Apple Specifications. Transit type would become "%s" but should be in [PKTransitTypeAir, PKTransitTypeBoat, PKTransitTypeBus, PKTransitTypeGeneric, PKTransitTypeTrain]',
BRC_NOT_SUPPORTED:
"Format not found among barcodes. Cannot set backward compatibility.",
BRC_FORMATTYPE_UNMATCH:
"Format must be a string or null. Cannot set backward compatibility.",
BRC_AUTC_MISSING_DATA:
"Unable to autogenerate barcodes. Data is not a string.",
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."
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.",
};
type AllMessages = keyof (typeof debugMessages & typeof errors);

View File

@@ -1,8 +1,21 @@
import * as path from "path";
import forge from "node-forge";
import formatMessage from "./messages";
import { FactoryOptions, PartitionedBundle, BundleUnit, Certificates, FinalCertificates, isValid } from "./schema";
import { removeHidden, splitBufferBundle, getAllFilesWithName, hasFilesWithName, deletePersonalization } from "./utils";
import {
FactoryOptions,
PartitionedBundle,
BundleUnit,
Certificates,
FinalCertificates,
isValid,
} from "./schema";
import {
removeHidden,
splitBufferBundle,
getAllFilesWithName,
hasFilesWithName,
deletePersonalization,
} from "./utils";
import fs from "fs";
import debug from "debug";
@@ -27,10 +40,9 @@ export async function getModelContents(model: FactoryOptions["model"]) {
}
const modelFiles = Object.keys(modelContents.bundle);
const isModelInitialized = (
const isModelInitialized =
modelFiles.includes("pass.json") &&
hasFilesWithName("icon", modelFiles, "startsWith")
);
hasFilesWithName("icon", modelFiles, "startsWith");
if (!isModelInitialized) {
throw new Error(formatMessage("MODEL_UNINITIALIZED", "parse result"));
@@ -46,19 +58,34 @@ export async function getModelContents(model: FactoryOptions["model"]) {
return modelContents;
}
const logoFullNames = getAllFilesWithName("personalizationLogo", modelFiles, "startsWith");
if (!(logoFullNames.length && modelContents.bundle[personalizationJsonFile].length)) {
const logoFullNames = getAllFilesWithName(
"personalizationLogo",
modelFiles,
"startsWith",
);
if (
!(
logoFullNames.length &&
modelContents.bundle[personalizationJsonFile].length
)
) {
deletePersonalization(modelContents.bundle, logoFullNames);
return modelContents;
}
try {
const parsedPersonalization = JSON.parse(modelContents.bundle[personalizationJsonFile].toString("utf8"));
const isPersonalizationValid = isValid(parsedPersonalization, "personalizationDict");
const parsedPersonalization = JSON.parse(
modelContents.bundle[personalizationJsonFile].toString("utf8"),
);
const isPersonalizationValid = isValid(
parsedPersonalization,
"personalizationDict",
);
if (!isPersonalizationValid) {
[...logoFullNames, personalizationJsonFile]
.forEach(file => delete modelContents.bundle[file]);
[...logoFullNames, personalizationJsonFile].forEach(
(file) => delete modelContents.bundle[file],
);
return modelContents;
}
@@ -76,55 +103,70 @@ export async function getModelContents(model: FactoryOptions["model"]) {
* @param model
*/
export async function getModelFolderContents(model: string): Promise<PartitionedBundle> {
export async function getModelFolderContents(
model: string,
): Promise<PartitionedBundle> {
try {
const modelPath = `${model}${!path.extname(model) && ".pass" || ""}`;
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")
const filteredFiles = removeHidden(modelFilesList).filter(
(f) =>
!/(manifest|signature)/i.test(f) &&
/.+$/.test(path.parse(f).ext),
);
const isModelInitialized =
filteredFiles.length &&
hasFilesWithName("icon", filteredFiles, "startsWith");
// Icon is required to proceed
if (!isModelInitialized) {
throw new Error(formatMessage(
"MODEL_UNINITIALIZED",
path.parse(model).name
));
throw new Error(
formatMessage("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 rawBundleFiles = filteredFiles.filter(
(entry) => !entry.includes(".lproj"),
);
const l10nFolders = filteredFiles.filter((entry) =>
entry.includes(".lproj"),
);
const bundle: BundleUnit = Object.assign({},
...rawBundleFiles.map((fileName, index) => ({ [fileName]: rawBundleBuffers[index] }))
const rawBundleBuffers = await Promise.all(
rawBundleFiles.map((file) =>
readFile(path.resolve(modelPath, file)),
),
);
const bundle: BundleUnit = Object.assign(
{},
...rawBundleFiles.map((fileName, index) => ({
[fileName]: rawBundleBuffers[index],
})),
);
// Reading concurrently localizations folder
// and their files and their buffers
const L10N_FilesListByFolder: Array<BundleUnit> = await Promise.all(
l10nFolders.map(async folderPath => {
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));
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)))
validFiles.map((file) =>
readFile(file).catch(() => Buffer.alloc(0)),
),
);
// Assigning each file path to its buffer
@@ -140,34 +182,37 @@ export async function getModelFolderContents(model: string): Promise<Partitioned
return {
...acc,
[fileName]: buffers[index]
[fileName]: buffers[index],
};
}, {});
})
}),
);
const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign(
{},
...L10N_FilesListByFolder
.map((folder, index) => ({ [l10nFolders[index]]: folder }))
...L10N_FilesListByFolder.map((folder, index) => ({
[l10nFolders[index]]: folder,
})),
);
return {
bundle,
l10nBundle
l10nBundle,
};
} catch (err) {
if (err?.code === "ENOENT") {
if (err.syscall === "open") {
// file opening failed
throw new Error(formatMessage("MODELF_NOT_FOUND", err.path))
throw new Error(formatMessage("MODELF_NOT_FOUND", err.path));
} else if (err.syscall === "scandir") {
// directory reading failed
const pathContents = (err.path as string).split(/(\/|\\\?)/);
throw new Error(formatMessage(
"MODELF_FILE_NOT_FOUND",
pathContents[pathContents.length - 1]
))
throw new Error(
formatMessage(
"MODELF_FILE_NOT_FOUND",
pathContents[pathContents.length - 1],
),
);
}
}
@@ -182,27 +227,28 @@ export async function getModelFolderContents(model: string): Promise<Partitioned
*/
export function getModelBufferContents(model: BundleUnit): PartitionedBundle {
const rawBundle = removeHidden(Object.keys(model)).reduce<BundleUnit>((acc, current) => {
// Checking if current file is one of the autogenerated ones or if its
// content is not available
const rawBundle = removeHidden(Object.keys(model)).reduce<BundleUnit>(
(acc, current) => {
// Checking if current file is one of the autogenerated ones or if its
// content is not available
if (/(manifest|signature)/.test(current) || !model[current]) {
return acc;
}
if (/(manifest|signature)/.test(current) || !model[current]) {
return acc;
}
return { ...acc, [current]: model[current] };
}, {});
return { ...acc, [current]: model[current] };
},
{},
);
const bundleKeys = Object.keys(rawBundle);
const isModelInitialized = (
bundleKeys.length &&
hasFilesWithName("icon", bundleKeys, "startsWith")
);
const isModelInitialized =
bundleKeys.length && hasFilesWithName("icon", bundleKeys, "startsWith");
// Icon is required to proceed
if (!isModelInitialized) {
throw new Error(formatMessage("MODEL_UNINITIALIZED", "Buffers"))
throw new Error(formatMessage("MODEL_UNINITIALIZED", "Buffers"));
}
// separing localization folders from bundle files
@@ -210,7 +256,7 @@ export function getModelBufferContents(model: BundleUnit): PartitionedBundle {
return {
bundle,
l10nBundle
l10nBundle,
};
}
@@ -224,8 +270,16 @@ type flatCertificates = Omit<Certificates, "signerKey"> & {
signerKey: string;
};
export async function readCertificatesFromOptions(options: Certificates): Promise<FinalCertificates> {
if (!(options && Object.keys(options).length && isValid(options, "certificatesSchema"))) {
export async function readCertificatesFromOptions(
options: Certificates,
): Promise<FinalCertificates> {
if (
!(
options &&
Object.keys(options).length &&
isValid(options, "certificatesSchema")
)
) {
throw new Error(formatMessage("CP_NO_CERTS"));
}
@@ -239,30 +293,31 @@ export async function readCertificatesFromOptions(options: Certificates): Promis
// 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;
const flattenedDocs = Object.assign({}, options, {
signerKey,
}) as flatCertificates;
// We read the contents
const rawContentsPromises = Object.keys(flattenedDocs)
.map(key => {
const content = flattenedDocs[key];
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);
}
});
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 passphrase =
(typeof options.signerKey === "object" &&
options.signerKey?.passphrase) ||
undefined;
const pem = parsePEM(certName, file, passphrase);
@@ -279,7 +334,9 @@ export async function readCertificatesFromOptions(options: Certificates): Promis
throw err;
}
throw new Error(formatMessage("INVALID_CERT_PATH", path.parse(err.path).base));
throw new Error(
formatMessage("INVALID_CERT_PATH", path.parse(err.path).base),
);
}
}

View File

@@ -7,7 +7,13 @@ import { ZipFile } from "yazl";
import * as schema from "./schema";
import formatMessage from "./messages";
import FieldsArray from "./fieldsArray";
import { generateStringFile, dateToW3CString, isValidRGB, deletePersonalization, getAllFilesWithName } from "./utils";
import {
generateStringFile,
dateToW3CString,
isValidRGB,
deletePersonalization,
getAllFilesWithName,
} from "./utils";
const barcodeDebug = debug("passkit:barcode");
const genericDebug = debug("passkit:generic");
@@ -20,7 +26,7 @@ const propsSchemaMap = new Map<string, schema.Schema>([
["barcode", "barcode"],
["beacons", "beaconsDict"],
["locations", "locationsDict"],
["nfc", "nfcDict"]
["nfc", "nfcDict"],
]);
export class Pass {
@@ -42,7 +48,9 @@ export class Pass {
private Certificates: schema.FinalCertificates;
private [transitType]: string = "";
private l10nTranslations: { [languageCode: string]: { [placeholder: string]: string } } = {};
private l10nTranslations: {
[languageCode: string]: { [placeholder: string]: string };
} = {};
constructor(options: schema.PassInstance) {
if (!schema.isValid(options, "instance")) {
@@ -54,77 +62,101 @@ export class Pass {
this.bundle = { ...options.model.bundle };
try {
this.passCore = JSON.parse(this.bundle["pass.json"].toString("utf8"));
this.passCore = JSON.parse(
this.bundle["pass.json"].toString("utf8"),
);
} catch (err) {
throw new Error(formatMessage("PASSFILE_VALIDATION_FAILED"));
}
// Parsing the options and extracting only the valid ones.
const validOverrides = schema.getValidated(options.overrides || {}, "supportedOptions") as schema.OverridesSupportedOptions;
const validOverrides = schema.getValidated(
options.overrides || {},
"supportedOptions",
) as schema.OverridesSupportedOptions;
if (validOverrides === null) {
throw new Error(formatMessage("OVV_KEYS_BADFORMAT"))
throw new Error(formatMessage("OVV_KEYS_BADFORMAT"));
}
this.type = Object.keys(this.passCore)
.find(key => /(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key)) as keyof schema.ValidPassType;
this.type = Object.keys(this.passCore).find((key) =>
/(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key),
) as keyof schema.ValidPassType;
if (!this.type) {
throw new Error(formatMessage("NO_PASS_TYPE"));
}
// Parsing and validating pass.json keys
const passCoreKeys = Object.keys(this.passCore) as (keyof schema.ValidPass)[];
const validatedPassKeys = passCoreKeys.reduce<schema.ValidPass>((acc, current) => {
if (this.type === current) {
// We want to exclude type keys (eventTicket,
// boardingPass, ecc.) and their content
return acc;
}
const passCoreKeys = Object.keys(
this.passCore,
) as (keyof schema.ValidPass)[];
const validatedPassKeys = passCoreKeys.reduce<schema.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] };
}
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)!;
const currentSchema = propsSchemaMap.get(current)!;
if (Array.isArray(this.passCore[current])) {
const valid = getValidInArray<schema.ArrayPassSchema>(
currentSchema,
this.passCore[current] as schema.ArrayPassSchema[]
);
return { ...acc, [current]: valid };
} else {
return {
...acc,
[current]: schema.isValid(
this.passCore[current],
currentSchema
) && this.passCore[current] || undefined
};
}
}, {});
if (Array.isArray(this.passCore[current])) {
const valid = getValidInArray<schema.ArrayPassSchema>(
currentSchema,
this.passCore[current] as schema.ArrayPassSchema[],
);
return { ...acc, [current]: valid };
} else {
return {
...acc,
[current]:
(schema.isValid(
this.passCore[current],
currentSchema,
) &&
this.passCore[current]) ||
undefined,
};
}
},
{},
);
this[passProps] = {
...(validatedPassKeys || {}),
...(validOverrides || {})
...(validOverrides || {}),
};
if (this.type === "boardingPass" && this.passCore[this.type]["transitType"]) {
if (
this.type === "boardingPass" &&
this.passCore[this.type]["transitType"]
) {
// We might want to generate a boarding pass without setting manually
// in the code the transit type but right in the model;
this[transitType] = this.passCore[this.type]["transitType"];
}
this._fields = ["primaryFields", "secondaryFields", "auxiliaryFields", "backFields", "headerFields"];
this._fields.forEach(fieldName => {
this._fields = [
"primaryFields",
"secondaryFields",
"auxiliaryFields",
"backFields",
"headerFields",
];
this._fields.forEach((fieldName) => {
this[fieldName] = new FieldsArray(
this.fieldsKeys,
...(this.passCore[this.type][fieldName] || [])
.filter(field => schema.isValid(field, "field"))
...(this.passCore[this.type][fieldName] || []).filter((field) =>
schema.isValid(field, "field"),
),
);
});
}
@@ -146,13 +178,19 @@ export class Pass {
*/
const currentBundleFiles = Object.keys(this.bundle);
if (!this[passProps].nfc && currentBundleFiles.includes("personalization.json")) {
if (
!this[passProps].nfc &&
currentBundleFiles.includes("personalization.json")
) {
genericDebug(formatMessage("PRS_REMOVED"));
deletePersonalization(this.bundle, getAllFilesWithName(
"personalizationLogo",
currentBundleFiles,
"startsWith"
));
deletePersonalization(
this.bundle,
getAllFilesWithName(
"personalizationLogo",
currentBundleFiles,
"startsWith",
),
);
}
const finalBundle = { ...this.bundle } as schema.BundleUnit;
@@ -161,7 +199,7 @@ export class Pass {
* Iterating through languages and generating pass.string file
*/
Object.keys(this.l10nTranslations).forEach(lang => {
Object.keys(this.l10nTranslations).forEach((lang) => {
const strings = generateStringFile(this.l10nTranslations[lang]);
const langInBundles = `${lang}.lproj`;
@@ -176,13 +214,21 @@ export class Pass {
this.l10nBundles[langInBundles] = {};
}
this.l10nBundles[langInBundles]["pass.strings"] = Buffer.concat([
this.l10nBundles[langInBundles]["pass.strings"] || Buffer.alloc(0),
strings
this.l10nBundles[langInBundles][
"pass.strings"
] = Buffer.concat([
this.l10nBundles[langInBundles]["pass.strings"] ||
Buffer.alloc(0),
strings,
]);
}
if (!(this.l10nBundles[langInBundles] && Object.keys(this.l10nBundles[langInBundles]).length)) {
if (
!(
this.l10nBundles[langInBundles] &&
Object.keys(this.l10nBundles[langInBundles]).length
)
) {
return;
}
@@ -194,33 +240,48 @@ export class Pass {
* composition.
*/
Object.assign(finalBundle, ...Object.keys(this.l10nBundles[langInBundles])
.map(fileName => {
const fullPath = path.join(langInBundles, fileName).replace(/\\/, "/");
return { [fullPath]: this.l10nBundles[langInBundles][fileName] };
})
Object.assign(
finalBundle,
...Object.keys(this.l10nBundles[langInBundles]).map(
(fileName) => {
const fullPath = path
.join(langInBundles, fileName)
.replace(/\\/, "/");
return {
[fullPath]: this.l10nBundles[langInBundles][
fileName
],
};
},
),
);
});
/*
* Parsing the buffers, pushing them into the archive
* and returning the compiled manifest
*/
* Parsing the buffers, pushing them into the archive
* and returning the compiled manifest
*/
const archive = new ZipFile();
const manifest = Object.keys(finalBundle).reduce<schema.Manifest>((acc, current) => {
let hashFlow = forge.md.sha1.create();
const manifest = Object.keys(finalBundle).reduce<schema.Manifest>(
(acc, current) => {
let hashFlow = forge.md.sha1.create();
hashFlow.update(finalBundle[current].toString("binary"));
archive.addBuffer(finalBundle[current], current);
acc[current] = hashFlow.digest().toHex();
hashFlow.update(finalBundle[current].toString("binary"));
archive.addBuffer(finalBundle[current], current);
acc[current] = hashFlow.digest().toHex();
return acc;
}, {});
return acc;
},
{},
);
const signatureBuffer = this._sign(manifest);
archive.addBuffer(signatureBuffer, "signature");
archive.addBuffer(Buffer.from(JSON.stringify(manifest)), "manifest.json");
archive.addBuffer(
Buffer.from(JSON.stringify(manifest)),
"manifest.json",
);
const passStream = new Stream.PassThrough();
archive.outputStream.pipe(passStream);
@@ -242,8 +303,15 @@ export class Pass {
* @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)) {
localize(
lang: string,
translations?: { [placeholder: string]: string },
): this {
if (
lang &&
typeof lang === "string" &&
(typeof translations === "object" || translations === undefined)
) {
this.l10nTranslations[lang] = translations || {};
}
@@ -292,7 +360,7 @@ export class Pass {
*/
beacons(resetFlag: null): this;
beacons(...data: schema.Beacon[]): this
beacons(...data: schema.Beacon[]): this;
beacons(...data: (schema.Beacon | null)[]): this {
if (data[0] === null) {
delete this[passProps]["beacons"];
@@ -322,7 +390,10 @@ export class Pass {
return this;
}
const valid = processRelevancySet("locations", data as schema.Location[]);
const valid = processRelevancySet(
"locations",
data as schema.Location[],
);
if (valid.length) {
this[passProps]["locations"] = valid;
@@ -390,19 +461,28 @@ export class Pass {
* Validation assign default value to missing parameters (if any).
*/
const validBarcodes = data.reduce<schema.Barcode[]>((acc, current) => {
if (!(current && current instanceof Object)) {
return acc;
}
const validBarcodes = data.reduce<schema.Barcode[]>(
(acc, current) => {
if (!(current && current instanceof Object)) {
return acc;
}
const validated = schema.getValidated(current, "barcode");
const validated = schema.getValidated(current, "barcode");
if (!(validated && validated instanceof Object && Object.keys(validated).length)) {
return acc;
}
if (
!(
validated &&
validated instanceof Object &&
Object.keys(validated).length
)
) {
return acc;
}
return [...acc, validated] as schema.Barcode[];
}, []);
return [...acc, validated] as schema.Barcode[];
},
[],
);
if (validBarcodes.length) {
this[passProps]["barcodes"] = validBarcodes;
@@ -446,7 +526,9 @@ export class Pass {
}
// Checking which object among barcodes has the same format of the specified one.
const index = barcodes.findIndex(b => b.format.toLowerCase().includes(chosenFormat.toLowerCase()));
const index = barcodes.findIndex((b) =>
b.format.toLowerCase().includes(chosenFormat.toLowerCase()),
);
if (index === -1) {
barcodeDebug(formatMessage("BRC_NOT_SUPPORTED"));
@@ -472,7 +554,14 @@ export class Pass {
return this;
}
if (!(data && typeof data === "object" && !Array.isArray(data) && schema.isValid(data, "nfcDict"))) {
if (
!(
data &&
typeof data === "object" &&
!Array.isArray(data) &&
schema.isValid(data, "nfcDict")
)
) {
genericDebug(formatMessage("NFC_INVALID"));
return this;
}
@@ -505,7 +594,10 @@ export class Pass {
private _sign(manifest: schema.Manifest): Buffer {
const signature = forge.pkcs7.createSignedData();
signature.content = forge.util.createBuffer(JSON.stringify(manifest), "utf8");
signature.content = forge.util.createBuffer(
JSON.stringify(manifest),
"utf8",
);
signature.addCertificate(this.Certificates.wwdr);
signature.addCertificate(this.Certificates.signerCert);
@@ -523,14 +615,18 @@ export class Pass {
key: this.Certificates.signerKey,
certificate: this.Certificates.signerCert,
digestAlgorithm: forge.pki.oids.sha1,
authenticatedAttributes: [{
type: forge.pki.oids.contentType,
value: forge.pki.oids.data
}, {
type: forge.pki.oids.messageDigest,
}, {
type: forge.pki.oids.signingTime,
}]
authenticatedAttributes: [
{
type: forge.pki.oids.contentType,
value: forge.pki.oids.data,
},
{
type: forge.pki.oids.messageDigest,
},
{
type: forge.pki.oids.signingTime,
},
],
});
/**
@@ -554,7 +650,10 @@ export class Pass {
* of beautiful things. ¯\_(ツ)_/¯
*/
return Buffer.from(forge.asn1.toDer(signature.toAsn1()).getBytes(), "binary");
return Buffer.from(
forge.asn1.toDer(signature.toAsn1()).getBytes(),
"binary",
);
}
/**
@@ -566,7 +665,9 @@ export class Pass {
*/
private _patch(passCoreBuffer: Buffer): Buffer {
const passFile = JSON.parse(passCoreBuffer.toString()) as schema.ValidPass;
const passFile = JSON.parse(
passCoreBuffer.toString(),
) as schema.ValidPass;
if (Object.keys(this[passProps]).length) {
/*
@@ -575,14 +676,22 @@ export class Pass {
* and then delete it from the passFile.
*/
const passColors = ["backgroundColor", "foregroundColor", "labelColor"] as Array<keyof schema.PassColors>;
passColors.filter(v => this[passProps][v] && !isValidRGB(this[passProps][v]))
.forEach(v => delete this[passProps][v]);
const passColors = [
"backgroundColor",
"foregroundColor",
"labelColor",
] as Array<keyof schema.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 => {
this._fields.forEach((field) => {
passFile[this.type][field] = this[field];
});
@@ -627,8 +736,14 @@ function barcodesFromUncompleteData(message: string): schema.Barcode[] {
"PKBarcodeFormatQR",
"PKBarcodeFormatPDF417",
"PKBarcodeFormatAztec",
"PKBarcodeFormatCode128"
].map(format => schema.getValidated({ format, message }, "barcode") as schema.Barcode);
"PKBarcodeFormatCode128",
].map(
(format) =>
schema.getValidated(
{ format, message },
"barcode",
) as schema.Barcode,
);
}
function processRelevancySet<T>(key: string, data: T[]): T[] {
@@ -636,7 +751,10 @@ function processRelevancySet<T>(key: string, data: T[]): T[] {
}
function getValidInArray<T>(schemaName: schema.Schema, contents: T[]): T[] {
return contents.filter(current => Object.keys(current).length && schema.isValid(current, schemaName));
return contents.filter(
(current) =>
Object.keys(current).length && schema.isValid(current, schemaName),
);
}
function processDate(key: string, date: Date): string | null {

View File

@@ -10,10 +10,12 @@ export interface Manifest {
export interface Certificates {
wwdr?: string;
signerCert?: string;
signerKey?: {
keyFile: string;
passphrase?: string;
} | string;
signerKey?:
| {
keyFile: string;
passphrase?: string;
}
| string;
}
export interface FactoryOptions {
@@ -29,7 +31,7 @@ export interface BundleUnit {
export interface PartitionedBundle {
bundle: BundleUnit;
l10nBundle: {
[key: string]: BundleUnit
[key: string]: BundleUnit;
};
}
@@ -49,17 +51,24 @@ export interface PassInstance {
// * JOI Schemas + Related Interfaces * //
// ************************************ //
const certificatesSchema = Joi.object().keys({
wwdr: Joi.alternatives(Joi.binary(), Joi.string()).required(),
signerCert: Joi.alternatives(Joi.binary(), Joi.string()).required(),
signerKey: Joi.alternatives().try(
Joi.object().keys({
keyFile: Joi.alternatives(Joi.binary(), Joi.string()).required(),
passphrase: Joi.string().required(),
}),
Joi.alternatives(Joi.binary(), Joi.string())
).required()
}).required();
const certificatesSchema = Joi.object()
.keys({
wwdr: Joi.alternatives(Joi.binary(), Joi.string()).required(),
signerCert: Joi.alternatives(Joi.binary(), Joi.string()).required(),
signerKey: Joi.alternatives()
.try(
Joi.object().keys({
keyFile: Joi.alternatives(
Joi.binary(),
Joi.string(),
).required(),
passphrase: Joi.string().required(),
}),
Joi.alternatives(Joi.binary(), Joi.string()),
)
.required(),
})
.required();
const instance = Joi.object().keys({
model: Joi.alternatives(Joi.object(), Joi.string()).required(),
@@ -88,28 +97,31 @@ export interface OverridesSupportedOptions {
maxDistance?: number;
}
const supportedOptions = Joi.object().keys({
serialNumber: Joi.string(),
description: Joi.string(),
organizationName: Joi.string(),
passTypeIdentifier: Joi.string(),
teamIdentifier: Joi.string(),
appLaunchURL: Joi.string(),
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]+)*/),
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(),
}).with("webServiceURL", "authenticationToken");
const supportedOptions = Joi.object()
.keys({
serialNumber: Joi.string(),
description: Joi.string(),
organizationName: Joi.string(),
passTypeIdentifier: Joi.string(),
teamIdentifier: Joi.string(),
appLaunchURL: Joi.string(),
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]+)*/,
),
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(),
})
.with("webServiceURL", "authenticationToken");
/* For a correct usage of semantics, please refer to https://apple.co/2I66Phk */
@@ -130,7 +142,7 @@ interface PersonNameComponent {
const personNameComponents = Joi.object().keys({
givenName: Joi.string().required(),
familyName: Joi.string().required()
familyName: Joi.string().required(),
});
interface Seat {
@@ -148,12 +160,12 @@ const seat = Joi.object().keys({
seatNumber: Joi.string(),
seatIdentifier: Joi.string(),
seatType: Joi.string(),
seatDescription: Joi.string()
seatDescription: Joi.string(),
});
const location = Joi.object().keys({
latitude: Joi.number().required(),
longitude: Joi.number().required()
longitude: Joi.number().required(),
});
interface Semantics {
@@ -201,7 +213,15 @@ interface Semantics {
venueEntrance?: string;
venuePhoneNumber?: string;
venueRoom?: string;
eventType?: "PKEventTypeGeneric" | "PKEventTypeLivePerformance" | "PKEventTypeMovie" | "PKEventTypeSports" | "PKEventTypeConference" | "PKEventTypeConvention" | "PKEventTypeWorkshop" | "PKEventTypeSocialGathering";
eventType?:
| "PKEventTypeGeneric"
| "PKEventTypeLivePerformance"
| "PKEventTypeMovie"
| "PKEventTypeSports"
| "PKEventTypeConference"
| "PKEventTypeConvention"
| "PKEventTypeWorkshop"
| "PKEventTypeSocialGathering";
eventStartDate?: string;
eventEndDate?: string;
artistIDs?: string;
@@ -270,7 +290,9 @@ const semantics = Joi.object().keys({
venueEntrance: Joi.string(),
venuePhoneNumber: Joi.string(),
venueRoom: Joi.string(),
eventType: Joi.string().regex(/(PKEventTypeGeneric|PKEventTypeLivePerformance|PKEventTypeMovie|PKEventTypeSports|PKEventTypeConference|PKEventTypeConvention|PKEventTypeWorkshop|PKEventTypeSocialGathering)/),
eventType: Joi.string().regex(
/(PKEventTypeGeneric|PKEventTypeLivePerformance|PKEventTypeMovie|PKEventTypeSports|PKEventTypeConference|PKEventTypeConvention|PKEventTypeWorkshop|PKEventTypeSocialGathering)/,
),
eventStartDate: Joi.string(),
eventEndDate: Joi.string(),
artistIDs: Joi.string(),
@@ -287,7 +309,7 @@ const semantics = Joi.object().keys({
awayTeamAbbreviation: Joi.string(),
sportName: Joi.string(),
// Store Card Passes
balance: currencyAmount
balance: currencyAmount,
});
export interface ValidPassType {
@@ -310,11 +332,16 @@ interface PassInterfacesProps {
voided?: boolean;
}
type AllPassProps = PassInterfacesProps & ValidPassType & OverridesSupportedOptions;
type AllPassProps = PassInterfacesProps &
ValidPassType &
OverridesSupportedOptions;
export type ValidPass = {
[K in keyof AllPassProps]: AllPassProps[K];
};
export type PassColors = Pick<OverridesSupportedOptions, "backgroundColor" | "foregroundColor" | "labelColor">;
export type PassColors = Pick<
OverridesSupportedOptions,
"backgroundColor" | "foregroundColor" | "labelColor"
>;
export interface Barcode {
altText?: string;
@@ -323,13 +350,22 @@ export interface Barcode {
message: string;
}
export type BarcodeFormat = "PKBarcodeFormatQR" | "PKBarcodeFormatPDF417" | "PKBarcodeFormatAztec" | "PKBarcodeFormatCode128";
export type BarcodeFormat =
| "PKBarcodeFormatQR"
| "PKBarcodeFormatPDF417"
| "PKBarcodeFormatAztec"
| "PKBarcodeFormatCode128";
const barcode = Joi.object().keys({
altText: Joi.string(),
messageEncoding: Joi.string().default("iso-8859-1"),
format: Joi.string().required().regex(/(PKBarcodeFormatQR|PKBarcodeFormatPDF417|PKBarcodeFormatAztec|PKBarcodeFormatCode128)/, "barcodeType"),
message: Joi.string().required()
format: Joi.string()
.required()
.regex(
/(PKBarcodeFormatQR|PKBarcodeFormatPDF417|PKBarcodeFormatAztec|PKBarcodeFormatCode128)/,
"barcodeType",
),
message: Joi.string().required(),
});
export interface Field {
@@ -350,30 +386,53 @@ export interface Field {
}
const field = Joi.object().keys({
attributedValue: Joi.alternatives(Joi.string().allow(""), Joi.number(), Joi.date().iso()),
attributedValue: Joi.alternatives(
Joi.string().allow(""),
Joi.number(),
Joi.date().iso(),
),
changeMessage: Joi.string(),
dataDetectorType: Joi.array().items(Joi.string().regex(/(PKDataDetectorTypePhoneNumber|PKDataDetectorTypeLink|PKDataDetectorTypeAddress|PKDataDetectorTypeCalendarEvent)/, "dataDetectorType")),
dataDetectorType: Joi.array().items(
Joi.string().regex(
/(PKDataDetectorTypePhoneNumber|PKDataDetectorTypeLink|PKDataDetectorTypeAddress|PKDataDetectorTypeCalendarEvent)/,
"dataDetectorType",
),
),
label: Joi.string().allow(""),
textAlignment: Joi.string().regex(/(PKTextAlignmentLeft|PKTextAlignmentCenter|PKTextAlignmentRight|PKTextAlignmentNatural)/, "graphic-alignment"),
textAlignment: Joi.string().regex(
/(PKTextAlignmentLeft|PKTextAlignmentCenter|PKTextAlignmentRight|PKTextAlignmentNatural)/,
"graphic-alignment",
),
key: Joi.string().required(),
value: Joi.alternatives(Joi.string().allow(""), Joi.number(), Joi.date().iso()).required(),
value: Joi.alternatives(
Joi.string().allow(""),
Joi.number(),
Joi.date().iso(),
).required(),
semantics,
// date fields formatters, all optionals
dateStyle: Joi.string().regex(/(PKDateStyleNone|PKDateStyleShort|PKDateStyleMedium|PKDateStyleLong|PKDateStyleFull)/, "date style"),
dateStyle: Joi.string().regex(
/(PKDateStyleNone|PKDateStyleShort|PKDateStyleMedium|PKDateStyleLong|PKDateStyleFull)/,
"date style",
),
ignoreTimeZone: Joi.boolean(),
isRelative: Joi.boolean(),
timeStyle: Joi.string().regex(/(PKDateStyleNone|PKDateStyleShort|PKDateStyleMedium|PKDateStyleLong|PKDateStyleFull)/, "date style"),
timeStyle: Joi.string().regex(
/(PKDateStyleNone|PKDateStyleShort|PKDateStyleMedium|PKDateStyleLong|PKDateStyleFull)/,
"date style",
),
// number fields formatters, all optionals
currencyCode: Joi.string()
.when("value", {
is: Joi.number(),
otherwise: Joi.string().forbidden()
}),
currencyCode: Joi.string().when("value", {
is: Joi.number(),
otherwise: Joi.string().forbidden(),
}),
numberStyle: Joi.string()
.regex(/(PKNumberStyleDecimal|PKNumberStylePercent|PKNumberStyleScientific|PKNumberStyleSpellOut)/)
.regex(
/(PKNumberStyleDecimal|PKNumberStylePercent|PKNumberStyleScientific|PKNumberStyleSpellOut)/,
)
.when("value", {
is: Joi.number(),
otherwise: Joi.string().forbidden()
otherwise: Joi.string().forbidden(),
}),
});
@@ -385,10 +444,14 @@ export interface Beacon {
}
const beaconsDict = Joi.object().keys({
major: Joi.number().integer().positive().max(65535).greater(Joi.ref("minor")),
major: Joi.number()
.integer()
.positive()
.max(65535)
.greater(Joi.ref("minor")),
minor: Joi.number().integer().min(0).max(65535),
proximityUUID: Joi.string().required(),
relevantText: Joi.string()
relevantText: Joi.string(),
});
export interface Location {
@@ -402,7 +465,7 @@ const locationsDict = Joi.object().keys({
altitude: Joi.number(),
latitude: Joi.number().required(),
longitude: Joi.number().required(),
relevantText: Joi.string()
relevantText: Joi.string(),
});
export interface PassFields {
@@ -414,18 +477,29 @@ export interface PassFields {
}
const passDict = Joi.object().keys({
auxiliaryFields: Joi.array().items(Joi.object().keys({
row: Joi.number().max(1).min(0)
}).concat(field)),
auxiliaryFields: Joi.array().items(
Joi.object()
.keys({
row: Joi.number().max(1).min(0),
})
.concat(field),
),
backFields: Joi.array().items(field),
headerFields: Joi.array().items(field),
primaryFields: Joi.array().items(field),
secondaryFields: Joi.array().items(field)
secondaryFields: Joi.array().items(field),
});
export type TransitType = "PKTransitTypeAir" | "PKTransitTypeBoat" | "PKTransitTypeBus" | "PKTransitTypeGeneric" | "PKTransitTypeTrain";
export type TransitType =
| "PKTransitTypeAir"
| "PKTransitTypeBoat"
| "PKTransitTypeBus"
| "PKTransitTypeGeneric"
| "PKTransitTypeTrain";
const transitType = Joi.string().regex(/(PKTransitTypeAir|PKTransitTypeBoat|PKTransitTypeBus|PKTransitTypeGeneric|PKTransitTypeTrain)/);
const transitType = Joi.string().regex(
/(PKTransitTypeAir|PKTransitTypeBoat|PKTransitTypeBus|PKTransitTypeGeneric|PKTransitTypeTrain)/,
);
export interface NFC {
message: string;
@@ -434,7 +508,7 @@ export interface NFC {
const nfcDict = Joi.object().keys({
message: Joi.string().required().max(64),
encryptionPublicKey: Joi.string()
encryptionPublicKey: Joi.string(),
});
// ************************************* //
@@ -447,11 +521,20 @@ export interface Personalization {
termsAndConditions?: string;
}
type PRSField = "PKPassPersonalizationFieldName" | "PKPassPersonalizationFieldPostalCode" | "PKPassPersonalizationFieldEmailAddress" | "PKPassPersonalizationFieldPhoneNumber";
type PRSField =
| "PKPassPersonalizationFieldName"
| "PKPassPersonalizationFieldPostalCode"
| "PKPassPersonalizationFieldEmailAddress"
| "PKPassPersonalizationFieldPhoneNumber";
const personalizationDict = Joi.object().keys({
requiredPersonalizationFields: Joi.array()
.items("PKPassPersonalizationFieldName", "PKPassPersonalizationFieldPostalCode", "PKPassPersonalizationFieldEmailAddress", "PKPassPersonalizationFieldPhoneNumber")
.items(
"PKPassPersonalizationFieldName",
"PKPassPersonalizationFieldPostalCode",
"PKPassPersonalizationFieldEmailAddress",
"PKPassPersonalizationFieldPhoneNumber",
)
.required(),
description: Joi.string().required(),
termsAndConditions: Joi.string(),
@@ -470,7 +553,7 @@ const schemas = {
transitType,
nfcDict,
supportedOptions,
personalizationDict
personalizationDict,
};
export type Schema = keyof typeof schemas;
@@ -491,14 +574,18 @@ export function isValid(opts: any, schemaName: Schema): boolean {
const resolvedSchema = resolveSchemaName(schemaName);
if (!resolvedSchema) {
schemaDebug(`validation failed due to missing or mispelled schema name`);
schemaDebug(
`validation failed due to missing or mispelled schema name`,
);
return false;
}
const validation = resolvedSchema.validate(opts);
if (validation.error) {
schemaDebug(`validation failed due to error: ${validation.error.message}`);
schemaDebug(
`validation failed due to error: ${validation.error.message}`,
);
}
return !validation.error;
@@ -511,18 +598,25 @@ export function isValid(opts: any, schemaName: Schema): boolean {
* @returns {object} the filtered value or empty object
*/
export function getValidated<T extends Object>(opts: any, schemaName: Schema): T | null {
export function getValidated<T extends Object>(
opts: any,
schemaName: Schema,
): T | null {
const resolvedSchema = resolveSchemaName(schemaName);
if (!resolvedSchema) {
schemaDebug(`validation failed due to missing or mispelled schema name`);
schemaDebug(
`validation failed due to missing or mispelled schema name`,
);
return null;
}
const validation = resolvedSchema.validate(opts, { stripUnknown: true });
if (validation.error) {
schemaDebug(`Validation failed in getValidated due to error: ${validation.error.message}`);
schemaDebug(
`Validation failed in getValidated due to error: ${validation.error.message}`,
);
return null;
}

View File

@@ -16,13 +16,15 @@ export function isValidRGB(value?: string): boolean {
return false;
}
const rgb = value.match(/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/);
const rgb = value.match(
/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/,
);
if (!rgb) {
return false;
}
return rgb.slice(1, 4).every(v => Math.abs(Number(v)) <= 255);
return rgb.slice(1, 4).every((v) => Math.abs(Number(v)) <= 255);
}
/**
@@ -57,7 +59,7 @@ export function dateToW3CString(date: Date) {
*/
export function removeHidden(from: Array<string>): Array<string> {
return from.filter(e => e.charAt(0) !== ".");
return from.filter((e) => e.charAt(0) !== ".");
}
/**
@@ -77,8 +79,9 @@ export function generateStringFile(lang: { [index: string]: string }): Buffer {
// Pass.strings format is the following one for each row:
// "key" = "value";
const strings = Object.keys(lang)
.map(key => `"${key}" = "${lang[key].replace(/"/g, '\"')}";`);
const strings = Object.keys(lang).map(
(key) => `"${key}" = "${lang[key].replace(/"/g, '"')}";`,
);
return Buffer.from(strings.join(EOL), "utf8");
}
@@ -89,47 +92,73 @@ export function generateStringFile(lang: { [index: string]: string }): Buffer {
* @param origin
*/
type PartitionedBundleElements = [PartitionedBundle["l10nBundle"], PartitionedBundle["bundle"]];
type PartitionedBundleElements = [
PartitionedBundle["l10nBundle"],
PartitionedBundle["bundle"],
];
export function splitBufferBundle(origin: BundleUnit): PartitionedBundleElements {
export function splitBufferBundle(
origin: 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
}
];
}
return Object.entries(origin).reduce<PartitionedBundleElements>(
([l10n, bundle], [key, buffer]) => {
if (!key.includes(".lproj")) {
return [
l10n,
{
...bundle,
[key]: buffer,
},
];
}
const pathComponents = key.split(sep);
const lang = pathComponents[0];
const file = pathComponents.slice(1).join("/");
const pathComponents = key.split(sep);
const lang = pathComponents[0];
const file = pathComponents.slice(1).join("/");
(l10n[lang] || (l10n[lang] = {}))[file] = buffer;
(l10n[lang] || (l10n[lang] = {}))[file] = buffer;
return [l10n, bundle];
}, initialValue);
return [l10n, bundle];
},
initialValue,
);
}
type StringSearchMode = "includes" | "startsWith" | "endsWith";
export function getAllFilesWithName(name: string, source: string[], mode: StringSearchMode = "includes", forceLowerCase: boolean = false): string[] {
return source.filter(file => (forceLowerCase && file.toLowerCase() || file)[mode](name));
export function getAllFilesWithName(
name: string,
source: string[],
mode: StringSearchMode = "includes",
forceLowerCase: boolean = false,
): string[] {
return source.filter((file) =>
((forceLowerCase && file.toLowerCase()) || file)[mode](name),
);
}
export function hasFilesWithName(name: string, source: string[], mode: StringSearchMode = "includes", forceLowerCase: boolean = false): boolean {
return source.some(file => (forceLowerCase && file.toLowerCase() || file)[mode](name));
export function hasFilesWithName(
name: string,
source: string[],
mode: StringSearchMode = "includes",
forceLowerCase: boolean = false,
): boolean {
return source.some((file) =>
((forceLowerCase && file.toLowerCase()) || file)[mode](name),
);
}
export function deletePersonalization(source: BundleUnit, logosNames: string[] = []): void {
[...logosNames, "personalization.json"]
.forEach(file => delete source[file]);
export function deletePersonalization(
source: BundleUnit,
logosNames: string[] = [],
): void {
[...logosNames, "personalization.json"].forEach(
(file) => delete source[file],
);
}