@walletpass/pass-js
Version:
Apple Wallet Pass generating and pushing updates from Node.js
85 lines • 4.74 kB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2017-2026 Konstantin Vyatkin <tino@vtkn.io>
// Apple caps remote image downloads at 2 MiB per entry.
const MAX_IMAGE_BYTES = 2 * 1024 * 1024;
const SHA256_HEX = /^[0-9a-f]{64}$/i;
/**
* Per-entry shape validation for `upcomingPassInformation`. Runs at
* setter time and does not look at any other pass field, so it cannot
* produce order-dependent failures when a caller hydrates a pass from
* a plain object whose key order puts `upcomingPassInformation`
* before `preferredStyleSchemes`.
*
* Cross-field checks (eventTicket style + `posterEventTicket` scheme)
* live in `assertUpcomingPassInformationContext` and run at pass-build
* time, in `Pass.validate()`.
*
* Throws `TypeError` on any rule violation; returns the input array
* unchanged on success.
*/
export function validateUpcomingPassInformationEntries(value) {
if (!Array.isArray(value))
throw new TypeError('upcomingPassInformation must be an array');
for (const [i, entry] of value.entries()) {
if (!entry || typeof entry !== 'object')
throw new TypeError(`upcomingPassInformation[${i}] must be an object`);
if (typeof entry.identifier !== 'string' || entry.identifier.length === 0)
throw new TypeError(`upcomingPassInformation[${i}].identifier must be a non-empty string`);
if (typeof entry.name !== 'string' || entry.name.length === 0)
throw new TypeError(`upcomingPassInformation[${i}].name must be a non-empty string`);
if (entry.type !== 'event')
throw new TypeError(`upcomingPassInformation[${i}].type must be "event"`);
if (entry.images) {
for (const slot of Object.keys(entry.images)) {
const img = entry.images[slot];
if (!img)
continue;
if (!Array.isArray(img.URLs) || img.URLs.length === 0)
throw new TypeError(`upcomingPassInformation[${i}].images.${slot}.URLs must be a non-empty array`);
for (const [j, url] of img.URLs.entries()) {
if (typeof url.URL !== 'string')
throw new TypeError(`upcomingPassInformation[${i}].images.${slot}.URLs[${j}].URL must be a string`);
// Apple spec: image URLs must be HTTPS.
const parsed = new URL(url.URL);
if (parsed.protocol !== 'https:')
throw new TypeError(`upcomingPassInformation[${i}].images.${slot}.URLs[${j}].URL must use https`);
if (typeof url.SHA256 !== 'string' || !SHA256_HEX.test(url.SHA256))
throw new TypeError(`upcomingPassInformation[${i}].images.${slot}.URLs[${j}].SHA256 must be a 64-char hex string`);
if (url.scale !== undefined &&
!(url.scale === 1 || url.scale === 2 || url.scale === 3))
throw new TypeError(`upcomingPassInformation[${i}].images.${slot}.URLs[${j}].scale must be 1, 2, or 3`);
if (url.size !== undefined) {
if (typeof url.size !== 'number' ||
!Number.isInteger(url.size) ||
url.size < 0)
throw new TypeError(`upcomingPassInformation[${i}].images.${slot}.URLs[${j}].size must be a non-negative integer`);
if (url.size > MAX_IMAGE_BYTES)
throw new TypeError(`upcomingPassInformation[${i}].images.${slot}.URLs[${j}].size exceeds 2 MiB`);
}
}
}
}
}
return value;
}
/**
* Cross-field context check for `upcomingPassInformation`: the pass
* must be an `eventTicket` opted into the `posterEventTicket` scheme.
* No-ops if `upcomingPassInformation` is unset.
*
* Intended to run at pass-build time (from `Pass.validate()`), after
* the caller has finished wiring the pass's fields. Keeping this
* separate from `validateUpcomingPassInformationEntries` avoids an
* order-dependency bug when hydrating a pass from a serialized object
* whose key order is not style-first.
*/
export function assertUpcomingPassInformationContext(pass) {
if (!pass.upcomingPassInformation)
return;
if (!('eventTicket' in pass))
throw new TypeError('upcomingPassInformation requires style "eventTicket"');
const schemes = pass.preferredStyleSchemes;
if (!Array.isArray(schemes) || !schemes.includes('posterEventTicket'))
throw new TypeError('upcomingPassInformation requires preferredStyleSchemes to include "posterEventTicket"');
}
//# sourceMappingURL=upcoming-pass-information.js.map