···
10
+
// HandleAppleAppSiteAssociation serves the iOS Universal Links configuration
11
+
// GET /.well-known/apple-app-site-association
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
19
+
// Spec: https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app
20
+
func 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")
35
+
// Apple requires application/json content type (no charset)
36
+
w.Header().Set("Content-Type", "application/json")
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{}{
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
49
+
"/app/oauth/callback", // Primary Universal Link OAuth callback
50
+
"/app/oauth/callback/*", // Catch-all for query params
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)
63
+
slog.Debug("served apple-app-site-association", "app_id", appleAppID)
66
+
// HandleAssetLinks serves the Android App Links configuration
67
+
// GET /.well-known/assetlinks.json
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
75
+
// Spec: https://developer.android.com/training/app-links/verify-android-applinks
76
+
func 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")
87
+
// Get SHA-256 fingerprint from environment
88
+
// This is the SHA-256 fingerprint of the app's signing certificate
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
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")
106
+
w.Header().Set("Content-Type", "application/json")
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{}{
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,
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)
134
+
slog.Debug("served assetlinks.json",
135
+
"package", androidPackage,
136
+
"fingerprint", androidFingerprint)