adattributionkit — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited adattributionkit (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 2 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 2 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Privacy-preserving ad attribution for iOS 17.4+ / Swift 6.3. AdAttributionKit lets ad networks measure conversions (installs and re-engagements) without exposing user-level data. It supports the App Store and alternative marketplaces, and interoperates with SKAdNetwork.
Three roles exist in the attribution flow: the ad network (signs impressions, receives postbacks), the publisher app (displays ads), and the advertised app (the app being promoted).
AdAttributionKit preserves user privacy through several mechanisms:
on the crowd size associated with the ad, ranging from Tier 0 (minimal data) to Tier 3 (most data including publisher ID and country code).
window close (first window) or 24-144 hours (second/third windows).
identifiers and conversion values, not device or user IDs.
number of digits returned depends on the crowd anonymity tier.
In migration and interoperability reviews, explicitly state that the system evaluates AdAttributionKit and SKAdNetwork impressions together, only one impression wins per conversion, click-through beats view-through, and recency breaks ties within click-through impressions before falling back to the most recent view-through impression.
A publisher app displays ads from registered ad networks. Add each ad network's ID to the app's Info.plist so its impressions qualify for install validation.
<key>AdNetworkIdentifiers</key>
<array>
<string>example123.adattributionkit</string>
<string>another456.adattributionkit</string>
</array>Ad network IDs must be lowercase. SKAdNetwork IDs (ending in .skadnetwork) are also accepted -- the frameworks share IDs.
For click-through custom-rendered ads, place one UIEventAttributionView over each tappable ad/control. It must cover the tappable area and stay above views that would intercept touches before handleTap() succeeds.
import UIKit
let attributionView = UIEventAttributionView()
attributionView.frame = adContentView.bounds
attributionView.isUserInteractionEnabled = true
adContentView.addSubview(attributionView)The advertised app is the app someone installs or re-engages with after seeing an ad. It must call a conversion value update at least once to begin the postback conversion window.
Add AttributionCopyEndpoint under the top-level AdAttributionKit Info.plist dictionary so the device sends a copy of the winning postback to your server:
<key>AdAttributionKit</key>
<dict>
<key>AttributionCopyEndpoint</key>
<string>https://example.com</string>
</dict>The system derives the well-known endpoint from the registrable domain in the URL, ignoring subdomains:
https://example.com/.well-known/appattribution/report-attribution/Configure your server to accept HTTPS POST requests at that path. The domain must have a valid SSL certificate.
Add a second key in the same AdAttributionKit dictionary to also receive copies of winning re-engagement postbacks:
<key>AdAttributionKit</key>
<dict>
<key>AttributionCopyEndpoint</key>
<string>https://example.com</string>
<key>OptInForReengagementPostbackCopies</key>
<true/>
</dict>Call a conversion value update as early as possible after first launch to begin the conversion window:
import AdAttributionKit
func applicationDidFinishLaunching() async {
do {
try await Postback.updateConversionValue(0, lockPostback: false)
} catch {
print("Failed to set initial conversion value: \(error)")
}
}Ad networks create signed impressions using JWS (JSON Web Signature). The publisher app uses AppImpression to register and handle those impressions.
import AdAttributionKit
let impression = try await AppImpression(compactJWS: signedJWSString)The JWS contains the ad network ID, advertised item ID, publisher item ID, source identifier, timestamp, and optional re-engagement eligibility flag. See references/adattributionkit-patterns.md for JWS generation details.
guard AppImpression.isSupported else {
// Fall back to alternative ad display
return
}Record a view impression when the ad content has been displayed and dismissed:
func handleAdViewed(impression: AppImpression) async {
do {
try await impression.handleView()
} catch {
print("Failed to record view-through impression: \(error)")
}
}For long-lived ad views, use beginView() and endView() to track view duration:
try await impression.beginView()
// ... ad remains visible ...
try await impression.endView()Respond to ad taps by calling handleTap() within 15 minutes of creating the AppImpression; otherwise request a fresh impression. If the advertised app is not installed, the system opens its App Store or marketplace page. If installed, the system launches it directly.
func handleAdTapped(impression: AppImpression) async {
do {
try await impression.handleTap()
} catch {
print("Failed to record click-through impression: \(error)")
}
}A UIEventAttributionView must overlay the ad for handleTap() to succeed.
Pass the impression to StoreKit overlay or product view controller APIs. StoreKit automatically records view-through impressions after 2 seconds of display and click-through impressions on tap.
import StoreKit
let config = SKOverlay.AppConfiguration(appIdentifier: "1234567890",
position: .bottom)
config.appImpression = impressionPostbacks are attribution reports the device sends to ad networks (and optionally to the advertised app developer) after a conversion event.
Three windows produce up to three postbacks for winning attributions:
| Window | Duration | Postback delay |
|---|---|---|
| 1st | Days 0-2 | 24-48 hours |
| 2nd | Days 3-7 | 24-144 hours |
| 3rd | Days 8-35 | 24-144 hours |
Tier 0 postbacks only produce the first postback. Nonwinning attributions produce only one postback.
| Event | Time limit |
|---|---|
| View-through to install | 24 hours (configurable up to 7 days) |
| Click-through to install | 30 days (configurable down to 1 day) |
| Install to first update | 60 days |
| Re-engagement to first update | 2 days |
Lock the postback to finalize a conversion value before the window ends and receive the postback sooner:
try await Postback.updateConversionValue(
42,
coarseConversionValue: .high,
lockPostback: true
)After locking, the system ignores further updates in that conversion window.
| Field | Tier 0 | Tier 1 | Tier 2 | Tier 3 |
|---|---|---|---|---|
source-identifier digits | 2 | 2 | 2-4 | 2-4 |
conversion-value (fine) | -- | -- | 1st only | 1st only |
coarse-conversion-value | -- | 1st only | 2nd/3rd | 2nd/3rd |
publisher-item-identifier | -- | -- | -- | Yes |
country-code | -- | -- | -- | Conditional |
Fine values are integers from 0...63 (6 bits). They are available only in the first postback and only at Tier 2 or higher:
try await Postback.updateConversionValue(
35,
coarseConversionValue: .medium,
lockPostback: false
)Three levels for lower tiers and second/third postbacks:
// CoarseConversionValue cases: .low, .medium, .high
try await Postback.updateConversionValue(
10,
coarseConversionValue: .high,
lockPostback: false
)Separate conversion values for install vs. re-engagement postbacks. In server JSON, use "conversion-type": "re-engagement" with the hyphen; Swift APIs use .reengagement without it.
let installUpdate = PostbackUpdate(
fineConversionValue: 20,
lockPostback: false,
conversionTypes: [.install]
)
try await Postback.updateConversionValue(installUpdate)
let reengagementUpdate = PostbackUpdate(
fineConversionValue: 12,
lockPostback: false,
conversionTypes: [.reengagement]
)
try await Postback.updateConversionValue(reengagementUpdate)Use conversion tags to selectively update specific postbacks when overlapping conversion windows exist:
let update = PostbackUpdate(
fineConversionValue: 15,
lockPostback: false,
conversionTag: savedConversionTag,
conversionTypes: [.reengagement]
)
try await Postback.updateConversionValue(update)The system delivers the conversion tag through the re-engagement URL's AdAttributionKitReengagementOpen query parameter.
Re-engagement tracks users who already have the advertised app installed and interact with an ad to return to it.
Set eligible-for-re-engagement to true in the JWS payload when generating the impression.
Pass a universal link that the system opens in the advertised app:
let reengagementURL = URL(string: "https://example.com/promo/summer")!
try await impression.handleTap(reengagementURL: reengagementURL)The system appends AdAttributionKitReengagementOpen as a query parameter. The advertised app checks for this parameter to detect AdAttributionKit-driven opens:
func handleUniversalLink(_ url: URL) {
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let isReengagement = components?.queryItems?.contains(where: {
$0.name == Postback.reengagementOpenURLParameter
}) ?? false
if isReengagement {
// AdAttributionKit opened this app via a re-engagement ad
}
}view-through).
limits.
AdAttributionKitReengagementOpen parameter is always present on theURL, even when the system does not create a postback.
// DON'T -- never updating the conversion value
func appDidLaunch() {
// No conversion value update; postback window never starts
}
// DO -- update conversion value on first launch
func appDidLaunch() async {
try? await Postback.updateConversionValue(0, lockPostback: false)
}<!-- DON'T -->
<string>Example123.AdAttributionKit</string>
<!-- DO -->
<string>example123.adattributionkit</string>// DON'T -- tap without a current attribution view tap or fresh impression
try await staleImpression.handleTap()
// Throws if the tap cannot be validated or the impression expired
// DO -- ensure UIEventAttributionView covers the ad and the impression is fresh
let attributionView = UIEventAttributionView()
attributionView.frame = adView.bounds
adView.addSubview(attributionView)
// Then handle the tap within 15 minutes after creating the AppImpression
try await impression.handleTap()// DON'T
try? await impression.handleTap()
// DO -- handle specific errors
do {
try await impression.handleTap()
} catch let error as AdAttributionKitError {
switch error {
case .impressionExpired:
// Impression expired or is stale for click-through handling
refreshAdImpression()
case .missingAttributionView:
// UIEventAttributionView not present
break
default:
print("Attribution error: \(error)")
}
}// DON'T -- silently dropping the request
// The device retries up to 9 times over 9 days on HTTP 500
// DO -- respond with 200 OK immediately
// Server handler:
func handlePostback(request: Request) -> Response {
// Process asynchronously, respond immediately
Task { await processPostback(request.body) }
return Response(status: .ok)
}AdNetworkIdentifiers(lowercase)
kidUIEventAttributionView overlays each tappable click-through ad/controlAppImpression is no older than 15 minutes at handleTap()updateConversionValue on first launchpostback-identifierAppImpression.isSupported checked before attempting impression APIs-- postback verification, server handling, testing, SKAdNetwork migration, alternative marketplaces, attribution rules configuration
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.