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 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