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