From bd6c856ebb6b71f5515733535ee07721035ccd8e Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 11 May 2019 14:12:16 +0200 Subject: [PATCH 001/127] Prepared project to move to Typescript --- .gitignore | 1 + .npmignore | 2 ++ package.json | 4 +++- tsconfig.json | 11 +++++++++++ 4 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index a23e66e..0f2d170 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ passModels/ certificates/ *.code-workspace .vscode/ +*.js diff --git a/.npmignore b/.npmignore index a680d0e..322b356 100644 --- a/.npmignore +++ b/.npmignore @@ -3,3 +3,5 @@ certificates/ *.code-workspace examples/ .vscode/ +*.ts +!*.d.ts diff --git a/package.json b/package.json index 389e9c3..1efd4b8 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "description": "The easiest way to generate custom Apple Wallet passes in Node.js", "main": "index.js", "scripts": { - "test": "jasmine spec/index.js" + "build": "tsc", + "prepublish": "npm run build", + "test": "npm run build && jasmine spec/index.js" }, "author": "Alexander Patrick Cerutti", "license": "MIT", diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6665423 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "commonjs", + "outDir": "dist", + "target": "es2018", + "esModuleInterop": true, + "newLine": "LF", + "noImplicitAny": true, + "noUnusedLocals": true, + } +} From b57c3bf61d586acda6be6a074ecdb278fd896d91 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 11 May 2019 15:09:07 +0200 Subject: [PATCH 002/127] Added typings and typescript to devDependencies --- package-lock.json | 81 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 9 +++++- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index aef5a5d..9287980 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,81 @@ "defer-to-connect": "^1.0.1" } }, + "@types/archiver": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-2.1.3.tgz", + "integrity": "sha512-x37dj6VvV8jArjvqvZP+qz5+24qOwgFesLMvn98uNz8qebjCg+uteqquRf9mqaxxhcM7S1vPl4YFhBs2/abcFQ==", + "dev": true, + "requires": { + "@types/glob": "*" + } + }, + "@types/debug": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.4.tgz", + "integrity": "sha512-D9MyoQFI7iP5VdpEyPZyjjqIJ8Y8EDNQFIFVLOmeg1rI1xiHOChyUPMPRUVfqFCerxfE+yS3vMyj37F6IdtOoQ==", + "dev": true + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true + }, + "@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/got": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/@types/got/-/got-9.4.4.tgz", + "integrity": "sha512-IGAJokJRE9zNoBdY5csIwN4U5qQn+20HxC0kM+BbUdfTKIXa7bOX/pdhy23NnLBRP8Wvyhx7X5e6EHJs+4d8HA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/tough-cookie": "*" + } + }, + "@types/joi": { + "version": "14.3.3", + "resolved": "https://registry.npmjs.org/@types/joi/-/joi-14.3.3.tgz", + "integrity": "sha512-6gAT/UkIzYb7zZulAbcof3lFxpiD5EI6xBeTvkL1wYN12pnFQ+y/+xl9BvnVgxkmaIDN89xWhGZLD9CvuOtZ9g==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/node": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.0.tgz", + "integrity": "sha512-Jrb/x3HT4PTJp6a4avhmJCDEVrPdqLfl3e8GGMbpkGGdwAV5UGlIs4vVEfsHHfylZVOKZWpOqmqFH8CbfOZ6kg==", + "dev": true + }, + "@types/node-forge": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.8.2.tgz", + "integrity": "sha512-tj6TSHR2JUvHMryMTsy+vAZ4Fnu6EBxJ22CAGhm+B1/B5ASMeYTOfeIa8KD6Y+GGMvm0Gora45NptakUAnwy0g==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/tough-cookie": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", + "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==", + "dev": true + }, "archiver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.0.0.tgz", @@ -586,6 +661,12 @@ } } }, + "typescript": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", + "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", + "dev": true + }, "url-parse-lax": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", diff --git a/package.json b/package.json index 1efd4b8..d2f123f 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,13 @@ "node": ">=8.1.0" }, "devDependencies": { - "jasmine": "^3.4.0" + "@types/archiver": "^2.1.3", + "@types/debug": "^4.1.4", + "@types/got": "^9.4.4", + "@types/joi": "^14.3.3", + "@types/node": "^12.0.0", + "@types/node-forge": "^0.8.2", + "jasmine": "^3.4.0", + "typescript": "^3.4.5" } } From 2657ba272fec75ec64f0f6855ab4e641f636fbb4 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 11 May 2019 15:10:44 +0200 Subject: [PATCH 003/127] Moved Schema.js to typescript with Joi-matched interfaces --- src/{schema.js => schema.ts} | 212 ++++++++++++++++++++++++++++++++--- 1 file changed, 195 insertions(+), 17 deletions(-) rename src/{schema.js => schema.ts} (61%) diff --git a/src/schema.js b/src/schema.ts similarity index 61% rename from src/schema.js rename to src/schema.ts index c72db2a..f9f4d37 100644 --- a/src/schema.js +++ b/src/schema.ts @@ -1,5 +1,35 @@ -const Joi = require("joi"); -const debug = require("debug")("Schema"); +import Joi from "joi"; +import debug from "debug"; + +const schemaDebug = debug("Schema"); + +export interface FactoryOptions { + model: { [key: string]: Buffer } | string; + certificates: { + wwdr: string; + signerCert: string; + signerKey: { + keyFile: string; + passphrase?: string; + }; + }; + overrides?: Object; + shouldOverwrite?: boolean; +} + +export interface PassInstance { + model: { [key: string]: Buffer }; + certificates: { + wwdr: string; + signerCert: string; + signerKey: { + keyFile: string; + passphrase?: string; + }; + }; + overrides?: OverridesSupportedOptions; + shouldOverwrite?: boolean; +} const instance = Joi.object().keys({ model: Joi.string().required(), @@ -15,6 +45,20 @@ const instance = Joi.object().keys({ shouldOverwrite: Joi.boolean() }); +interface OverridesSupportedOptions { + serialNumber?: string; + description?: string; + userInfo?: Object | Array; + webServiceURL?: string; + authenticationToken?: string; + sharingProhibited?: boolean; + backgroundColor?: string; + foregroundColor?: string; + labelColor?: string; + groupingIdentifier?: string; + suppressStripShine?: boolean; +} + const supportedOptions = Joi.object().keys({ serialNumber: Joi.string(), description: Joi.string(), @@ -34,16 +78,35 @@ const supportedOptions = Joi.object().keys({ /* For a correct usage of semantics, please refer to https://apple.co/2I66Phk */ +interface CurrencyAmount { + currencyCode: string; + amount: string; +} + const currencyAmount = Joi.object().keys({ currencyCode: Joi.string().required(), amount: Joi.string().required(), }); +interface PersonNameComponent { + givenName: string; + familyName: string; +} + const personNameComponents = Joi.object().keys({ givenName: Joi.string().required(), familyName: Joi.string().required() }); +interface Seat { + seatSection?: string; + seatRow?: string; + seatNumber?: string; + seatIdentifier?: string; + seatType?: string; + seatDescription?: string; +} + const seat = Joi.object().keys({ seatSection: Joi.string(), seatRow: Joi.string(), @@ -58,6 +121,69 @@ const location = Joi.object().keys({ longitude: Joi.number().required() }); +interface Semantics { + totalPrice?: CurrencyAmount; + duration?: number; + seats?: Seat[]; + silenceRequested?: boolean; + departureLocation?: Location; + destinationLocation?: Location; + destinationLocationDescription?: Location; + transitProvider?: string; + vehicleName?: string; + vehicleType?: string; + originalDepartureDate?: string; + currentDepartureDate?: string; + originalArrivalDate?: string; + currentArrivalDate?: string; + originalBoardingDate?: string; + currentBoardingDate?: string; + boardingGroup?: string; + boardingSequenceNumber?: string; + confirmationNumber?: string; + transitStatus?: string; + transitStatuReason?: string; + passengetName?: PersonNameComponent; + membershipProgramName?: string; + membershipProgramNumber?: string; + priorityStatus?: string; + securityScreening?: string; + flightCode?: string; + airlineCode?: string; + flightNumber?: number; + departureAirportCode?: string; + departureAirportName?: string; + destinationTerminal?: string; + destinationGate?: string; + departurePlatform?: string; + departureStationName?: string; + destinationPlatform?: string; + destinationStationName?: string; + carNumber?: string; + eventName?: string; + venueName?: string; + venueLocation?: Location; + venueEntrance?: string; + venuePhoneNumber?: string; + venueRoom?: string; + eventType?: "PKEventTypeGeneric" | "PKEventTypeLivePerformance" | "PKEventTypeMovie" | "PKEventTypeSports" | "PKEventTypeConference" | "PKEventTypeConvention" | "PKEventTypeWorkshop" | "PKEventTypeSocialGathering"; + eventStartDate?: string; + eventEndDate?: string; + artistIDs?: string; + performerNames?: string[]; + genre?: string; + leagueName?: string; + leagueAbbreviation?: string; + homeTeamLocation?: string; + homeTeamName?: string; + homeTeamAbbreviation?: string; + awayTeamLocation?: string; + awayTeamName?: string; + awayTeamAbbreviation?: string; + sportName?: string; + balance?: CurrencyAmount; +} + const semantics = Joi.object().keys({ // All totalPrice: currencyAmount, @@ -129,6 +255,13 @@ const semantics = Joi.object().keys({ balance: currencyAmount }); +export interface Barcode { + altText?: string; + messageEncoding?: string; + format: string; + message: string; +} + const barcode = Joi.object().keys({ altText: Joi.string(), messageEncoding: Joi.string().default("iso-8859-1"), @@ -136,6 +269,24 @@ const barcode = Joi.object().keys({ message: Joi.string().required() }); +export interface Field { + attributedValue?: string | number | Date; + changeMessage?: string; + dataDetectorType?: string[]; + label?: string; + textAlignment?: string; + key: string; + value: string | number | Date; + semantics: Semantics; + dateStyle?: string; + ignoreTimeZone?: boolean; + isRelative?: boolean; + timeStyle?: string; + currencyCode?: string; + numberStyle?: string; + row?: number; +} + const field = Joi.object().keys({ attributedValue: Joi.alternatives(Joi.string().allow(""), Joi.number(), Joi.date().iso()), changeMessage: Joi.string(), @@ -164,6 +315,13 @@ const field = Joi.object().keys({ }), }); +export interface Beacon { + major?: number; + minor?: number; + relevantText?: string; + proximityUUID: string; +} + const beaconsDict = Joi.object().keys({ major: Joi.number().integer().positive().max(65535).greater(Joi.ref("minor")), minor: Joi.number().integer().positive().max(65535).less(Joi.ref("major")), @@ -171,6 +329,13 @@ const beaconsDict = Joi.object().keys({ relevantText: Joi.string() }); +export interface Location { + relevantText?: string; + altitude?: number; + latitude: number; + longitude: number; +} + const locationsDict = Joi.object().keys({ altitude: Joi.number(), latitude: Joi.number().required(), @@ -178,6 +343,14 @@ const locationsDict = Joi.object().keys({ relevantText: Joi.string() }); +export interface Pass { + auxiliaryFields: Field[]; + backFields: Field[]; + headerFields: Field[]; + primaryFields: Field[]; + secondaryFields: Field[]; +} + const passDict = Joi.object().keys({ auxiliaryFields: Joi.array().items(Joi.object().keys({ row: Joi.number().max(1).min(0) @@ -188,8 +361,15 @@ const passDict = Joi.object().keys({ secondaryFields: Joi.array().items(field) }); +export type TransitType = "PKTransitTypeAir" | "PKTransitTypeBoat" | "PKTransitTypeBus" | "PKTransitTypeGeneric" | "PKTransitTypeTrain"; + const transitType = Joi.string().regex(/(PKTransitTypeAir|PKTransitTypeBoat|PKTransitTypeBus|PKTransitTypeGeneric|PKTransitTypeTrain)/); +export interface NFC { + message: string; + encryptionPublicKey?: string; +} + const nfcDict = Joi.object().keys({ message: Joi.string().required().max(64), encryptionPublicKey: Joi.string() @@ -197,7 +377,10 @@ const nfcDict = Joi.object().keys({ // --------- UTILITIES ---------- // -const schemas = { +type Schemas = { + [index: string]: Joi.ObjectSchema | Joi.StringSchema; +}; +const schemas: Schemas = { instance, barcode, field, @@ -209,8 +392,8 @@ const schemas = { supportedOptions }; -function resolveSchemaName(name) { - return schemas[name] || ""; +function resolveSchemaName(name: keyof Schemas) { + return schemas[name] || undefined; } /** @@ -220,18 +403,18 @@ function resolveSchemaName(name) { * @returns {boolean} - result of the check */ -function isValid(opts, schemaName) { - let resolvedSchema = resolveSchemaName(schemaName); +export function isValid(opts: any, schemaName: keyof Schemas): boolean { + const resolvedSchema = resolveSchemaName(schemaName); if (!resolvedSchema) { - debug(`validation failed due to missing or mispelled schema name`); + schemaDebug(`validation failed due to missing or mispelled schema name`); return false; } - let validation = Joi.validate(opts, resolvedSchema); + const validation = Joi.validate(opts, resolvedSchema); if (validation.error) { - debug(`validation failed due to error: ${validation.error.message}`); + schemaDebug(`validation failed due to error: ${validation.error.message}`); } return !validation.error; @@ -244,19 +427,14 @@ function isValid(opts, schemaName) { * @returns {object} the filtered value or empty object */ -function getValidated(opts, schemaName) { +export function getValidated(opts: any, schemaName: keyof Schemas): Object { let resolvedSchema = resolveSchemaName(schemaName); let validation = Joi.validate(opts, resolvedSchema, { stripUnknown: true }); if (validation.error) { - debug(`Validation failed in getValidated due to error: ${validation.error.message}`); + schemaDebug(`Validation failed in getValidated due to error: ${validation.error.message}`); return null; } return validation.value; } - -module.exports = { - isValid, - getValidated -}; From 680ef5b8b714b023b6532da4bc8cdb7dc9144bd9 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 11 May 2019 20:48:58 +0200 Subject: [PATCH 004/127] Moved utils.js to typescript --- src/{utils.js => utils.ts} | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) rename src/{utils.js => utils.ts} (77%) diff --git a/src/utils.js b/src/utils.ts similarity index 77% rename from src/utils.js rename to src/utils.ts index 61c7504..77f1243 100644 --- a/src/utils.js +++ b/src/utils.ts @@ -1,5 +1,5 @@ -const moment = require("moment"); -const { EOL } = require("os"); +import moment from "moment"; +import { EOL } from "os"; /** * Checks if an rgb value is compliant with CSS-like syntax @@ -9,7 +9,7 @@ const { EOL } = require("os"); * @returns {Boolean} True if valid rgb, false otherwise */ -function isValidRGB(value) { +export function isValidRGB(value: string): boolean { if (!value || typeof value !== "string") { return false; } @@ -33,7 +33,7 @@ function isValidRGB(value) { * undefined otherwise */ -function dateToW3CString(date, format) { +export function dateToW3CString(date: string | Date, format?: string) { if (typeof date !== "string" && !(date instanceof Date)) { return ""; } @@ -55,7 +55,7 @@ function dateToW3CString(date, format) { * @return {String[]} */ -function removeHidden(from) { +export function removeHidden(from: Array): Array { return from.filter(e => e.charAt(0) !== "."); } @@ -68,7 +68,7 @@ function removeHidden(from) { * @see https://apple.co/2M9LWVu - String Resources */ -function generateStringFile(lang) { +export function generateStringFile(lang: { [index: string]: string }): Buffer { if (!Object.keys(lang).length) { return Buffer.from("", "utf8"); } @@ -77,7 +77,7 @@ function generateStringFile(lang) { // "key" = "value"; const strings = Object.keys(lang) - .map(key => `"${key}" = "${lang[key].replace(/"/g, /\\"/)}";`); + .map(key => `"${key}" = "${lang[key].replace(/"/g, '\"')}";`); return Buffer.from(strings.join(EOL), "utf8"); } @@ -88,14 +88,6 @@ function generateStringFile(lang) { * @param {Array>} source - the main sources of properties */ -function assignLength(length, ...sources) { +export function assignLength(length: number, ...sources: Array<{ [key: string]: any }>): Array<{ [key: string]: any }> { return Object.assign({ length }, ...sources); } - -module.exports = { - assignLength, - generateStringFile, - removeHidden, - dateToW3CString, - isValidRGB -}; From 464f23e96535a19dadbb1cd164ad968c24d5205b Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 11 May 2019 20:53:19 +0200 Subject: [PATCH 005/127] Moved messages.js to typescript --- src/{messages.js => messages.ts} | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) rename src/{messages.js => messages.ts} (92%) diff --git a/src/messages.js b/src/messages.ts similarity index 92% rename from src/messages.js rename to src/messages.ts index 19b1261..6fa1c95 100644 --- a/src/messages.js +++ b/src/messages.ts @@ -1,4 +1,8 @@ -const errors = { +interface MessageGroup { + [key: string]: string; +} + +const errors: MessageGroup = { PASSFILE_VALIDATION_FAILED: "Validation of pass type failed. Pass file is not a valid buffer or (more probably) does not respect the schema.\nRefer to https://apple.co/2Nvshvn to build a correct pass.", REQUIR_VALID_FAILED: "The options passed to Pass constructor does not meet the requirements.\nRefer to the documentation to compile them correctly.", MODEL_UNINITIALIZED: "Provided model ( %s ) matched but unitialized or may not contain icon.\nRefer to https://apple.co/2IhJr0Q, https://apple.co/2Nvshvn and documentation to fill the model correctly.", @@ -11,7 +15,7 @@ const errors = { NO_PASS_TYPE: "Cannot proceed with pass creation. Model definition (pass.json) has no valid type in it.\nRefer to https://apple.co/2wzyL5J to choose a valid pass type." }; -const debugMessages = { +const debugMessages: MessageGroup = { TRSTYPE_NOT_VALID: "Transit type changing rejected as not compliant with Apple Specifications. Transit type would become \"%s\" but should be in [PKTransitTypeAir, PKTransitTypeBoat, PKTransitTypeBus, PKTransitTypeGeneric, PKTransitTypeTrain]", BRC_NOT_SUPPORTED: "Format not found among barcodes. Cannot set backward compatibility.", BRC_FORMATTYPE_UNMATCH: "Format must be a string or null. Cannot set backward compatibility.", @@ -29,7 +33,7 @@ const debugMessages = { * @param {any[]} values */ -function format(messageName, ...values) { +export default function format(messageName: string, ...values: any[]) { // reversing because it is better popping than shifting. let replaceValues = values.reverse(); return resolveMessageName(messageName).replace(/%s/g, () => { @@ -43,12 +47,10 @@ function format(messageName, ...values) { * @param {string} name */ -function resolveMessageName(name) { +function resolveMessageName(name: string): string { if (!errors[name] && !debugMessages[name]) { return ``; } return errors[name] || debugMessages[name]; } - -module.exports = format; From 7ee36c6f874a8ac9e7b4e4193a520c83c2d02c29 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 11 May 2019 21:07:27 +0200 Subject: [PATCH 006/127] Moved fieldsArray to typescript --- src/{fieldsArray.js => fieldsArray.ts} | 37 +++++++++++++------------- 1 file changed, 18 insertions(+), 19 deletions(-) rename src/{fieldsArray.js => fieldsArray.ts} (55%) diff --git a/src/fieldsArray.js b/src/fieldsArray.ts similarity index 55% rename from src/fieldsArray.js rename to src/fieldsArray.ts index 68a4dc2..114b94d 100644 --- a/src/fieldsArray.js +++ b/src/fieldsArray.ts @@ -1,5 +1,7 @@ -const schema = require("./schema"); -const debug = require("debug")("passkit:fields"); +import * as schema from "./schema"; +import debug from "debug"; + +const fieldsDebug = debug("passkit:fields"); /** * Class to represent lower-level keys pass fields @@ -8,9 +10,8 @@ const debug = require("debug")("passkit:fields"); const poolSymbol = Symbol("pool"); -class FieldsArray extends Array { - - constructor(pool,...args) { +export default class FieldsArray extends Array { + constructor(pool: Set, ...args: any[]) { super(...args); this[poolSymbol] = pool; } @@ -20,18 +21,18 @@ class FieldsArray extends Array { * also uniqueKeys set. */ - push(...fieldsData) { - const validFields = fieldsData.reduce((acc, current) => { + push(...fieldsData: schema.Field[]): number { + const validFields = fieldsData.reduce((acc: schema.Field[], current: schema.Field) => { if (!(typeof current === "object") || !schema.isValid(current, "field")) { return acc; } if (acc.some(e => e.key === current.key) || this[poolSymbol].has(current.key)) { - debug(`Field with key "${key}" discarded: fields must be unique in pass scope.`); - } - - this[poolSymbol].add(current.key) - acc.push(current) + 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; }, []); @@ -44,9 +45,9 @@ class FieldsArray extends Array { * also uniqueKeys set */ - pop() { - const element = Array.prototype.pop.call(this); - this[poolSymbol].delete(element.key) + pop(): schema.Field { + const element: schema.Field = Array.prototype.pop.call(this); + this[poolSymbol].delete(element.key); return element; } @@ -55,16 +56,14 @@ class FieldsArray extends Array { * also uniqueKeys set */ - splice(start, deleteCount, ...items) { + splice(start: number, deleteCount: number, ...items: schema.Field[]): schema.Field[] { const removeList = this.slice(start, deleteCount+start); removeList.forEach(item => this[poolSymbol].delete(item.key)); return Array.prototype.splice.call(this, start, deleteCount, items); } - get length() { + get length(): number { return this.length; } } - -module.exports = FieldsArray; From 9609187fa5af66dc17c5642a93eca1b6bbabbf5c Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Mon, 20 May 2019 22:08:08 +0200 Subject: [PATCH 007/127] Moved pass.json to typescript; Removed load method from pass implementation --- API.md | 40 ------------- README.md | 2 +- package.json | 2 - src/messages.ts | 5 +- src/{pass.js => pass.ts} | 124 +++++++++++++-------------------------- 5 files changed, 43 insertions(+), 130 deletions(-) rename src/{pass.js => pass.ts} (90%) diff --git a/API.md b/API.md index 5cddabd..a65d555 100644 --- a/API.md +++ b/API.md @@ -44,8 +44,6 @@ ___ * [.relevance()](#method_relevance) * Setting NFC * [.nfc()](#method_nfc) - * Getting remote resources - * [.load()](#method_load) * [Setting Pass Structure Keys (primaryFields, secondaryFields, ...)](#prop_fields) * [TransitType](#prop_transitType) * Generating the compiled pass. @@ -402,44 +400,6 @@ ___ **Getting remote resources**: ___ - - -#### .load() - -```javascript -pass.load(resource, name); -``` - -**Returns**: - -`Object (this)` - -**Description**: - -Sets the resources to be downloaded in runtime to be pushed in the pass. -Use `name` param to give your downloaded file a name or to provide the folder path it will be pushed into (with the name, _obv._). - -Requests are not cached and load method can only load pictures right now (no other types should be required). In case of conflict between downloaded files and model files, downloaded files will have the priority and will be putted in the zip file. - -When in debug mode, file header is shown. - -**Arguments**: - -| Key | Type | Description | Optional | Default Value | -|-----|------|-------------|----------|:-------------:| -| resource | String | The URL where to fetch the picture | false | - -| name | String | The name / path to be used to call this | false | - - -**Example**: - -```javascript -pass.load("http://...", "icon.png"); -pass.load("http://...", "en.lproj/icon.png"); -``` - -
-
- ___ diff --git a/README.md b/README.md index 826b3e6..5597c45 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ This package was created with a specific architecture in mind: **application** a Actually, pass creation and population doesn't fully happen within the application in runtime. Pass template is a folder in, for example, _your application directory_ (but nothing will stop you from putting it outside), that will contain all the objects needed (static medias) and structure to make a pass work. -Pass template will be read and pushed as is in the resulting .zip file along with web-fetched medias (also considered dynamic objects), while dynamic objects will be patched against `pass.json` or generated in runtime (`manifest.json`, `signature` and translation files). +Pass template will be read and pushed as is in the resulting .zip file, while dynamic objects will be patched against `pass.json` or generated in runtime (`manifest.json`, `signature` and translation files). This package comes with an [API documentation](./API.md), that makes available a series of methods to customize passes. diff --git a/package.json b/package.json index d2f123f..6fa9a70 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "dependencies": { "archiver": "^3.0.0", "debug": "^3.2.6", - "got": "^9.6.0", "joi": "^13.7.0", "moment": "^2.24.0", "node-forge": "^0.7.6" @@ -32,7 +31,6 @@ "devDependencies": { "@types/archiver": "^2.1.3", "@types/debug": "^4.1.4", - "@types/got": "^9.4.4", "@types/joi": "^14.3.3", "@types/node": "^12.0.0", "@types/node-forge": "^0.8.2", diff --git a/src/messages.ts b/src/messages.ts index 6fa1c95..c70e66f 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -21,10 +21,7 @@ const debugMessages: MessageGroup = { BRC_FORMATTYPE_UNMATCH: "Format must be a string or null. Cannot set backward compatibility.", BRC_AUTC_MISSING_DATA: "Unable to autogenerate barcodes. Data is not a string or an object with no message field", BRC_BW_FORMAT_UNSUPPORTED: "This format is not supported (by Apple) for backward support. Please choose another one.", - DATE_FORMAT_UNMATCH: "%s was not set due to incorrect date format.", - LOAD_TYPES_UNMATCH: "Resource and name are not valid strings. No action will be taken for the specified medias.", - LOAD_MIME: "Picture MIME-type: %s", - LOAD_NORES: "Was not able to fetch resource %s. Error: %s" + DATE_FORMAT_UNMATCH: "%s was not set due to incorrect date format." }; /** diff --git a/src/pass.js b/src/pass.ts similarity index 90% rename from src/pass.js rename to src/pass.ts index 4dc410a..382e38e 100644 --- a/src/pass.js +++ b/src/pass.ts @@ -1,24 +1,22 @@ -const fs = require("fs"); -const path = require("path"); -const { promisify } = require("util"); -const stream = require("stream"); -const forge = require("node-forge"); -const archiver = require("archiver"); -const debug = require("debug"); -const got = require("got"); +import fs from "fs"; +import path from "path"; +import { promisify } from "util"; +import stream from "stream"; +import forge from "node-forge"; +import archiver from "archiver"; +import debug from "debug"; -const barcodeDebug = debug("passkit:barcode"); -const genericDebug = debug("passkit:generic"); -const loadDebug = debug("passkit:load"); - -const schema = require("./schema"); -const formatMessage = require("./messages"); -const FieldsArray = require("./fieldsArray"); -const { +import * as schema from "./schema"; +import formatMessage from "./messages"; +import FieldsArray from "./fieldsArray"; +import { assignLength, generateStringFile, removeHidden, dateToW3CString, isValidRGB -} = require("./utils"); +} from "./utils"; + +const barcodeDebug = debug("passkit:barcode"); +const genericDebug = debug("passkit:generic"); const readdir = promisify(fs.readdir); const readFile = promisify(fs.readFile); @@ -28,8 +26,23 @@ const transitType = Symbol("transitType"); const barcodesFillMissing = Symbol("bfm"); const barcodesSetBackward = Symbol("bsb"); -class Pass { - constructor(options) { +interface PassIndexSignature { + [key: string]: any; +} + +export class Pass implements PassIndexSignature { + private model: string; + private _fields: string[]; + private _props: { [key: string]: any }; + private type: string; + private fieldsKeys: Set; + + Certificates: schema.Certificates; + l10n: { [key: string]: { [key: string]: string } } = {}; + shouldOverwrite: boolean; + [transitType]: string = ""; + + constructor(options: schema.PassInstance) { this.Certificates = { // Even if this assigning will fail, it will be captured below // in _parseSettings, since this won't match with the schema. @@ -38,8 +51,6 @@ class Pass { options.overrides = options.overrides || {}; - this.l10n = {}; - this._remoteResources = []; this.shouldOverwrite = !(options.hasOwnProperty("shouldOverwrite") && !options.shouldOverwrite); this._fields = ["primaryFields", "secondaryFields", "auxiliaryFields", "backFields", "headerFields"]; @@ -69,37 +80,10 @@ class Pass { // Reading the model const modelFilesList = await readdir(this.model); - /** - * Getting the buffers for remote files - */ - - const buffersPromise = await this._remoteResources.reduce(async (acc, current) => { - try { - const response = await got(current[0], { encoding: null }); - loadDebug(formatMessage("LOAD_MIME", response.headers["content-type"])); - - if (!Buffer.isBuffer(response.body)) { - throw "LOADED_RESOURCE_NOT_A_BUFFER"; - } - - if (!response.headers["content-type"].includes("image/")) { - throw "LOADED_RESOURCE_NOT_A_PICTURE"; - } - - return [...acc, response.body]; - } catch (err) { - loadDebug(formatMessage("LOAD_NORES", current[1], err)); - return acc; - } - }, []); - - const remoteFilesList = buffersPromise.length ? this._remoteResources.map(r => r[1]): []; - // list without dynamic components like manifest, signature or pass files (will be added later in the flow) and hidden files. const noDynList = removeHidden(modelFilesList).filter(f => !/(manifest|signature|pass)/i.test(f)); - const hasAssets = noDynList.length || remoteFilesList.length; - if (!hasAssets || ![...noDynList, ...remoteFilesList].some(f => f.toLowerCase().includes("icon"))) { + if (!noDynList.length || !noDynList.some(f => f.toLowerCase().includes("icon"))) { let eMessage = formatMessage("MODEL_UNINITIALIZED", path.parse(this.model).name); throw new Error(eMessage); } @@ -132,12 +116,6 @@ class Pass { /* Getting all bundle file buffers, pass.json included, and appending path */ - if (remoteFilesList.length) { - // Removing files in bundle that also exist in remoteFilesList - // I'm giving priority to downloaded files - bundle = bundle.filter(file => !remoteFilesList.includes(file)); - } - // Reading bundle files to buffers without pass.json - it gets read below // to use a different parsing process @@ -145,7 +123,7 @@ class Pass { const passBuffer = this._extractPassDefinition(); bundle.push("pass.json"); - const buffers = await Promise.all([...bundleBuffers, passBuffer, ...buffersPromise]); + const buffers = await Promise.all([...bundleBuffers, passBuffer]); Object.keys(this.l10n).forEach(lang => { const strings = generateStringFile(this.l10n[lang]); @@ -183,9 +161,6 @@ class Pass { } }); - // Pushing the remote files into the bundle - bundle.push(...remoteFilesList); - /* * Parsing the buffers, pushing them into the archive * and returning the compiled manifest @@ -331,7 +306,7 @@ class Pass { genericDebug(formatMessage("DATE_FORMAT_UNMATCH", "Relevant Date")); return this; } - + let dateParse = dateToW3CString(data, relevanceDateFormat); if (!dateParse) { @@ -506,23 +481,6 @@ class Pass { return this; } - /** - * Loads a web resource (image) - * @param {string} resource - * @param {string} name - */ - - load(resource, name) { - if (typeof resource !== "string" && typeof name !== "string") { - loadDebug(formatMessage("LOAD_TYPES_UNMATCH")); - return; - } - - this._remoteResources.push([resource, name]); - - return this; - } - /** * Checks if pass model type is one of the supported ones * @@ -589,8 +547,9 @@ class Pass { */ signature.addSigner({ - key: this.Certificates.signerKey, + key: this.Certificates.signerKey.keyFile, certificate: this.Certificates.signerCert, + digestAlgorithm: forge.pki.oids.sha1, authenticatedAttributes: [{ type: forge.pki.oids.contentType, value: forge.pki.oids.data @@ -731,7 +690,7 @@ class Pass { * @returns {Object} - parsed certificates to be pushed to Pass.Certificates. */ -function readCertificates(certificates) { +function readCertificates(certificates: schema.Certificates) { if (certificates.wwdr && certificates.signerCert && typeof certificates.signerKey === "object") { // Nothing must be added. Void object is returned. return Promise.resolve({}); @@ -740,14 +699,14 @@ function readCertificates(certificates) { const raw = certificates._raw; const optCertsNames = Object.keys(raw); const certPaths = optCertsNames.map((val) => { - const cert = raw[val]; + const cert: string | typeof certificates.signerKey = raw[val]; // realRawValue exists as signerKey might be an object const realRawValue = !(cert instanceof Object) ? cert : cert["keyFile"]; // We are checking if the string is a path or a content if (!!path.parse(realRawValue).ext) { const resolvedPath = path.resolve(realRawValue); - return readFile(resolvedPath); + return readFile(resolvedPath, { encoding: "utf8" }); } else { return Promise.resolve(realRawValue); } @@ -759,6 +718,7 @@ function readCertificates(certificates) { // which is conjoint later with the other pems return Object.assign( + {}, ...contents.map((file, index) => { const certName = optCertsNames[index]; const pem = parsePEM(certName, file, raw[certName].passphrase); @@ -825,5 +785,3 @@ function barcodesFromUncompleteData(origin) { ) ); } - -module.exports = { Pass }; From 96004162c60e084244d3dfb2de2b3a204ddf5ecc Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Mon, 20 May 2019 22:09:49 +0200 Subject: [PATCH 008/127] Updated types and typescript settings --- package-lock.json | 12 ++++++------ package.json | 4 ++-- tsconfig.json | 2 -- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9287980..f889244 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,9 +18,9 @@ } }, "@types/archiver": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-2.1.3.tgz", - "integrity": "sha512-x37dj6VvV8jArjvqvZP+qz5+24qOwgFesLMvn98uNz8qebjCg+uteqquRf9mqaxxhcM7S1vPl4YFhBs2/abcFQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-3.0.0.tgz", + "integrity": "sha512-orghAMOF+//wSg4ru2znk6jt0eIPvKTtMVLH7XcYcjbcRyAXRClDlh27QVdqnAvVM37yu9xDP6Nh7egRhNr8tQ==", "dev": true, "requires": { "@types/glob": "*" @@ -78,9 +78,9 @@ "dev": true }, "@types/node-forge": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.8.2.tgz", - "integrity": "sha512-tj6TSHR2JUvHMryMTsy+vAZ4Fnu6EBxJ22CAGhm+B1/B5ASMeYTOfeIa8KD6Y+GGMvm0Gora45NptakUAnwy0g==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.8.3.tgz", + "integrity": "sha512-cDc9enmIRJdF5b3rkKsDMBhE/UrvwbDEwCYL8y9k/v7HUWPaSeK6lG2LF1SrrkqFyKPkQBTFL940YZGO+OSbaQ==", "dev": true, "requires": { "@types/node": "*" diff --git a/package.json b/package.json index 6fa9a70..d6d50e9 100644 --- a/package.json +++ b/package.json @@ -29,11 +29,11 @@ "node": ">=8.1.0" }, "devDependencies": { - "@types/archiver": "^2.1.3", + "@types/archiver": "^3.0.0", "@types/debug": "^4.1.4", "@types/joi": "^14.3.3", "@types/node": "^12.0.0", - "@types/node-forge": "^0.8.2", + "@types/node-forge": "^0.8.3", "jasmine": "^3.4.0", "typescript": "^3.4.5" } diff --git a/tsconfig.json b/tsconfig.json index 6665423..aa1517f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,5 @@ "target": "es2018", "esModuleInterop": true, "newLine": "LF", - "noImplicitAny": true, - "noUnusedLocals": true, } } From 5971e478532e4e0d869160d16ed46c4496e2f6a0 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Mon, 20 May 2019 22:11:01 +0200 Subject: [PATCH 009/127] Added Certificates interface --- src/schema.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/schema.ts b/src/schema.ts index f9f4d37..5675bcf 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -3,16 +3,19 @@ import debug from "debug"; const schemaDebug = debug("Schema"); +export interface Certificates { + wwdr?: string; + signerCert?: string; + signerKey?: { + keyFile: string; + passphrase?: string; + }; + _raw?: Certificates; +} + export interface FactoryOptions { model: { [key: string]: Buffer } | string; - certificates: { - wwdr: string; - signerCert: string; - signerKey: { - keyFile: string; - passphrase?: string; - }; - }; + certificates: Certificates; overrides?: Object; shouldOverwrite?: boolean; } From a549d94d677bcf91c143d7740c39fcfd1a790566 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Mon, 20 May 2019 23:32:40 +0200 Subject: [PATCH 010/127] package.json: moved prepublish to prepublishOnly --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d6d50e9..febfcdd 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "build": "tsc", - "prepublish": "npm run build", + "prepublishOnly": "npm run build", "test": "npm run build && jasmine spec/index.js" }, "author": "Alexander Patrick Cerutti", From ffdaf8ac61e47bd688b4e8fadc5f03dfb13ca95c Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Mon, 20 May 2019 23:51:13 +0200 Subject: [PATCH 011/127] Added more return types --- src/pass.ts | 32 ++++++++++++++++---------------- src/schema.ts | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 382e38e..e0adc81 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -1,7 +1,7 @@ import fs from "fs"; import path from "path"; import { promisify } from "util"; -import stream from "stream"; +import stream, { Stream } from "stream"; import forge from "node-forge"; import archiver from "archiver"; import debug from "debug"; @@ -75,7 +75,7 @@ export class Pass implements PassIndexSignature { * @return {Promise} A Promise containing the stream of the generated pass. */ - async generate() { + async generate(): Promise { try { // Reading the model const modelFilesList = await readdir(this.model); @@ -216,7 +216,7 @@ export class Pass implements PassIndexSignature { * @see https://apple.co/2KOv0OW - Passes support localization */ - localize(lang, translations) { + localize(lang: string, translations?: { [key: string]: string }) { if (lang && typeof lang === "string" && (typeof translations === "object" || translations === undefined)) { this.l10n[lang] = translations || {}; } @@ -233,7 +233,7 @@ export class Pass implements PassIndexSignature { * @returns {this} */ - expiration(date, format) { + expiration(date: string | Date, format?: string) { if (typeof date !== "string" && !(date instanceof Date)) { return this; } @@ -257,7 +257,7 @@ export class Pass implements PassIndexSignature { * @return {this} */ - void() { + void(): this { this._props.voided = true; return this; } @@ -272,7 +272,7 @@ export class Pass implements PassIndexSignature { * @return {Number} The quantity of data pushed */ - relevance(type, data, relevanceDateFormat) { + relevance(type: string, data: any, relevanceDateFormat?: string) { let types = ["beacons", "locations", "maxDistance", "relevantDate"]; if (!type || !data || !types.includes(type)) { @@ -470,7 +470,7 @@ export class Pass implements PassIndexSignature { * @returns {this} */ - nfc(data) { + nfc(data: schema.NFC) { if (!(typeof data === "object" && !Array.isArray(data) && schema.isValid(data, "nfcDict"))) { genericDebug("Invalid NFC data provided"); return this; @@ -489,7 +489,7 @@ export class Pass implements PassIndexSignature { * @returns {Boolean} true if type is supported, false otherwise. */ - _hasValidType(passFile) { + _hasValidType(passFile: schema.Pass): boolean { let passTypes = ["boardingPass", "eventTicket", "coupon", "generic", "storeCard"]; this.type = passTypes.find(type => passFile.hasOwnProperty(type)); @@ -509,7 +509,7 @@ export class Pass implements PassIndexSignature { * @return {Promise} The patched pass.json buffer */ - async _extractPassDefinition() { + async _extractPassDefinition(): Promise { const passStructBuffer = await readFile(path.resolve(this.model, "pass.json")) const parsedPassDefinition = JSON.parse(passStructBuffer.toString("utf8")); @@ -529,7 +529,7 @@ export class Pass implements PassIndexSignature { * @returns {Buffer} */ - _sign(manifest) { + _sign(manifest: { [key: string]: string }): Buffer { let signature = forge.pkcs7.createSignedData(); signature.content = forge.util.createBuffer(JSON.stringify(manifest), "utf8"); @@ -592,7 +592,7 @@ export class Pass implements PassIndexSignature { * @returns {Promise} Edited pass.json buffer or Object containing error. */ - _patch(passFile) { + _patch(passFile: schema.Pass): Buffer { if (Object.keys(this._props).length) { // We filter the existing (in passFile) and non-valid keys from // the below array keys that accept rgb values @@ -645,7 +645,7 @@ export class Pass implements PassIndexSignature { * @returns {Object} - model path and filtered options */ - _parseSettings(options) { + _parseSettings(options: schema.PassInstance): { model: string, _props: Object } { if (!schema.isValid(options, "instance")) { throw new Error(formatMessage("REQUIR_VALID_FAILED")); } @@ -667,7 +667,7 @@ export class Pass implements PassIndexSignature { }; } - set transitType(v) { + set transitType(v: string) { if (schema.isValid(v, "transitType")) { this[transitType] = v; } else { @@ -676,7 +676,7 @@ export class Pass implements PassIndexSignature { } } - get transitType() { + get transitType(): string { return this[transitType]; } } @@ -767,7 +767,7 @@ function parsePEM(pemName, element, passphrase) { * @return {Object[]} Object array barcodeDict compliant */ -function barcodesFromUncompleteData(origin) { +function barcodesFromUncompleteData(origin: schema.Barcode): schema.Barcode[] { if (!(origin.message && typeof origin.message === "string")) { barcodeDebug(formatMessage("BRC_AUTC_MISSING_DATA")); return []; @@ -780,7 +780,7 @@ function barcodesFromUncompleteData(origin) { "PKBarcodeFormatCode128" ].map(format => schema.getValidated( - Object.assign(origin, { format }), + Object.assign({}, origin, { format }), "barcode" ) ); diff --git a/src/schema.ts b/src/schema.ts index 5675bcf..4cf160f 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -430,7 +430,7 @@ export function isValid(opts: any, schemaName: keyof Schemas): boolean { * @returns {object} the filtered value or empty object */ -export function getValidated(opts: any, schemaName: keyof Schemas): Object { +export function getValidated(opts: any, schemaName: keyof Schemas): T { let resolvedSchema = resolveSchemaName(schemaName); let validation = Joi.validate(opts, resolvedSchema, { stripUnknown: true }); From 1ec1c55483be0aaf925470e84d731ae63264511d Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 26 May 2019 20:02:52 +0200 Subject: [PATCH 012/127] Added first version of factory with certificates reading --- src/factory.ts | 144 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/factory.ts diff --git a/src/factory.ts b/src/factory.ts new file mode 100644 index 0000000..200ee61 --- /dev/null +++ b/src/factory.ts @@ -0,0 +1,144 @@ +import { Pass } from "./pass"; +import { Certificates, isValid } from "./schema"; + +import { promisify } from "util"; +import { readFile as _readFile, readdir as _readdir } from "fs"; +import * as path from "path"; +import forge from "node-forge"; +import formatMessage from "./messages"; + +const readDir = promisify(_readdir); +const readFile = promisify(_readFile); + +interface FactoryOptions { + model: string | { [key: string]: Buffer }, + certificates: Certificates; + overrides?: Object; +} + +async function createPass(options: FactoryOptions) { + if (!(options && Object.keys(options).length)) { + throw new Error("Unable to create Pass: no options were passed"); + } + + // Voglio leggere i certificati + // Voglio leggere il model (se non è un oggetto) + + /* Model checks */ + + if (!options.model) { + throw new Error("Unable to create Pass: no model passed"); + } + + if (typeof options.model !== "string" && typeof options.model !== "object") { + throw new Error("Unable to create Pass: unsupported type"); + } + + if (typeof options.model === "object" && !Object.keys(options.model).length) { + throw new Error("Unable to create Pass: object model has no content"); + } + + /* Certificates checks */ + + const { certificates } = await Promise.all([ + readCertificatesFromOptions(options.certificates) + ]); + + // Controllo se il model è un oggetto o una stringa + // Se è un oggetto passo avanti + // Se è una stringa controllo se è un path. Se è un path + // faccio readdir + // altrimenti throw + + // Creare una funzione che possa controllare ed estrarre i certificati + // Creare una funzione che possa controllare ed estrarre i file + // Entrambe devono ritornare Promise, così faccio await Promise.all + + return new Pass(); +} + +/** + * Reads certificate contents, if the passed content is a path, + * and parses them as a PEM. + * @param options + */ + +interface FinalCertificates { + wwdr: forge.pki.Certificate; + signerCert: forge.pki.Certificate; + signerKey: forge.pki.PrivateKey; +} + +async function readCertificatesFromOptions(options: Certificates): Promise { + if (!isValid(options, "certificatesSchema")) { + throw new Error("Unable to create Pass: certificates schema validation failed."); + } + + // if the signerKey is an object, we want to get + // all the real contents and don't care of passphrase + const flattenedDocs = Object.assign({}, options, { + signerKey: ( + typeof options.signerKey === "string" + ? options.signerKey + : options.signerKey.keyFile + ) + }); + + // We read the contents + const rawContentsPromises = Object.keys(flattenedDocs) + .map(content => { + if (!!path.parse(content).ext) { + // The content is a path to the document + return readFile(path.resolve(content), { encoding: "utf8"}); + } else { + // Content is the real document content + return Promise.resolve(content); + } + }); + + try { + const parsedContents = await Promise.all(rawContentsPromises); + const pemParsedContents = parsedContents.map((file, index) => { + const certName = Object.keys(options)[index]; + const pem = parsePEM( + certName, + file, + typeof options.signerKey === "object" + ? options.signerKey.passphrase + : undefined + ); + + if (!pem) { + throw new Error(formatMessage("INVALID_CERTS", certName)); + } + + return { [certName]: pem }; + }); + + return Object.assign({}, ...pemParsedContents); + } catch (err) { + if (!err.path) { + throw err; + } + + throw new Error(formatMessage("INVALID_CERT_PATH", path.parse(err.path).base)); + } +} + +/** + * Parses the PEM-formatted passed text (certificates) + * + * @param element - Text content of .pem files + * @param passphrase - passphrase for the key + * @returns The parsed certificate or key in node forge format + */ + +function parsePEM(pemName: string, element: string, passphrase?: string) { + if (pemName === "signerKey" && passphrase) { + return forge.pki.decryptRsaPrivateKey(element, String(passphrase)); + } else { + return forge.pki.certificateFromPem(element); + } +} + +module.exports = { createPass }; From 95240a91f7861fa8a05c8229e831c3511ccce2cc Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 26 May 2019 20:03:25 +0200 Subject: [PATCH 013/127] Splitted certificates schema from instance one --- src/schema.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/schema.ts b/src/schema.ts index 4cf160f..b338ab3 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -34,16 +34,21 @@ export interface PassInstance { shouldOverwrite?: boolean; } -const instance = Joi.object().keys({ - model: Joi.string().required(), - certificates: Joi.object().keys({ - wwdr: Joi.string().required(), - signerCert: Joi.string().required(), - signerKey: Joi.object().keys({ +const certificatesSchema = Joi.object().keys({ + wwdr: Joi.string().required(), + signerCert: Joi.string().required(), + signerKey: Joi.alternatives().try( + Joi.object().keys({ keyFile: Joi.string().required(), passphrase: Joi.string().required(), - }).required() - }).required(), + }), + Joi.string() + ).required() +}).required(); + +const instance = Joi.object().keys({ + model: Joi.string().required(), + certificates: certificatesSchema, overrides: Joi.object(), shouldOverwrite: Joi.boolean() }); @@ -385,6 +390,7 @@ type Schemas = { }; const schemas: Schemas = { instance, + certificatesSchema, barcode, field, passDict, From 1cc8ae2975f65cae279b7153849c2d5614e85366 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Tue, 28 May 2019 23:19:32 +0200 Subject: [PATCH 014/127] Added functions getModelBufferContents, getModelFolderContents and getModelContents; --- src/factory.ts | 413 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 269 insertions(+), 144 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 200ee61..f2ea1b0 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -1,144 +1,269 @@ -import { Pass } from "./pass"; -import { Certificates, isValid } from "./schema"; - -import { promisify } from "util"; -import { readFile as _readFile, readdir as _readdir } from "fs"; -import * as path from "path"; -import forge from "node-forge"; -import formatMessage from "./messages"; - -const readDir = promisify(_readdir); -const readFile = promisify(_readFile); - -interface FactoryOptions { - model: string | { [key: string]: Buffer }, - certificates: Certificates; - overrides?: Object; -} - -async function createPass(options: FactoryOptions) { - if (!(options && Object.keys(options).length)) { - throw new Error("Unable to create Pass: no options were passed"); - } - - // Voglio leggere i certificati - // Voglio leggere il model (se non è un oggetto) - - /* Model checks */ - - if (!options.model) { - throw new Error("Unable to create Pass: no model passed"); - } - - if (typeof options.model !== "string" && typeof options.model !== "object") { - throw new Error("Unable to create Pass: unsupported type"); - } - - if (typeof options.model === "object" && !Object.keys(options.model).length) { - throw new Error("Unable to create Pass: object model has no content"); - } - - /* Certificates checks */ - - const { certificates } = await Promise.all([ - readCertificatesFromOptions(options.certificates) - ]); - - // Controllo se il model è un oggetto o una stringa - // Se è un oggetto passo avanti - // Se è una stringa controllo se è un path. Se è un path - // faccio readdir - // altrimenti throw - - // Creare una funzione che possa controllare ed estrarre i certificati - // Creare una funzione che possa controllare ed estrarre i file - // Entrambe devono ritornare Promise, così faccio await Promise.all - - return new Pass(); -} - -/** - * Reads certificate contents, if the passed content is a path, - * and parses them as a PEM. - * @param options - */ - -interface FinalCertificates { - wwdr: forge.pki.Certificate; - signerCert: forge.pki.Certificate; - signerKey: forge.pki.PrivateKey; -} - -async function readCertificatesFromOptions(options: Certificates): Promise { - if (!isValid(options, "certificatesSchema")) { - throw new Error("Unable to create Pass: certificates schema validation failed."); - } - - // if the signerKey is an object, we want to get - // all the real contents and don't care of passphrase - const flattenedDocs = Object.assign({}, options, { - signerKey: ( - typeof options.signerKey === "string" - ? options.signerKey - : options.signerKey.keyFile - ) - }); - - // We read the contents - const rawContentsPromises = Object.keys(flattenedDocs) - .map(content => { - if (!!path.parse(content).ext) { - // The content is a path to the document - return readFile(path.resolve(content), { encoding: "utf8"}); - } else { - // Content is the real document content - return Promise.resolve(content); - } - }); - - try { - const parsedContents = await Promise.all(rawContentsPromises); - const pemParsedContents = parsedContents.map((file, index) => { - const certName = Object.keys(options)[index]; - const pem = parsePEM( - certName, - file, - typeof options.signerKey === "object" - ? options.signerKey.passphrase - : undefined - ); - - if (!pem) { - throw new Error(formatMessage("INVALID_CERTS", certName)); - } - - return { [certName]: pem }; - }); - - return Object.assign({}, ...pemParsedContents); - } catch (err) { - if (!err.path) { - throw err; - } - - throw new Error(formatMessage("INVALID_CERT_PATH", path.parse(err.path).base)); - } -} - -/** - * Parses the PEM-formatted passed text (certificates) - * - * @param element - Text content of .pem files - * @param passphrase - passphrase for the key - * @returns The parsed certificate or key in node forge format - */ - -function parsePEM(pemName: string, element: string, passphrase?: string) { - if (pemName === "signerKey" && passphrase) { - return forge.pki.decryptRsaPrivateKey(element, String(passphrase)); - } else { - return forge.pki.certificateFromPem(element); - } -} - -module.exports = { createPass }; +import { Pass } from "./pass"; +import { Certificates, isValid } from "./schema"; + +import { promisify } from "util"; +import { readFile as _readFile, readdir as _readdir } from "fs"; +import * as path from "path"; +import forge from "node-forge"; +import formatMessage from "./messages"; + +const readDir = promisify(_readdir); +const readFile = promisify(_readFile); + +interface FactoryOptions { + model: string | { [key: string]: Buffer }, + certificates: Certificates; + overrides?: Object; +} + +async function createPass(options: FactoryOptions) { + if (!(options && Object.keys(options).length)) { + throw new Error("Unable to create Pass: no options were passed"); + } + + // Voglio leggere i certificati + // Voglio leggere il model (se non è un oggetto) + + /* Model checks */ + + if (!options.model) { + throw new Error("Unable to create Pass: no model passed"); + } + + if (typeof options.model !== "string" && typeof options.model !== "object") { + throw new Error("Unable to create Pass: unsupported type"); + } + + if (typeof options.model === "object" && !Object.keys(options.model).length) { + throw new Error("Unable to create Pass: object model has no content"); + } + + /* Certificates checks */ + + const { certificates } = await Promise.all([ + readCertificatesFromOptions(options.certificates) + ]); + + // Controllo se il model è un oggetto o una stringa + // Se è un oggetto passo avanti + // Se è una stringa controllo se è un path. Se è un path + // faccio readdir + // altrimenti throw + + // Creare una funzione che possa controllare ed estrarre i certificati + // Creare una funzione che possa controllare ed estrarre i file + // Entrambe devono ritornare Promise, così faccio await Promise.all + + return new Pass(); +} + +interface BundleUnit { + [key: string]: Buffer; +} + +async function getModelContents(model: FactoryOptions["model"]) { + if (!(model && (typeof model === "string" || (typeof model === "object" && Object.keys(model).length)))) { + throw new Error("Unable to create Pass: invalid model provided"); + } + + let modelContents: { + bundle: BundleUnit, + l10nBundle: { + [key: string]: BundleUnit + }, + }; + + if (typeof model === "string") { + modelContents = await getModelFolderContents(model); + } else { + modelContents = getModelBufferContents(model); + } + + const modelFiles = Object.keys(modelContents); + + if (!(modelFiles.includes("pass.json") && modelFiles.some(file => file.includes("icon")))) { + throw new Error("missing icon or pass.json"); + } + + return modelContents; +} + +async function getModelFolderContents(model: string) { + const modelPath = path.resolve(model) + (!!model && !path.extname(model) ? ".pass" : ""); + const modelFilesList = await readDir(modelPath); + + // No dot-starting files, manifest and signature + const filteredFiles = removeHidden(modelFilesList).filter(f => !/(manifest|signature|pass)/i.test(f)); + + // Icon is required to proceed + if (!(filteredFiles.length && filteredFiles.some(file => file.toLowerCase().includes("icon")))) { + const eMessage = formatMessage("MODEL_UNINITIALIZED", path.parse(this.model).name); + throw new Error(eMessage); + } + + // Splitting files from localization folders + const bundle = filteredFiles.filter(entry => !entry.includes(".lproj")); + const l10nFolders = filteredFiles.filter(entry => entry.includes(".lproj")); + + const bundleBuffers = bundle.map(file => readFile(path.resolve(model, file))); + const buffers = await Promise.all(bundleBuffers); + + const bundleMap = Object.assign({}, + ...bundle.map((fileName, index) => ({ [fileName]: buffers[index] })) + ) as BundleUnit; + + // Reading concurrently localizations folder + // and their files and their buffers + const L10N_FilesListByFolder: Array = await Promise.all( + l10nFolders.map(folderPath => { + // Reading current folder + const currentLangPath = path.join(model, folderPath); + return readDir(currentLangPath) + .then(files => { + // Transforming files path to a model-relative path + const validFiles = removeHidden(files) + .map(file => path.join(currentLangPath, file)); + + // Getting all the buffers from file paths + return Promise.all([ + ...validFiles.map(file => readFile(file)) + ]).then(buffers => + // Assigning each file path to its buffer + Object.assign({}, ...validFiles.map((file, index) => + ({ [file]: buffers[index] }) + )) as BundleUnit + ); + }); + }) + ); + + return { + bundle: bundleMap, + l10nBundle: Object.assign({}, ...L10N_FilesListByFolder + .map((folder, index) => ({ [l10nFolders[index]]: folder })) + ) as { [key: string]: BundleUnit } + }; +} + +function getModelBufferContents(model: BundleUnit) { + const bundle = removeHidden(Object.keys(model)).reduce((acc, current) => { + // Checking if current file is one of the autogenerated ones or if its + // content is not available + if (/(manifest|signature)/.test(current) || !bundle[current]) { + return acc; + } + + return { ...acc, [current]: model[current] }; + }, {}); + + const bundleKeys = Object.keys(bundle); + + if (!bundleKeys.length) { + throw new Error("Cannot proceed with pass creation: bundle initialized") + } + + // separing localization folders + const l10nFolders = bundleKeys.filter(file => file.includes(".lproj")); + const l10nBundle: { [key: string]: BundleUnit } = Object.assign({}, + ...l10nFolders.map(folder => + ({ [folder]: bundle[folder] }) as BundleUnit + ) + ); + + const unLocalizedBundle = Object.assign({}, + ...bundleKeys + .filter(file => !file.includes(".lproj")) + .map(file => ({ [file]: bundle[file] })) + ) as BundleUnit; + + return { + bundle: unLocalizedBundle, + l10nBundle + }; +} + +/** + * Reads certificate contents, if the passed content is a path, + * and parses them as a PEM. + * @param options + */ + +interface FinalCertificates { + wwdr: forge.pki.Certificate; + signerCert: forge.pki.Certificate; + signerKey: forge.pki.PrivateKey; +} + +async function readCertificatesFromOptions(options: Certificates): Promise { + if (!isValid(options, "certificatesSchema")) { + throw new Error("Unable to create Pass: certificates schema validation failed."); + } + + // if the signerKey is an object, we want to get + // all the real contents and don't care of passphrase + const flattenedDocs = Object.assign({}, options, { + signerKey: ( + typeof options.signerKey === "string" + ? options.signerKey + : options.signerKey.keyFile + ) + }); + + // We read the contents + const rawContentsPromises = Object.keys(flattenedDocs) + .map(content => { + if (!!path.parse(content).ext) { + // The content is a path to the document + return readFile(path.resolve(content), { encoding: "utf8"}); + } else { + // Content is the real document content + return Promise.resolve(content); + } + }); + + try { + const parsedContents = await Promise.all(rawContentsPromises); + const pemParsedContents = parsedContents.map((file, index) => { + const certName = Object.keys(options)[index]; + const pem = parsePEM( + certName, + file, + typeof options.signerKey === "object" + ? options.signerKey.passphrase + : undefined + ); + + if (!pem) { + throw new Error(formatMessage("INVALID_CERTS", certName)); + } + + return { [certName]: pem }; + }); + + return Object.assign({}, ...pemParsedContents); + } catch (err) { + if (!err.path) { + throw err; + } + + throw new Error(formatMessage("INVALID_CERT_PATH", path.parse(err.path).base)); + } +} + +/** + * Parses the PEM-formatted passed text (certificates) + * + * @param element - Text content of .pem files + * @param passphrase - passphrase for the key + * @returns The parsed certificate or key in node forge format + */ + +function parsePEM(pemName: string, element: string, passphrase?: string) { + if (pemName === "signerKey" && passphrase) { + return forge.pki.decryptRsaPrivateKey(element, String(passphrase)); + } else { + return forge.pki.certificateFromPem(element); + } +} + +module.exports = { createPass }; From 3976d86aa1d0545bd93b02f5a3c2ab34e1b4d770 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Tue, 28 May 2019 23:20:17 +0200 Subject: [PATCH 015/127] Improved FactoryOptions model --- src/factory.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/factory.ts b/src/factory.ts index f2ea1b0..f806f5c 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -11,7 +11,9 @@ const readDir = promisify(_readdir); const readFile = promisify(_readFile); interface FactoryOptions { - model: string | { [key: string]: Buffer }, + model: string | { + [key: string]: Buffer + }, certificates: Certificates; overrides?: Object; } From b3b0b1c8770c02fbba8f9d47f56c9b1bd4bb6f81 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 1 Jun 2019 00:25:16 +0200 Subject: [PATCH 016/127] Added function comments to factory model analysis functions --- src/factory.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index f806f5c..0ee82a1 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -90,7 +90,13 @@ async function getModelContents(model: FactoryOptions["model"]) { return modelContents; } -async function getModelFolderContents(model: string) { +/** + * Reads and model contents and creates a splitted + * bundles-object. + * @param model + */ + +async function getModelFolderContents(model: string): Promise { const modelPath = path.resolve(model) + (!!model && !path.extname(model) ? ".pass" : ""); const modelFilesList = await readDir(modelPath); @@ -147,7 +153,13 @@ async function getModelFolderContents(model: string) { }; } -function getModelBufferContents(model: BundleUnit) { +/** + * Analyzes the passed buffer model and splits it to + * return buffers and localization files buffers. + * @param model + */ + +function getModelBufferContents(model: BundleUnit): PartitionedBundle { const bundle = removeHidden(Object.keys(model)).reduce((acc, current) => { // Checking if current file is one of the autogenerated ones or if its // content is not available From cd3b977b1f78b409fcf307606ba09a4745567ef7 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 1 Jun 2019 00:26:23 +0200 Subject: [PATCH 017/127] Improved interfaces and model --- src/factory.ts | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 0ee82a1..5567439 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -10,10 +10,25 @@ import formatMessage from "./messages"; const readDir = promisify(_readdir); const readFile = promisify(_readFile); +interface BundleUnit { + [key: string]: Buffer; +} + +interface PartitionedBundle { + bundle: BundleUnit; + l10nBundle: { + [key: string]: BundleUnit + }; +} + +interface FinalCertificates { + wwdr: forge.pki.Certificate; + signerCert: forge.pki.Certificate; + signerKey: forge.pki.PrivateKey; +} + interface FactoryOptions { - model: string | { - [key: string]: Buffer - }, + model: string | BundleUnit, certificates: Certificates; overrides?: Object; } @@ -59,21 +74,12 @@ async function createPass(options: FactoryOptions) { return new Pass(); } -interface BundleUnit { - [key: string]: Buffer; -} - async function getModelContents(model: FactoryOptions["model"]) { if (!(model && (typeof model === "string" || (typeof model === "object" && Object.keys(model).length)))) { throw new Error("Unable to create Pass: invalid model provided"); } - let modelContents: { - bundle: BundleUnit, - l10nBundle: { - [key: string]: BundleUnit - }, - }; + let modelContents: PartitionedBundle; if (typeof model === "string") { modelContents = await getModelFolderContents(model); @@ -149,7 +155,7 @@ async function getModelFolderContents(model: string): Promise bundle: bundleMap, l10nBundle: Object.assign({}, ...L10N_FilesListByFolder .map((folder, index) => ({ [l10nFolders[index]]: folder })) - ) as { [key: string]: BundleUnit } + ) as PartitionedBundle["l10nBundle"] }; } @@ -202,12 +208,6 @@ function getModelBufferContents(model: BundleUnit): PartitionedBundle { * @param options */ -interface FinalCertificates { - wwdr: forge.pki.Certificate; - signerCert: forge.pki.Certificate; - signerKey: forge.pki.PrivateKey; -} - async function readCertificatesFromOptions(options: Certificates): Promise { if (!isValid(options, "certificatesSchema")) { throw new Error("Unable to create Pass: certificates schema validation failed."); From b6005bd821b5cdf43118125909648b921aacfc83 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 1 Jun 2019 00:38:42 +0200 Subject: [PATCH 018/127] improved types fix1 --- src/factory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/factory.ts b/src/factory.ts index 5567439..b47d686 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -184,7 +184,7 @@ function getModelBufferContents(model: BundleUnit): PartitionedBundle { // separing localization folders const l10nFolders = bundleKeys.filter(file => file.includes(".lproj")); - const l10nBundle: { [key: string]: BundleUnit } = Object.assign({}, + const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign({}, ...l10nFolders.map(folder => ({ [folder]: bundle[folder] }) as BundleUnit ) From 6f25dc40cfb6cf966dad85c697e1e33822c227e4 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 1 Jun 2019 00:40:34 +0200 Subject: [PATCH 019/127] Improved l10n bundle distribution --- src/factory.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index b47d686..3b9c3ad 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -151,11 +151,15 @@ async function getModelFolderContents(model: string): Promise }) ); + const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign( + {}, + ...L10N_FilesListByFolder + .map((folder, index) => ({ [l10nFolders[index]]: folder })) + ); + return { bundle: bundleMap, - l10nBundle: Object.assign({}, ...L10N_FilesListByFolder - .map((folder, index) => ({ [l10nFolders[index]]: folder })) - ) as PartitionedBundle["l10nBundle"] + l10nBundle }; } @@ -190,11 +194,11 @@ function getModelBufferContents(model: BundleUnit): PartitionedBundle { ) ); - const unLocalizedBundle = Object.assign({}, + const unLocalizedBundle: BundleUnit = Object.assign({}, ...bundleKeys .filter(file => !file.includes(".lproj")) .map(file => ({ [file]: bundle[file] })) - ) as BundleUnit; + ); return { bundle: unLocalizedBundle, From 66e224205e90cb763e0608c08261cc4d865575db Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 1 Jun 2019 00:45:20 +0200 Subject: [PATCH 020/127] Uniformed factory cross-functions naming --- src/factory.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 3b9c3ad..1264e02 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -116,15 +116,15 @@ async function getModelFolderContents(model: string): Promise } // Splitting files from localization folders - const bundle = filteredFiles.filter(entry => !entry.includes(".lproj")); + const rawBundle = filteredFiles.filter(entry => !entry.includes(".lproj")); const l10nFolders = filteredFiles.filter(entry => entry.includes(".lproj")); - const bundleBuffers = bundle.map(file => readFile(path.resolve(model, file))); + const bundleBuffers = rawBundle.map(file => readFile(path.resolve(model, file))); const buffers = await Promise.all(bundleBuffers); - const bundleMap = Object.assign({}, - ...bundle.map((fileName, index) => ({ [fileName]: buffers[index] })) - ) as BundleUnit; + const bundle: BundleUnit = Object.assign({}, + ...rawBundle.map((fileName, index) => ({ [fileName]: buffers[index] })) + ); // Reading concurrently localizations folder // and their files and their buffers @@ -158,7 +158,7 @@ async function getModelFolderContents(model: string): Promise ); return { - bundle: bundleMap, + bundle, l10nBundle }; } @@ -170,17 +170,17 @@ async function getModelFolderContents(model: string): Promise */ function getModelBufferContents(model: BundleUnit): PartitionedBundle { - const bundle = removeHidden(Object.keys(model)).reduce((acc, current) => { + const rawBundle = removeHidden(Object.keys(model)).reduce((acc, current) => { // Checking if current file is one of the autogenerated ones or if its // content is not available - if (/(manifest|signature)/.test(current) || !bundle[current]) { + if (/(manifest|signature)/.test(current) || !rawBundle[current]) { return acc; } return { ...acc, [current]: model[current] }; }, {}); - const bundleKeys = Object.keys(bundle); + const bundleKeys = Object.keys(rawBundle); if (!bundleKeys.length) { throw new Error("Cannot proceed with pass creation: bundle initialized") @@ -189,19 +189,19 @@ function getModelBufferContents(model: BundleUnit): PartitionedBundle { // separing localization folders const l10nFolders = bundleKeys.filter(file => file.includes(".lproj")); const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign({}, - ...l10nFolders.map(folder => - ({ [folder]: bundle[folder] }) as BundleUnit + ...l10nFolders.map(folder => + ({ [folder]: rawBundle[folder] }) ) ); - const unLocalizedBundle: BundleUnit = Object.assign({}, + const bundle: BundleUnit = Object.assign({}, ...bundleKeys .filter(file => !file.includes(".lproj")) - .map(file => ({ [file]: bundle[file] })) + .map(file => ({ [file]: rawBundle[file] })) ); return { - bundle: unLocalizedBundle, + bundle, l10nBundle }; } From 8d9cbe6698da468884697885f364ac8c97986dcc Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 1 Jun 2019 00:48:32 +0200 Subject: [PATCH 021/127] Delegated model checks to getModelContents function; Added try-catch to handle failure --- src/factory.ts | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 1264e02..4d6f54a 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -41,25 +41,14 @@ async function createPass(options: FactoryOptions) { // Voglio leggere i certificati // Voglio leggere il model (se non è un oggetto) - /* Model checks */ - - if (!options.model) { - throw new Error("Unable to create Pass: no model passed"); - } - - if (typeof options.model !== "string" && typeof options.model !== "object") { - throw new Error("Unable to create Pass: unsupported type"); - } - - if (typeof options.model === "object" && !Object.keys(options.model).length) { - throw new Error("Unable to create Pass: object model has no content"); - } - - /* Certificates checks */ - - const { certificates } = await Promise.all([ + try { + const [model, certificates] = await Promise.all([ + getModelContents(options.model), readCertificatesFromOptions(options.certificates) ]); + } catch (err) { + // @TODO: analyze the error and stop the execution somehow + } // Controllo se il model è un oggetto o una stringa // Se è un oggetto passo avanti From 65b8eea99914163c51857f0596ca0fef71021cbd Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 1 Jun 2019 00:49:04 +0200 Subject: [PATCH 022/127] Added exports to createPass function; Integrated missing dependency --- src/factory.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/factory.ts b/src/factory.ts index 4d6f54a..584787b 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -6,6 +6,7 @@ import { readFile as _readFile, readdir as _readdir } from "fs"; import * as path from "path"; import forge from "node-forge"; import formatMessage from "./messages"; +import { removeHidden } from "./utils"; const readDir = promisify(_readdir); const readFile = promisify(_readFile); @@ -33,7 +34,7 @@ interface FactoryOptions { overrides?: Object; } -async function createPass(options: FactoryOptions) { +export async function createPass(options: FactoryOptions) { if (!(options && Object.keys(options).length)) { throw new Error("Unable to create Pass: no options were passed"); } From 083142330c5eb5451b232fb911f411e8b94a5f52 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 1 Jun 2019 13:58:40 +0200 Subject: [PATCH 023/127] Added management for non-existing folder-model bundle files --- src/factory.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 584787b..06ee3fa 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -130,12 +130,18 @@ async function getModelFolderContents(model: string): Promise // Getting all the buffers from file paths return Promise.all([ - ...validFiles.map(file => readFile(file)) + ...validFiles.map(file => + readFile(file).catch(() => Buffer.alloc(0)) + ) ]).then(buffers => // Assigning each file path to its buffer - Object.assign({}, ...validFiles.map((file, index) => - ({ [file]: buffers[index] }) - )) as BundleUnit + validFiles.reduce((acc, file, index) => { + if (!buffers[index].length) { + return acc; + } + + return { ...acc, [file]: buffers[index] }; + }, {}) ); }); }) From 75ca49d89aca22fbd5f08c19c67ff7d178cec5e2 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 1 Jun 2019 14:01:25 +0200 Subject: [PATCH 024/127] Added existence check for readCertificates --- src/factory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/factory.ts b/src/factory.ts index 06ee3fa..41ed06a 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -209,7 +209,7 @@ function getModelBufferContents(model: BundleUnit): PartitionedBundle { */ async function readCertificatesFromOptions(options: Certificates): Promise { - if (!isValid(options, "certificatesSchema")) { + if (!(options && Object.keys(options).length && isValid(options, "certificatesSchema"))) { throw new Error("Unable to create Pass: certificates schema validation failed."); } From d02ed747e3b41b48c0ce490d2766c5f9a7f3d298 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 9 Jun 2019 11:43:27 +0200 Subject: [PATCH 025/127] Added index.ts --- index.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 index.ts diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..7bc5732 --- /dev/null +++ b/index.ts @@ -0,0 +1 @@ +export { createPass } from "./src/factory"; From 46bd6d3b3c0e12484a5c40261530fe944b0bce42 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 9 Jun 2019 11:46:28 +0200 Subject: [PATCH 026/127] Moved types from factory to schema --- src/factory.ts | 25 +------------------------ src/schema.ts | 33 +++++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 41ed06a..6a18ba2 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -11,30 +11,7 @@ import { removeHidden } from "./utils"; const readDir = promisify(_readdir); const readFile = promisify(_readFile); -interface BundleUnit { - [key: string]: Buffer; -} - -interface PartitionedBundle { - bundle: BundleUnit; - l10nBundle: { - [key: string]: BundleUnit - }; -} - -interface FinalCertificates { - wwdr: forge.pki.Certificate; - signerCert: forge.pki.Certificate; - signerKey: forge.pki.PrivateKey; -} - -interface FactoryOptions { - model: string | BundleUnit, - certificates: Certificates; - overrides?: Object; -} - -export async function createPass(options: FactoryOptions) { +export async function createPass(options: FactoryOptions): Promise { if (!(options && Object.keys(options).length)) { throw new Error("Unable to create Pass: no options were passed"); } diff --git a/src/schema.ts b/src/schema.ts index b338ab3..e7549cd 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -20,20 +20,33 @@ export interface FactoryOptions { shouldOverwrite?: boolean; } -export interface PassInstance { - model: { [key: string]: Buffer }; - certificates: { +export interface BundleUnit { + [key: string]: Buffer; +} + +export interface PartitionedBundle { + bundle: BundleUnit; + l10nBundle: { + [key: string]: BundleUnit + }; +} + +export interface FinalCertificates { wwdr: string; signerCert: string; - signerKey: { - keyFile: string; - passphrase?: string; - }; - }; - overrides?: OverridesSupportedOptions; - shouldOverwrite?: boolean; + signerKey: string; } +export interface PassInstance { + model: PartitionedBundle; + certificates: FinalCertificates; + overrides?: OverridesSupportedOptions; +} + +// ************************************ // +// * JOI Schemas + Related Interfaces * // +// ************************************ // + const certificatesSchema = Joi.object().keys({ wwdr: Joi.string().required(), signerCert: Joi.string().required(), From 61a37eaa24b37d4d7a59337b92cb060dbb5d7419 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 9 Jun 2019 11:47:42 +0200 Subject: [PATCH 027/127] Fixed problem with bundles --- src/factory.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 6a18ba2..0e817d6 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -54,9 +54,9 @@ async function getModelContents(model: FactoryOptions["model"]) { modelContents = getModelBufferContents(model); } - const modelFiles = Object.keys(modelContents); + const modelFiles = Object.keys(modelContents.bundle); - if (!(modelFiles.includes("pass.json") && modelFiles.some(file => file.includes("icon")))) { + if (!(modelFiles.includes("pass.json") && modelContents.bundle["pass.json"].length && modelFiles.some(file => Boolean(file.includes("icon") && modelContents.bundle[file].length)))) { throw new Error("missing icon or pass.json"); } @@ -156,7 +156,7 @@ function getModelBufferContents(model: BundleUnit): PartitionedBundle { const bundleKeys = Object.keys(rawBundle); if (!bundleKeys.length) { - throw new Error("Cannot proceed with pass creation: bundle initialized") + throw new Error("Cannot proceed with pass creation: bundle not initialized") } // separing localization folders From 16a9ebcd5c04185ead8ec7af6dca573f13e42d1b Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 9 Jun 2019 11:48:25 +0200 Subject: [PATCH 028/127] Renamed model in bundle --- src/factory.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 0e817d6..ec19976 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -16,11 +16,8 @@ export async function createPass(options: FactoryOptions): Promise { throw new Error("Unable to create Pass: no options were passed"); } - // Voglio leggere i certificati - // Voglio leggere il model (se non è un oggetto) - try { - const [model, certificates] = await Promise.all([ + const [bundle, certificates] = await Promise.all([ getModelContents(options.model), readCertificatesFromOptions(options.certificates) ]); From 8a841301fa7c86bb56f1e6c586bfb0f416865c4f Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 9 Jun 2019 11:51:33 +0200 Subject: [PATCH 029/127] Changed Pass constructor to conform to the new architecture; --- src/factory.ts | 18 ++++++----------- src/pass.ts | 55 +++++++++++++++++++++++++++++++++----------------- src/schema.ts | 28 ++++++++++++++++++++----- 3 files changed, 65 insertions(+), 36 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index ec19976..37d6896 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -21,21 +21,15 @@ export async function createPass(options: FactoryOptions): Promise { getModelContents(options.model), readCertificatesFromOptions(options.certificates) ]); + + return new Pass({ + model: bundle, + certificates, + overrides: options.overrides + }); } catch (err) { // @TODO: analyze the error and stop the execution somehow } - - // Controllo se il model è un oggetto o una stringa - // Se è un oggetto passo avanti - // Se è una stringa controllo se è un path. Se è un path - // faccio readdir - // altrimenti throw - - // Creare una funzione che possa controllare ed estrarre i certificati - // Creare una funzione che possa controllare ed estrarre i file - // Entrambe devono ritornare Promise, così faccio await Promise.all - - return new Pass(); } async function getModelContents(model: FactoryOptions["model"]) { diff --git a/src/pass.ts b/src/pass.ts index e0adc81..0e4dbe2 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -31,40 +31,57 @@ interface PassIndexSignature { } export class Pass implements PassIndexSignature { - private model: string; + // private model: string; + private bundle: schema.BundleUnit; + private l10nBundles: schema.PartitionedBundle["l10nBundle"]; private _fields: string[]; - private _props: { [key: string]: any }; + private _props: schema.ValidPass; private type: string; private fieldsKeys: Set; + private passCore: schema.ValidPass; - Certificates: schema.Certificates; - l10n: { [key: string]: { [key: string]: string } } = {}; - shouldOverwrite: boolean; + Certificates: schema.FinalCertificates; + l10nTranslations: { [key: string]: { [key: string]: string } } = {}; [transitType]: string = ""; constructor(options: schema.PassInstance) { - this.Certificates = { - // Even if this assigning will fail, it will be captured below - // in _parseSettings, since this won't match with the schema. - _raw: options.certificates || {}, - }; + this.Certificates = options.certificates; + this.l10nBundles = options.model.l10nBundle; + this.bundle = { ...options.model.bundle }; options.overrides = options.overrides || {}; - this.shouldOverwrite = !(options.hasOwnProperty("shouldOverwrite") && !options.shouldOverwrite); + // getting pass.json + this.passCore = JSON.parse(this.bundle["pass.json"].toString("utf8")); - this._fields = ["primaryFields", "secondaryFields", "auxiliaryFields", "backFields", "headerFields"]; + this.type = Object.keys(this.passCore).find(key => /(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key)); + + if (!this.type) { + throw new Error("Missing type in model"); + } + + if (this.type === "boardingPass" && this.passCore[this.type]["transitType"]) { + // We might want to generate a boarding pass without setting manually + // in the code the transit type but right in the model; + this[transitType] = this.passCore[this.type]["transitType"]; + } this.fieldsKeys = new Set(); - this._fields.forEach(name => { - this[name] = new FieldsArray(this.fieldsKeys); + const typeFields = Object.keys(this.passCore[this.type]); + + this._fields = ["primaryFields", "secondaryFields", "auxiliaryFields", "backFields", "headerFields"]; + this._fields.forEach(fieldName => { + if (typeFields.includes(fieldName)) { + this[fieldName] = new FieldsArray( + this.fieldsKeys, + ...this.passCore[this.type][fieldName] + .filter((field: schema.Field) => schema.isValid(field, "field")) + ); + } else { + this[fieldName] = new FieldsArray(this.fieldsKeys); + } }); - - this[transitType] = ""; - - // Assigning model and _props to this - Object.assign(this, this._parseSettings(options)); } /** diff --git a/src/schema.ts b/src/schema.ts index e7549cd..a879e7a 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -10,14 +10,12 @@ export interface Certificates { keyFile: string; passphrase?: string; }; - _raw?: Certificates; } export interface FactoryOptions { model: { [key: string]: Buffer } | string; certificates: Certificates; overrides?: Object; - shouldOverwrite?: boolean; } export interface BundleUnit { @@ -32,8 +30,8 @@ export interface PartitionedBundle { } export interface FinalCertificates { - wwdr: string; - signerCert: string; + wwdr: string; + signerCert: string; signerKey: string; } @@ -276,6 +274,26 @@ const semantics = Joi.object().keys({ balance: currencyAmount }); +interface ValidPassType { + boardingPass?: PassFields & { transitType: TransitType }; + eventTicket?: PassFields; + coupon?: PassFields; + generic?: PassFields; + storeCard?: PassFields; +} + +export interface ValidPass extends OverridesSupportedOptions, ValidPassType { + barcode?: Barcode; + barcodes?: Barcode[]; + beacons?: Beacon[]; + locations?: Location[]; + maxDistance?: number; + relevantDate?: string; + nfc?: NFC; + expirationDate?: string; + voided?: boolean; +} + export interface Barcode { altText?: string; messageEncoding?: string; @@ -364,7 +382,7 @@ const locationsDict = Joi.object().keys({ relevantText: Joi.string() }); -export interface Pass { +export interface PassFields { auxiliaryFields: Field[]; backFields: Field[]; headerFields: Field[]; From 38d1ccbfb2a940c8414d178017a61d1400073623 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 9 Jun 2019 12:00:54 +0200 Subject: [PATCH 030/127] Removed unused imported dependencies --- src/factory.ts | 2 +- src/pass.ts | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 37d6896..150dad2 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -1,5 +1,5 @@ import { Pass } from "./pass"; -import { Certificates, isValid } from "./schema"; +import { Certificates, isValid, FactoryOptions, PartitionedBundle, BundleUnit, FinalCertificates } from "./schema"; import { promisify } from "util"; import { readFile as _readFile, readdir as _readdir } from "fs"; diff --git a/src/pass.ts b/src/pass.ts index 0e4dbe2..a8515fb 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -1,6 +1,4 @@ -import fs from "fs"; import path from "path"; -import { promisify } from "util"; import stream, { Stream } from "stream"; import forge from "node-forge"; import archiver from "archiver"; @@ -11,15 +9,12 @@ import formatMessage from "./messages"; import FieldsArray from "./fieldsArray"; import { assignLength, generateStringFile, - removeHidden, dateToW3CString, - isValidRGB + dateToW3CString, isValidRGB } from "./utils"; const barcodeDebug = debug("passkit:barcode"); const genericDebug = debug("passkit:generic"); -const readdir = promisify(fs.readdir); -const readFile = promisify(fs.readFile); const noop = () => {}; const transitType = Symbol("transitType"); From 70b42509c332f373dbf7c186df7a212db753be57 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 9 Jun 2019 12:04:39 +0200 Subject: [PATCH 031/127] Changed generate method: removed --- src/pass.ts | 127 +++++++++++++++------------------------------------- 1 file changed, 36 insertions(+), 91 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index a8515fb..0d35866 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -88,89 +88,49 @@ export class Pass implements PassIndexSignature { */ async generate(): Promise { - try { - // Reading the model - const modelFilesList = await readdir(this.model); + // Editing Pass.json - // list without dynamic components like manifest, signature or pass files (will be added later in the flow) and hidden files. - const noDynList = removeHidden(modelFilesList).filter(f => !/(manifest|signature|pass)/i.test(f)); + this.bundle["pass.json"] = this._patch(this.bundle["pass.json"]); - if (!noDynList.length || !noDynList.some(f => f.toLowerCase().includes("icon"))) { - let eMessage = formatMessage("MODEL_UNINITIALIZED", path.parse(this.model).name); - throw new Error(eMessage); - } + const finalBundle = { ...this.bundle } as schema.BundleUnit; - // list without localization files (they will be added later in the flow) - let bundle = noDynList.filter(f => !f.includes(".lproj")); - - // Localization folders only - const L10N = noDynList.filter(f => f.includes(".lproj") && Object.keys(this.l10n).includes(path.parse(f).name)); - - /* - * Reading all the localization selected folders and removing hidden files (the ones that starts with ".") - * from the list. - */ - - const L10N_FilesListByFolder = await Promise.all( - L10N.map(async folderPath => { - const list = await readdir(path.join(this.model, folderPath)) - return removeHidden(list); - }) - ); - - // Pushing into the bundle the composed paths for localization files - - L10N_FilesListByFolder.forEach((filesList, index) => - bundle.push( - ...filesList.map(file => path.join(L10N[index], file)) - ) - ); - - /* Getting all bundle file buffers, pass.json included, and appending path */ - - // Reading bundle files to buffers without pass.json - it gets read below - // to use a different parsing process - - const bundleBuffers = bundle.map(file => readFile(path.resolve(this.model, file))); - const passBuffer = this._extractPassDefinition(); - bundle.push("pass.json"); - - const buffers = await Promise.all([...bundleBuffers, passBuffer]); - - Object.keys(this.l10n).forEach(lang => { - const strings = generateStringFile(this.l10n[lang]); - - /** - * if .string file buffer is empty, no translations were added - * but still wanted to include the language - */ - - if (!strings.length) { - return; - } + Object.keys(this.l10nTranslations).forEach(lang => { + const strings = generateStringFile(this.l10nTranslations[lang]); + if (strings.length) { /** * if there's already a buffer of the same folder and called * `pass.strings`, we'll merge the two buffers. We'll create * it otherwise. + */ + + if (!this.l10nBundles[lang]) { + this.l10nBundles[lang] = {}; + } + + this.l10nBundles[lang]["pass.strings"] = Buffer.concat([ + this.l10nBundles[lang]["pass.strings"] || Buffer.from("", "utf8"), + strings + ]); + } + + if (!(this.l10nBundles[lang] && Object.keys(this.l10nBundles[lang]).length)) { + return; + } + + /** + * Assigning all the localization files to the final bundle + * by mapping the buffer to the pass-relative file path; * * We are replacing the slashes to avoid Windows slashes * composition. */ - - const stringFilePath = path.join(`${lang}.lproj`, "pass.strings").replace(/\\/, "/"); - - const stringFileIndex = bundle.findIndex(file => file === stringFilePath); - - if (stringFileIndex > -1) { - buffers[stringFileIndex] = Buffer.concat([ - buffers[stringFileIndex], - strings - ]); - } else { - buffers.push(strings); - bundle.push(stringFilePath); - } + Object.assign(finalBundle, ...Object.keys(this.l10nBundles[lang]) + .map(fileName => { + const fullPath = path.join(`${lang}.lproj`, fileName).replace(/\\/, "/"); + return { [fullPath]: this.l10nBundles[lang][fileName] }; + }) + ); }); /* @@ -178,24 +138,17 @@ export class Pass implements PassIndexSignature { * and returning the compiled manifest */ const archive = archiver("zip"); - const manifest = buffers.reduce((acc, current, index) => { - let filename = bundle[index]; + const manifest = Object.keys(finalBundle).reduce((acc, current) => { let hashFlow = forge.md.sha1.create(); - hashFlow.update(current.toString("binary")); - archive.append(current, { name: filename }); + hashFlow.update(finalBundle[current].toString("binary")); + archive.append(current, { name: current }); - acc[filename] = hashFlow.digest().toHex(); + acc[current] = hashFlow.digest().toHex(); return acc; }, {}); - // Reading the certificates, - // signing the manifest, appending signature an manifest to the archive - // and returning the generated pass stream. - - Object.assign(this.Certificates, await readCertificates(this.Certificates)); - const signatureBuffer = this._sign(manifest); archive.append(signatureBuffer, { name: "signature" }); @@ -206,14 +159,6 @@ export class Pass implements PassIndexSignature { archive.pipe(passStream); return archive.finalize().then(() => passStream); - } catch (err) { - if (err.code && err.code === "ENOENT") { - // No model available at this path - renaming the error - throw new Error(formatMessage("MODEL_NOT_FOUND", this.model)); - } - - throw new Error(err); - } } /** @@ -230,7 +175,7 @@ export class Pass implements PassIndexSignature { localize(lang: string, translations?: { [key: string]: string }) { if (lang && typeof lang === "string" && (typeof translations === "object" || translations === undefined)) { - this.l10n[lang] = translations || {}; + this.l10nTranslations[lang] = translations || {}; } return this; From d562053a14e347b2b31ddf30e957fa93f6f55d3b Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 9 Jun 2019 12:12:48 +0200 Subject: [PATCH 032/127] Removed unused methods hasValidType, _parseSettings, readCertificates, etc. --- src/pass.ts | 145 ---------------------------------------------------- 1 file changed, 145 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 0d35866..6ab85e6 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -438,46 +438,6 @@ export class Pass implements PassIndexSignature { return this; } - /** - * Checks if pass model type is one of the supported ones - * - * @method _hasValidType - * @params {string} passFile - parsed pass structure content - * @returns {Boolean} true if type is supported, false otherwise. - */ - - _hasValidType(passFile: schema.Pass): boolean { - let passTypes = ["boardingPass", "eventTicket", "coupon", "generic", "storeCard"]; - - this.type = passTypes.find(type => passFile.hasOwnProperty(type)); - - if (!this.type) { - genericDebug(formatMessage("NO_PASS_TYPE")); - return false; - } - - return schema.isValid(passFile[this.type], "passDict"); - } - - /** - * Reads pass.json file and returns the patched version - * @function - * @name passExtractor - * @return {Promise} The patched pass.json buffer - */ - - async _extractPassDefinition(): Promise { - const passStructBuffer = await readFile(path.resolve(this.model, "pass.json")) - const parsedPassDefinition = JSON.parse(passStructBuffer.toString("utf8")); - - if (!this._hasValidType(parsedPassDefinition)) { - const eMessage = formatMessage("PASSFILE_VALIDATION_FAILED"); - throw new Error(eMessage); - } - - return this._patch(parsedPassDefinition); - } - /** * Generates the PKCS #7 cryptografic signature for the manifest file. * @@ -594,36 +554,6 @@ export class Pass implements PassIndexSignature { return Buffer.from(JSON.stringify(passFile)); } - /** - * Validates the contents of the passed options and handle them - * - * @method _parseSettings - * @params {Object} options - the options passed to be parsed - * @returns {Object} - model path and filtered options - */ - - _parseSettings(options: schema.PassInstance): { model: string, _props: Object } { - if (!schema.isValid(options, "instance")) { - throw new Error(formatMessage("REQUIR_VALID_FAILED")); - } - - if (!options.model || typeof options.model !== "string") { - throw new Error(formatMessage("MODEL_NOT_STRING")); - } - - const modelPath = path.resolve(options.model) + (!!options.model && !path.extname(options.model) ? ".pass" : ""); - const filteredOpts = schema.getValidated(options.overrides, "supportedOptions"); - - if (!filteredOpts) { - throw new Error(formatMessage("OVV_KEYS_BADFORMAT")) - } - - return { - model: modelPath, - _props: filteredOpts - }; - } - set transitType(v: string) { if (schema.isValid(v, "transitType")) { this[transitType] = v; @@ -638,81 +568,6 @@ export class Pass implements PassIndexSignature { } } -/** - * Validates the contents of the passed options and handle them - * - * @function readCertificates - * @params {Object} certificates - certificates object with raw content - * and, optionally, the already parsed certificates - * @returns {Object} - parsed certificates to be pushed to Pass.Certificates. - */ - -function readCertificates(certificates: schema.Certificates) { - if (certificates.wwdr && certificates.signerCert && typeof certificates.signerKey === "object") { - // Nothing must be added. Void object is returned. - return Promise.resolve({}); - } - - const raw = certificates._raw; - const optCertsNames = Object.keys(raw); - const certPaths = optCertsNames.map((val) => { - const cert: string | typeof certificates.signerKey = raw[val]; - // realRawValue exists as signerKey might be an object - const realRawValue = !(cert instanceof Object) ? cert : cert["keyFile"]; - - // We are checking if the string is a path or a content - if (!!path.parse(realRawValue).ext) { - const resolvedPath = path.resolve(realRawValue); - return readFile(resolvedPath, { encoding: "utf8" }); - } else { - return Promise.resolve(realRawValue); - } - }); - - return Promise.all(certPaths) - .then(contents => { - // Mapping each file content to a PEM structure, returned in form of one-key-object - // which is conjoint later with the other pems - - return Object.assign( - {}, - ...contents.map((file, index) => { - const certName = optCertsNames[index]; - const pem = parsePEM(certName, file, raw[certName].passphrase); - - if (!pem) { - throw new Error(formatMessage("INVALID_CERTS", certName)); - } - - return { [certName]: pem }; - }) - ); - }).catch(err => { - if (!err.path) { - throw err; - } - - throw new Error(formatMessage("INVALID_CERT_PATH", path.parse(err.path).base)); - }); -} - -/** - * Parses the PEM-formatted passed text (certificates) - * - * @function parsePEM - * @params {String} element - Text content of .pem files - * @params {String=} passphrase - passphrase for the key - * @returns {Object} The parsed certificate or key in node forge format - */ - -function parsePEM(pemName, element, passphrase) { - if (pemName === "signerKey" && passphrase) { - return forge.pki.decryptRsaPrivateKey(element, String(passphrase)); - } else { - return forge.pki.certificateFromPem(element); - } -} - /** * Automatically generates barcodes for all the types given common info * From 688b4da4f79b0539828849bff677e382ba6a2ac9 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 9 Jun 2019 12:22:16 +0200 Subject: [PATCH 033/127] Changed _patch method to accept Buffer and return patched buffer --- src/pass.ts | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 6ab85e6..5b5984b 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -509,7 +509,9 @@ export class Pass implements PassIndexSignature { * @returns {Promise} Edited pass.json buffer or Object containing error. */ - _patch(passFile: schema.Pass): Buffer { + _patch(passCoreBuffer: Buffer): Buffer { + const passFile = JSON.parse(passCoreBuffer.toString()); + if (Object.keys(this._props).length) { // We filter the existing (in passFile) and non-valid keys from // the below array keys that accept rgb values @@ -518,38 +520,26 @@ export class Pass implements PassIndexSignature { .filter(v => this._props[v] && !isValidRGB(this._props[v])) .forEach(v => delete this._props[v]); - if (this.shouldOverwrite) { - Object.assign(passFile, this._props); - } else { Object.keys(this._props).forEach(prop => { - if (passFile[prop]) { if (passFile[prop] instanceof Array) { passFile[prop].push(...this._props[prop]); } else if (passFile[prop] instanceof Object) { Object.assign(passFile[prop], this._props[prop]); - } } else { passFile[prop] = this._props[prop]; } }); } - } - this._fields.forEach(area => { - if (this[area].length) { - if (this.shouldOverwrite) { - passFile[this.type][area] = [...this[area]]; - } else { - passFile[this.type][area].push(...this[area]); - } - } + this._fields.forEach(field => { + passFile[this.type][field] = this[field]; }); - if (this.type === "boardingPass" && !this.transitType) { + if (this.type === "boardingPass" && !this[transitType]) { throw new Error(formatMessage("TRSTYPE_REQUIRED")); } - passFile[this.type]["transitType"] = this.transitType; + passFile[this.type]["transitType"] = this[transitType]; return Buffer.from(JSON.stringify(passFile)); } From 91008de66e6f7b87913c58428b81d9fe467be271 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 9 Jun 2019 12:23:04 +0200 Subject: [PATCH 034/127] Changed pass _sign to accept signerkey content --- src/pass.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pass.ts b/src/pass.ts index 5b5984b..bfba123 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -464,7 +464,7 @@ export class Pass implements PassIndexSignature { */ signature.addSigner({ - key: this.Certificates.signerKey.keyFile, + key: this.Certificates.signerKey, certificate: this.Certificates.signerCert, digestAlgorithm: forge.pki.oids.sha1, authenticatedAttributes: [{ From 85e9f63907ee4c37a6f26e7165f1a50df42b3749 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 9 Jun 2019 23:39:35 +0200 Subject: [PATCH 035/127] Removed download example --- examples/download.js | 60 -------------------------------------------- 1 file changed, 60 deletions(-) delete mode 100644 examples/download.js diff --git a/examples/download.js b/examples/download.js deleted file mode 100644 index ca63389..0000000 --- a/examples/download.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * .void() and .expiration() methods example - * To check if a ticket is void, look at the barcode; - * If it is grayed, the ticket is voided. May not be showed on macOS. - * - * To check if a ticket has an expiration date, you'll - * have to wait two minutes. - */ - -const app = require("./webserver"); -const { Pass } = require(".."); - -app.all(function manageRequest(request, response) { - let passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, ""); - - let pass = new Pass({ - model: `./models/${request.params.modelName}`, - certificates: { - wwdr: "../certificates/WWDR.pem", - signerCert: "../certificates/signerCert.pem", - signerKey: { - keyFile: "../certificates/signerKey.pem", - passphrase: "123456" - } - }, - overrides: request.body || request.params || request.query, - }); - - pass.load("https://s.gravatar.com/avatar/83cd11399b7ea79977bc302f3931ee52?size=32&default=retro", "icon.png"); - pass.load("https://s.gravatar.com/avatar/83cd11399b7ea79977bc302f3931ee52?size=64&default=retro", "icon@2x.png"); - - // This to import them directly in the localization folder - /* - pass.load("https://s.gravatar.com/avatar/83cd11399b7ea79977bc302f3931ee52?size=32&default=retro", "en.lproj/icon.png"); - pass.load("https://s.gravatar.com/avatar/83cd11399b7ea79977bc302f3931ee52?size=64&default=retro", "en.lproj/icon@2x.png"); - - pass.localize("en", { - "EVENT": "Event", - "LOCATION": "Location" - }); - */ - - pass.generate().then(function (stream) { - response.set({ - "Content-type": "application/vnd.apple.pkpass", - "Content-disposition": `attachment; filename=${passName}.pkpass` - }); - - stream.pipe(response); - }).catch(err => { - - console.log(err); - - response.set({ - "Content-type": "text/html", - }); - - response.send(err.message); - }); -}); From 79a55f64d4c8eadbde5377192c359eaa340a6209 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 9 Jun 2019 23:41:20 +0200 Subject: [PATCH 036/127] Fixed wrong variables, added initialization --- src/factory.ts | 10 ++++++---- src/pass.ts | 12 +++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 150dad2..4463937 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -65,7 +65,7 @@ async function getModelFolderContents(model: string): Promise const modelFilesList = await readDir(modelPath); // No dot-starting files, manifest and signature - const filteredFiles = removeHidden(modelFilesList).filter(f => !/(manifest|signature|pass)/i.test(f)); + const filteredFiles = removeHidden(modelFilesList).filter(f => !/(manifest|signature)/i.test(f)); // Icon is required to proceed if (!(filteredFiles.length && filteredFiles.some(file => file.toLowerCase().includes("icon")))) { @@ -77,7 +77,7 @@ async function getModelFolderContents(model: string): Promise const rawBundle = filteredFiles.filter(entry => !entry.includes(".lproj")); const l10nFolders = filteredFiles.filter(entry => entry.includes(".lproj")); - const bundleBuffers = rawBundle.map(file => readFile(path.resolve(model, file))); + const bundleBuffers = rawBundle.map(file => readFile(path.resolve(modelPath, file))); const buffers = await Promise.all(bundleBuffers); const bundle: BundleUnit = Object.assign({}, @@ -89,7 +89,7 @@ async function getModelFolderContents(model: string): Promise const L10N_FilesListByFolder: Array = await Promise.all( l10nFolders.map(folderPath => { // Reading current folder - const currentLangPath = path.join(model, folderPath); + const currentLangPath = path.join(modelPath, folderPath); return readDir(currentLangPath) .then(files => { // Transforming files path to a model-relative path @@ -193,7 +193,9 @@ async function readCertificatesFromOptions(options: Certificates): Promise { + .map(key => { + const content = flattenedDocs[key]; + if (!!path.parse(content).ext) { // The content is a path to the document return readFile(path.resolve(content), { encoding: "utf8"}); diff --git a/src/pass.ts b/src/pass.ts index bfba123..af6d8f2 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -30,10 +30,10 @@ export class Pass implements PassIndexSignature { private bundle: schema.BundleUnit; private l10nBundles: schema.PartitionedBundle["l10nBundle"]; private _fields: string[]; - private _props: schema.ValidPass; - private type: string; - private fieldsKeys: Set; - private passCore: schema.ValidPass; + private _props: schema.ValidPass = {}; + private type: string = ""; + private fieldsKeys: Set = new Set(); + private passCore: schema.ValidPass = {}; Certificates: schema.FinalCertificates; l10nTranslations: { [key: string]: { [key: string]: string } } = {}; @@ -61,8 +61,6 @@ export class Pass implements PassIndexSignature { this[transitType] = this.passCore[this.type]["transitType"]; } - this.fieldsKeys = new Set(); - const typeFields = Object.keys(this.passCore[this.type]); this._fields = ["primaryFields", "secondaryFields", "auxiliaryFields", "backFields", "headerFields"]; @@ -142,7 +140,7 @@ export class Pass implements PassIndexSignature { let hashFlow = forge.md.sha1.create(); hashFlow.update(finalBundle[current].toString("binary")); - archive.append(current, { name: current }); + archive.append(finalBundle[current], { name: current }); acc[current] = hashFlow.digest().toHex(); From 833aac08f23e275dbf40cfe0489633f3d8b5bf79 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Mon, 10 Jun 2019 20:58:37 +0200 Subject: [PATCH 037/127] Changes tsconfig.json --- tsconfig.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index aa1517f..5afd13f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,11 @@ { "compilerOptions": { "module": "commonjs", - "outDir": "dist", "target": "es2018", "esModuleInterop": true, "newLine": "LF", - } + }, + "exclude": [ + "node_modules/" + ] } From 48e8f4ef84734d90724c4c7952450fe127e484a2 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Mon, 10 Jun 2019 21:00:38 +0200 Subject: [PATCH 038/127] Removed older version of index.js for tests and project --- index.js | 1 - spec/index.js | 299 -------------------------------------------------- 2 files changed, 300 deletions(-) delete mode 100644 index.js delete mode 100644 spec/index.js diff --git a/index.js b/index.js deleted file mode 100644 index bf361ce..0000000 --- a/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./src/pass"); diff --git a/spec/index.js b/spec/index.js deleted file mode 100644 index 07d647a..0000000 --- a/spec/index.js +++ /dev/null @@ -1,299 +0,0 @@ -const Passkit = require(".."); - -/* - * Yes, I know that I'm checking against "private" properties - * and that I shouldn't do that, but there's no other way to check - * the final results for each test. The only possible way is to - * read the generated stream of the zip file, unzip it - * (hopefully in memory) and check each property in pass.json file - * and .lproj directories. I hope who is reading this, will understand. - * - * Tests created upon Jasmine testing suite. - */ - -describe("Node-Passkit-generator", function () { - let pass; - beforeEach(() => { - pass = new Passkit.Pass({ - model: "../examples/examplePass.pass", - certificates: { - wwdr: "certificates/WWDR.pem", - signerCert: "certificates/signerCert.pem", - signerKey: { - keyFile: "certificates/signerKey.pem", - passphrase: "123456" - } - }, - overrides: {} - }); - }); - - describe("localize()", () => { - it("Won't apply changes without at least one parameter", () => { - pass.localize(); - expect(Object.keys(pass.l10n).length).toBe(0); - }); - - it("Passing first argument not a string, won't apply changes", () => { - pass.localize(5); - expect(Object.keys(pass.l10n).length).toBe(0); - }); - - it("Not passing the second argument, will apply changes (.lproj folder inclusion)", () => { - pass.localize("en"); - expect(Object.keys(pass.l10n).length).toBe(1); - }); - - it("Second argument of type different from object or undefined, won't apply changes.", () => { - pass.localize("en", 42); - expect(Object.keys(pass.l10n).length).toBe(0); - }); - - it("A second argument of type object will apply changes", () => { - pass.localize("it", { - "Test": "Prova" - }); - - expect(typeof pass.l10n["it"]).toBe("object"); - expect(pass.l10n["it"]["Test"]).toBe("Prova"); - }); - }); - - describe("expiration()", () => { - it("Missing first argument or not a string won't apply changes", () => { - pass.expiration(); - expect(pass._props["expirationDate"]).toBe(undefined); - pass.expiration(42); - expect(pass._props["expirationDate"]).toBe(undefined); - }); - - it("A date with defined format DD-MM-YYYY will apply changes", () => { - pass.expiration("10-04-2021", "DD-MM-YYYY"); - // this is made to avoid problems with winter and summer time: - // we focus only on the date and time for the tests. - let noTimeZoneDateTime = pass._props["expirationDate"].split("+")[0]; - expect(noTimeZoneDateTime).toBe("2021-04-10T00:00:00"); - }); - - it("A date with undefined custom format, will apply changes", () => { - pass.expiration("10-04-2021"); - // this is made to avoid problems with winter and summer time: - // we focus only on the date and time for the tests. - let noTimeZoneDateTime = pass._props["expirationDate"].split("+")[0]; - expect(noTimeZoneDateTime).toBe("2021-10-04T00:00:00"); - }); - - it("A date with defined format but with slashes will apply changes", () => { - pass.expiration("10/04/2021", "DD-MM-YYYY"); - // this is made to avoid problems with winter and summer time: - // we focus only on the date and time for the tests. - let noTimeZoneDateTime = pass._props["expirationDate"].split("+")[0]; - expect(noTimeZoneDateTime).toBe("2021-04-10T00:00:00"); - }); - - it("A date as a Date object will apply changes", () => { - pass.expiration(new Date(2020,5,1,0,0,0)); - // this is made to avoid problems with winter and summer time: - // we focus only on the date and time for the tests. - let noTimeZoneDateTime = pass._props["expirationDate"].split("+")[0]; - expect(noTimeZoneDateTime).toBe("2020-06-01T00:00:00"); - }); - - it("An invalid date, will not apply changes", () => { - pass.expiration("32/18/228317"); - expect(pass._props["expirationDate"]).toBe(undefined); - - pass.expiration("32/18/228317", "DD-MM-YYYY"); - expect(pass._props["expirationDate"]).toBe(undefined); - }); - }); - - describe("relevance()", () => { - describe("relevance('relevantDate')", () => { - it("A date with defined format DD-MM-YYYY will apply changes", () => { - pass.relevance("relevantDate", "10-04-2021", "DD-MM-YYYY"); - // this is made to avoid problems with winter and summer time: - // we focus only on the date and time for the tests. - let noTimeZoneDateTime = pass._props["relevantDate"].split("+")[0]; - expect(noTimeZoneDateTime).toBe("2021-04-10T00:00:00"); - }); - - it("A date with undefined custom format, will apply changes", () => { - pass.relevance("relevantDate", "10-04-2021"); - // this is made to avoid problems with winter and summer time: - // we focus only on the date and time for the tests. - let noTimeZoneDateTime = pass._props["relevantDate"].split("+")[0]; - expect(noTimeZoneDateTime).toBe("2021-10-04T00:00:00"); - }); - - it("A date with defined format but with slashes will apply changes", () => { - pass.relevance("relevantDate", "10/04/2021", "DD-MM-YYYY"); - // this is made to avoid problems with winter and summer time: - // we focus only on the date and time for the tests. - let noTimeZoneDateTime = pass._props["relevantDate"].split("+")[0]; - expect(noTimeZoneDateTime).toBe("2021-04-10T00:00:00"); - }); - - it("A date as a Date object will apply changes", () => { - pass.relevance("relevantDate",new Date(2020,5,1,0,0,0)); - // this is made to avoid problems with winter and summer time: - // we focus only on the date and time for the tests. - let noTimeZoneDateTime = pass._props["relevantDate"].split("+")[0]; - expect(noTimeZoneDateTime).toBe("2020-06-01T00:00:00"); - }); - }); - - describe("relevance('maxDistance')", () => { - it("A string is accepted and converted to Number", () => { - pass.relevance("maxDistance", "150"); - expect(pass._props["maxDistance"]).toBe(150); - }); - - it("A number is accepeted and will apply changes", () => { - pass.relevance("maxDistance", 150); - expect(pass._props["maxDistance"]).toBe(150); - }); - - it("Passing NaN value won't apply changes", () => { - pass.relevance("maxDistance", NaN); - expect(pass._props["maxDistance"]).toBe(undefined); - }); - }); - - describe("relevance('locations') && relevance('beacons')", () => { - it("A one-Invalid-schema location won't apply changes", () => { - pass.relevance("locations", [{ - "ibrupofene": "no", - "longitude": 0.00000000 - }]); - - expect(pass._props["locations"]).toBe(undefined); - }); - - it("A two locations, with one invalid, will be filtered", () => { - pass.relevance("locations", [{ - "ibrupofene": "no", - "longitude": 0.00000000 - }, { - "longitude": 4.42634523, - "latitude": 5.344233323352 - }]); - - expect(pass._props["locations"].length).toBe(1); - }); - }); - }); - - describe("barcode()", () => { - it("Missing data will won't apply changes", () => { - pass.barcode(); - - expect(pass._props["barcode"]).toBe(undefined); - expect(pass._props["barcodes"]).toBe(undefined); - }); - - it("Boolean parameter won't apply changes", () => { - pass.barcode(true); - - expect(pass._props["barcode"]).toBe(undefined); - expect(pass._props["barcodes"]).toBe(undefined); - }); - - it("Numeric parameter won't apply changes", () => { - pass.barcode(42); - - expect(pass._props["barcode"]).toBe(undefined); - expect(pass._props["barcodes"]).toBe(undefined); - }); - - it("String parameter will autogenerate all the objects", () => { - pass.barcode("28363516282"); - - expect(pass._props["barcode"] instanceof Object).toBe(true); - expect(pass._props["barcode"].message).toBe("28363516282"); - expect(pass._props["barcodes"].length).toBe(4); - }); - - it("Object parameter will be automatically converted to one-element Array", () => { - pass.barcode({ - message: "28363516282", - format: "PKBarcodeFormatPDF417", - messageEncoding: "utf8" - }); - - expect(pass._props["barcode"] instanceof Object).toBe(true); - expect(pass._props["barcode"].format).toBe("PKBarcodeFormatPDF417"); - expect(pass._props["barcodes"].length).toBe(1); - }); - - it("Array parameter will apply changes", () => { - pass.barcode({ - message: "28363516282", - format: "PKBarcodeFormatPDF417", - messageEncoding: "utf8" - }); - - expect(pass._props["barcode"] instanceof Object).toBe(true); - expect(pass._props["barcode"].format).toBe("PKBarcodeFormatPDF417"); - expect(pass._props["barcodes"].length).toBe(1); - }); - - it("Missing messageEncoding gets automatically added.", () => { - pass.barcode([{ - message: "28363516282", - format: "PKBarcodeFormatPDF417", - }]); - - expect(pass._props["barcode"] instanceof Object).toBe(true); - expect(pass._props["barcode"].messageEncoding).toBe("iso-8859-1"); - expect(pass._props["barcodes"][0].messageEncoding).toBe("iso-8859-1"); - }); - - it("Object without message property, will be filtered out", () => { - pass.barcode([{ - format: "PKBarcodeFormatPDF417", - }]); - - expect(pass._props["barcode"]).toBe(undefined); - expect(pass._props["barcodes"]).toBe(undefined); - }); - - it("Array containing non-object elements will be filtered out", () => { - pass.barcode([5, 10, 15, { - message: "28363516282", - format: "PKBarcodeFormatPDF417" - }, 7, 1]); - - expect(pass._props["barcode"] instanceof Object).toBe(true); - expect(pass._props["barcodes"].length).toBe(1); - expect(pass._props["barcodes"][0] instanceof Object).toBe(true); - }); - }); - - describe("barcode().backward()", () => { - it("Passing argument of type different from string or null, won't apply changes", function () { - pass - .barcode("Message-22645272183") - .backward(5); - - // unchanged - expect(pass._props["barcode"].format).toBe("PKBarcodeFormatQR"); - }); - - it("Null will delete backward support", () => { - pass - .barcode("Message-22645272183") - .backward(null); - - expect(pass._props["barcode"]).toBe(undefined); - }); - - it("Unknown format won't apply changes", () => { - pass - .barcode("Message-22645272183") - .backward("PKBingoBongoFormat"); - - expect(pass._props["barcode"].format).toBe("PKBarcodeFormatQR"); - }); - }); -}); From 9ed541dca7e6d79f068eb535750093cf7e57ed1f Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Wed, 12 Jun 2019 23:23:37 +0200 Subject: [PATCH 039/127] Removed String-Date and its format on methods that uses dates for native Date object --- src/pass.ts | 20 +++--- src/utils.ts | 185 +++++++++++++++++++++++++-------------------------- 2 files changed, 101 insertions(+), 104 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index af6d8f2..b28eaed 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -183,17 +183,16 @@ export class Pass implements PassIndexSignature { * Sets expirationDate property to the W3C date * * @method expiration - * @params {String} date - the date in string - * @params {String} format - a custom format for the date + * @params date * @returns {this} */ - expiration(date: string | Date, format?: string) { - if (typeof date !== "string" && !(date instanceof Date)) { + expiration(date: Date) { + if (!(date instanceof Date)) { return this; } - let dateParse = dateToW3CString(date, format); + const dateParse = dateToW3CString(date); if (!dateParse) { genericDebug(formatMessage("DATE_FORMAT_UNMATCH", "Expiration date")); @@ -221,13 +220,12 @@ export class Pass implements PassIndexSignature { * Checks and sets data for "beacons", "locations", "maxDistance" and "relevantDate" keys * * @method relevance - * @params {String} type - one of the key above - * @params {Any[]} data - the data to be pushed to the property - * @params {String} [relevanceDateFormat] - A custom format for the date + * @params type - one of the key above + * @params data - the data to be pushed to the property * @return {Number} The quantity of data pushed */ - relevance(type: string, data: any, relevanceDateFormat?: string) { + relevance(type: string, data: any) { let types = ["beacons", "locations", "maxDistance", "relevantDate"]; if (!type || !data || !types.includes(type)) { @@ -257,12 +255,12 @@ export class Pass implements PassIndexSignature { return assignLength(Number(!cond), this); } else if (type === "relevantDate") { - if (typeof data !== "string" && !(data instanceof Date)) { + if (!(data instanceof Date)) { genericDebug(formatMessage("DATE_FORMAT_UNMATCH", "Relevant Date")); return this; } - let dateParse = dateToW3CString(data, relevanceDateFormat); + let dateParse = dateToW3CString(data); if (!dateParse) { genericDebug(formatMessage("DATE_FORMAT_UNMATCH", "Relevant Date")); diff --git a/src/utils.ts b/src/utils.ts index 77f1243..2784756 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,93 +1,92 @@ -import moment from "moment"; -import { EOL } from "os"; - -/** - * Checks if an rgb value is compliant with CSS-like syntax - * - * @function isValidRGB - * @params {String} value - string to analyze - * @returns {Boolean} True if valid rgb, false otherwise - */ - -export function isValidRGB(value: string): boolean { - if (!value || typeof value !== "string") { - return false; - } - - const rgb = value.match(/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/); - - if (!rgb) { - return false; - } - - return rgb.slice(1, 4).every(v => Math.abs(Number(v)) <= 255); -} - -/** - * Converts a date to W3C Standard format - * - * @function dateToW3Cstring - * @params {String} date - The date to be parsed - * @params {String} [format] - a custom format - * @returns {String|undefined} The parsed string if the parameter is valid, - * undefined otherwise - */ - -export function dateToW3CString(date: string | Date, format?: string) { - if (typeof date !== "string" && !(date instanceof Date)) { - return ""; - } - - const parsedDate = date instanceof Date ? moment(date).format() : moment(date.replace(/\//g, "-"), format || ["MM-DD-YYYY hh:mm:ss", "DD-MM-YYYY hh:mm:ss"]).format(); - - if (parsedDate === "Invalid date") { - return undefined; - } - - return parsedDate; -} - -/** - * Apply a filter to arg0 to remove hidden files names (starting with dot) - * - * @function removeHidden - * @params {String[]} from - list of file names - * @return {String[]} - */ - -export function removeHidden(from: Array): Array { - return from.filter(e => e.charAt(0) !== "."); -} - -/** - * Creates a buffer of translations in Apple .strings format - * - * @function generateStringFile - * @params {Object} lang - structure containing related to ISO 3166 alpha-2 code for the language - * @returns {Buffer} Buffer to be written in pass.strings for language in lang - * @see https://apple.co/2M9LWVu - String Resources - */ - -export function generateStringFile(lang: { [index: string]: string }): Buffer { - if (!Object.keys(lang).length) { - return Buffer.from("", "utf8"); - } - - // Pass.strings format is the following one for each row: - // "key" = "value"; - - const strings = Object.keys(lang) - .map(key => `"${key}" = "${lang[key].replace(/"/g, '\"')}";`); - - return Buffer.from(strings.join(EOL), "utf8"); -} - -/** - * Creates a new object with custom length property - * @param {number} value - the length - * @param {Array>} source - the main sources of properties - */ - -export function assignLength(length: number, ...sources: Array<{ [key: string]: any }>): Array<{ [key: string]: any }> { - return Object.assign({ length }, ...sources); -} +import moment from "moment"; +import { EOL } from "os"; + +/** + * Checks if an rgb value is compliant with CSS-like syntax + * + * @function isValidRGB + * @params {String} value - string to analyze + * @returns {Boolean} True if valid rgb, false otherwise + */ + +export function isValidRGB(value: string): boolean { + if (!value || typeof value !== "string") { + return false; + } + + const rgb = value.match(/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/); + + if (!rgb) { + return false; + } + + return rgb.slice(1, 4).every(v => Math.abs(Number(v)) <= 255); +} + +/** + * Converts a date to W3C Standard format + * + * @function dateToW3Cstring + * @params date - The date to be parsed + * @returns - The parsed string if the parameter is valid, + * undefined otherwise + */ + +export function dateToW3CString(date: Date) { + if (!(date instanceof Date)) { + return ""; + } + + const parsedDate = moment(date).format(); + + if (parsedDate === "Invalid date") { + return undefined; + } + + return parsedDate; +} + +/** + * Apply a filter to arg0 to remove hidden files names (starting with dot) + * + * @function removeHidden + * @params {String[]} from - list of file names + * @return {String[]} + */ + +export function removeHidden(from: Array): Array { + return from.filter(e => e.charAt(0) !== "."); +} + +/** + * Creates a buffer of translations in Apple .strings format + * + * @function generateStringFile + * @params {Object} lang - structure containing related to ISO 3166 alpha-2 code for the language + * @returns {Buffer} Buffer to be written in pass.strings for language in lang + * @see https://apple.co/2M9LWVu - String Resources + */ + +export function generateStringFile(lang: { [index: string]: string }): Buffer { + if (!Object.keys(lang).length) { + return Buffer.from("", "utf8"); + } + + // Pass.strings format is the following one for each row: + // "key" = "value"; + + const strings = Object.keys(lang) + .map(key => `"${key}" = "${lang[key].replace(/"/g, '\"')}";`); + + return Buffer.from(strings.join(EOL), "utf8"); +} + +/** + * Creates a new object with custom length property + * @param {number} value - the length + * @param {Array>} source - the main sources of properties + */ + +export function assignLength(length: number, ...sources: Array<{ [key: string]: any }>): Array<{ [key: string]: any }> { + return Object.assign({ length }, ...sources); +} From 5213b5e190372b6a52bb1a1a835f3a5008a332c1 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Wed, 12 Jun 2019 23:26:44 +0200 Subject: [PATCH 040/127] Changed pass.json props patching --- src/pass.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index b28eaed..4dad3fe 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -517,9 +517,9 @@ export class Pass implements PassIndexSignature { .forEach(v => delete this._props[v]); Object.keys(this._props).forEach(prop => { - if (passFile[prop] instanceof Array) { + if (passFile[prop] && passFile[prop] instanceof Array) { passFile[prop].push(...this._props[prop]); - } else if (passFile[prop] instanceof Object) { + } else if (passFile[prop] && passFile[prop] instanceof Object) { Object.assign(passFile[prop], this._props[prop]); } else { passFile[prop] = this._props[prop]; From 3bf14be4a68b05dca167f79844e718211c905e8c Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Wed, 12 Jun 2019 23:27:49 +0200 Subject: [PATCH 041/127] Added Pass fields as property (for typescript) --- src/pass.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pass.ts b/src/pass.ts index 4dad3fe..3ef2ba3 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -35,6 +35,12 @@ export class Pass implements PassIndexSignature { private fieldsKeys: Set = new Set(); private passCore: schema.ValidPass = {}; + public headerFields: FieldsArray; + public primaryFields: FieldsArray; + public secondaryFields: FieldsArray; + public auxiliaryFields: FieldsArray; + public backFields: FieldsArray; + Certificates: schema.FinalCertificates; l10nTranslations: { [key: string]: { [key: string]: string } } = {}; [transitType]: string = ""; From 85a107f7ed7f84f3a7a2f1ae8e995e7f987448f0 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Wed, 12 Jun 2019 23:28:21 +0200 Subject: [PATCH 042/127] Added jasmine typs as dev dependency --- package-lock.json | 172 ++-------------------------------------------- package.json | 1 + 2 files changed, 6 insertions(+), 167 deletions(-) diff --git a/package-lock.json b/package-lock.json index f889244..672e864 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,19 +4,6 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" - }, - "@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", - "requires": { - "defer-to-connect": "^1.0.1" - } - }, "@types/archiver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-3.0.0.tgz", @@ -49,15 +36,11 @@ "@types/node": "*" } }, - "@types/got": { - "version": "9.4.4", - "resolved": "https://registry.npmjs.org/@types/got/-/got-9.4.4.tgz", - "integrity": "sha512-IGAJokJRE9zNoBdY5csIwN4U5qQn+20HxC0kM+BbUdfTKIXa7bOX/pdhy23NnLBRP8Wvyhx7X5e6EHJs+4d8HA==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/tough-cookie": "*" - } + "@types/jasmine": { + "version": "3.3.13", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.3.13.tgz", + "integrity": "sha512-iczmLoIiVymaD1TIr2UctxjFkNEslVE/QtNAUmpDsD71cZfZBAsPCUv1Y+8AwsfA8bLx2ccr7d95T9w/UAirlQ==", + "dev": true }, "@types/joi": { "version": "14.3.3", @@ -86,12 +69,6 @@ "@types/node": "*" } }, - "@types/tough-cookie": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", - "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==", - "dev": true - }, "archiver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.0.0.tgz", @@ -194,28 +171,6 @@ "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" }, - "cacheable-request": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.0.0.tgz", - "integrity": "sha512-2N7AmszH/WPPpl5Z3XMw1HAP+8d+xugnKQAeKvxFZ/04dbT/CAznqwbl+7eSr3HkwdepNwtb2yx3CAMQWvG01Q==", - "requires": { - "clone-response": "^1.0.2", - "get-stream": "^4.0.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^1.0.1", - "normalize-url": "^3.1.0", - "responselike": "^1.0.2" - } - }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "requires": { - "mimic-response": "^1.0.0" - } - }, "compress-commons": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz", @@ -272,24 +227,6 @@ "ms": "^2.1.1" } }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "requires": { - "mimic-response": "^1.0.0" - } - }, - "defer-to-connect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.0.2.tgz", - "integrity": "sha512-k09hcQcTDY+cwgiwa6PYKLm3jlagNzQ+RSvhjzESOGOx+MNOuXkxTfEvPrO1IOQ81tArCFYQgi631clB70RpQw==" - }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" - }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -308,14 +245,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } - }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", @@ -329,24 +258,6 @@ "path-is-absolute": "^1.0.0" } }, - "got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "requires": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - } - }, "graceful-fs": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", @@ -357,11 +268,6 @@ "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" }, - "http-cache-semantics": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz", - "integrity": "sha512-TcIMG3qeVLgDr1TEd2XvHaTnMPwYQUQMIBLy+5pLSDKYFc7UIqj39w8EGzZkaxoLv/l2K8HaI0t5AVA+YYgUew==" - }, "ieee754": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", @@ -436,19 +342,6 @@ "topo": "3.x.x" } }, - "json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" - }, - "keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", - "requires": { - "json-buffer": "3.0.0" - } - }, "lazystream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", @@ -497,16 +390,6 @@ "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" - }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" - }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -535,11 +418,6 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, - "normalize-url": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", - "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==" - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -548,35 +426,16 @@ "wrappy": "1" } }, - "p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" - }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" - }, "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -601,14 +460,6 @@ "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" }, - "responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", - "requires": { - "lowercase-keys": "^1.0.0" - } - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -641,11 +492,6 @@ "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" }, - "to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" - }, "topo": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz", @@ -667,14 +513,6 @@ "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", "dev": true }, - "url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", - "requires": { - "prepend-http": "^2.0.0" - } - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index febfcdd..0686314 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "devDependencies": { "@types/archiver": "^3.0.0", "@types/debug": "^4.1.4", + "@types/jasmine": "^3.3.13", "@types/joi": "^14.3.3", "@types/node": "^12.0.0", "@types/node-forge": "^0.8.3", From 48b9cd0b741eb971bf93e06a947713f40eb1e57e Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Wed, 12 Jun 2019 23:28:59 +0200 Subject: [PATCH 043/127] Added generic type to assignLength method --- src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 2784756..ce613b1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -87,6 +87,6 @@ export function generateStringFile(lang: { [index: string]: string }): Buffer { * @param {Array>} source - the main sources of properties */ -export function assignLength(length: number, ...sources: Array<{ [key: string]: any }>): Array<{ [key: string]: any }> { +export function assignLength(length: number, ...sources: Array<{ [key: string]: any }>): { [key: string]: any } & T { return Object.assign({ length }, ...sources); } From 10ef5f30b60b472b9e69af38a6c0694e99e515d6 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Wed, 12 Jun 2019 23:29:14 +0200 Subject: [PATCH 044/127] Set Field semantics field as optional --- src/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/schema.ts b/src/schema.ts index a879e7a..88afe6a 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -316,7 +316,7 @@ export interface Field { textAlignment?: string; key: string; value: string | number | Date; - semantics: Semantics; + semantics?: Semantics; dateStyle?: string; ignoreTimeZone?: boolean; isRelative?: boolean; From 8ff9ed51c89994c0366e6a8d615994fcaa5b91ea Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Wed, 12 Jun 2019 23:36:38 +0200 Subject: [PATCH 045/127] Converted examples to Typescript --- examples/README.md | 2 +- examples/{barcode.js => barcode.ts} | 12 +++++++----- examples/{expiration.js => expiration.ts} | 13 +++++++------ examples/{fields.js => fields.ts} | 8 ++++---- examples/{localization.js => localization.ts} | 17 ++++++++--------- examples/{webserver.js => webserver.ts} | 4 ++-- 6 files changed, 29 insertions(+), 27 deletions(-) rename examples/{barcode.js => barcode.ts} (87%) rename examples/{expiration.js => expiration.ts} (87%) rename examples/{fields.js => fields.ts} (96%) rename examples/{localization.js => localization.ts} (78%) rename examples/{webserver.js => webserver.ts} (86%) diff --git a/examples/README.md b/examples/README.md index a11c109..b8e1677 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,7 +7,7 @@ Express.js **was not** inserted as dipendency. git clone https://github.com/alexandercerutti/passkit-generator.git; cd passkit-generator; npm install; -npm install -g express; +npm install --no-save express; cd examples; node .js ``` diff --git a/examples/barcode.js b/examples/barcode.ts similarity index 87% rename from examples/barcode.js rename to examples/barcode.ts index 24ed126..69d6d9a 100644 --- a/examples/barcode.js +++ b/examples/barcode.ts @@ -8,14 +8,14 @@ * by a string */ -const app = require("./webserver"); -const { Pass } = require(".."); +import app from "./webserver"; +import { createPass } from ".."; -app.all(function manageRequest(request, response) { +app.all(async function manageRequest(request, response) { - let passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, ""); + const passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, ""); - let pass = new Pass({ + let pass = await createPass({ model: `./models/${request.params.modelName}`, certificates: { wwdr: "../certificates/WWDR.pem", @@ -68,7 +68,9 @@ app.all(function manageRequest(request, response) { bc.autocomplete(); } + // @ts-ignore - ignoring for logging purposes console.log("Barcode property is now:", pass._props["barcode"]); + // @ts-ignore - ignoring for logging purposes console.log("Barcodes support is autocompleted:", pass._props["barcodes"]); pass.generate().then(function (stream) { diff --git a/examples/expiration.js b/examples/expiration.ts similarity index 87% rename from examples/expiration.js rename to examples/expiration.ts index 662a83a..ce2bcdc 100644 --- a/examples/expiration.js +++ b/examples/expiration.ts @@ -5,12 +5,13 @@ * * To check if a ticket has an expiration date, you'll * have to wait two minutes. + * */ -const app = require("./webserver"); -const { Pass } = require(".."); +import app from "./webserver"; +import { createPass } from ".."; -app.all(function manageRequest(request, response) { +app.all(async function manageRequest(request, response) { if (!request.query.fn) { response.send("Generate a voided pass.
Generate a pass with expiration date"); return; @@ -18,7 +19,7 @@ app.all(function manageRequest(request, response) { let passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, ""); - let pass = new Pass({ + let pass = await createPass({ model: `./models/${request.params.modelName}`, certificates: { wwdr: "../certificates/WWDR.pem", @@ -35,11 +36,11 @@ app.all(function manageRequest(request, response) { pass.void(); } else if (request.query.fn === "expiration") { // 2 minutes later... - let d = new Date(); + const d = new Date(); d.setMinutes(d.getMinutes() + 2); // setting the expiration - pass.expiration(d.toLocaleString()); + pass.expiration(d); } pass.generate().then(function (stream) { diff --git a/examples/fields.js b/examples/fields.ts similarity index 96% rename from examples/fields.js rename to examples/fields.ts index 9832f97..3addfec 100644 --- a/examples/fields.js +++ b/examples/fields.ts @@ -9,13 +9,13 @@ * @Author: Alexander P. Cerutti */ -const app = require("./webserver"); -const { Pass } = require(".."); +import app from "./webserver"; +import { createPass } from ".."; -app.all(function manageRequest(request, response) { +app.all(async function manageRequest(request, response) { let passName = "exampleBooking" + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, ""); - let pass = new Pass({ + let pass = await createPass({ model: `./models/exampleBooking`, certificates: { wwdr: "../certificates/WWDR.pem", diff --git a/examples/localization.js b/examples/localization.ts similarity index 78% rename from examples/localization.js rename to examples/localization.ts index 720338d..6087f48 100644 --- a/examples/localization.js +++ b/examples/localization.ts @@ -4,14 +4,13 @@ * .pkpass file and check for .lproj folders */ -const app = require("./webserver"); -const { Pass } = require(".."); +import app from "./webserver"; +import { createPass } from ".."; -app.all(function manageRequest(request, response) { +app.all(async function manageRequest(request, response) { + const passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, ""); - let passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, ""); - - let pass = new Pass({ + const pass = await createPass({ model: `./models/${request.params.modelName}`, certificates: { wwdr: "../certificates/WWDR.pem", @@ -21,7 +20,7 @@ app.all(function manageRequest(request, response) { 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 @@ -50,8 +49,8 @@ app.all(function manageRequest(request, response) { // This language does not exist but is still added as .lproj folder pass.localize("zu", {}); - - console.log("Added languages", Object.keys(pass.l10n).join(", ")) + // @ts-ignore - ignoring for logging purposes. Do not replicate + console.log("Added languages", Object.keys(pass.l10nBundles).join(", ")) pass.generate().then(function (stream) { response.set({ diff --git a/examples/webserver.js b/examples/webserver.ts similarity index 86% rename from examples/webserver.js rename to examples/webserver.ts index 0bc72ed..a951e58 100644 --- a/examples/webserver.js +++ b/examples/webserver.ts @@ -4,7 +4,7 @@ * Requires express to run */ -const express = require("express"); +import express from "express"; const app = express(); app.use(express.json()); @@ -23,4 +23,4 @@ app.route("/gen") res.send("Cannot generate a pass. Specify a modelName in the url to continue.
Usage: /gen/modelName") }); -module.exports = app.route("/gen/:modelName"); +export default app.route("/gen/:modelName"); From f9206bafac517c23b76cb85ed88d318cb11a745d Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Wed, 12 Jun 2019 23:42:23 +0200 Subject: [PATCH 046/127] Moved tests to typescript --- spec/index.ts | 297 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 spec/index.ts diff --git a/spec/index.ts b/spec/index.ts new file mode 100644 index 0000000..dc9b18d --- /dev/null +++ b/spec/index.ts @@ -0,0 +1,297 @@ +import { createPass } from ".."; +import { Pass } from "../src/pass"; + +/* + * Yes, I know that I'm checking against "private" properties + * and that I shouldn't do that, but there's no other way to check + * the final results for each test. The only possible way is to + * read the generated stream of the zip file, unzip it + * (hopefully in memory) and check each property in pass.json file + * and .lproj directories. I hope who is reading this, will understand. + * + * Tests created upon Jasmine testing suite. + */ + +describe("Node-Passkit-generator", function () { + let pass: Pass; + beforeEach(async () => { + pass = await createPass({ + model: "examples/models/examplePass.pass", + certificates: { + wwdr: "certificates/WWDR.pem", + signerCert: "certificates/signerCert.pem", + signerKey: { + keyFile: "certificates/signerKey.pem", + passphrase: "123456" + } + }, + overrides: {} + }); + }); + + describe("localize()", () => { + it("Won't apply changes without at least one parameter", () => { + // @ts-ignore -- Ignoring for test purposes + pass.localize(); + expect(Object.keys(pass.l10nTranslations).length).toBe(0); + }); + + it("Passing first argument not a string, won't apply changes", () => { + // @ts-ignore -- Ignoring for test purposes + pass.localize(5); + expect(Object.keys(pass.l10nTranslations).length).toBe(0); + }); + + it("Not passing the second argument, will apply changes (.lproj folder inclusion)", () => { + pass.localize("en"); + expect(Object.keys(pass.l10nTranslations).length).toBe(1); + }); + + it("Second argument of type different from object or undefined, won't apply changes.", () => { + // @ts-ignore -- Ignoring for test purposes + pass.localize("en", 42); + expect(Object.keys(pass.l10nTranslations).length).toBe(0); + }); + + it("A second argument of type object will apply changes", () => { + pass.localize("it", { + "Test": "Prova" + }); + + expect(typeof pass.l10nTranslations["it"]).toBe("object"); + expect(pass.l10nTranslations["it"]["Test"]).toBe("Prova"); + }); + }); + + describe("expiration()", () => { + it("Missing first argument or not a string won't apply changes", () => { + // @ts-ignore -- Ignoring for test purposes + pass.expiration(); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["expirationDate"]).toBe(undefined); + // @ts-ignore -- Ignoring for test purposes + pass.expiration(42); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["expirationDate"]).toBe(undefined); + }); + + it("A date as a Date object will apply changes", () => { + pass.expiration(new Date(2020,5,1,0,0,0)); + // this is made to avoid problems with winter and summer time: + // we focus only on the date and time for the tests. + // @ts-ignore -- Ignoring for test purposes + let noTimeZoneDateTime = pass._props["expirationDate"].split("+")[0]; + expect(noTimeZoneDateTime).toBe("2020-06-01T00:00:00"); + }); + + it("An invalid date, will not apply changes", () => { + // @ts-ignore -- Ignoring for test purposes + pass.expiration("32/18/228317"); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["expirationDate"]).toBe(undefined); + + // @ts-ignore -- Ignoring for test purposes + pass.expiration("32/18/228317"); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["expirationDate"]).toBe(undefined); + }); + }); + + describe("relevance()", () => { + describe("relevance('relevantDate')", () => { + it("A date object will apply changes", () => { + pass.relevance("relevantDate", new Date("10-04-2021")); + // this is made to avoid problems with winter and summer time: + // we focus only on the date and time for the tests. + // @ts-ignore -- Ignoring for test purposes + let noTimeZoneDateTime = pass._props["relevantDate"].split("+")[0]; + expect(noTimeZoneDateTime).toBe("2021-04-10T00:00:00"); + }); + }); + + describe("relevance('maxDistance')", () => { + it("A string is accepted and converted to Number", () => { + pass.relevance("maxDistance", "150"); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["maxDistance"]).toBe(150); + }); + + it("A number is accepeted and will apply changes", () => { + pass.relevance("maxDistance", 150); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["maxDistance"]).toBe(150); + }); + + it("Passing NaN value won't apply changes", () => { + pass.relevance("maxDistance", NaN); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["maxDistance"]).toBe(undefined); + }); + }); + + describe("relevance('locations') && relevance('beacons')", () => { + it("A one-Invalid-schema location won't apply changes", () => { + pass.relevance("locations", [{ + "ibrupofene": "no", + "longitude": 0.00000000 + }]); + + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["locations"]).toBe(undefined); + }); + + it("A two locations, with one invalid, will be filtered", () => { + pass.relevance("locations", [{ + "ibrupofene": "no", + "longitude": 0.00000000 + }, { + "longitude": 4.42634523, + "latitude": 5.344233323352 + }]); + + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["locations"].length).toBe(1); + }); + }); + }); + + describe("barcode()", () => { + it("Missing data will won't apply changes", () => { + // @ts-ignore -- Ignoring for test purposes + pass.barcode(); + + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcode"]).toBe(undefined); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcodes"]).toBe(undefined); + }); + + it("Boolean parameter won't apply changes", () => { + pass.barcode(true); + + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcode"]).toBe(undefined); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcodes"]).toBe(undefined); + }); + + it("Numeric parameter won't apply changes", () => { + pass.barcode(42); + + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcode"]).toBe(undefined); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcodes"]).toBe(undefined); + }); + + it("String parameter will autogenerate all the objects", () => { + pass.barcode("28363516282"); + + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcode"] instanceof Object).toBe(true); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcode"].message).toBe("28363516282"); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcodes"].length).toBe(4); + }); + + it("Object parameter will be automatically converted to one-element Array", () => { + pass.barcode({ + message: "28363516282", + format: "PKBarcodeFormatPDF417", + messageEncoding: "utf8" + }); + + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcode"] instanceof Object).toBe(true); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcode"].format).toBe("PKBarcodeFormatPDF417"); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcodes"].length).toBe(1); + }); + + it("Array parameter will apply changes", () => { + pass.barcode({ + message: "28363516282", + format: "PKBarcodeFormatPDF417", + messageEncoding: "utf8" + }); + + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcode"] instanceof Object).toBe(true); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcode"].format).toBe("PKBarcodeFormatPDF417"); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcodes"].length).toBe(1); + }); + + it("Missing messageEncoding gets automatically added.", () => { + pass.barcode([{ + message: "28363516282", + format: "PKBarcodeFormatPDF417", + }]); + + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcode"] instanceof Object).toBe(true); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcode"].messageEncoding).toBe("iso-8859-1"); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcodes"][0].messageEncoding).toBe("iso-8859-1"); + }); + + it("Object without message property, will be filtered out", () => { + pass.barcode([{ + format: "PKBarcodeFormatPDF417", + }]); + + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcode"]).toBe(undefined); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcodes"]).toBe(undefined); + }); + + it("Array containing non-object elements will be filtered out", () => { + pass.barcode([5, 10, 15, { + message: "28363516282", + format: "PKBarcodeFormatPDF417" + }, 7, 1]); + + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcode"] instanceof Object).toBe(true); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcodes"].length).toBe(1); + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcodes"][0] instanceof Object).toBe(true); + }); + }); + + describe("barcode().backward()", () => { + it("Passing argument of type different from string or null, won't apply changes", function () { + pass + .barcode("Message-22645272183") + .backward(5); + + // unchanged + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcode"].format).toBe("PKBarcodeFormatQR"); + }); + + it("Null will delete backward support", () => { + pass + .barcode("Message-22645272183") + .backward(null); + + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcode"]).toBe(undefined); + }); + + it("Unknown format won't apply changes", () => { + pass + .barcode("Message-22645272183") + .backward("PKBingoBongoFormat"); + + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["barcode"].format).toBe("PKBarcodeFormatQR"); + }); + }); +}); From 5c5e573fde516c1ee46d391bb2edde1ef902db8f Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 15 Jun 2019 01:21:26 +0200 Subject: [PATCH 047/127] Recreated Barcodes functions to accept a string and an object --- src/pass.ts | 140 ++++++++++++++++++++++++++-------------------------- 1 file changed, 71 insertions(+), 69 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 3ef2ba3..9069b32 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -280,28 +280,38 @@ export class Pass implements PassIndexSignature { /** * Adds barcodes to "barcode" and "barcodes" properties. - * It will let later to add the missing versions + * It will let to add the missing versions later. * * @method barcode - * @params {Object|String} data - the data to be added + * @params data - the data to be added * @return {this} Improved this with length property and other methods */ - barcode(data) { - if (!data) { + barcode(first: string | schema.Barcode, ...data: schema.Barcode[]): this { + const isFirstParameterValid = ( + first && ( + typeof first === "string" && first.length || ( + typeof first === "object" && + first.hasOwnProperty("message") + ) + ) + ); + + if (!isFirstParameterValid) { return assignLength(0, this, { autocomplete: noop, backward: noop, }); } - if (typeof data === "string" || (data instanceof Object && !Array.isArray(data) && !data.format && data.message)) { - const autogen = barcodesFromUncompleteData(data instanceof Object ? data : { message: data }); + if (typeof first === "string") { + const autogen = barcodesFromUncompleteData(first); if (!autogen.length) { + barcodeDebug(formatMessage("BRC_AUTC_MISSING_DATA")); return assignLength(0, this, { autocomplete: noop, - backward: noop + backward: noop, }); } @@ -310,50 +320,48 @@ export class Pass implements PassIndexSignature { return assignLength(autogen.length, this, { autocomplete: noop, - backward: (format) => this[barcodesSetBackward](format) + backward: (format: schema.BarcodeFormat) => this[barcodesSetBackward](format) + }); + } else { + const barcodes = [first, ...(data || [])]; + + // Stripping from the array not-object elements + // and the ones that does not pass validation. + // Validation assign default value to missing parameters (if any). + + const valid = barcodes.reduce((acc, current) => { + if (!(current && current instanceof Object)) { + return acc; + } + + const validated = schema.getValidated(current, "barcode"); + + if (!(validated && validated instanceof Object && Object.keys(validated).length)) { + return acc; + } + + return [...acc, validated] as schema.Barcode[]; + }, []); + + if (valid.length) { + // With this check, we want to avoid that + // PKBarcodeFormatCode128 gets chosen automatically + // if it is the first. If true, we'll get 1 + // (so not the first index) + const barcodeFirstValidIndex = Number(valid[0].format === "PKBarcodeFormatCode128"); + + if (valid.length > 0 && valid[barcodeFirstValidIndex]) { + this._props["barcode"] = valid[barcodeFirstValidIndex]; + } + + this._props["barcodes"] = valid; + } + + return assignLength(valid.length, this, { + autocomplete: () => this[barcodesFillMissing](), + backward: (format: schema.BarcodeFormat) => this[barcodesSetBackward](format), }); } - - if (!(data instanceof Array)) { - data = [data]; - } - - // Stripping from the array not-object elements, objects with no message - // and the ones that does not pass validation. - // Validation assign default value to missing parameters (if any). - - const valid = data.reduce((acc, current) => { - if (!(current && current instanceof Object && current.hasOwnProperty("message"))) { - return acc; - } - - const validated = schema.getValidated(current, "barcode"); - - if (!(validated && validated instanceof Object && Object.keys(validated).length)) { - return acc; - } - - return [...acc, validated]; - }, []); - - if (valid.length) { - // With this check, we want to avoid that - // PKBarcodeFormatCode128 gets chosen automatically - // if it is the first. If true, we'll get 1 - // (so not the first index) - const barcodeFirstValidIndex = Number(valid[0].format === "PKBarcodeFormatCode128"); - - if (valid.length > 0) { - this._props["barcode"] = valid[barcodeFirstValidIndex]; - } - - this._props["barcodes"] = valid; - } - - return assignLength(valid.length, this, { - autocomplete: () => this[barcodesFillMissing](), - backward: (format) => this[barcodesSetBackward](format), - }); } /** @@ -366,20 +374,20 @@ export class Pass implements PassIndexSignature { */ [barcodesFillMissing]() { - let props = this._props["barcodes"]; + const props = this._props["barcodes"]; if (props.length === 4 || !props.length) { return assignLength(0, this, { autocomplete: noop, - backward: (format) => this[barcodesSetBackward](format) + backward: (format: schema.BarcodeFormat) => this[barcodesSetBackward](format) }); } - this._props["barcodes"] = barcodesFromUncompleteData(props[0]); + this._props["barcodes"] = barcodesFromUncompleteData(props[0].message); return assignLength(4 - props.length, this, { autocomplete: noop, - backward: (format) => this[barcodesSetBackward](format) + backward: (format: schema.BarcodeFormat) => this[barcodesSetBackward](format) }); } @@ -389,11 +397,11 @@ export class Pass implements PassIndexSignature { * property "barcode". * * @method Symbol/barcodesSetBackward - * @params {String} format - the format, or part of it, to be used + * @params format - the format to be used * @return {this} */ - [barcodesSetBackward](format) { + [barcodesSetBackward](format: schema.BarcodeFormat | null): this { if (format === null) { this._props["barcode"] = undefined; return this; @@ -564,16 +572,15 @@ export class Pass implements PassIndexSignature { * Automatically generates barcodes for all the types given common info * * @method barcodesFromMessage - * @params {Object} data - common info, may be object or the message itself - * @params {String} data.message - the content to be placed inside "message" field - * @params {String} [data.altText=data.message] - alternativeText, is message content if not overwritten - * @params {String} [data.messageEncoding=iso-8859-1] - the encoding - * @return {Object[]} Object array barcodeDict compliant + * @params data - common info, may be object or the message itself + * @params data.message - the content to be placed inside "message" field + * @params [data.altText=data.message] - alternativeText, is message content if not overwritten + * @params [data.messageEncoding=iso-8859-1] - the encoding + * @return Object array barcodeDict compliant */ -function barcodesFromUncompleteData(origin: schema.Barcode): schema.Barcode[] { - if (!(origin.message && typeof origin.message === "string")) { - barcodeDebug(formatMessage("BRC_AUTC_MISSING_DATA")); +function barcodesFromUncompleteData(message: string): schema.Barcode[] { + if (!(message && typeof message === "string")) { return []; } @@ -582,10 +589,5 @@ function barcodesFromUncompleteData(origin: schema.Barcode): schema.Barcode[] { "PKBarcodeFormatPDF417", "PKBarcodeFormatAztec", "PKBarcodeFormatCode128" - ].map(format => - schema.getValidated( - Object.assign({}, origin, { format }), - "barcode" - ) - ); + ].map(format => schema.getValidated({ format, message }, "barcode")); } From 82dd8711ba4de7a5bde329c9ba2df36413cb1b7a Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 15 Jun 2019 01:22:01 +0200 Subject: [PATCH 048/127] Added BarcodeFormat as type --- src/schema.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/schema.ts b/src/schema.ts index 88afe6a..6d168e5 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -301,6 +301,8 @@ export interface Barcode { message: string; } +export type BarcodeFormat = "PKBarcodeFormatQR" | "PKBarcodeFormatPDF417" | "PKBarcodeFormatAztec" | "PKBarcodeFormatCode128"; + const barcode = Joi.object().keys({ altText: Joi.string(), messageEncoding: Joi.string().default("iso-8859-1"), From 7c0a667a3ceea59762113c5064f7efca0ee5b218 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 15 Jun 2019 19:04:08 +0200 Subject: [PATCH 049/127] Adapted barcode example to new implementation --- examples/barcode.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/barcode.ts b/examples/barcode.ts index 69d6d9a..deaad66 100644 --- a/examples/barcode.ts +++ b/examples/barcode.ts @@ -10,6 +10,7 @@ import app from "./webserver"; import { createPass } from ".."; +import { PassWithBarcodeMethods } from "../src/pass"; app.all(async function manageRequest(request, response) { @@ -28,7 +29,7 @@ app.all(async function manageRequest(request, response) { overrides: request.body || request.params || request.query, }); - let bc; + let bc: PassWithBarcodeMethods; if (request.query.alt === true) { // After this, pass.props["barcodes"] will have support for all the formats @@ -40,7 +41,7 @@ app.all(async function manageRequest(request, response) { // of the passed format (the valid ones) and pass.props["barcode"] the first of barcodes. // if not specified, altText is automatically the message - bc = pass.barcode([{ + bc = pass.barcode({ message: "Thank you for using this package <3", format: "PKBarcodeFormatCode128" }, { @@ -49,7 +50,7 @@ app.all(async function manageRequest(request, response) { }, { message: "Thank you for using this package <3", format: "PKBarcodeFormatMock44617" - }]); + }); } // You can change the format chosen for barcode prop support by calling .backward() From 978b5699cea35025eb824cc8a79ebe02378d227a Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 15 Jun 2019 19:05:32 +0200 Subject: [PATCH 050/127] Improved internal typings --- src/pass.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 9069b32..debec20 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -25,6 +25,11 @@ interface PassIndexSignature { [key: string]: any; } +export interface PassWithBarcodeMethods extends Pass { + backward: (format: schema.BarcodeFormat) => Pass; + autocomplete: () => Pass; +} + export class Pass implements PassIndexSignature { // private model: string; private bundle: schema.BundleUnit; @@ -193,7 +198,7 @@ export class Pass implements PassIndexSignature { * @returns {this} */ - expiration(date: Date) { + expiration(date: Date): this { if (!(date instanceof Date)) { return this; } @@ -287,7 +292,7 @@ export class Pass implements PassIndexSignature { * @return {this} Improved this with length property and other methods */ - barcode(first: string | schema.Barcode, ...data: schema.Barcode[]): this { + barcode(first: string | schema.Barcode, ...data: schema.Barcode[]): PassWithBarcodeMethods { const isFirstParameterValid = ( first && ( typeof first === "string" && first.length || ( @@ -373,7 +378,7 @@ export class Pass implements PassIndexSignature { * @returns {this} Improved this, with length property and retroCompatibility method. */ - [barcodesFillMissing]() { + [barcodesFillMissing](): this { const props = this._props["barcodes"]; if (props.length === 4 || !props.length) { @@ -433,11 +438,11 @@ export class Pass implements PassIndexSignature { * Sets nfc fields in properties * * @method nfc - * @params {Object} data - the data to be pushed in the pass + * @params data - the data to be pushed in the pass * @returns {this} */ - nfc(data: schema.NFC) { + nfc(data: schema.NFC): this { if (!(typeof data === "object" && !Array.isArray(data) && schema.isValid(data, "nfcDict"))) { genericDebug("Invalid NFC data provided"); return this; From 691aa69d506e3e94a05ecfe0897f0f0b7a506233 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 15 Jun 2019 19:05:50 +0200 Subject: [PATCH 051/127] Updated BRC_AUTC_MISSING_DATA message --- src/messages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/messages.ts b/src/messages.ts index c70e66f..05e67ce 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -19,7 +19,7 @@ const debugMessages: MessageGroup = { TRSTYPE_NOT_VALID: "Transit type changing rejected as not compliant with Apple Specifications. Transit type would become \"%s\" but should be in [PKTransitTypeAir, PKTransitTypeBoat, PKTransitTypeBus, PKTransitTypeGeneric, PKTransitTypeTrain]", BRC_NOT_SUPPORTED: "Format not found among barcodes. Cannot set backward compatibility.", BRC_FORMATTYPE_UNMATCH: "Format must be a string or null. Cannot set backward compatibility.", - BRC_AUTC_MISSING_DATA: "Unable to autogenerate barcodes. Data is not a string or an object with no message field", + BRC_AUTC_MISSING_DATA: "Unable to autogenerate barcodes. Data is not a string.", BRC_BW_FORMAT_UNSUPPORTED: "This format is not supported (by Apple) for backward support. Please choose another one.", DATE_FORMAT_UNMATCH: "%s was not set due to incorrect date format." }; From 97ac79f2f4f6b8fbd086b9baa0cac676d507f76d Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 15 Jun 2019 22:08:21 +0200 Subject: [PATCH 052/127] Indentation fix --- src/pass.ts | 63 +++++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index debec20..ea344f4 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -111,7 +111,7 @@ export class Pass implements PassIndexSignature { * if there's already a buffer of the same folder and called * `pass.strings`, we'll merge the two buffers. We'll create * it otherwise. - */ + */ if (!this.l10nBundles[lang]) { this.l10nBundles[lang] = {}; @@ -124,50 +124,51 @@ export class Pass implements PassIndexSignature { } if (!(this.l10nBundles[lang] && Object.keys(this.l10nBundles[lang]).length)) { - return; - } + return; + } - /** + /** * Assigning all the localization files to the final bundle * by mapping the buffer to the pass-relative file path; - * - * We are replacing the slashes to avoid Windows slashes - * composition. - */ + * + * We are replacing the slashes to avoid Windows slashes + * composition. + */ + Object.assign(finalBundle, ...Object.keys(this.l10nBundles[lang]) .map(fileName => { const fullPath = path.join(`${lang}.lproj`, fileName).replace(/\\/, "/"); return { [fullPath]: this.l10nBundles[lang][fileName] }; }) ); - }); + }); - /* - * Parsing the buffers, pushing them into the archive - * and returning the compiled manifest - */ - const archive = archiver("zip"); + /* + * Parsing the buffers, pushing them into the archive + * and returning the compiled manifest + */ + const archive = archiver("zip"); const manifest = Object.keys(finalBundle).reduce((acc, current) => { - let hashFlow = forge.md.sha1.create(); + let hashFlow = forge.md.sha1.create(); hashFlow.update(finalBundle[current].toString("binary")); archive.append(finalBundle[current], { name: current }); acc[current] = hashFlow.digest().toHex(); - return acc; - }, {}); + return acc; + }, {}); - const signatureBuffer = this._sign(manifest); + const signatureBuffer = this._sign(manifest); - archive.append(signatureBuffer, { name: "signature" }); - archive.append(JSON.stringify(manifest), { name: "manifest.json" }); + archive.append(signatureBuffer, { name: "signature" }); + archive.append(JSON.stringify(manifest), { name: "manifest.json" }); - const passStream = new stream.PassThrough(); + const passStream = new stream.PassThrough(); - archive.pipe(passStream); + archive.pipe(passStream); - return archive.finalize().then(() => passStream); + return archive.finalize().then(() => passStream); } /** @@ -535,16 +536,16 @@ export class Pass implements PassIndexSignature { .filter(v => this._props[v] && !isValidRGB(this._props[v])) .forEach(v => delete this._props[v]); - Object.keys(this._props).forEach(prop => { + Object.keys(this._props).forEach(prop => { if (passFile[prop] && passFile[prop] instanceof Array) { - passFile[prop].push(...this._props[prop]); + passFile[prop].push(...this._props[prop]); } else if (passFile[prop] && passFile[prop] instanceof Object) { - Object.assign(passFile[prop], this._props[prop]); - } else { - passFile[prop] = this._props[prop]; - } - }); - } + Object.assign(passFile[prop], this._props[prop]); + } else { + passFile[prop] = this._props[prop]; + } + }); + } this._fields.forEach(field => { passFile[this.type][field] = this[field]; From e83906102c521f5da53ce4358c42f3c238793bca Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 15 Jun 2019 22:08:50 +0200 Subject: [PATCH 053/127] Removed useless check in fieldsArray --- src/fieldsArray.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fieldsArray.ts b/src/fieldsArray.ts index 114b94d..5cbda83 100644 --- a/src/fieldsArray.ts +++ b/src/fieldsArray.ts @@ -27,7 +27,7 @@ export default class FieldsArray extends Array { return acc; } - if (acc.some(e => e.key === current.key) || this[poolSymbol].has(current.key)) { + 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); From 2c2e4ef79d9d158802f25ee34d473f47e73ac7c6 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 15 Jun 2019 23:33:06 +0200 Subject: [PATCH 054/127] Added PassWithLengthField interface --- src/pass.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pass.ts b/src/pass.ts index ea344f4..46b6bf1 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -25,7 +25,11 @@ interface PassIndexSignature { [key: string]: any; } -export interface PassWithBarcodeMethods extends Pass { +export interface PassWithLengthField extends Pass { + length: number; +} + +export interface PassWithBarcodeMethods extends PassWithLengthField { backward: (format: schema.BarcodeFormat) => Pass; autocomplete: () => Pass; } From 1668678f7cc5c005254717f8738cd0f2a5a0cadc Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 15 Jun 2019 23:34:46 +0200 Subject: [PATCH 055/127] Splitted relevance method in beacons, locations, relevantDate and maxDistance as override --- src/pass.ts | 89 ++++++++++++++++++++++++++++++++------------------- src/schema.ts | 2 ++ 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 46b6bf1..8b0f308 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -233,59 +233,82 @@ export class Pass implements PassIndexSignature { } /** - * Checks and sets data for "beacons", "locations", "maxDistance" and "relevantDate" keys - * - * @method relevance - * @params type - one of the key above - * @params data - the data to be pushed to the property - * @return {Number} The quantity of data pushed + * Sets current pass' relevancy through beacons + * @param data + * @returns {Pass} */ - relevance(type: string, data: any) { - let types = ["beacons", "locations", "maxDistance", "relevantDate"]; - - if (!type || !data || !types.includes(type)) { + beacons(...data: schema.Beacon[]): this { + if (!data.length) { return assignLength(0, this); } - if (type === "beacons" || type === "locations") { - if (!(data instanceof Array)) { - data = [data]; + const validBeacons = data.reduce((acc, current) => { + if (!(Object.keys(current).length && schema.isValid(current, "locations"))) { + return acc; } - let valid = data.filter(d => schema.isValid(d, type + "Dict")); + return [...acc, current]; + }, []); - this._props[type] = valid.length ? valid : undefined; - - return assignLength(valid.length, this); + if (!validBeacons.length) { + return assignLength(0, this); } - if (type === "maxDistance" && (typeof data === "string" || typeof data === "number")) { - let conv = Number(data); - // condition to proceed - let cond = isNaN(conv); + this._props["beacons"] = validBeacons; - if (!cond) { - this._props[type] = conv; + return assignLength(validBeacons.length, this); + } + + /** + * Sets current pass' relevancy through locations + * @param data + * @returns {Pass} + */ + + locations(...data: schema.Location[]): this { + if (!data.length) { + return assignLength(0, this); } - return assignLength(Number(!cond), this); - } else if (type === "relevantDate") { - if (!(data instanceof Date)) { + const validLocations = data.reduce((acc, current) => { + if (!(Object.keys(current).length && schema.isValid(current, "locations"))) { + return acc; + } + + return [...acc, current]; + }, []); + + if (!validLocations.length) { + return assignLength(0, this); + } + + this._props["locations"] = validLocations; + + return assignLength(validLocations.length, this); + } + + /** + * Sets current pass' relevancy through a date + * @param data + * @returns {Pass} + */ + + relevantDate(date: Date): this { + if (!(date instanceof Date)) { genericDebug(formatMessage("DATE_FORMAT_UNMATCH", "Relevant Date")); return this; } - let dateParse = dateToW3CString(data); + const parsedDate = dateToW3CString(date); - if (!dateParse) { - genericDebug(formatMessage("DATE_FORMAT_UNMATCH", "Relevant Date")); - } else { - this._props[type] = dateParse; + if (!parsedDate) { + // @TODO: create message "Unable to format date" + return this; } - return assignLength(Number(!!dateParse), this); - } + this._props["relevantDate"] = parsedDate; + return this; } /** diff --git a/src/schema.ts b/src/schema.ts index 6d168e5..f871f18 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -76,6 +76,7 @@ interface OverridesSupportedOptions { labelColor?: string; groupingIdentifier?: string; suppressStripShine?: boolean; + maxDistance?: number; } const supportedOptions = Joi.object().keys({ @@ -92,6 +93,7 @@ const supportedOptions = Joi.object().keys({ groupingIdentifier: Joi.string(), suppressStripShine: Joi.boolean(), logoText: Joi.string(), + maxDistance: Joi.number().positive(), }).with("webServiceURL", "authenticationToken"); From 734a9abc3bcb8446dd489e2642f8ad10afdde5ee Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 15 Jun 2019 23:39:33 +0200 Subject: [PATCH 056/127] Improved Localize comments and signature --- src/pass.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 8b0f308..0e5041f 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -179,15 +179,15 @@ export class Pass implements PassIndexSignature { * Adds traslated strings object to the list of translation to be inserted into the pass * * @method localize - * @params {String} lang - the ISO 3166 alpha-2 code for the language - * @params {Object} translations - key/value pairs where key is the + * @params lang - the ISO 3166 alpha-2 code for the language + * @params translations - key/value pairs where key is the * string appearing in pass.json and value the translated string * @returns {this} * * @see https://apple.co/2KOv0OW - Passes support localization */ - localize(lang: string, translations?: { [key: string]: string }) { + localize(lang: string, translations?: { [key: string]: string }): this { if (lang && typeof lang === "string" && (typeof translations === "object" || translations === undefined)) { this.l10nTranslations[lang] = translations || {}; } From f5cb43827ebcfc9a07d3be7c71054c3e8f991229 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 15 Jun 2019 23:57:18 +0200 Subject: [PATCH 057/127] Fixed schema names --- src/pass.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 0e5041f..4cbea6e 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -244,7 +244,7 @@ export class Pass implements PassIndexSignature { } const validBeacons = data.reduce((acc, current) => { - if (!(Object.keys(current).length && schema.isValid(current, "locations"))) { + if (!(Object.keys(current).length && schema.isValid(current, "beaconsDict"))) { return acc; } @@ -269,10 +269,10 @@ export class Pass implements PassIndexSignature { locations(...data: schema.Location[]): this { if (!data.length) { return assignLength(0, this); - } + } const validLocations = data.reduce((acc, current) => { - if (!(Object.keys(current).length && schema.isValid(current, "locations"))) { + if (!(Object.keys(current).length && schema.isValid(current, "locationsDict"))) { return acc; } @@ -286,7 +286,7 @@ export class Pass implements PassIndexSignature { this._props["locations"] = validLocations; return assignLength(validLocations.length, this); - } + } /** * Sets current pass' relevancy through a date From 7befe2ab98cce417eb9aa088bb726c6ee0a377a2 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 16 Jun 2019 11:47:51 +0200 Subject: [PATCH 058/127] Improved types --- src/pass.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 4cbea6e..aacf704 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -38,9 +38,9 @@ export class Pass implements PassIndexSignature { // private model: string; private bundle: schema.BundleUnit; private l10nBundles: schema.PartitionedBundle["l10nBundle"]; - private _fields: string[]; + private _fields: (keyof schema.PassFields)[]; private _props: schema.ValidPass = {}; - private type: string = ""; + private type: keyof schema.ValidPassType; private fieldsKeys: Set = new Set(); private passCore: schema.ValidPass = {}; @@ -64,7 +64,8 @@ export class Pass implements PassIndexSignature { // getting pass.json this.passCore = JSON.parse(this.bundle["pass.json"].toString("utf8")); - this.type = Object.keys(this.passCore).find(key => /(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key)); + this.type = Object.keys(this.passCore) + .find(key => /(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key)) as keyof schema.ValidPassType; if (!this.type) { throw new Error("Missing type in model"); @@ -76,7 +77,7 @@ export class Pass implements PassIndexSignature { this[transitType] = this.passCore[this.type]["transitType"]; } - const typeFields = Object.keys(this.passCore[this.type]); + const typeFields = Object.keys(this.passCore[this.type]) as (keyof schema.PassFields)[]; this._fields = ["primaryFields", "secondaryFields", "auxiliaryFields", "backFields", "headerFields"]; this._fields.forEach(fieldName => { @@ -84,7 +85,7 @@ export class Pass implements PassIndexSignature { this[fieldName] = new FieldsArray( this.fieldsKeys, ...this.passCore[this.type][fieldName] - .filter((field: schema.Field) => schema.isValid(field, "field")) + .filter(field => schema.isValid(field, "field")) ); } else { this[fieldName] = new FieldsArray(this.fieldsKeys); @@ -238,7 +239,7 @@ export class Pass implements PassIndexSignature { * @returns {Pass} */ - beacons(...data: schema.Beacon[]): this { + beacons(...data: schema.Beacon[]): PassWithLengthField { if (!data.length) { return assignLength(0, this); } @@ -266,7 +267,7 @@ export class Pass implements PassIndexSignature { * @returns {Pass} */ - locations(...data: schema.Location[]): this { + locations(...data: schema.Location[]): PassWithLengthField { if (!data.length) { return assignLength(0, this); } From 8a6b473f846389ec194622735b5a3f5cadd180f1 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Mon, 17 Jun 2019 23:38:36 +0200 Subject: [PATCH 059/127] Replaced Archiver with yazl; Changed generate interface to return directly the stream; --- package-lock.json | 355 ++++------------------------------------------ package.json | 6 +- src/pass.ts | 26 ++-- 3 files changed, 42 insertions(+), 345 deletions(-) diff --git a/package-lock.json b/package-lock.json index 672e864..2ea2871 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,38 +4,12 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@types/archiver": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-3.0.0.tgz", - "integrity": "sha512-orghAMOF+//wSg4ru2znk6jt0eIPvKTtMVLH7XcYcjbcRyAXRClDlh27QVdqnAvVM37yu9xDP6Nh7egRhNr8tQ==", - "dev": true, - "requires": { - "@types/glob": "*" - } - }, "@types/debug": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.4.tgz", "integrity": "sha512-D9MyoQFI7iP5VdpEyPZyjjqIJ8Y8EDNQFIFVLOmeg1rI1xiHOChyUPMPRUVfqFCerxfE+yS3vMyj37F6IdtOoQ==", "dev": true }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", - "dev": true - }, - "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "dev": true, - "requires": { - "@types/events": "*", - "@types/minimatch": "*", - "@types/node": "*" - } - }, "@types/jasmine": { "version": "3.3.13", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.3.13.tgz", @@ -48,12 +22,6 @@ "integrity": "sha512-6gAT/UkIzYb7zZulAbcof3lFxpiD5EI6xBeTvkL1wYN12pnFQ+y/+xl9BvnVgxkmaIDN89xWhGZLD9CvuOtZ9g==", "dev": true }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, "@types/node": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.0.tgz", @@ -69,155 +37,41 @@ "@types/node": "*" } }, - "archiver": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.0.0.tgz", - "integrity": "sha512-5QeR6Xc5hSA9X1rbQfcuQ6VZuUXOaEdB65Dhmk9duuRJHYif/ZyJfuyJqsQrj34PFjU5emv5/MmfgA8un06onw==", + "@types/yazl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/yazl/-/yazl-2.4.1.tgz", + "integrity": "sha512-uTgQOl6gCKZ6ys5x2BmnNCd/Em8TqCltjPtyHFc1mz8Q6/+Na7yWnoPgCPhsl44M7S6MfaL6spL6pUM1c7NcDg==", + "dev": true, "requires": { - "archiver-utils": "^2.0.0", - "async": "^2.0.0", - "buffer-crc32": "^0.2.1", - "glob": "^7.0.0", - "readable-stream": "^2.0.0", - "tar-stream": "^1.5.0", - "zip-stream": "^2.0.1" - } - }, - "archiver-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.0.0.tgz", - "integrity": "sha512-JRBgcVvDX4Mwu2RBF8bBaHcQCSxab7afsxAPYDQ5W+19quIPP5CfKE7Ql+UHs9wYvwsaNR8oDuhtf5iqrKmzww==", - "requires": { - "glob": "^7.0.0", - "graceful-fs": "^4.1.0", - "lazystream": "^1.0.0", - "lodash.assign": "^4.2.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.toarray": "^4.4.0", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - } - }, - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "requires": { - "lodash": "^4.17.10" + "@types/node": "*" } }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" - }, - "bl": { - "version": "1.2.2", - "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", - "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", - "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" - }, "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" - }, - "compress-commons": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz", - "integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=", - "requires": { - "buffer-crc32": "^0.2.1", - "crc32-stream": "^2.0.0", - "normalize-path": "^2.0.0", - "readable-stream": "^2.0.0" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "requires": { - "buffer": "^5.1.0" - } - }, - "crc32-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz", - "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=", - "requires": { - "crc": "^3.4.4", - "readable-stream": "^2.0.0" - } + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "debug": { "version": "3.2.6", @@ -227,56 +81,22 @@ "ms": "^2.1.1" } }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" - } - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "hoek": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" }, - "ieee754": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", - "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==" - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -285,12 +105,8 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true }, "isemail": { "version": "3.2.0", @@ -342,58 +158,11 @@ "topo": "3.x.x" } }, - "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", - "requires": { - "readable-stream": "^2.0.5" - } - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" - }, - "lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" - }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" - }, - "lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" - }, - "lodash.toarray": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", - "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=" - }, - "lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" - }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -413,15 +182,11 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==" }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1" } @@ -429,69 +194,14 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, - "readable-stream": { - "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "tar-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", - "requires": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" - } - }, - "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" - }, "topo": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz", @@ -513,29 +223,18 @@ "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", "dev": true }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "zip-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.0.1.tgz", - "integrity": "sha512-c+eUhhkDpaK87G/py74wvWLtz2kzMPNCCkUApkun50ssE0oQliIQzWpTnwjB+MTKVIf2tGzIgHyqW/Y+W77ecQ==", + "yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", "requires": { - "archiver-utils": "^2.0.0", - "compress-commons": "^1.2.0", - "readable-stream": "^2.0.0" + "buffer-crc32": "~0.2.3" } } } diff --git a/package.json b/package.json index 0686314..1db65ed 100644 --- a/package.json +++ b/package.json @@ -19,22 +19,22 @@ "Pass" ], "dependencies": { - "archiver": "^3.0.0", "debug": "^3.2.6", "joi": "^13.7.0", "moment": "^2.24.0", - "node-forge": "^0.7.6" + "node-forge": "^0.7.6", + "yazl": "^2.5.1" }, "engines": { "node": ">=8.1.0" }, "devDependencies": { - "@types/archiver": "^3.0.0", "@types/debug": "^4.1.4", "@types/jasmine": "^3.3.13", "@types/joi": "^14.3.3", "@types/node": "^12.0.0", "@types/node-forge": "^0.8.3", + "@types/yazl": "^2.4.1", "jasmine": "^3.4.0", "typescript": "^3.4.5" } diff --git a/src/pass.ts b/src/pass.ts index aacf704..0e3e358 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -1,7 +1,7 @@ import path from "path"; import stream, { Stream } from "stream"; import forge from "node-forge"; -import archiver from "archiver"; +import { ZipFile } from "yazl"; import debug from "debug"; import * as schema from "./schema"; @@ -101,9 +101,8 @@ export class Pass implements PassIndexSignature { * @return {Promise} A Promise containing the stream of the generated pass. */ - async generate(): Promise { + generate(): Stream { // Editing Pass.json - this.bundle["pass.json"] = this._patch(this.bundle["pass.json"]); const finalBundle = { ...this.bundle } as schema.BundleUnit; @@ -149,16 +148,15 @@ export class Pass implements PassIndexSignature { }); /* - * Parsing the buffers, pushing them into the archive - * and returning the compiled manifest - */ - const archive = archiver("zip"); + * Parsing the buffers, pushing them into the archive + * and returning the compiled manifest + */ + const archive = new ZipFile(); const manifest = Object.keys(finalBundle).reduce((acc, current) => { let hashFlow = forge.md.sha1.create(); hashFlow.update(finalBundle[current].toString("binary")); - archive.append(finalBundle[current], { name: current }); - + archive.addBuffer(finalBundle[current], current); acc[current] = hashFlow.digest().toHex(); return acc; @@ -166,14 +164,14 @@ export class Pass implements PassIndexSignature { const signatureBuffer = this._sign(manifest); - archive.append(signatureBuffer, { name: "signature" }); - archive.append(JSON.stringify(manifest), { name: "manifest.json" }); - + archive.addBuffer(signatureBuffer, "signature"); + archive.addBuffer(Buffer.from(JSON.stringify(manifest)), "manifest.json"); const passStream = new stream.PassThrough(); - archive.pipe(passStream); + archive.outputStream.pipe(passStream); + archive.end(); - return archive.finalize().then(() => passStream); + return passStream; } /** From 0e13c6137ff7964d8283708d37d4f4a80ef6b98f Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Tue, 18 Jun 2019 00:01:33 +0200 Subject: [PATCH 060/127] Changed all examples to new generate signature; Added internal package.json for examples --- examples/README.md | 12 +- examples/barcode.ts | 111 ++++++----- examples/expiration.ts | 50 ++--- examples/fields.ts | 254 ++++++++++++------------- examples/localization.ts | 80 ++++---- examples/package-lock.json | 374 +++++++++++++++++++++++++++++++++++++ examples/package.json | 10 + 7 files changed, 637 insertions(+), 254 deletions(-) create mode 100644 examples/package-lock.json create mode 100644 examples/package.json diff --git a/examples/README.md b/examples/README.md index b8e1677..0c0fe7c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,14 +1,14 @@ # Examples -This is examples folder. Each example is linked to webserver.js, which *requires* express.js to run. -Express.js **was not** inserted as dipendency. +This is examples folder. These examples are used to test new features and as sample showcases. + +Each example is linked to webserver.js, which *requires* express.js to run. +Express.js has been inserted as "example package" dipendency. ```sh git clone https://github.com/alexandercerutti/passkit-generator.git; -cd passkit-generator; -npm install; -npm install --no-save express; -cd examples; +cd passkit-generator && npm install; +cd examples && npm install; node .js ``` diff --git a/examples/barcode.ts b/examples/barcode.ts index deaad66..056d6fb 100644 --- a/examples/barcode.ts +++ b/examples/barcode.ts @@ -13,76 +13,75 @@ import { createPass } from ".."; import { PassWithBarcodeMethods } from "../src/pass"; app.all(async function manageRequest(request, response) { - const passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, ""); - let pass = await createPass({ - model: `./models/${request.params.modelName}`, - certificates: { - wwdr: "../certificates/WWDR.pem", - signerCert: "../certificates/signerCert.pem", - signerKey: { - keyFile: "../certificates/signerKey.pem", - passphrase: "123456" - } - }, - overrides: request.body || request.params || request.query, - }); - - let bc: PassWithBarcodeMethods; - - if (request.query.alt === true) { - // After this, pass.props["barcodes"] will have support for all the formats - // while pass.props["barcode"] will be the first of barcodes. - - bc = pass.barcode("Thank you for using this package <3"); - } else { - // After this, pass.props["barcodes"] will have support for just two of three - // of the passed format (the valid ones) and pass.props["barcode"] the first of barcodes. - // if not specified, altText is automatically the message - - bc = pass.barcode({ - message: "Thank you for using this package <3", - format: "PKBarcodeFormatCode128" - }, { - message: "Thank you for using this package <3", - format: "PKBarcodeFormatPDF417" - }, { - message: "Thank you for using this package <3", - format: "PKBarcodeFormatMock44617" + try { + const pass = await createPass({ + model: `./models/${request.params.modelName}`, + certificates: { + wwdr: "../certificates/WWDR.pem", + signerCert: "../certificates/signerCert.pem", + signerKey: { + keyFile: "../certificates/signerKey.pem", + passphrase: "123456" + } + }, + overrides: request.body || request.params || request.query, }); - } - // You can change the format chosen for barcode prop support by calling .backward() - // or cancel the support by calling empty .backward - // like bc.backward(). - // If the property passed does not exists, things does not change. + let bc: PassWithBarcodeMethods; - bc.backward("PKBarcodeFormatPDF417"); + if (request.query.alt === true) { + // After this, pass.props["barcodes"] will have support for all the formats + // while pass.props["barcode"] will be the first of barcodes. - // If your barcode structures got not autogenerated yet (as happens with string - // parameter of barcode) you can call .autocomplete() to generate the support - // to all the structures. Please beware that this will overwrite ONLY barcodes and not barcode. + bc = pass.barcode("Thank you for using this package <3"); + } else { + // After this, pass.props["barcodes"] will have support for just two of three + // of the passed format (the valid ones) and pass.props["barcode"] the first of barcodes. + // if not specified, altText is automatically the message - if (!request.query.alt) { - // String generated barcode returns autocomplete as empty function - bc.autocomplete(); - } + bc = pass.barcode({ + message: "Thank you for using this package <3", + format: "PKBarcodeFormatCode128" + }, { + message: "Thank you for using this package <3", + format: "PKBarcodeFormatPDF417" + }, { + message: "Thank you for using this package <3", + format: "PKBarcodeFormatMock44617" + }); + } - // @ts-ignore - ignoring for logging purposes - console.log("Barcode property is now:", pass._props["barcode"]); - // @ts-ignore - ignoring for logging purposes - console.log("Barcodes support is autocompleted:", pass._props["barcodes"]); + // You can change the format chosen for barcode prop support by calling .backward() + // or cancel the support by calling empty .backward + // like bc.backward(). + // If the property passed does not exists, things does not change. - pass.generate().then(function (stream) { + bc.backward("PKBarcodeFormatPDF417"); + + // If your barcode structures got not autogenerated yet (as happens with string + // parameter of barcode) you can call .autocomplete() to generate the support + // to all the structures. Please beware that this will overwrite ONLY barcodes and not barcode. + + if (!request.query.alt) { + // String generated barcode returns autocomplete as empty function + bc.autocomplete(); + } + + // @ts-ignore - ignoring for logging purposes + console.log("Barcode property is now:", pass._props["barcode"]); + // @ts-ignore - ignoring for logging purposes + console.log("Barcodes support is autocompleted:", pass._props["barcodes"]); + + const stream = pass.generate(); response.set({ "Content-type": "application/vnd.apple.pkpass", "Content-disposition": `attachment; filename=${passName}.pkpass` }); stream.pipe(response); - }).catch(err => { - + } catch(err) { console.log(err); response.set({ @@ -90,5 +89,5 @@ app.all(async function manageRequest(request, response) { }); response.send(err.message); - }); + } }); diff --git a/examples/expiration.ts b/examples/expiration.ts index ce2bcdc..87a107d 100644 --- a/examples/expiration.ts +++ b/examples/expiration.ts @@ -19,39 +19,39 @@ app.all(async function manageRequest(request, response) { let passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, ""); - let pass = await createPass({ - model: `./models/${request.params.modelName}`, - certificates: { - wwdr: "../certificates/WWDR.pem", - signerCert: "../certificates/signerCert.pem", - signerKey: { - keyFile: "../certificates/signerKey.pem", - passphrase: "123456" - } - }, - overrides: request.body || request.params || request.query, - }); + try { + let pass = await createPass({ + model: `./models/${request.params.modelName}`, + certificates: { + wwdr: "../certificates/WWDR.pem", + signerCert: "../certificates/signerCert.pem", + signerKey: { + keyFile: "../certificates/signerKey.pem", + passphrase: "123456" + } + }, + overrides: request.body || request.params || request.query, + }); - if (request.query.fn === "void") { - pass.void(); - } else if (request.query.fn === "expiration") { - // 2 minutes later... - const d = new Date(); - d.setMinutes(d.getMinutes() + 2); + if (request.query.fn === "void") { + pass.void(); + } else if (request.query.fn === "expiration") { + // 2 minutes later... + const d = new Date(); + d.setMinutes(d.getMinutes() + 2); - // setting the expiration - pass.expiration(d); - } + // setting the expiration + pass.expiration(d); + } - pass.generate().then(function (stream) { + const stream = pass.generate(); response.set({ "Content-type": "application/vnd.apple.pkpass", "Content-disposition": `attachment; filename=${passName}.pkpass` }); stream.pipe(response); - }).catch(err => { - + } catch(err) { console.log(err); response.set({ @@ -59,5 +59,5 @@ app.all(async function manageRequest(request, response) { }); response.send(err.message); - }); + } }); diff --git a/examples/fields.ts b/examples/fields.ts index 3addfec..0b03699 100644 --- a/examples/fields.ts +++ b/examples/fields.ts @@ -14,145 +14,145 @@ import { createPass } from ".."; app.all(async function manageRequest(request, response) { let passName = "exampleBooking" + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, ""); + try { + let pass = await createPass({ + model: `./models/exampleBooking`, + certificates: { + wwdr: "../certificates/WWDR.pem", + signerCert: "../certificates/signerCert.pem", + signerKey: { + keyFile: "../certificates/signerKey.pem", + passphrase: "123456" + } + }, + overrides: request.body || request.params || request.query, + }); - let pass = await createPass({ - model: `./models/exampleBooking`, - certificates: { - wwdr: "../certificates/WWDR.pem", - signerCert: "../certificates/signerCert.pem", - signerKey: { - keyFile: "../certificates/signerKey.pem", - passphrase: "123456" - } - }, - overrides: request.body || request.params || request.query, - }); + pass.transitType = "PKTransitTypeAir"; - pass.transitType = "PKTransitTypeAir"; + pass.headerFields.push({ + "key": "header1", + "label": "Data", + "value": "25 mag", + "textAlignment": "PKTextAlignmentCenter" + }, { + "key": "header2", + "label": "Volo", + "value": "EZY997", + "textAlignment": "PKTextAlignmentCenter" + }); - pass.headerFields.push({ - "key": "header1", - "label": "Data", - "value": "25 mag", - "textAlignment": "PKTextAlignmentCenter" - }, { - "key": "header2", - "label": "Volo", - "value": "EZY997", - "textAlignment": "PKTextAlignmentCenter" - }); + pass.primaryFields.push({ + key: "IATA-source", + value: "NAP", + label: "Napoli", + textAlignment: "PKTextAlignmentLeft" + }, { + key: "IATA-destination", + value: "VCE", + label: "Venezia Marco Polo", + textAlignment: "PKTextAlignmentRight" + }); - pass.primaryFields.push({ - key: "IATA-source", - value: "NAP", - label: "Napoli", - textAlignment: "PKTextAlignmentLeft" - }, { - key: "IATA-destination", - value: "VCE", - label: "Venezia Marco Polo", - textAlignment: "PKTextAlignmentRight" - }); + pass.secondaryFields.push({ + "key": "secondary1", + "label": "Imbarco chiuso", + "value": "18:40", + "textAlignment": "PKTextAlignmentCenter", + }, { + "key": "sec2", + "label": "Partenze", + "value": "19:10", + "textAlignment": "PKTextAlignmentCenter" + }, { + "key": "sec3", + "label": "SB", + "value": "Sì", + "textAlignment": "PKTextAlignmentCenter" + }, { + "key": "sec4", + "label": "Imbarco", + "value": "Anteriore", + "textAlignment": "PKTextAlignmentCenter" + }); - pass.secondaryFields.push({ - "key": "secondary1", - "label": "Imbarco chiuso", - "value": "18:40", - "textAlignment": "PKTextAlignmentCenter", - }, { - "key": "sec2", - "label": "Partenze", - "value": "19:10", - "textAlignment": "PKTextAlignmentCenter" - }, { - "key": "sec3", - "label": "SB", - "value": "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.auxiliaryFields.push({ - "key": "aux1", - "label": "Passeggero", - "value": "MR. WHO KNOWS", - "textAlignment": "PKTextAlignmentLeft" - }, { - "key": "aux2", - "label": "Posto", - "value": "1A*", - "textAlignment": "PKTextAlignmentCenter" - }); + pass.backFields.push({ + "key": "document number", + "label": "Numero documento:", + "value": "- -", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "You're checked in, what next", + "label": "Hai effettuato il check-in, Quali sono le prospettive", + "value": "", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "Check In", + "label": "1. check-in✓", + "value": "", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "checkIn", + "label": "", + "value": "Le uscite d'imbarco chiudono 30 minuti prima della partenza, quindi sii puntuale. In questo aeroporto puoi utilizzare la corsia Fast Track ai varchi di sicurezza.", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "2. Bags", + "label": "2. Bagaglio", + "value": "", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "Require special assistance", + "label": "Assistenza speciale", + "value": "Se hai richiesto assistenza speciale, presentati a un membro del personale nell'area di Consegna bagagli almeno 90 minuti prima del volo.", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "3. Departures", + "label": "3. Partenze", + "value": "", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "photoId", + "label": "Un documento d’identità corredato di fotografia", + "value": "è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta d’identità.", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "yourSeat", + "label": "Il tuo posto:", + "value": "verifica il tuo numero di posto nella parte superiore. Durante l’imbarco utilizza le scale anteriori e posteriori: per le file 1-10 imbarcati dalla parte anteriore; per le file 11-31 imbarcati dalla parte posteriore. Colloca le borse di dimensioni ridotte sotto il sedile davanti a te.", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "Pack safely", + "label": "Bagaglio sicuro", + "value": "Fai clic http://easyjet.com/it/articoli-pericolosi per maggiori informazioni sulle merci pericolose oppure visita il sito CAA http://www.caa.co.uk/default.aspx?catid=2200", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "Thank you for travelling easyJet", + "label": "Grazie per aver viaggiato con easyJet", + "value": "", + "textAlignment": "PKTextAlignmentLeft" + }); - pass.backFields.push({ - "key": "document number", - "label": "Numero documento:", - "value": "- -", - "textAlignment": "PKTextAlignmentLeft" - }, { - "key": "You're checked in, what next", - "label": "Hai effettuato il check-in, Quali sono le prospettive", - "value": "", - "textAlignment": "PKTextAlignmentLeft" - }, { - "key": "Check In", - "label": "1. check-in✓", - "value": "", - "textAlignment": "PKTextAlignmentLeft" - }, { - "key": "checkIn", - "label": "", - "value": "Le uscite d'imbarco chiudono 30 minuti prima della partenza, quindi sii puntuale. In questo aeroporto puoi utilizzare la corsia Fast Track ai varchi di sicurezza.", - "textAlignment": "PKTextAlignmentLeft" - }, { - "key": "2. Bags", - "label": "2. Bagaglio", - "value": "", - "textAlignment": "PKTextAlignmentLeft" - }, { - "key": "Require special assistance", - "label": "Assistenza speciale", - "value": "Se hai richiesto assistenza speciale, presentati a un membro del personale nell'area di Consegna bagagli almeno 90 minuti prima del volo.", - "textAlignment": "PKTextAlignmentLeft" - }, { - "key": "3. Departures", - "label": "3. Partenze", - "value": "", - "textAlignment": "PKTextAlignmentLeft" - }, { - "key": "photoId", - "label": "Un documento d’identità corredato di fotografia", - "value": "è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta d’identità.", - "textAlignment": "PKTextAlignmentLeft" - }, { - "key": "yourSeat", - "label": "Il tuo posto:", - "value": "verifica il tuo numero di posto nella parte superiore. Durante l’imbarco utilizza le scale anteriori e posteriori: per le file 1-10 imbarcati dalla parte anteriore; per le file 11-31 imbarcati dalla parte posteriore. Colloca le borse di dimensioni ridotte sotto il sedile davanti a te.", - "textAlignment": "PKTextAlignmentLeft" - }, { - "key": "Pack safely", - "label": "Bagaglio sicuro", - "value": "Fai clic http://easyjet.com/it/articoli-pericolosi per maggiori informazioni sulle merci pericolose oppure visita il sito CAA http://www.caa.co.uk/default.aspx?catid=2200", - "textAlignment": "PKTextAlignmentLeft" - }, { - "key": "Thank you for travelling easyJet", - "label": "Grazie per aver viaggiato con easyJet", - "value": "", - "textAlignment": "PKTextAlignmentLeft" - }); - - pass.generate().then(function (stream) { + const stream = pass.generate(); response.set({ "Content-type": "application/vnd.apple.pkpass", "Content-disposition": `attachment; filename=${passName}.pkpass` }); stream.pipe(response); - }).catch(err => { + } catch(err) { console.log(err); response.set({ @@ -160,5 +160,5 @@ app.all(async function manageRequest(request, response) { }); response.send(err.message); - }); + } }); diff --git a/examples/localization.ts b/examples/localization.ts index 6087f48..4ea4c9b 100644 --- a/examples/localization.ts +++ b/examples/localization.ts @@ -10,57 +10,57 @@ import { createPass } from ".."; app.all(async function manageRequest(request, response) { const passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, ""); - const pass = await createPass({ - model: `./models/${request.params.modelName}`, - certificates: { - wwdr: "../certificates/WWDR.pem", - signerCert: "../certificates/signerCert.pem", - signerKey: { - keyFile: "../certificates/signerKey.pem", - passphrase: "123456" - } - }, - overrides: request.body || request.params || request.query - }); + try { + const pass = await createPass({ + model: `./models/${request.params.modelName}`, + certificates: { + wwdr: "../certificates/WWDR.pem", + signerCert: "../certificates/signerCert.pem", + signerKey: { + keyFile: "../certificates/signerKey.pem", + passphrase: "123456" + } + }, + overrides: request.body || request.params || request.query + }); - // For each language you include, an .lproj folder in pass bundle - // is created or included. You may not want to add translations but - // only images for a specific language. So you create manually - // an .lproj folder in your pass model then add the language here below. - // If no translations were added, the folder - // is included or created but without pass.strings file + // For each language you include, an .lproj folder in pass bundle + // is created or included. You may not want to add translations but + // only images for a specific language. So you create manually + // an .lproj folder in your pass model then add the language here below. + // If no translations were added, the folder + // is included or created but without pass.strings file - // English, does not has an .lproj folder and no translation - // Text placeholders may not be showed for the english language - // (e.g. "Event" and "Location" as literal) and another language may be used instead - pass.localize("en"); + // English, does not has an .lproj folder and no translation + // Text placeholders may not be showed for the english language + // (e.g. "Event" and "Location" as literal) and another language may be used instead + pass.localize("en"); - // Italian, already has an .lproj which gets included - pass.localize("it", { - "EVENT": "Evento", - "LOCATION": "Dove" - }); + // Italian, already has an .lproj which gets included + pass.localize("it", { + "EVENT": "Evento", + "LOCATION": "Dove" + }); - // German, doesn't, so is created - pass.localize("de", { - "EVENT": "Ereignis", - "LOCATION": "Ort" - }); + // German, doesn't, so is created + pass.localize("de", { + "EVENT": "Ereignis", + "LOCATION": "Ort" + }); - // This language does not exist but is still added as .lproj folder - pass.localize("zu", {}); - // @ts-ignore - ignoring for logging purposes. Do not replicate - console.log("Added languages", Object.keys(pass.l10nBundles).join(", ")) + // This language does not exist but is still added as .lproj folder + pass.localize("zu", {}); + // @ts-ignore - ignoring for logging purposes. Do not replicate + console.log("Added languages", Object.keys(pass.l10nBundles).join(", ")) - pass.generate().then(function (stream) { + const stream = pass.generate(); response.set({ "Content-type": "application/vnd.apple.pkpass", "Content-disposition": `attachment; filename=${passName}.pkpass` }); stream.pipe(response); - }).catch(err => { - + } catch(err) { console.log(err); response.set({ @@ -68,5 +68,5 @@ app.all(async function manageRequest(request, response) { }); response.send(err.message); - }); + } }); diff --git a/examples/package-lock.json b/examples/package-lock.json new file mode 100644 index 0000000..037320f --- /dev/null +++ b/examples/package-lock.json @@ -0,0 +1,374 @@ +{ + "name": "examples", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + } + } +} diff --git a/examples/package.json b/examples/package.json new file mode 100644 index 0000000..796b1c7 --- /dev/null +++ b/examples/package.json @@ -0,0 +1,10 @@ +{ + "name": "examples", + "version": "0.0.0", + "description": "Passkit-generator examples", + "author": "Alexander P. Cerutti ", + "license": "ISC", + "dependencies": { + "express": "^4.17.1" + } +} From 0e2f61cfef6a096dea2953a21f715323fa54058f Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Tue, 18 Jun 2019 00:02:06 +0200 Subject: [PATCH 061/127] Changed tests to reflect new relevancy methods --- spec/index.ts | 102 +++++++++++++++++++++++++++++--------------------- 1 file changed, 60 insertions(+), 42 deletions(-) diff --git a/spec/index.ts b/spec/index.ts index dc9b18d..7246f10 100644 --- a/spec/index.ts +++ b/spec/index.ts @@ -97,62 +97,76 @@ describe("Node-Passkit-generator", function () { }); }); - describe("relevance()", () => { - describe("relevance('relevantDate')", () => { + describe("Relevancy:", () => { + describe("Relevant Date", () => { it("A date object will apply changes", () => { - pass.relevance("relevantDate", new Date("10-04-2021")); + pass.relevantDate(new Date("10-04-2021")); // this is made to avoid problems with winter and summer time: // we focus only on the date and time for the tests. // @ts-ignore -- Ignoring for test purposes let noTimeZoneDateTime = pass._props["relevantDate"].split("+")[0]; - expect(noTimeZoneDateTime).toBe("2021-04-10T00:00:00"); + expect(noTimeZoneDateTime).toBe("2021-10-04T00:00:00"); }); }); - describe("relevance('maxDistance')", () => { - it("A string is accepted and converted to Number", () => { - pass.relevance("maxDistance", "150"); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["maxDistance"]).toBe(150); - }); - - it("A number is accepeted and will apply changes", () => { - pass.relevance("maxDistance", 150); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["maxDistance"]).toBe(150); - }); - - it("Passing NaN value won't apply changes", () => { - pass.relevance("maxDistance", NaN); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["maxDistance"]).toBe(undefined); - }); - }); - - describe("relevance('locations') && relevance('beacons')", () => { - it("A one-Invalid-schema location won't apply changes", () => { - pass.relevance("locations", [{ + describe("locations :: ", () => { + it("One-Invalid-schema location won't apply changes", () => { + pass.locations({ + // @ts-ignore "ibrupofene": "no", "longitude": 0.00000000 - }]); + }); // @ts-ignore -- Ignoring for test purposes expect(pass._props["locations"]).toBe(undefined); }); - it("A two locations, with one invalid, will be filtered", () => { - pass.relevance("locations", [{ + it("Two locations, with one invalid, will be filtered", () => { + pass.locations({ + //@ts-ignore "ibrupofene": "no", "longitude": 0.00000000 }, { "longitude": 4.42634523, "latitude": 5.344233323352 - }]); + }); // @ts-ignore -- Ignoring for test purposes expect(pass._props["locations"].length).toBe(1); }); }); + + describe("Beacons :: ", () => { + it("One-Invalid-schema beacon data won't apply changes", () => { + pass.beacons({ + // @ts-ignore + "ibrupofene": "no", + "major": 55, + "minor": 0, + "proximityUUID": "2707c5f4-deb9-48ff-b760-671bc885b6a7" + }); + + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["beacons"]).toBe(undefined); + }); + + it("Two beacons sets, with one invalid, will be filtered", () => { + pass.beacons({ + "major": 55, + "minor": 0, + "proximityUUID": "59da0f96-3fb5-43aa-9028-2bc796c3d0c5" + }, { + "major": 55, + "minor": 0, + "proximityUUID": "fdcbbf48-a4ae-4ffb-9200-f8a373c5c18e", + // @ts-ignore + "animal": "Monkey" + }); + + // @ts-ignore -- Ignoring for test purposes + expect(pass._props["beacons"].length).toBe(1); + }); + }); }); describe("barcode()", () => { @@ -167,6 +181,7 @@ describe("Node-Passkit-generator", function () { }); it("Boolean parameter won't apply changes", () => { + // @ts-ignore -- Ignoring for test purposes pass.barcode(true); // @ts-ignore -- Ignoring for test purposes @@ -176,6 +191,7 @@ describe("Node-Passkit-generator", function () { }); it("Numeric parameter won't apply changes", () => { + // @ts-ignore -- Ignoring for test purposes pass.barcode(42); // @ts-ignore -- Ignoring for test purposes @@ -226,10 +242,10 @@ describe("Node-Passkit-generator", function () { }); it("Missing messageEncoding gets automatically added.", () => { - pass.barcode([{ + pass.barcode({ message: "28363516282", format: "PKBarcodeFormatPDF417", - }]); + }); // @ts-ignore -- Ignoring for test purposes expect(pass._props["barcode"] instanceof Object).toBe(true); @@ -240,9 +256,10 @@ describe("Node-Passkit-generator", function () { }); it("Object without message property, will be filtered out", () => { - pass.barcode([{ + // @ts-ignore -- Ignoring for test purposes + pass.barcode({ format: "PKBarcodeFormatPDF417", - }]); + }); // @ts-ignore -- Ignoring for test purposes expect(pass._props["barcode"]).toBe(undefined); @@ -250,18 +267,17 @@ describe("Node-Passkit-generator", function () { expect(pass._props["barcodes"]).toBe(undefined); }); - it("Array containing non-object elements will be filtered out", () => { - pass.barcode([5, 10, 15, { + it("Array containing non-object elements will be rejected", () => { + // @ts-ignore -- Ignoring for test purposes + pass.barcode(5, 10, 15, { message: "28363516282", format: "PKBarcodeFormatPDF417" - }, 7, 1]); + }, 7, 1); // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"] instanceof Object).toBe(true); + expect(pass._props["barcode"] instanceof Object).toBe(false); // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcodes"].length).toBe(1); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcodes"][0] instanceof Object).toBe(true); + expect(pass._props["barcodes"]).toBeUndefined(); }); }); @@ -269,6 +285,7 @@ describe("Node-Passkit-generator", function () { it("Passing argument of type different from string or null, won't apply changes", function () { pass .barcode("Message-22645272183") + // @ts-ignore -- Ignoring for test purposes .backward(5); // unchanged @@ -288,6 +305,7 @@ describe("Node-Passkit-generator", function () { it("Unknown format won't apply changes", () => { pass .barcode("Message-22645272183") + // @ts-ignore -- Ignoring for test purposes .backward("PKBingoBongoFormat"); // @ts-ignore -- Ignoring for test purposes From 96c9dfa9125645d5343f706d7ad4b27192649b7d Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Tue, 18 Jun 2019 00:02:30 +0200 Subject: [PATCH 062/127] Changed BeaconDict.minor Joi signature --- src/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/schema.ts b/src/schema.ts index f871f18..db22a2b 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -367,7 +367,7 @@ export interface Beacon { const beaconsDict = Joi.object().keys({ major: Joi.number().integer().positive().max(65535).greater(Joi.ref("minor")), - minor: Joi.number().integer().positive().max(65535).less(Joi.ref("major")), + minor: Joi.number().integer().min(0).max(65535).less(Joi.ref("major")), proximityUUID: Joi.string().required(), relevantText: Joi.string() }); From 816f315fca3b4a491fc0e0168b6c15f017fad01d Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Tue, 18 Jun 2019 00:03:22 +0200 Subject: [PATCH 063/127] Small improvements --- src/factory.ts | 1 + src/pass.ts | 26 ++++++++++++-------------- src/schema.ts | 2 +- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 4463937..8d4a261 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -103,6 +103,7 @@ async function getModelFolderContents(model: string): Promise ) ]).then(buffers => // Assigning each file path to its buffer + // and discarding the empty ones validFiles.reduce((acc, file, index) => { if (!buffers[index].length) { return acc; diff --git a/src/pass.ts b/src/pass.ts index 0e3e358..538716e 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -15,7 +15,6 @@ import { const barcodeDebug = debug("passkit:barcode"); const genericDebug = debug("passkit:generic"); - const noop = () => {}; const transitType = Symbol("transitType"); const barcodesFillMissing = Symbol("bfm"); @@ -35,7 +34,6 @@ export interface PassWithBarcodeMethods extends PassWithLengthField { } export class Pass implements PassIndexSignature { - // private model: string; private bundle: schema.BundleUnit; private l10nBundles: schema.PartitionedBundle["l10nBundle"]; private _fields: (keyof schema.PassFields)[]; @@ -214,7 +212,7 @@ export class Pass implements PassIndexSignature { return this; } - this._props.expirationDate = dateParse; + this._props["expirationDate"] = dateParse; return this; } @@ -227,7 +225,7 @@ export class Pass implements PassIndexSignature { */ void(): this { - this._props.voided = true; + this._props["voided"] = true; return this; } @@ -295,16 +293,16 @@ export class Pass implements PassIndexSignature { relevantDate(date: Date): this { if (!(date instanceof Date)) { - genericDebug(formatMessage("DATE_FORMAT_UNMATCH", "Relevant Date")); - return this; - } + genericDebug(formatMessage("DATE_FORMAT_UNMATCH", "Relevant Date")); + return this; + } const parsedDate = dateToW3CString(date); if (!parsedDate) { // @TODO: create message "Unable to format date" return this; - } + } this._props["relevantDate"] = parsedDate; return this; @@ -405,7 +403,7 @@ export class Pass implements PassIndexSignature { * @returns {this} Improved this, with length property and retroCompatibility method. */ - [barcodesFillMissing](): this { + private [barcodesFillMissing](): this { const props = this._props["barcodes"]; if (props.length === 4 || !props.length) { @@ -433,7 +431,7 @@ export class Pass implements PassIndexSignature { * @return {this} */ - [barcodesSetBackward](format: schema.BarcodeFormat | null): this { + private [barcodesSetBackward](format: schema.BarcodeFormat | null): this { if (format === null) { this._props["barcode"] = undefined; return this; @@ -488,7 +486,7 @@ export class Pass implements PassIndexSignature { * @returns {Buffer} */ - _sign(manifest: { [key: string]: string }): Buffer { + private _sign(manifest: { [key: string]: string }): Buffer { let signature = forge.pkcs7.createSignedData(); signature.content = forge.util.createBuffer(JSON.stringify(manifest), "utf8"); @@ -551,7 +549,7 @@ export class Pass implements PassIndexSignature { * @returns {Promise} Edited pass.json buffer or Object containing error. */ - _patch(passCoreBuffer: Buffer): Buffer { + private _patch(passCoreBuffer: Buffer): Buffer { const passFile = JSON.parse(passCoreBuffer.toString()); if (Object.keys(this._props).length) { @@ -564,9 +562,9 @@ export class Pass implements PassIndexSignature { Object.keys(this._props).forEach(prop => { if (passFile[prop] && passFile[prop] instanceof Array) { - passFile[prop].push(...this._props[prop]); + passFile[prop] = [ ...passFile[prop], ...this._props[prop] ]; } else if (passFile[prop] && passFile[prop] instanceof Object) { - Object.assign(passFile[prop], this._props[prop]); + passFile[prop] = { ...passFile[prop], ...this._props[prop] }; } else { passFile[prop] = this._props[prop]; } diff --git a/src/schema.ts b/src/schema.ts index db22a2b..30ca83f 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -276,7 +276,7 @@ const semantics = Joi.object().keys({ balance: currencyAmount }); -interface ValidPassType { +export interface ValidPassType { boardingPass?: PassFields & { transitType: TransitType }; eventTicket?: PassFields; coupon?: PassFields; From c733a4ea58208d8c71d9e833c649691e581f99f0 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Tue, 18 Jun 2019 22:33:09 +0200 Subject: [PATCH 064/127] Added optimization for creation of FieldsArray --- src/pass.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 538716e..2271a23 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -75,19 +75,13 @@ export class Pass implements PassIndexSignature { this[transitType] = this.passCore[this.type]["transitType"]; } - const typeFields = Object.keys(this.passCore[this.type]) as (keyof schema.PassFields)[]; - this._fields = ["primaryFields", "secondaryFields", "auxiliaryFields", "backFields", "headerFields"]; this._fields.forEach(fieldName => { - if (typeFields.includes(fieldName)) { - this[fieldName] = new FieldsArray( - this.fieldsKeys, - ...this.passCore[this.type][fieldName] - .filter(field => schema.isValid(field, "field")) - ); - } else { - this[fieldName] = new FieldsArray(this.fieldsKeys); - } + this[fieldName] = new FieldsArray( + this.fieldsKeys, + ...(this.passCore[this.type][fieldName] || []) + .filter(field => schema.isValid(field, "field")) + ); }); } From 894266de2857ace1e501586b347b824680bb9d3e Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 20 Jun 2019 00:30:12 +0200 Subject: [PATCH 065/127] Improved security checks; Added back the formatted messages and added new ones; --- src/factory.ts | 183 +++++++++++++++++++++++++++++------------------- src/messages.ts | 8 ++- src/pass.ts | 28 ++++++-- 3 files changed, 141 insertions(+), 78 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 8d4a261..881f3b4 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -13,14 +13,14 @@ const readFile = promisify(_readFile); export async function createPass(options: FactoryOptions): Promise { if (!(options && Object.keys(options).length)) { - throw new Error("Unable to create Pass: no options were passed"); + throw new Error(formatMessage("CP_NO_OPTS")); } try { const [bundle, certificates] = await Promise.all([ getModelContents(options.model), - readCertificatesFromOptions(options.certificates) - ]); + readCertificatesFromOptions(options.certificates) + ]); return new Pass({ model: bundle, @@ -28,13 +28,23 @@ export async function createPass(options: FactoryOptions): Promise { overrides: options.overrides }); } catch (err) { - // @TODO: analyze the error and stop the execution somehow + console.log(err); + throw new Error(formatMessage("CP_INIT_ERROR")); } } async function getModelContents(model: FactoryOptions["model"]) { - if (!(model && (typeof model === "string" || (typeof model === "object" && Object.keys(model).length)))) { - throw new Error("Unable to create Pass: invalid model provided"); + const isModelValid = ( + model && ( + typeof model === "string" || ( + typeof model === "object" && + Object.keys(model).length + ) + ) + ); + + if (!isModelValid) { + throw new Error(formatMessage("MODEL_NOT_VALID")); } let modelContents: PartitionedBundle; @@ -61,71 +71,96 @@ async function getModelContents(model: FactoryOptions["model"]) { */ async function getModelFolderContents(model: string): Promise { - const modelPath = path.resolve(model) + (!!model && !path.extname(model) ? ".pass" : ""); - const modelFilesList = await readDir(modelPath); + try { + const modelPath = path.resolve(model) + (!!model && !path.extname(model) ? ".pass" : ""); + const modelFilesList = await readDir(modelPath); - // No dot-starting files, manifest and signature - const filteredFiles = removeHidden(modelFilesList).filter(f => !/(manifest|signature)/i.test(f)); + // No dot-starting files, manifest and signature + const filteredFiles = removeHidden(modelFilesList).filter(f => !/(manifest|signature)/i.test(f)); - // Icon is required to proceed - if (!(filteredFiles.length && filteredFiles.some(file => file.toLowerCase().includes("icon")))) { - const eMessage = formatMessage("MODEL_UNINITIALIZED", path.parse(this.model).name); - throw new Error(eMessage); + const isModelInitialized = ( + filteredFiles.length && + filteredFiles.some(file => file.toLowerCase().includes("icon")) + ); + + // Icon is required to proceed + if (!isModelInitialized) { + throw new Error(formatMessage( + "MODEL_UNINITIALIZED", + path.parse(this.model).name + )); + } + + // Splitting files from localization folders + const rawBundle = filteredFiles.filter(entry => !entry.includes(".lproj")); + const l10nFolders = filteredFiles.filter(entry => entry.includes(".lproj")); + + const bundleBuffers = rawBundle.map(file => readFile(path.resolve(modelPath, file))); + const buffers = await Promise.all(bundleBuffers); + + const bundle: BundleUnit = Object.assign({}, + ...rawBundle.map((fileName, index) => ({ [fileName]: buffers[index] })) + ); + + // Reading concurrently localizations folder + // and their files and their buffers + const L10N_FilesListByFolder: Array = await Promise.all( + l10nFolders.map(folderPath => { + // Reading current folder + const currentLangPath = path.join(modelPath, folderPath); + return readDir(currentLangPath) + .then(files => { + // Transforming files path to a model-relative path + const validFiles = removeHidden(files) + .map(file => path.join(currentLangPath, file)); + + // Getting all the buffers from file paths + return Promise.all([ + ...validFiles.map(file => + readFile(file).catch(() => Buffer.alloc(0)) + ) + ]).then(buffers => + // Assigning each file path to its buffer + // and discarding the empty ones + validFiles.reduce((acc, file, index) => { + if (!buffers[index].length) { + return acc; + } + + return { ...acc, [file]: buffers[index] }; + }, {}) + ); + }); + }) + ); + + const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign( + {}, + ...L10N_FilesListByFolder + .map((folder, index) => ({ [l10nFolders[index]]: folder })) + ); + + return { + bundle, + l10nBundle + }; + } catch (err) { + if (err.code && err.code === "ENOENT") { + if (err.syscall === "open") { + // file opening failed + throw new Error(formatMessage("MODELF_NOT_FOUND", err.path)) + } else if (err.syscall === "scandir") { + // directory reading failed + const pathContents = (err.path as string).split(/(\/|\\\?)/); + throw new Error(formatMessage( + "MODELF_FILE_NOT_FOUND", + pathContents[pathContents.length-1] + )) + } + } + + throw err; } - - // Splitting files from localization folders - const rawBundle = filteredFiles.filter(entry => !entry.includes(".lproj")); - const l10nFolders = filteredFiles.filter(entry => entry.includes(".lproj")); - - const bundleBuffers = rawBundle.map(file => readFile(path.resolve(modelPath, file))); - const buffers = await Promise.all(bundleBuffers); - - const bundle: BundleUnit = Object.assign({}, - ...rawBundle.map((fileName, index) => ({ [fileName]: buffers[index] })) - ); - - // Reading concurrently localizations folder - // and their files and their buffers - const L10N_FilesListByFolder: Array = await Promise.all( - l10nFolders.map(folderPath => { - // Reading current folder - const currentLangPath = path.join(modelPath, folderPath); - return readDir(currentLangPath) - .then(files => { - // Transforming files path to a model-relative path - const validFiles = removeHidden(files) - .map(file => path.join(currentLangPath, file)); - - // Getting all the buffers from file paths - return Promise.all([ - ...validFiles.map(file => - readFile(file).catch(() => Buffer.alloc(0)) - ) - ]).then(buffers => - // Assigning each file path to its buffer - // and discarding the empty ones - validFiles.reduce((acc, file, index) => { - if (!buffers[index].length) { - return acc; - } - - return { ...acc, [file]: buffers[index] }; - }, {}) - ); - }); - }) - ); - - const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign( - {}, - ...L10N_FilesListByFolder - .map((folder, index) => ({ [l10nFolders[index]]: folder })) - ); - - return { - bundle, - l10nBundle - }; } /** @@ -147,8 +182,14 @@ function getModelBufferContents(model: BundleUnit): PartitionedBundle { const bundleKeys = Object.keys(rawBundle); - if (!bundleKeys.length) { - throw new Error("Cannot proceed with pass creation: bundle not initialized") + const isModelInitialized = ( + bundleKeys.length && + bundleKeys.some(file => file.toLowerCase().includes("icon")) + ); + + // Icon is required to proceed + if (!isModelInitialized) { + throw new Error(formatMessage("MODEL_UNINITIALIZED", "Buffers")) } // separing localization folders @@ -179,7 +220,7 @@ function getModelBufferContents(model: BundleUnit): PartitionedBundle { async function readCertificatesFromOptions(options: Certificates): Promise { if (!(options && Object.keys(options).length && isValid(options, "certificatesSchema"))) { - throw new Error("Unable to create Pass: certificates schema validation failed."); + throw new Error(formatMessage("CP_NO_CERTS")); } // if the signerKey is an object, we want to get diff --git a/src/messages.ts b/src/messages.ts index 05e67ce..3ef035f 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -3,11 +3,15 @@ interface MessageGroup { } const errors: MessageGroup = { + CP_INIT_ERROR: "Something went really bad in the initialization, dude! Please look at the log above this message. It should contain all the infos about the problem.", + CP_NO_OPTS: "Cannot initialize the pass creation: no options were passed.", + CP_NO_CERTS: "Cannot initialize the pass creation: no valid certificates were passed.", PASSFILE_VALIDATION_FAILED: "Validation of pass type failed. Pass file is not a valid buffer or (more probably) does not respect the schema.\nRefer to https://apple.co/2Nvshvn to build a correct pass.", REQUIR_VALID_FAILED: "The options passed to Pass constructor does not meet the requirements.\nRefer to the documentation to compile them correctly.", MODEL_UNINITIALIZED: "Provided model ( %s ) matched but unitialized or may not contain icon.\nRefer to https://apple.co/2IhJr0Q, https://apple.co/2Nvshvn and documentation to fill the model correctly.", - MODEL_NOT_STRING: "A string model name must be provided in order to continue.", - MODEL_NOT_FOUND: "Model %s not found. Provide a valid one to continue.", + MODEL_NOT_VALID: "A model must be provided in form of path (string) or object { 'fileName': Buffer } in order to continue.", + MODELF_NOT_FOUND: "Model %s not found. Provide a valid one to continue.", + MODELF_FILE_NOT_FOUND: "File %s not found.", INVALID_CERTS: "Invalid certificate(s) loaded: %s. Please provide valid WWDR certificates and developer signer certificate and key (with passphrase).\nRefer to docs to obtain them.", INVALID_CERT_PATH: "Invalid certificate loaded. %s does not exist.", TRSTYPE_REQUIRED: "Cannot proceed with pass creation. transitType field is required for boardingPasses.", diff --git a/src/pass.ts b/src/pass.ts index 2271a23..34294e4 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -53,20 +53,38 @@ export class Pass implements PassIndexSignature { [transitType]: string = ""; constructor(options: schema.PassInstance) { + if (!schema.isValid(options, "instance")) { + throw new Error(formatMessage("REQUIR_VALID_FAILED")); + } + this.Certificates = options.certificates; this.l10nBundles = options.model.l10nBundle; this.bundle = { ...options.model.bundle }; - options.overrides = options.overrides || {}; + // Parsing the options and extracting only the valid ones. + const validOvverrides = schema.getValidated(options.overrides || {}, "supportedOptions") as schema.OverridesSupportedOptions; - // getting pass.json - this.passCore = JSON.parse(this.bundle["pass.json"].toString("utf8")); + if (validOvverrides === null) { + throw new Error(formatMessage("OVV_KEYS_BADFORMAT")) + } + + if (Object.keys(validOvverrides).length) { + this._props = { ...validOvverrides }; + } + + try { + // getting pass.json + this.passCore = JSON.parse(this.bundle["pass.json"].toString("utf8")); + } catch (err) { + throw new Error(formatMessage("PASSFILE_VALIDATION_FAILED")); + } this.type = Object.keys(this.passCore) .find(key => /(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key)) as keyof schema.ValidPassType; if (!this.type) { - throw new Error("Missing type in model"); + // @TODO: change error message to say it is invalid or missing + throw new Error(formatMessage("NO_PASS_TYPE")); } if (this.type === "boardingPass" && this.passCore[this.type]["transitType"]) { @@ -432,7 +450,7 @@ export class Pass implements PassIndexSignature { } if (typeof format !== "string") { - barcodeDebug(formatMessage("BRC_FORMAT_UNMATCH")); + barcodeDebug(formatMessage("BRC_FORMATTYPE_UNMATCH")); return this; } From f2383fee99a53ea93ebbd3966e7172f63d7ef30a Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 20 Jun 2019 00:30:23 +0200 Subject: [PATCH 066/127] Improved schema --- src/schema.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/schema.ts b/src/schema.ts index 30ca83f..009ae07 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -46,25 +46,24 @@ export interface PassInstance { // ************************************ // const certificatesSchema = Joi.object().keys({ - wwdr: Joi.string().required(), - signerCert: Joi.string().required(), + wwdr: Joi.alternatives(Joi.binary(), Joi.string()).required(), + signerCert: Joi.alternatives(Joi.binary(), Joi.string()).required(), signerKey: Joi.alternatives().try( Joi.object().keys({ - keyFile: Joi.string().required(), + keyFile: Joi.alternatives(Joi.binary(), Joi.string()).required(), passphrase: Joi.string().required(), }), - Joi.string() + Joi.alternatives(Joi.binary(), Joi.string()) ).required() }).required(); const instance = Joi.object().keys({ - model: Joi.string().required(), - certificates: certificatesSchema, + model: Joi.alternatives(Joi.object(), Joi.string()).required(), + certificates: Joi.object(), overrides: Joi.object(), - shouldOverwrite: Joi.boolean() }); -interface OverridesSupportedOptions { +export interface OverridesSupportedOptions { serialNumber?: string; description?: string; userInfo?: Object | Array; From 18bacc8f47c225356ae158ecc5d4d0e6770298bb Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 23 Jun 2019 15:14:01 +0200 Subject: [PATCH 067/127] Updated declarations --- index.d.ts | 154 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 100 insertions(+), 54 deletions(-) diff --git a/index.d.ts b/index.d.ts index 1942c9e..689b46d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,7 +1,9 @@ import { Stream } from "stream"; -export declare class Pass { - constructor(options: Schema.Instance); +export function createPass(options: Schema.FactoryOptions): Promise; + +declare class Pass { + constructor(options: Schema.PassInstance); public transitType: "PKTransitTypeAir" | "PKTransitTypeBoat" | "PKTransitTypeBus" | "PKTransitTypeGeneric" | "PKTransitTypeTrain"; public headerFields: Schema.Field[]; @@ -11,72 +13,90 @@ export declare class Pass { public backFields: Schema.Field[]; /** - * Generates a Stream of a zip file using the infos passed through overrides or methods. - * (MIME: `application/vnd.apple.pkpass`) + * Generates the pass Stream + * + * @method generate + * @return A Stream of the generated pass. */ - generate(): Promise; + generate(): Stream; /** - * Generates pass.strings translation files in the specified language - * @param lang - lang in ISO 3166 alpha-2 format (e.g. `en` or `en-US`); - * @param translations - Object in format `{ "placeholder" : "translated-text" }` - * @see https://apple.co/2KOv0OW + * Adds traslated strings object to the list of translation to be inserted into the pass + * + * @method localize + * @params lang - the ISO 3166 alpha-2 code for the language + * @params translations - key/value pairs where key is the + * placeholder in pass.json localizable strings + * and value the real translated string. + * @returns {this} + * + * @see https://apple.co/2KOv0OW - Passes support localization */ localize(lang: string, translations: Object): this; /** - * Sets pass expiration date - * @param date - A date in the format you want (see "format") - * @param format - A custom date format. If `undefined`, the date will be parsed in the following formats: `MM-DD-YYYY`, `MM-DD-YYYY hh:mm:ss`, `DD-MM-YYYY`, `DD-MM-YYYY hh:mm:ss`. - */ - expiration(date: string, format?: string | string[]): this; + * Sets expirationDate property to a W3C-formatted date + * + * @method expiration + * @params date + * @returns {this} + */ + expiration(date: Date): this; - /** Generates a voided pass. Useful for backend pass updates. */ + /** + * Sets voided property to true + * + * @method void + * @return {this} + */ void(): this; /** - * Sets relevance for pass (conditions to appear in the lockscren). - * @param type - must be `beacons`, `locations`, `maxDistance` or `relevantDate` - * @param data - if object, will be treated as one-element array - * @param relevanceDateFormat - custom format to be used in case of "relevatDate" as type. Otherwise the date will be parsed in the following formats: `MM-DD-YYYY`, `MM-DD-YYYY hh:mm:ss`, `DD-MM-YYYY`, `DD-MM-YYYY hh:mm:ss`. + * Sets current pass' relevancy through beacons + * @param data + * @returns Pass instance with `length` property to check the + * valid structures added */ - relevance(type: Schema.RelevanceType, data: string | Schema.Location | Schema.Location[] | Schema.Beacon | Schema.Beacon[], relevanceDateFormat?: string): SuccessfulOperations; + beacons(...data: Schema.Beacon[]): PassWithLengthField; + + /** + * Sets current pass' relevancy through locations + * @param data + * @returns Pass instance with `length` property to check the + * valid structures added + */ + locations(...data: Schema.Location[]): PassWithLengthField; + + /** + * Sets current pass' relevancy through a date + * @param data + * @returns {Pass} + */ + relevantDate(date: Date): this; /** * Adds barcode to the pass. If data is an Object, will be treated as one-element array. - * @param data - data to be used to generate a barcode. If string, Barcode will contain structures for all the supported types and `data` will be used message and altText. + * @param first - data to be used to generate a barcode. If string, Barcode will contain structures for all the supported types. + * @param data - the other Barcode structures to be used * @see https://apple.co/2C74kbm */ - barcode(data: Schema.Barcode | Schema.Barcode[] | string): BarcodeInterfaces; + barcode(first: string | Schema.Barcode, ...data: Schema.Barcode[]): PassWithBarcodeMethods; /** * Sets nfc infos for the pass * @param data - NFC data * @see https://apple.co/2wTxiaC */ - nfc(...data: Schema.NFC[]): this; - - /** - * Sets resources to be downloaded right inside - * the pass archive. - * @param resource - url - * @param name - name (or path) to be used inside the archive - * @returns this; - */ - - load(resource: string, name: string): this; + nfc(data: Schema.NFC): this; } -interface BarcodeInterfaces extends BarcodeSuccessfulOperations { - autocomplete: () => void | BarcodeSuccessfulOperations +declare interface PassWithLengthField extends Pass { + length: number; } -interface BarcodeSuccessfulOperations extends SuccessfulOperations { - backward: (format: null | string) => void | ThisType -} - -interface SuccessfulOperations extends ThisType { - length: number +declare interface PassWithBarcodeMethods extends PassWithLengthField { + backward: (format: Schema.BarcodeFormat | null) => Pass; + autocomplete: () => Pass; } declare namespace Schema { @@ -88,31 +108,57 @@ declare namespace Schema { type RelevanceType = "beacons" | "locations" | "maxDistance" | "relevantDate"; type SemanticsEventType = "PKEventTypeGeneric" | "PKEventTypeLivePerformance" | "PKEventTypeMovie" | "PKEventTypeSports" | "PKEventTypeConference" | "PKEventTypeConvention" | "PKEventTypeWorkshop" | "PKEventTypeSocialGathering"; - interface Instance { - model: string; - certificates: { - wwdr: string; - signerCert: string; - signerKey: { - keyFile: string; - passphrase: string; - } + interface Certificates { + wwdr?: string; + signerCert?: string; + signerKey?: { + keyFile: string; + passphrase?: string; }; - overrides: SupportedOptions; - shouldOverwrite?: boolean; } - interface SupportedOptions { + interface FactoryOptions { + model: BundleUnit | string; + certificates: Certificates; + overrides?: Object; + } + + interface BundleUnit { + [key: string]: Buffer; + } + + interface PartitionedBundle { + bundle: BundleUnit; + l10nBundle: { + [key: string]: BundleUnit + }; + } + + interface FinalCertificates { + wwdr: string; + signerCert: string; + signerKey: string; + } + + interface PassInstance { + model: PartitionedBundle; + certificates: FinalCertificates; + overrides?: OverridesSupportedOptions; + } + + interface OverridesSupportedOptions { serialNumber?: string; description?: string; - userInfo?: Object | any[]; + userInfo?: Object | Array; webServiceURL?: string; authenticationToken?: string; + sharingProhibited?: boolean; backgroundColor?: string; foregroundColor?: string; labelColor?: string; groupingIdentifier?: string; suppressStripShine?: boolean; + maxDistance?: number; } interface Field { From 91c6ff1b94868f95cc0a5e0cde696725ff10b2b2 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 23 Jun 2019 16:45:06 +0200 Subject: [PATCH 068/127] Small Improvements to comments and code --- src/pass.ts | 99 ++++++++++++++++++++++++++++----------------------- src/schema.ts | 2 +- 2 files changed, 56 insertions(+), 45 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 34294e4..b7611ba 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -62,14 +62,14 @@ export class Pass implements PassIndexSignature { this.bundle = { ...options.model.bundle }; // Parsing the options and extracting only the valid ones. - const validOvverrides = schema.getValidated(options.overrides || {}, "supportedOptions") as schema.OverridesSupportedOptions; + const validOverrides = schema.getValidated(options.overrides || {}, "supportedOptions") as schema.OverridesSupportedOptions; - if (validOvverrides === null) { + if (validOverrides === null) { throw new Error(formatMessage("OVV_KEYS_BADFORMAT")) } - if (Object.keys(validOvverrides).length) { - this._props = { ...validOvverrides }; + if (Object.keys(validOverrides).length) { + this._props = { ...validOverrides }; } try { @@ -106,10 +106,9 @@ export class Pass implements PassIndexSignature { /** * Generates the pass Stream * - * @async * @method generate - * @return {Promise} A Promise containing the stream of the generated pass. - */ + * @return A Stream of the generated pass. + */ generate(): Stream { // Editing Pass.json @@ -117,6 +116,10 @@ export class Pass implements PassIndexSignature { const finalBundle = { ...this.bundle } as schema.BundleUnit; + /** + * Iterating through languages and generating pass.string file + */ + Object.keys(this.l10nTranslations).forEach(lang => { const strings = generateStringFile(this.l10nTranslations[lang]); @@ -132,7 +135,7 @@ export class Pass implements PassIndexSignature { } this.l10nBundles[lang]["pass.strings"] = Buffer.concat([ - this.l10nBundles[lang]["pass.strings"] || Buffer.from("", "utf8"), + this.l10nBundles[lang]["pass.strings"] || Buffer.alloc(0), strings ]); } @@ -190,7 +193,8 @@ export class Pass implements PassIndexSignature { * @method localize * @params lang - the ISO 3166 alpha-2 code for the language * @params translations - key/value pairs where key is the - * string appearing in pass.json and value the translated string + * placeholder in pass.json localizable strings + * and value the real translated string. * @returns {this} * * @see https://apple.co/2KOv0OW - Passes support localization @@ -205,7 +209,7 @@ export class Pass implements PassIndexSignature { } /** - * Sets expirationDate property to the W3C date + * Sets expirationDate property to a W3C-formatted date * * @method expiration * @params date @@ -248,7 +252,7 @@ export class Pass implements PassIndexSignature { */ beacons(...data: schema.Beacon[]): PassWithLengthField { - if (!data.length) { + if (!data || !data.length) { return assignLength(0, this); } @@ -367,9 +371,11 @@ export class Pass implements PassIndexSignature { } else { const barcodes = [first, ...(data || [])]; - // Stripping from the array not-object elements - // and the ones that does not pass validation. - // Validation assign default value to missing parameters (if any). + /** + * Stripping from the array not-object elements + * and the ones that does not pass validation. + * Validation assign default value to missing parameters (if any). + */ const valid = barcodes.reduce((acc, current) => { if (!(current && current instanceof Object)) { @@ -386,10 +392,12 @@ export class Pass implements PassIndexSignature { }, []); if (valid.length) { - // With this check, we want to avoid that - // PKBarcodeFormatCode128 gets chosen automatically - // if it is the first. If true, we'll get 1 - // (so not the first index) + /** + * With this check, we want to avoid that + * PKBarcodeFormatCode128 gets chosen automatically + * if it is the first. If true, we'll get 1 + * (so not the first index) + */ const barcodeFirstValidIndex = Number(valid[0].format === "PKBarcodeFormatCode128"); if (valid.length > 0 && valid[barcodeFirstValidIndex]) { @@ -416,18 +424,18 @@ export class Pass implements PassIndexSignature { */ private [barcodesFillMissing](): this { - const props = this._props["barcodes"]; + const { barcodes } = this._props; - if (props.length === 4 || !props.length) { + if (barcodes.length === 4 || !barcodes.length) { return assignLength(0, this, { autocomplete: noop, backward: (format: schema.BarcodeFormat) => this[barcodesSetBackward](format) }); } - this._props["barcodes"] = barcodesFromUncompleteData(props[0].message); + this._props["barcodes"] = barcodesFromUncompleteData(barcodes[0].message); - return assignLength(4 - props.length, this, { + return assignLength(4 - barcodes.length, this, { autocomplete: noop, backward: (format: schema.BarcodeFormat) => this[barcodesSetBackward](format) }); @@ -443,31 +451,33 @@ export class Pass implements PassIndexSignature { * @return {this} */ - private [barcodesSetBackward](format: schema.BarcodeFormat | null): this { - if (format === null) { - this._props["barcode"] = undefined; + private [barcodesSetBackward](chosenFormat: schema.BarcodeFormat | null): this { + let { barcode, barcodes } = this._props; + + if (chosenFormat === null) { + barcode = undefined; return this; } - if (typeof format !== "string") { + if (typeof chosenFormat !== "string") { barcodeDebug(formatMessage("BRC_FORMATTYPE_UNMATCH")); return this; } - if (format === "PKBarcodeFormatCode128") { + if (chosenFormat === "PKBarcodeFormatCode128") { barcodeDebug(formatMessage("BRC_BW_FORMAT_UNSUPPORTED")); return this; } // Checking which object among barcodes has the same format of the specified one. - let index = this._props["barcodes"].findIndex(b => b.format.toLowerCase().includes(format.toLowerCase())); + const index = barcodes.findIndex(b => b.format.toLowerCase().includes(chosenFormat.toLowerCase())); if (index === -1) { barcodeDebug(formatMessage("BRC_NOT_SUPPORTED")); return this; } - this._props["barcode"] = this._props["barcodes"][index]; + barcode = barcodes[index]; return this; } @@ -499,7 +509,7 @@ export class Pass implements PassIndexSignature { */ private _sign(manifest: { [key: string]: string }): Buffer { - let signature = forge.pkcs7.createSignedData(); + const signature = forge.pkcs7.createSignedData(); signature.content = forge.util.createBuffer(JSON.stringify(manifest), "utf8"); @@ -565,9 +575,12 @@ export class Pass implements PassIndexSignature { const passFile = JSON.parse(passCoreBuffer.toString()); if (Object.keys(this._props).length) { - // We filter the existing (in passFile) and non-valid keys from - // the below array keys that accept rgb values - // and then delete it from the passFile. + /* + * We filter the existing (in passFile) and non-valid keys from + * the below array keys that accept rgb values + * and then delete it from the passFile. + */ + ["backgroundColor", "foregroundColor", "labelColor"] .filter(v => this._props[v] && !isValidRGB(this._props[v])) .forEach(v => delete this._props[v]); @@ -596,13 +609,14 @@ export class Pass implements PassIndexSignature { return Buffer.from(JSON.stringify(passFile)); } - set transitType(v: string) { - if (schema.isValid(v, "transitType")) { - this[transitType] = v; - } else { - genericDebug(formatMessage("TRSTYPE_NOT_VALID", v)); + set transitType(value: string) { + if (!schema.isValid(value, "transitType")) { + genericDebug(formatMessage("TRSTYPE_NOT_VALID", value)); this[transitType] = this[transitType] || ""; + return; } + + this[transitType] = value; } get transitType(): string { @@ -613,12 +627,9 @@ export class Pass implements PassIndexSignature { /** * Automatically generates barcodes for all the types given common info * - * @method barcodesFromMessage - * @params data - common info, may be object or the message itself - * @params data.message - the content to be placed inside "message" field - * @params [data.altText=data.message] - alternativeText, is message content if not overwritten - * @params [data.messageEncoding=iso-8859-1] - the encoding - * @return Object array barcodeDict compliant + * @method barcodesFromUncompleteData + * @params message - the content to be placed inside "message" field + * @return Array of barcodeDict compliant */ function barcodesFromUncompleteData(message: string): schema.Barcode[] { diff --git a/src/schema.ts b/src/schema.ts index 009ae07..a4efd17 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -13,7 +13,7 @@ export interface Certificates { } export interface FactoryOptions { - model: { [key: string]: Buffer } | string; + model: BundleUnit | string; certificates: Certificates; overrides?: Object; } From 701e020116ce075b85f94381a2e9b0753bf1703c Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 27 Jun 2019 00:43:50 +0200 Subject: [PATCH 069/127] Added Pass as exported type --- index.ts | 2 +- src/factory.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/index.ts b/index.ts index 7bc5732..527fdbc 100644 --- a/index.ts +++ b/index.ts @@ -1 +1 @@ -export { createPass } from "./src/factory"; +export { createPass, Pass } from "./src/factory"; diff --git a/src/factory.ts b/src/factory.ts index 881f3b4..734d311 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -11,6 +11,8 @@ import { removeHidden } from "./utils"; const readDir = promisify(_readdir); const readFile = promisify(_readFile); +export type Pass = InstanceType + export async function createPass(options: FactoryOptions): Promise { if (!(options && Object.keys(options).length)) { throw new Error(formatMessage("CP_NO_OPTS")); From a82057d61bfe7ade89e41479301357cb5292a98b Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 27 Jun 2019 00:47:19 +0200 Subject: [PATCH 070/127] Added export on Pass declaration --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 689b46d..d4764ed 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,7 +2,7 @@ import { Stream } from "stream"; export function createPass(options: Schema.FactoryOptions): Promise; -declare class Pass { +export declare class Pass { constructor(options: Schema.PassInstance); public transitType: "PKTransitTypeAir" | "PKTransitTypeBoat" | "PKTransitTypeBus" | "PKTransitTypeGeneric" | "PKTransitTypeTrain"; From 9f924bbdcdad4e8e6e95ff733e822234df4689ae Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 27 Jun 2019 22:39:52 +0200 Subject: [PATCH 071/127] Added support for pass.json properties to be inserted in _props --- src/pass.ts | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index b7611ba..dcbb5df 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -61,6 +61,13 @@ export class Pass implements PassIndexSignature { this.l10nBundles = options.model.l10nBundle; this.bundle = { ...options.model.bundle }; + try { + // getting pass.json + this.passCore = JSON.parse(this.bundle["pass.json"].toString("utf8")); + } catch (err) { + throw new Error(formatMessage("PASSFILE_VALIDATION_FAILED")); + } + // Parsing the options and extracting only the valid ones. const validOverrides = schema.getValidated(options.overrides || {}, "supportedOptions") as schema.OverridesSupportedOptions; @@ -68,15 +75,22 @@ export class Pass implements PassIndexSignature { throw new Error(formatMessage("OVV_KEYS_BADFORMAT")) } - if (Object.keys(validOverrides).length) { - this._props = { ...validOverrides }; - } + this._props = [ + "barcodes", "barcode", + "expirationDate", "voided", + "beacons", "locations", + "relevantDate", "nfc" + ].reduce((acc, current) => { + if (!this.passCore[current]) { + return acc; + } - try { - // getting pass.json - this.passCore = JSON.parse(this.bundle["pass.json"].toString("utf8")); - } catch (err) { - throw new Error(formatMessage("PASSFILE_VALIDATION_FAILED")); + acc[current] = this.passCore[current]; + return acc; + }, {}); + + if (Object.keys(validOverrides).length) { + this._props = { ...this._props, ...validOverrides }; } this.type = Object.keys(this.passCore) From 05193aa32a1322c3066bad879d308f130a427193 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 27 Jun 2019 23:43:29 +0200 Subject: [PATCH 072/127] Added support to fetching current values of non-overrides props --- src/pass.ts | 86 +++++++++++++++++++++++++++++++++++++++------------ src/schema.ts | 1 + 2 files changed, 67 insertions(+), 20 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index dcbb5df..c691923 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -214,7 +214,11 @@ export class Pass implements PassIndexSignature { * @see https://apple.co/2KOv0OW - Passes support localization */ - localize(lang: string, translations?: { [key: string]: string }): this { + localize(lang?: string, translations?: { [key: string]: string }): this | string[] { + if (lang === undefined && translations === undefined) { + return Object.keys(this.l10nTranslations); + } + if (lang && typeof lang === "string" && (typeof translations === "object" || translations === undefined)) { this.l10nTranslations[lang] = translations || {}; } @@ -230,7 +234,11 @@ export class Pass implements PassIndexSignature { * @returns {this} */ - expiration(date: Date): this { + expiration(date?: Date): this | string { + if (date === undefined) { + return this._props["expirationDate"]; + } + if (!(date instanceof Date)) { return this; } @@ -265,8 +273,13 @@ export class Pass implements PassIndexSignature { * @returns {Pass} */ - beacons(...data: schema.Beacon[]): PassWithLengthField { - if (!data || !data.length) { + beacons(...data: schema.Beacon[]): PassWithLengthField | schema.Beacon[] { + if (data === undefined) { + return this._props["beacons"]; + } + + if (!data.length) { + this._props["beacons"] = []; return assignLength(0, this); } @@ -282,9 +295,9 @@ export class Pass implements PassIndexSignature { return assignLength(0, this); } - this._props["beacons"] = validBeacons; + (this._props["beacons"] || (this._props["beacons"] = [])).push(...validBeacons); - return assignLength(validBeacons.length, this); + return assignLength(this._props["beacons"].length, this); } /** @@ -293,8 +306,13 @@ export class Pass implements PassIndexSignature { * @returns {Pass} */ - locations(...data: schema.Location[]): PassWithLengthField { + locations(...data: schema.Location[]): PassWithLengthField | schema.Location[] { + if (data === undefined) { + return this._props["locations"]; + } + if (!data.length) { + this._props["locations"] = []; return assignLength(0, this); } @@ -310,9 +328,11 @@ export class Pass implements PassIndexSignature { return assignLength(0, this); } - this._props["locations"] = validLocations; + console.log("Locations:", this._props["locations"]); - return assignLength(validLocations.length, this); + (this._props["locations"] || (this._props["locations"] = [])).push(...validLocations); + + return assignLength(this._props["locations"].length, this); } /** @@ -321,7 +341,16 @@ export class Pass implements PassIndexSignature { * @returns {Pass} */ - relevantDate(date: Date): this { + relevantDate(date?: Date): this | string { + if (date === undefined) { + return this._props["relevantDate"]; + } + + if (date === null) { + delete this._props["relevantDate"]; + return this; + } + if (!(date instanceof Date)) { genericDebug(formatMessage("DATE_FORMAT_UNMATCH", "Relevant Date")); return this; @@ -347,7 +376,20 @@ export class Pass implements PassIndexSignature { * @return {this} Improved this with length property and other methods */ - barcode(first: string | schema.Barcode, ...data: schema.Barcode[]): PassWithBarcodeMethods { + barcode(first?: string | schema.Barcode, ...data: schema.Barcode[]): PassWithBarcodeMethods | schema.Barcode[] { + console.log(first, data); + if (first === undefined && (data === undefined || !data.length)) { + return this._props["barcodes"]; + } + + if (first === null) { + delete this._props["barcodes"]; + return assignLength(0, this, { + autocomplete: noop, + backward: (format: schema.BarcodeFormat) => this[barcodesSetBackward](format), + }); + } + const isFirstParameterValid = ( first && ( typeof first === "string" && first.length || ( @@ -369,7 +411,7 @@ export class Pass implements PassIndexSignature { if (!autogen.length) { barcodeDebug(formatMessage("BRC_AUTC_MISSING_DATA")); - return assignLength(0, this, { + return assignLength(0, this, { autocomplete: noop, backward: noop, }); @@ -378,7 +420,7 @@ export class Pass implements PassIndexSignature { this._props["barcode"] = autogen[0]; this._props["barcodes"] = autogen; - return assignLength(autogen.length, this, { + return assignLength(autogen.length, this, { autocomplete: noop, backward: (format: schema.BarcodeFormat) => this[barcodesSetBackward](format) }); @@ -421,7 +463,7 @@ export class Pass implements PassIndexSignature { this._props["barcodes"] = valid; } - return assignLength(valid.length, this, { + return assignLength(valid.length, this, { autocomplete: () => this[barcodesFillMissing](), backward: (format: schema.BarcodeFormat) => this[barcodesSetBackward](format), }); @@ -437,7 +479,7 @@ export class Pass implements PassIndexSignature { * @returns {this} Improved this, with length property and retroCompatibility method. */ - private [barcodesFillMissing](): this { + private [barcodesFillMissing](): PassWithBarcodeMethods { const { barcodes } = this._props; if (barcodes.length === 4 || !barcodes.length) { @@ -449,7 +491,7 @@ export class Pass implements PassIndexSignature { this._props["barcodes"] = barcodesFromUncompleteData(barcodes[0].message); - return assignLength(4 - barcodes.length, this, { + return assignLength(4 - barcodes.length, this, { autocomplete: noop, backward: (format: schema.BarcodeFormat) => this[barcodesSetBackward](format) }); @@ -466,10 +508,10 @@ export class Pass implements PassIndexSignature { */ private [barcodesSetBackward](chosenFormat: schema.BarcodeFormat | null): this { - let { barcode, barcodes } = this._props; + let { barcodes } = this._props; if (chosenFormat === null) { - barcode = undefined; + this._props["barcode"] = undefined; return this; } @@ -491,7 +533,7 @@ export class Pass implements PassIndexSignature { return this; } - barcode = barcodes[index]; + this._props["barcode"] = barcodes[index]; return this; } @@ -503,7 +545,11 @@ export class Pass implements PassIndexSignature { * @returns {this} */ - nfc(data: schema.NFC): this { + nfc(data?: schema.NFC): this | schema.NFC { + if (data === undefined) { + return this._props["nfc"]; + } + if (!(typeof data === "object" && !Array.isArray(data) && schema.isValid(data, "nfcDict"))) { genericDebug("Invalid NFC data provided"); return this; diff --git a/src/schema.ts b/src/schema.ts index a4efd17..7c6ff28 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -75,6 +75,7 @@ export interface OverridesSupportedOptions { labelColor?: string; groupingIdentifier?: string; suppressStripShine?: boolean; + logoText?: string; maxDistance?: number; } From 53441a9b2fcf1df32f5ffb2967d2a77c109ca3de Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 27 Jun 2019 23:43:44 +0200 Subject: [PATCH 073/127] Updated examples and tests --- examples/barcode.ts | 4 ++-- examples/localization.ts | 3 ++- spec/index.ts | 52 +++++++++++++++++++--------------------- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/examples/barcode.ts b/examples/barcode.ts index 056d6fb..526cc3a 100644 --- a/examples/barcode.ts +++ b/examples/barcode.ts @@ -35,7 +35,7 @@ app.all(async function manageRequest(request, response) { // After this, pass.props["barcodes"] will have support for all the formats // while pass.props["barcode"] will be the first of barcodes. - bc = pass.barcode("Thank you for using this package <3"); + bc = pass.barcode("Thank you for using this package <3") as PassWithBarcodeMethods } else { // After this, pass.props["barcodes"] will have support for just two of three // of the passed format (the valid ones) and pass.props["barcode"] the first of barcodes. @@ -50,7 +50,7 @@ app.all(async function manageRequest(request, response) { }, { message: "Thank you for using this package <3", format: "PKBarcodeFormatMock44617" - }); + }) as PassWithBarcodeMethods; } // You can change the format chosen for barcode prop support by calling .backward() diff --git a/examples/localization.ts b/examples/localization.ts index 4ea4c9b..75e2523 100644 --- a/examples/localization.ts +++ b/examples/localization.ts @@ -50,8 +50,9 @@ app.all(async function manageRequest(request, response) { // This language does not exist but is still added as .lproj folder pass.localize("zu", {}); + // @ts-ignore - ignoring for logging purposes. Do not replicate - console.log("Added languages", Object.keys(pass.l10nBundles).join(", ")) + console.log("Added languages", pass.localize().join(", ")) const stream = pass.generate(); response.set({ diff --git a/spec/index.ts b/spec/index.ts index 7246f10..4e8b3e9 100644 --- a/spec/index.ts +++ b/spec/index.ts @@ -1,5 +1,5 @@ import { createPass } from ".."; -import { Pass } from "../src/pass"; +import { Pass, PassWithBarcodeMethods } from "../src/pass"; /* * Yes, I know that I'm checking against "private" properties @@ -111,6 +111,8 @@ describe("Node-Passkit-generator", function () { describe("locations :: ", () => { it("One-Invalid-schema location won't apply changes", () => { + const oldAmountOfLocations = pass.locations().length; + pass.locations({ // @ts-ignore "ibrupofene": "no", @@ -118,10 +120,12 @@ describe("Node-Passkit-generator", function () { }); // @ts-ignore -- Ignoring for test purposes - expect(pass._props["locations"]).toBe(undefined); + expect(pass._props["locations"].length).toBe(oldAmountOfLocations); }); it("Two locations, with one invalid, will be filtered", () => { + const oldAmountOfLocations = pass.locations().length; + pass.locations({ //@ts-ignore "ibrupofene": "no", @@ -132,7 +136,7 @@ describe("Node-Passkit-generator", function () { }); // @ts-ignore -- Ignoring for test purposes - expect(pass._props["locations"].length).toBe(1); + expect(pass._props["locations"].length).toBe(oldAmountOfLocations+1); }); }); @@ -170,34 +174,31 @@ describe("Node-Passkit-generator", function () { }); describe("barcode()", () => { - it("Missing data will won't apply changes", () => { - // @ts-ignore -- Ignoring for test purposes - pass.barcode(); + it("Missing data will return the current data", () => { + const oldAmountOfBarcodes = pass.barcode().length; // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"]).toBe(undefined); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcodes"]).toBe(undefined); + expect(pass.barcode().length).toBe(oldAmountOfBarcodes); }); it("Boolean parameter won't apply changes", () => { + const oldAmountOfBarcodes = pass.barcode().length; + // @ts-ignore -- Ignoring for test purposes pass.barcode(true); // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"]).toBe(undefined); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcodes"]).toBe(undefined); + expect(pass.barcode().length).toBe(oldAmountOfBarcodes); }); it("Numeric parameter won't apply changes", () => { + const oldAmountOfBarcodes = pass.barcode().length; + // @ts-ignore -- Ignoring for test purposes pass.barcode(42); // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"]).toBe(undefined); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcodes"]).toBe(undefined); + expect(pass.barcode().length).toBe(oldAmountOfBarcodes); }); it("String parameter will autogenerate all the objects", () => { @@ -207,11 +208,10 @@ describe("Node-Passkit-generator", function () { expect(pass._props["barcode"] instanceof Object).toBe(true); // @ts-ignore -- Ignoring for test purposes expect(pass._props["barcode"].message).toBe("28363516282"); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcodes"].length).toBe(4); + expect(pass.barcode().length).toBe(4); }); - it("Object parameter will be automatically converted to one-element Array", () => { + it("Object parameter will be accepted", () => { pass.barcode({ message: "28363516282", format: "PKBarcodeFormatPDF417", @@ -223,7 +223,7 @@ describe("Node-Passkit-generator", function () { // @ts-ignore -- Ignoring for test purposes expect(pass._props["barcode"].format).toBe("PKBarcodeFormatPDF417"); // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcodes"].length).toBe(1); + expect(pass.barcode().length).toBe(1); }); it("Array parameter will apply changes", () => { @@ -256,18 +256,19 @@ describe("Node-Passkit-generator", function () { }); it("Object without message property, will be filtered out", () => { + const oldAmountOfBarcodes = pass.barcode().length; + // @ts-ignore -- Ignoring for test purposes pass.barcode({ format: "PKBarcodeFormatPDF417", }); // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"]).toBe(undefined); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcodes"]).toBe(undefined); + expect(pass.barcode().length).toBe(oldAmountOfBarcodes); }); it("Array containing non-object elements will be rejected", () => { + const oldAmountOfBarcodes = pass.barcode().length; // @ts-ignore -- Ignoring for test purposes pass.barcode(5, 10, 15, { message: "28363516282", @@ -275,9 +276,7 @@ describe("Node-Passkit-generator", function () { }, 7, 1); // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"] instanceof Object).toBe(false); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcodes"]).toBeUndefined(); + expect(pass.barcode().length).toBe(1) }); }); @@ -294,8 +293,7 @@ describe("Node-Passkit-generator", function () { }); it("Null will delete backward support", () => { - pass - .barcode("Message-22645272183") + (pass.barcode("Message-22645272183") as PassWithBarcodeMethods) .backward(null); // @ts-ignore -- Ignoring for test purposes From 75b22daf73bbc85d8d285e1e595f05c40ec17ba0 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 27 Jun 2019 23:49:52 +0200 Subject: [PATCH 074/127] Removed selected patching based on type for a direct replacement of the properties --- src/pass.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index c691923..2dd49d2 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -632,7 +632,7 @@ export class Pass implements PassIndexSignature { */ private _patch(passCoreBuffer: Buffer): Buffer { - const passFile = JSON.parse(passCoreBuffer.toString()); + let passFile = JSON.parse(passCoreBuffer.toString()); if (Object.keys(this._props).length) { /* @@ -645,15 +645,7 @@ export class Pass implements PassIndexSignature { .filter(v => this._props[v] && !isValidRGB(this._props[v])) .forEach(v => delete this._props[v]); - Object.keys(this._props).forEach(prop => { - if (passFile[prop] && passFile[prop] instanceof Array) { - passFile[prop] = [ ...passFile[prop], ...this._props[prop] ]; - } else if (passFile[prop] && passFile[prop] instanceof Object) { - passFile[prop] = { ...passFile[prop], ...this._props[prop] }; - } else { - passFile[prop] = this._props[prop]; - } - }); + passFile = { ...passFile, ...this._props }; } this._fields.forEach(field => { From a981571c42b8b1ca7819b617883777cc85d104ae Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Fri, 28 Jun 2019 00:18:55 +0200 Subject: [PATCH 075/127] Fixed beacons and locations content getting --- spec/index.ts | 12 ++++++++---- src/pass.ts | 19 ++++++++----------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/spec/index.ts b/spec/index.ts index 4e8b3e9..bb56934 100644 --- a/spec/index.ts +++ b/spec/index.ts @@ -120,7 +120,7 @@ describe("Node-Passkit-generator", function () { }); // @ts-ignore -- Ignoring for test purposes - expect(pass._props["locations"].length).toBe(oldAmountOfLocations); + expect(pass.locations().length).toBe(oldAmountOfLocations); }); it("Two locations, with one invalid, will be filtered", () => { @@ -136,12 +136,14 @@ describe("Node-Passkit-generator", function () { }); // @ts-ignore -- Ignoring for test purposes - expect(pass._props["locations"].length).toBe(oldAmountOfLocations+1); + expect(pass.locations().length).toBe(oldAmountOfLocations+1); }); }); describe("Beacons :: ", () => { it("One-Invalid-schema beacon data won't apply changes", () => { + const oldBeacons = pass.beacons(); + pass.beacons({ // @ts-ignore "ibrupofene": "no", @@ -151,10 +153,12 @@ describe("Node-Passkit-generator", function () { }); // @ts-ignore -- Ignoring for test purposes - expect(pass._props["beacons"]).toBe(undefined); + expect(pass.beacons()).toBe(oldBeacons ? oldBeacons.length : undefined); }); it("Two beacons sets, with one invalid, will be filtered", () => { + const oldBeacons = pass.beacons(); + pass.beacons({ "major": 55, "minor": 0, @@ -168,7 +172,7 @@ describe("Node-Passkit-generator", function () { }); // @ts-ignore -- Ignoring for test purposes - expect(pass._props["beacons"].length).toBe(1); + expect(pass.beacons().length).toBe(oldBeacons ? oldBeacons.length+1 : 1); }); }); }); diff --git a/src/pass.ts b/src/pass.ts index 2dd49d2..7320639 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -274,13 +274,13 @@ export class Pass implements PassIndexSignature { */ beacons(...data: schema.Beacon[]): PassWithLengthField | schema.Beacon[] { - if (data === undefined) { - return this._props["beacons"]; + if (data === null) { + delete this._props["beacons"]; + return assignLength(0, this); } if (!data.length) { - this._props["beacons"] = []; - return assignLength(0, this); + return this._props["beacons"]; } const validBeacons = data.reduce((acc, current) => { @@ -307,13 +307,13 @@ export class Pass implements PassIndexSignature { */ locations(...data: schema.Location[]): PassWithLengthField | schema.Location[] { - if (data === undefined) { - return this._props["locations"]; + if (data === null) { + delete this._props["locations"]; + return assignLength(0, this); } if (!data.length) { - this._props["locations"] = []; - return assignLength(0, this); + return this._props["locations"]; } const validLocations = data.reduce((acc, current) => { @@ -328,8 +328,6 @@ export class Pass implements PassIndexSignature { return assignLength(0, this); } - console.log("Locations:", this._props["locations"]); - (this._props["locations"] || (this._props["locations"] = [])).push(...validLocations); return assignLength(this._props["locations"].length, this); @@ -377,7 +375,6 @@ export class Pass implements PassIndexSignature { */ barcode(first?: string | schema.Barcode, ...data: schema.Barcode[]): PassWithBarcodeMethods | schema.Barcode[] { - console.log(first, data); if (first === undefined && (data === undefined || !data.length)) { return this._props["barcodes"]; } From b410a435dfff630fe9e20bf22964f6e12cfb8dda Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 29 Jun 2019 17:50:50 +0200 Subject: [PATCH 076/127] Unified date processing of expiration and relevantDate --- src/pass.ts | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 7320639..790ec85 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -243,15 +243,12 @@ export class Pass implements PassIndexSignature { return this; } - const dateParse = dateToW3CString(date); + const parsedDate = processDate("expirationDate", date); - if (!dateParse) { - genericDebug(formatMessage("DATE_FORMAT_UNMATCH", "Expiration date")); - return this; + if (parsedDate) { + this._props["expirationDate"] = parsedDate; } - this._props["expirationDate"] = dateParse; - return this; } @@ -349,19 +346,12 @@ export class Pass implements PassIndexSignature { return this; } - if (!(date instanceof Date)) { - genericDebug(formatMessage("DATE_FORMAT_UNMATCH", "Relevant Date")); - return this; + const parsedDate = processDate("relevandDate", date); + + if (parsedDate) { + this._props["relevantDate"] = parsedDate; } - const parsedDate = dateToW3CString(date); - - if (!parsedDate) { - // @TODO: create message "Unable to format date" - return this; - } - - this._props["relevantDate"] = parsedDate; return this; } @@ -693,3 +683,18 @@ function barcodesFromUncompleteData(message: string): schema.Barcode[] { "PKBarcodeFormatCode128" ].map(format => schema.getValidated({ format, message }, "barcode")); } + +function processDate(key: string, date: Date): string | null { + if (!(date instanceof Date)) { + return null; + } + + const dateParse = dateToW3CString(date); + + if (!dateParse) { + genericDebug(formatMessage("DATE_FORMAT_UNMATCH", key)); + return null; + } + + return dateParse; +} From 003b58422123d6c13998017736c7af6170cbac61 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 29 Jun 2019 17:56:19 +0200 Subject: [PATCH 077/127] Set back beacons and locations to overwrite the current properties; Merged beacons and locations relevancy logic --- src/pass.ts | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 790ec85..48e1d25 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -276,14 +276,11 @@ export class Pass implements PassIndexSignature { return assignLength(0, this); } - if (!data.length) { - return this._props["beacons"]; - } + const valid = processRelevancySet("beacons", data); - const validBeacons = data.reduce((acc, current) => { - if (!(Object.keys(current).length && schema.isValid(current, "beaconsDict"))) { - return acc; - } + if (valid.length) { + this._props["beacons"] = valid; + } return [...acc, current]; }, []); @@ -309,14 +306,11 @@ export class Pass implements PassIndexSignature { return assignLength(0, this); } - if (!data.length) { - return this._props["locations"]; - } + const valid = processRelevancySet("locations", data); - const validLocations = data.reduce((acc, current) => { - if (!(Object.keys(current).length && schema.isValid(current, "locationsDict"))) { - return acc; - } + if (valid.length) { + this._props["locations"] = valid; + } return [...acc, current]; }, []); @@ -684,6 +678,16 @@ function barcodesFromUncompleteData(message: string): schema.Barcode[] { ].map(format => schema.getValidated({ format, message }, "barcode")); } +function processRelevancySet(key: string, data: T[]): T[] { + return data.reduce((acc, current) => { + if (!(Object.keys(current).length && schema.isValid(current, `${key}Dict`))) { + return acc; + } + + return [...acc, current]; + }, []); +} + function processDate(key: string, date: Date): string | null { if (!(date instanceof Date)) { return null; From 5babdf685483c3ddf7c28ca19978970c80ac4176 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 29 Jun 2019 18:09:02 +0200 Subject: [PATCH 078/127] Removed assignLength; Removed passing of undefined to get the current value; --- src/pass.ts | 80 +++++++++++------------------------------------------ 1 file changed, 16 insertions(+), 64 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 48e1d25..2e44f13 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -214,11 +214,7 @@ export class Pass implements PassIndexSignature { * @see https://apple.co/2KOv0OW - Passes support localization */ - localize(lang?: string, translations?: { [key: string]: string }): this | string[] { - if (lang === undefined && translations === undefined) { - return Object.keys(this.l10nTranslations); - } - + localize(lang?: string, translations?: { [key: string]: string }): this { if (lang && typeof lang === "string" && (typeof translations === "object" || translations === undefined)) { this.l10nTranslations[lang] = translations || {}; } @@ -235,11 +231,8 @@ export class Pass implements PassIndexSignature { */ expiration(date?: Date): this | string { - if (date === undefined) { - return this._props["expirationDate"]; - } - - if (!(date instanceof Date)) { + if (date === null) { + delete this._props["expirationDate"]; return this; } @@ -270,10 +263,10 @@ export class Pass implements PassIndexSignature { * @returns {Pass} */ - beacons(...data: schema.Beacon[]): PassWithLengthField | schema.Beacon[] { + beacons(...data: schema.Beacon[] | null): this { if (data === null) { delete this._props["beacons"]; - return assignLength(0, this); + return this; } const valid = processRelevancySet("beacons", data); @@ -282,28 +275,19 @@ export class Pass implements PassIndexSignature { this._props["beacons"] = valid; } - return [...acc, current]; - }, []); - - if (!validBeacons.length) { - return assignLength(0, this); + return this; } - (this._props["beacons"] || (this._props["beacons"] = [])).push(...validBeacons); - - return assignLength(this._props["beacons"].length, this); - } - /** * Sets current pass' relevancy through locations * @param data * @returns {Pass} */ - locations(...data: schema.Location[]): PassWithLengthField | schema.Location[] { + locations(...data: schema.Location[]): this { if (data === null) { delete this._props["locations"]; - return assignLength(0, this); + return this; } const valid = processRelevancySet("locations", data); @@ -312,18 +296,9 @@ export class Pass implements PassIndexSignature { this._props["locations"] = valid; } - return [...acc, current]; - }, []); - - if (!validLocations.length) { - return assignLength(0, this); + return this; } - (this._props["locations"] || (this._props["locations"] = [])).push(...validLocations); - - return assignLength(this._props["locations"].length, this); - } - /** * Sets current pass' relevancy through a date * @param data @@ -331,10 +306,6 @@ export class Pass implements PassIndexSignature { */ relevantDate(date?: Date): this | string { - if (date === undefined) { - return this._props["relevantDate"]; - } - if (date === null) { delete this._props["relevantDate"]; return this; @@ -358,22 +329,15 @@ export class Pass implements PassIndexSignature { * @return {this} Improved this with length property and other methods */ - barcode(first?: string | schema.Barcode, ...data: schema.Barcode[]): PassWithBarcodeMethods | schema.Barcode[] { - if (first === undefined && (data === undefined || !data.length)) { - return this._props["barcodes"]; - } - + barcode(first?: string | schema.Barcode, ...data: schema.Barcode[]): this { if (first === null) { delete this._props["barcodes"]; - return assignLength(0, this, { - autocomplete: noop, - backward: (format: schema.BarcodeFormat) => this[barcodesSetBackward](format), - }); + return this; } const isFirstParameterValid = ( first && ( - typeof first === "string" && first.length || ( + typeof first === "string" || ( typeof first === "object" && first.hasOwnProperty("message") ) @@ -381,10 +345,7 @@ export class Pass implements PassIndexSignature { ); if (!isFirstParameterValid) { - return assignLength(0, this, { - autocomplete: noop, - backward: noop, - }); + return this; } if (typeof first === "string") { @@ -392,19 +353,13 @@ export class Pass implements PassIndexSignature { if (!autogen.length) { barcodeDebug(formatMessage("BRC_AUTC_MISSING_DATA")); - return assignLength(0, this, { - autocomplete: noop, - backward: noop, - }); + return this; } this._props["barcode"] = autogen[0]; this._props["barcodes"] = autogen; - return assignLength(autogen.length, this, { - autocomplete: noop, - backward: (format: schema.BarcodeFormat) => this[barcodesSetBackward](format) - }); + return this; } else { const barcodes = [first, ...(data || [])]; @@ -444,10 +399,7 @@ export class Pass implements PassIndexSignature { this._props["barcodes"] = valid; } - return assignLength(valid.length, this, { - autocomplete: () => this[barcodesFillMissing](), - backward: (format: schema.BarcodeFormat) => this[barcodesSetBackward](format), - }); + return this; } } From c5a8de49649d1d5ffff60bc8564fb2ce2efde467 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 29 Jun 2019 18:10:55 +0200 Subject: [PATCH 079/127] Renamed barcode to barcodes and backward for barcodes to barcode --- src/pass.ts | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 2e44f13..61d6a6d 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -329,7 +329,7 @@ export class Pass implements PassIndexSignature { * @return {this} Improved this with length property and other methods */ - barcode(first?: string | schema.Barcode, ...data: schema.Barcode[]): this { + barcodes(first?: string | schema.Barcode, ...data: schema.Barcode[]): this { if (first === null) { delete this._props["barcodes"]; return this; @@ -356,7 +356,6 @@ export class Pass implements PassIndexSignature { return this; } - this._props["barcode"] = autogen[0]; this._props["barcodes"] = autogen; return this; @@ -384,18 +383,6 @@ export class Pass implements PassIndexSignature { }, []); if (valid.length) { - /** - * With this check, we want to avoid that - * PKBarcodeFormatCode128 gets chosen automatically - * if it is the first. If true, we'll get 1 - * (so not the first index) - */ - const barcodeFirstValidIndex = Number(valid[0].format === "PKBarcodeFormatCode128"); - - if (valid.length > 0 && valid[barcodeFirstValidIndex]) { - this._props["barcode"] = valid[barcodeFirstValidIndex]; - } - this._props["barcodes"] = valid; } @@ -435,16 +422,16 @@ export class Pass implements PassIndexSignature { * this let you choose which structure to use for retrocompatibility * property "barcode". * - * @method Symbol/barcodesSetBackward + * @method barcode * @params format - the format to be used * @return {this} */ - private [barcodesSetBackward](chosenFormat: schema.BarcodeFormat | null): this { + barcode(chosenFormat: schema.BarcodeFormat | null): this { let { barcodes } = this._props; if (chosenFormat === null) { - this._props["barcode"] = undefined; + delete this._props["barcode"]; return this; } From e4c39d837ae622b8c51e73915c020d89fea5fd8d Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 29 Jun 2019 18:15:07 +0200 Subject: [PATCH 080/127] Removed barcodes autocompletion --- src/pass.ts | 32 +------------------------------- src/utils.ts | 10 ---------- 2 files changed, 1 insertion(+), 41 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 61d6a6d..70dc932 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -8,17 +8,14 @@ import * as schema from "./schema"; import formatMessage from "./messages"; import FieldsArray from "./fieldsArray"; import { - assignLength, generateStringFile, + generateStringFile, dateToW3CString, isValidRGB } from "./utils"; const barcodeDebug = debug("passkit:barcode"); const genericDebug = debug("passkit:generic"); -const noop = () => {}; const transitType = Symbol("transitType"); -const barcodesFillMissing = Symbol("bfm"); -const barcodesSetBackward = Symbol("bsb"); interface PassIndexSignature { [key: string]: any; @@ -390,33 +387,6 @@ export class Pass implements PassIndexSignature { } } - /** - * Given an already compiled props["barcodes"] with missing objects - * (less than 4), takes infos from the first object and replicate them - * in the missing structures. - * - * @method Symbol/barcodesFillMissing - * @returns {this} Improved this, with length property and retroCompatibility method. - */ - - private [barcodesFillMissing](): PassWithBarcodeMethods { - const { barcodes } = this._props; - - if (barcodes.length === 4 || !barcodes.length) { - return assignLength(0, this, { - autocomplete: noop, - backward: (format: schema.BarcodeFormat) => this[barcodesSetBackward](format) - }); - } - - this._props["barcodes"] = barcodesFromUncompleteData(barcodes[0].message); - - return assignLength(4 - barcodes.length, this, { - autocomplete: noop, - backward: (format: schema.BarcodeFormat) => this[barcodesSetBackward](format) - }); - } - /** * Given an index <= the amount of already set "barcodes", * this let you choose which structure to use for retrocompatibility diff --git a/src/utils.ts b/src/utils.ts index ce613b1..2872955 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -80,13 +80,3 @@ export function generateStringFile(lang: { [index: string]: string }): Buffer { return Buffer.from(strings.join(EOL), "utf8"); } - -/** - * Creates a new object with custom length property - * @param {number} value - the length - * @param {Array>} source - the main sources of properties - */ - -export function assignLength(length: number, ...sources: Array<{ [key: string]: any }>): { [key: string]: any } & T { - return Object.assign({ length }, ...sources); -} From dba3a7a02a44c9f9dbf8cbd9fa43e474b9016e0d Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 29 Jun 2019 23:51:38 +0200 Subject: [PATCH 081/127] Renamed `_props` to a symbol-property --- src/pass.ts | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 70dc932..057b5f8 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -16,6 +16,7 @@ const barcodeDebug = debug("passkit:barcode"); const genericDebug = debug("passkit:generic"); const transitType = Symbol("transitType"); +const passProps = Symbol("_props"); interface PassIndexSignature { [key: string]: any; @@ -34,7 +35,7 @@ export class Pass implements PassIndexSignature { private bundle: schema.BundleUnit; private l10nBundles: schema.PartitionedBundle["l10nBundle"]; private _fields: (keyof schema.PassFields)[]; - private _props: schema.ValidPass = {}; + private [passProps]: schema.ValidPass = {}; private type: keyof schema.ValidPassType; private fieldsKeys: Set = new Set(); private passCore: schema.ValidPass = {}; @@ -229,14 +230,14 @@ export class Pass implements PassIndexSignature { expiration(date?: Date): this | string { if (date === null) { - delete this._props["expirationDate"]; + delete this[passProps]["expirationDate"]; return this; } const parsedDate = processDate("expirationDate", date); if (parsedDate) { - this._props["expirationDate"] = parsedDate; + this[passProps]["expirationDate"] = parsedDate; } return this; @@ -250,7 +251,7 @@ export class Pass implements PassIndexSignature { */ void(): this { - this._props["voided"] = true; + this[passProps]["voided"] = true; return this; } @@ -262,14 +263,14 @@ export class Pass implements PassIndexSignature { beacons(...data: schema.Beacon[] | null): this { if (data === null) { - delete this._props["beacons"]; + delete this[passProps]["beacons"]; return this; } const valid = processRelevancySet("beacons", data); if (valid.length) { - this._props["beacons"] = valid; + this[passProps]["beacons"] = valid; } return this; @@ -283,14 +284,14 @@ export class Pass implements PassIndexSignature { locations(...data: schema.Location[]): this { if (data === null) { - delete this._props["locations"]; + delete this[passProps]["locations"]; return this; } const valid = processRelevancySet("locations", data); if (valid.length) { - this._props["locations"] = valid; + this[passProps]["locations"] = valid; } return this; @@ -304,14 +305,14 @@ export class Pass implements PassIndexSignature { relevantDate(date?: Date): this | string { if (date === null) { - delete this._props["relevantDate"]; + delete this[passProps]["relevantDate"]; return this; } const parsedDate = processDate("relevandDate", date); if (parsedDate) { - this._props["relevantDate"] = parsedDate; + this[passProps]["relevantDate"] = parsedDate; } return this; @@ -328,7 +329,7 @@ export class Pass implements PassIndexSignature { barcodes(first?: string | schema.Barcode, ...data: schema.Barcode[]): this { if (first === null) { - delete this._props["barcodes"]; + delete this[passProps]["barcodes"]; return this; } @@ -353,7 +354,7 @@ export class Pass implements PassIndexSignature { return this; } - this._props["barcodes"] = autogen; + this[passProps]["barcodes"] = autogen; return this; } else { @@ -380,7 +381,7 @@ export class Pass implements PassIndexSignature { }, []); if (valid.length) { - this._props["barcodes"] = valid; + this[passProps]["barcodes"] = valid; } return this; @@ -398,10 +399,10 @@ export class Pass implements PassIndexSignature { */ barcode(chosenFormat: schema.BarcodeFormat | null): this { - let { barcodes } = this._props; + let { barcodes } = this[passProps]; if (chosenFormat === null) { - delete this._props["barcode"]; + delete this[passProps]["barcode"]; return this; } @@ -423,7 +424,7 @@ export class Pass implements PassIndexSignature { return this; } - this._props["barcode"] = barcodes[index]; + this[passProps]["barcode"] = barcodes[index]; return this; } @@ -437,7 +438,7 @@ export class Pass implements PassIndexSignature { nfc(data?: schema.NFC): this | schema.NFC { if (data === undefined) { - return this._props["nfc"]; + return this[passProps]["nfc"]; } if (!(typeof data === "object" && !Array.isArray(data) && schema.isValid(data, "nfcDict"))) { @@ -445,7 +446,7 @@ export class Pass implements PassIndexSignature { return this; } - this._props["nfc"] = data; + this[passProps]["nfc"] = data; return this; } @@ -524,7 +525,7 @@ export class Pass implements PassIndexSignature { private _patch(passCoreBuffer: Buffer): Buffer { let passFile = JSON.parse(passCoreBuffer.toString()); - if (Object.keys(this._props).length) { + if (Object.keys(this[passProps]).length) { /* * We filter the existing (in passFile) and non-valid keys from * the below array keys that accept rgb values @@ -532,10 +533,10 @@ export class Pass implements PassIndexSignature { */ ["backgroundColor", "foregroundColor", "labelColor"] - .filter(v => this._props[v] && !isValidRGB(this._props[v])) - .forEach(v => delete this._props[v]); + .filter(v => this[passProps][v] && !isValidRGB(this[passProps][v])) + .forEach(v => delete this[passProps][v]); - passFile = { ...passFile, ...this._props }; + passFile = { ...passFile, ...this[passProps] }; } this._fields.forEach(field => { From a5ac1e13a4d5b31fe309ddfedb917d19bab0ec1e Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 30 Jun 2019 02:00:39 +0200 Subject: [PATCH 082/127] Added props getter to get a copy of current props --- src/pass.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 057b5f8..16fb19a 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -60,7 +60,6 @@ export class Pass implements PassIndexSignature { this.bundle = { ...options.model.bundle }; try { - // getting pass.json this.passCore = JSON.parse(this.bundle["pass.json"].toString("utf8")); } catch (err) { throw new Error(formatMessage("PASSFILE_VALIDATION_FAILED")); @@ -274,7 +273,7 @@ export class Pass implements PassIndexSignature { } return this; - } + } /** * Sets current pass' relevancy through locations @@ -295,7 +294,7 @@ export class Pass implements PassIndexSignature { } return this; - } + } /** * Sets current pass' relevancy through a date @@ -451,6 +450,10 @@ export class Pass implements PassIndexSignature { return this; } + get props(): schema.ValidPass { + return deepCopy(this[passProps]); + } + /** * Generates the PKCS #7 cryptografic signature for the manifest file. * From 25aa60ba99bb4a1087ccaf1e87da93497c9ed6a9 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 30 Jun 2019 02:01:17 +0200 Subject: [PATCH 083/127] Updated tests and barcode example to latest changes --- examples/barcode.ts | 35 +++------ spec/index.ts | 174 ++++++++++++++++++++------------------------ 2 files changed, 89 insertions(+), 120 deletions(-) diff --git a/examples/barcode.ts b/examples/barcode.ts index 526cc3a..81ae8fb 100644 --- a/examples/barcode.ts +++ b/examples/barcode.ts @@ -29,19 +29,16 @@ app.all(async function manageRequest(request, response) { overrides: request.body || request.params || request.query, }); - let bc: PassWithBarcodeMethods; - if (request.query.alt === true) { // After this, pass.props["barcodes"] will have support for all the formats // while pass.props["barcode"] will be the first of barcodes. - bc = pass.barcode("Thank you for using this package <3") as PassWithBarcodeMethods + pass.barcodes("Thank you for using this package <3"); } else { // After this, pass.props["barcodes"] will have support for just two of three - // of the passed format (the valid ones) and pass.props["barcode"] the first of barcodes. - // if not specified, altText is automatically the message + // of the passed format (the valid ones); - bc = pass.barcode({ + pass.barcodes({ message: "Thank you for using this package <3", format: "PKBarcodeFormatCode128" }, { @@ -50,29 +47,17 @@ app.all(async function manageRequest(request, response) { }, { message: "Thank you for using this package <3", format: "PKBarcodeFormatMock44617" - }) as PassWithBarcodeMethods; + }); } - // You can change the format chosen for barcode prop support by calling .backward() - // or cancel the support by calling empty .backward - // like bc.backward(). - // If the property passed does not exists, things does not change. + // You can change the format chosen for barcode prop support by calling .barcode() + // or cancel the support by calling empty .barcode + // like pass.barcode(). - bc.backward("PKBarcodeFormatPDF417"); + pass.barcode("PKBarcodeFormatPDF417"); - // If your barcode structures got not autogenerated yet (as happens with string - // parameter of barcode) you can call .autocomplete() to generate the support - // to all the structures. Please beware that this will overwrite ONLY barcodes and not barcode. - - if (!request.query.alt) { - // String generated barcode returns autocomplete as empty function - bc.autocomplete(); - } - - // @ts-ignore - ignoring for logging purposes - console.log("Barcode property is now:", pass._props["barcode"]); - // @ts-ignore - ignoring for logging purposes - console.log("Barcodes support is autocompleted:", pass._props["barcodes"]); + console.log("Barcode property is now:", pass.props["barcode"]); + console.log("Barcodes support is autocompleted:", pass.props["barcodes"]); const stream = pass.generate(); response.set({ diff --git a/spec/index.ts b/spec/index.ts index bb56934..d092e80 100644 --- a/spec/index.ts +++ b/spec/index.ts @@ -67,33 +67,28 @@ describe("Node-Passkit-generator", function () { it("Missing first argument or not a string won't apply changes", () => { // @ts-ignore -- Ignoring for test purposes pass.expiration(); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["expirationDate"]).toBe(undefined); + expect(pass.props["expirationDate"]).toBe(undefined); // @ts-ignore -- Ignoring for test purposes pass.expiration(42); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["expirationDate"]).toBe(undefined); + expect(pass.props["expirationDate"]).toBe(undefined); }); it("A date as a Date object will apply changes", () => { pass.expiration(new Date(2020,5,1,0,0,0)); // this is made to avoid problems with winter and summer time: // we focus only on the date and time for the tests. - // @ts-ignore -- Ignoring for test purposes - let noTimeZoneDateTime = pass._props["expirationDate"].split("+")[0]; + let noTimeZoneDateTime = pass.props["expirationDate"].split("+")[0]; expect(noTimeZoneDateTime).toBe("2020-06-01T00:00:00"); }); it("An invalid date, will not apply changes", () => { // @ts-ignore -- Ignoring for test purposes pass.expiration("32/18/228317"); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["expirationDate"]).toBe(undefined); + expect(pass.props["expirationDate"]).toBe(undefined); // @ts-ignore -- Ignoring for test purposes pass.expiration("32/18/228317"); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["expirationDate"]).toBe(undefined); + expect(pass.props["expirationDate"]).toBe(undefined); }); }); @@ -103,28 +98,32 @@ describe("Node-Passkit-generator", function () { pass.relevantDate(new Date("10-04-2021")); // this is made to avoid problems with winter and summer time: // we focus only on the date and time for the tests. - // @ts-ignore -- Ignoring for test purposes - let noTimeZoneDateTime = pass._props["relevantDate"].split("+")[0]; + let noTimeZoneDateTime = pass.props["relevantDate"].split("+")[0]; expect(noTimeZoneDateTime).toBe("2021-10-04T00:00:00"); }); }); describe("locations :: ", () => { it("One-Invalid-schema location won't apply changes", () => { - const oldAmountOfLocations = pass.locations().length; + const props = pass.props["locations"] || []; + const oldAmountOfLocations = props && props.length || 0; pass.locations({ // @ts-ignore "ibrupofene": "no", "longitude": 0.00000000 - }); + }, ...props); - // @ts-ignore -- Ignoring for test purposes - expect(pass.locations().length).toBe(oldAmountOfLocations); + if (oldAmountOfLocations) { + expect(pass.props["locations"].length).toBe(oldAmountOfLocations); + } else { + expect(pass.props["locations"]).toBe(undefined); + } }); it("Two locations, with one invalid, will be filtered", () => { - const oldAmountOfLocations = pass.locations().length; + const props = pass.props["locations"] || []; + const oldAmountOfLocations = props && props.length || 0; pass.locations({ //@ts-ignore @@ -133,16 +132,16 @@ describe("Node-Passkit-generator", function () { }, { "longitude": 4.42634523, "latitude": 5.344233323352 - }); + }, ...(pass.props["locations"] || [])); - // @ts-ignore -- Ignoring for test purposes - expect(pass.locations().length).toBe(oldAmountOfLocations+1); + expect(pass.props["locations"].length).toBe((oldAmountOfLocations || 0) + 1); }); }); describe("Beacons :: ", () => { it("One-Invalid-schema beacon data won't apply changes", () => { - const oldBeacons = pass.beacons(); + const props = pass.props["beacons"] || []; + const oldAmountOfBeacons = props && props.length || 0; pass.beacons({ // @ts-ignore @@ -150,14 +149,18 @@ describe("Node-Passkit-generator", function () { "major": 55, "minor": 0, "proximityUUID": "2707c5f4-deb9-48ff-b760-671bc885b6a7" - }); + }, ...props); - // @ts-ignore -- Ignoring for test purposes - expect(pass.beacons()).toBe(oldBeacons ? oldBeacons.length : undefined); + if (oldAmountOfBeacons) { + expect(pass.props["beacons"].length).toBe(oldAmountOfBeacons); + } else { + expect(pass.props["beacons"]).toBe(undefined); + } }); - it("Two beacons sets, with one invalid, will be filtered", () => { - const oldBeacons = pass.beacons(); + it("Two beacons sets, with one invalid, will be filtered out", () => { + const props = pass.props["beacons"] || []; + const oldAmountOfBeacons = props && props.length || 0; pass.beacons({ "major": 55, @@ -169,149 +172,130 @@ describe("Node-Passkit-generator", function () { "proximityUUID": "fdcbbf48-a4ae-4ffb-9200-f8a373c5c18e", // @ts-ignore "animal": "Monkey" - }); + }, ...props); - // @ts-ignore -- Ignoring for test purposes - expect(pass.beacons().length).toBe(oldBeacons ? oldBeacons.length+1 : 1); + + expect(pass.props["beacons"].length).toBe(oldAmountOfBeacons + 1); }); }); }); - describe("barcode()", () => { - it("Missing data will return the current data", () => { - const oldAmountOfBarcodes = pass.barcode().length; + describe("barcodes()", () => { + it("Missing data will left situation unchanged", () => { + const props = pass.props["barcodes"] || []; + const oldAmountOfBarcodes = props && props.length || 0; - // @ts-ignore -- Ignoring for test purposes - expect(pass.barcode().length).toBe(oldAmountOfBarcodes); + pass.barcodes(); + expect(pass.props["barcodes"].length).toBe(oldAmountOfBarcodes); }); it("Boolean parameter won't apply changes", () => { - const oldAmountOfBarcodes = pass.barcode().length; + const props = pass.props["barcodes"] || []; + const oldAmountOfBarcodes = props && props.length || 0; // @ts-ignore -- Ignoring for test purposes pass.barcode(true); - - // @ts-ignore -- Ignoring for test purposes - expect(pass.barcode().length).toBe(oldAmountOfBarcodes); + expect(props.length).toBe(oldAmountOfBarcodes); }); it("Numeric parameter won't apply changes", () => { - const oldAmountOfBarcodes = pass.barcode().length; + const props = pass.props["barcodes"] || []; + const oldAmountOfBarcodes = props && props.length || 0; // @ts-ignore -- Ignoring for test purposes - pass.barcode(42); - - // @ts-ignore -- Ignoring for test purposes - expect(pass.barcode().length).toBe(oldAmountOfBarcodes); + pass.barcodes(42); + expect(pass.props["barcodes"].length).toBe(oldAmountOfBarcodes); }); it("String parameter will autogenerate all the objects", () => { - pass.barcode("28363516282"); - - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"] instanceof Object).toBe(true); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"].message).toBe("28363516282"); - expect(pass.barcode().length).toBe(4); + pass.barcodes("28363516282"); + expect(pass.props["barcodes"].length).toBe(4); }); it("Object parameter will be accepted", () => { - pass.barcode({ + pass.barcodes({ message: "28363516282", format: "PKBarcodeFormatPDF417", messageEncoding: "utf8" }); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"] instanceof Object).toBe(true); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"].format).toBe("PKBarcodeFormatPDF417"); - // @ts-ignore -- Ignoring for test purposes - expect(pass.barcode().length).toBe(1); + expect(pass.props["barcodes"].length).toBe(1); }); it("Array parameter will apply changes", () => { - pass.barcode({ + pass.barcodes({ message: "28363516282", format: "PKBarcodeFormatPDF417", messageEncoding: "utf8" }); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"] instanceof Object).toBe(true); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"].format).toBe("PKBarcodeFormatPDF417"); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcodes"].length).toBe(1); + expect(pass.props["barcodes"].length).toBe(1); }); it("Missing messageEncoding gets automatically added.", () => { - pass.barcode({ + pass.barcodes({ message: "28363516282", format: "PKBarcodeFormatPDF417", }); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"] instanceof Object).toBe(true); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"].messageEncoding).toBe("iso-8859-1"); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcodes"][0].messageEncoding).toBe("iso-8859-1"); + expect(pass.props["barcodes"][0].messageEncoding).toBe("iso-8859-1"); }); - it("Object without message property, will be filtered out", () => { - const oldAmountOfBarcodes = pass.barcode().length; + it("Object without message property, will be ignored", () => { + const props = pass.props["barcodes"] || []; + const oldAmountOfBarcodes = props && props.length || 0; // @ts-ignore -- Ignoring for test purposes - pass.barcode({ + pass.barcodes({ format: "PKBarcodeFormatPDF417", }); - // @ts-ignore -- Ignoring for test purposes - expect(pass.barcode().length).toBe(oldAmountOfBarcodes); + expect(pass.props["barcodes"].length).toBe(oldAmountOfBarcodes); }); it("Array containing non-object elements will be rejected", () => { - const oldAmountOfBarcodes = pass.barcode().length; // @ts-ignore -- Ignoring for test purposes - pass.barcode(5, 10, 15, { + pass.barcodes(5, 10, 15, { message: "28363516282", format: "PKBarcodeFormatPDF417" }, 7, 1); - // @ts-ignore -- Ignoring for test purposes - expect(pass.barcode().length).toBe(1) + expect(pass.props["barcodes"].length).toBe(1) }); }); - describe("barcode().backward()", () => { + describe("barcode retrocompatibility", () => { it("Passing argument of type different from string or null, won't apply changes", function () { + const oldBarcode = pass.props["barcode"] || undefined; + pass - .barcode("Message-22645272183") + .barcodes("Message-22645272183") // @ts-ignore -- Ignoring for test purposes - .backward(5); + .barcode(55) // unchanged - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"].format).toBe("PKBarcodeFormatQR"); + expect(pass.props["barcode"]).toEqual(oldBarcode); }); it("Null will delete backward support", () => { - (pass.barcode("Message-22645272183") as PassWithBarcodeMethods) - .backward(null); + pass.barcodes("Message-22645272183") + .barcode("PKBarcodeFormatAztec"); - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"]).toBe(undefined); + expect(pass.props["barcode"].format).toBe("PKBarcodeFormatAztec"); + + pass.barcode(null); + expect(pass.props["barcode"]).toBe(undefined); }); it("Unknown format won't apply changes", () => { - pass - .barcode("Message-22645272183") - // @ts-ignore -- Ignoring for test purposes - .backward("PKBingoBongoFormat"); + const oldBarcode = pass.props["barcode"] || undefined; - // @ts-ignore -- Ignoring for test purposes - expect(pass._props["barcode"].format).toBe("PKBarcodeFormatQR"); + pass + .barcodes("Message-22645272183") + // @ts-ignore -- Ignoring for test purposes + .barcode("PKBingoBongoFormat"); + + expect(pass.props["barcode"]).toEqual(oldBarcode); }); }); }); From c61921721e9d0e0731ed94b7954bbf0d287018fe Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 30 Jun 2019 02:02:24 +0200 Subject: [PATCH 084/127] Added BRC_NO_POOL error --- src/messages.ts | 1 + src/pass.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/messages.ts b/src/messages.ts index 3ef035f..edf2266 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -25,6 +25,7 @@ const debugMessages: MessageGroup = { BRC_FORMATTYPE_UNMATCH: "Format must be a string or null. Cannot set backward compatibility.", BRC_AUTC_MISSING_DATA: "Unable to autogenerate barcodes. Data is not a string.", BRC_BW_FORMAT_UNSUPPORTED: "This format is not supported (by Apple) for backward support. Please choose another one.", + BRC_NO_POOL: "Cannot set barcode: no barcodes found. Please set barcodes first. Barcode is for retrocompatibility only.", DATE_FORMAT_UNMATCH: "%s was not set due to incorrect date format." }; diff --git a/src/pass.ts b/src/pass.ts index 16fb19a..6d8fc8d 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -415,6 +415,11 @@ export class Pass implements PassIndexSignature { return this; } + if (!(barcodes && barcodes.length)) { + barcodeDebug(formatMessage("BRC_NO_POOL")) + return this; + } + // Checking which object among barcodes has the same format of the specified one. const index = barcodes.findIndex(b => b.format.toLowerCase().includes(chosenFormat.toLowerCase())); From 4546774916e3d563b6ebc301d00c5b7fb6d3262e Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 30 Jun 2019 02:03:24 +0200 Subject: [PATCH 085/127] Improved how pass props are loaded from overrides and pass.json --- src/pass.ts | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 6d8fc8d..c26ca8e 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -72,29 +72,25 @@ export class Pass implements PassIndexSignature { throw new Error(formatMessage("OVV_KEYS_BADFORMAT")) } - this._props = [ + this[passProps] = { + ...( + [ "barcodes", "barcode", "expirationDate", "voided", "beacons", "locations", "relevantDate", "nfc" - ].reduce((acc, current) => { - if (!this.passCore[current]) { - return acc; - } - - acc[current] = this.passCore[current]; - return acc; - }, {}); - - if (Object.keys(validOverrides).length) { - this._props = { ...this._props, ...validOverrides }; - } + ].reduce((acc, current) => + !this.passCore.hasOwnProperty(current) && acc || + ({ ...acc, [current]: this.passCore[current] || undefined }) + , {}) + ), + ...(validOverrides || {}) + }; this.type = Object.keys(this.passCore) .find(key => /(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key)) as keyof schema.ValidPassType; if (!this.type) { - // @TODO: change error message to say it is invalid or missing throw new Error(formatMessage("NO_PASS_TYPE")); } From 1226bb21c7eeb85dac2d2e1e149dcf56b845505f Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 30 Jun 2019 02:06:55 +0200 Subject: [PATCH 086/127] Removed forgotter useless parameters and fixed nfc method to accept null; Added null to more signatures; --- src/pass.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index c26ca8e..6c3f9e2 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -75,10 +75,10 @@ export class Pass implements PassIndexSignature { this[passProps] = { ...( [ - "barcodes", "barcode", - "expirationDate", "voided", - "beacons", "locations", - "relevantDate", "nfc" + "barcodes", "barcode", + "expirationDate", "voided", + "beacons", "locations", + "relevantDate", "nfc" ].reduce((acc, current) => !this.passCore.hasOwnProperty(current) && acc || ({ ...acc, [current]: this.passCore[current] || undefined }) @@ -207,7 +207,7 @@ export class Pass implements PassIndexSignature { * @see https://apple.co/2KOv0OW - Passes support localization */ - localize(lang?: string, translations?: { [key: string]: string }): this { + localize(lang: string, translations?: { [key: string]: string }): this { if (lang && typeof lang === "string" && (typeof translations === "object" || translations === undefined)) { this.l10nTranslations[lang] = translations || {}; } @@ -223,7 +223,7 @@ export class Pass implements PassIndexSignature { * @returns {this} */ - expiration(date?: Date): this | string { + expiration(date: Date | null): this | string { if (date === null) { delete this[passProps]["expirationDate"]; return this; @@ -277,7 +277,7 @@ export class Pass implements PassIndexSignature { * @returns {Pass} */ - locations(...data: schema.Location[]): this { + locations(...data: schema.Location[] | null): this { if (data === null) { delete this[passProps]["locations"]; return this; @@ -298,7 +298,7 @@ export class Pass implements PassIndexSignature { * @returns {Pass} */ - relevantDate(date?: Date): this | string { + relevantDate(date: Date | null): this | string { if (date === null) { delete this[passProps]["relevantDate"]; return this; @@ -322,7 +322,7 @@ export class Pass implements PassIndexSignature { * @return {this} Improved this with length property and other methods */ - barcodes(first?: string | schema.Barcode, ...data: schema.Barcode[]): this { + barcodes(first: null | string | schema.Barcode, ...data: schema.Barcode[]): this { if (first === null) { delete this[passProps]["barcodes"]; return this; @@ -436,9 +436,10 @@ export class Pass implements PassIndexSignature { * @returns {this} */ - nfc(data?: schema.NFC): this | schema.NFC { - if (data === undefined) { - return this[passProps]["nfc"]; + nfc(data: schema.NFC | null): this | schema.NFC { + if (data === null) { + delete this[passProps]["nfc"]; + return this; } if (!(typeof data === "object" && !Array.isArray(data) && schema.isValid(data, "nfcDict"))) { From 35789a3d34bb8bfbcbee9ce05d909e2a771adf24 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 30 Jun 2019 02:10:12 +0200 Subject: [PATCH 087/127] Added NFC_INVALID message --- src/messages.ts | 3 ++- src/pass.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/messages.ts b/src/messages.ts index edf2266..127aff2 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -26,7 +26,8 @@ const debugMessages: MessageGroup = { 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." }; /** diff --git a/src/pass.ts b/src/pass.ts index 6c3f9e2..62ce64b 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -442,8 +442,8 @@ export class Pass implements PassIndexSignature { return this; } - if (!(typeof data === "object" && !Array.isArray(data) && schema.isValid(data, "nfcDict"))) { - genericDebug("Invalid NFC data provided"); + if (!(data && typeof data === "object" && !Array.isArray(data) && schema.isValid(data, "nfcDict"))) { + genericDebug(formatMessage("NFC_INVALID")); return this; } From 8b6f1ba948fa4b385666ec157318a4324494abf3 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 30 Jun 2019 02:25:30 +0200 Subject: [PATCH 088/127] Cleanup --- examples/barcode.ts | 1 - examples/localization.ts | 2 +- spec/index.ts | 17 ++++++-------- src/pass.ts | 50 +++++++++++++++++++--------------------- 4 files changed, 32 insertions(+), 38 deletions(-) diff --git a/examples/barcode.ts b/examples/barcode.ts index 81ae8fb..a39e84d 100644 --- a/examples/barcode.ts +++ b/examples/barcode.ts @@ -10,7 +10,6 @@ import app from "./webserver"; import { createPass } from ".."; -import { PassWithBarcodeMethods } from "../src/pass"; app.all(async function manageRequest(request, response) { const passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, ""); diff --git a/examples/localization.ts b/examples/localization.ts index 75e2523..0226fed 100644 --- a/examples/localization.ts +++ b/examples/localization.ts @@ -52,7 +52,7 @@ app.all(async function manageRequest(request, response) { pass.localize("zu", {}); // @ts-ignore - ignoring for logging purposes. Do not replicate - console.log("Added languages", pass.localize().join(", ")) + console.log("Added languages", Object.keys(pass.l10nTranslations).join(", ")) const stream = pass.generate(); response.set({ diff --git a/spec/index.ts b/spec/index.ts index d092e80..68186aa 100644 --- a/spec/index.ts +++ b/spec/index.ts @@ -1,19 +1,15 @@ import { createPass } from ".."; -import { Pass, PassWithBarcodeMethods } from "../src/pass"; -/* - * Yes, I know that I'm checking against "private" properties - * and that I shouldn't do that, but there's no other way to check - * the final results for each test. The only possible way is to - * read the generated stream of the zip file, unzip it - * (hopefully in memory) and check each property in pass.json file - * and .lproj directories. I hope who is reading this, will understand. - * +// This is used to extract the type of a Promise (like Promise => Pass) +// found here: https://medium.com/@curtistatewilkinson/this-can-be-done-using-conditional-types-like-so-633cf9787c8b +type Unpacked = T extends Promise ? U : T; + +/** * Tests created upon Jasmine testing suite. */ describe("Node-Passkit-generator", function () { - let pass: Pass; + let pass: Unpacked>; beforeEach(async () => { pass = await createPass({ model: "examples/models/examplePass.pass", @@ -185,6 +181,7 @@ describe("Node-Passkit-generator", function () { const props = pass.props["barcodes"] || []; const oldAmountOfBarcodes = props && props.length || 0; + // @ts-ignore - Ignoring for test purposes pass.barcodes(); expect(pass.props["barcodes"].length).toBe(oldAmountOfBarcodes); }); diff --git a/src/pass.ts b/src/pass.ts index 62ce64b..eec3375 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -1,8 +1,8 @@ import path from "path"; -import stream, { Stream } from "stream"; import forge from "node-forge"; -import { ZipFile } from "yazl"; import debug from "debug"; +import { Stream } from "stream"; +import { ZipFile } from "yazl"; import * as schema from "./schema"; import formatMessage from "./messages"; @@ -18,20 +18,7 @@ const genericDebug = debug("passkit:generic"); const transitType = Symbol("transitType"); const passProps = Symbol("_props"); -interface PassIndexSignature { - [key: string]: any; -} - -export interface PassWithLengthField extends Pass { - length: number; -} - -export interface PassWithBarcodeMethods extends PassWithLengthField { - backward: (format: schema.BarcodeFormat) => Pass; - autocomplete: () => Pass; -} - -export class Pass implements PassIndexSignature { +export class Pass { private bundle: schema.BundleUnit; private l10nBundles: schema.PartitionedBundle["l10nBundle"]; private _fields: (keyof schema.PassFields)[]; @@ -186,7 +173,7 @@ export class Pass implements PassIndexSignature { archive.addBuffer(signatureBuffer, "signature"); archive.addBuffer(Buffer.from(JSON.stringify(manifest)), "manifest.json"); - const passStream = new stream.PassThrough(); + const passStream = new Stream.PassThrough(); archive.outputStream.pipe(passStream); archive.end(); @@ -223,7 +210,7 @@ export class Pass implements PassIndexSignature { * @returns {this} */ - expiration(date: Date | null): this | string { + expiration(date: Date | null): this { if (date === null) { delete this[passProps]["expirationDate"]; return this; @@ -298,7 +285,7 @@ export class Pass implements PassIndexSignature { * @returns {Pass} */ - relevantDate(date: Date | null): this | string { + relevantDate(date: Date | null): this { if (date === null) { delete this[passProps]["relevantDate"]; return this; @@ -314,11 +301,13 @@ export class Pass implements PassIndexSignature { } /** - * Adds barcodes to "barcode" and "barcodes" properties. - * It will let to add the missing versions later. + * Adds barcodes "barcodes" property. + * It allows to pass a string to autogenerate all the structures. * * @method barcode - * @params data - the data to be added + * @params first - a structure or the string (message) that will generate + * all the barcodes + * @params data - other barcodes support * @return {this} Improved this with length property and other methods */ @@ -412,7 +401,7 @@ export class Pass implements PassIndexSignature { } if (!(barcodes && barcodes.length)) { - barcodeDebug(formatMessage("BRC_NO_POOL")) + barcodeDebug(formatMessage("BRC_NO_POOL")); return this; } @@ -434,9 +423,10 @@ export class Pass implements PassIndexSignature { * @method nfc * @params data - the data to be pushed in the pass * @returns {this} + * @see https://apple.co/2wTxiaC */ - nfc(data: schema.NFC | null): this | schema.NFC { + nfc(data: schema.NFC | null): this { if (data === null) { delete this[passProps]["nfc"]; return this; @@ -452,8 +442,16 @@ export class Pass implements PassIndexSignature { return this; } - get props(): schema.ValidPass { - return deepCopy(this[passProps]); + /** + * Allows to get the current inserted props; + * will return all props from valid overrides, + * template's pass.json and methods-inserted ones; + * + * @returns The properties will be inserted in the pass. + */ + + get props(): Readonly { + return this[passProps]; } /** From 60f9e8320bad708ac9cb4198bc5af02325da29ab Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 30 Jun 2019 23:28:06 +0200 Subject: [PATCH 089/127] Improved passFile props assignment --- src/pass.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pass.ts b/src/pass.ts index eec3375..7a4f02f 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -539,7 +539,7 @@ export class Pass { .filter(v => this[passProps][v] && !isValidRGB(this[passProps][v])) .forEach(v => delete this[passProps][v]); - passFile = { ...passFile, ...this[passProps] }; + Object.assign(passFile, this[passProps]); } this._fields.forEach(field => { From 5a96f5004a337a2b8b8c496e47e72dc525141f51 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Mon, 1 Jul 2019 23:48:48 +0200 Subject: [PATCH 090/127] Updated declarations --- index.d.ts | 98 +++++++++++++++++++++++++---------- package-lock.json | 128 ++++++++++++++++++++++------------------------ package.json | 14 ++--- src/schema.ts | 2 +- 4 files changed, 140 insertions(+), 102 deletions(-) diff --git a/index.d.ts b/index.d.ts index d4764ed..dfd19ff 100644 --- a/index.d.ts +++ b/index.d.ts @@ -5,7 +5,7 @@ export function createPass(options: Schema.FactoryOptions): Promise; export declare class Pass { constructor(options: Schema.PassInstance); - public transitType: "PKTransitTypeAir" | "PKTransitTypeBoat" | "PKTransitTypeBus" | "PKTransitTypeGeneric" | "PKTransitTypeTrain"; + public transitType: Schema.TransitType; public headerFields: Schema.Field[]; public primaryFields: Schema.Field[]; public secondaryFields: Schema.Field[]; @@ -32,7 +32,7 @@ export declare class Pass { * * @see https://apple.co/2KOv0OW - Passes support localization */ - localize(lang: string, translations: Object): this; + localize(lang: string, translations?: { [key: string]: string }): this; /** * Sets expirationDate property to a W3C-formatted date @@ -41,7 +41,7 @@ export declare class Pass { * @params date * @returns {this} */ - expiration(date: Date): this; + expiration(date: Date | null): this; /** * Sets voided property to true @@ -54,49 +54,65 @@ export declare class Pass { /** * Sets current pass' relevancy through beacons * @param data - * @returns Pass instance with `length` property to check the - * valid structures added + * @returns {Pass} */ - beacons(...data: Schema.Beacon[]): PassWithLengthField; + beacons(...data: Schema.Beacon[] | null): this; /** * Sets current pass' relevancy through locations * @param data - * @returns Pass instance with `length` property to check the - * valid structures added + * @returns {Pass} */ - locations(...data: Schema.Location[]): PassWithLengthField; + locations(...data: Schema.Location[] | null): this; /** * Sets current pass' relevancy through a date * @param data * @returns {Pass} */ - relevantDate(date: Date): this; + relevantDate(date: Date | null): this; /** - * Adds barcode to the pass. If data is an Object, will be treated as one-element array. - * @param first - data to be used to generate a barcode. If string, Barcode will contain structures for all the supported types. - * @param data - the other Barcode structures to be used - * @see https://apple.co/2C74kbm + * Adds barcodes "barcodes" property. + * It allows to pass a string to autogenerate all the structures. + * + * @method barcode + * @params first - a structure or the string (message) that will generate + * all the barcodes + * @params data - other barcodes support + * @return {this} Improved this with length property and other methods */ - barcode(first: string | Schema.Barcode, ...data: Schema.Barcode[]): PassWithBarcodeMethods; + barcodes(first: null | string | Schema.Barcode, ...data: Schema.Barcode[]): this; /** - * Sets nfc infos for the pass - * @param data - NFC data + * Given an index <= the amount of already set "barcodes", + * this let you choose which structure to use for retrocompatibility + * property "barcode". + * + * @method barcode + * @params format - the format to be used + * @return {this} + */ + barcode(chosenFormat: Schema.BarcodeFormat | null): this; + + /** + * Sets nfc fields in properties + * + * @method nfc + * @params data - the data to be pushed in the pass + * @returns {this} * @see https://apple.co/2wTxiaC */ - nfc(data: Schema.NFC): this; -} + nfc(data: Schema.NFC | null): this; -declare interface PassWithLengthField extends Pass { - length: number; -} - -declare interface PassWithBarcodeMethods extends PassWithLengthField { - backward: (format: Schema.BarcodeFormat | null) => Pass; - autocomplete: () => Pass; + /** + * Allows to get the current inserted props; + * will return all props from valid overrides, + * template's pass.json and methods-inserted ones; + * + * @returns The properties will be inserted in the pass. + */ + readonly props: Readonly; } declare namespace Schema { @@ -105,8 +121,8 @@ declare namespace Schema { type DateTimeStyle = "PKDateStyleNone" | "PKDateStyleShort" | "PKDateStyleMedium" | "PKDateStyleLong" | "PKDateStyleFull"; type NumberStyle = "PKNumberStyleDecimal" | "PKNumberStylePercent" | "PKNumberStyleScientific" | "PKNumberStyleSpellOut"; type BarcodeFormat = "PKBarcodeFormatQR" | "PKBarcodeFormatPDF417" | "PKBarcodeFormatAztec" | "PKBarcodeFormatCode128"; - type RelevanceType = "beacons" | "locations" | "maxDistance" | "relevantDate"; type SemanticsEventType = "PKEventTypeGeneric" | "PKEventTypeLivePerformance" | "PKEventTypeMovie" | "PKEventTypeSports" | "PKEventTypeConference" | "PKEventTypeConvention" | "PKEventTypeWorkshop" | "PKEventTypeSocialGathering"; + type TransitType = "PKTransitTypeAir" | "PKTransitTypeBoat" | "PKTransitTypeBus" | "PKTransitTypeGeneric" | "PKTransitTypeTrain"; interface Certificates { wwdr?: string; @@ -178,6 +194,34 @@ declare namespace Schema { semantics?: Semantics; } + interface PassFields { + auxiliaryFields: Field[]; + backFields: Field[]; + headerFields: Field[]; + primaryFields: Field[]; + secondaryFields: Field[]; + } + + interface ValidPassType { + boardingPass?: PassFields & { transitType: TransitType }; + eventTicket?: PassFields; + coupon?: PassFields; + generic?: PassFields; + storeCard?: PassFields; + } + + interface ValidPass extends OverridesSupportedOptions, ValidPassType { + barcode?: Barcode; + barcodes?: Barcode[]; + beacons?: Beacon[]; + locations?: Location[]; + maxDistance?: number; + relevantDate?: string; + nfc?: NFC; + expirationDate?: string; + voided?: boolean; + } + interface Beacon { major?: number; minor?: number; diff --git a/package-lock.json b/package-lock.json index 2ea2871..a65cc85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,34 +4,71 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@hapi/address": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.0.0.tgz", + "integrity": "sha512-mV6T0IYqb0xL1UALPFplXYQmR0twnXG0M6jUswpquqT2sD12BOiCiLy3EvMp/Fy7s3DZElC4/aPjEjo2jeZpvw==" + }, + "@hapi/hoek": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-6.2.4.tgz", + "integrity": "sha512-HOJ20Kc93DkDVvjwHyHawPwPkX44sIrbXazAUDiUXaY2R9JwQGo2PhFfnQtdrsIe4igjG2fPgMra7NYw7qhy0A==" + }, + "@hapi/joi": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.0.tgz", + "integrity": "sha512-n6kaRQO8S+kepUTbXL9O/UOL788Odqs38/VOfoCrATDtTvyfiO3fgjlSRaNkHabpTLgM7qru9ifqXlXbXk8SeQ==", + "requires": { + "@hapi/address": "2.x.x", + "@hapi/hoek": "6.x.x", + "@hapi/marker": "1.x.x", + "@hapi/topo": "3.x.x" + } + }, + "@hapi/marker": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@hapi/marker/-/marker-1.0.0.tgz", + "integrity": "sha512-JOfdekTXnJexfE8PyhZFyHvHjt81rBFSAbTIRAhF2vv/2Y1JzoKsGqxH/GpZJoF7aEfYok8JVcAHmSz1gkBieA==" + }, + "@hapi/topo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.0.tgz", + "integrity": "sha512-gZDI/eXOIk8kP2PkUKjWu9RW8GGVd2Hkgjxyr/S7Z+JF+0mr7bAlbw+DkTRxnD580o8Kqxlnba9wvqp5aOHBww==", + "requires": { + "@hapi/hoek": "6.x.x" + } + }, "@types/debug": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.4.tgz", "integrity": "sha512-D9MyoQFI7iP5VdpEyPZyjjqIJ8Y8EDNQFIFVLOmeg1rI1xiHOChyUPMPRUVfqFCerxfE+yS3vMyj37F6IdtOoQ==", "dev": true }, + "@types/hapi__joi": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@types/hapi__joi/-/hapi__joi-15.0.2.tgz", + "integrity": "sha512-EsOuX8cbAdSgp/9mo5NoI4vMnZ68c8Jk1fl3tyA07zd9aOq4q4udsJ2/YjhaFw0u2Zp6hBonUBrKEWotZg7PDQ==", + "dev": true, + "requires": { + "@types/hapi__joi": "*" + } + }, "@types/jasmine": { "version": "3.3.13", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.3.13.tgz", "integrity": "sha512-iczmLoIiVymaD1TIr2UctxjFkNEslVE/QtNAUmpDsD71cZfZBAsPCUv1Y+8AwsfA8bLx2ccr7d95T9w/UAirlQ==", "dev": true }, - "@types/joi": { - "version": "14.3.3", - "resolved": "https://registry.npmjs.org/@types/joi/-/joi-14.3.3.tgz", - "integrity": "sha512-6gAT/UkIzYb7zZulAbcof3lFxpiD5EI6xBeTvkL1wYN12pnFQ+y/+xl9BvnVgxkmaIDN89xWhGZLD9CvuOtZ9g==", - "dev": true - }, "@types/node": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.0.tgz", - "integrity": "sha512-Jrb/x3HT4PTJp6a4avhmJCDEVrPdqLfl3e8GGMbpkGGdwAV5UGlIs4vVEfsHHfylZVOKZWpOqmqFH8CbfOZ6kg==", + "version": "12.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.10.tgz", + "integrity": "sha512-LcsGbPomWsad6wmMNv7nBLw7YYYyfdYcz6xryKYQhx89c3XXan+8Q6AJ43G5XDIaklaVkK3mE4fCb0SBvMiPSQ==", "dev": true }, "@types/node-forge": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.8.3.tgz", - "integrity": "sha512-cDc9enmIRJdF5b3rkKsDMBhE/UrvwbDEwCYL8y9k/v7HUWPaSeK6lG2LF1SrrkqFyKPkQBTFL940YZGO+OSbaQ==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.8.4.tgz", + "integrity": "sha512-bueB7eD1EUkWaz7SW57QYor6nZSQtH0gJwfcp9kuXhdnypYy6Frnqa73LNXqX41E71ANffBK9EJX+aQH2/eTrQ==", "dev": true, "requires": { "@types/node": "*" @@ -74,9 +111,9 @@ "dev": true }, "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "requires": { "ms": "^2.1.1" } @@ -87,11 +124,6 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -108,14 +140,6 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, - "isemail": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", - "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", - "requires": { - "punycode": "2.x.x" - } - }, "jasmine": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.4.0.tgz", @@ -148,16 +172,6 @@ "integrity": "sha512-HU/YxV4i6GcmiH4duATwAbJQMlE0MsDIR5XmSVxURxKHn3aGAdbY1/ZJFmVRbKtnLwIxxMJD7gYaPsypcbYimg==", "dev": true }, - "joi": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz", - "integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==", - "requires": { - "hoek": "5.x.x", - "isemail": "3.x.x", - "topo": "3.x.x" - } - }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -173,14 +187,14 @@ "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node-forge": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", - "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==" + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz", + "integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q==" }, "once": { "version": "1.4.0", @@ -197,30 +211,10 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "topo": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz", - "integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==", - "requires": { - "hoek": "6.x.x" - }, - "dependencies": { - "hoek": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.0.3.tgz", - "integrity": "sha512-TU6RyZ/XaQCTWRLrdqZZtZqwxUVr6PDMfi6MlWNURZ7A6czanQqX4pFE1mdOUQR9FdPCsZ0UzL8jI/izZ+eBSQ==" - } - } - }, "typescript": { - "version": "3.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", - "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", + "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==", "dev": true }, "wrappy": { diff --git a/package.json b/package.json index 1db65ed..387e304 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,10 @@ "Pass" ], "dependencies": { - "debug": "^3.2.6", - "joi": "^13.7.0", + "@hapi/joi": "^15.1.0", + "debug": "^4.1.1", "moment": "^2.24.0", - "node-forge": "^0.7.6", + "node-forge": "^0.8.5", "yazl": "^2.5.1" }, "engines": { @@ -30,12 +30,12 @@ }, "devDependencies": { "@types/debug": "^4.1.4", + "@types/hapi__joi": "^15.0.2", "@types/jasmine": "^3.3.13", - "@types/joi": "^14.3.3", - "@types/node": "^12.0.0", - "@types/node-forge": "^0.8.3", + "@types/node": "^12.0.10", + "@types/node-forge": "^0.8.4", "@types/yazl": "^2.4.1", "jasmine": "^3.4.0", - "typescript": "^3.4.5" + "typescript": "^3.5.2" } } diff --git a/src/schema.ts b/src/schema.ts index 7c6ff28..f9ca5fb 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,4 +1,4 @@ -import Joi from "joi"; +import Joi from "@hapi/joi"; import debug from "debug"; const schemaDebug = debug("Schema"); From e0be1e75273b59e56f99fc14064a3918013c54ad Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Tue, 2 Jul 2019 22:05:14 +0200 Subject: [PATCH 091/127] Moved factory methods to new parser file --- src/factory.ts | 273 +------------------------------------------------ src/parser.ts | 269 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 271 insertions(+), 271 deletions(-) create mode 100644 src/parser.ts diff --git a/src/factory.ts b/src/factory.ts index 734d311..7f34274 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -1,15 +1,7 @@ import { Pass } from "./pass"; -import { Certificates, isValid, FactoryOptions, PartitionedBundle, BundleUnit, FinalCertificates } from "./schema"; - -import { promisify } from "util"; -import { readFile as _readFile, readdir as _readdir } from "fs"; -import * as path from "path"; -import forge from "node-forge"; +import { FactoryOptions } from "./schema"; import formatMessage from "./messages"; -import { removeHidden } from "./utils"; - -const readDir = promisify(_readdir); -const readFile = promisify(_readFile); +import { getModelContents, readCertificatesFromOptions } from "./parser"; export type Pass = InstanceType @@ -34,264 +26,3 @@ export async function createPass(options: FactoryOptions): Promise { throw new Error(formatMessage("CP_INIT_ERROR")); } } - -async function getModelContents(model: FactoryOptions["model"]) { - const isModelValid = ( - model && ( - typeof model === "string" || ( - typeof model === "object" && - Object.keys(model).length - ) - ) - ); - - if (!isModelValid) { - throw new Error(formatMessage("MODEL_NOT_VALID")); - } - - let modelContents: PartitionedBundle; - - if (typeof model === "string") { - modelContents = await getModelFolderContents(model); - } else { - modelContents = getModelBufferContents(model); - } - - const modelFiles = Object.keys(modelContents.bundle); - - if (!(modelFiles.includes("pass.json") && modelContents.bundle["pass.json"].length && modelFiles.some(file => Boolean(file.includes("icon") && modelContents.bundle[file].length)))) { - throw new Error("missing icon or pass.json"); - } - - return modelContents; -} - -/** - * Reads and model contents and creates a splitted - * bundles-object. - * @param model - */ - -async function getModelFolderContents(model: string): Promise { - try { - const modelPath = path.resolve(model) + (!!model && !path.extname(model) ? ".pass" : ""); - const modelFilesList = await readDir(modelPath); - - // No dot-starting files, manifest and signature - const filteredFiles = removeHidden(modelFilesList).filter(f => !/(manifest|signature)/i.test(f)); - - const isModelInitialized = ( - filteredFiles.length && - filteredFiles.some(file => file.toLowerCase().includes("icon")) - ); - - // Icon is required to proceed - if (!isModelInitialized) { - throw new Error(formatMessage( - "MODEL_UNINITIALIZED", - path.parse(this.model).name - )); - } - - // Splitting files from localization folders - const rawBundle = filteredFiles.filter(entry => !entry.includes(".lproj")); - const l10nFolders = filteredFiles.filter(entry => entry.includes(".lproj")); - - const bundleBuffers = rawBundle.map(file => readFile(path.resolve(modelPath, file))); - const buffers = await Promise.all(bundleBuffers); - - const bundle: BundleUnit = Object.assign({}, - ...rawBundle.map((fileName, index) => ({ [fileName]: buffers[index] })) - ); - - // Reading concurrently localizations folder - // and their files and their buffers - const L10N_FilesListByFolder: Array = await Promise.all( - l10nFolders.map(folderPath => { - // Reading current folder - const currentLangPath = path.join(modelPath, folderPath); - return readDir(currentLangPath) - .then(files => { - // Transforming files path to a model-relative path - const validFiles = removeHidden(files) - .map(file => path.join(currentLangPath, file)); - - // Getting all the buffers from file paths - return Promise.all([ - ...validFiles.map(file => - readFile(file).catch(() => Buffer.alloc(0)) - ) - ]).then(buffers => - // Assigning each file path to its buffer - // and discarding the empty ones - validFiles.reduce((acc, file, index) => { - if (!buffers[index].length) { - return acc; - } - - return { ...acc, [file]: buffers[index] }; - }, {}) - ); - }); - }) - ); - - const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign( - {}, - ...L10N_FilesListByFolder - .map((folder, index) => ({ [l10nFolders[index]]: folder })) - ); - - return { - bundle, - l10nBundle - }; - } catch (err) { - if (err.code && err.code === "ENOENT") { - if (err.syscall === "open") { - // file opening failed - throw new Error(formatMessage("MODELF_NOT_FOUND", err.path)) - } else if (err.syscall === "scandir") { - // directory reading failed - const pathContents = (err.path as string).split(/(\/|\\\?)/); - throw new Error(formatMessage( - "MODELF_FILE_NOT_FOUND", - pathContents[pathContents.length-1] - )) - } - } - - throw err; - } -} - -/** - * Analyzes the passed buffer model and splits it to - * return buffers and localization files buffers. - * @param model - */ - -function getModelBufferContents(model: BundleUnit): PartitionedBundle { - const rawBundle = removeHidden(Object.keys(model)).reduce((acc, current) => { - // Checking if current file is one of the autogenerated ones or if its - // content is not available - if (/(manifest|signature)/.test(current) || !rawBundle[current]) { - return acc; - } - - return { ...acc, [current]: model[current] }; - }, {}); - - const bundleKeys = Object.keys(rawBundle); - - const isModelInitialized = ( - bundleKeys.length && - bundleKeys.some(file => file.toLowerCase().includes("icon")) - ); - - // Icon is required to proceed - if (!isModelInitialized) { - throw new Error(formatMessage("MODEL_UNINITIALIZED", "Buffers")) - } - - // separing localization folders - const l10nFolders = bundleKeys.filter(file => file.includes(".lproj")); - const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign({}, - ...l10nFolders.map(folder => - ({ [folder]: rawBundle[folder] }) - ) - ); - - const bundle: BundleUnit = Object.assign({}, - ...bundleKeys - .filter(file => !file.includes(".lproj")) - .map(file => ({ [file]: rawBundle[file] })) - ); - - return { - bundle, - l10nBundle - }; -} - -/** - * Reads certificate contents, if the passed content is a path, - * and parses them as a PEM. - * @param options - */ - -async function readCertificatesFromOptions(options: Certificates): Promise { - if (!(options && Object.keys(options).length && isValid(options, "certificatesSchema"))) { - throw new Error(formatMessage("CP_NO_CERTS")); - } - - // if the signerKey is an object, we want to get - // all the real contents and don't care of passphrase - const flattenedDocs = Object.assign({}, options, { - signerKey: ( - typeof options.signerKey === "string" - ? options.signerKey - : options.signerKey.keyFile - ) - }); - - // We read the contents - const rawContentsPromises = Object.keys(flattenedDocs) - .map(key => { - const content = flattenedDocs[key]; - - if (!!path.parse(content).ext) { - // The content is a path to the document - return readFile(path.resolve(content), { encoding: "utf8"}); - } else { - // Content is the real document content - return Promise.resolve(content); - } - }); - - try { - const parsedContents = await Promise.all(rawContentsPromises); - const pemParsedContents = parsedContents.map((file, index) => { - const certName = Object.keys(options)[index]; - const pem = parsePEM( - certName, - file, - typeof options.signerKey === "object" - ? options.signerKey.passphrase - : undefined - ); - - if (!pem) { - throw new Error(formatMessage("INVALID_CERTS", certName)); - } - - return { [certName]: pem }; - }); - - return Object.assign({}, ...pemParsedContents); - } catch (err) { - if (!err.path) { - throw err; - } - - throw new Error(formatMessage("INVALID_CERT_PATH", path.parse(err.path).base)); - } -} - -/** - * Parses the PEM-formatted passed text (certificates) - * - * @param element - Text content of .pem files - * @param passphrase - passphrase for the key - * @returns The parsed certificate or key in node forge format - */ - -function parsePEM(pemName: string, element: string, passphrase?: string) { - if (pemName === "signerKey" && passphrase) { - return forge.pki.decryptRsaPrivateKey(element, String(passphrase)); - } else { - return forge.pki.certificateFromPem(element); - } -} - -module.exports = { createPass }; diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 0000000..535399d --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,269 @@ +import * as path from "path"; +import forge from "node-forge"; +import formatMessage from "./messages"; +import { FactoryOptions, PartitionedBundle, BundleUnit, Certificates, FinalCertificates, isValid } from "./schema"; +import { removeHidden } from "./utils"; +import { promisify } from "util"; +import { readFile as _readFile, readdir as _readdir } from "fs"; + +const readDir = promisify(_readdir); +const readFile = promisify(_readFile); + +export async function getModelContents(model: FactoryOptions["model"]) { + const isModelValid = ( + model && ( + typeof model === "string" || ( + typeof model === "object" && + Object.keys(model).length + ) + ) + ); + + if (!isModelValid) { + throw new Error(formatMessage("MODEL_NOT_VALID")); + } + + let modelContents: PartitionedBundle; + + if (typeof model === "string") { + modelContents = await getModelFolderContents(model); + } else { + modelContents = getModelBufferContents(model); + } + + const modelFiles = Object.keys(modelContents.bundle); + + if (!(modelFiles.includes("pass.json") && modelContents.bundle["pass.json"].length && modelFiles.some(file => Boolean(file.includes("icon") && modelContents.bundle[file].length)))) { + throw new Error("missing icon or pass.json"); + } + + return modelContents; +} + +/** + * Reads and model contents and creates a splitted + * bundles-object. + * @param model + */ + +export async function getModelFolderContents(model: string): Promise { + try { + const modelPath = path.resolve(model) + (!!model && !path.extname(model) ? ".pass" : ""); + const modelFilesList = await readDir(modelPath); + + // No dot-starting files, manifest and signature + const filteredFiles = removeHidden(modelFilesList).filter(f => !/(manifest|signature)/i.test(f)); + + const isModelInitialized = ( + filteredFiles.length && + filteredFiles.some(file => file.toLowerCase().includes("icon")) + ); + + // Icon is required to proceed + if (!isModelInitialized) { + throw new Error(formatMessage( + "MODEL_UNINITIALIZED", + path.parse(this.model).name + )); + } + + // Splitting files from localization folders + const rawBundle = filteredFiles.filter(entry => !entry.includes(".lproj")); + const l10nFolders = filteredFiles.filter(entry => entry.includes(".lproj")); + + const bundleBuffers = rawBundle.map(file => readFile(path.resolve(modelPath, file))); + const buffers = await Promise.all(bundleBuffers); + + const bundle: BundleUnit = Object.assign({}, + ...rawBundle.map((fileName, index) => ({ [fileName]: buffers[index] })) + ); + + // Reading concurrently localizations folder + // and their files and their buffers + const L10N_FilesListByFolder: Array = await Promise.all( + l10nFolders.map(folderPath => { + // Reading current folder + const currentLangPath = path.join(modelPath, folderPath); + return readDir(currentLangPath) + .then(files => { + // Transforming files path to a model-relative path + const validFiles = removeHidden(files) + .map(file => path.join(currentLangPath, file)); + + // Getting all the buffers from file paths + return Promise.all([ + ...validFiles.map(file => + readFile(file).catch(() => Buffer.alloc(0)) + ) + ]).then(buffers => + // Assigning each file path to its buffer + // and discarding the empty ones + validFiles.reduce((acc, file, index) => { + if (!buffers[index].length) { + return acc; + } + + return { ...acc, [file]: buffers[index] }; + }, {}) + ); + }); + }) + ); + + const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign( + {}, + ...L10N_FilesListByFolder + .map((folder, index) => ({ [l10nFolders[index]]: folder })) + ); + + return { + bundle, + l10nBundle + }; + } catch (err) { + if (err.code && err.code === "ENOENT") { + if (err.syscall === "open") { + // file opening failed + throw new Error(formatMessage("MODELF_NOT_FOUND", err.path)) + } else if (err.syscall === "scandir") { + // directory reading failed + const pathContents = (err.path as string).split(/(\/|\\\?)/); + throw new Error(formatMessage( + "MODELF_FILE_NOT_FOUND", + pathContents[pathContents.length-1] + )) + } + } + + throw err; + } +} + +/** + * Analyzes the passed buffer model and splits it to + * return buffers and localization files buffers. + * @param model + */ + +export function getModelBufferContents(model: BundleUnit): PartitionedBundle { + const rawBundle = removeHidden(Object.keys(model)).reduce((acc, current) => { + // Checking if current file is one of the autogenerated ones or if its + // content is not available + if (/(manifest|signature)/.test(current) || !rawBundle[current]) { + return acc; + } + + return { ...acc, [current]: model[current] }; + }, {}); + + const bundleKeys = Object.keys(rawBundle); + + const isModelInitialized = ( + bundleKeys.length && + bundleKeys.some(file => file.toLowerCase().includes("icon")) + ); + + // Icon is required to proceed + if (!isModelInitialized) { + throw new Error(formatMessage("MODEL_UNINITIALIZED", "Buffers")) + } + + // separing localization folders + const l10nFolders = bundleKeys.filter(file => file.includes(".lproj")); + const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign({}, + ...l10nFolders.map(folder => + ({ [folder]: rawBundle[folder] }) + ) + ); + + const bundle: BundleUnit = Object.assign({}, + ...bundleKeys + .filter(file => !file.includes(".lproj")) + .map(file => ({ [file]: rawBundle[file] })) + ); + + return { + bundle, + l10nBundle + }; +} + +/** + * Reads certificate contents, if the passed content is a path, + * and parses them as a PEM. + * @param options + */ + +export async function readCertificatesFromOptions(options: Certificates): Promise { + if (!(options && Object.keys(options).length && isValid(options, "certificatesSchema"))) { + throw new Error(formatMessage("CP_NO_CERTS")); + } + + // if the signerKey is an object, we want to get + // all the real contents and don't care of passphrase + const flattenedDocs = Object.assign({}, options, { + signerKey: ( + typeof options.signerKey === "string" + ? options.signerKey + : options.signerKey.keyFile + ) + }); + + // We read the contents + const rawContentsPromises = Object.keys(flattenedDocs) + .map(key => { + const content = flattenedDocs[key]; + + if (!!path.parse(content).ext) { + // The content is a path to the document + return readFile(path.resolve(content), { encoding: "utf8"}); + } else { + // Content is the real document content + return Promise.resolve(content); + } + }); + + try { + const parsedContents = await Promise.all(rawContentsPromises); + const pemParsedContents = parsedContents.map((file, index) => { + const certName = Object.keys(options)[index]; + const pem = parsePEM( + certName, + file, + typeof options.signerKey === "object" + ? options.signerKey.passphrase + : undefined + ); + + if (!pem) { + throw new Error(formatMessage("INVALID_CERTS", certName)); + } + + return { [certName]: pem }; + }); + + return Object.assign({}, ...pemParsedContents); + } catch (err) { + if (!err.path) { + throw err; + } + + throw new Error(formatMessage("INVALID_CERT_PATH", path.parse(err.path).base)); + } +} + +/** + * Parses the PEM-formatted passed text (certificates) + * + * @param element - Text content of .pem files + * @param passphrase - passphrase for the key + * @returns The parsed certificate or key in node forge format + */ + +function parsePEM(pemName: string, element: string, passphrase?: string) { + if (pemName === "signerKey" && passphrase) { + return forge.pki.decryptRsaPrivateKey(element, String(passphrase)); + } else { + return forge.pki.certificateFromPem(element); + } +} From f9c7686a5e7686532dbee369047d33cd75d244d5 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 7 Jul 2019 03:00:40 +0200 Subject: [PATCH 092/127] Added schema-validation for props in pass.json before importing; Added to props also the other props of pass.json --- src/pass.ts | 69 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/src/pass.ts b/src/pass.ts index 7a4f02f..a006f82 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -18,6 +18,14 @@ const genericDebug = debug("passkit:generic"); const transitType = Symbol("transitType"); const passProps = Symbol("_props"); +const propsSchemaMap = new Map([ + ["barcodes", "barcode"], + ["barcode", "barcode"], + ["beacons", "beaconsDict"], + ["locations", "locationsDict"], + ["nfc", "nfcDict"] +]); + export class Pass { private bundle: schema.BundleUnit; private l10nBundles: schema.PartitionedBundle["l10nBundle"]; @@ -59,21 +67,6 @@ export class Pass { throw new Error(formatMessage("OVV_KEYS_BADFORMAT")) } - this[passProps] = { - ...( - [ - "barcodes", "barcode", - "expirationDate", "voided", - "beacons", "locations", - "relevantDate", "nfc" - ].reduce((acc, current) => - !this.passCore.hasOwnProperty(current) && acc || - ({ ...acc, [current]: this.passCore[current] || undefined }) - , {}) - ), - ...(validOverrides || {}) - }; - this.type = Object.keys(this.passCore) .find(key => /(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key)) as keyof schema.ValidPassType; @@ -81,6 +74,42 @@ export class Pass { throw new Error(formatMessage("NO_PASS_TYPE")); } + // Parsing and validating pass.json keys + const validatedPassKeys = Object.keys(this.passCore).reduce((acc, current) => { + if (this.type === current) { + // We want to exclude type keys (eventTicket, + // boardingPass, ecc.) and their content + return acc; + } + + if (!propsSchemaMap.has(current)) { + // If the property is unknown (we don't care if + // it is valid or not for Wallet), we return + // directly the content + return { ...acc, [current]: this.passCore[current] }; + } + + const currentSchema = propsSchemaMap.get(current); + + if (Array.isArray(this.passCore[current])) { + const valid = getValidInArray(currentSchema, this.passCore[current]); + return { ...acc, [current]: valid }; + } else { + return { + ...acc, + [current]: schema.isValid( + this.passCore[current], + currentSchema + ) && this.passCore[current] || undefined + }; + } + }, {}); + + this[passProps] = { + ...(validatedPassKeys || {}), + ...(validOverrides || {}) + }; + if (this.type === "boardingPass" && this.passCore[this.type]["transitType"]) { // We might want to generate a boarding pass without setting manually // in the code the transit type but right in the model; @@ -592,13 +621,11 @@ function barcodesFromUncompleteData(message: string): schema.Barcode[] { } function processRelevancySet(key: string, data: T[]): T[] { - return data.reduce((acc, current) => { - if (!(Object.keys(current).length && schema.isValid(current, `${key}Dict`))) { - return acc; - } + return getValidInArray(`${key}Dict`, data); +} - return [...acc, current]; - }, []); +function getValidInArray(schemaName: string, contents: T[]): T[] { + return contents.filter(current => Object.keys(current).length && schema.isValid(current, schemaName)); } function processDate(key: string, date: Date): string | null { From 2c882f17d8e34e1435dd670e778dc7c8399501af Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 7 Jul 2019 03:01:29 +0200 Subject: [PATCH 093/127] Updated API and README documents --- API.md | 362 ++++++++++++++++++++++++++++++++---------------------- README.md | 86 +++++++------ 2 files changed, 260 insertions(+), 188 deletions(-) diff --git a/API.md b/API.md index a65d555..22e6d07 100644 --- a/API.md +++ b/API.md @@ -34,16 +34,19 @@ ___ * [Localizing Passes](#localizing_passes) * [.localize()](#method_localize) * Setting barcode + * [.barcodes()](#method_barcodes) * [.barcode()](#method_barcode) - * [.backward()](#method_bBackward) - * [.autocomplete()](#method_bAutocomplete) * Setting expiration / voiding the pass * [.expiration()](#method_expiration) * [.void()](#method_void) * Setting relevance - * [.relevance()](#method_relevance) + * [.beacons()](#method_beacons) + * [.locations()](#method_locations) + * [.relevantDate()][#method_revdate] * Setting NFC * [.nfc()](#method_nfc) + * Getting the current information + * [.props](#getter_props) * [Setting Pass Structure Keys (primaryFields, secondaryFields, ...)](#prop_fields) * [TransitType](#prop_transitType) * Generating the compiled pass. @@ -56,20 +59,20 @@ ___ #### constructor() -```javascript -var pass = new Pass(options); +```typescript +const pass = await createPass({ ... }); ``` **Returns**: -`Object` +`Promise` **Arguments**: | Key | Type | Description | Optional | Default Value | |-----|------|---------------|:-------------:|:-----------:| | options | Object | The options to create the pass | false | - -| options.model | String/Path | The model path to be used to generate a new model. | false | - +| options.model | String/Path/Buffer Object | The model path or a Buffer Object with path as key and Buffer as content | false | - | options.certificates | Object | The certificate object containing the paths to certs files. | false | - | options.certificates.wwdr | String/Path | The path to Apple WWDR certificate or its content. | false | - | options.certificates.signerCert | String/Path | The path to Developer certificate file or its content. | false | - @@ -77,7 +80,6 @@ var pass = new Pass(options); | options.certificates.signerKey.keyFile | String/Path | The path to developer certificate key or its content. | false | - | options.certificates.signerKey.passphrase | String \| Number | The passphrase to use to unlock the key. | false | - | options.overrides | Object | Dictionary containing all the keys you can override in the pass.json file and does not have a method to get overridden. | true | { } -| options.shouldOverwrite | Boolean | Setting this property to false, will make properties in `overrides` and fields to be pushed along with the ones added through methods to the existing ones in pass.json. | true | true

@@ -90,6 +92,7 @@ Following Apple Developer Documentation, localization (L10N) is done by creating In this library, localization can be done in three ways: **media-only** (images), **translations-only** or both. The only differences stands in the way the only method below is used and how the model is designed. +If this method is used for translations and the model already contains a `pass.strings` for the specified language, the translations will be appended to that file. > If you are designing your pass for a language only, you can directly replace the placeholders in `pass.json` with translation. @@ -98,8 +101,8 @@ The only differences stands in the way the only method below is used and how the #### .localize() -```javascript -pass.localize(lang, options); +```typescript +pass.localize(lang: string, options = {}); ``` **Returns**: @@ -119,7 +122,7 @@ In the other two cases, you'll need to specify also the second argument (the tra | Key | Type | Description | Optional | Default Value | |-----|------|-------------|----------|:-------------:| | lang | String | The ISO-3166-1 language code | false | - -| options | Object | Translations in format PLACEHOLDER : TRANSLATED-VALUE. | true | undefined \| { } +| options | Object | Translations in format `{ : "TRANSLATED-VALUE"}`. | true | undefined \| { } **Example**: @@ -142,72 +145,66 @@ ___ **Setting barcodes**: ___ + + +#### .barcodes() + +```typescript +pass.barcodes(first: string | schema.Barcode, ...data: schema.Barcodes[]) : this; +``` + +**Returns**: + +`Object (this)` + +**Description**: + +Setting barcodes can happen in two ways: `controlled` and `uncontrolled` (autogenerated), which mean how many [barcode structures](https://apple.co/2myAbst) you will have in your pass. + +Passing a `string` to the method, will lead to an `uncontrolled` way: starting from the message (content), all the structures will be generated. Any further parameter will be ignored. + +Passing *N* barcode structures (see below), will only validate them and push only the valid ones. + +This method will not take take of setting retro-compatibility, of which responsability is assigned to `.barcode()`. + +**Arguments**: + +| Key | Type | Description | Optional | +|-------|------|-------------|----------| +| first | `String` \| `schema.Barcode` | first value of barcodes | false +| ...data | `schema.Barcode[]` | the other barcode values | true + +**Examples**: + +```typescript +pass.barcodes("11424771526"); + +// or + +pass.barcodes({ + message: "11424771526", + format: "PKBarcodeFormatCode128" + altText: "11424771526" +}, { + message: "11424771526", + format: "PKBarcodeFormatQR" + altText: "11424771526" +}, { + message: "11424771526", + format: "PKBarcodeFormatPDF417" + altText: "11424771526" +}); +``` + +**See**: [PassKit Package Format Reference # Barcode Dictionary](https://apple.co/2myAbst) +
+
#### .barcode() ```javascript -pass.barcode(data); -``` - -**Returns**: - -`Improved Object (this with some "private" methods available to be called under aliases, as below)` - -**Description**: - -Each object in `data` will be filtered against a schema ([Apple reference](https://apple.co/2myAbst)) validation and used if correctly formed. - -If the argument is an Object, it will be treated as one-element Array. - -If the argument is a String or an Object with `format` parameter missing, but `message` available, the structure will be **autogenerated** complete of all the fallbacks (4 dictionaries). - -To support versions prior to iOS 9, `barcode` key is automatically supported as the first valid value of the provided (or generated) barcode. To change the key, see below. - -**Arguments**: - -| Key | Type | Description | Optional | Default Value | -|-----|------|-------------|----------|:-------------:| -| data | String \| Array\ \| Object | Data to be used in the barcode | false | - - -**Examples**: - -```javascript - pass.barcode("11424771526"); - - // or - - pass.barcode({ - message: "11424771526", - format: "PKBarcodeFormatCode128" - altText: "11424771526" - }); - - // or - - pass.barcode([{ - message: "11424771526", - format: "PKBarcodeFormatCode128" - altText: "11424771526" - }, { - message: "11424771526", - format: "PKBarcodeFormatQR" - altText: "11424771526" - }, { - message: "11424771526", - format: "PKBarcodeFormatPDF417" - altText: "11424771526" - }]); -``` - -**See**: [PassKit Package Format Reference # Barcode Dictionary](https://apple.co/2myAbst) -
- - -#### .barcode().backward() - -```javascript -pass.barcode(data).backward(format); +pass.barcode(data: string); ``` **Returns**: @@ -217,44 +214,30 @@ pass.barcode(data).backward(format); **Description**: It will let you choose the format to be used in barcode property as backward compatibility. -Also it will work only if `data` is provided to `barcode()` method and will fail if the selected format is not found among barcodes dictionaries array. +Also it will work only if `barcodes()` method has already been called or if the current properties already have at least one barcode structure in it and if it matches with the specified one. + +`PKBarcodeFormatCode128` is not supported in barcode. Therefore any attempt to set it, will fail. **Arguments**: | Key | Type | Description | Optional | Default Value | |-----|------|-------------|----------|:-------------:| -| format | String | Format to be used. Must be one of these types: *PKBarcodeFormatQR*, *PKBarcodeFormatPDF417*, *PKBarcodeFormatAztec* | false | - +| format | String | Format to be used. Must be one of these types: `PKBarcodeFormatQR`, `PKBarcodeFormatPDF417`, `PKBarcodeFormatAztec` | false | - **Example**: ```javascript -// Based on the previous example +// Based on the previous (barcodes) example pass - .barcode(...) - .backward("PKBarcodeFormatQR"); + .barcodes(...) + .barcode("PKBarcodeFormatQR"); // This won't set the property since not found. pass - .barcode(...) - .backward("PKBarcodeFormatAztec"); + .barcodes(...) + .barcode("PKBarcodeFormatAztec"); ``` -
- - -#### .barcode().autocomplete() - -```javascript -pass.barcode(data).autocomplete(); -``` - -**Returns**: - -`Improved Object ("this" with backward() support and length prop. reporting how many structs have been added).` - -**Description**: - -It will generate all the barcodes fallback starting from the first dictionary in `barcodes`.

___ @@ -266,8 +249,8 @@ ___ #### .expiration() -```javascript -pass.expiration(date [, format]); +```typescript +pass.expiration(date: Date) : this; ``` **Returns**: @@ -276,22 +259,18 @@ pass.expiration(date [, format]); **Description**: -It sets the date of expiration to the passed argument. The date will be automatically parsed in order in the following formats: - -* **MM-DD-YYYY hh:mm:ss**, -* **DD-MM-YYYY hh:mm:ss**. - -Otherwise you can specify a personal format to use. - -Seconds are not optionals. +It sets the date of expiration to the passed argument. If the parsing fails, the error will be emitted only in debug mode and the property won't be set. +Passing `null` as the parameter, will remove the value. **Arguments**: -| Key | Type | Description | Optional | Default Value | -|-----|------|-------------|----------|:-------------:| -| date | String/date | The date on which the pass will expire | false | - -| format | String | A custom format to be used to parse the date | true | undefined +| Key | Type | Description | Optional | +|-----|------|-------------|----------| +| date | String/date | The date on which the pass will expire | false + +
+
@@ -307,7 +286,7 @@ pass.void(); **Description**: -It sets directly the pass as voided (void: true). +It sets directly the pass as voided.

___ @@ -315,53 +294,107 @@ ___ **Setting relevance**: ___ - + -#### .relevance() +#### .beacons() -```javascript -pass.relevance(key, value [, relevanceDateFormat]); +```typescript +pass.beacons(...data: schema.Beacons[]): this; ``` **Returns**: -`Improved Object (this with length property)` +`Object (this)` **Description**: -It sets the relevance key in the pass among four: **beacons**, **locations**, **relevantDate** and **maxDistance**. -See [Apple Documentation dedicated page](https://apple.co/2QiE9Ds) for more. - -For the first two keys, the argument 'value' (which will be of type **Array\**) will be checked and filtered against dedicated schema. - -For *relevantDate*, the date is parsed in the same formats of [#expiration()](#method_expiration). For *maxDistance*, the value is simply converted as Number and pushed only with successful conversion. - +Sets the beacons information in the passes. +If other beacons structures are available in the structure, they will be overwritten. +Passing `null` as parameter, will remove the content. **Arguments**: | Key | Type | Description | Optional | Default Value | |-----|------|-------------|----------|:-------------:| -| key | String | The relevance key to be set, among **beacons**, **locations**, **relevantDate** and **maxDistance** | false | - -| value | String \| Number \| Array\ | Type depends on the key. Please refer to the description above for more details | false | - -| relevanceDateFormat | String | Custom date format. Will be only used when using `relevanceDate` key | true | undefined +| ...data | [schema.Beacons[]](https://apple.co/2XPDoYX) \| `null` | The beacons structures | false | - **Example**: -```javascript -pass.relevance("location", [{ - longitude: "73.2943532945212", - latitude: "-42.3088613015625", -]); - -pass.relevance("maxDistance", 150); - -// DD-MM-YYYY -> April, 10th 2021 -pass.relevance("relevantDate", "10/04/2021", "DD-MM-YYYY"); - -// MM-DD-YYYY -> October, 4th 2021 -pass.relevance("relevantDate", "10/04/2021"); +```typescript +pass.beacons({ + "major": 55, + "minor": 0, + "proximityUUID": "59da0f96-3fb5-43aa-9028-2bc796c3d0c5" +}, { + "major": 65, + "minor": 46, + "proximityUUID": "fdcbbf48-a4ae-4ffb-9200-f8a373c5c18e", +}); ``` +
+
+ + + +#### .locations() + +```typescript +pass.locations(...data: schema.Locations[]): this; +``` + +**Returns**: + +`Object (this)` + +**Description**: + +Sets the location-relevance information in the passes. +If other location structures are available in the structure, they will be overwritten. +Passing `null` as parameter, will remove its content; + +**Arguments**: + +| Key | Type | Description | Optional | Default Value | +|-----|------|-------------|----------|:-------------:| +| ...data | [schema.Locations[]](https://apple.co/2LE00VZ) \| `null` | The location structures | false | - + +**Example**: + +```typescript +pass.locations({ + "latitude": 66.45725212, + "longitude": 33.010004420 +}, { + "longitude": 4.42634523, + "latitude": 5.344233323352 +}); +``` +
+
+ + + +#### .relevantDate() + +```typescript +pass.relevantDate(date: Date): this; +``` + +**Returns**: + +`Object (this)` + +**Description**: + +Sets the relevant date for the current pass. Passing `null` to the parameter, will remove its content. + +**Arguments**: + +| Key | Type | Description | Optional | Default Value | +|-----|------|-------------|----------|:-------------:| +| date | Date \| `null` | The relevant date | false | - +

___ @@ -372,8 +405,8 @@ ___ #### .nfc() -```javascript -pass.nfc([data, ...]) +```typescript +pass.nfc(data: schema.NFC): this ``` **Returns**: @@ -382,23 +415,54 @@ pass.nfc([data, ...]) **Description**: -It sets the property for nfc dictionary. -An Object as argument will be treated as one-element array. +It sets NFC info for the current pass. Passing `null` as parameter, will remove its value. >*Notice*: **I had the possibility to test in no way this pass feature and, therefore, the implementation. If you need it and this won't work, feel free to contact me and we will investigate together 😄** **Arguments**: -| Key | Type | Description | Optional | Default Value | -|-----|------|-------------|----------|:-------------:| -| data | Array\ \| Object | The data regarding to be used for nfc | false | - +| Key | Type | Description | Optional | +|-----|------|-------------|----------| +| data | [schema.NFC](https://apple.co/2XrXwMr) \| `null` | NFC structure | false **See**: [PassKit Package Format Reference # NFC](https://apple.co/2wTxiaC) -

-___ -**Getting remote resources**: -___ +

+
+ + + +#### .props() + +```typescript +pass.props; +``` + +**Returns**: + +An object containing all the current props; + +**Description**: + +This is a getter: a way to access to the current props before generating a pass. In here are available the props set both from pass.json reading and this package methods usage, along with the valid overrides passed to `createPass`. The keys are the same used in pass.json. + +It does not contain fields content (`primaryFields`, `secondaryFields`...) and `transitType`, which are still accessible through their own props. + +**Example**: + +```typescript +const currentLocations = pass.props["locations"]; +pass.locations({ + "latitude": 66.45725212, + "longitude": 33.010004420 +}, { + "longitude": 4.42634523, + "latitude": 5.344233323352 +}, +...currentLocations); +``` + +

___ diff --git a/README.md b/README.md index 5597c45..6d7d64e 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,12 @@ ### Architecture -This package was created with a specific architecture in mind: **application** and **model**, to split as much as possible static objects (such as logo, background, icon, etc.) from dynamic ones (translations, barcodes, serialNumber, ...). +This package was created with a specific architecture in mind: **application** and **model** (as preprocessed entity), to split as much as possible static objects (such as logo, background, icon, etc.) from dynamic ones (translations, barcodes, serialNumber, ...). -Actually, pass creation and population doesn't fully happen within the application in runtime. Pass template is a folder in, for example, _your application directory_ (but nothing will stop you from putting it outside), that will contain all the objects needed (static medias) and structure to make a pass work. +Pass creation and population doesn't fully happen in runtime. Pass template (model) can be one of a set of buffers or a folder, that will contain all the objects needed (static medias) and structure to make a pass work. -Pass template will be read and pushed as is in the resulting .zip file, while dynamic objects will be patched against `pass.json` or generated in runtime (`manifest.json`, `signature` and translation files). +Both Pass template will be read and pushed as they are in the resulting .zip file, while dynamic objects will be patched against `pass.json` or generated in runtime (`manifest.json`, `signature` and translation files). +All the static medias from both sources, will be read and pushed as they are in the resulting .zip file; dynamic object will be patched against `pass.json`, generated on runtime (`manifest.json`, `signature`) or merged if already existing (translation files). This package comes with an [API documentation](./API.md), that makes available a series of methods to customize passes. @@ -35,14 +36,18 @@ ___ ##### Model -The first thing you'll have to do, is to start creating a model. A model is a folder in your project directory, with inside the basic pass infos, like the thumbnails, the icon, and the background and **pass.json** containing all the static infos about the pass, like Team identifier, Pass type identifier, colors, etc. +The first thing you'll have to do, is to start creating a model. A model will contain all the basic pass infos, like the thumbnails, the icon, and the background and **pass.json** containing all the static infos about the pass, like Team identifier, Pass type identifier, colors, etc. + +If starting from scratch, the preferred solution is to use the folder as model, as it will allow you to access easily all the files. Also, a buffer model is mainly designed for models that are ready to be used in your application. + +Let's suppose you have a file `model.zip` stored somewhere: you unzip it in runtime and then get the access to its files as buffers. Those buffers should be available for the rest of your application run-time and you shouldn't be in need to read them every time you are going to create a pass. ___ > Using the .pass extension is a best practice, showing that the directory is a pass package. > ([Build your first pass - Apple Developer Portal](https://apple.co/2LYXWo3)). -Following to this best practice, the package is set to require each model to have a **_.pass_** extension. -If the extension is not specified in the configuration (as in [Usage Example](#usage_example), at "model" key), it will be added forcefully. +Following to this best practice, the package is set to require each folder-model to have a **_.pass_** extension. +If omitted in the configuration (as in [Usage Example](#usage_example), at "model" key), it will be forcefully added. ___ @@ -55,9 +60,11 @@ Follow the [Apple Developer documentation](https://apple.co/2wuJLC1) (_Package S You can also create `.lproj` folders (e.g. *en.lproj* or *it.lproj*) containing localized media. To include a folder or translate texts inside the pass, please refer to [Localizing Passes](./API.md#method_localize) in the API documentation. +To include a file that belongs to an `.lproj` folder in buffers, you'll just have to name a key like `en.lproj/thumbnail.png`. + ##### Pass.json -Create a `pass.json` by taking example from examples folder models or the one provided by Apple for the [first tutorial](https://apple.co/2NA2nus) and fill it with the basic informations, that is `teamIdentifier`, `passTypeIdentifier` and all the other basic keys like pass type. Please refer to [Top-Level Keys/Standard Keys](https://apple.co/2PRfSnu) and [Top-Level Keys/Style Keys](https://apple.co/2wzyL5J). +Create a `pass.json` by taking example from examples folder models or the one provided by Apple for the [first tutorial](https://apple.co/2NA2nus) and fill it with the basic informations, that are `teamIdentifier`, `passTypeIdentifier` and all the other basic keys like pass type. Please refer to [Top-Level Keys/Standard Keys](https://apple.co/2PRfSnu) and [Top-Level Keys/Style Keys](https://apple.co/2wzyL5J). ```json { @@ -113,40 +120,41 @@ ___ ## Usage example -```javascript -const { Pass } = require("passkit-generator"); +```typescript +const { createPass, Pass } = require("passkit-generator"); +// or, for typescript +import { createPass, Pass } from "passkit-generator"; -let examplePass = new Pass({ - model: "./passModels/myFirstModel", - certificates: { - wwdr: "./certs/wwdr.pem", - signerCert: "./certs/signercert.pem", - signerKey: { - keyFile: "./certs/signerkey.pem", - passphrase: "123456" +let examplePass: Pass; + +try { + examplePass = await createPass({ + model: "./passModels/myFirstModel", + certificates: { + wwdr: "./certs/wwdr.pem", + signerCert: "./certs/signercert.pem", + signerKey: { + keyFile: "./certs/signerkey.pem", + passphrase: "123456" + } + }, + overrides: { + // keys to be added or overridden + serialNumber: "AAGH44625236dddaffbda" } - }, - overrides: { - // keys to be added or overridden - serialNumber: "AAGH44625236dddaffbda" - }, - // if true, existing keys added through methods get overwritten - // pushed in queue otherwise. - shouldOverwrite: true -}); - -// Adding some settings to be written inside pass.json -examplePass.localize("en", { ... }); -examplePass.barcode("36478105430"); // Random value - -// Generate the stream, which gets returned through a Promise -examplePass.generate() - .then(stream => { - doSomethingWithTheStream(stream); - }) - .catch(err => { - doSomethingWithTheError(err); }); + + // Adding some settings to be written inside pass.json + examplePass.localize("en", { ... }); + examplePass.barcode("36478105430"); // Random value + + // Generate the stream, which gets returned through a Promise + const stream: Stream = examplePass.generate(); + + doSomethingWithTheStream(stream); +} catch (err) { + doSomethingWithTheError(err); +} ``` ___ @@ -157,7 +165,7 @@ If you used this package in any of your projects, feel free to open a topic in i The idea to develop this package, was born during the Apple Developer Academy 17/18, in Naples, Italy, driven by the need to create an iOS app component regarding passes generation for events. -A big thanks to all the people and friends in the Apple Developer Academy (and not) that pushed me and helped me into realizing something like this and a big thanks to the ones that helped me to make technical choices. +A big thanks to all the people and friends in the Apple Developer Academy (and not) that pushed me and helped me into realizing something like this and a big thanks to the ones that helped me to make technical choices and to all the contributors. Any contribution, is welcome. Made with ❤️ in Italy. From d2c97a0d6d4a829a92d5392778dc10c10f97a94c Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 7 Jul 2019 15:08:00 +0200 Subject: [PATCH 094/127] Added beta notice --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 6d7d64e..34e3645 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,16 @@ This package comes with an [API documentation](./API.md), that makes available a > ⚠ Do not rely on branches outside "master", as might not be stable and will be removed once merged. +
+ +> ⚠⚠ Please notice this is a beta. It is not yet available publicly on NPM. Therefore you'll have to build through **typescript** it before using it: + +
+ +```sh +$ npm run build +``` + ### Install ```sh $ npm install passkit-generator --save From d4f67d9b126f3287110b01d757526b66bcee5dc7 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sun, 7 Jul 2019 16:15:39 +0200 Subject: [PATCH 095/127] Update API.md Generic improvements and fixes to API Document --- API.md | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/API.md b/API.md index 22e6d07..3d88106 100644 --- a/API.md +++ b/API.md @@ -42,7 +42,7 @@ ___ * Setting relevance * [.beacons()](#method_beacons) * [.locations()](#method_locations) - * [.relevantDate()][#method_revdate] + * [.relevantDate()](#method_revdate) * Setting NFC * [.nfc()](#method_nfc) * Getting the current information @@ -204,7 +204,7 @@ pass.barcodes({ #### .barcode() ```javascript -pass.barcode(data: string); +pass.barcode(chosenFormat: string): this; ``` **Returns**: @@ -498,7 +498,7 @@ pass.primaryFields.pop(); #### .transitType -```javascript +```typescript pass.transitType = "PKTransitTypeAir"; ``` @@ -522,26 +522,21 @@ As you can see in [examples folder](/examples), to send a .pkpass file, a basic #### .generate() -```javascript -pass.generate(); +```typescript +pass.generate(): Stream; ``` -**Returns**: `Promise` +**Returns**: `Stream` **Description**: -The returned Promise will contain a stream or an error. +Creates a pass zip as Stream. **Examples**: -```javascript -pass.generate() - .then(stream => { - doSomethingWithPassStream(); - }) - .catch(error => { - doSomethingWithThrownError(); - }); +```typescript +const passStream = pass.generate(); +doSomethingWithPassStream(stream); ``` ___ From 6252f4d6a4f88a333b61d576ef6ca5d3ce0ef99a Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Tue, 9 Jul 2019 23:44:36 +0200 Subject: [PATCH 096/127] Added support for additional buffers --- API.md | 21 +++++++++++---------- src/factory.ts | 25 +++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/API.md b/API.md index 3d88106..c881aa7 100644 --- a/API.md +++ b/API.md @@ -60,7 +60,7 @@ ___ #### constructor() ```typescript -const pass = await createPass({ ... }); +const pass = await createPass({ ... }, Buffer.from([ ... ])); ``` **Returns**: @@ -71,15 +71,16 @@ const pass = await createPass({ ... }); | Key | Type | Description | Optional | Default Value | |-----|------|---------------|:-------------:|:-----------:| -| options | Object | The options to create the pass | false | - -| options.model | String/Path/Buffer Object | The model path or a Buffer Object with path as key and Buffer as content | false | - -| options.certificates | Object | The certificate object containing the paths to certs files. | false | - -| options.certificates.wwdr | String/Path | The path to Apple WWDR certificate or its content. | false | - -| options.certificates.signerCert | String/Path | The path to Developer certificate file or its content. | false | - -| options.certificates.signerKey | Object/String | The object containing developer certificate's key and passphrase. If string, it can be the path to the key file or its content. If object, must have `keyFile` key and might need `passphrase`. | false | - -| options.certificates.signerKey.keyFile | String/Path | The path to developer certificate key or its content. | false | - -| options.certificates.signerKey.passphrase | String \| Number | The passphrase to use to unlock the key. | false | - -| options.overrides | Object | Dictionary containing all the keys you can override in the pass.json file and does not have a method to get overridden. | true | { } +| options | Object | The options to create the pass | `false` | - +| options.model | String \| Path \| Buffer Object | The model path or a Buffer Object with path as key and Buffer as content | `false` | - +| options.certificates | Object | The certificate object containing the paths to certs files. | `false` | - +| options.certificates.wwdr | String \| Path | The path to Apple WWDR certificate or its content. | `false` | - +| options.certificates.signerCert | String \| Path | The path to Developer certificate file or its content. | `false` | - +| options.certificates.signerKey | Object/String | The object containing developer certificate's key and passphrase. If string, it can be the path to the key file or its content. If object, must have `keyFile` key and might need `passphrase`. | `false` | - +| options.certificates.signerKey.keyFile | String \| Path | The path to developer certificate key or its content. | `false` | - +| options.certificates.signerKey.passphrase | String \| Number | The passphrase to use to unlock the key. | `false` | - +| options.overrides | Object | Dictionary containing all the keys you can override in the pass.json file and does not have a method to get overridden. | `true` | `{ }` +| additionalBuffers | Buffer Object | Dictionary with path as key and Buffer as a content. Each will represent a file to be added to the final model. These will have priority on model ones | `true` | `{ }`

diff --git a/src/factory.ts b/src/factory.ts index 7f34274..5949758 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -1,11 +1,11 @@ import { Pass } from "./pass"; -import { FactoryOptions } from "./schema"; +import { FactoryOptions, BundleUnit } from "./schema"; import formatMessage from "./messages"; import { getModelContents, readCertificatesFromOptions } from "./parser"; export type Pass = InstanceType -export async function createPass(options: FactoryOptions): Promise { +export async function createPass(options: FactoryOptions, additionalBuffers: BundleUnit): Promise { if (!(options && Object.keys(options).length)) { throw new Error(formatMessage("CP_NO_OPTS")); } @@ -16,6 +16,12 @@ export async function createPass(options: FactoryOptions): Promise { readCertificatesFromOptions(options.certificates) ]); + if (additionalBuffers) { + const [ additionalL10n, additionalBundle ] = splitBundle(additionalBuffers); + Object.assign(bundle["l10nBundle"], additionalL10n); + Object.assign(bundle["bundle"], additionalBundle); + } + return new Pass({ model: bundle, certificates, @@ -26,3 +32,18 @@ export async function createPass(options: FactoryOptions): Promise { throw new Error(formatMessage("CP_INIT_ERROR")); } } + +/** + * Applies a partition to split one bundle + * to two + * @param origin + */ + +function splitBundle(origin: Object): [BundleUnit, BundleUnit] { + const keys = Object.keys(origin); + return keys.reduce(([ l10n, bundle ], current) => + current.includes(".lproj") && + [ { ...l10n, [current]: origin[current] }, bundle] || + [ l10n, {...bundle, [current]: origin[current] }] + , [{},{}]); +} From 6d39139dc7969db306abf15a9f2b37c44b8aec70 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Tue, 9 Jul 2019 23:55:21 +0200 Subject: [PATCH 097/127] Removed left console.log --- src/factory.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/factory.ts b/src/factory.ts index 5949758..af6b4d4 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -28,7 +28,6 @@ export async function createPass(options: FactoryOptions, additionalBuffers: Bun overrides: options.overrides }); } catch (err) { - console.log(err); throw new Error(formatMessage("CP_INIT_ERROR")); } } From f4dabb42b0b66032055152d57a2065cfab2a4911 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Wed, 10 Jul 2019 00:24:53 +0200 Subject: [PATCH 098/127] Moved splitBundle to utils and set optional to additionalBuffers --- src/factory.ts | 20 +++----------------- src/utils.ts | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index af6b4d4..8dd8378 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -2,10 +2,11 @@ import { Pass } from "./pass"; import { FactoryOptions, BundleUnit } from "./schema"; import formatMessage from "./messages"; import { getModelContents, readCertificatesFromOptions } from "./parser"; +import { splitBundle } from "./utils"; export type Pass = InstanceType -export async function createPass(options: FactoryOptions, additionalBuffers: BundleUnit): Promise { +export async function createPass(options: FactoryOptions, additionalBuffers?: BundleUnit): Promise { if (!(options && Object.keys(options).length)) { throw new Error(formatMessage("CP_NO_OPTS")); } @@ -28,21 +29,6 @@ export async function createPass(options: FactoryOptions, additionalBuffers: Bun overrides: options.overrides }); } catch (err) { - throw new Error(formatMessage("CP_INIT_ERROR")); + throw new Error(formatMessage("CP_INIT_ERROR", err)); } } - -/** - * Applies a partition to split one bundle - * to two - * @param origin - */ - -function splitBundle(origin: Object): [BundleUnit, BundleUnit] { - const keys = Object.keys(origin); - return keys.reduce(([ l10n, bundle ], current) => - current.includes(".lproj") && - [ { ...l10n, [current]: origin[current] }, bundle] || - [ l10n, {...bundle, [current]: origin[current] }] - , [{},{}]); -} diff --git a/src/utils.ts b/src/utils.ts index 2872955..2968bbf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,6 @@ import moment from "moment"; import { EOL } from "os"; +import { BundleUnit } from "./schema"; /** * Checks if an rgb value is compliant with CSS-like syntax @@ -80,3 +81,18 @@ export function generateStringFile(lang: { [index: string]: string }): Buffer { return Buffer.from(strings.join(EOL), "utf8"); } + +/** + * Applies a partition to split one bundle + * to two + * @param origin + */ + +export function splitBundle(origin: Object): [BundleUnit, BundleUnit] { + const keys = Object.keys(origin); + return keys.reduce(([ l10n, bundle ], current) => + current.includes(".lproj") && + [ { ...l10n, [current]: origin[current] }, bundle] || + [ l10n, {...bundle, [current]: origin[current] }] + , [{},{}]); +} From 29a90d7dc2bd232fd4dcd23ee68bb0a36e3bb2ce Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Wed, 10 Jul 2019 00:25:07 +0200 Subject: [PATCH 099/127] Improved CP_INIT_ERROR --- src/messages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/messages.ts b/src/messages.ts index 127aff2..df7b43b 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -3,7 +3,7 @@ interface MessageGroup { } const errors: MessageGroup = { - CP_INIT_ERROR: "Something went really bad in the initialization, dude! Please look at the log above this message. It should contain all the infos about the problem.", + CP_INIT_ERROR: "Something went really bad in the initialization, dude! Please look at the log below this message. It should contain all the infos about the problem: \n%s", CP_NO_OPTS: "Cannot initialize the pass creation: no options were passed.", CP_NO_CERTS: "Cannot initialize the pass creation: no valid certificates were passed.", PASSFILE_VALIDATION_FAILED: "Validation of pass type failed. Pass file is not a valid buffer or (more probably) does not respect the schema.\nRefer to https://apple.co/2Nvshvn to build a correct pass.", From ef4ce53dd619d922a14377aa3e8a8880dffca995 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 11 Jul 2019 23:27:20 +0200 Subject: [PATCH 100/127] Fixed problem with buffer models --- src/parser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/parser.ts b/src/parser.ts index 535399d..bfae2ff 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -149,7 +149,8 @@ export function getModelBufferContents(model: BundleUnit): PartitionedBundle { const rawBundle = removeHidden(Object.keys(model)).reduce((acc, current) => { // Checking if current file is one of the autogenerated ones or if its // content is not available - if (/(manifest|signature)/.test(current) || !rawBundle[current]) { + + if (/(manifest|signature)/.test(current) || !model[current]) { return acc; } From 92a48eccbec710346ad228afb6198c5dd95b8011 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 11 Jul 2019 23:29:23 +0200 Subject: [PATCH 101/127] Renamed splitBundle to splitBufferBundle and edited it to return splitted l10n/rootBundle object --- src/factory.ts | 4 ++-- src/utils.ts | 23 ++++++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 8dd8378..2043c54 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -2,7 +2,7 @@ import { Pass } from "./pass"; import { FactoryOptions, BundleUnit } from "./schema"; import formatMessage from "./messages"; import { getModelContents, readCertificatesFromOptions } from "./parser"; -import { splitBundle } from "./utils"; +import { splitBufferBundle } from "./utils"; export type Pass = InstanceType @@ -18,7 +18,7 @@ export async function createPass(options: FactoryOptions, additionalBuffers?: Bu ]); if (additionalBuffers) { - const [ additionalL10n, additionalBundle ] = splitBundle(additionalBuffers); + const [ additionalL10n, additionalBundle ] = splitBufferBundle(additionalBuffers); Object.assign(bundle["l10nBundle"], additionalL10n); Object.assign(bundle["bundle"], additionalBundle); } diff --git a/src/utils.ts b/src/utils.ts index 2968bbf..9f3f528 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,7 @@ import moment from "moment"; import { EOL } from "os"; -import { BundleUnit } from "./schema"; +import { PartitionedBundle } from "./schema"; +import { sep } from "path"; /** * Checks if an rgb value is compliant with CSS-like syntax @@ -88,11 +89,19 @@ export function generateStringFile(lang: { [index: string]: string }): Buffer { * @param origin */ -export function splitBundle(origin: Object): [BundleUnit, BundleUnit] { +export function splitBufferBundle(origin: Object): [PartitionedBundle["l10nBundle"], PartitionedBundle["bundle"]] { const keys = Object.keys(origin); - return keys.reduce(([ l10n, bundle ], current) => - current.includes(".lproj") && - [ { ...l10n, [current]: origin[current] }, bundle] || - [ l10n, {...bundle, [current]: origin[current] }] - , [{},{}]); + return keys.reduce(([ l10n, bundle ], current) => { + if (current.includes(".lproj")) { + const pathComponents = current.split(sep); + const lang = pathComponents[0]; + const file = pathComponents.slice(1).join("/"); + + (l10n[lang] || (l10n[lang] = {}))[file] = origin[current]; + + return [ l10n, bundle ]; + } else { + return [ l10n, { ...bundle, [current]: origin[current] }]; + } + }, [{},{}]); } From 6ce2b9ab22b58e69683ff87cd7158e16e737d969 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 11 Jul 2019 23:30:43 +0200 Subject: [PATCH 102/127] Optimized Buffer model content splitting through splitBufferBundle --- src/parser.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index bfae2ff..0ba4912 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -2,7 +2,7 @@ import * as path from "path"; import forge from "node-forge"; import formatMessage from "./messages"; import { FactoryOptions, PartitionedBundle, BundleUnit, Certificates, FinalCertificates, isValid } from "./schema"; -import { removeHidden } from "./utils"; +import { removeHidden, splitBufferBundle } from "./utils"; import { promisify } from "util"; import { readFile as _readFile, readdir as _readdir } from "fs"; @@ -169,19 +169,8 @@ export function getModelBufferContents(model: BundleUnit): PartitionedBundle { throw new Error(formatMessage("MODEL_UNINITIALIZED", "Buffers")) } - // separing localization folders - const l10nFolders = bundleKeys.filter(file => file.includes(".lproj")); - const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign({}, - ...l10nFolders.map(folder => - ({ [folder]: rawBundle[folder] }) - ) - ); - - const bundle: BundleUnit = Object.assign({}, - ...bundleKeys - .filter(file => !file.includes(".lproj")) - .map(file => ({ [file]: rawBundle[file] })) - ); + // separing localization folders from bundle files + const [ l10nBundle, bundle ] = splitBufferBundle(rawBundle); return { bundle, From 4ec86415bfb768f2bbfb388bfd67d8decdaee850 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 11 Jul 2019 23:31:48 +0200 Subject: [PATCH 103/127] Fixed problem with localization files path/fileNames --- src/parser.ts | 10 +++++++--- src/pass.ts | 17 +++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 0ba4912..6e5a6e3 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -48,11 +48,12 @@ export async function getModelContents(model: FactoryOptions["model"]) { export async function getModelFolderContents(model: string): Promise { try { - const modelPath = path.resolve(model) + (!!model && !path.extname(model) ? ".pass" : ""); + const modelPath = model + (!path.extname(model) && ".pass"); const modelFilesList = await readDir(modelPath); // No dot-starting files, manifest and signature - const filteredFiles = removeHidden(modelFilesList).filter(f => !/(manifest|signature)/i.test(f)); + const filteredFiles = removeHidden(modelFilesList) + .filter(f => !/(manifest|signature)/i.test(f) && /.+$/.test(path.parse(f).ext)); const isModelInitialized = ( filteredFiles.length && @@ -103,7 +104,10 @@ export async function getModelFolderContents(model: string): Promise { const strings = generateStringFile(this.l10nTranslations[lang]); + const langInBundles = `${lang}.lproj`; if (strings.length) { /** @@ -153,17 +154,17 @@ export class Pass { * it otherwise. */ - if (!this.l10nBundles[lang]) { - this.l10nBundles[lang] = {}; + if (!this.l10nBundles[langInBundles]) { + this.l10nBundles[langInBundles] = {}; } - this.l10nBundles[lang]["pass.strings"] = Buffer.concat([ - this.l10nBundles[lang]["pass.strings"] || Buffer.alloc(0), + this.l10nBundles[langInBundles]["pass.strings"] = Buffer.concat([ + this.l10nBundles[langInBundles]["pass.strings"] || Buffer.alloc(0), strings ]); } - if (!(this.l10nBundles[lang] && Object.keys(this.l10nBundles[lang]).length)) { + if (!(this.l10nBundles[langInBundles] && Object.keys(this.l10nBundles[langInBundles]).length)) { return; } @@ -175,10 +176,10 @@ export class Pass { * composition. */ - Object.assign(finalBundle, ...Object.keys(this.l10nBundles[lang]) + Object.assign(finalBundle, ...Object.keys(this.l10nBundles[langInBundles]) .map(fileName => { - const fullPath = path.join(`${lang}.lproj`, fileName).replace(/\\/, "/"); - return { [fullPath]: this.l10nBundles[lang][fileName] }; + const fullPath = path.join(langInBundles, fileName).replace(/\\/, "/"); + return { [fullPath]: this.l10nBundles[langInBundles][fileName] }; }) ); }); From 1036b553c85e731734a7c955e1d8953f09ba0c07 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 13 Jul 2019 00:04:47 +0200 Subject: [PATCH 104/127] Added german language folder to examplePass --- .../models/examplePass.pass/de.lproj/icon.png | Bin 0 -> 4573 bytes .../examplePass.pass/de.lproj/icon@2x.png | Bin 0 -> 9008 bytes .../examplePass.pass/de.lproj/thumbnail.png | Bin 0 -> 29674 bytes .../examplePass.pass/de.lproj/thumbnail@2x.png | Bin 0 -> 108585 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/models/examplePass.pass/de.lproj/icon.png create mode 100644 examples/models/examplePass.pass/de.lproj/icon@2x.png create mode 100644 examples/models/examplePass.pass/de.lproj/thumbnail.png create mode 100644 examples/models/examplePass.pass/de.lproj/thumbnail@2x.png diff --git a/examples/models/examplePass.pass/de.lproj/icon.png b/examples/models/examplePass.pass/de.lproj/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a7b90c9625b535ae1c2a2947ae5842a53f87e604 GIT binary patch literal 4573 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 Hu0mjfCO@4m literal 0 HcmV?d00001 diff --git a/examples/models/examplePass.pass/de.lproj/icon@2x.png b/examples/models/examplePass.pass/de.lproj/icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9f44f21d0ca9a863dfb6dbda26065cc6e84899cf GIT binary patch literal 9008 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 literal 0 HcmV?d00001 diff --git a/examples/models/examplePass.pass/de.lproj/thumbnail@2x.png b/examples/models/examplePass.pass/de.lproj/thumbnail@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..dea4491b405f92b6a6ea3cc5ee04cdacf0445b60 GIT binary patch literal 108585 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 z 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 xPcXk)Nf)H%vcFSO5 Date: Thu, 25 Jul 2019 21:41:16 +0200 Subject: [PATCH 105/127] Small improvements and fixes --- src/parser.ts | 6 ++++++ src/pass.ts | 7 ++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 6e5a6e3..169a731 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -9,6 +9,12 @@ import { readFile as _readFile, readdir as _readdir } from "fs"; const readDir = promisify(_readdir); const readFile = promisify(_readFile); +/** + * Performs checks on the passed model to + * determine how to parse it + * @param model + */ + export async function getModelContents(model: FactoryOptions["model"]) { const isModelValid = ( model && ( diff --git a/src/pass.ts b/src/pass.ts index 75dabc6..d74da1b 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -7,10 +7,7 @@ import { ZipFile } from "yazl"; import * as schema from "./schema"; import formatMessage from "./messages"; import FieldsArray from "./fieldsArray"; -import { - generateStringFile, - dateToW3CString, isValidRGB -} from "./utils"; +import { generateStringFile, dateToW3CString, isValidRGB } from "./utils"; const barcodeDebug = debug("passkit:barcode"); const genericDebug = debug("passkit:generic"); @@ -321,7 +318,7 @@ export class Pass { return this; } - const parsedDate = processDate("relevandDate", date); + const parsedDate = processDate("relevantDate", date); if (parsedDate) { this[passProps]["relevantDate"] = parsedDate; From 693694a9ebb8e74d0f3e3264039a79f02f58da80 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 25 Jul 2019 22:09:27 +0200 Subject: [PATCH 106/127] Improved modelPath composition --- src/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser.ts b/src/parser.ts index 169a731..26fb96b 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -54,7 +54,7 @@ export async function getModelContents(model: FactoryOptions["model"]) { export async function getModelFolderContents(model: string): Promise { try { - const modelPath = model + (!path.extname(model) && ".pass"); + const modelPath = `${model}${!path.extname(model) && ".pass" || ""}`; const modelFilesList = await readDir(modelPath); // No dot-starting files, manifest and signature From 8bd7978e0b23335b53c4b10ecfce299bf5b51173 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 25 Jul 2019 23:06:52 +0200 Subject: [PATCH 107/127] Examples improvements --- examples/barcode.ts | 2 +- examples/package-lock.json | 60 ++++++++++++++++++++++++++++++++++++++ examples/package.json | 4 +++ examples/webserver.ts | 2 +- 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/examples/barcode.ts b/examples/barcode.ts index a39e84d..211c7cf 100644 --- a/examples/barcode.ts +++ b/examples/barcode.ts @@ -1,5 +1,5 @@ /** - * .barcode(), autocomplete and backward() methods example + * .barcode() and .barcodes() methods example * Here we set the barcode. To see all the results, you can * both unzip .pkpass file or check the properties before * generating the whole bundle diff --git a/examples/package-lock.json b/examples/package-lock.json index 037320f..aee85ab 100644 --- a/examples/package-lock.json +++ b/examples/package-lock.json @@ -4,6 +4,66 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/body-parser": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.0.tgz", + "integrity": "sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.16.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.7.tgz", + "integrity": "sha512-847KvL8Q1y3TtFLRTXcVakErLJQgdpFSaq+k043xefz9raEf0C7HalpSY7OW5PyjCnY8P7bPW5t/Co9qqp+USg==", + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" + }, + "@types/node": { + "version": "12.6.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.8.tgz", + "integrity": "sha512-aX+gFgA5GHcDi89KG5keey2zf0WfZk/HAQotEamsK2kbey+8yGKcson0hbK8E+v0NArlCJQCqMP161YhV6ZXLg==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", diff --git a/examples/package.json b/examples/package.json index 796b1c7..5129247 100644 --- a/examples/package.json +++ b/examples/package.json @@ -4,7 +4,11 @@ "description": "Passkit-generator examples", "author": "Alexander P. Cerutti ", "license": "ISC", + "scripts": { + "build": "cd ..; npm run build; cd examples" + }, "dependencies": { + "@types/express": "^4.17.0", "express": "^4.17.1" } } diff --git a/examples/webserver.ts b/examples/webserver.ts index a951e58..01141b9 100644 --- a/examples/webserver.ts +++ b/examples/webserver.ts @@ -5,7 +5,7 @@ */ import express from "express"; -const app = express(); +export const app = express(); app.use(express.json()); From 040e4a891ff1e9f76ab4e1873e6d0e2ab020b2d2 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 25 Jul 2019 23:07:53 +0200 Subject: [PATCH 108/127] Added implementation of abstract models --- examples/abstractModel.ts | 160 ++++++++++++++++++++++++++++++++++++++ src/abstract.ts | 63 +++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 examples/abstractModel.ts create mode 100644 src/abstract.ts diff --git a/examples/abstractModel.ts b/examples/abstractModel.ts new file mode 100644 index 0000000..cdf2dbb --- /dev/null +++ b/examples/abstractModel.ts @@ -0,0 +1,160 @@ +import genRoute, { app } from "./webserver"; +import { createPass, createAbstractModel, AbstractModel } from ".."; + +let abstractModel: AbstractModel; + +(async () => { + abstractModel = await createAbstractModel({ + model: `./models/exampleBooking.pass`, + certificates: { + wwdr: "../certificates/WWDR.pem", + signerCert: "../certificates/signerCert.pem", + signerKey: { + keyFile: "../certificates/signerKey.pem", + passphrase: "123456" + } + }, + // overrides: request.body || request.params || request.query, + }); +})(); + +genRoute.all(async function manageRequest(request, response) { + const passName = request.params.modelName + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, ""); + + try { + const pass = await createPass(abstractModel); + + pass.transitType = "PKTransitTypeAir"; + + pass.headerFields.push({ + "key": "header1", + "label": "Data", + "value": "25 mag", + "textAlignment": "PKTextAlignmentCenter" + }, { + "key": "header2", + "label": "Volo", + "value": "EZY997", + "textAlignment": "PKTextAlignmentCenter" + }); + + pass.primaryFields.push({ + key: "IATA-source", + value: "NAP", + label: "Napoli", + textAlignment: "PKTextAlignmentLeft" + }, { + key: "IATA-destination", + value: "VCE", + label: "Venezia Marco Polo", + textAlignment: "PKTextAlignmentRight" + }); + + pass.secondaryFields.push({ + "key": "secondary1", + "label": "Imbarco chiuso", + "value": "18:40", + "textAlignment": "PKTextAlignmentCenter", + }, { + "key": "sec2", + "label": "Partenze", + "value": "19:10", + "textAlignment": "PKTextAlignmentCenter" + }, { + "key": "sec3", + "label": "SB", + "value": "Sì", + "textAlignment": "PKTextAlignmentCenter" + }, { + "key": "sec4", + "label": "Imbarco", + "value": "Anteriore", + "textAlignment": "PKTextAlignmentCenter" + }); + + pass.auxiliaryFields.push({ + "key": "aux1", + "label": "Passeggero", + "value": "MR. WHO KNOWS", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "aux2", + "label": "Posto", + "value": "1A*", + "textAlignment": "PKTextAlignmentCenter" + }); + + pass.backFields.push({ + "key": "document number", + "label": "Numero documento:", + "value": "- -", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "You're checked in, what next", + "label": "Hai effettuato il check-in, Quali sono le prospettive", + "value": "", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "Check In", + "label": "1. check-in✓", + "value": "", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "checkIn", + "label": "", + "value": "Le uscite d'imbarco chiudono 30 minuti prima della partenza, quindi sii puntuale. In questo aeroporto puoi utilizzare la corsia Fast Track ai varchi di sicurezza.", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "2. Bags", + "label": "2. Bagaglio", + "value": "", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "Require special assistance", + "label": "Assistenza speciale", + "value": "Se hai richiesto assistenza speciale, presentati a un membro del personale nell'area di Consegna bagagli almeno 90 minuti prima del volo.", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "3. Departures", + "label": "3. Partenze", + "value": "", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "photoId", + "label": "Un documento d’identità corredato di fotografia", + "value": "è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta d’identità.", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "yourSeat", + "label": "Il tuo posto:", + "value": "verifica il tuo numero di posto nella parte superiore. Durante l’imbarco utilizza le scale anteriori e posteriori: per le file 1-10 imbarcati dalla parte anteriore; per le file 11-31 imbarcati dalla parte posteriore. Colloca le borse di dimensioni ridotte sotto il sedile davanti a te.", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "Pack safely", + "label": "Bagaglio sicuro", + "value": "Fai clic http://easyjet.com/it/articoli-pericolosi per maggiori informazioni sulle merci pericolose oppure visita il sito CAA http://www.caa.co.uk/default.aspx?catid=2200", + "textAlignment": "PKTextAlignmentLeft" + }, { + "key": "Thank you for travelling easyJet", + "label": "Grazie per aver viaggiato con easyJet", + "value": "", + "textAlignment": "PKTextAlignmentLeft" + }); + + const stream = pass.generate(); + response.set({ + "Content-type": "application/vnd.apple.pkpass", + "Content-disposition": `attachment; filename=${passName}.pkpass` + }); + + stream.pipe(response); + } catch(err) { + console.log(err); + + response.set({ + "Content-type": "text/html" + }); + + response.send(err.message); + } +}); diff --git a/src/abstract.ts b/src/abstract.ts new file mode 100644 index 0000000..7f76d15 --- /dev/null +++ b/src/abstract.ts @@ -0,0 +1,63 @@ +import { Certificates, FinalCertificates, PartitionedBundle, OverridesSupportedOptions, FactoryOptions } from "./schema"; +import { getModelContents, readCertificatesFromOptions } from "./parser"; +import formatMessage from "./messages"; + +const abmCertificates = Symbol("certificates"); +const abmModel = Symbol("model"); +const abmOverrides = Symbol("overrides"); + +export interface AbstractFactoryOptions extends Omit { + certificates?: Certificates; +} + +interface AbstractModelOptions { + bundle: PartitionedBundle; + certificates: FinalCertificates; + overrides?: OverridesSupportedOptions; +} + +export async function createAbstractModel(options: AbstractFactoryOptions) { + if (!(options && Object.keys(options).length)) { + throw new Error(formatMessage("CP_NO_OPTS")); + } + + try { + const [bundle, certificates] = await Promise.all([ + getModelContents(options.model), + readCertificatesFromOptions(options.certificates) + ]); + + return new AbstractModel({ + bundle, + certificates, + overrides: options.overrides + }); + } catch (err) { + console.log(err); + throw new Error(formatMessage("CP_INIT_ERROR", "abstract model", err)); + } +} + +export class AbstractModel { + private [abmCertificates]: FinalCertificates; + private [abmModel]: PartitionedBundle; + private [abmOverrides]: OverridesSupportedOptions; + + constructor(options: AbstractModelOptions) { + this[abmModel] = options.bundle; + this[abmCertificates] = options.certificates, + this[abmOverrides] = options.overrides + } + + get certificates(): FinalCertificates { + return this[abmCertificates]; + } + + get bundle(): PartitionedBundle { + return this[abmModel]; + } + + get overrides(): OverridesSupportedOptions { + return this[abmOverrides]; + } +} From 9321ccc4ba639ca1c21a64a3175f907ec33714f3 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 25 Jul 2019 23:08:21 +0200 Subject: [PATCH 109/127] Improved types, messages and exported objects --- index.ts | 8 +++++++- src/messages.ts | 4 ++-- src/schema.ts | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/index.ts b/index.ts index 527fdbc..c5a60fa 100644 --- a/index.ts +++ b/index.ts @@ -1 +1,7 @@ -export { createPass, Pass } from "./src/factory"; +import { Pass } from "./src/pass"; +import { AbstractModel } from "./src/abstract"; + +export { createPass } from "./src/factory"; +export { createAbstractModel } from "./src/abstract"; +export type Pass = InstanceType +export type AbstractModel = InstanceType diff --git a/src/messages.ts b/src/messages.ts index df7b43b..e05d175 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -3,8 +3,8 @@ interface MessageGroup { } const errors: MessageGroup = { - CP_INIT_ERROR: "Something went really bad in the initialization, dude! Please look at the log below this message. It should contain all the infos about the problem: \n%s", - CP_NO_OPTS: "Cannot initialize the pass creation: no options were passed.", + CP_INIT_ERROR: "Something went really bad in the %s initialization! Look at the log below this message. It should contain all the infos about the problem: \n%s", + CP_NO_OPTS: "Cannot initialize the pass or abstract model creation: no options were passed.", CP_NO_CERTS: "Cannot initialize the pass creation: no valid certificates were passed.", PASSFILE_VALIDATION_FAILED: "Validation of pass type failed. Pass file is not a valid buffer or (more probably) does not respect the schema.\nRefer to https://apple.co/2Nvshvn to build a correct pass.", REQUIR_VALID_FAILED: "The options passed to Pass constructor does not meet the requirements.\nRefer to the documentation to compile them correctly.", diff --git a/src/schema.ts b/src/schema.ts index f9ca5fb..fd1d6a1 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -15,7 +15,7 @@ export interface Certificates { export interface FactoryOptions { model: BundleUnit | string; certificates: Certificates; - overrides?: Object; + overrides?: OverridesSupportedOptions; } export interface BundleUnit { From b44d8d764fbff4a85557babdf27ad01183cfb804 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 25 Jul 2019 23:08:48 +0200 Subject: [PATCH 110/127] Added createPass support to AbstractModel --- src/factory.ts | 71 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 2043c54..988b4e9 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -1,34 +1,65 @@ import { Pass } from "./pass"; -import { FactoryOptions, BundleUnit } from "./schema"; +import { FactoryOptions, BundleUnit, FinalCertificates } from "./schema"; import formatMessage from "./messages"; import { getModelContents, readCertificatesFromOptions } from "./parser"; import { splitBufferBundle } from "./utils"; +import { AbstractModel, AbstractFactoryOptions } from "./abstract"; -export type Pass = InstanceType - -export async function createPass(options: FactoryOptions, additionalBuffers?: BundleUnit): Promise { - if (!(options && Object.keys(options).length)) { +export async function createPass( + options: FactoryOptions | AbstractModel, + additionalBuffers?: BundleUnit, + abstractMissingData?: Omit +): Promise { + if (!(options && (options instanceof AbstractModel || Object.keys(options).length))) { throw new Error(formatMessage("CP_NO_OPTS")); } try { - const [bundle, certificates] = await Promise.all([ - getModelContents(options.model), - readCertificatesFromOptions(options.certificates) - ]); + if (options instanceof AbstractModel) { + let certificates: FinalCertificates; - if (additionalBuffers) { - const [ additionalL10n, additionalBundle ] = splitBufferBundle(additionalBuffers); - Object.assign(bundle["l10nBundle"], additionalL10n); - Object.assign(bundle["bundle"], additionalBundle); + if (!(options.certificates && options.certificates.signerCert && options.certificates.signerKey) && abstractMissingData.certificates) { + certificates = Object.assign( + options.certificates, + await readCertificatesFromOptions(abstractMissingData.certificates) + ); + } else { + certificates = options.certificates; + } + + if (additionalBuffers) { + const [ additionalL10n, additionalBundle ] = splitBufferBundle(additionalBuffers); + Object.assign(options.bundle["l10nBundle"], additionalL10n); + Object.assign(options.bundle["bundle"], additionalBundle); + } + + return new Pass({ + model: options.bundle, + certificates: certificates, + overrides: { + ...(options.overrides || {}), + ...(abstractMissingData && abstractMissingData.overrides || {}) + } + }); + } else { + const [bundle, certificates] = await Promise.all([ + getModelContents(options.model), + readCertificatesFromOptions(options.certificates) + ]); + + if (additionalBuffers) { + const [ additionalL10n, additionalBundle ] = splitBufferBundle(additionalBuffers); + Object.assign(bundle["l10nBundle"], additionalL10n); + Object.assign(bundle["bundle"], additionalBundle); + } + + return new Pass({ + model: bundle, + certificates, + overrides: options.overrides + }); } - - return new Pass({ - model: bundle, - certificates, - overrides: options.overrides - }); } catch (err) { - throw new Error(formatMessage("CP_INIT_ERROR", err)); + throw new Error(formatMessage("CP_INIT_ERROR", "pass", err)); } } From 920651beeab7304c440294b211d1ea39b776c5ab Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 25 Jul 2019 23:47:10 +0200 Subject: [PATCH 111/127] Updated API docs --- API.md | 108 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 94 insertions(+), 14 deletions(-) diff --git a/API.md b/API.md index c881aa7..5dfdacc 100644 --- a/API.md +++ b/API.md @@ -30,7 +30,7 @@ ___ ### Index: -* [Instance](#method_constructor) +* [Create a Pass](#pass_class_constructor) * [Localizing Passes](#localizing_passes) * [.localize()](#method_localize) * Setting barcode @@ -51,16 +51,23 @@ ___ * [TransitType](#prop_transitType) * Generating the compiled pass. * [.generate()](#method_generate) +* [Create an Abstract Models](#abs_class_constructor) + * [.bundle](#getter_abmbundle) + * [.certificates](#getter_abmcertificates) + * [.overrides](#getter_abmoverrides)

___ -
+## Create a Pass +___ + + #### constructor() ```typescript -const pass = await createPass({ ... }, Buffer.from([ ... ])); +const pass = await createPass({ ... }, Buffer.from([ ... ], { ... })); ``` **Returns**: @@ -71,16 +78,19 @@ const pass = await createPass({ ... }, Buffer.from([ ... ])); | Key | Type | Description | Optional | Default Value | |-----|------|---------------|:-------------:|:-----------:| -| options | Object | The options to create the pass | `false` | - -| options.model | String \| Path \| Buffer Object | The model path or a Buffer Object with path as key and Buffer as content | `false` | - -| options.certificates | Object | The certificate object containing the paths to certs files. | `false` | - -| options.certificates.wwdr | String \| Path | The path to Apple WWDR certificate or its content. | `false` | - -| options.certificates.signerCert | String \| Path | The path to Developer certificate file or its content. | `false` | - -| options.certificates.signerKey | Object/String | The object containing developer certificate's key and passphrase. If string, it can be the path to the key file or its content. If object, must have `keyFile` key and might need `passphrase`. | `false` | - -| options.certificates.signerKey.keyFile | String \| Path | The path to developer certificate key or its content. | `false` | - -| options.certificates.signerKey.passphrase | String \| Number | The passphrase to use to unlock the key. | `false` | - -| options.overrides | Object | Dictionary containing all the keys you can override in the pass.json file and does not have a method to get overridden. | `true` | `{ }` -| additionalBuffers | Buffer Object | Dictionary with path as key and Buffer as a content. Each will represent a file to be added to the final model. These will have priority on model ones | `true` | `{ }` +| options | Object \| [Abstract Model](#abs_class_constructor) | The options to create the pass. It can also be an instance of an [Abstract Model](#abs_class_constructor). If instance, below `options` keys are not valid (obv.) and both `abstractMissingData` and `additionalBuffers` can be used. `additionalBuffers` usage is **NOT** restricted to Abstract Models. | false | - +| options.model | String \| Path \| Buffer Object | The model path or a Buffer Object with path as key and Buffer as content | false | - +| options.certificates | Object | The certificate object containing the paths to certs files. | false | - +| options.certificates.wwdr | String \| Path | The path to Apple WWDR certificate or its content. | false | - +| options.certificates.signerCert | String \| Path | The path to Developer certificate file or its content. | false | - +| options.certificates.signerKey | Object/String | The object containing developer certificate's key and passphrase. If string, it can be the path to the key file or its content. If object, must have `keyFile` key and might need `passphrase`. | false | - +| options.certificates.signerKey.keyFile | String \| Path | The path to developer certificate key or its content. | false | - +| options.certificates.signerKey.passphrase | String \| Number | The passphrase to use to unlock the key. | false | - +| options.overrides | Object | Dictionary containing all the keys you can override in the pass.json file and does not have a method to get overridden. | true | `{ }` +| additionalBuffers | Buffer Object | Dictionary with path as key and Buffer as a content. Each will represent a file to be added to the final model. These will have priority on model ones | true | - +| abstractMissingData | Object | An object containing missing datas. It will be used only if `options` is an [Abstract Model](#abs_class_constructor). | true | - +| abstractMissingData.certificates | Object | The same as `options.certificates` and its keys. | true | - +| abstractMissingData.overrides | Object | The same as `options.overrides`. | true | -

@@ -433,7 +443,7 @@ It sets NFC info for the current pass. Passing `null` as parameter, will remove -#### .props() +#### [Getter] .props() ```typescript pass.props; @@ -539,6 +549,76 @@ Creates a pass zip as Stream. const passStream = pass.generate(); doSomethingWithPassStream(stream); ``` + +

+ +___ + +## Create an Abstract Model +___ + + + +#### constructor() + +```typescript +const abstractModel = await createAbstractModel({ ... }); +``` + +**Returns**: + +`Promise` + +**Description**: + +The purpose of this class, is to create a model to be kept in memory during the application runtime. It contains a processed version of the passed `model` (already read and splitted) and, if passed, a processed version of the `certificates`, along with the chosen overrides. +Since `certificates` and `overrides` might differ time to time or not available at the moment of the abstract model creation, an additional attribute has been added to [`createPass`](#pass_class_constructor) function. It is an object that accepts `overrides` and raw `certificates` + +**Arguments**: + +It accepts only one argument: an `options` object, which is identical to the first parameter of [`createPass`](#pass_class_constructor). You can refer to that method to compile it correctly. + +
+
+ + + +#### [Getter] .bundle() + +```typescript +abstractModel.bundle +``` + +**Returns**: + +An object containing processed model. + +
+ + +#### [Getter] .certificates() + +```typescript +abstractModel.certificates +``` + +**Returns**: + +An object containing processed certificates. + +
+ + +#### [Getter] .overrides() + +```typescript +abstractModel.overrides +``` + +**Returns**: + +An object containing passed overrides. + ___ Thanks for using this library. ❤️ Every contribution is welcome. From a66af799b08f234ad56aed9b7c16620e7b8a1673 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 25 Jul 2019 23:51:14 +0200 Subject: [PATCH 112/127] Updated Readme.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 34e3645..eb9417b 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,8 @@ try { } ``` +For more complex usage examples, please refer to [examples](https://github.com/alexandercerutti/passkit-generator/tree/master/examples) folder. + ___ ## Other From 60ae0f06b1531ddbf1eedb13e3575bd830d0e174 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 25 Jul 2019 23:52:57 +0200 Subject: [PATCH 113/127] Updated package version --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index a65cc85..8cc8c56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "passkit-generator", - "version": "1.6.5", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 387e304..b4e7578 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passkit-generator", - "version": "1.6.5", + "version": "2.0.0", "description": "The easiest way to generate custom Apple Wallet passes in Node.js", "main": "index.js", "scripts": { From 7d1cb785bc63d99c91aa623a2f24cb45b8d06929 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 25 Jul 2019 23:58:52 +0200 Subject: [PATCH 114/127] Updated examples Readme with building --- examples/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/README.md b/examples/README.md index 0c0fe7c..6310f5e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,10 +6,11 @@ Each example is linked to webserver.js, which *requires* express.js to run. Express.js has been inserted as "example package" dipendency. ```sh -git clone https://github.com/alexandercerutti/passkit-generator.git; -cd passkit-generator && npm install; -cd examples && npm install; -node .js +$ git clone https://github.com/alexandercerutti/passkit-generator.git; +$ cd passkit-generator && npm install; +$ cd examples && npm install; +$ npm run build; +$ node .js ``` Certificates paths in examples are linked to a folder `certificates` in the root of this project which is not provided. From 7d68c81aeb1e8a9c3a0947d1fc58008e5fdcadd7 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Fri, 26 Jul 2019 00:07:15 +0200 Subject: [PATCH 115/127] Improved API Docs --- API.md | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/API.md b/API.md index 5dfdacc..cc2d585 100644 --- a/API.md +++ b/API.md @@ -11,20 +11,10 @@ Some details: * Properties will be checked against schemas, which are built to reflect Apple's specifications, that can be found on Apple Developer Portal, at [PassKit Package Format Reference](https://apple.co/2MUHsm0). -* Here below are listed all the available methods that library will let you use. - -* In case of troubleshooting, you can start your project with "debug flag" as follows: - -```sh -$ DEBUG=* node index.js -``` - -For other OSs, see [Debug Documentation](https://www.npmjs.com/package/debug). - -* Keep this as always valid for the reference: +* In case of troubleshooting, you can refer to the [Self-help](https://github.com/alexandercerutti/passkit-generator/wiki/Self-help) guide in Wiki or open an issue. ```javascript -const { Pass } = require("passkit-generator"); +const { createPass } = require("passkit-generator"); ``` ___ From ced3caf0ace235082d797fda167b47c88dcf1e41 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Fri, 26 Jul 2019 00:09:35 +0200 Subject: [PATCH 116/127] Added back mistakenly removed row of API docs --- API.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/API.md b/API.md index cc2d585..fa7e6ce 100644 --- a/API.md +++ b/API.md @@ -13,6 +13,8 @@ Some details: * In case of troubleshooting, you can refer to the [Self-help](https://github.com/alexandercerutti/passkit-generator/wiki/Self-help) guide in Wiki or open an issue. +* Keep this as always valid for the reference: + ```javascript const { createPass } = require("passkit-generator"); ``` From d650dbb31fe09dba52bf3896bc2e8e8e0af088b8 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 27 Jul 2019 00:40:28 +0200 Subject: [PATCH 117/127] Added more utils functions --- src/utils.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 9f3f528..610d3e2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import moment from "moment"; import { EOL } from "os"; -import { PartitionedBundle } from "./schema"; +import { PartitionedBundle, BundleUnit } from "./schema"; import { sep } from "path"; /** @@ -105,3 +105,18 @@ export function splitBufferBundle(origin: Object): [PartitionedBundle["l10nBundl } }, [{},{}]); } + +type StringSearchMode = "includes" | "startsWith" | "endsWith"; + +export function getAllFilesWithName(name: string, source: string[], mode: StringSearchMode = "includes", forceLowerCase: boolean = false): string[] { + return source.filter(file => (forceLowerCase && file.toLowerCase() || file)[mode](name)); +} + +export function hasFilesWithName(name: string, source: string[], mode: StringSearchMode = "includes", forceLowerCase: boolean = false): boolean { + return source.some(file => (forceLowerCase && file.toLowerCase() || file)[mode](name)); +} + +export function deletePersonalization(source: BundleUnit, logosNames: string[] = []): void { + [...logosNames, "personalization.json"] + .forEach(file => delete source[file]); +} From 2444d52cb2d0a7ffbbc451af935234cf74b7f69b Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 27 Jul 2019 00:40:54 +0200 Subject: [PATCH 118/127] Added personalization messages and schemas --- src/messages.ts | 6 ++++-- src/schema.ts | 23 ++++++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/messages.ts b/src/messages.ts index e05d175..7b9ee75 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -8,7 +8,7 @@ const errors: MessageGroup = { CP_NO_CERTS: "Cannot initialize the pass creation: no valid certificates were passed.", PASSFILE_VALIDATION_FAILED: "Validation of pass type failed. Pass file is not a valid buffer or (more probably) does not respect the schema.\nRefer to https://apple.co/2Nvshvn to build a correct pass.", REQUIR_VALID_FAILED: "The options passed to Pass constructor does not meet the requirements.\nRefer to the documentation to compile them correctly.", - MODEL_UNINITIALIZED: "Provided model ( %s ) matched but unitialized or may not contain icon.\nRefer to https://apple.co/2IhJr0Q, https://apple.co/2Nvshvn and documentation to fill the model correctly.", + MODEL_UNINITIALIZED: "Provided model ( %s ) matched but unitialized or may not contain icon or a valid pass.json.\nRefer to https://apple.co/2IhJr0Q, https://apple.co/2Nvshvn and documentation to fill the model correctly.", MODEL_NOT_VALID: "A model must be provided in form of path (string) or object { 'fileName': Buffer } in order to continue.", MODELF_NOT_FOUND: "Model %s not found. Provide a valid one to continue.", MODELF_FILE_NOT_FOUND: "File %s not found.", @@ -27,7 +27,9 @@ const debugMessages: MessageGroup = { BRC_BW_FORMAT_UNSUPPORTED: "This format is not supported (by Apple) for backward support. Please choose another one.", BRC_NO_POOL: "Cannot set barcode: no barcodes found. Please set barcodes first. Barcode is for retrocompatibility only.", DATE_FORMAT_UNMATCH: "%s was not set due to incorrect date format.", - NFC_INVALID: "Unable to set NFC properties: data not compliant with schema." + NFC_INVALID: "Unable to set NFC properties: data not compliant with schema.", + PRS_INVALID: "Unable to parse Personalization.json. File is not a valid JSON. Error: %s", + PRS_REMOVED: "Personalization has been removed as it requires an NFC-enabled pass to work." }; /** diff --git a/src/schema.ts b/src/schema.ts index fd1d6a1..d8ec67d 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -418,6 +418,26 @@ const nfcDict = Joi.object().keys({ encryptionPublicKey: Joi.string() }); +// ************************************* // +// *** Personalizable Passes Schemas *** // +// ************************************* // + +export interface Personalization { + requiredPersonalizationFields: PRSField[]; + description: string; + termsAndConditions?: string; +} + +type PRSField = "PKPassPersonalizationFieldName" | "PKPassPersonalizationFieldPostalCode" | "PKPassPersonalizationFieldEmailAddress" | "PKPassPersonalizationFieldPhoneNumber"; + +const personalizationDict = Joi.object().keys({ + requiredPersonalizationFields: Joi.array() + .items("PKPassPersonalizationFieldName", "PKPassPersonalizationFieldPostalCode", "PKPassPersonalizationFieldEmailAddress", "PKPassPersonalizationFieldPhoneNumber") + .required(), + description: Joi.string().required(), + termsAndConditions: Joi.string(), +}); + // --------- UTILITIES ---------- // type Schemas = { @@ -433,7 +453,8 @@ const schemas: Schemas = { locationsDict, transitType, nfcDict, - supportedOptions + supportedOptions, + personalizationDict }; function resolveSchemaName(name: keyof Schemas) { From eecfdb048fdc18fd594ea91198186242175d395f Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 27 Jul 2019 00:41:48 +0200 Subject: [PATCH 119/127] Added support for personalization / Rewards Enrollment passes --- src/parser.ts | 48 +++++++++++++++++++++++++++++++++++++++++++----- src/pass.ts | 17 ++++++++++++++++- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 26fb96b..e4ece3e 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -2,10 +2,12 @@ import * as path from "path"; import forge from "node-forge"; import formatMessage from "./messages"; import { FactoryOptions, PartitionedBundle, BundleUnit, Certificates, FinalCertificates, isValid } from "./schema"; -import { removeHidden, splitBufferBundle } from "./utils"; +import { removeHidden, splitBufferBundle, getAllFilesWithName, hasFilesWithName, deletePersonalization } from "./utils"; import { promisify } from "util"; import { readFile as _readFile, readdir as _readdir } from "fs"; +import debug from "debug"; +const prsDebug = debug("Personalization"); const readDir = promisify(_readdir); const readFile = promisify(_readFile); @@ -38,9 +40,45 @@ export async function getModelContents(model: FactoryOptions["model"]) { } const modelFiles = Object.keys(modelContents.bundle); + const isModelInitialized = ( + modelFiles.includes("pass.json") && + hasFilesWithName("icon", modelFiles, "startsWith") + ); - if (!(modelFiles.includes("pass.json") && modelContents.bundle["pass.json"].length && modelFiles.some(file => Boolean(file.includes("icon") && modelContents.bundle[file].length)))) { - throw new Error("missing icon or pass.json"); + if (!isModelInitialized) { + // @TODO: set a good error message + throw new Error(formatMessage("MODEL_UNINITIALIZED", "parse result")); + } + + // ======================= // + // *** Personalization *** // + // ======================= // + + const personalizationJsonFile = "personalization.json"; + + if (!modelFiles.includes(personalizationJsonFile)) { + return modelContents; + } + + const logoFullNames = getAllFilesWithName("personalizationLogo@", modelFiles, "startsWith"); + if (!(logoFullNames.length && modelContents.bundle[personalizationJsonFile].length)) { + deletePersonalization(modelContents.bundle, logoFullNames); + return modelContents; + } + + try { + const parsedPersonalization = JSON.parse(modelContents.bundle[personalizationJsonFile].toString("utf8")); + const isPersonalizationValid = isValid(parsedPersonalization, "personalizationDict"); + + if (!isPersonalizationValid) { + [...logoFullNames, personalizationJsonFile] + .forEach(file => delete modelContents.bundle[file]); + + return modelContents; + } + } catch (err) { + prsDebug(formatMessage("PRS_INVALID", err)); + deletePersonalization(modelContents.bundle, logoFullNames); } return modelContents; @@ -63,7 +101,7 @@ export async function getModelFolderContents(model: string): Promise file.toLowerCase().includes("icon")) + hasFilesWithName("icon", filteredFiles, "startsWith") ); // Icon is required to proceed @@ -171,7 +209,7 @@ export function getModelBufferContents(model: BundleUnit): PartitionedBundle { const isModelInitialized = ( bundleKeys.length && - bundleKeys.some(file => file.toLowerCase().includes("icon")) + hasFilesWithName("icon", bundleKeys, "startsWith") ); // Icon is required to proceed diff --git a/src/pass.ts b/src/pass.ts index d74da1b..a06d1c8 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -7,7 +7,7 @@ import { ZipFile } from "yazl"; import * as schema from "./schema"; import formatMessage from "./messages"; import FieldsArray from "./fieldsArray"; -import { generateStringFile, dateToW3CString, isValidRGB } from "./utils"; +import { generateStringFile, dateToW3CString, isValidRGB, deletePersonalization, getAllFilesWithName } from "./utils"; const barcodeDebug = debug("passkit:barcode"); const genericDebug = debug("passkit:generic"); @@ -134,6 +134,21 @@ export class Pass { // Editing Pass.json this.bundle["pass.json"] = this._patch(this.bundle["pass.json"]); + /** + * Checking Personalization, as this is available only with NFC + * @see https://apple.co/2SHfb22 + */ + const currentBundleFiles = Object.keys(this.bundle); + + if (!this[passProps].nfc && currentBundleFiles.includes("personalization.json")) { + genericDebug(formatMessage("PRS_REMOVED")); + deletePersonalization(this.bundle, getAllFilesWithName( + "personalizationLogo@", + currentBundleFiles, + "startsWith" + )); + } + const finalBundle = { ...this.bundle } as schema.BundleUnit; /** From 0aaaf78451ee9a3109e23e0d90dce45d46c90184 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 27 Jul 2019 00:50:32 +0200 Subject: [PATCH 120/127] API: removed nfc untested message --- API.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/API.md b/API.md index fa7e6ce..cd94637 100644 --- a/API.md +++ b/API.md @@ -420,8 +420,6 @@ pass.nfc(data: schema.NFC): this It sets NFC info for the current pass. Passing `null` as parameter, will remove its value. ->*Notice*: **I had the possibility to test in no way this pass feature and, therefore, the implementation. If you need it and this won't work, feel free to contact me and we will investigate together 😄** - **Arguments**: | Key | Type | Description | Optional | From 8c89b18a7589c434e42e6892a6810fa895f55152 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 27 Jul 2019 00:51:12 +0200 Subject: [PATCH 121/127] Updated API for Personalization --- API.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/API.md b/API.md index cd94637..fc003fc 100644 --- a/API.md +++ b/API.md @@ -37,6 +37,7 @@ ___ * [.relevantDate()](#method_revdate) * Setting NFC * [.nfc()](#method_nfc) + * [Personalization](#personalization) * Getting the current information * [.props](#getter_props) * [Setting Pass Structure Keys (primaryFields, secondaryFields, ...)](#prop_fields) @@ -428,6 +429,18 @@ It sets NFC info for the current pass. Passing `null` as parameter, will remove **See**: [PassKit Package Format Reference # NFC](https://apple.co/2wTxiaC) +
+
+ + + +#### Personalization / Reward Enrollment passes + +Personalization (or [Reward Enrollment](https://apple.co/2YkS12N) passes) is supported only if `personalization.json` is available and it's a valid json file (checked against a schema), `personalizationLogo@XX.png` (with 'XX' => x2, x3) is available and NFC is setted. +If these conditions are not met, the personalization gets removed from the output pass. + +>*Notice*: **I had the possibility to test in no way this feature on any real pass. If you need it and this won't work, feel free to contact me and we will investigate together 😄** +


From 9aa151ebb3abd669cb3fb3b17a91e46a9acc0685 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 27 Jul 2019 00:51:25 +0200 Subject: [PATCH 122/127] Removed @ from personalizationLogo search --- src/parser.ts | 2 +- src/pass.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index e4ece3e..5c466ce 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -60,7 +60,7 @@ export async function getModelContents(model: FactoryOptions["model"]) { return modelContents; } - const logoFullNames = getAllFilesWithName("personalizationLogo@", modelFiles, "startsWith"); + const logoFullNames = getAllFilesWithName("personalizationLogo", modelFiles, "startsWith"); if (!(logoFullNames.length && modelContents.bundle[personalizationJsonFile].length)) { deletePersonalization(modelContents.bundle, logoFullNames); return modelContents; diff --git a/src/pass.ts b/src/pass.ts index a06d1c8..3698821 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -143,7 +143,7 @@ export class Pass { if (!this[passProps].nfc && currentBundleFiles.includes("personalization.json")) { genericDebug(formatMessage("PRS_REMOVED")); deletePersonalization(this.bundle, getAllFilesWithName( - "personalizationLogo@", + "personalizationLogo", currentBundleFiles, "startsWith" )); From 0175a2858ef0a58f7a87eb8eac7a65e7411f1251 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 27 Jul 2019 00:53:37 +0200 Subject: [PATCH 123/127] Removed a TODO left --- src/parser.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/parser.ts b/src/parser.ts index 5c466ce..1a1748e 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -46,7 +46,6 @@ export async function getModelContents(model: FactoryOptions["model"]) { ); if (!isModelInitialized) { - // @TODO: set a good error message throw new Error(formatMessage("MODEL_UNINITIALIZED", "parse result")); } From 80b874077f6f6c760723722aea690a3f703e88fb Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Wed, 31 Jul 2019 23:21:58 +0200 Subject: [PATCH 124/127] Moved row field in schema from Field to PassFields.auxiliaryFields; Added optional row on typings --- index.d.ts | 2 +- src/schema.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/index.d.ts b/index.d.ts index dfd19ff..bfec422 100644 --- a/index.d.ts +++ b/index.d.ts @@ -9,7 +9,7 @@ export declare class Pass { public headerFields: Schema.Field[]; public primaryFields: Schema.Field[]; public secondaryFields: Schema.Field[]; - public auxiliaryFields: (Schema.Field & { row: number })[]; + public auxiliaryFields: (Schema.Field & { row?: number })[]; public backFields: Schema.Field[]; /** diff --git a/src/schema.ts b/src/schema.ts index d8ec67d..f395c3c 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -327,7 +327,6 @@ export interface Field { timeStyle?: string; currencyCode?: string; numberStyle?: string; - row?: number; } const field = Joi.object().keys({ @@ -387,7 +386,7 @@ const locationsDict = Joi.object().keys({ }); export interface PassFields { - auxiliaryFields: Field[]; + auxiliaryFields: (Field & { row?: number })[]; backFields: Field[]; headerFields: Field[]; primaryFields: Field[]; From 522b2fca620dcc89b410d5a4af6dfb5ac0790a6d Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Wed, 14 Aug 2019 15:51:04 +0200 Subject: [PATCH 125/127] Added comments --- src/abstract.ts | 6 ++++++ src/factory.ts | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/src/abstract.ts b/src/abstract.ts index 7f76d15..699e7a0 100644 --- a/src/abstract.ts +++ b/src/abstract.ts @@ -16,6 +16,12 @@ interface AbstractModelOptions { overrides?: OverridesSupportedOptions; } +/** + * Creates an abstract model to keep data + * in memory for future passes creation + * @param options + */ + export async function createAbstractModel(options: AbstractFactoryOptions) { if (!(options && Object.keys(options).length)) { throw new Error(formatMessage("CP_NO_OPTS")); diff --git a/src/factory.ts b/src/factory.ts index 988b4e9..9c92ac2 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -5,6 +5,14 @@ import { getModelContents, readCertificatesFromOptions } from "./parser"; import { splitBufferBundle } from "./utils"; import { AbstractModel, AbstractFactoryOptions } from "./abstract"; +/** + * Creates a new Pass instance. + * + * @param options Options to be used to create the instance or an Abstract Model reference + * @param additionalBuffers More buffers (with file name) to be added on runtime (if you are downloading some files from the web) + * @param abstractMissingData Additional data for abstract models, that might vary from pass to pass. + */ + export async function createPass( options: FactoryOptions | AbstractModel, additionalBuffers?: BundleUnit, From 681d9d144d9246440529b113e4e15a869a2df32d Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Wed, 14 Aug 2019 15:51:26 +0200 Subject: [PATCH 126/127] Improved declaration file --- index.d.ts | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index bfec422..ad15c5f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,6 +1,14 @@ import { Stream } from "stream"; -export function createPass(options: Schema.FactoryOptions): Promise; +/** + * Creates a new Pass instance. + * + * @param options Options to be used to create the instance or an Abstract Model reference + * @param additionalBuffers More buffers (with file name) to be added on runtime (if you are downloading some files from the web) + * @param abstractMissingData Additional data for abstract models, that might vary from pass to pass. + */ +export declare function createPass(options: Schema.FactoryOptions | AbstractModel, additionalBuffers?: Schema.BundleUnit, abstractMissingData?: Omit): Promise; + export declare class Pass { constructor(options: Schema.PassInstance); @@ -115,6 +123,20 @@ export declare class Pass { readonly props: Readonly; } +/** + * Creates an abstract model to keep data + * in memory for future passes creation + * @param options + */ +export declare function createAbstractModel(options: Schema.AbstractFactoryOptions): Promise; + +export declare class AbstractModel { + constructor(options: Schema.AbstractModelOptions); + readonly certificates: Schema.FinalCertificates; + readonly bundle: Schema.PartitionedBundle; + readonly overrides: Schema.OverridesSupportedOptions; +} + declare namespace Schema { type DataDetectorType = "PKDataDetectorTypePhoneNumber" | "PKDataDetectorTypeLink" | "PKDataDetectorTypeAddress" | "PKDataDetectorTypeCalendarEvent"; type TextAlignment = "PKTextAlignmentLeft" | "PKTextAlignmentCenter" | "PKTextAlignmentRight" | "PKTextAlignmentNatural"; @@ -156,6 +178,16 @@ declare namespace Schema { signerKey: string; } + interface AbstractFactoryOptions extends Omit { + certificates?: Certificates; + } + + interface AbstractModelOptions { + bundle: PartitionedBundle; + certificates: FinalCertificates; + overrides?: OverridesSupportedOptions; + } + interface PassInstance { model: PartitionedBundle; certificates: FinalCertificates; From 36a5452b77c0a20bd8b7204164d578dea268d1ec Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Wed, 14 Aug 2019 15:58:49 +0200 Subject: [PATCH 127/127] Removed beta notice in v2 --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index eb9417b..90df136 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,6 @@ This package comes with an [API documentation](./API.md), that makes available a > ⚠ Do not rely on branches outside "master", as might not be stable and will be removed once merged. -
- -> ⚠⚠ Please notice this is a beta. It is not yet available publicly on NPM. Therefore you'll have to build through **typescript** it before using it: - -
- -```sh -$ npm run build -``` ### Install ```sh