@@ -47,10 +47,11 @@ import {
4747
4848// Session user type
4949export interface SessionUser {
50- id : string ; // Google user ID (sub claim)
50+ id : string ; // User ID (Google sub claim or Apple sub )
5151 email : string ;
5252 name : string ;
5353 picture : string ;
54+ provider : 'google' | 'apple' ; // Authentication provider
5455}
5556
5657// Extend express session
@@ -80,6 +81,11 @@ export interface AppConfig {
8081 systemKey ?: string ; // For x402 gateway integration
8182 s3AdminJwt ?: string ; // For internal S3 fetch (share links)
8283 s3InternalUrl ?: string ; // Internal S3 endpoint (default: http://127.0.0.1:9000)
84+ // Apple Sign-In configuration
85+ appleClientId ?: string ;
86+ appleTeamId ?: string ;
87+ appleKeyId ?: string ;
88+ applePrivateKey ?: string ;
8389}
8490
8591// Database operations type (async for PostgreSQL)
@@ -298,7 +304,7 @@ export function createApp(config: AppConfig, options?: { skipRateLimit?: boolean
298304 contentSecurityPolicy : {
299305 directives : {
300306 defaultSrc : [ "'self'" ] ,
301- scriptSrc : [ "'self'" , "'unsafe-inline'" , "'wasm-unsafe-eval'" , "https://accounts.google.com" , "https://apis.google.com" , "https://www.gstatic.com" ] ,
307+ scriptSrc : [ "'self'" , "'unsafe-inline'" , "'wasm-unsafe-eval'" , "https://accounts.google.com" , "https://apis.google.com" , "https://www.gstatic.com" , "https://appleid.cdn-apple.com" ] ,
302308 styleSrc : [ "'self'" , "'unsafe-inline'" , "https://fonts.googleapis.com" , "https://accounts.google.com" ] ,
303309 fontSrc : [ "'self'" , "https://fonts.gstatic.com" ] ,
304310 imgSrc : [ "'self'" , "data:" , "https:" , "blob:" ] ,
@@ -345,8 +351,10 @@ export function createApp(config: AppConfig, options?: { skipRateLimit?: boolean
345351 // Solana (for Phantom)
346352 "https://*.solana.com" ,
347353 "wss://*.solana.com" ,
354+ // Apple Sign-In
355+ "https://appleid.apple.com" ,
348356 ] ,
349- frameSrc : [ "'self'" , "blob:" , "https://accounts.google.com" , "https://*.phantom.app" , "https://verify.walletconnect.org" , "https://verify.walletconnect.com" , "https://*.walletconnect.org" , "https://*.walletconnect.com" ] ,
357+ frameSrc : [ "'self'" , "blob:" , "https://accounts.google.com" , "https://appleid.apple.com" , "https:// *.phantom.app", "https://verify.walletconnect.org" , "https://verify.walletconnect.com" , "https://*.walletconnect.org" , "https://*.walletconnect.com" ] ,
350358 objectSrc : [ "'self'" , "blob:" ] ,
351359 mediaSrc : [ "'self'" , "blob:" ] ,
352360 frameAncestors : [ "'self'" ] ,
@@ -453,6 +461,7 @@ export function createApp(config: AppConfig, options?: { skipRateLimit?: boolean
453461 email : email ,
454462 name : name || '' ,
455463 picture : picture || '' ,
464+ provider : 'google' ,
456465 } ;
457466
458467 res . json ( {
@@ -466,6 +475,68 @@ export function createApp(config: AppConfig, options?: { skipRateLimit?: boolean
466475 }
467476 } ) ;
468477
478+ // Apple Sign-In endpoint
479+ app . post ( '/auth/apple' , async ( req : Request , res : Response ) => {
480+ try {
481+ const { identityToken, user : appleUser , referralCode } = req . body ;
482+
483+ if ( ! identityToken ) {
484+ return res . status ( 400 ) . json ( { error : 'Missing identity token' } ) ;
485+ }
486+
487+ if ( ! config . appleClientId ) {
488+ return res . status ( 500 ) . json ( { error : 'Apple Sign-In not configured' } ) ;
489+ }
490+
491+ // Dynamically import apple-signin-auth (ESM module)
492+ const AppleSignIn = await import ( 'apple-signin-auth' ) ;
493+
494+ // Verify the identity token with Apple
495+ const applePayload = await AppleSignIn . default . verifyIdToken ( identityToken , {
496+ audience : config . appleClientId ,
497+ ignoreExpiration : false ,
498+ } ) ;
499+
500+ const { sub, email : tokenEmail } = applePayload ;
501+
502+ if ( ! sub ) {
503+ return res . status ( 400 ) . json ( { error : 'Invalid token: missing user ID' } ) ;
504+ }
505+
506+ // Apple only sends email on first sign-in, so we need to handle both cases
507+ // Priority: token email > user object email
508+ const userEmail = tokenEmail || appleUser ?. email ;
509+
510+ if ( ! userEmail ) {
511+ return res . status ( 400 ) . json ( { error : 'Email is required. Please ensure you share your email with the app.' } ) ;
512+ }
513+
514+ // Get name from user object (only provided on first sign-in)
515+ const userName = appleUser ?. name
516+ ? `${ appleUser . name . firstName || '' } ${ appleUser . name . lastName || '' } ` . trim ( )
517+ : '' ;
518+
519+ const user = await dbOps . getOrCreateUser ( userEmail , userName , '' , referralCode || undefined ) ;
520+
521+ req . session . user = {
522+ id : sub ,
523+ email : userEmail ,
524+ name : userName || user . name || '' ,
525+ picture : '' , // Apple doesn't provide profile pictures
526+ provider : 'apple' ,
527+ } ;
528+
529+ res . json ( {
530+ success : true ,
531+ user : req . session . user ,
532+ isNew : user . isNew ,
533+ } ) ;
534+ } catch ( error ) {
535+ console . error ( '[webui] Apple auth error:' , error ) ;
536+ res . status ( 401 ) . json ( { error : 'Authentication failed' } ) ;
537+ }
538+ } ) ;
539+
469540 app . post ( '/auth/logout' , ( req : Request , res : Response ) => {
470541 req . session . destroy ( ( err ) => {
471542 if ( err ) {
0 commit comments