UNPKG

@walletpass/pass-js

Version:

Apple Wallet Pass generating and pushing updates from Node.js

85 lines 4.74 kB
// 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