[G1-Sync] Manual knowledge update

This commit is contained in:
Antigravity Agent
2026-05-09 21:08:02 +09:00
parent f0befc887a
commit 93ec7e9056
363 changed files with 68333 additions and 64 deletions
@@ -0,0 +1,332 @@
---
id: mobile-deep-link-verification
title: Deep Link Verification — Universal / App Links / 검증
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [mobile, deep-link, universal-link, app-link, vibe-coding]
tech_stack: { language: "Swift / Kotlin", applicable_to: ["iOS", "Android"] }
applied_in: []
aliases: [App Links, Universal Links, AASA, assetlinks, deep link verification, deferred deep link]
---
# Deep Link Verification
> Web URL → app. **iOS = Universal Links + AASA, Android = App Links + assetlinks.json**. 검증 안 되면 browser 가 처리. Deferred = install 후 의도 복원.
## 📖 핵심 개념
- Verification: 서버 가 owns this domain.
- AASA (iOS): apple-app-site-association.
- assetlinks (Android): .well-known/assetlinks.json.
- Deferred: install 후 의도 복원 (Branch / Adjust).
## 💻 코드 패턴
### iOS — AASA
```json
// https://example.com/.well-known/apple-app-site-association
// (HTTP/2 + HTTPS + 정확 Content-Type: application/json)
{
"applinks": {
"details": [
{
"appIDs": ["TEAMID.com.example.app"],
"components": [
{ "/": "/order/*" },
{ "/": "/user/*", "?": { "ref": "?*" } },
{ "/": "/admin/*", "exclude": true }
]
}
]
},
"webcredentials": {
"apps": ["TEAMID.com.example.app"]
}
}
```
### iOS — Associated Domains
```
Xcode → Signing & Capabilities → Associated Domains
+ applinks:example.com
+ webcredentials:example.com # password autofill
```
```swift
// SwiftUI handle
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
guard let url = activity.webpageURL else { return }
handle(url)
}
.onOpenURL { url in
handle(url)
}
func handle(_ url: URL) {
let comps = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let path = comps?.path else { return }
// Path / params
if path.hasPrefix("/order/") {
let id = String(path.dropFirst("/order/".count))
guard isValidUuid(id) else { return }
navigationStore.navigate(.order(id))
}
}
```
### Android — assetlinks.json
```json
// https://example.com/.well-known/assetlinks.json
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": ["AA:BB:CC:..."]
}
}
]
```
→ SHA256 = signing cert. release / debug 둘 다 포함 가능.
```bash
# Cert fingerprint 추출
keytool -list -v -keystore my-release-key.keystore
# 또는 Play Console — App signing
```
### Android — Manifest
```xml
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTask">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="example.com" />
<data android:pathPattern="/order/.*" />
</intent-filter>
</activity>
```
`android:autoVerify="true"` = OS 가 install 시 verify.
### Android — handle
```kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent?.data?.let(::handleDeepLink)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
intent.data?.let(::handleDeepLink)
}
private fun handleDeepLink(uri: Uri) {
val path = uri.path ?: return
if (path.startsWith("/order/")) {
val id = path.removePrefix("/order/")
navigate(Screen.Order(id))
}
}
}
```
### Verification check
```bash
# iOS
swcutil verify --domain example.com --bundle-identifier com.example.app
# Android
adb shell pm get-app-links com.example.app
# 또는 Play Console → App Links
# Apple validator
https://branch.io/resources/aasa-validator/
```
### 검증 실패 원인
```
iOS:
- AASA 가 redirect / 401
- Content-Type 가 application/json X
- 잘못된 appID (TeamID 정확)
- 옛 cache (다음 install 까지 wait)
Android:
- assetlinks.json HTTP / 잘못
- SHA256 fingerprint 불일치 (debug vs release)
- pathPattern 불일치
- autoVerify 안 한 channel
```
### Cache 무효화
```bash
# iOS
xcrun simctl openurl booted https://example.com/test
# Android — clear app links
adb shell pm clear-app-links com.example.app
adb shell pm verify-app-links --re-verify com.example.app
```
### Deferred deep link (install 후 의도 복원)
```
사용자가 ad → install → first launch.
원래 URL 의 의도 (특정 product) 복원.
iOS / Android 자체 X — 외부 SDK:
- Branch.io
- Adjust
- AppsFlyer
- Singular
```
```ts
// Branch
import branch from 'react-native-branch';
const { params } = await branch.getLatestReferringParams();
if (params.product_id) navigation.navigate('Product', { id: params.product_id });
```
### Custom URL scheme (legacy)
```xml
<!-- iOS — Info.plist -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array><string>myapp</string></array>
</dict>
</array>
```
```kotlin
<!-- Android -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>
```
`myapp://order/123` — but Universal/App Links 권장. Custom scheme 위조 가능.
### 보안 — Path / params 검증
```swift
func handle(_ url: URL) {
// 1. Whitelist host
guard url.host == "example.com" else { return }
// 2. Path validation
guard let path = url.path.split(...) else { return }
// 3. Param sanitization
guard let id = url.queryItems["id"], isValidUuid(id) else { return }
// 4. Auth link login X
if requiresLogin(path) && !isLoggedIn() {
navigateAfterLogin(.path(path))
return
}
navigate(...)
}
```
### Notification + deep link
```ts
// Push payload
{
aps: { alert: { ... } },
link: 'https://example.com/order/42'
}
```
```swift
// Tap notification openURL
let link = userInfo["link"] as? String
if let url = URL(string: link) { handle(url) }
```
### Web fallback
```html
<!-- App 미설치 시 web 표시 -->
<!-- example.com/order/42 -->
<!DOCTYPE html>
<html>
<head>
<title>Order 42</title>
<meta name="apple-itunes-app" content="app-id=123456789">
<meta property="al:android:url" content="myapp://order/42">
<meta property="al:android:package" content="com.example.app">
</head>
<body>
<p>Open in app</p>
<a href="myapp://order/42">Open in app</a>
<a href="https://apps.apple.com/app/...">Install on iOS</a>
<a href="https://play.google.com/store/apps/...">Install on Android</a>
</body>
</html>
```
### App Clip (iOS) / Instant App (Android)
```
사용자가 install 안 해도 app 의 작은 부분 실행.
QR / NFC / link 로.
```
→ [[iOS_App_Clips]].
### Test
```ts
// Maestro
- launchApp:
arguments:
url: "https://example.com/order/42"
# adb
adb shell am start -W -a android.intent.action.VIEW -d "https://example.com/order/42" com.example.app
# iOS
xcrun simctl openurl booted "https://example.com/order/42"
```
## 🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| 일반 deep link | Universal Links (iOS) + App Links (Android) |
| Marketing campaign | + Deferred (Branch / Adjust) |
| Private (auth callback) | Custom scheme (잠시 OK) |
| App Clip / Instant App | 가벼운 entry point |
| Notification 안 link | Universal Link 또는 custom scheme |
## ❌ 안티패턴
- **AASA / assetlinks 가 redirect**: verification 깨짐.
- **SHA256 fingerprint 불일치 (release / debug 다름)**: 옛 release 가 안 됨.
- **Path / params 검증 X**: SQL / XSS injection.
- **Deep link 가 auth 우회**: 인증 검사 매번.
- **Custom scheme 만 의존**: 위조 가능.
- **Wildcard path (`*`)**: 의도 외 link.
- **Web fallback 없음**: 미설치 시 dead link.
## 🤖 LLM 활용 힌트
- AASA + assetlinks + Associated Domains + autoVerify 4종.
- Path / params validate (zod-style).
- Auth check 매번.
- Deferred = Branch / Adjust.
## 🔗 관련 문서
- [[iOS_Universal_Links_Deep_Linking]]
- [[Android_Bluetooth_LE_Scanning]]
- [[Mobile_Push_Deep]]