33 * Run from the extension bundle root:
44 * npx tsx src/shared/__tests__/test-wallet-passes.ts
55 */
6+ import { createSign } from 'node:crypto'
67import fs from 'node:fs'
78import path from 'node:path'
89import { config } from 'dotenv'
@@ -23,8 +24,154 @@ const dummyTicket: WalletPassInput = {
2324 websiteUrl : 'https://programmier.bar' ,
2425}
2526
27+ // --- Google Wallet REST API helpers ---
28+
29+ function base64url ( input : string | Buffer ) : string {
30+ const buf = typeof input === 'string' ? Buffer . from ( input ) : input
31+ return buf . toString ( 'base64' ) . replace ( / \+ / g, '-' ) . replace ( / \/ / g, '_' ) . replace ( / = + $ / , '' )
32+ }
33+
34+ function createOAuthJwt ( serviceAccountEmail : string , privateKey : string ) : string {
35+ const now = Math . floor ( Date . now ( ) / 1000 )
36+ const header = { alg : 'RS256' , typ : 'JWT' }
37+ const payload = {
38+ iss : serviceAccountEmail ,
39+ scope : 'https://www.googleapis.com/auth/wallet_object.issuer' ,
40+ aud : 'https://oauth2.googleapis.com/token' ,
41+ iat : now ,
42+ exp : now + 3600 ,
43+ }
44+ const encodedHeader = base64url ( JSON . stringify ( header ) )
45+ const encodedPayload = base64url ( JSON . stringify ( payload ) )
46+ const sign = createSign ( 'RSA-SHA256' )
47+ sign . update ( `${ encodedHeader } .${ encodedPayload } ` )
48+ const signature = base64url ( sign . sign ( privateKey ) )
49+ return `${ encodedHeader } .${ encodedPayload } .${ signature } `
50+ }
51+
52+ async function getAccessToken ( serviceAccountEmail : string , privateKey : string ) : Promise < string > {
53+ const jwt = createOAuthJwt ( serviceAccountEmail , privateKey )
54+ const res = await fetch ( 'https://oauth2.googleapis.com/token' , {
55+ method : 'POST' ,
56+ headers : { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
57+ body : `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${ jwt } ` ,
58+ } )
59+ const data = await res . json ( ) as any
60+ if ( ! res . ok ) {
61+ throw new Error ( `OAuth failed: ${ JSON . stringify ( data ) } ` )
62+ }
63+ return data . access_token
64+ }
65+
66+ async function upsertGoogleWalletClass ( env : Record < string , string > ) : Promise < void > {
67+ const issuerId = env . GOOGLE_WALLET_ISSUER_ID
68+ const email = env . GOOGLE_WALLET_SERVICE_ACCOUNT_EMAIL
69+ const privateKey = Buffer . from ( env . GOOGLE_WALLET_PRIVATE_KEY_BASE64 , 'base64' ) . toString ( 'utf-8' )
70+
71+ // Must match the constant in wallet-pass-generator.ts
72+ const classSuffix = 'programmiercon_ticket_v4'
73+ const classId = `${ issuerId } .${ classSuffix } `
74+
75+ const classDefinition = {
76+ id : classId ,
77+ issuerName : 'programmier.bar' ,
78+ logo : {
79+ sourceUri : {
80+ uri : `${ dummyTicket . websiteUrl } /wallet_google_logo.png` ,
81+ } ,
82+ } ,
83+ hexBackgroundColor : '#003F64' ,
84+ eventName : {
85+ defaultValue : {
86+ language : 'de' ,
87+ value : dummyTicket . conferenceTitle ,
88+ } ,
89+ } ,
90+ venue : {
91+ name : {
92+ defaultValue : {
93+ language : 'de' ,
94+ value : 'Bad Nauheim' ,
95+ } ,
96+ } ,
97+ address : {
98+ defaultValue : {
99+ language : 'de' ,
100+ value : 'Bad Nauheim, Deutschland' ,
101+ } ,
102+ } ,
103+ } ,
104+ dateTime : {
105+ start : dummyTicket . conferenceDate ,
106+ end : dummyTicket . conferenceEndDate ,
107+ } ,
108+ reviewStatus : 'UNDER_REVIEW' ,
109+ }
110+
111+ console . log ( 'Getting OAuth access token...' )
112+ const accessToken = await getAccessToken ( email , privateKey )
113+
114+ console . log ( `Upserting class ${ classId } ...` )
115+
116+ // Try PUT (update) first, then POST (create) if 404
117+ const putRes = await fetch (
118+ `https://walletobjects.googleapis.com/walletobjects/v1/eventTicketClass/${ classId } ` ,
119+ {
120+ method : 'PUT' ,
121+ headers : {
122+ Authorization : `Bearer ${ accessToken } ` ,
123+ 'Content-Type' : 'application/json' ,
124+ } ,
125+ body : JSON . stringify ( classDefinition ) ,
126+ }
127+ )
128+
129+ if ( putRes . ok ) {
130+ console . log ( 'Class updated successfully via REST API' )
131+ const data = await putRes . json ( )
132+ console . log ( 'Class data:' , JSON . stringify ( data , null , 2 ) )
133+ return
134+ }
135+
136+ if ( putRes . status === 404 ) {
137+ console . log ( 'Class not found, creating...' )
138+ const postRes = await fetch (
139+ 'https://walletobjects.googleapis.com/walletobjects/v1/eventTicketClass' ,
140+ {
141+ method : 'POST' ,
142+ headers : {
143+ Authorization : `Bearer ${ accessToken } ` ,
144+ 'Content-Type' : 'application/json' ,
145+ } ,
146+ body : JSON . stringify ( classDefinition ) ,
147+ }
148+ )
149+ if ( postRes . ok ) {
150+ console . log ( 'Class created successfully via REST API' )
151+ const data = await postRes . json ( )
152+ console . log ( 'Class data:' , JSON . stringify ( data , null , 2 ) )
153+ } else {
154+ const err = await postRes . text ( )
155+ console . error ( `Failed to create class (${ postRes . status } ): ${ err } ` )
156+ }
157+ return
158+ }
159+
160+ const err = await putRes . text ( )
161+ console . error ( `Failed to update class (${ putRes . status } ): ${ err } ` )
162+ }
163+
164+ // --- Main ---
165+
26166async function main ( ) {
27167 const env = process . env as Record < string , string >
168+ const command = process . argv [ 2 ]
169+
170+ // If called with "upsert-class", just create/update the Google class via REST API
171+ if ( command === 'upsert-class' ) {
172+ await upsertGoogleWalletClass ( env )
173+ return
174+ }
28175
29176 console . log ( '\n--- Apple Wallet ---' )
30177 const hasAppleConfig = ! ! (
@@ -57,7 +204,6 @@ async function main() {
57204 console . log ( '\n--- Google Wallet ---' )
58205 const hasGoogleConfig = ! ! (
59206 env . GOOGLE_WALLET_ISSUER_ID &&
60- env . GOOGLE_WALLET_CLASS_ID &&
61207 env . GOOGLE_WALLET_SERVICE_ACCOUNT_EMAIL &&
62208 env . GOOGLE_WALLET_PRIVATE_KEY_BASE64
63209 )
@@ -90,7 +236,7 @@ async function main() {
90236 ' Apple: APPLE_WALLET_PASS_TYPE_ID, APPLE_WALLET_TEAM_ID, APPLE_WALLET_SIGNER_CERT_BASE64, APPLE_WALLET_SIGNER_KEY_BASE64, APPLE_WALLET_WWDR_BASE64'
91237 )
92238 console . log (
93- ' Google: GOOGLE_WALLET_ISSUER_ID, GOOGLE_WALLET_CLASS_ID, GOOGLE_WALLET_SERVICE_ACCOUNT_EMAIL, GOOGLE_WALLET_PRIVATE_KEY_BASE64'
239+ ' Google: GOOGLE_WALLET_ISSUER_ID, GOOGLE_WALLET_SERVICE_ACCOUNT_EMAIL, GOOGLE_WALLET_PRIVATE_KEY_BASE64'
94240 )
95241 }
96242}
0 commit comments