passlet 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1378 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2412 -0
- package/dist/index.d.ts +2412 -0
- package/dist/index.js +1367 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1378 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
var JSZip = require('jszip');
|
|
5
|
+
var forge = require('node-forge');
|
|
6
|
+
var jose = require('jose');
|
|
7
|
+
var zod = require('zod');
|
|
8
|
+
|
|
9
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var JSZip__default = /*#__PURE__*/_interopDefault(JSZip);
|
|
12
|
+
var forge__default = /*#__PURE__*/_interopDefault(forge);
|
|
13
|
+
|
|
14
|
+
// src/errors.ts
|
|
15
|
+
var WALLET_ERROR_CODES = {
|
|
16
|
+
// Config validation — message comes from the schema
|
|
17
|
+
PASS_CONFIG_INVALID: "PassConfig invalid",
|
|
18
|
+
CREATE_CONFIG_INVALID: "CreateConfig invalid",
|
|
19
|
+
// Apple — provider-level constraints the schema cannot enforce
|
|
20
|
+
APPLE_INVALID_SIGNER_CERT: "Apple signing failed: signerCert is not a valid PEM certificate",
|
|
21
|
+
APPLE_INVALID_SIGNER_KEY: "Apple signing failed: signerKey is not a valid PEM private key",
|
|
22
|
+
APPLE_INVALID_WWDR: "Apple signing failed: wwdr is not a valid PEM certificate",
|
|
23
|
+
APPLE_SIGNING_FAILED: "Apple signing failed: could not create PKCS#7 signature",
|
|
24
|
+
APPLE_MISSING_ICON: "Apple Wallet requires an icon image for every pass",
|
|
25
|
+
APPLE_UNSUPPORTED_BARCODE_FORMAT: "Apple Wallet does not support this barcode format \u2014 use QR, PDF417, or Aztec",
|
|
26
|
+
APPLE_BOARDING_MISSING_TRANSIT_TYPE: "Apple Wallet boarding passes require a transitType",
|
|
27
|
+
// Google — provider-level constraints the schema cannot enforce
|
|
28
|
+
GOOGLE_INVALID_PRIVATE_KEY: "Google signing failed: privateKey is not a valid PKCS#8 PEM private key",
|
|
29
|
+
GOOGLE_SIGNING_FAILED: "Google signing failed: could not sign the Wallet JWT",
|
|
30
|
+
GOOGLE_API_ERROR: "Google Wallet API error",
|
|
31
|
+
GOOGLE_MISSING_LOGO: "Google Wallet requires a public logo URL for this pass type",
|
|
32
|
+
GOOGLE_FLIGHT_MISSING_CLASS_FIELDS: "Google Wallet flightClass requires flightHeader and localScheduledDepartureDateTime",
|
|
33
|
+
GOOGLE_FLIGHT_MISSING_PASSENGER_NAME: "Google Wallet flightObject requires passengerName",
|
|
34
|
+
// Images
|
|
35
|
+
IMAGE_FETCH_NETWORK_ERROR: "Failed to fetch image: network error",
|
|
36
|
+
IMAGE_FETCH_FAILED: "Failed to fetch image: non-2xx response"
|
|
37
|
+
};
|
|
38
|
+
var WalletError = class extends Error {
|
|
39
|
+
code;
|
|
40
|
+
constructor(code, message, options) {
|
|
41
|
+
super(message ?? WALLET_ERROR_CODES[code], options);
|
|
42
|
+
this.name = "WalletError";
|
|
43
|
+
this.code = code;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
function signManifest(options) {
|
|
47
|
+
const { manifest, signerCert, signerKey, wwdr } = options;
|
|
48
|
+
let cert;
|
|
49
|
+
let key;
|
|
50
|
+
let wwdrCert;
|
|
51
|
+
try {
|
|
52
|
+
cert = forge__default.default.pki.certificateFromPem(signerCert);
|
|
53
|
+
} catch (cause) {
|
|
54
|
+
throw new WalletError("APPLE_INVALID_SIGNER_CERT", void 0, { cause });
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
key = forge__default.default.pki.privateKeyFromPem(signerKey);
|
|
58
|
+
} catch (cause) {
|
|
59
|
+
throw new WalletError("APPLE_INVALID_SIGNER_KEY", void 0, { cause });
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
wwdrCert = forge__default.default.pki.certificateFromPem(wwdr);
|
|
63
|
+
} catch (cause) {
|
|
64
|
+
throw new WalletError("APPLE_INVALID_WWDR", void 0, { cause });
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const binaryStr = new TextDecoder("iso-8859-1").decode(manifest);
|
|
68
|
+
const p7 = forge__default.default.pkcs7.createSignedData();
|
|
69
|
+
p7.content = forge__default.default.util.createBuffer(binaryStr);
|
|
70
|
+
p7.addCertificate(cert);
|
|
71
|
+
p7.addCertificate(wwdrCert);
|
|
72
|
+
p7.addSigner({
|
|
73
|
+
key,
|
|
74
|
+
certificate: cert,
|
|
75
|
+
digestAlgorithm: forge__default.default.pki.oids.sha1,
|
|
76
|
+
authenticatedAttributes: [
|
|
77
|
+
{
|
|
78
|
+
type: forge__default.default.pki.oids.contentType,
|
|
79
|
+
value: forge__default.default.pki.oids.data
|
|
80
|
+
},
|
|
81
|
+
{ type: forge__default.default.pki.oids.messageDigest },
|
|
82
|
+
{ type: forge__default.default.pki.oids.signingTime }
|
|
83
|
+
]
|
|
84
|
+
});
|
|
85
|
+
p7.sign({ detached: true });
|
|
86
|
+
const der = forge__default.default.asn1.toDer(p7.toAsn1()).getBytes();
|
|
87
|
+
return Uint8Array.from(der, (c) => c.charCodeAt(0));
|
|
88
|
+
} catch (cause) {
|
|
89
|
+
throw new WalletError("APPLE_SIGNING_FAILED", void 0, { cause });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/providers/apple/utils.ts
|
|
94
|
+
function hexToRgb(hex) {
|
|
95
|
+
const clean = hex.replace("#", "");
|
|
96
|
+
const r = Number.parseInt(clean.slice(0, 2), 16);
|
|
97
|
+
const g = Number.parseInt(clean.slice(2, 4), 16);
|
|
98
|
+
const b = Number.parseInt(clean.slice(4, 6), 16);
|
|
99
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
100
|
+
}
|
|
101
|
+
var APPLE_BARCODE_FORMAT = {
|
|
102
|
+
QR: "PKBarcodeFormatQR",
|
|
103
|
+
PDF417: "PKBarcodeFormatPDF417",
|
|
104
|
+
Aztec: "PKBarcodeFormatAztec",
|
|
105
|
+
Code128: "PKBarcodeFormatCode128"
|
|
106
|
+
};
|
|
107
|
+
function toAppleBarcodeFormat(format) {
|
|
108
|
+
return APPLE_BARCODE_FORMAT[format] ?? "PKBarcodeFormatQR";
|
|
109
|
+
}
|
|
110
|
+
async function fetchAsBytes(url) {
|
|
111
|
+
let response;
|
|
112
|
+
try {
|
|
113
|
+
response = await fetch(url);
|
|
114
|
+
} catch (cause) {
|
|
115
|
+
throw new WalletError(
|
|
116
|
+
"IMAGE_FETCH_NETWORK_ERROR",
|
|
117
|
+
`Failed to fetch image: ${url} (network error)`,
|
|
118
|
+
{ cause }
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
throw new WalletError(
|
|
123
|
+
"IMAGE_FETCH_FAILED",
|
|
124
|
+
`Failed to fetch image: ${url} (${response.status})`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
128
|
+
}
|
|
129
|
+
function resolveSource(src) {
|
|
130
|
+
return src instanceof Uint8Array ? Promise.resolve(src) : fetchAsBytes(src);
|
|
131
|
+
}
|
|
132
|
+
async function resolveImageSet(name, imageSet2, warnings) {
|
|
133
|
+
if (!imageSet2) {
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
const files = {};
|
|
137
|
+
const tryLoad = async (filename, src) => {
|
|
138
|
+
try {
|
|
139
|
+
files[filename] = await resolveSource(src);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
warnings.push(
|
|
142
|
+
`Could not load ${filename}: ${e instanceof Error ? e.message : String(e)}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
if (typeof imageSet2 === "string" || imageSet2 instanceof Uint8Array) {
|
|
147
|
+
await tryLoad(`${name}.png`, imageSet2);
|
|
148
|
+
} else {
|
|
149
|
+
await tryLoad(`${name}.png`, imageSet2.base);
|
|
150
|
+
if (imageSet2.retina) {
|
|
151
|
+
await tryLoad(`${name}@2x.png`, imageSet2.retina);
|
|
152
|
+
}
|
|
153
|
+
if (imageSet2.superRetina) {
|
|
154
|
+
await tryLoad(`${name}@3x.png`, imageSet2.superRetina);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return files;
|
|
158
|
+
}
|
|
159
|
+
async function resolveRequiredImageSet(name, imageSet2) {
|
|
160
|
+
const files = {};
|
|
161
|
+
if (typeof imageSet2 === "string" || imageSet2 instanceof Uint8Array) {
|
|
162
|
+
files[`${name}.png`] = await resolveSource(imageSet2);
|
|
163
|
+
} else {
|
|
164
|
+
files[`${name}.png`] = await resolveSource(imageSet2.base);
|
|
165
|
+
if (imageSet2.retina) {
|
|
166
|
+
files[`${name}@2x.png`] = await resolveSource(imageSet2.retina);
|
|
167
|
+
}
|
|
168
|
+
if (imageSet2.superRetina) {
|
|
169
|
+
files[`${name}@3x.png`] = await resolveSource(imageSet2.superRetina);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return files;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/providers/apple/index.ts
|
|
176
|
+
var PASS_TYPE_KEY = {
|
|
177
|
+
loyalty: "storeCard",
|
|
178
|
+
coupon: "coupon",
|
|
179
|
+
event: "eventTicket",
|
|
180
|
+
flight: "boardingPass",
|
|
181
|
+
giftCard: "storeCard",
|
|
182
|
+
generic: "generic"
|
|
183
|
+
};
|
|
184
|
+
var TRANSIT_TYPE = {
|
|
185
|
+
air: "PKTransitTypeAir",
|
|
186
|
+
train: "PKTransitTypeTrain",
|
|
187
|
+
bus: "PKTransitTypeBus",
|
|
188
|
+
boat: "PKTransitTypeBoat"
|
|
189
|
+
};
|
|
190
|
+
var SLOT_KEY = {
|
|
191
|
+
header: "headerFields",
|
|
192
|
+
primary: "primaryFields",
|
|
193
|
+
secondary: "secondaryFields",
|
|
194
|
+
auxiliary: "auxiliaryFields",
|
|
195
|
+
back: "backFields"
|
|
196
|
+
};
|
|
197
|
+
function validateAppleRequirements(pass) {
|
|
198
|
+
if (!pass.apple?.icon) {
|
|
199
|
+
throw new WalletError("APPLE_MISSING_ICON");
|
|
200
|
+
}
|
|
201
|
+
if (pass.type === "flight" && !pass.transitType) {
|
|
202
|
+
throw new WalletError("APPLE_BOARDING_MISSING_TRANSIT_TYPE");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function buildSlots(fields, values) {
|
|
206
|
+
const slots = {
|
|
207
|
+
headerFields: [],
|
|
208
|
+
primaryFields: [],
|
|
209
|
+
secondaryFields: [],
|
|
210
|
+
auxiliaryFields: [],
|
|
211
|
+
backFields: []
|
|
212
|
+
};
|
|
213
|
+
for (const f of fields) {
|
|
214
|
+
const value = f.key in values ? values[f.key] : f.value;
|
|
215
|
+
if (value === null || value === void 0) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
slots[SLOT_KEY[f.slot]]?.push({
|
|
219
|
+
key: f.key,
|
|
220
|
+
label: f.label,
|
|
221
|
+
value,
|
|
222
|
+
...f.changeMessage && { changeMessage: f.changeMessage },
|
|
223
|
+
...f.dateStyle && { dateStyle: f.dateStyle },
|
|
224
|
+
...f.timeStyle && { timeStyle: f.timeStyle },
|
|
225
|
+
...f.numberStyle && { numberStyle: f.numberStyle },
|
|
226
|
+
...f.currencyCode && { currencyCode: f.currencyCode },
|
|
227
|
+
...f.textAlignment && { textAlignment: f.textAlignment },
|
|
228
|
+
...f.row !== void 0 && { row: f.row }
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return slots;
|
|
232
|
+
}
|
|
233
|
+
function buildPassTypeContent(pass, slots) {
|
|
234
|
+
const content = { ...slots };
|
|
235
|
+
if (pass.type === "flight") {
|
|
236
|
+
content.transitType = TRANSIT_TYPE[pass.transitType ?? "air"] ?? "PKTransitTypeAir";
|
|
237
|
+
}
|
|
238
|
+
return content;
|
|
239
|
+
}
|
|
240
|
+
function buildEventAppleFields(pass) {
|
|
241
|
+
const a = pass.apple;
|
|
242
|
+
return {
|
|
243
|
+
eventLogoText: a?.eventLogoText,
|
|
244
|
+
footerBackgroundColor: a?.footerBackgroundColor ? hexToRgb(a.footerBackgroundColor) : void 0,
|
|
245
|
+
suppressHeaderDarkening: a?.suppressHeaderDarkening,
|
|
246
|
+
useAutomaticColors: a?.useAutomaticColors,
|
|
247
|
+
preferredStyleSchemes: a?.preferredStyleSchemes,
|
|
248
|
+
auxiliaryStoreIdentifiers: a?.auxiliaryStoreIdentifiers,
|
|
249
|
+
accessibilityURL: a?.accessibilityURL,
|
|
250
|
+
addOnURL: a?.addOnURL,
|
|
251
|
+
bagPolicyURL: a?.bagPolicyURL,
|
|
252
|
+
contactVenueEmail: a?.contactVenueEmail,
|
|
253
|
+
contactVenuePhoneNumber: a?.contactVenuePhoneNumber,
|
|
254
|
+
contactVenueWebsite: a?.contactVenueWebsite,
|
|
255
|
+
directionsInformationURL: a?.directionsInformationURL,
|
|
256
|
+
merchandiseURL: a?.merchandiseURL,
|
|
257
|
+
orderFoodURL: a?.orderFoodURL,
|
|
258
|
+
parkingInformationURL: a?.parkingInformationURL,
|
|
259
|
+
purchaseParkingURL: a?.purchaseParkingURL,
|
|
260
|
+
sellURL: a?.sellURL,
|
|
261
|
+
transferURL: a?.transferURL,
|
|
262
|
+
transitInformationURL: a?.transitInformationURL
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function buildFlightAppleFields(pass) {
|
|
266
|
+
const a = pass.apple;
|
|
267
|
+
return {
|
|
268
|
+
changeSeatURL: a?.changeSeatURL,
|
|
269
|
+
entertainmentURL: a?.entertainmentURL,
|
|
270
|
+
managementURL: a?.managementURL,
|
|
271
|
+
purchaseAdditionalBaggageURL: a?.purchaseAdditionalBaggageURL,
|
|
272
|
+
purchaseLoungeAccessURL: a?.purchaseLoungeAccessURL,
|
|
273
|
+
purchaseWifiURL: a?.purchaseWifiURL,
|
|
274
|
+
registerServiceAnimalURL: a?.registerServiceAnimalURL,
|
|
275
|
+
reportLostBagURL: a?.reportLostBagURL,
|
|
276
|
+
requestWheelchairURL: a?.requestWheelchairURL,
|
|
277
|
+
trackBagsURL: a?.trackBagsURL,
|
|
278
|
+
transitProviderEmail: a?.transitProviderEmail,
|
|
279
|
+
transitProviderPhoneNumber: a?.transitProviderPhoneNumber,
|
|
280
|
+
transitProviderWebsiteURL: a?.transitProviderWebsiteURL,
|
|
281
|
+
upgradeURL: a?.upgradeURL
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function buildAppleCommonFields(pass, createConfig) {
|
|
285
|
+
const a = pass.apple;
|
|
286
|
+
return {
|
|
287
|
+
backgroundColor: pass.color ? hexToRgb(pass.color) : void 0,
|
|
288
|
+
foregroundColor: a?.foregroundColor ? hexToRgb(a.foregroundColor) : void 0,
|
|
289
|
+
labelColor: a?.labelColor ? hexToRgb(a.labelColor) : void 0,
|
|
290
|
+
expirationDate: createConfig.expiresAt,
|
|
291
|
+
voided: createConfig.apple?.voided,
|
|
292
|
+
// Barcode — Apple supports up to 10 but we expose one per recipient
|
|
293
|
+
barcodes: createConfig.barcode ? [
|
|
294
|
+
{
|
|
295
|
+
message: createConfig.barcode.value,
|
|
296
|
+
format: toAppleBarcodeFormat(createConfig.barcode.format),
|
|
297
|
+
messageEncoding: "iso-8859-1",
|
|
298
|
+
altText: createConfig.barcode.altText ?? createConfig.barcode.value
|
|
299
|
+
}
|
|
300
|
+
] : void 0,
|
|
301
|
+
// Locations — altitude and relevantText are Apple-only
|
|
302
|
+
locations: pass.locations?.map(
|
|
303
|
+
({ latitude, longitude, altitude, relevantText }) => ({
|
|
304
|
+
latitude,
|
|
305
|
+
longitude,
|
|
306
|
+
altitude,
|
|
307
|
+
relevantText
|
|
308
|
+
})
|
|
309
|
+
),
|
|
310
|
+
beacons: a?.beacons,
|
|
311
|
+
relevantDate: a?.relevantDate,
|
|
312
|
+
relevantDates: a?.relevantDates,
|
|
313
|
+
groupingIdentifier: a?.groupingIdentifier,
|
|
314
|
+
suppressStripShine: a?.suppressStripShine,
|
|
315
|
+
sharingProhibited: a?.sharingProhibited,
|
|
316
|
+
maxDistance: a?.maxDistance,
|
|
317
|
+
nfc: a?.nfc ? {
|
|
318
|
+
message: a.nfc.message,
|
|
319
|
+
encryptionPublicKey: a.nfc.encryptionPublicKey
|
|
320
|
+
} : void 0,
|
|
321
|
+
appLaunchURL: a?.appLaunchURL,
|
|
322
|
+
associatedStoreIdentifiers: a?.associatedStoreIdentifiers,
|
|
323
|
+
webServiceURL: a?.webServiceURL,
|
|
324
|
+
authenticationToken: a?.webServiceURL ? a.authenticationToken : void 0,
|
|
325
|
+
userInfo: a?.userInfo
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function buildPassJson(pass, createConfig, credentials) {
|
|
329
|
+
const passTypeKey = PASS_TYPE_KEY[pass.type] ?? "generic";
|
|
330
|
+
const slots = buildSlots(pass.fields, createConfig.values ?? {});
|
|
331
|
+
const a = pass.apple;
|
|
332
|
+
return {
|
|
333
|
+
formatVersion: 1,
|
|
334
|
+
passTypeIdentifier: credentials.passTypeIdentifier,
|
|
335
|
+
serialNumber: createConfig.serialNumber,
|
|
336
|
+
teamIdentifier: credentials.teamId,
|
|
337
|
+
organizationName: pass.name,
|
|
338
|
+
description: a?.description ?? pass.name,
|
|
339
|
+
logoText: a?.logoText ?? pass.name,
|
|
340
|
+
...buildAppleCommonFields(pass, createConfig),
|
|
341
|
+
...pass.type === "event" && buildEventAppleFields(pass),
|
|
342
|
+
...pass.type === "flight" && buildFlightAppleFields(pass),
|
|
343
|
+
[passTypeKey]: buildPassTypeContent(pass, slots)
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
async function collectImages(pass, warnings) {
|
|
347
|
+
const images = {};
|
|
348
|
+
const icon = pass.apple?.icon;
|
|
349
|
+
if (!icon) {
|
|
350
|
+
throw new WalletError("APPLE_MISSING_ICON");
|
|
351
|
+
}
|
|
352
|
+
const iconFiles = await resolveRequiredImageSet("icon", icon);
|
|
353
|
+
Object.assign(images, iconFiles);
|
|
354
|
+
const optional = await Promise.all([
|
|
355
|
+
resolveImageSet("logo", pass.logo, warnings),
|
|
356
|
+
resolveImageSet("strip", pass.banner, warnings),
|
|
357
|
+
// banner → strip on Apple
|
|
358
|
+
resolveImageSet("background", pass.apple?.background, warnings),
|
|
359
|
+
resolveImageSet("thumbnail", pass.apple?.thumbnail, warnings),
|
|
360
|
+
resolveImageSet("footer", pass.apple?.footer, warnings)
|
|
361
|
+
]);
|
|
362
|
+
for (const set of optional) {
|
|
363
|
+
Object.assign(images, set);
|
|
364
|
+
}
|
|
365
|
+
return images;
|
|
366
|
+
}
|
|
367
|
+
async function generateApplePass(pass, createConfig, credentials) {
|
|
368
|
+
const warnings = [];
|
|
369
|
+
validateAppleRequirements(pass);
|
|
370
|
+
const encoder = new TextEncoder();
|
|
371
|
+
const zip = new JSZip__default.default();
|
|
372
|
+
const files = {};
|
|
373
|
+
const passJson = buildPassJson(pass, createConfig, credentials);
|
|
374
|
+
files["pass.json"] = encoder.encode(JSON.stringify(passJson));
|
|
375
|
+
if (pass.locales) {
|
|
376
|
+
for (const [language, translations] of Object.entries(pass.locales)) {
|
|
377
|
+
const lines = Object.entries(translations).map(
|
|
378
|
+
([key, value]) => `"${key}" = "${value.replace(/"/g, '\\"')}";`
|
|
379
|
+
);
|
|
380
|
+
files[`${language}.lproj/pass.strings`] = encoder.encode(
|
|
381
|
+
lines.join("\n")
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const images = await collectImages(pass, warnings);
|
|
386
|
+
for (const [name, bytes] of Object.entries(images)) {
|
|
387
|
+
files[name] = bytes;
|
|
388
|
+
}
|
|
389
|
+
const manifest = {};
|
|
390
|
+
for (const [name, content] of Object.entries(files)) {
|
|
391
|
+
manifest[name] = crypto.createHash("sha1").update(content).digest("hex");
|
|
392
|
+
}
|
|
393
|
+
const manifestBytes = encoder.encode(JSON.stringify(manifest));
|
|
394
|
+
const signature = signManifest({
|
|
395
|
+
manifest: manifestBytes,
|
|
396
|
+
signerCert: credentials.signerCert,
|
|
397
|
+
signerKey: credentials.signerKey,
|
|
398
|
+
wwdr: credentials.wwdr
|
|
399
|
+
});
|
|
400
|
+
for (const [name, content] of Object.entries(files)) {
|
|
401
|
+
zip.file(name, content);
|
|
402
|
+
}
|
|
403
|
+
zip.file("manifest.json", manifestBytes);
|
|
404
|
+
zip.file("signature", signature);
|
|
405
|
+
return { pass: await zip.generateAsync({ type: "uint8array" }), warnings };
|
|
406
|
+
}
|
|
407
|
+
var WALLET_BASE = "https://walletobjects.googleapis.com/walletobjects/v1";
|
|
408
|
+
var TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
409
|
+
var WALLET_SCOPE = "https://www.googleapis.com/auth/wallet_object.issuer";
|
|
410
|
+
var tokenCache = /* @__PURE__ */ new Map();
|
|
411
|
+
async function importGoogleKey(credentials) {
|
|
412
|
+
try {
|
|
413
|
+
return await jose.importPKCS8(credentials.privateKey, "RS256");
|
|
414
|
+
} catch (cause) {
|
|
415
|
+
throw new WalletError("GOOGLE_INVALID_PRIVATE_KEY", void 0, { cause });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async function getAccessToken(credentials, privateKey) {
|
|
419
|
+
const cacheKey = `${credentials.issuerId}:${credentials.clientEmail}`;
|
|
420
|
+
const cached = tokenCache.get(cacheKey);
|
|
421
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
422
|
+
return cached.token;
|
|
423
|
+
}
|
|
424
|
+
let assertion;
|
|
425
|
+
try {
|
|
426
|
+
assertion = await new jose.SignJWT({ scope: WALLET_SCOPE }).setProtectedHeader({ alg: "RS256" }).setIssuedAt().setExpirationTime("1h").setIssuer(credentials.clientEmail).setAudience(TOKEN_URL).sign(privateKey);
|
|
427
|
+
} catch (cause) {
|
|
428
|
+
throw new WalletError("GOOGLE_SIGNING_FAILED", void 0, { cause });
|
|
429
|
+
}
|
|
430
|
+
const response = await fetch(TOKEN_URL, {
|
|
431
|
+
method: "POST",
|
|
432
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
433
|
+
body: new URLSearchParams({
|
|
434
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
435
|
+
assertion
|
|
436
|
+
}).toString()
|
|
437
|
+
});
|
|
438
|
+
if (!response.ok) {
|
|
439
|
+
const text = await response.text();
|
|
440
|
+
throw new WalletError(
|
|
441
|
+
"GOOGLE_API_ERROR",
|
|
442
|
+
`Failed to obtain access token (${response.status}): ${text}`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const data = await response.json();
|
|
446
|
+
tokenCache.set(cacheKey, {
|
|
447
|
+
token: data.access_token,
|
|
448
|
+
expiresAt: Date.now() + 55 * 60 * 1e3
|
|
449
|
+
});
|
|
450
|
+
return data.access_token;
|
|
451
|
+
}
|
|
452
|
+
async function walletRequest(method, path, credentials, privateKey, body) {
|
|
453
|
+
const accessToken = await getAccessToken(credentials, privateKey);
|
|
454
|
+
return fetch(`${WALLET_BASE}${path}`, {
|
|
455
|
+
method,
|
|
456
|
+
headers: {
|
|
457
|
+
Authorization: `Bearer ${accessToken}`,
|
|
458
|
+
"Content-Type": "application/json"
|
|
459
|
+
},
|
|
460
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
async function assertOk(response) {
|
|
464
|
+
if (!response.ok) {
|
|
465
|
+
const text = await response.text();
|
|
466
|
+
throw new WalletError(
|
|
467
|
+
"GOOGLE_API_ERROR",
|
|
468
|
+
`Google Wallet API error (${response.status}): ${text}`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async function ensureClass(classType, classId, classBody, credentials, privateKey) {
|
|
473
|
+
const existing = await walletRequest(
|
|
474
|
+
"GET",
|
|
475
|
+
`/${classType}/${classId}`,
|
|
476
|
+
credentials,
|
|
477
|
+
privateKey
|
|
478
|
+
);
|
|
479
|
+
if (existing.ok) {
|
|
480
|
+
await existing.body?.cancel();
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (existing.status !== 404) {
|
|
484
|
+
const text = await existing.text();
|
|
485
|
+
throw new WalletError(
|
|
486
|
+
"GOOGLE_API_ERROR",
|
|
487
|
+
`Google Wallet API error (${existing.status}): ${text}`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
await assertOk(
|
|
491
|
+
await walletRequest("POST", `/${classType}`, credentials, privateKey, {
|
|
492
|
+
id: classId,
|
|
493
|
+
...classBody
|
|
494
|
+
})
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
async function deleteObject(objectType, objectId, credentials, privateKey) {
|
|
498
|
+
const response = await walletRequest(
|
|
499
|
+
"DELETE",
|
|
500
|
+
`/${objectType}/${objectId}`,
|
|
501
|
+
credentials,
|
|
502
|
+
privateKey
|
|
503
|
+
);
|
|
504
|
+
if (response.status === 404) {
|
|
505
|
+
await response.body?.cancel();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
await assertOk(response);
|
|
509
|
+
await response.body?.cancel();
|
|
510
|
+
}
|
|
511
|
+
async function patchObject(objectType, objectId, patch, credentials, privateKey) {
|
|
512
|
+
const response = await walletRequest(
|
|
513
|
+
"PATCH",
|
|
514
|
+
`/${objectType}/${objectId}`,
|
|
515
|
+
credentials,
|
|
516
|
+
privateKey,
|
|
517
|
+
patch
|
|
518
|
+
);
|
|
519
|
+
await assertOk(response);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/providers/google/utils.ts
|
|
523
|
+
var GOOGLE_BARCODE_TYPE = {
|
|
524
|
+
QR: "QR_CODE",
|
|
525
|
+
PDF417: "PDF_417",
|
|
526
|
+
Aztec: "AZTEC",
|
|
527
|
+
Code128: "CODE_128"
|
|
528
|
+
};
|
|
529
|
+
function toGoogleBarcodeType(format) {
|
|
530
|
+
return GOOGLE_BARCODE_TYPE[format] ?? "QR_CODE";
|
|
531
|
+
}
|
|
532
|
+
function localized(value, language = "en-US", translatedValues) {
|
|
533
|
+
return {
|
|
534
|
+
defaultValue: { language, value },
|
|
535
|
+
translatedValues: translatedValues?.length ? translatedValues : void 0
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function translationsFor(key, locales) {
|
|
539
|
+
if (!locales) {
|
|
540
|
+
return void 0;
|
|
541
|
+
}
|
|
542
|
+
const result = Object.entries(locales).filter(([, t]) => t[key] !== void 0).map(([lang, t]) => ({ language: lang, value: t[key] }));
|
|
543
|
+
return result.length > 0 ? result : void 0;
|
|
544
|
+
}
|
|
545
|
+
function imageUri(url) {
|
|
546
|
+
return typeof url === "string" ? { sourceUri: { uri: url } } : void 0;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/providers/google/index.ts
|
|
550
|
+
var CLASS_TYPE = {
|
|
551
|
+
loyalty: "loyaltyClass",
|
|
552
|
+
event: "eventTicketClass",
|
|
553
|
+
flight: "flightClass",
|
|
554
|
+
coupon: "offerClass",
|
|
555
|
+
giftCard: "giftCardClass",
|
|
556
|
+
generic: "genericClass"
|
|
557
|
+
};
|
|
558
|
+
var OBJECT_TYPE = {
|
|
559
|
+
loyalty: "loyaltyObject",
|
|
560
|
+
event: "eventTicketObject",
|
|
561
|
+
flight: "flightObject",
|
|
562
|
+
coupon: "offerObject",
|
|
563
|
+
giftCard: "giftCardObject",
|
|
564
|
+
generic: "genericObject"
|
|
565
|
+
};
|
|
566
|
+
function validateGoogleRequirements(pass) {
|
|
567
|
+
if (pass.logo !== void 0 && pass.logo instanceof Uint8Array) {
|
|
568
|
+
throw new WalletError(
|
|
569
|
+
"GOOGLE_MISSING_LOGO",
|
|
570
|
+
"Google Wallet logo must be a URL string, not bytes"
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
if (pass.type === "flight") {
|
|
574
|
+
const { carrier, flightNumber, origin, destination } = pass;
|
|
575
|
+
if (!(carrier && flightNumber && origin && destination)) {
|
|
576
|
+
throw new WalletError(
|
|
577
|
+
"GOOGLE_FLIGHT_MISSING_CLASS_FIELDS",
|
|
578
|
+
"Flight passes require carrier, flightNumber, origin, and destination"
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function buildTextModules(fields, values, excludeSlots) {
|
|
584
|
+
const modules = [];
|
|
585
|
+
for (const f of fields) {
|
|
586
|
+
if (excludeSlots.includes(f.slot)) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const value = f.key in values ? values[f.key] : f.value;
|
|
590
|
+
if (value === null || value === void 0) {
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
modules.push({ header: f.label, body: value, id: f.key });
|
|
594
|
+
}
|
|
595
|
+
return modules;
|
|
596
|
+
}
|
|
597
|
+
function buildInfoModuleData(fields, values) {
|
|
598
|
+
const rows = [];
|
|
599
|
+
for (const f of fields) {
|
|
600
|
+
if (f.slot !== "back") {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
const value = f.key in values ? values[f.key] : f.value;
|
|
604
|
+
if (value === null || value === void 0) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
rows.push({ label: f.label, value });
|
|
608
|
+
}
|
|
609
|
+
if (rows.length === 0) {
|
|
610
|
+
return void 0;
|
|
611
|
+
}
|
|
612
|
+
return { labelValueRows: rows.map((r) => ({ columns: [r] })) };
|
|
613
|
+
}
|
|
614
|
+
function buildClassTypeFields(pass, locales) {
|
|
615
|
+
const nameTranslations = translationsFor("name", locales);
|
|
616
|
+
if (pass.type === "loyalty") {
|
|
617
|
+
return { programName: pass.name };
|
|
618
|
+
}
|
|
619
|
+
if (pass.type === "event") {
|
|
620
|
+
return {
|
|
621
|
+
eventName: localized(pass.name, "en-US", nameTranslations),
|
|
622
|
+
localScheduledStartDateTime: pass.startsAt?.replace("Z", ""),
|
|
623
|
+
localScheduledEndDateTime: pass.endsAt?.replace("Z", "")
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
if (pass.type === "flight") {
|
|
627
|
+
return {
|
|
628
|
+
// flightClass represents a single flight — departure time is class-level
|
|
629
|
+
flightHeader: {
|
|
630
|
+
carrier: { carrierIataCode: pass.carrier },
|
|
631
|
+
flightNumber: pass.flightNumber,
|
|
632
|
+
operatingCarrier: { carrierIataCode: pass.carrier },
|
|
633
|
+
operatingFlightNumber: pass.flightNumber
|
|
634
|
+
},
|
|
635
|
+
localScheduledDepartureDateTime: pass.departure?.replace("Z", ""),
|
|
636
|
+
origin: pass.origin ? { airportIataCode: pass.origin } : void 0,
|
|
637
|
+
destination: pass.destination ? {
|
|
638
|
+
airportIataCode: pass.destination,
|
|
639
|
+
localScheduledArrivalDateTime: pass.arrival?.replace("Z", "")
|
|
640
|
+
} : void 0
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
if (pass.type === "coupon") {
|
|
644
|
+
return {
|
|
645
|
+
title: pass.name,
|
|
646
|
+
// provider is required by Google offerClass — defaults to the pass name
|
|
647
|
+
provider: pass.name,
|
|
648
|
+
redemptionChannel: pass.redemptionChannel.toUpperCase()
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
return { cardTitle: localized(pass.name, "en-US", nameTranslations) };
|
|
652
|
+
}
|
|
653
|
+
function buildAppLinkInfo(info) {
|
|
654
|
+
return {
|
|
655
|
+
appLogoImage: imageUri(info.logoUrl),
|
|
656
|
+
title: info.title ? localized(info.title) : void 0,
|
|
657
|
+
description: info.description ? localized(info.description) : void 0,
|
|
658
|
+
appTarget: { targetUri: { uri: info.uri } }
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function buildAppLinkData(d) {
|
|
662
|
+
return {
|
|
663
|
+
androidAppLinkInfo: d.android ? buildAppLinkInfo(d.android) : void 0,
|
|
664
|
+
iosAppLinkInfo: d.ios ? buildAppLinkInfo(d.ios) : void 0,
|
|
665
|
+
webAppLinkInfo: d.web ? buildAppLinkInfo(d.web) : void 0
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function buildClassBody(pass) {
|
|
669
|
+
const logo = imageUri(pass.logo);
|
|
670
|
+
const wideLogo = imageUri(pass.google?.wideLogo);
|
|
671
|
+
const hero = imageUri(
|
|
672
|
+
typeof pass.banner === "string" ? pass.banner : void 0
|
|
673
|
+
);
|
|
674
|
+
const body = {
|
|
675
|
+
...buildClassTypeFields(pass, pass.locales),
|
|
676
|
+
hexBackgroundColor: pass.color,
|
|
677
|
+
issuerName: pass.google?.issuerName ?? pass.name
|
|
678
|
+
};
|
|
679
|
+
if (pass.type === "loyalty") {
|
|
680
|
+
if (logo) {
|
|
681
|
+
body.programLogo = logo;
|
|
682
|
+
}
|
|
683
|
+
} else if (logo) {
|
|
684
|
+
body.logo = logo;
|
|
685
|
+
}
|
|
686
|
+
if (wideLogo) {
|
|
687
|
+
body.wideProgramBanner = wideLogo;
|
|
688
|
+
}
|
|
689
|
+
if (hero) {
|
|
690
|
+
body.heroImage = hero;
|
|
691
|
+
}
|
|
692
|
+
if (pass.type !== "generic") {
|
|
693
|
+
body.reviewStatus = pass.google?.reviewStatus ?? "UNDER_REVIEW";
|
|
694
|
+
}
|
|
695
|
+
if (pass.google?.enableSmartTap) {
|
|
696
|
+
body.enableSmartTap = pass.google.enableSmartTap;
|
|
697
|
+
}
|
|
698
|
+
if (pass.google?.redemptionIssuers) {
|
|
699
|
+
body.redemptionIssuers = pass.google.redemptionIssuers;
|
|
700
|
+
}
|
|
701
|
+
if (pass.google?.messages) {
|
|
702
|
+
body.messages = pass.google.messages;
|
|
703
|
+
}
|
|
704
|
+
if (pass.google?.appLinkData) {
|
|
705
|
+
body.appLinkData = buildAppLinkData(pass.google.appLinkData);
|
|
706
|
+
}
|
|
707
|
+
if (pass.locations?.length) {
|
|
708
|
+
body.locations = pass.locations.map(({ latitude, longitude }) => ({
|
|
709
|
+
latitude,
|
|
710
|
+
longitude
|
|
711
|
+
}));
|
|
712
|
+
}
|
|
713
|
+
return body;
|
|
714
|
+
}
|
|
715
|
+
function buildLoyaltyObjectFields(fields, values) {
|
|
716
|
+
const resolve = (key) => values[key] ?? fields.find((f) => f.key === key)?.value;
|
|
717
|
+
const points = resolve("points");
|
|
718
|
+
const member = resolve("member");
|
|
719
|
+
const memberId = resolve("memberId");
|
|
720
|
+
return {
|
|
721
|
+
loyaltyPoints: points == null ? void 0 : { balance: { string: points } },
|
|
722
|
+
accountName: member ?? void 0,
|
|
723
|
+
accountId: memberId ?? void 0
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function buildFlightObjectFields(_pass, serialNumber, values, warnings) {
|
|
727
|
+
const passengerName = values.passengerName;
|
|
728
|
+
if (!passengerName) {
|
|
729
|
+
warnings.push(
|
|
730
|
+
"Google flight pass: passengerName not set in values \u2014 passenger name will be blank"
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
passengerName: passengerName ?? "",
|
|
735
|
+
reservationInfo: { confirmationCode: serialNumber }
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function buildGiftCardObjectFields(pass, fields, values) {
|
|
739
|
+
const raw = values.balance ?? fields.find((f) => f.key === "balance")?.value;
|
|
740
|
+
return {
|
|
741
|
+
balance: raw == null ? void 0 : {
|
|
742
|
+
micros: String(Math.round(Number.parseFloat(raw) * 1e6)),
|
|
743
|
+
currencyCode: pass.currency ?? "USD"
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function buildDisplayFields(fields, values, locales) {
|
|
748
|
+
const primaryField = fields.find((f) => f.slot === "primary");
|
|
749
|
+
const primaryValue = primaryField && (primaryField.key in values ? values[primaryField.key] : primaryField.value);
|
|
750
|
+
const textModules = buildTextModules(fields, values, ["primary", "back"]);
|
|
751
|
+
return {
|
|
752
|
+
subheader: primaryField && primaryValue != null ? localized(
|
|
753
|
+
primaryField.label,
|
|
754
|
+
"en-US",
|
|
755
|
+
translationsFor(primaryField.key, locales)
|
|
756
|
+
) : void 0,
|
|
757
|
+
header: primaryField && primaryValue != null ? localized(
|
|
758
|
+
primaryValue,
|
|
759
|
+
"en-US",
|
|
760
|
+
translationsFor(`${primaryField.key}_value`, locales)
|
|
761
|
+
) : void 0,
|
|
762
|
+
textModulesData: textModules.length > 0 ? textModules : void 0,
|
|
763
|
+
infoModuleData: buildInfoModuleData(fields, values)
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function buildObjectBody(pass, createConfig, classId, objectId, warnings) {
|
|
767
|
+
const values = createConfig.values ?? {};
|
|
768
|
+
const fields = pass.fields;
|
|
769
|
+
return {
|
|
770
|
+
id: objectId,
|
|
771
|
+
classId,
|
|
772
|
+
state: "ACTIVE",
|
|
773
|
+
barcode: createConfig.barcode ? {
|
|
774
|
+
type: toGoogleBarcodeType(createConfig.barcode.format),
|
|
775
|
+
value: createConfig.barcode.value,
|
|
776
|
+
alternateText: createConfig.barcode.altText ?? createConfig.barcode.value
|
|
777
|
+
} : void 0,
|
|
778
|
+
validTimeInterval: createConfig.validFrom || createConfig.expiresAt ? {
|
|
779
|
+
start: createConfig.validFrom ? { date: createConfig.validFrom } : void 0,
|
|
780
|
+
end: createConfig.expiresAt ? { date: createConfig.expiresAt } : void 0
|
|
781
|
+
} : void 0,
|
|
782
|
+
// Smart Tap: per-recipient redemption value sent to NFC terminals
|
|
783
|
+
smartTapRedemptionValue: createConfig.google?.smartTapRedemptionValue,
|
|
784
|
+
// Rotating barcode replaces the static barcode when set
|
|
785
|
+
rotatingBarcode: createConfig.google?.rotatingBarcode,
|
|
786
|
+
// Per-recipient messages
|
|
787
|
+
messages: createConfig.google?.messages,
|
|
788
|
+
...pass.type === "loyalty" && buildLoyaltyObjectFields(fields, values),
|
|
789
|
+
...pass.type === "flight" && buildFlightObjectFields(
|
|
790
|
+
pass,
|
|
791
|
+
createConfig.serialNumber,
|
|
792
|
+
values,
|
|
793
|
+
warnings
|
|
794
|
+
),
|
|
795
|
+
...pass.type === "giftCard" && buildGiftCardObjectFields(pass, fields, values),
|
|
796
|
+
// genericObject requires cardTitle in the object body (in addition to the class)
|
|
797
|
+
...pass.type === "generic" && {
|
|
798
|
+
cardTitle: localized(
|
|
799
|
+
pass.name,
|
|
800
|
+
"en-US",
|
|
801
|
+
translationsFor("name", pass.locales)
|
|
802
|
+
)
|
|
803
|
+
},
|
|
804
|
+
...buildDisplayFields(fields, values, pass.locales)
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
async function generateGooglePass(pass, createConfig, credentials) {
|
|
808
|
+
const warnings = [];
|
|
809
|
+
validateGoogleRequirements(pass);
|
|
810
|
+
const privateKey = await importGoogleKey(credentials);
|
|
811
|
+
const classType = CLASS_TYPE[pass.type];
|
|
812
|
+
const objectType = OBJECT_TYPE[pass.type];
|
|
813
|
+
const classId = `${credentials.issuerId}.${pass.id}`;
|
|
814
|
+
const objectId = `${credentials.issuerId}.${createConfig.serialNumber}`;
|
|
815
|
+
const classBody = buildClassBody(pass);
|
|
816
|
+
await ensureClass(classType, classId, classBody, credentials, privateKey);
|
|
817
|
+
const objectBody = buildObjectBody(
|
|
818
|
+
pass,
|
|
819
|
+
createConfig,
|
|
820
|
+
classId,
|
|
821
|
+
objectId,
|
|
822
|
+
warnings
|
|
823
|
+
);
|
|
824
|
+
const objectsKey = objectType.replace("Object", "Objects");
|
|
825
|
+
const payload = {
|
|
826
|
+
iss: credentials.clientEmail,
|
|
827
|
+
aud: "google",
|
|
828
|
+
typ: "savetowallet",
|
|
829
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
830
|
+
payload: {
|
|
831
|
+
[objectsKey]: [objectBody]
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
const { SignJWT: SignJWT2 } = await import('jose');
|
|
835
|
+
const jwt = await new SignJWT2(payload).setProtectedHeader({ alg: "RS256" }).sign(privateKey);
|
|
836
|
+
return { pass: jwt, warnings };
|
|
837
|
+
}
|
|
838
|
+
async function updateGooglePass(pass, createConfig, credentials) {
|
|
839
|
+
const privateKey = await importGoogleKey(credentials);
|
|
840
|
+
const objectType = OBJECT_TYPE[pass.type];
|
|
841
|
+
const classId = `${credentials.issuerId}.${pass.id}`;
|
|
842
|
+
const objectId = `${credentials.issuerId}.${createConfig.serialNumber}`;
|
|
843
|
+
const patch = buildObjectBody(pass, createConfig, classId, objectId, []);
|
|
844
|
+
await patchObject(objectType, objectId, patch, credentials, privateKey);
|
|
845
|
+
}
|
|
846
|
+
async function deleteGooglePass(pass, serialNumber, credentials) {
|
|
847
|
+
const privateKey = await importGoogleKey(credentials);
|
|
848
|
+
const objectType = OBJECT_TYPE[pass.type];
|
|
849
|
+
const objectId = `${credentials.issuerId}.${serialNumber}`;
|
|
850
|
+
await deleteObject(objectType, objectId, credentials, privateKey);
|
|
851
|
+
}
|
|
852
|
+
async function expireGooglePass(pass, serialNumber, credentials) {
|
|
853
|
+
const privateKey = await importGoogleKey(credentials);
|
|
854
|
+
const objectType = OBJECT_TYPE[pass.type];
|
|
855
|
+
const objectId = `${credentials.issuerId}.${serialNumber}`;
|
|
856
|
+
await patchObject(
|
|
857
|
+
objectType,
|
|
858
|
+
objectId,
|
|
859
|
+
{ state: "EXPIRED" },
|
|
860
|
+
credentials,
|
|
861
|
+
privateKey
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
var hexColor = zod.z.string().regex(/^#[0-9a-fA-F]{6}$/, 'must be a 6-digit hex color like "#1a1a1a"').optional();
|
|
865
|
+
var BCP47_RE = /^[a-z]{2,3}(-[A-Za-z0-9]+)*$/;
|
|
866
|
+
var localeCodeSchema = zod.z.string().regex(
|
|
867
|
+
BCP47_RE,
|
|
868
|
+
'must be a BCP 47 language tag (e.g. "en-US", "es", "zh-Hans")'
|
|
869
|
+
);
|
|
870
|
+
var imageValue = zod.z.union([
|
|
871
|
+
zod.z.url(),
|
|
872
|
+
zod.z.custom((v) => v instanceof Uint8Array)
|
|
873
|
+
]);
|
|
874
|
+
var imageSet = zod.z.union([
|
|
875
|
+
imageValue,
|
|
876
|
+
zod.z.object({
|
|
877
|
+
base: imageValue,
|
|
878
|
+
retina: imageValue.optional(),
|
|
879
|
+
superRetina: imageValue.optional()
|
|
880
|
+
})
|
|
881
|
+
]).optional();
|
|
882
|
+
var dateStyleSchema = zod.z.enum([
|
|
883
|
+
"none",
|
|
884
|
+
"short",
|
|
885
|
+
"medium",
|
|
886
|
+
"long",
|
|
887
|
+
"full"
|
|
888
|
+
]);
|
|
889
|
+
var numberStyleSchema = zod.z.enum([
|
|
890
|
+
"decimal",
|
|
891
|
+
"percent",
|
|
892
|
+
"scientific",
|
|
893
|
+
"spellOut"
|
|
894
|
+
]);
|
|
895
|
+
var textAlignmentSchema = zod.z.enum([
|
|
896
|
+
"left",
|
|
897
|
+
"center",
|
|
898
|
+
"right",
|
|
899
|
+
"natural"
|
|
900
|
+
]);
|
|
901
|
+
var fieldDefSchema = zod.z.object({
|
|
902
|
+
// Apple: headerFields / primaryFields / secondaryFields / auxiliaryFields / backFields
|
|
903
|
+
// Google: primary → subheader (label) + header (value), others → textModulesData
|
|
904
|
+
slot: zod.z.enum(["header", "primary", "secondary", "auxiliary", "back"]),
|
|
905
|
+
key: zod.z.string(),
|
|
906
|
+
label: zod.z.string(),
|
|
907
|
+
value: zod.z.string().optional(),
|
|
908
|
+
changeMessage: zod.z.string().optional(),
|
|
909
|
+
dateStyle: dateStyleSchema.optional(),
|
|
910
|
+
timeStyle: dateStyleSchema.optional(),
|
|
911
|
+
numberStyle: numberStyleSchema.optional(),
|
|
912
|
+
currencyCode: zod.z.string().optional(),
|
|
913
|
+
textAlignment: textAlignmentSchema.optional(),
|
|
914
|
+
row: zod.z.union([zod.z.literal(0), zod.z.literal(1)]).optional()
|
|
915
|
+
});
|
|
916
|
+
var barcodeFormatSchema = zod.z.enum(["QR", "PDF417", "Aztec", "Code128"]);
|
|
917
|
+
var barcodeSchema = zod.z.object({
|
|
918
|
+
format: barcodeFormatSchema.default("QR"),
|
|
919
|
+
value: zod.z.string().min(1, "barcode.value must not be empty"),
|
|
920
|
+
altText: zod.z.string().optional()
|
|
921
|
+
});
|
|
922
|
+
var beaconSchema = zod.z.object({
|
|
923
|
+
// Required: device UUID of the Bluetooth Low Energy beacon
|
|
924
|
+
proximityUUID: zod.z.uuid(),
|
|
925
|
+
// 16-bit major value to narrow the region of the beacon
|
|
926
|
+
major: zod.z.number().int().min(0).max(65535).optional(),
|
|
927
|
+
// 16-bit minor value to further narrow the region of the beacon
|
|
928
|
+
minor: zod.z.number().int().min(0).max(65535).optional(),
|
|
929
|
+
// Text shown on lock screen when the pass becomes relevant near this beacon
|
|
930
|
+
relevantText: zod.z.string().optional()
|
|
931
|
+
});
|
|
932
|
+
var relevantDateSchema = zod.z.object({
|
|
933
|
+
startDate: zod.z.iso.datetime({
|
|
934
|
+
message: 'must be an ISO datetime e.g. "2024-06-01T20:00:00Z"'
|
|
935
|
+
}),
|
|
936
|
+
endDate: zod.z.iso.datetime({
|
|
937
|
+
message: 'must be an ISO datetime e.g. "2024-06-01T23:00:00Z"'
|
|
938
|
+
}).optional()
|
|
939
|
+
});
|
|
940
|
+
var appleOptionsSchema = zod.z.object({
|
|
941
|
+
// Required by Apple Wallet — validated at create() time
|
|
942
|
+
icon: imageSet,
|
|
943
|
+
// Apple-only image slots
|
|
944
|
+
background: imageSet,
|
|
945
|
+
thumbnail: imageSet,
|
|
946
|
+
footer: imageSet,
|
|
947
|
+
// Apple: description (shown in Wallet list view, defaults to pass name)
|
|
948
|
+
description: zod.z.string().optional(),
|
|
949
|
+
// Apple: logoText (text shown next to the logo, not for poster event tickets)
|
|
950
|
+
logoText: zod.z.string().optional(),
|
|
951
|
+
// Apple: foregroundColor (text color), labelColor (label text color)
|
|
952
|
+
foregroundColor: hexColor,
|
|
953
|
+
labelColor: hexColor,
|
|
954
|
+
// Deprecated — use relevantDates instead
|
|
955
|
+
relevantDate: zod.z.iso.datetime({
|
|
956
|
+
message: 'must be an ISO datetime e.g. "2024-06-01T20:00:00Z"'
|
|
957
|
+
}).optional(),
|
|
958
|
+
// Date intervals during which the pass is relevant (replaces relevantDate)
|
|
959
|
+
relevantDates: zod.z.array(relevantDateSchema).optional(),
|
|
960
|
+
// Groups passes of the same type into a single stack in Wallet
|
|
961
|
+
groupingIdentifier: zod.z.string().optional(),
|
|
962
|
+
// Disables the glossy shine effect rendered over strip images
|
|
963
|
+
suppressStripShine: zod.z.boolean().optional(),
|
|
964
|
+
// NFC payload — message is passed to the contactless reader on tap
|
|
965
|
+
nfc: zod.z.object({
|
|
966
|
+
message: zod.z.string(),
|
|
967
|
+
// Public key used to encrypt the NFC payload (Base64-encoded X.509)
|
|
968
|
+
encryptionPublicKey: zod.z.string().optional()
|
|
969
|
+
}).optional(),
|
|
970
|
+
// Deep link opened when the user taps "Open" on the pass (requires associatedStoreIdentifiers)
|
|
971
|
+
appLaunchURL: zod.z.url().optional(),
|
|
972
|
+
// App Store app IDs — adds an "Open" button that launches your app from Wallet
|
|
973
|
+
associatedStoreIdentifiers: zod.z.array(zod.z.number().int().positive()).optional(),
|
|
974
|
+
// Maximum distance in meters from a location at which the pass is shown
|
|
975
|
+
maxDistance: zod.z.number().positive().optional(),
|
|
976
|
+
// Removes the Share button from the back of the pass
|
|
977
|
+
sharingProhibited: zod.z.boolean().optional(),
|
|
978
|
+
// Arbitrary JSON passed to your companion app via NFC or URL — not shown to users
|
|
979
|
+
userInfo: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
980
|
+
// URL for a web service that receives push update notifications for this pass
|
|
981
|
+
webServiceURL: zod.z.url().optional(),
|
|
982
|
+
// Authentication token sent with web service requests (required with webServiceURL)
|
|
983
|
+
authenticationToken: zod.z.string().min(16).optional(),
|
|
984
|
+
// Bluetooth LE beacons that trigger lock screen relevance
|
|
985
|
+
beacons: zod.z.array(beaconSchema).optional()
|
|
986
|
+
});
|
|
987
|
+
var appleEventOptionsSchema = appleOptionsSchema.extend({
|
|
988
|
+
// Text next to the logo on poster event tickets (use logoText for standard event tickets)
|
|
989
|
+
eventLogoText: zod.z.string().optional(),
|
|
990
|
+
// Background color for the footer bar on poster event tickets
|
|
991
|
+
footerBackgroundColor: hexColor,
|
|
992
|
+
// Disables the header darkening gradient on poster event tickets
|
|
993
|
+
suppressHeaderDarkening: zod.z.boolean().optional(),
|
|
994
|
+
// Derives foreground and label colors from the background image (poster event tickets only)
|
|
995
|
+
useAutomaticColors: zod.z.boolean().optional(),
|
|
996
|
+
// Schemes to validate the pass against (falls back to designed type if all fail)
|
|
997
|
+
preferredStyleSchemes: zod.z.array(zod.z.string()).optional(),
|
|
998
|
+
// Additional App Store app IDs shown in the event guide (poster event tickets only)
|
|
999
|
+
auxiliaryStoreIdentifiers: zod.z.array(zod.z.number().int().positive()).optional(),
|
|
1000
|
+
// Poster event ticket action URLs
|
|
1001
|
+
accessibilityURL: zod.z.url().optional(),
|
|
1002
|
+
addOnURL: zod.z.url().optional(),
|
|
1003
|
+
bagPolicyURL: zod.z.url().optional(),
|
|
1004
|
+
contactVenueEmail: zod.z.email().optional(),
|
|
1005
|
+
contactVenuePhoneNumber: zod.z.string().optional(),
|
|
1006
|
+
contactVenueWebsite: zod.z.url().optional(),
|
|
1007
|
+
directionsInformationURL: zod.z.url().optional(),
|
|
1008
|
+
merchandiseURL: zod.z.url().optional(),
|
|
1009
|
+
orderFoodURL: zod.z.url().optional(),
|
|
1010
|
+
parkingInformationURL: zod.z.url().optional(),
|
|
1011
|
+
purchaseParkingURL: zod.z.url().optional(),
|
|
1012
|
+
sellURL: zod.z.url().optional(),
|
|
1013
|
+
transferURL: zod.z.url().optional(),
|
|
1014
|
+
transitInformationURL: zod.z.url().optional()
|
|
1015
|
+
});
|
|
1016
|
+
var appleFlightOptionsSchema = appleOptionsSchema.extend({
|
|
1017
|
+
changeSeatURL: zod.z.url().optional(),
|
|
1018
|
+
entertainmentURL: zod.z.url().optional(),
|
|
1019
|
+
managementURL: zod.z.url().optional(),
|
|
1020
|
+
purchaseAdditionalBaggageURL: zod.z.url().optional(),
|
|
1021
|
+
purchaseLoungeAccessURL: zod.z.url().optional(),
|
|
1022
|
+
purchaseWifiURL: zod.z.url().optional(),
|
|
1023
|
+
registerServiceAnimalURL: zod.z.url().optional(),
|
|
1024
|
+
reportLostBagURL: zod.z.url().optional(),
|
|
1025
|
+
requestWheelchairURL: zod.z.url().optional(),
|
|
1026
|
+
trackBagsURL: zod.z.url().optional(),
|
|
1027
|
+
transitProviderEmail: zod.z.email().optional(),
|
|
1028
|
+
transitProviderPhoneNumber: zod.z.string().optional(),
|
|
1029
|
+
transitProviderWebsiteURL: zod.z.url().optional(),
|
|
1030
|
+
upgradeURL: zod.z.url().optional()
|
|
1031
|
+
});
|
|
1032
|
+
var googleMessageSchema = zod.z.object({
|
|
1033
|
+
header: zod.z.string(),
|
|
1034
|
+
body: zod.z.string(),
|
|
1035
|
+
id: zod.z.string().optional(),
|
|
1036
|
+
// TEXT (default), expireNotification, or TEXT_AND_NOTIFY (push + in-app)
|
|
1037
|
+
messageType: zod.z.enum(["TEXT", "expireNotification", "TEXT_AND_NOTIFY"]).default("TEXT"),
|
|
1038
|
+
displayInterval: zod.z.object({
|
|
1039
|
+
start: zod.z.object({ date: zod.z.iso.datetime() }).optional(),
|
|
1040
|
+
end: zod.z.object({ date: zod.z.iso.datetime() }).optional()
|
|
1041
|
+
}).optional()
|
|
1042
|
+
});
|
|
1043
|
+
var googleRotatingBarcodeSchema = zod.z.object({
|
|
1044
|
+
type: zod.z.enum(["QR_CODE", "PDF_417", "AZTEC", "CODE_128"]).default("QR_CODE"),
|
|
1045
|
+
// Pattern containing the TOTP placeholder, e.g. "https://example.com/redeem/{totp_value_hex}"
|
|
1046
|
+
valuePattern: zod.z.string().min(1, "rotatingBarcode.valuePattern must not be empty"),
|
|
1047
|
+
totpDetails: zod.z.object({
|
|
1048
|
+
periodMillis: zod.z.string().default("30000"),
|
|
1049
|
+
algorithm: zod.z.literal("TOTP_SHA1").default("TOTP_SHA1"),
|
|
1050
|
+
parameters: zod.z.array(
|
|
1051
|
+
zod.z.object({
|
|
1052
|
+
key: zod.z.string(),
|
|
1053
|
+
valueLength: zod.z.number().int().min(1).max(8)
|
|
1054
|
+
})
|
|
1055
|
+
)
|
|
1056
|
+
}),
|
|
1057
|
+
renderEncoding: zod.z.literal("UTF_8").optional()
|
|
1058
|
+
});
|
|
1059
|
+
var googleAppLinkInfoSchema = zod.z.object({
|
|
1060
|
+
// Deep link URI (e.g. intent:// for Android, https:// scheme for iOS universal links)
|
|
1061
|
+
uri: zod.z.url(),
|
|
1062
|
+
title: zod.z.string().optional(),
|
|
1063
|
+
description: zod.z.string().optional(),
|
|
1064
|
+
logoUrl: zod.z.url().optional()
|
|
1065
|
+
});
|
|
1066
|
+
var googleAppLinkDataSchema = zod.z.object({
|
|
1067
|
+
android: googleAppLinkInfoSchema.optional(),
|
|
1068
|
+
ios: googleAppLinkInfoSchema.optional(),
|
|
1069
|
+
web: googleAppLinkInfoSchema.optional()
|
|
1070
|
+
});
|
|
1071
|
+
var googleOptionsSchema = zod.z.object({
|
|
1072
|
+
// Google: wideLogo — wider variant of the logo shown on some pass layouts
|
|
1073
|
+
wideLogo: zod.z.url().optional(),
|
|
1074
|
+
// Google: issuerName — displayed as the pass issuer
|
|
1075
|
+
issuerName: zod.z.string().optional(),
|
|
1076
|
+
// Required by Google for loyalty, event, flight, coupon, and giftCard classes.
|
|
1077
|
+
// "UNDER_REVIEW" is the default for new classes; set to "APPROVED" once approved in the console.
|
|
1078
|
+
reviewStatus: zod.z.enum(["UNDER_REVIEW", "APPROVED", "REJECTED", "DRAFT"]).default("UNDER_REVIEW"),
|
|
1079
|
+
// Smart Tap NFC — enable tap-to-redeem at supported terminals
|
|
1080
|
+
enableSmartTap: zod.z.boolean().optional(),
|
|
1081
|
+
// Smart Tap issuer IDs allowed to redeem this pass (required when enableSmartTap is true)
|
|
1082
|
+
redemptionIssuers: zod.z.array(zod.z.string()).optional(),
|
|
1083
|
+
// Class-level info messages shown inside the pass view for all holders
|
|
1084
|
+
messages: zod.z.array(googleMessageSchema).optional(),
|
|
1085
|
+
// App link shown on the pass to open a companion app
|
|
1086
|
+
appLinkData: googleAppLinkDataSchema.optional()
|
|
1087
|
+
});
|
|
1088
|
+
var locationSchema = zod.z.object({
|
|
1089
|
+
latitude: zod.z.number(),
|
|
1090
|
+
longitude: zod.z.number(),
|
|
1091
|
+
// Apple: altitude in meters above sea level (optional)
|
|
1092
|
+
altitude: zod.z.number().optional(),
|
|
1093
|
+
// Apple: text shown on lock screen when the pass becomes relevant near this location
|
|
1094
|
+
// Google: no equivalent — ignored
|
|
1095
|
+
relevantText: zod.z.string().optional()
|
|
1096
|
+
});
|
|
1097
|
+
var basePassSchema = zod.z.object({
|
|
1098
|
+
// Apple: description (pass name shown in Wallet list)
|
|
1099
|
+
// Google: cardTitle
|
|
1100
|
+
id: zod.z.string().min(1, "PassConfig missing: id"),
|
|
1101
|
+
name: zod.z.string().min(1, "PassConfig missing: name"),
|
|
1102
|
+
// Apple: backgroundColor
|
|
1103
|
+
// Google: hexBackgroundColor
|
|
1104
|
+
color: hexColor,
|
|
1105
|
+
// Shared logo image.
|
|
1106
|
+
// Apple: logo (accepts bytes or URL)
|
|
1107
|
+
// Google: logo (URL only — validated at create() time)
|
|
1108
|
+
logo: imageSet,
|
|
1109
|
+
// Shared banner image — same visual purpose, different placement per platform.
|
|
1110
|
+
// Apple: strip (shown behind the fields, top of pass)
|
|
1111
|
+
// Google: hero (shown at the bottom of the pass)
|
|
1112
|
+
banner: imageSet,
|
|
1113
|
+
// Geo-relevance — show pass on lock screen when near these coordinates.
|
|
1114
|
+
// Apple: locations[] — up to 10 entries
|
|
1115
|
+
// Google: locations[] — up to 20 entries
|
|
1116
|
+
locations: zod.z.array(locationSchema).optional(),
|
|
1117
|
+
// Display fields — use field.primary(), field.secondary(), etc.
|
|
1118
|
+
// Apple: maps to headerFields / primaryFields / secondaryFields / auxiliaryFields / backFields
|
|
1119
|
+
// Google: primary → subheader + header, all others → textModulesData
|
|
1120
|
+
fields: zod.z.array(fieldDefSchema).default([]),
|
|
1121
|
+
// Translations for field labels and pass-level strings.
|
|
1122
|
+
// Keys are field keys (matching field.key) or the reserved key "name" for the pass title.
|
|
1123
|
+
// Use "fieldKey_value" to translate a field's static default value.
|
|
1124
|
+
// Apple: generates {language}.lproj/pass.strings files in the .pkpass zip.
|
|
1125
|
+
// Google: adds translatedValues to LocalizedString objects.
|
|
1126
|
+
locales: zod.z.record(localeCodeSchema, zod.z.record(zod.z.string(), zod.z.string())).optional(),
|
|
1127
|
+
apple: appleOptionsSchema.optional(),
|
|
1128
|
+
google: googleOptionsSchema.optional()
|
|
1129
|
+
});
|
|
1130
|
+
var loyaltyPassSchema = basePassSchema.extend({
|
|
1131
|
+
type: zod.z.literal("loyalty")
|
|
1132
|
+
// No extra structured props — Google maps field keys by convention:
|
|
1133
|
+
// "points" → loyaltyPoints, "member" → accountName, "memberId" → accountId
|
|
1134
|
+
});
|
|
1135
|
+
var eventPassSchema = basePassSchema.extend({
|
|
1136
|
+
type: zod.z.literal("event"),
|
|
1137
|
+
// Apple: relevant date for lock screen suggestion
|
|
1138
|
+
// Google: localScheduledStartDateTime (required for eventTicketClass)
|
|
1139
|
+
startsAt: zod.z.iso.datetime({
|
|
1140
|
+
message: 'must be an ISO datetime e.g. "2024-06-01T20:00:00Z"'
|
|
1141
|
+
}).optional(),
|
|
1142
|
+
endsAt: zod.z.iso.datetime({
|
|
1143
|
+
message: 'must be an ISO datetime e.g. "2024-06-01T23:00:00Z"'
|
|
1144
|
+
}).optional()
|
|
1145
|
+
}).extend({ apple: appleEventOptionsSchema.optional() });
|
|
1146
|
+
var flightPassSchema = basePassSchema.extend({
|
|
1147
|
+
type: zod.z.literal("flight"),
|
|
1148
|
+
// Apple: transitType (required for boardingPass layout, defaults to "air")
|
|
1149
|
+
// Google: inferred from flightHeader
|
|
1150
|
+
transitType: zod.z.enum(["air", "train", "bus", "boat"]).optional(),
|
|
1151
|
+
// Required by Google flightClass — IATA codes and datetimes
|
|
1152
|
+
// Apple: shown as display fields; provider maps these to the correct slots
|
|
1153
|
+
carrier: zod.z.string().regex(
|
|
1154
|
+
/^[A-Z0-9]{2}$/,
|
|
1155
|
+
'must be a 2-character IATA carrier code e.g. "AA"'
|
|
1156
|
+
).optional(),
|
|
1157
|
+
flightNumber: zod.z.string().regex(/^\d{1,4}[A-Z]?$/, 'must be a flight number e.g. "100" or "1234A"').optional(),
|
|
1158
|
+
origin: zod.z.string().regex(/^[A-Z]{3}$/, 'must be a 3-letter IATA airport code e.g. "JFK"').optional(),
|
|
1159
|
+
destination: zod.z.string().regex(/^[A-Z]{3}$/, 'must be a 3-letter IATA airport code e.g. "LAX"').optional(),
|
|
1160
|
+
departure: zod.z.iso.datetime({
|
|
1161
|
+
message: 'must be an ISO datetime e.g. "2024-06-01T08:00:00Z"'
|
|
1162
|
+
}).optional(),
|
|
1163
|
+
arrival: zod.z.iso.datetime({
|
|
1164
|
+
message: 'must be an ISO datetime e.g. "2024-06-01T11:30:00Z"'
|
|
1165
|
+
}).optional()
|
|
1166
|
+
// passengerName is per-recipient — pass in values at create() time
|
|
1167
|
+
}).extend({ apple: appleFlightOptionsSchema.optional() });
|
|
1168
|
+
var couponPassSchema = basePassSchema.extend({
|
|
1169
|
+
type: zod.z.literal("coupon"),
|
|
1170
|
+
// Google: redemptionChannel (required for offerClass)
|
|
1171
|
+
// Apple: no equivalent — ignored
|
|
1172
|
+
// Defaults to "both" — Google requires this field for offerClass
|
|
1173
|
+
redemptionChannel: zod.z.enum(["online", "instore", "both"]).default("both")
|
|
1174
|
+
});
|
|
1175
|
+
var giftCardPassSchema = basePassSchema.extend({
|
|
1176
|
+
type: zod.z.literal("giftCard"),
|
|
1177
|
+
// Google: balance.currencyCode (needed to format the balance amount)
|
|
1178
|
+
// Apple: use currencyCode on the balance field definition instead
|
|
1179
|
+
currency: zod.z.string().regex(/^[A-Z]{3}$/, 'must be a 3-letter ISO 4217 currency code e.g. "USD"').optional()
|
|
1180
|
+
});
|
|
1181
|
+
var genericPassSchema = basePassSchema.extend({
|
|
1182
|
+
type: zod.z.literal("generic")
|
|
1183
|
+
// No extra structured props — full control via fields
|
|
1184
|
+
});
|
|
1185
|
+
var passConfigSchema = zod.z.discriminatedUnion("type", [
|
|
1186
|
+
loyaltyPassSchema,
|
|
1187
|
+
eventPassSchema,
|
|
1188
|
+
flightPassSchema,
|
|
1189
|
+
couponPassSchema,
|
|
1190
|
+
giftCardPassSchema,
|
|
1191
|
+
genericPassSchema
|
|
1192
|
+
]);
|
|
1193
|
+
var createConfigSchema = zod.z.object({
|
|
1194
|
+
serialNumber: zod.z.string().min(1, "CreateConfig missing: serialNumber"),
|
|
1195
|
+
barcode: barcodeSchema.optional(),
|
|
1196
|
+
// Apple: no equivalent — ignored
|
|
1197
|
+
// Google: validTimeInterval.start
|
|
1198
|
+
validFrom: zod.z.iso.datetime({
|
|
1199
|
+
message: 'must be an ISO datetime e.g. "2024-01-01T00:00:00Z"'
|
|
1200
|
+
}).optional(),
|
|
1201
|
+
expiresAt: zod.z.iso.datetime({
|
|
1202
|
+
message: 'must be an ISO datetime e.g. "2025-01-01T00:00:00Z"'
|
|
1203
|
+
}).optional(),
|
|
1204
|
+
// Per-recipient field values. null hides the field for this recipient.
|
|
1205
|
+
values: zod.z.record(zod.z.string(), zod.z.string().nullable()).optional(),
|
|
1206
|
+
// Apple-specific per-recipient options.
|
|
1207
|
+
apple: zod.z.object({
|
|
1208
|
+
// Mark this issued pass as void. Displays a "Void" banner on the pass.
|
|
1209
|
+
// For Google, use pass.expire() instead — it transitions state via the API.
|
|
1210
|
+
voided: zod.z.boolean().optional()
|
|
1211
|
+
}).optional(),
|
|
1212
|
+
// Google-specific per-recipient options.
|
|
1213
|
+
google: zod.z.object({
|
|
1214
|
+
// Smart Tap NFC value for this specific pass holder (required when enableSmartTap is true)
|
|
1215
|
+
smartTapRedemptionValue: zod.z.string().optional(),
|
|
1216
|
+
// TOTP rotating barcode — replaces the static barcode for this pass holder
|
|
1217
|
+
rotatingBarcode: googleRotatingBarcodeSchema.optional(),
|
|
1218
|
+
// Per-recipient info messages shown inside the pass view
|
|
1219
|
+
messages: zod.z.array(googleMessageSchema).optional()
|
|
1220
|
+
}).optional()
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
// src/pass.ts
|
|
1224
|
+
function resolveOptions(arg) {
|
|
1225
|
+
if (arg === void 0) {
|
|
1226
|
+
return void 0;
|
|
1227
|
+
}
|
|
1228
|
+
return typeof arg === "string" ? { value: arg } : arg;
|
|
1229
|
+
}
|
|
1230
|
+
var field = {
|
|
1231
|
+
/** Top-right of the pass. Compact — typically one field. */
|
|
1232
|
+
header: (key, label, arg) => ({
|
|
1233
|
+
slot: "header",
|
|
1234
|
+
key,
|
|
1235
|
+
label,
|
|
1236
|
+
...resolveOptions(arg)
|
|
1237
|
+
}),
|
|
1238
|
+
/** Large, prominent area. Apple: primaryFields. Google: subheader (label) + header (value). */
|
|
1239
|
+
primary: (key, label, arg) => ({
|
|
1240
|
+
slot: "primary",
|
|
1241
|
+
key,
|
|
1242
|
+
label,
|
|
1243
|
+
...resolveOptions(arg)
|
|
1244
|
+
}),
|
|
1245
|
+
/** Below primary. Apple: secondaryFields. Google: textModulesData. */
|
|
1246
|
+
secondary: (key, label, arg) => ({
|
|
1247
|
+
slot: "secondary",
|
|
1248
|
+
key,
|
|
1249
|
+
label,
|
|
1250
|
+
...resolveOptions(arg)
|
|
1251
|
+
}),
|
|
1252
|
+
/** Below secondary. Supports two rows via { row: 0 | 1 }. Apple: auxiliaryFields. Google: textModulesData. */
|
|
1253
|
+
auxiliary: (key, label, arg) => ({
|
|
1254
|
+
slot: "auxiliary",
|
|
1255
|
+
key,
|
|
1256
|
+
label,
|
|
1257
|
+
...resolveOptions(arg)
|
|
1258
|
+
}),
|
|
1259
|
+
/** Back of the pass — hidden by default. Apple: backFields. Google: infoModuleData. */
|
|
1260
|
+
back: (key, label, arg) => ({
|
|
1261
|
+
slot: "back",
|
|
1262
|
+
key,
|
|
1263
|
+
label,
|
|
1264
|
+
...resolveOptions(arg)
|
|
1265
|
+
})
|
|
1266
|
+
};
|
|
1267
|
+
function validatePassConfig(config) {
|
|
1268
|
+
const result = passConfigSchema.safeParse(config);
|
|
1269
|
+
if (!result.success) {
|
|
1270
|
+
throw new WalletError(
|
|
1271
|
+
"PASS_CONFIG_INVALID",
|
|
1272
|
+
result.error.issues[0]?.message
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
function validateCreateConfig(config) {
|
|
1277
|
+
const result = createConfigSchema.safeParse(config);
|
|
1278
|
+
if (!result.success) {
|
|
1279
|
+
throw new WalletError(
|
|
1280
|
+
"CREATE_CONFIG_INVALID",
|
|
1281
|
+
result.error.issues[0]?.message
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
var Pass = class {
|
|
1286
|
+
config;
|
|
1287
|
+
credentials;
|
|
1288
|
+
constructor(config, credentials) {
|
|
1289
|
+
validatePassConfig(config);
|
|
1290
|
+
this.config = config;
|
|
1291
|
+
this.credentials = credentials;
|
|
1292
|
+
}
|
|
1293
|
+
async create(createConfig) {
|
|
1294
|
+
validateCreateConfig(createConfig);
|
|
1295
|
+
const [appleResult, googleResult] = await Promise.allSettled([
|
|
1296
|
+
this.credentials.apple ? generateApplePass(this.config, createConfig, this.credentials.apple) : Promise.resolve({ pass: null, warnings: [] }),
|
|
1297
|
+
this.credentials.google ? generateGooglePass(this.config, createConfig, this.credentials.google) : Promise.resolve({ pass: null, warnings: [] })
|
|
1298
|
+
]);
|
|
1299
|
+
if (appleResult.status === "rejected") {
|
|
1300
|
+
throw appleResult.reason;
|
|
1301
|
+
}
|
|
1302
|
+
if (googleResult.status === "rejected") {
|
|
1303
|
+
throw googleResult.reason;
|
|
1304
|
+
}
|
|
1305
|
+
return {
|
|
1306
|
+
apple: appleResult.value.pass,
|
|
1307
|
+
google: googleResult.value.pass,
|
|
1308
|
+
warnings: [...appleResult.value.warnings, ...googleResult.value.warnings]
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
// Update an existing pass. For Google: PATCHes the object via the Wallet REST API.
|
|
1312
|
+
async update(createConfig) {
|
|
1313
|
+
validateCreateConfig(createConfig);
|
|
1314
|
+
if (this.credentials.google) {
|
|
1315
|
+
await updateGooglePass(
|
|
1316
|
+
this.config,
|
|
1317
|
+
createConfig,
|
|
1318
|
+
this.credentials.google
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
// Delete a pass. For Google: permanently removes the object via the Wallet REST API.
|
|
1323
|
+
// Apple passes cannot be remotely deleted — use expire() to invalidate them instead.
|
|
1324
|
+
async delete(serialNumber) {
|
|
1325
|
+
if (this.credentials.google) {
|
|
1326
|
+
await deleteGooglePass(
|
|
1327
|
+
this.config,
|
|
1328
|
+
serialNumber,
|
|
1329
|
+
this.credentials.google
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
// Expire a pass. For Google: sets object state to EXPIRED via the Wallet REST API.
|
|
1334
|
+
// Apple passes expire automatically when expiresAt is reached.
|
|
1335
|
+
async expire(serialNumber) {
|
|
1336
|
+
if (this.credentials.google) {
|
|
1337
|
+
await expireGooglePass(
|
|
1338
|
+
this.config,
|
|
1339
|
+
serialNumber,
|
|
1340
|
+
this.credentials.google
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1346
|
+
// src/wallet.ts
|
|
1347
|
+
var Wallet = class {
|
|
1348
|
+
credentials;
|
|
1349
|
+
constructor(credentials) {
|
|
1350
|
+
this.credentials = credentials;
|
|
1351
|
+
}
|
|
1352
|
+
loyalty(config) {
|
|
1353
|
+
return new Pass({ ...config, type: "loyalty" }, this.credentials);
|
|
1354
|
+
}
|
|
1355
|
+
event(config) {
|
|
1356
|
+
return new Pass({ ...config, type: "event" }, this.credentials);
|
|
1357
|
+
}
|
|
1358
|
+
flight(config) {
|
|
1359
|
+
return new Pass({ ...config, type: "flight" }, this.credentials);
|
|
1360
|
+
}
|
|
1361
|
+
coupon(config) {
|
|
1362
|
+
return new Pass({ ...config, type: "coupon" }, this.credentials);
|
|
1363
|
+
}
|
|
1364
|
+
giftCard(config) {
|
|
1365
|
+
return new Pass({ ...config, type: "giftCard" }, this.credentials);
|
|
1366
|
+
}
|
|
1367
|
+
generic(config) {
|
|
1368
|
+
return new Pass({ ...config, type: "generic" }, this.credentials);
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
exports.Pass = Pass;
|
|
1373
|
+
exports.WALLET_ERROR_CODES = WALLET_ERROR_CODES;
|
|
1374
|
+
exports.Wallet = Wallet;
|
|
1375
|
+
exports.WalletError = WalletError;
|
|
1376
|
+
exports.field = field;
|
|
1377
|
+
//# sourceMappingURL=index.cjs.map
|
|
1378
|
+
//# sourceMappingURL=index.cjs.map
|