Added first version of serverless-based implementation example

This commit is contained in:
Alexander Cerutti
2021-10-31 19:37:29 +01:00
parent 32205871af
commit 8552f217ee
16 changed files with 22177 additions and 0 deletions

8
examples/serverless/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
# package directories
node_modules
jspm_packages
# Serverless directories
.serverless
.build
!*.js

View File

@@ -0,0 +1,52 @@
# Serverless Examples
This is a sample project for showing passkit-generator being used on cloud functions.
Typescript compilation happens automatically through `serverless-plugin-typescript` when serverless is started.
## Configuration
These examples are basically made for being executed locally. In the file `config.json`, some constants can be customized.
```json
/** Passkit signerKey passphrase **/
"SIGNER_KEY_PASSPHRASE": "123456",
/** Bucket name where a pass is saved before being served. */
"PASSES_S3_TEMP_BUCKET": "pkge-test",
/** S3 Access key ID - "S3RVER" is default for `serverless-s3-local`. If this example is run offline, "S3RVER" will always be used. */
"ACCESS_KEY_ID": "S3RVER",
/** S3 Secret - "S3RVER" is default for `serverless-s3-local` */
"SECRET_ACCESS_KEY": "S3RVER",
/** Bucket that contains pass models **/
"MODELS_S3_BUCKET": "pkge-mdbk"
```
## Run examples
Install the dependencies and run serverless. Installing the dependencies will link the latest version of passkit-generator in the parent workspace.
```sh
$ npm install;
$ npm run run-offline;
```
This will start `serverless offline` with an additional host option (mainly for WSL environment).
Serverless will start, by default, on `0.0.0.0:8080`.
### Available examples
All the examples, except fields ones, require a `modelName` to be passed in queryString. The name will be checked against local FS or S3 bucket if example is deployed.
Pass in queryString all the pass props you want to apply them to the final result.
| Example name | Endpoint name | Additional notes |
| -------------- | ----------------- | ----------------------------------------------------------------------------------------------------- |
| localize | `/localize` | - |
| fields | `/fields` | - |
| expirationDate | `/expirationDate` | - |
| scratch | `/scratch` | - |
| barcodes | `/barcodes` | Using `?alt=true` query parameter, will lead to barcode string message usage instead of selected ones |
| pkpasses | `/pkpasses` | - |

View File

@@ -0,0 +1,7 @@
{
"SIGNER_KEY_PASSPHRASE": "123456",
"PASSES_S3_TEMP_BUCKET": "pkge-test",
"ACCESS_KEY_ID": "S3RVER",
"SECRET_ACCESS_KEY": "S3RVER",
"MODELS_S3_BUCKET": "pkge-mdbk"
}

21368
examples/serverless/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
{
"name": "examples-aws-lambda",
"version": "0.0.0",
"private": true,
"description": "Passkit-generator examples for running in AWS Lambda",
"author": "Alexander P. Cerutti <cerutti.alexander@gmail.com>",
"license": "ISC",
"main": "src/index.js",
"scripts": {
"preinstall": "npm run clear:deps && npm unlink --no-save passkit-generator",
"postinstall": "npm --prefix ../.. run build && npm --prefix ../.. link && npm link passkit-generator",
"clear:deps": "rm -rf node_modules",
"run-offline": "npx serverless offline --host 0.0.0.0; :'specifying host due to WSL limits'"
},
"dependencies": {
"aws-sdk": "^2.1018.0",
"tslib": "^2.3.1"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.84",
"@types/express": "4.17.8",
"serverless-offline": "^8.2.0",
"serverless-plugin-typescript": "^2.1.0",
"serverless-s3-local": "^0.6.20",
"typescript": "^4.4.4"
}
}

View File

@@ -0,0 +1,56 @@
service: passkit-generator-test-lambda
frameworkVersion: "2"
plugins:
- serverless-offline
- serverless-plugin-typescript
- serverless-s3-local
provider:
name: aws
runtime: nodejs14.x
lambdaHashingVersion: "20201221"
functions:
fields:
handler: src/index.fields
events:
- httpApi:
path: /fields
method: get
expiration:
handler: src/index.expirationDate
events:
- httpApi:
path: /expirationDate
method: get
localize:
handler: src/index.localize
events:
- httpApi:
path: /localize
method: get
barcodes:
handler: src/index.barcodes
events:
- httpApi:
path: /barcodes
method: get
scratch:
handler: src/index.scratch
events:
- httpApi:
path: /scratch
method: get
pkpasses:
handler: src/index.pkpasses
events:
- httpApi:
path: /pkpasses
method: get
custom:
serverless-offline:
httpPort: 8080
s3:
directory: /tmp

View File

@@ -0,0 +1,43 @@
import { ALBEvent, ALBResult } from "aws-lambda";
import { PKPass } from "../../../../lib";
import { finish400WithoutModelName, createPassGenerator } from "../shared";
/**
* Lambda for barcodes example
*/
export async function barcodes(event: ALBEvent) {
finish400WithoutModelName(event);
const { modelName, alt, ...passOptions } = event.queryStringParameters;
const passGenerator = createPassGenerator(modelName, passOptions);
const pass = (await passGenerator.next()).value as PKPass;
if (alt === "true") {
// After this, pass.props["barcodes"] will have support for all the formats
pass.setBarcodes("Thank you for using this package <3");
console.log(
"Barcodes support is autocompleted:",
pass.props["barcodes"],
);
} else {
// After this, pass.props["barcodes"] will have support for just two of three
// of the passed format (the valid ones);
pass.setBarcodes(
{
message: "Thank you for using this package <3",
format: "PKBarcodeFormatCode128",
},
{
message: "Thank you for using this package <3",
format: "PKBarcodeFormatPDF417",
},
);
}
return (await passGenerator.next(pass as PKPass)).value as ALBResult;
}

View File

@@ -0,0 +1,31 @@
import { ALBEvent, ALBResult } from "aws-lambda";
import { Context } from "vm";
import { PKPass } from "passkit-generator";
import { finish400WithoutModelName, createPassGenerator } from "../shared";
/**
* Lambda for expirationDate example
*/
export async function expirationDate(event: ALBEvent, context: Context) {
finish400WithoutModelName(event);
const { modelName, ...passOptions } = event.queryStringParameters;
const passGenerator = createPassGenerator(modelName, passOptions);
const pass = (await passGenerator.next()).value as PKPass;
// 2 minutes later...
const d = new Date();
d.setMinutes(d.getMinutes() + 2);
// setting the expiration
(pass as PKPass).setExpirationDate(d);
console.log(
"EXPIRATION DATE EXPECTED:",
(pass as PKPass).props["expirationDate"],
);
return (await passGenerator.next(pass as PKPass)).value as ALBResult;
}

View File

@@ -0,0 +1,160 @@
import { ALBEvent, ALBResult } from "aws-lambda";
import { PKPass } from "passkit-generator";
import { createPassGenerator } from "../shared";
/**
* Lambda for fields example
*/
export async function fields(event: ALBEvent) {
const { modelName, ...passOptions } = event.queryStringParameters;
const passGenerator = createPassGenerator("exampleBooking", passOptions);
const pass = (await passGenerator.next()).value as PKPass;
pass.transitType = "PKTransitTypeAir";
pass.headerFields.push(
{
key: "header1",
label: "Data",
value: "25 mag",
textAlignment: "PKTextAlignmentCenter",
},
{
key: "header2",
label: "Volo",
value: "EZY997",
textAlignment: "PKTextAlignmentCenter",
},
);
pass.primaryFields.push(
{
key: "IATA-source",
value: "NAP",
label: "Napoli",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "IATA-destination",
value: "VCE",
label: "Venezia Marco Polo",
textAlignment: "PKTextAlignmentRight",
},
);
pass.secondaryFields.push(
{
key: "secondary1",
label: "Imbarco chiuso",
value: "18:40",
textAlignment: "PKTextAlignmentCenter",
},
{
key: "sec2",
label: "Partenze",
value: "19:10",
textAlignment: "PKTextAlignmentCenter",
},
{
key: "sec3",
label: "SB",
value: "Sì",
textAlignment: "PKTextAlignmentCenter",
},
{
key: "sec4",
label: "Imbarco",
value: "Anteriore",
textAlignment: "PKTextAlignmentCenter",
},
);
pass.auxiliaryFields.push(
{
key: "aux1",
label: "Passeggero",
value: "MR. WHO KNOWS",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "aux2",
label: "Posto",
value: "1A*",
textAlignment: "PKTextAlignmentCenter",
},
);
pass.backFields.push(
{
key: "document number",
label: "Numero documento:",
value: "- -",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "You're checked in, what next",
label: "Hai effettuato il check-in, Quali sono le prospettive",
value: "",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "Check In",
label: "1. check-in✓",
value: "",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "checkIn",
label: "",
value: "Le uscite d'imbarco chiudono 30 minuti prima della partenza, quindi sii puntuale. In questo aeroporto puoi utilizzare la corsia Fast Track ai varchi di sicurezza.",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "2. Bags",
label: "2. Bagaglio",
value: "",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "Require special assistance",
label: "Assistenza speciale",
value: "Se hai richiesto assistenza speciale, presentati a un membro del personale nell'area di Consegna bagagli almeno 90 minuti prima del volo.",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "3. Departures",
label: "3. Partenze",
value: "",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "photoId",
label: "Un documento didentità corredato di fotografia",
value: "è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta didentità.",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "yourSeat",
label: "Il tuo posto:",
value: "verifica il tuo numero di posto nella parte superiore. Durante limbarco utilizza le scale anteriori e posteriori: per le file 1-10 imbarcati dalla parte anteriore; per le file 11-31 imbarcati dalla parte posteriore. Colloca le borse di dimensioni ridotte sotto il sedile davanti a te.",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "Pack safely",
label: "Bagaglio sicuro",
value: "Fai clic http://easyjet.com/it/articoli-pericolosi per maggiori informazioni sulle merci pericolose oppure visita il sito CAA http://www.caa.co.uk/default.aspx?catid=2200",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "Thank you for travelling easyJet",
label: "Grazie per aver viaggiato con easyJet",
value: "",
textAlignment: "PKTextAlignmentLeft",
},
);
return (await passGenerator.next(pass as PKPass)).value as ALBResult;
}

View File

@@ -0,0 +1,6 @@
export * from "./barcodes";
export * from "./expirationDate";
export * from "./fields";
export * from "./localize";
export * from "./pkpasses";
export * from "./scratch";

View File

@@ -0,0 +1,45 @@
import { finish400WithoutModelName, createPassGenerator } from "../shared";
import type { ALBEvent, ALBResult } from "aws-lambda";
import type { PKPass } from "passkit-generator";
import { localizationSymbol } from "passkit-generator/lib/PKPass";
/**
* Lambda for localize example
*/
export async function localize(event: ALBEvent) {
finish400WithoutModelName(event);
const { modelName, ...passOptions } = event.queryStringParameters;
const passGenerator = createPassGenerator(modelName, passOptions);
const pass = (await passGenerator.next()).value as PKPass;
/**
* Italian and German already has an .lproj which gets included
* but it doesn't have translations
*/
pass.localize("it", {
EVENT: "Evento",
LOCATION: "Dove",
});
pass.localize("de", {
EVENT: "Ereignis",
LOCATION: "Ort",
});
// ...while Norwegian doesn't, so it gets created
pass.localize("nn", {
EVENT: "Begivenhet",
LOCATION: "plassering",
});
console.log(
"Added languages",
Object.keys(pass[localizationSymbol]).join(", "),
);
return (await passGenerator.next(pass as PKPass)).value as ALBResult;
}

View File

@@ -0,0 +1,115 @@
import { ALBEvent } from "aws-lambda";
import { PKPass } from "passkit-generator";
import {
getCertificates,
getSpecificFileInModel,
getS3Instance,
getRandomColorPart,
finish400WithoutModelName,
} from "../shared";
import config from "../../config.json";
/**
* Lambda for PkPasses example
*/
export async function pkpasses(event: ALBEvent) {
finish400WithoutModelName(event);
const [certificates, iconFromModel, s3] = await Promise.all([
getCertificates(),
getSpecificFileInModel(
"icon.png",
event.queryStringParameters.modelName,
),
getS3Instance(),
]);
function createPass() {
const pass = new PKPass({}, certificates, {
description: "Example Apple Wallet Pass",
passTypeIdentifier: "pass.com.passkitgenerator",
serialNumber: "nmyuxofgna",
organizationName: `Test Organization ${Math.random()}`,
teamIdentifier: "F53WB8AE67",
foregroundColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
labelColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
backgroundColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
});
pass.type = "boardingPass";
pass.transitType = "PKTransitTypeAir";
pass.headerFields.push(
{
key: "header-field-test-1",
value: "Unknown",
},
{
key: "header-field-test-2",
value: "unknown",
},
);
pass.primaryFields.push(
{
key: "primaryField-1",
value: "NAP",
},
{
key: "primaryField-2",
value: "VCE",
},
);
/**
* Required by Apple. If one is not available, a
* pass might be openable on a Mac but not on a
* specific iPhone model
*/
pass.addBuffer("icon.png", iconFromModel);
pass.addBuffer("icon@2x.png", iconFromModel);
pass.addBuffer("icon@3x.png", iconFromModel);
return pass;
}
const passes = await Promise.all([
Promise.resolve(createPass()),
Promise.resolve(createPass()),
Promise.resolve(createPass()),
Promise.resolve(createPass()),
]);
const pkpasses = PKPass.pack(...passes);
const buffer = pkpasses.getAsBuffer();
const passName = `GeneratedPass-${Math.random()}.pkpass`;
const { Location } = await s3
.upload({
Bucket: config.PASSES_S3_TEMP_BUCKET,
Key: passName,
ContentType: pkpasses.mimeType,
/** Marking it as expiring in 5 minutes, because passes should not be stored */
Expires: new Date(Date.now() + 5 * 60 * 1000),
Body: buffer,
})
.promise();
/**
* Please note that redirection to `Location` does not work
* if you open this code in another device if this is run
* offline. This because `Location` is on localhost. Didn't
* find yet a way to solve this.
*/
return {
statusCode: 302,
headers: {
"Content-Type": "application/vnd.apple.pkpass",
Location: Location,
},
};
}

View File

@@ -0,0 +1,71 @@
import { ALBEvent, ALBResult } from "aws-lambda";
import { PKPass } from "passkit-generator";
import {
createPassGenerator,
getRandomColorPart,
getSpecificFileInModel,
} from "../shared";
/**
* Lambda for scratch example
*/
export async function scratch(event: ALBEvent) {
const passGenerator = createPassGenerator(undefined, {
description: "Example Apple Wallet Pass",
passTypeIdentifier: "pass.com.passkitgenerator",
serialNumber: "nmyuxofgna",
organizationName: `Test Organization ${Math.random()}`,
teamIdentifier: "F53WB8AE67",
foregroundColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
labelColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
backgroundColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
});
const [{ value }, iconFromModel] = await Promise.all([
passGenerator.next(),
getSpecificFileInModel(
"icon.png",
event.queryStringParameters.modelName,
),
]);
const pass = value as PKPass;
pass.type = "boardingPass";
pass.transitType = "PKTransitTypeAir";
pass.headerFields.push(
{
key: "header-field-test-1",
value: "Unknown",
},
{
key: "header-field-test-2",
value: "unknown",
},
);
pass.primaryFields.push(
{
key: "primaryField-1",
value: "NAP",
},
{
key: "primaryField-2",
value: "VCE",
},
);
/**
* Required by Apple. If one is not available, a
* pass might be openable on a Mac but not on a
* specific iPhone model
*/
pass.addBuffer("icon.png", iconFromModel);
pass.addBuffer("icon@2x.png", iconFromModel);
pass.addBuffer("icon@3x.png", iconFromModel);
return (await passGenerator.next(pass as PKPass)).value as ALBResult;
}

View File

@@ -0,0 +1 @@
export * from "./functions";

View File

@@ -0,0 +1,176 @@
import { ALBEvent, ALBResult } from "aws-lambda";
import AWS from "aws-sdk";
import { promises as fs } from "fs";
import path from "path";
import config from "../config.json";
import { PKPass } from "passkit-generator";
const S3: { instance: AWS.S3 } = { instance: undefined };
export function finish400WithoutModelName(event: ALBEvent) {
if (event.queryStringParameters?.modelName) {
return;
}
return {
statusCode: 400,
body: JSON.stringify({
message: "modelName is missing in query params",
}),
};
}
export function getRandomColorPart() {
return Math.floor(Math.random() * 255);
}
export async function getModel(
modelName: string,
): Promise<string | { [key: string]: Buffer }> {
if (process.env.IS_OFFLINE === "true") {
console.log("model offline retrieving");
return path.resolve(__dirname, `../../models/${modelName}`);
}
const s3 = await getS3Instance();
const result = await s3
.getObject({ Bucket: config.MODELS_S3_BUCKET, Key: modelName })
.promise();
return {}; // @TODO, like when it is run on s3
}
export async function getCertificates(): Promise<{
signerCert: string | Buffer;
signerKey: string | Buffer;
wwdr: string | Buffer;
signerKeyPassphrase?: string;
}> {
let signerCert: string;
let signerKey: string;
let wwdr: string;
let signerKeyPassphrase: string;
if (process.env.IS_OFFLINE) {
console.log("Fetching Certificates locally");
[signerCert, signerKey, wwdr, signerKeyPassphrase] = await Promise.all([
fs.readFile(
path.resolve(__dirname, "../../../certificates/signerCert.pem"),
"utf-8",
),
fs.readFile(
path.resolve(__dirname, "../../../certificates/signerKey.pem"),
"utf-8",
),
fs.readFile(
path.resolve(__dirname, "../../../certificates/WWDR.pem"),
"utf-8",
),
Promise.resolve(config.SIGNER_KEY_PASSPHRASE),
]);
} else {
// @TODO
}
return {
signerCert,
signerKey,
wwdr,
signerKeyPassphrase,
};
}
export async function getS3Instance() {
if (S3.instance) {
return S3.instance;
}
const instance = new AWS.S3({
s3ForcePathStyle: true,
accessKeyId: process.env.IS_OFFLINE ? "S3RVER" : config.ACCESS_KEY_ID, // This specific key is required when working offline
secretAccessKey: config.SECRET_ACCESS_KEY,
endpoint: new AWS.Endpoint("http://localhost:4569"),
});
S3.instance = instance;
try {
/** Trying to create a new bucket. If it fails, it already exists (at least in theory) */
await instance
.createBucket({ Bucket: config.PASSES_S3_TEMP_BUCKET })
.promise();
} catch (err) {}
return instance;
}
export async function getSpecificFileInModel(
fileName: string,
modelName: string,
) {
const model = await getModel(modelName);
if (typeof model === "string") {
return fs.readFile(path.resolve(`${model}.pass`, fileName));
}
return model[fileName];
}
export async function* createPassGenerator(
modelName?: string,
passOptions?: Object,
): AsyncGenerator<PKPass, ALBResult, PKPass> {
const [template, certificates, s3] = await Promise.all([
modelName ? getModel(modelName) : Promise.resolve({}),
getCertificates(),
getS3Instance(),
]);
let pass: PKPass;
if (template instanceof Object) {
pass = new PKPass(template, certificates, passOptions);
} else if (typeof template === "string") {
pass = await PKPass.from(
{
model: template,
certificates,
},
passOptions,
);
}
pass = yield pass;
const buffer = pass.getAsBuffer();
const passName = `GeneratedPass-${Math.random()}.pkpass`;
const { Location } = await s3
.upload({
Bucket: config.PASSES_S3_TEMP_BUCKET,
Key: passName,
ContentType: pass.mimeType,
/** Marking it as expiring in 5 minutes, because passes should not be stored */
Expires: new Date(Date.now() + 5 * 60 * 1000),
Body: buffer,
})
.promise();
/**
* Please note that redirection to `Location` does not work
* if you open this code in another device if this is run
* offline. This because `Location` is on localhost. Didn't
* find yet a way to solve this.
*/
return {
statusCode: 302,
headers: {
"Content-Type": "application/vnd.apple.pkpass",
Location: Location,
},
};
}

View File

@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
"outDir": "build",
"moduleResolution": "node",
"resolveJsonModule": true
},
"exclude": ["node_modules"]
}