@walletpass/pass-js
Version:
Apple Wallet Pass generating and pushing updates from Node.js
83 lines • 3.95 kB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2017-2026 Konstantin Vyatkin <tino@vtkn.io>
// Generate a pass file.
import { getBufferHash } from './lib/get-buffer-hash.js';
import { signManifest } from './lib/sign-manifest.js';
import { PassBase } from './lib/base-pass.js';
import { writeZip } from './lib/zip.js';
import { assertUpcomingPassInformationContext } from './lib/upcoming-pass-information.js';
import { createPersonalizationEntry, isPersonalizationLogoPath, } from './lib/personalization.js';
function hasNfcPayload(pass) {
const nfc = pass.nfc;
if (!nfc || typeof nfc !== 'object')
return false;
const message = nfc.message;
return typeof message === 'string' && message.length > 0;
}
// Create a new pass.
//
// template - The template
// fields - Pass fields (description, serialNumber, logoText)
export class Pass extends PassBase {
template;
constructor(template, fields = {}, images, localization, options, personalization) {
super(fields, images, localization, options, personalization);
this.template = template;
Object.preventExtensions(this);
}
// Validate pass, throws error if missing a mandatory top-level field or image.
validate() {
for (const requiredField of [
'description',
'organizationName',
'passTypeIdentifier',
'serialNumber',
'teamIdentifier',
]) {
if (!(requiredField in this.fields))
throw new ReferenceError(`${requiredField} is required in a Pass`);
}
if ('webServiceURL' in this.fields) {
if (typeof this.fields.authenticationToken !== 'string')
throw new Error('While webServiceURL is present, authenticationToken also required!');
if (this.fields.authenticationToken.length < 16)
throw new ReferenceError('authenticationToken must be at least 16 characters long!');
}
else if ('authenticationToken' in this.fields) {
throw new TypeError('authenticationToken is present in Pass data while webServiceURL is missing!');
}
// Cross-field check deferred from the `upcomingPassInformation`
// setter — runs here so construction order doesn't matter.
assertUpcomingPassInformationContext(this.fields);
this.images.validate();
}
async asBuffer() {
this.validate();
if (!this.template.certificate)
throw new ReferenceError(`Set pass certificate in template before producing pass buffers`);
if (!this.template.key)
throw new ReferenceError(`Set private key in pass template before producing pass buffers`);
const zip = [];
const passJson = JSON.stringify(this);
const passObject = JSON.parse(passJson);
const imageEntries = await this.images.toArray();
const personalization = this.personalization;
const canPersonalize = Boolean(personalization &&
hasNfcPayload(passObject) &&
imageEntries.some(entry => isPersonalizationLogoPath(entry.path)));
zip.push({ path: 'pass.json', data: Buffer.from(passJson) });
zip.push(...this.localization.toArray());
zip.push(...imageEntries.filter(entry => canPersonalize || !isPersonalizationLogoPath(entry.path)));
if (canPersonalize && personalization)
zip.push(createPersonalizationEntry(personalization));
const manifestJson = JSON.stringify(zip.reduce((res, { path, data }) => {
res[path] = getBufferHash(data);
return res;
}, {}));
zip.push({ path: 'manifest.json', data: manifestJson });
const signature = signManifest(this.template.certificate, this.template.key, manifestJson);
zip.push({ path: 'signature', data: signature });
return writeZip(zip);
}
}
//# sourceMappingURL=pass.js.map