@walletpass/pass-js
Version:
Apple Wallet Pass generating and pushing updates from Node.js
93 lines • 4.38 kB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2017-2026 Konstantin Vyatkin <tino@vtkn.io>
/**
* Checks if given string is a valid W3C date representation
*
* @param {string} dateStr
* @returns {boolean}
*/
export function isValidW3CDateString(dateStr) {
if (typeof dateStr !== 'string')
return false;
// W3C date format: YYYY-MM-DDThh:mm(:ss)?(Z|±HH:MM)
// Must match the parser in getDateFromW3CString exactly so the two don't
// disagree (the parser crashes on strings the validator greenlights).
// - `$` anchors the whole string, not just the offset branch.
// - Timezone minutes accept 00–59 (some offsets are :45, e.g. Nepal +05:45,
// Chatham Is. +12:45 — the old [03]0 class rejected them).
return /^20\d{2}-[01]\d-[0-3]\dT[0-5]\d:[0-5]\d(?::[0-5]\d)?(?:Z|[+-][01]\d:[0-5]\d)$/.test(dateStr);
}
/**
* Converts given string or Date instance into valid W3C date string
*
* @param {string | Date} value
* @throws if given string can't be converted into w3C date
* @returns {string}
*/
export function getW3CDateString(value) {
if (typeof value !== 'string' && !(value instanceof Date))
throw new TypeError('Argument must be either a string or Date object');
if (typeof value === 'string' && isValidW3CDateString(value))
return value;
const date = value instanceof Date ? value : new Date(value);
// creating W3C date (we will always do without seconds)
const month = (1 + date.getMonth()).toFixed().padStart(2, '0');
const day = date.getDate().toFixed().padStart(2, '0');
const hours = date.getHours().toFixed().padStart(2, '0');
const minutes = date.getMinutes().toFixed().padStart(2, '0');
const offset = -date.getTimezoneOffset();
const offsetHours = Math.abs(Math.floor(offset / 60))
.toFixed()
.padStart(2, '0');
const offsetMinutes = (Math.abs(offset) - parseInt(offsetHours, 10) * 60)
.toFixed()
.padStart(2, '0');
const offsetSign = offset < 0 ? '-' : '+';
return `${date.getFullYear()}-${month}-${day}T${hours}:${minutes}${offsetSign}${offsetHours}:${offsetMinutes}`;
}
/**
* Recursively walks a plain-object tree (arrays, objects, primitives) and
* replaces every `Date` with `getW3CDateString(date)`. Returns a new tree;
* the input is not mutated. Non-plain values (class instances other than
* `Date`, functions, symbols) pass through unchanged.
*
* Use this instead of trusting `JSON.stringify` for any object containing
* a pass field that may carry `Date` values. The default
* `Date.prototype.toJSON` emits ISO 8601 with milliseconds and trailing
* `Z`, which diverges from the W3C format Apple expects in pkpass
* `pass.json` entries.
*/
export function normalizeDatesDeep(value) {
if (value instanceof Date) {
return getW3CDateString(value);
}
if (Array.isArray(value)) {
return value.map(v => normalizeDatesDeep(v));
}
if (value && typeof value === 'object' && value.constructor === Object) {
const out = {};
for (const [k, v] of Object.entries(value))
out[k] = normalizeDatesDeep(v);
return out;
}
return value;
}
export function getDateFromW3CString(value) {
if (!isValidW3CDateString(value))
throw new TypeError(`Date string ${value} is not a valid W3C date string`);
// Accept everything the validator accepts: optional seconds, and either
// `Z` or `±HH:MM` timezone.
const res = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T(?<hours>\d{2}):(?<mins>\d{2})(?::(?<secs>\d{2}))?(?:Z|(?<tzSign>[+-])(?<tzHour>\d{2}):(?<tzMin>\d{2}))$/.exec(value);
if (!res)
throw new TypeError(`Date string ${value} is not a valid W3C date string`);
const { year, month, day, hours, mins, secs, tzSign, tzHour, tzMin } = res.groups;
let utcdate = Date.UTC(parseInt(year, 10), parseInt(month, 10) - 1, // months are zero-offset
parseInt(day, 10), parseInt(hours, 10), parseInt(mins, 10), secs ? parseInt(secs, 10) : 0);
// Apply non-UTC timezone offset if present. `Z` means already UTC.
if (tzSign && tzHour && tzMin) {
const offsetMinutes = parseInt(tzHour, 10) * 60 + parseInt(tzMin, 10);
utcdate += (tzSign === '+' ? -1 : 1) * offsetMinutes * 60000;
}
return new Date(utcdate);
}
//# sourceMappingURL=w3cdate.js.map