A community based topic aggregation platform built on atproto
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}