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/ passModels/
certificates/ certificates/
*.code-workspace *.code-workspace
.vscode/ .vscode/*
!.vscode/settings.json
*.js *.js
lib/ lib/
examples/build 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. 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. Express.js has been inserted as "example package" dipendency.
```sh ```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. 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. Please note that `field.js` example will force you to download `exampleBooking.pass`, no matter what.
___
---
Every contribution is really appreciated. ❤️ Thank you! Every contribution is really appreciated. ❤️ Thank you!

View File

@@ -1,5 +1,9 @@
import genRoute, { app } from "./webserver"; import genRoute, { app } from "./webserver";
import { createPass, createAbstractModel, AbstractModel } from "passkit-generator"; import {
createPass,
createAbstractModel,
AbstractModel,
} from "passkit-generator";
let abstractModel: AbstractModel; let abstractModel: AbstractModel;
@@ -11,148 +15,182 @@ let abstractModel: AbstractModel;
signerCert: "../certificates/signerCert.pem", signerCert: "../certificates/signerCert.pem",
signerKey: { signerKey: {
keyFile: "../certificates/signerKey.pem", keyFile: "../certificates/signerKey.pem",
passphrase: "123456" passphrase: "123456",
} },
}, },
// overrides: request.body || request.params || request.query, // overrides: request.body || request.params || request.query,
}); });
})(); })();
genRoute.all(async function manageRequest(request, response) { 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 { try {
const pass = await createPass(abstractModel); const pass = await createPass(abstractModel);
pass.transitType = "PKTransitTypeAir"; pass.transitType = "PKTransitTypeAir";
pass.headerFields.push({ pass.headerFields.push(
"key": "header1", {
"label": "Data", key: "header1",
"value": "25 mag", label: "Data",
"textAlignment": "PKTextAlignmentCenter" value: "25 mag",
}, { textAlignment: "PKTextAlignmentCenter",
"key": "header2", },
"label": "Volo", {
"value": "EZY997", key: "header2",
"textAlignment": "PKTextAlignmentCenter" label: "Volo",
}); value: "EZY997",
textAlignment: "PKTextAlignmentCenter",
},
);
pass.primaryFields.push({ pass.primaryFields.push(
key: "IATA-source", {
value: "NAP", key: "IATA-source",
label: "Napoli", value: "NAP",
textAlignment: "PKTextAlignmentLeft" label: "Napoli",
}, { textAlignment: "PKTextAlignmentLeft",
key: "IATA-destination", },
value: "VCE", {
label: "Venezia Marco Polo", key: "IATA-destination",
textAlignment: "PKTextAlignmentRight" value: "VCE",
}); label: "Venezia Marco Polo",
textAlignment: "PKTextAlignmentRight",
},
);
pass.secondaryFields.push({ pass.secondaryFields.push(
"key": "secondary1", {
"label": "Imbarco chiuso", key: "secondary1",
"value": "18:40", label: "Imbarco chiuso",
"textAlignment": "PKTextAlignmentCenter", value: "18:40",
}, { textAlignment: "PKTextAlignmentCenter",
"key": "sec2", },
"label": "Partenze", {
"value": "19:10", key: "sec2",
"textAlignment": "PKTextAlignmentCenter" label: "Partenze",
}, { value: "19:10",
"key": "sec3", textAlignment: "PKTextAlignmentCenter",
"label": "SB", },
"value": "Sì", {
"textAlignment": "PKTextAlignmentCenter" key: "sec3",
}, { label: "SB",
"key": "sec4", value: "",
"label": "Imbarco", textAlignment: "PKTextAlignmentCenter",
"value": "Anteriore", },
"textAlignment": "PKTextAlignmentCenter" {
}); key: "sec4",
label: "Imbarco",
value: "Anteriore",
textAlignment: "PKTextAlignmentCenter",
},
);
pass.auxiliaryFields.push({ pass.auxiliaryFields.push(
"key": "aux1", {
"label": "Passeggero", key: "aux1",
"value": "MR. WHO KNOWS", label: "Passeggero",
"textAlignment": "PKTextAlignmentLeft" value: "MR. WHO KNOWS",
}, { textAlignment: "PKTextAlignmentLeft",
"key": "aux2", },
"label": "Posto", {
"value": "1A*", key: "aux2",
"textAlignment": "PKTextAlignmentCenter" label: "Posto",
}); value: "1A*",
textAlignment: "PKTextAlignmentCenter",
},
);
pass.backFields.push({ pass.backFields.push(
"key": "document number", {
"label": "Numero documento:", key: "document number",
"value": "- -", label: "Numero documento:",
"textAlignment": "PKTextAlignmentLeft" value: "- -",
}, { textAlignment: "PKTextAlignmentLeft",
"key": "You're checked in, what next", },
"label": "Hai effettuato il check-in, Quali sono le prospettive", {
"value": "", key: "You're checked in, what next",
"textAlignment": "PKTextAlignmentLeft" label: "Hai effettuato il check-in, Quali sono le prospettive",
}, { value: "",
"key": "Check In", textAlignment: "PKTextAlignmentLeft",
"label": "1. check-in✓", },
"value": "", {
"textAlignment": "PKTextAlignmentLeft" key: "Check In",
}, { label: "1. check-in✓",
"key": "checkIn", value: "",
"label": "", textAlignment: "PKTextAlignmentLeft",
"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: "checkIn",
"key": "2. Bags", label: "",
"label": "2. Bagaglio", value:
"value": "", "Le uscite d'imbarco chiudono 30 minuti prima della partenza, quindi sii puntuale. In questo aeroporto puoi utilizzare la corsia Fast Track ai varchi di sicurezza.",
"textAlignment": "PKTextAlignmentLeft" textAlignment: "PKTextAlignmentLeft",
}, { },
"key": "Require special assistance", {
"label": "Assistenza speciale", key: "2. Bags",
"value": "Se hai richiesto assistenza speciale, presentati a un membro del personale nell'area di Consegna bagagli almeno 90 minuti prima del volo.", label: "2. Bagaglio",
"textAlignment": "PKTextAlignmentLeft" value: "",
}, { textAlignment: "PKTextAlignmentLeft",
"key": "3. Departures", },
"label": "3. Partenze", {
"value": "", key: "Require special assistance",
"textAlignment": "PKTextAlignmentLeft" label: "Assistenza speciale",
}, { value:
"key": "photoId", "Se hai richiesto assistenza speciale, presentati a un membro del personale nell'area di Consegna bagagli almeno 90 minuti prima del volo.",
"label": "Un documento didentità corredato di fotografia", textAlignment: "PKTextAlignmentLeft",
"value": "è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta didentità.", },
"textAlignment": "PKTextAlignmentLeft" {
}, { key: "3. Departures",
"key": "yourSeat", label: "3. Partenze",
"label": "Il tuo posto:", value: "",
"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",
"textAlignment": "PKTextAlignmentLeft" },
}, { {
"key": "Pack safely", key: "photoId",
"label": "Bagaglio sicuro", label: "Un documento didentità corredato di fotografia",
"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", value:
"textAlignment": "PKTextAlignmentLeft" "è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta didentità.",
}, { textAlignment: "PKTextAlignmentLeft",
"key": "Thank you for travelling easyJet", },
"label": "Grazie per aver viaggiato con easyJet", {
"value": "", key: "yourSeat",
"textAlignment": "PKTextAlignmentLeft" 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(); const stream = pass.generate();
response.set({ response.set({
"Content-type": "application/vnd.apple.pkpass", "Content-type": "application/vnd.apple.pkpass",
"Content-disposition": `attachment; filename=${passName}.pkpass` "Content-disposition": `attachment; filename=${passName}.pkpass`,
}); });
stream.pipe(response); stream.pipe(response);
} catch(err) { } catch (err) {
console.log(err); console.log(err);
response.set({ response.set({
"Content-type": "text/html" "Content-type": "text/html",
}); });
response.send(err.message); response.send(err.message);

View File

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

View File

@@ -12,7 +12,10 @@ import app from "./webserver";
import { createPass } from "passkit-generator"; import { createPass } from "passkit-generator";
app.all(async function manageRequest(request, response) { 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 { try {
const pass = await createPass({ const pass = await createPass({
@@ -22,8 +25,8 @@ app.all(async function manageRequest(request, response) {
signerCert: "../certificates/signerCert.pem", signerCert: "../certificates/signerCert.pem",
signerKey: { signerKey: {
keyFile: "../certificates/signerKey.pem", keyFile: "../certificates/signerKey.pem",
passphrase: "123456" passphrase: "123456",
} },
}, },
overrides: request.body || request.params || request.query, 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 // After this, pass.props["barcodes"] will have support for just two of three
// of the passed format (the valid ones); // of the passed format (the valid ones);
pass.barcodes({ pass.barcodes(
message: "Thank you for using this package <3", {
format: "PKBarcodeFormatCode128" 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",
message: "Thank you for using this package <3", format: "PKBarcodeFormatPDF417",
// @ts-expect-error },
format: "PKBarcodeFormatMock44617" {
}); 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() // 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"); pass.barcode("PKBarcodeFormatPDF417");
console.log("Barcode property is now:", pass.props["barcode"]); 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(); const stream = pass.generate();
response.set({ response.set({
"Content-type": "application/vnd.apple.pkpass", "Content-type": "application/vnd.apple.pkpass",
"Content-disposition": `attachment; filename=${passName}.pkpass` "Content-disposition": `attachment; filename=${passName}.pkpass`,
}); });
stream.pipe(response); stream.pipe(response);

View File

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

View File

@@ -13,7 +13,10 @@ import app from "./webserver";
import { createPass } from "passkit-generator"; import { createPass } from "passkit-generator";
app.all(async function manageRequest(request, response) { 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 { try {
let pass = await createPass({ let pass = await createPass({
model: `./models/exampleBooking`, model: `./models/exampleBooking`,
@@ -22,133 +25,164 @@ app.all(async function manageRequest(request, response) {
signerCert: "../certificates/signerCert.pem", signerCert: "../certificates/signerCert.pem",
signerKey: { signerKey: {
keyFile: "../certificates/signerKey.pem", keyFile: "../certificates/signerKey.pem",
passphrase: "123456" passphrase: "123456",
} },
}, },
overrides: request.body || request.params || request.query, overrides: request.body || request.params || request.query,
}); });
pass.transitType = "PKTransitTypeAir"; pass.transitType = "PKTransitTypeAir";
pass.headerFields.push({ pass.headerFields.push(
"key": "header1", {
"label": "Data", key: "header1",
"value": "25 mag", label: "Data",
"textAlignment": "PKTextAlignmentCenter" value: "25 mag",
}, { textAlignment: "PKTextAlignmentCenter",
"key": "header2", },
"label": "Volo", {
"value": "EZY997", key: "header2",
"textAlignment": "PKTextAlignmentCenter" label: "Volo",
}); value: "EZY997",
textAlignment: "PKTextAlignmentCenter",
},
);
pass.primaryFields.push({ pass.primaryFields.push(
key: "IATA-source", {
value: "NAP", key: "IATA-source",
label: "Napoli", value: "NAP",
textAlignment: "PKTextAlignmentLeft" label: "Napoli",
}, { textAlignment: "PKTextAlignmentLeft",
key: "IATA-destination", },
value: "VCE", {
label: "Venezia Marco Polo", key: "IATA-destination",
textAlignment: "PKTextAlignmentRight" value: "VCE",
}); label: "Venezia Marco Polo",
textAlignment: "PKTextAlignmentRight",
},
);
pass.secondaryFields.push({ pass.secondaryFields.push(
"key": "secondary1", {
"label": "Imbarco chiuso", key: "secondary1",
"value": "18:40", label: "Imbarco chiuso",
"textAlignment": "PKTextAlignmentCenter", value: "18:40",
}, { textAlignment: "PKTextAlignmentCenter",
"key": "sec2", },
"label": "Partenze", {
"value": "19:10", key: "sec2",
"textAlignment": "PKTextAlignmentCenter" label: "Partenze",
}, { value: "19:10",
"key": "sec3", textAlignment: "PKTextAlignmentCenter",
"label": "SB", },
"value": "Sì", {
"textAlignment": "PKTextAlignmentCenter" key: "sec3",
}, { label: "SB",
"key": "sec4", value: "",
"label": "Imbarco", textAlignment: "PKTextAlignmentCenter",
"value": "Anteriore", },
"textAlignment": "PKTextAlignmentCenter" {
}); key: "sec4",
label: "Imbarco",
value: "Anteriore",
textAlignment: "PKTextAlignmentCenter",
},
);
pass.auxiliaryFields.push({ pass.auxiliaryFields.push(
"key": "aux1", {
"label": "Passeggero", key: "aux1",
"value": "MR. WHO KNOWS", label: "Passeggero",
"textAlignment": "PKTextAlignmentLeft" value: "MR. WHO KNOWS",
}, { textAlignment: "PKTextAlignmentLeft",
"key": "aux2", },
"label": "Posto", {
"value": "1A*", key: "aux2",
"textAlignment": "PKTextAlignmentCenter" label: "Posto",
}); value: "1A*",
textAlignment: "PKTextAlignmentCenter",
},
);
pass.backFields.push({ pass.backFields.push(
"key": "document number", {
"label": "Numero documento:", key: "document number",
"value": "- -", label: "Numero documento:",
"textAlignment": "PKTextAlignmentLeft" value: "- -",
}, { textAlignment: "PKTextAlignmentLeft",
"key": "You're checked in, what next", },
"label": "Hai effettuato il check-in, Quali sono le prospettive", {
"value": "", key: "You're checked in, what next",
"textAlignment": "PKTextAlignmentLeft" label: "Hai effettuato il check-in, Quali sono le prospettive",
}, { value: "",
"key": "Check In", textAlignment: "PKTextAlignmentLeft",
"label": "1. check-in✓", },
"value": "", {
"textAlignment": "PKTextAlignmentLeft" key: "Check In",
}, { label: "1. check-in✓",
"key": "checkIn", value: "",
"label": "", textAlignment: "PKTextAlignmentLeft",
"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: "checkIn",
"key": "2. Bags", label: "",
"label": "2. Bagaglio", value:
"value": "", "Le uscite d'imbarco chiudono 30 minuti prima della partenza, quindi sii puntuale. In questo aeroporto puoi utilizzare la corsia Fast Track ai varchi di sicurezza.",
"textAlignment": "PKTextAlignmentLeft" textAlignment: "PKTextAlignmentLeft",
}, { },
"key": "Require special assistance", {
"label": "Assistenza speciale", key: "2. Bags",
"value": "Se hai richiesto assistenza speciale, presentati a un membro del personale nell'area di Consegna bagagli almeno 90 minuti prima del volo.", label: "2. Bagaglio",
"textAlignment": "PKTextAlignmentLeft" value: "",
}, { textAlignment: "PKTextAlignmentLeft",
"key": "3. Departures", },
"label": "3. Partenze", {
"value": "", key: "Require special assistance",
"textAlignment": "PKTextAlignmentLeft" label: "Assistenza speciale",
}, { value:
"key": "photoId", "Se hai richiesto assistenza speciale, presentati a un membro del personale nell'area di Consegna bagagli almeno 90 minuti prima del volo.",
"label": "Un documento didentità corredato di fotografia", textAlignment: "PKTextAlignmentLeft",
"value": "è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta didentità.", },
"textAlignment": "PKTextAlignmentLeft" {
}, { key: "3. Departures",
"key": "yourSeat", label: "3. Partenze",
"label": "Il tuo posto:", value: "",
"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",
"textAlignment": "PKTextAlignmentLeft" },
}, { {
"key": "Pack safely", key: "photoId",
"label": "Bagaglio sicuro", label: "Un documento didentità corredato di fotografia",
"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", value:
"textAlignment": "PKTextAlignmentLeft" "è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta didentità.",
}, { textAlignment: "PKTextAlignmentLeft",
"key": "Thank you for travelling easyJet", },
"label": "Grazie per aver viaggiato con easyJet", {
"value": "", key: "yourSeat",
"textAlignment": "PKTextAlignmentLeft" 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(); const stream = pass.generate();
response.set({ response.set({
"Content-type": "application/vnd.apple.pkpass", "Content-type": "application/vnd.apple.pkpass",
"Content-disposition": `attachment; filename=${passName}.pkpass` "Content-disposition": `attachment; filename=${passName}.pkpass`,
}); });
stream.pipe(response); stream.pipe(response);
@@ -156,7 +190,7 @@ app.all(async function manageRequest(request, response) {
console.log(err); console.log(err);
response.set({ response.set({
"Content-type": "text/html" "Content-type": "text/html",
}); });
response.send(err.message); response.send(err.message);

View File

@@ -8,7 +8,10 @@ import app from "./webserver";
import { createPass } from "passkit-generator"; import { createPass } from "passkit-generator";
app.all(async function manageRequest(request, response) { 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 { try {
const pass = await createPass({ const pass = await createPass({
@@ -18,10 +21,10 @@ app.all(async function manageRequest(request, response) {
signerCert: "../certificates/signerCert.pem", signerCert: "../certificates/signerCert.pem",
signerKey: { signerKey: {
keyFile: "../certificates/signerKey.pem", 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 // 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 // Italian, already has an .lproj which gets included
pass.localize("it", { pass.localize("it", {
"EVENT": "Evento", EVENT: "Evento",
"LOCATION": "Dove" LOCATION: "Dove",
}); });
// German, doesn't, so is created // German, doesn't, so is created
pass.localize("de", { pass.localize("de", {
"EVENT": "Ereignis", EVENT: "Ereignis",
"LOCATION": "Ort" LOCATION: "Ort",
}); });
// This language does not exist but is still added as .lproj folder // This language does not exist but is still added as .lproj folder
pass.localize("zu", {}); pass.localize("zu", {});
// @ts-ignore - ignoring for logging purposes. Do not replicate // @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(); const stream = pass.generate();
response.set({ response.set({
"Content-type": "application/vnd.apple.pkpass", "Content-type": "application/vnd.apple.pkpass",
"Content-disposition": `attachment; filename=${passName}.pkpass` "Content-disposition": `attachment; filename=${passName}.pkpass`,
}); });
stream.pipe(response); stream.pipe(response);
} catch(err) { } catch (err) {
console.log(err); console.log(err);
response.set({ response.set({

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -9,7 +9,7 @@ export const app = express();
app.use(express.json()); 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."); console.log("Webserver started.");
}); });
@@ -17,10 +17,11 @@ app.all("/", function (request, response) {
response.redirect("/gen/"); response.redirect("/gen/");
}); });
app.route("/gen") app.route("/gen").all((req, res) => {
.all((req, res) => { res.set("Content-Type", "text/html");
res.set("Content-Type", "text/html"); res.send(
res.send("Cannot generate a pass. Specify a modelName in the url to continue. <br/>Usage: /gen/<i>modelName</i>") "Cannot generate a pass. Specify a modelName in the url to continue. <br/>Usage: /gen/<i>modelName</i>",
}); );
});
export default app.route("/gen/:modelName"); export default app.route("/gen/:modelName");

6
package-lock.json generated
View File

@@ -209,6 +209,12 @@
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true "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": { "rimraf": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",

View File

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

View File

@@ -6,7 +6,7 @@ import * as path from "path";
describe("createPass", () => { describe("createPass", () => {
it("should throw if first argument is not provided", async () => { it("should throw if first argument is not provided", async () => {
await expectAsync(createPass(undefined)).toBeRejectedWithError( await expectAsync(createPass(undefined)).toBeRejectedWithError(
formatMessage("CP_NO_OPTS") formatMessage("CP_NO_OPTS"),
); );
}); });
@@ -27,71 +27,144 @@ describe("createPass", () => {
if (certificatesPath) { if (certificatesPath) {
it("should return a pass instance", async () => { it("should return a pass instance", async () => {
await expectAsync(createPass({ await expectAsync(
model: path.resolve(__dirname, "../examples/models/exampleBooking.pass"), createPass({
certificates: { model: path.resolve(
signerCert: path.resolve(__dirname, certificatesPath, "./signerCert.pem"), __dirname,
signerKey: { "../examples/models/exampleBooking.pass",
keyFile: path.resolve(__dirname, certificatesPath, "./signerKey.pem"), ),
passphrase: "password1234" 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", () => { describe("Model validation", () => {
it("Should reject with non valid model", async () => { it("Should reject with non valid model", async () => {
await expectAsync(createPass({ await expectAsync(
// @ts-expect-error createPass({
model: 0, // @ts-expect-error
certificates: { model: 0,
signerCert: path.resolve(__dirname, certificatesPath, "./signerCert.pem"), certificates: {
signerKey: { signerCert: path.resolve(
keyFile: path.resolve(__dirname, certificatesPath, "./signerKey.pem"), __dirname,
passphrase: "password1234" 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({ await expectAsync(
model: undefined, createPass({
certificates: { model: undefined,
signerCert: path.resolve(__dirname, certificatesPath, "./signerCert.pem"), certificates: {
signerKey: { signerCert: path.resolve(
keyFile: path.resolve(__dirname, certificatesPath, "./signerKey.pem"), __dirname,
passphrase: "password1234" 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({ await expectAsync(
model: null, createPass({
certificates: { model: null,
signerCert: path.resolve(__dirname, certificatesPath, "./signerCert.pem"), certificates: {
signerKey: { signerCert: path.resolve(
keyFile: path.resolve(__dirname, certificatesPath, "./signerKey.pem"), __dirname,
passphrase: "password1234" 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({ await expectAsync(
model: {}, createPass({
certificates: { model: {},
signerCert: path.resolve(__dirname, certificatesPath, "./signerCert.pem"), certificates: {
signerKey: { signerCert: path.resolve(
keyFile: path.resolve(__dirname, certificatesPath, "./signerKey.pem"), __dirname,
passphrase: "password1234" 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; let pass: Pass;
beforeEach(async () => { beforeEach(async () => {
pass = await createPass({ pass = await createPass({
model: path.resolve(__dirname, "../examples/models/examplePass.pass"), model: path.resolve(
__dirname,
"../examples/models/examplePass.pass",
),
certificates: { certificates: {
wwdr: path.resolve(__dirname, "../certificates/WWDR.pem"), wwdr: path.resolve(__dirname, "../certificates/WWDR.pem"),
signerCert: path.resolve(__dirname, "../certificates/signerCert.pem"), signerCert: path.resolve(
__dirname,
"../certificates/signerCert.pem",
),
signerKey: { signerKey: {
keyFile: path.resolve(__dirname, "../certificates/signerKey.pem"), keyFile: path.resolve(
passphrase: "123456" __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", () => { it("Will apply changes if a second object argument with translations is passed", () => {
pass.localize("it", { pass.localize("it", {
"Test": "Prova" Test: "Prova",
}); });
expect(typeof pass["l10nTranslations"]["it"]).toBe("object"); expect(typeof pass["l10nTranslations"]["it"]).toBe("object");
@@ -99,16 +108,21 @@ describe("Passkit-generator", function () {
describe("locations()", () => { describe("locations()", () => {
it("Won't apply changes if invalid location objects are passed", () => { it("Won't apply changes if invalid location objects are passed", () => {
const props = pass.props["locations"] || []; const props = pass.props["locations"] || [];
const oldAmountOfLocations = props && props.length || 0; const oldAmountOfLocations = (props && props.length) || 0;
pass.locations({ pass.locations(
// @ts-expect-error {
"ibrupofene": "no", // @ts-expect-error
"longitude": 0.00000000 ibrupofene: "no",
}, ...props); longitude: 0.0,
},
...props,
);
if (oldAmountOfLocations) { if (oldAmountOfLocations) {
expect(pass.props["locations"].length).toBe(oldAmountOfLocations); expect(pass.props["locations"].length).toBe(
oldAmountOfLocations,
);
} else { } else {
expect(pass.props["locations"]).toBe(undefined); expect(pass.props["locations"]).toBe(undefined);
} }
@@ -116,33 +130,42 @@ describe("Passkit-generator", function () {
it("Will filter out invalid location objects", () => { it("Will filter out invalid location objects", () => {
const props = pass.props["locations"] || []; const props = pass.props["locations"] || [];
const oldAmountOfLocations = props && props.length || 0; const oldAmountOfLocations = (props && props.length) || 0;
pass.locations({ pass.locations(
// @ts-expect-error {
"ibrupofene": "no", // @ts-expect-error
"longitude": 0.00000000 ibrupofene: "no",
}, { longitude: 0.0,
"longitude": 4.42634523, },
"latitude": 5.344233323352 {
}, ...(pass.props["locations"] || [])); 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()", () => { describe("Beacons()", () => {
it("Won't apply changes if invalid beacon objects are passed", () => { it("Won't apply changes if invalid beacon objects are passed", () => {
const props = pass.props["beacons"] || []; const props = pass.props["beacons"] || [];
const oldAmountOfBeacons = props && props.length || 0; const oldAmountOfBeacons = (props && props.length) || 0;
pass.beacons({ pass.beacons(
// @ts-expect-error {
"ibrupofene": "no", // @ts-expect-error
"major": 55, ibrupofene: "no",
"minor": 0, major: 55,
"proximityUUID": "2707c5f4-deb9-48ff-b760-671bc885b6a7" minor: 0,
}, ...props); proximityUUID: "2707c5f4-deb9-48ff-b760-671bc885b6a7",
},
...props,
);
if (oldAmountOfBeacons) { if (oldAmountOfBeacons) {
expect(pass.props["beacons"].length).toBe(oldAmountOfBeacons); expect(pass.props["beacons"].length).toBe(oldAmountOfBeacons);
@@ -153,20 +176,23 @@ describe("Passkit-generator", function () {
it("Will filter out invalid beacons objects", () => { it("Will filter out invalid beacons objects", () => {
const props = pass.props["beacons"] || []; const props = pass.props["beacons"] || [];
const oldAmountOfBeacons = props && props.length || 0; 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);
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); expect(pass.props["beacons"].length).toBe(oldAmountOfBeacons + 1);
}); });
@@ -175,7 +201,7 @@ describe("Passkit-generator", function () {
describe("barcodes()", () => { describe("barcodes()", () => {
it("Won't apply changes if no data is passed", () => { it("Won't apply changes if no data is passed", () => {
const props = pass.props["barcodes"] || []; const props = pass.props["barcodes"] || [];
const oldAmountOfBarcodes = props && props.length || 0; const oldAmountOfBarcodes = (props && props.length) || 0;
pass.barcodes(); pass.barcodes();
expect(pass.props["barcodes"].length).toBe(oldAmountOfBarcodes); expect(pass.props["barcodes"].length).toBe(oldAmountOfBarcodes);
@@ -183,7 +209,7 @@ describe("Passkit-generator", function () {
it("Will ignore boolean parameter", () => { it("Will ignore boolean parameter", () => {
const props = pass.props["barcodes"] || []; const props = pass.props["barcodes"] || [];
const oldAmountOfBarcodes = props && props.length || 0; const oldAmountOfBarcodes = (props && props.length) || 0;
// @ts-expect-error // @ts-expect-error
pass.barcode(true); pass.barcode(true);
@@ -192,7 +218,7 @@ describe("Passkit-generator", function () {
it("Will ignore numeric parameter", () => { it("Will ignore numeric parameter", () => {
const props = pass.props["barcodes"] || []; const props = pass.props["barcodes"] || [];
const oldAmountOfBarcodes = props && props.length || 0; const oldAmountOfBarcodes = (props && props.length) || 0;
// @ts-expect-error // @ts-expect-error
pass.barcodes(42); pass.barcodes(42);
@@ -208,7 +234,7 @@ describe("Passkit-generator", function () {
pass.barcodes({ pass.barcodes({
message: "28363516282", message: "28363516282",
format: "PKBarcodeFormatPDF417", format: "PKBarcodeFormatPDF417",
messageEncoding: "utf8" messageEncoding: "utf8",
}); });
expect(pass.props["barcodes"].length).toBe(1); expect(pass.props["barcodes"].length).toBe(1);
@@ -220,12 +246,14 @@ describe("Passkit-generator", function () {
format: "PKBarcodeFormatPDF417", 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", () => { it("Will ignore objects without message property", () => {
const props = pass.props["barcodes"] || []; const props = pass.props["barcodes"] || [];
const oldAmountOfBarcodes = props && props.length || 0; const oldAmountOfBarcodes = (props && props.length) || 0;
// @ts-expect-error // @ts-expect-error
pass.barcodes({ pass.barcodes({
@@ -237,12 +265,19 @@ describe("Passkit-generator", function () {
it("Will ignore non-Barcodes schema compliant objects", () => { it("Will ignore non-Barcodes schema compliant objects", () => {
// @ts-expect-error // @ts-expect-error
pass.barcodes(5, 10, 15, { pass.barcodes(
message: "28363516282", 5,
format: "PKBarcodeFormatPDF417" 10,
}, 7, 1); 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", () => { 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 () { it("Will ignore non string or null arguments", function () {
const oldBarcode = pass.props["barcode"] || undefined; const oldBarcode = pass.props["barcode"] || undefined;
pass pass.barcodes("Message-22645272183")
.barcodes("Message-22645272183")
// @ts-expect-error // @ts-expect-error
.barcode(55) .barcode(55);
// unchanged // unchanged
expect(pass.props["barcode"]).toEqual(oldBarcode); expect(pass.props["barcode"]).toEqual(oldBarcode);
}); });
it("Will reset backward value on null", () => { it("Will reset backward value on null", () => {
pass.barcodes("Message-22645272183") pass.barcodes("Message-22645272183").barcode(
.barcode("PKBarcodeFormatAztec"); "PKBarcodeFormatAztec",
);
expect(pass.props["barcode"].format).toBe("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", () => { it("Won't apply changes if unknown format is passed", () => {
const oldBarcode = pass.props["barcode"] || undefined; const oldBarcode = pass.props["barcode"] || undefined;
pass pass.barcodes("Message-22645272183")
.barcodes("Message-22645272183")
// @ts-expect-error // @ts-expect-error
.barcode("PKBingoBongoFormat"); .barcode("PKBingoBongoFormat");

View File

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

View File

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

View File

@@ -1,4 +1,10 @@
import { Certificates, FinalCertificates, PartitionedBundle, OverridesSupportedOptions, FactoryOptions } from "./schema"; import {
Certificates,
FinalCertificates,
PartitionedBundle,
OverridesSupportedOptions,
FactoryOptions,
} from "./schema";
import { getModelContents, readCertificatesFromOptions } from "./parser"; import { getModelContents, readCertificatesFromOptions } from "./parser";
import formatMessage from "./messages"; import formatMessage from "./messages";
@@ -6,7 +12,8 @@ const abmCertificates = Symbol("certificates");
const abmModel = Symbol("model"); const abmModel = Symbol("model");
const abmOverrides = Symbol("overrides"); const abmOverrides = Symbol("overrides");
export interface AbstractFactoryOptions extends Omit<FactoryOptions, "certificates"> { export interface AbstractFactoryOptions
extends Omit<FactoryOptions, "certificates"> {
certificates?: Certificates; certificates?: Certificates;
} }
@@ -30,13 +37,13 @@ export async function createAbstractModel(options: AbstractFactoryOptions) {
try { try {
const [bundle, certificates] = await Promise.all([ const [bundle, certificates] = await Promise.all([
getModelContents(options.model), getModelContents(options.model),
readCertificatesFromOptions(options.certificates) readCertificatesFromOptions(options.certificates),
]); ]);
return new AbstractModel({ return new AbstractModel({
bundle, bundle,
certificates, certificates,
overrides: options.overrides overrides: options.overrides,
}); });
} catch (err) { } catch (err) {
throw new Error(formatMessage("CP_INIT_ERROR", "abstract model", err)); throw new Error(formatMessage("CP_INIT_ERROR", "abstract model", err));
@@ -51,7 +58,7 @@ export class AbstractModel {
constructor(options: AbstractModelOptions) { constructor(options: AbstractModelOptions) {
this[abmModel] = options.bundle; this[abmModel] = options.bundle;
this[abmCertificates] = options.certificates; this[abmCertificates] = options.certificates;
this[abmOverrides] = options.overrides this[abmOverrides] = options.overrides;
} }
get certificates(): FinalCertificates { get certificates(): FinalCertificates {

View File

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

View File

@@ -24,20 +24,28 @@ export default class FieldsArray extends Array {
*/ */
push(...fieldsData: schema.Field[]): number { push(...fieldsData: schema.Field[]): number {
const validFields = fieldsData.reduce((acc: schema.Field[], current: schema.Field) => { const validFields = fieldsData.reduce(
if (!(typeof current === "object") || !schema.isValid(current, "field")) { (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; 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); return Array.prototype.push.call(this, ...validFields);
} }
@@ -58,9 +66,13 @@ export default class FieldsArray extends Array {
* also uniqueKeys set * 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); 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); 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 { createPass } from "./factory";
export { createAbstractModel } from "./abstract"; export { createAbstractModel } from "./abstract";
export type Pass = InstanceType<typeof PassClass> export type Pass = InstanceType<typeof PassClass>;
export type AbstractModel = InstanceType<typeof AbstractModelClass> export type AbstractModel = InstanceType<typeof AbstractModelClass>;

View File

@@ -1,31 +1,51 @@
const errors = { 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_INIT_ERROR:
CP_NO_OPTS: "Cannot initialize the pass or abstract model creation: no options were passed.", "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_CERTS: "Cannot initialize the pass creation: no valid certificates were passed.", CP_NO_OPTS:
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.", "Cannot initialize the pass or abstract model creation: no options were passed.",
REQUIR_VALID_FAILED: "The options passed to Pass constructor does not meet the requirements.\nRefer to the documentation to compile them correctly.", CP_NO_CERTS:
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.", "Cannot initialize the pass creation: no valid certificates were passed.",
MODEL_NOT_VALID: "A model must be provided in form of path (string) or object { 'fileName': Buffer } in order to continue.", 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_NOT_FOUND: "Model %s not found. Provide a valid one to continue.",
MODELF_FILE_NOT_FOUND: "File %s not found.", 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.", INVALID_CERT_PATH: "Invalid certificate loaded. %s does not exist.",
TRSTYPE_REQUIRED: "Cannot proceed with pass creation. transitType field is required for boardingPasses.", TRSTYPE_REQUIRED:
OVV_KEYS_BADFORMAT: "Cannot proceed with pass creation due to bad keys format in overrides.", "Cannot proceed with pass creation. transitType field is required for boardingPasses.",
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." 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 = { 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]", TRSTYPE_NOT_VALID:
BRC_NOT_SUPPORTED: "Format not found among barcodes. Cannot set backward compatibility.", '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_FORMATTYPE_UNMATCH: "Format must be a string or null. Cannot set backward compatibility.", BRC_NOT_SUPPORTED:
BRC_AUTC_MISSING_DATA: "Unable to autogenerate barcodes. Data is not a string.", "Format not found among barcodes. Cannot set backward compatibility.",
BRC_BW_FORMAT_UNSUPPORTED: "This format is not supported (by Apple) for backward support. Please choose another one.", BRC_FORMATTYPE_UNMATCH:
BRC_NO_POOL: "Cannot set barcode: no barcodes found. Please set barcodes first. Barcode is for retrocompatibility only.", "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.", DATE_FORMAT_UNMATCH: "%s was not set due to incorrect date format.",
NFC_INVALID: "Unable to set NFC properties: data not compliant with schema.", NFC_INVALID:
PRS_INVALID: "Unable to parse Personalization.json. File is not a valid JSON. Error: %s", "Unable to set NFC properties: data not compliant with schema.",
PRS_REMOVED: "Personalization has been removed as it requires an NFC-enabled pass to work." 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); type AllMessages = keyof (typeof debugMessages & typeof errors);

View File

@@ -1,8 +1,21 @@
import * as path from "path"; import * as path from "path";
import forge from "node-forge"; import forge from "node-forge";
import formatMessage from "./messages"; import formatMessage from "./messages";
import { FactoryOptions, PartitionedBundle, BundleUnit, Certificates, FinalCertificates, isValid } from "./schema"; import {
import { removeHidden, splitBufferBundle, getAllFilesWithName, hasFilesWithName, deletePersonalization } from "./utils"; FactoryOptions,
PartitionedBundle,
BundleUnit,
Certificates,
FinalCertificates,
isValid,
} from "./schema";
import {
removeHidden,
splitBufferBundle,
getAllFilesWithName,
hasFilesWithName,
deletePersonalization,
} from "./utils";
import fs from "fs"; import fs from "fs";
import debug from "debug"; import debug from "debug";
@@ -27,10 +40,9 @@ export async function getModelContents(model: FactoryOptions["model"]) {
} }
const modelFiles = Object.keys(modelContents.bundle); const modelFiles = Object.keys(modelContents.bundle);
const isModelInitialized = ( const isModelInitialized =
modelFiles.includes("pass.json") && modelFiles.includes("pass.json") &&
hasFilesWithName("icon", modelFiles, "startsWith") hasFilesWithName("icon", modelFiles, "startsWith");
);
if (!isModelInitialized) { if (!isModelInitialized) {
throw new Error(formatMessage("MODEL_UNINITIALIZED", "parse result")); throw new Error(formatMessage("MODEL_UNINITIALIZED", "parse result"));
@@ -46,19 +58,34 @@ export async function getModelContents(model: FactoryOptions["model"]) {
return modelContents; return modelContents;
} }
const logoFullNames = getAllFilesWithName("personalizationLogo", modelFiles, "startsWith"); const logoFullNames = getAllFilesWithName(
if (!(logoFullNames.length && modelContents.bundle[personalizationJsonFile].length)) { "personalizationLogo",
modelFiles,
"startsWith",
);
if (
!(
logoFullNames.length &&
modelContents.bundle[personalizationJsonFile].length
)
) {
deletePersonalization(modelContents.bundle, logoFullNames); deletePersonalization(modelContents.bundle, logoFullNames);
return modelContents; return modelContents;
} }
try { try {
const parsedPersonalization = JSON.parse(modelContents.bundle[personalizationJsonFile].toString("utf8")); const parsedPersonalization = JSON.parse(
const isPersonalizationValid = isValid(parsedPersonalization, "personalizationDict"); modelContents.bundle[personalizationJsonFile].toString("utf8"),
);
const isPersonalizationValid = isValid(
parsedPersonalization,
"personalizationDict",
);
if (!isPersonalizationValid) { if (!isPersonalizationValid) {
[...logoFullNames, personalizationJsonFile] [...logoFullNames, personalizationJsonFile].forEach(
.forEach(file => delete modelContents.bundle[file]); (file) => delete modelContents.bundle[file],
);
return modelContents; return modelContents;
} }
@@ -76,55 +103,70 @@ export async function getModelContents(model: FactoryOptions["model"]) {
* @param model * @param model
*/ */
export async function getModelFolderContents(model: string): Promise<PartitionedBundle> { export async function getModelFolderContents(
model: string,
): Promise<PartitionedBundle> {
try { try {
const modelPath = `${model}${!path.extname(model) && ".pass" || ""}`; const modelPath = `${model}${(!path.extname(model) && ".pass") || ""}`;
const modelFilesList = await readDir(modelPath); const modelFilesList = await readDir(modelPath);
// No dot-starting files, manifest and signature // No dot-starting files, manifest and signature
const filteredFiles = removeHidden(modelFilesList) const filteredFiles = removeHidden(modelFilesList).filter(
.filter(f => !/(manifest|signature)/i.test(f) && /.+$/.test(path.parse(f).ext)); (f) =>
!/(manifest|signature)/i.test(f) &&
const isModelInitialized = ( /.+$/.test(path.parse(f).ext),
filteredFiles.length &&
hasFilesWithName("icon", filteredFiles, "startsWith")
); );
const isModelInitialized =
filteredFiles.length &&
hasFilesWithName("icon", filteredFiles, "startsWith");
// Icon is required to proceed // Icon is required to proceed
if (!isModelInitialized) { if (!isModelInitialized) {
throw new Error(formatMessage( throw new Error(
"MODEL_UNINITIALIZED", formatMessage("MODEL_UNINITIALIZED", path.parse(model).name),
path.parse(model).name );
));
} }
// Splitting files from localization folders // Splitting files from localization folders
const rawBundleFiles = filteredFiles.filter(entry => !entry.includes(".lproj")); const rawBundleFiles = filteredFiles.filter(
const l10nFolders = filteredFiles.filter(entry => entry.includes(".lproj")); (entry) => !entry.includes(".lproj"),
);
const rawBundleBuffers = await Promise.all( const l10nFolders = filteredFiles.filter((entry) =>
rawBundleFiles.map(file => readFile(path.resolve(modelPath, file))) entry.includes(".lproj"),
); );
const bundle: BundleUnit = Object.assign({}, const rawBundleBuffers = await Promise.all(
...rawBundleFiles.map((fileName, index) => ({ [fileName]: rawBundleBuffers[index] })) rawBundleFiles.map((file) =>
readFile(path.resolve(modelPath, file)),
),
);
const bundle: BundleUnit = Object.assign(
{},
...rawBundleFiles.map((fileName, index) => ({
[fileName]: rawBundleBuffers[index],
})),
); );
// Reading concurrently localizations folder // Reading concurrently localizations folder
// and their files and their buffers // and their files and their buffers
const L10N_FilesListByFolder: Array<BundleUnit> = await Promise.all( const L10N_FilesListByFolder: Array<BundleUnit> = await Promise.all(
l10nFolders.map(async folderPath => { l10nFolders.map(async (folderPath) => {
// Reading current folder // Reading current folder
const currentLangPath = path.join(modelPath, folderPath); const currentLangPath = path.join(modelPath, folderPath);
const files = await readDir(currentLangPath); const files = await readDir(currentLangPath);
// Transforming files path to a model-relative path // Transforming files path to a model-relative path
const validFiles = removeHidden(files) const validFiles = removeHidden(files).map((file) =>
.map(file => path.join(currentLangPath, file)); path.join(currentLangPath, file),
);
// Getting all the buffers from file paths // Getting all the buffers from file paths
const buffers = await Promise.all( 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 // Assigning each file path to its buffer
@@ -140,34 +182,37 @@ export async function getModelFolderContents(model: string): Promise<Partitioned
return { return {
...acc, ...acc,
[fileName]: buffers[index] [fileName]: buffers[index],
}; };
}, {}); }, {});
}) }),
); );
const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign( const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign(
{}, {},
...L10N_FilesListByFolder ...L10N_FilesListByFolder.map((folder, index) => ({
.map((folder, index) => ({ [l10nFolders[index]]: folder })) [l10nFolders[index]]: folder,
})),
); );
return { return {
bundle, bundle,
l10nBundle l10nBundle,
}; };
} catch (err) { } catch (err) {
if (err?.code === "ENOENT") { if (err?.code === "ENOENT") {
if (err.syscall === "open") { if (err.syscall === "open") {
// file opening failed // 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") { } else if (err.syscall === "scandir") {
// directory reading failed // directory reading failed
const pathContents = (err.path as string).split(/(\/|\\\?)/); const pathContents = (err.path as string).split(/(\/|\\\?)/);
throw new Error(formatMessage( throw new Error(
"MODELF_FILE_NOT_FOUND", formatMessage(
pathContents[pathContents.length - 1] "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 { export function getModelBufferContents(model: BundleUnit): PartitionedBundle {
const rawBundle = removeHidden(Object.keys(model)).reduce<BundleUnit>((acc, current) => { const rawBundle = removeHidden(Object.keys(model)).reduce<BundleUnit>(
// Checking if current file is one of the autogenerated ones or if its (acc, current) => {
// content is not available // Checking if current file is one of the autogenerated ones or if its
// content is not available
if (/(manifest|signature)/.test(current) || !model[current]) { if (/(manifest|signature)/.test(current) || !model[current]) {
return acc; return acc;
} }
return { ...acc, [current]: model[current] }; return { ...acc, [current]: model[current] };
}, {}); },
{},
);
const bundleKeys = Object.keys(rawBundle); const bundleKeys = Object.keys(rawBundle);
const isModelInitialized = ( const isModelInitialized =
bundleKeys.length && bundleKeys.length && hasFilesWithName("icon", bundleKeys, "startsWith");
hasFilesWithName("icon", bundleKeys, "startsWith")
);
// Icon is required to proceed // Icon is required to proceed
if (!isModelInitialized) { if (!isModelInitialized) {
throw new Error(formatMessage("MODEL_UNINITIALIZED", "Buffers")) throw new Error(formatMessage("MODEL_UNINITIALIZED", "Buffers"));
} }
// separing localization folders from bundle files // separing localization folders from bundle files
@@ -210,7 +256,7 @@ export function getModelBufferContents(model: BundleUnit): PartitionedBundle {
return { return {
bundle, bundle,
l10nBundle l10nBundle,
}; };
} }
@@ -224,8 +270,16 @@ type flatCertificates = Omit<Certificates, "signerKey"> & {
signerKey: string; signerKey: string;
}; };
export async function readCertificatesFromOptions(options: Certificates): Promise<FinalCertificates> { export async function readCertificatesFromOptions(
if (!(options && Object.keys(options).length && isValid(options, "certificatesSchema"))) { options: Certificates,
): Promise<FinalCertificates> {
if (
!(
options &&
Object.keys(options).length &&
isValid(options, "certificatesSchema")
)
) {
throw new Error(formatMessage("CP_NO_CERTS")); 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 // if the signerKey is an object, we want to get
// all the real contents and don't care of passphrase // 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 // We read the contents
const rawContentsPromises = Object.keys(flattenedDocs) const rawContentsPromises = Object.keys(flattenedDocs).map((key) => {
.map(key => { const content = flattenedDocs[key];
const content = flattenedDocs[key];
if (!!path.parse(content).ext) { if (!!path.parse(content).ext) {
// The content is a path to the document // The content is a path to the document
return readFile(path.resolve(content), { encoding: "utf8" }); return readFile(path.resolve(content), { encoding: "utf8" });
} else { } else {
// Content is the real document content // Content is the real document content
return Promise.resolve(content); return Promise.resolve(content);
} }
}); });
try { try {
const parsedContents = await Promise.all(rawContentsPromises); const parsedContents = await Promise.all(rawContentsPromises);
const pemParsedContents = parsedContents.map((file, index) => { const pemParsedContents = parsedContents.map((file, index) => {
const certName = Object.keys(options)[index]; const certName = Object.keys(options)[index];
const passphrase = ( const passphrase =
typeof options.signerKey === "object" && (typeof options.signerKey === "object" &&
options.signerKey?.passphrase options.signerKey?.passphrase) ||
) || undefined; undefined;
const pem = parsePEM(certName, file, passphrase); const pem = parsePEM(certName, file, passphrase);
@@ -279,7 +334,9 @@ export async function readCertificatesFromOptions(options: Certificates): Promis
throw err; 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 * as schema from "./schema";
import formatMessage from "./messages"; import formatMessage from "./messages";
import FieldsArray from "./fieldsArray"; 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 barcodeDebug = debug("passkit:barcode");
const genericDebug = debug("passkit:generic"); const genericDebug = debug("passkit:generic");
@@ -20,7 +26,7 @@ const propsSchemaMap = new Map<string, schema.Schema>([
["barcode", "barcode"], ["barcode", "barcode"],
["beacons", "beaconsDict"], ["beacons", "beaconsDict"],
["locations", "locationsDict"], ["locations", "locationsDict"],
["nfc", "nfcDict"] ["nfc", "nfcDict"],
]); ]);
export class Pass { export class Pass {
@@ -42,7 +48,9 @@ export class Pass {
private Certificates: schema.FinalCertificates; private Certificates: schema.FinalCertificates;
private [transitType]: string = ""; private [transitType]: string = "";
private l10nTranslations: { [languageCode: string]: { [placeholder: string]: string } } = {}; private l10nTranslations: {
[languageCode: string]: { [placeholder: string]: string };
} = {};
constructor(options: schema.PassInstance) { constructor(options: schema.PassInstance) {
if (!schema.isValid(options, "instance")) { if (!schema.isValid(options, "instance")) {
@@ -54,77 +62,101 @@ export class Pass {
this.bundle = { ...options.model.bundle }; this.bundle = { ...options.model.bundle };
try { try {
this.passCore = JSON.parse(this.bundle["pass.json"].toString("utf8")); this.passCore = JSON.parse(
this.bundle["pass.json"].toString("utf8"),
);
} catch (err) { } catch (err) {
throw new Error(formatMessage("PASSFILE_VALIDATION_FAILED")); throw new Error(formatMessage("PASSFILE_VALIDATION_FAILED"));
} }
// Parsing the options and extracting only the valid ones. // 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) { if (validOverrides === null) {
throw new Error(formatMessage("OVV_KEYS_BADFORMAT")) throw new Error(formatMessage("OVV_KEYS_BADFORMAT"));
} }
this.type = Object.keys(this.passCore) this.type = Object.keys(this.passCore).find((key) =>
.find(key => /(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key)) as keyof schema.ValidPassType; /(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key),
) as keyof schema.ValidPassType;
if (!this.type) { if (!this.type) {
throw new Error(formatMessage("NO_PASS_TYPE")); throw new Error(formatMessage("NO_PASS_TYPE"));
} }
// Parsing and validating pass.json keys // Parsing and validating pass.json keys
const passCoreKeys = Object.keys(this.passCore) as (keyof schema.ValidPass)[]; const passCoreKeys = Object.keys(
const validatedPassKeys = passCoreKeys.reduce<schema.ValidPass>((acc, current) => { this.passCore,
if (this.type === current) { ) as (keyof schema.ValidPass)[];
// We want to exclude type keys (eventTicket, const validatedPassKeys = passCoreKeys.reduce<schema.ValidPass>(
// boardingPass, ecc.) and their content (acc, current) => {
return acc; if (this.type === current) {
} // We want to exclude type keys (eventTicket,
// boardingPass, ecc.) and their content
return acc;
}
if (!propsSchemaMap.has(current)) { if (!propsSchemaMap.has(current)) {
// If the property is unknown (we don't care if // If the property is unknown (we don't care if
// it is valid or not for Wallet), we return // it is valid or not for Wallet), we return
// directly the content // directly the content
return { ...acc, [current]: this.passCore[current] }; return { ...acc, [current]: this.passCore[current] };
} }
const currentSchema = propsSchemaMap.get(current)!; const currentSchema = propsSchemaMap.get(current)!;
if (Array.isArray(this.passCore[current])) { if (Array.isArray(this.passCore[current])) {
const valid = getValidInArray<schema.ArrayPassSchema>( const valid = getValidInArray<schema.ArrayPassSchema>(
currentSchema, currentSchema,
this.passCore[current] as schema.ArrayPassSchema[] this.passCore[current] as schema.ArrayPassSchema[],
); );
return { ...acc, [current]: valid }; return { ...acc, [current]: valid };
} else { } else {
return { return {
...acc, ...acc,
[current]: schema.isValid( [current]:
this.passCore[current], (schema.isValid(
currentSchema this.passCore[current],
) && this.passCore[current] || undefined currentSchema,
}; ) &&
} this.passCore[current]) ||
}, {}); undefined,
};
}
},
{},
);
this[passProps] = { this[passProps] = {
...(validatedPassKeys || {}), ...(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 // We might want to generate a boarding pass without setting manually
// in the code the transit type but right in the model; // in the code the transit type but right in the model;
this[transitType] = this.passCore[this.type]["transitType"]; this[transitType] = this.passCore[this.type]["transitType"];
} }
this._fields = ["primaryFields", "secondaryFields", "auxiliaryFields", "backFields", "headerFields"]; this._fields = [
this._fields.forEach(fieldName => { "primaryFields",
"secondaryFields",
"auxiliaryFields",
"backFields",
"headerFields",
];
this._fields.forEach((fieldName) => {
this[fieldName] = new FieldsArray( this[fieldName] = new FieldsArray(
this.fieldsKeys, this.fieldsKeys,
...(this.passCore[this.type][fieldName] || []) ...(this.passCore[this.type][fieldName] || []).filter((field) =>
.filter(field => schema.isValid(field, "field")) schema.isValid(field, "field"),
),
); );
}); });
} }
@@ -146,13 +178,19 @@ export class Pass {
*/ */
const currentBundleFiles = Object.keys(this.bundle); 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")); genericDebug(formatMessage("PRS_REMOVED"));
deletePersonalization(this.bundle, getAllFilesWithName( deletePersonalization(
"personalizationLogo", this.bundle,
currentBundleFiles, getAllFilesWithName(
"startsWith" "personalizationLogo",
)); currentBundleFiles,
"startsWith",
),
);
} }
const finalBundle = { ...this.bundle } as schema.BundleUnit; const finalBundle = { ...this.bundle } as schema.BundleUnit;
@@ -161,7 +199,7 @@ export class Pass {
* Iterating through languages and generating pass.string file * 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 strings = generateStringFile(this.l10nTranslations[lang]);
const langInBundles = `${lang}.lproj`; const langInBundles = `${lang}.lproj`;
@@ -176,13 +214,21 @@ export class Pass {
this.l10nBundles[langInBundles] = {}; this.l10nBundles[langInBundles] = {};
} }
this.l10nBundles[langInBundles]["pass.strings"] = Buffer.concat([ this.l10nBundles[langInBundles][
this.l10nBundles[langInBundles]["pass.strings"] || Buffer.alloc(0), "pass.strings"
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; return;
} }
@@ -194,33 +240,48 @@ export class Pass {
* composition. * composition.
*/ */
Object.assign(finalBundle, ...Object.keys(this.l10nBundles[langInBundles]) Object.assign(
.map(fileName => { finalBundle,
const fullPath = path.join(langInBundles, fileName).replace(/\\/, "/"); ...Object.keys(this.l10nBundles[langInBundles]).map(
return { [fullPath]: this.l10nBundles[langInBundles][fileName] }; (fileName) => {
}) const fullPath = path
.join(langInBundles, fileName)
.replace(/\\/, "/");
return {
[fullPath]: this.l10nBundles[langInBundles][
fileName
],
};
},
),
); );
}); });
/* /*
* Parsing the buffers, pushing them into the archive * Parsing the buffers, pushing them into the archive
* and returning the compiled manifest * and returning the compiled manifest
*/ */
const archive = new ZipFile(); const archive = new ZipFile();
const manifest = Object.keys(finalBundle).reduce<schema.Manifest>((acc, current) => { const manifest = Object.keys(finalBundle).reduce<schema.Manifest>(
let hashFlow = forge.md.sha1.create(); (acc, current) => {
let hashFlow = forge.md.sha1.create();
hashFlow.update(finalBundle[current].toString("binary")); hashFlow.update(finalBundle[current].toString("binary"));
archive.addBuffer(finalBundle[current], current); archive.addBuffer(finalBundle[current], current);
acc[current] = hashFlow.digest().toHex(); acc[current] = hashFlow.digest().toHex();
return acc; return acc;
}, {}); },
{},
);
const signatureBuffer = this._sign(manifest); const signatureBuffer = this._sign(manifest);
archive.addBuffer(signatureBuffer, "signature"); 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(); const passStream = new Stream.PassThrough();
archive.outputStream.pipe(passStream); archive.outputStream.pipe(passStream);
@@ -242,8 +303,15 @@ export class Pass {
* @see https://apple.co/2KOv0OW - Passes support localization * @see https://apple.co/2KOv0OW - Passes support localization
*/ */
localize(lang: string, translations?: { [placeholder: string]: string }): this { localize(
if (lang && typeof lang === "string" && (typeof translations === "object" || translations === undefined)) { lang: string,
translations?: { [placeholder: string]: string },
): this {
if (
lang &&
typeof lang === "string" &&
(typeof translations === "object" || translations === undefined)
) {
this.l10nTranslations[lang] = translations || {}; this.l10nTranslations[lang] = translations || {};
} }
@@ -292,7 +360,7 @@ export class Pass {
*/ */
beacons(resetFlag: null): this; beacons(resetFlag: null): this;
beacons(...data: schema.Beacon[]): this beacons(...data: schema.Beacon[]): this;
beacons(...data: (schema.Beacon | null)[]): this { beacons(...data: (schema.Beacon | null)[]): this {
if (data[0] === null) { if (data[0] === null) {
delete this[passProps]["beacons"]; delete this[passProps]["beacons"];
@@ -322,7 +390,10 @@ export class Pass {
return this; return this;
} }
const valid = processRelevancySet("locations", data as schema.Location[]); const valid = processRelevancySet(
"locations",
data as schema.Location[],
);
if (valid.length) { if (valid.length) {
this[passProps]["locations"] = valid; this[passProps]["locations"] = valid;
@@ -390,19 +461,28 @@ export class Pass {
* Validation assign default value to missing parameters (if any). * Validation assign default value to missing parameters (if any).
*/ */
const validBarcodes = data.reduce<schema.Barcode[]>((acc, current) => { const validBarcodes = data.reduce<schema.Barcode[]>(
if (!(current && current instanceof Object)) { (acc, current) => {
return acc; 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)) { if (
return acc; !(
} 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) { if (validBarcodes.length) {
this[passProps]["barcodes"] = validBarcodes; this[passProps]["barcodes"] = validBarcodes;
@@ -446,7 +526,9 @@ export class Pass {
} }
// Checking which object among barcodes has the same format of the specified one. // 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) { if (index === -1) {
barcodeDebug(formatMessage("BRC_NOT_SUPPORTED")); barcodeDebug(formatMessage("BRC_NOT_SUPPORTED"));
@@ -472,7 +554,14 @@ export class Pass {
return this; 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")); genericDebug(formatMessage("NFC_INVALID"));
return this; return this;
} }
@@ -505,7 +594,10 @@ export class Pass {
private _sign(manifest: schema.Manifest): Buffer { private _sign(manifest: schema.Manifest): Buffer {
const signature = forge.pkcs7.createSignedData(); 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.wwdr);
signature.addCertificate(this.Certificates.signerCert); signature.addCertificate(this.Certificates.signerCert);
@@ -523,14 +615,18 @@ export class Pass {
key: this.Certificates.signerKey, key: this.Certificates.signerKey,
certificate: this.Certificates.signerCert, certificate: this.Certificates.signerCert,
digestAlgorithm: forge.pki.oids.sha1, digestAlgorithm: forge.pki.oids.sha1,
authenticatedAttributes: [{ authenticatedAttributes: [
type: forge.pki.oids.contentType, {
value: forge.pki.oids.data type: forge.pki.oids.contentType,
}, { value: forge.pki.oids.data,
type: forge.pki.oids.messageDigest, },
}, { {
type: forge.pki.oids.signingTime, type: forge.pki.oids.messageDigest,
}] },
{
type: forge.pki.oids.signingTime,
},
],
}); });
/** /**
@@ -554,7 +650,10 @@ export class Pass {
* of beautiful things. ¯\_(ツ)_/¯ * 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 { 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) { if (Object.keys(this[passProps]).length) {
/* /*
@@ -575,14 +676,22 @@ export class Pass {
* and then delete it from the passFile. * and then delete it from the passFile.
*/ */
const passColors = ["backgroundColor", "foregroundColor", "labelColor"] as Array<keyof schema.PassColors>; const passColors = [
passColors.filter(v => this[passProps][v] && !isValidRGB(this[passProps][v])) "backgroundColor",
.forEach(v => delete this[passProps][v]); "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]); Object.assign(passFile, this[passProps]);
} }
this._fields.forEach(field => { this._fields.forEach((field) => {
passFile[this.type][field] = this[field]; passFile[this.type][field] = this[field];
}); });
@@ -627,8 +736,14 @@ function barcodesFromUncompleteData(message: string): schema.Barcode[] {
"PKBarcodeFormatQR", "PKBarcodeFormatQR",
"PKBarcodeFormatPDF417", "PKBarcodeFormatPDF417",
"PKBarcodeFormatAztec", "PKBarcodeFormatAztec",
"PKBarcodeFormatCode128" "PKBarcodeFormatCode128",
].map(format => schema.getValidated({ format, message }, "barcode") as schema.Barcode); ].map(
(format) =>
schema.getValidated(
{ format, message },
"barcode",
) as schema.Barcode,
);
} }
function processRelevancySet<T>(key: string, data: T[]): T[] { 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[] { 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 { function processDate(key: string, date: Date): string | null {

View File

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

View File

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