A community based topic aggregation platform built on atproto
at main 5.5 kB view raw
1package wellknown 2 3import ( 4 "encoding/json" 5 "log/slog" 6 "net/http" 7 "os" 8) 9 10// HandleAppleAppSiteAssociation serves the iOS Universal Links configuration 11// GET /.well-known/apple-app-site-association 12// 13// Universal Links provide cryptographic binding between the app and domain: 14// - Requires apple-app-site-association file served over HTTPS 15// - App must have Associated Domains capability configured 16// - System verifies domain ownership before routing deep links 17// - Prevents malicious apps from intercepting deep links 18// 19// Spec: https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app 20func HandleAppleAppSiteAssociation(w http.ResponseWriter, r *http.Request) { 21 // Get Apple App ID from environment (format: <Team ID>.<Bundle ID>) 22 // Example: "ABCD1234.social.coves.app" 23 // Find Team ID in Apple Developer Portal -> Membership 24 // Bundle ID is configured in Xcode project 25 appleAppID := os.Getenv("APPLE_APP_ID") 26 if appleAppID == "" { 27 // Development fallback - allows testing without real Team ID 28 // IMPORTANT: This MUST be set in production for Universal Links to work 29 appleAppID = "DEVELOPMENT.social.coves.app" 30 slog.Warn("APPLE_APP_ID not set, using development placeholder", 31 "app_id", appleAppID, 32 "note", "Set APPLE_APP_ID env var for production Universal Links") 33 } 34 35 // Apple requires application/json content type (no charset) 36 w.Header().Set("Content-Type", "application/json") 37 38 // Construct the response per Apple's spec 39 // See: https://developer.apple.com/documentation/bundleresources/applinks 40 response := map[string]interface{}{ 41 "applinks": map[string]interface{}{ 42 "apps": []string{}, // Must be empty array per Apple spec 43 "details": []map[string]interface{}{ 44 { 45 "appID": appleAppID, 46 // Paths that trigger Universal Links when opened in Safari/other apps 47 // These URLs will open the app instead of the browser 48 "paths": []string{ 49 "/app/oauth/callback", // Primary Universal Link OAuth callback 50 "/app/oauth/callback/*", // Catch-all for query params 51 }, 52 }, 53 }, 54 }, 55 } 56 57 if err := json.NewEncoder(w).Encode(response); err != nil { 58 slog.Error("failed to encode apple-app-site-association", "error", err) 59 http.Error(w, "internal server error", http.StatusInternalServerError) 60 return 61 } 62 63 slog.Debug("served apple-app-site-association", "app_id", appleAppID) 64} 65 66// HandleAssetLinks serves the Android App Links configuration 67// GET /.well-known/assetlinks.json 68// 69// App Links provide cryptographic binding between the app and domain: 70// - Requires assetlinks.json file served over HTTPS 71// - App must have intent-filter with android:autoVerify="true" 72// - System verifies domain ownership via SHA-256 certificate fingerprint 73// - Prevents malicious apps from intercepting deep links 74// 75// Spec: https://developer.android.com/training/app-links/verify-android-applinks 76func HandleAssetLinks(w http.ResponseWriter, r *http.Request) { 77 // Get Android package name from environment 78 // Example: "social.coves.app" 79 androidPackage := os.Getenv("ANDROID_PACKAGE_NAME") 80 if androidPackage == "" { 81 androidPackage = "social.coves.app" // Default for development 82 slog.Warn("ANDROID_PACKAGE_NAME not set, using default", 83 "package", androidPackage, 84 "note", "Set ANDROID_PACKAGE_NAME env var for production App Links") 85 } 86 87 // Get SHA-256 fingerprint from environment 88 // This is the SHA-256 fingerprint of the app's signing certificate 89 // 90 // To get the fingerprint: 91 // Production: keytool -list -v -keystore release.jks -alias release 92 // Debug: keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android 93 // 94 // Look for "SHA256:" in the output 95 // Format: AA:BB:CC:DD:...:FF (64 hex characters separated by colons) 96 androidFingerprint := os.Getenv("ANDROID_SHA256_FINGERPRINT") 97 if androidFingerprint == "" { 98 // Development fallback - this won't work for real App Links verification 99 // IMPORTANT: This MUST be set in production for App Links to work 100 androidFingerprint = "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00" 101 slog.Warn("ANDROID_SHA256_FINGERPRINT not set, using development placeholder", 102 "fingerprint", androidFingerprint, 103 "note", "Set ANDROID_SHA256_FINGERPRINT env var for production App Links") 104 } 105 106 w.Header().Set("Content-Type", "application/json") 107 108 // Construct the response per Google's Digital Asset Links spec 109 // See: https://developers.google.com/digital-asset-links/v1/getting-started 110 response := []map[string]interface{}{ 111 { 112 // delegate_permission/common.handle_all_urls grants the app permission 113 // to handle URLs for this domain 114 "relation": []string{"delegate_permission/common.handle_all_urls"}, 115 "target": map[string]interface{}{ 116 "namespace": "android_app", 117 "package_name": androidPackage, 118 // List of certificate fingerprints that can sign the app 119 // Multiple fingerprints can be provided for different signing keys 120 // (e.g., debug + release) 121 "sha256_cert_fingerprints": []string{ 122 androidFingerprint, 123 }, 124 }, 125 }, 126 } 127 128 if err := json.NewEncoder(w).Encode(response); err != nil { 129 slog.Error("failed to encode assetlinks.json", "error", err) 130 http.Error(w, "internal server error", http.StatusInternalServerError) 131 return 132 } 133 134 slog.Debug("served assetlinks.json", 135 "package", androidPackage, 136 "fingerprint", androidFingerprint) 137}