dumbtv
dumbtv가 ADB로 Fire TV에서 광고·추적·업데이트 알림을 제거, 루트·앱 변경 없이, 무료 오픈소스
무료 체험 남용·쿠폰 남용·자기 추천을 차단
Stop free-trial abuse, coupon abuse and self-referrals. FingerPrint Checker gives every visitor and app install a server-verified device ID and answers allow, verify or deny for trials, coupons, invit
보안Swift · TypeScriptGitHub @AndrewYukon온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
Open-source SDKs for FingerPrint Checker: device identity and free-trial abuse protection for web apps, iOS apps and Android apps
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗English | 简体中文 | 日本語 | Español | Français
Device identity and free-trial abuse protection for web apps, iOS apps and Android apps (plus macOS apps and command-line tools), from FingerPrint Checker.
Your client collects a small set of device signals, seals them with an HMAC, optionally attaches a platform attestation
(Play Integrity on Android, App Attest and DeviceCheck on Apple devices), and receives a stable visitorId, a trust
score (only if the project's scoreDetails policy allows it), and an attestation token. Your own backend sends that token to the FingerPrint Checker API and gets back
allow, a human check, or deny for free trials, coupons, sign-up bonuses, invite rewards and anonymous usage limits.
This repository holds the open-source native SDKs and the protocol they speak.
People who abuse free trials don't stop at one browser. They try a second browser, then the laptop, then the phone, then your app. FingerPrint Checker refuses all of them once one has claimed. Every case below was tested on real devices (a Mac with three browsers, an iPhone, two Android phones, one home Wi-Fi):
visitorId.GET /v1/claims/flags. Nothing is revoked automatically.| Platform | How to integrate | Status |
|---|---|---|
| Web apps (any browser) | The FingerPrint Checker browser agent fp-agent.min.js, served from your own domain; see Web apps | Available |
| Android apps (Kotlin / Java) | com.fpchecker:fpchecker-android on Maven Central, source in android/ | Available, 0.7.0 |
| iOS apps (Swift Package) | https://github.com/AndrewYukon/FingerPrintChecker, source in apple/ | Available, 0.7.1 |
| macOS apps (Swift Package) | Same package as iOS | Available, 0.7.1 |
| Flutter apps | git dependency, ref flutter/v0.1.1, source in flutter/ | Available, 0.1.1 |
| React Native apps | npm install @fingerprintchecker/react-native, source in react-native/ | Available, 0.1.1 |
| Command-line tools | Protocol + examples in cli/ | Examples |
| Management CLI and MCP server | npx -y @fingerprintchecker/cli, source in tools/cli/: manage projects, rulesets and webhooks from a terminal or an AI assistant |
Every platform ends the same way: the client gets an attestation, and only your backend, holding your secret key,
asks for a decision. The wire format of the native SDKs is specified in PROTOCOL.md; conformance vectors
live in test-vectors/protocol-v1.json and every SDK must reproduce them.
The newer v2 protocol is described in PROTOCOL-V2.md.
Protocol v2 removes the seal key from the client. Your backend creates a short-lived intent with its secret key and
hands the opaque intentToken to the app; the SDK runs identify with it and returns an opaque attestation; the app sends
that to your backend, which asks /v1/eligibility or /v1/quota for the decision.
backend POST /v2/intents (secret key) -> intentToken
app sdk.identify(intentToken) -> attestation
backend POST /v1/eligibility (secret key) -> allow / human check / deny
SDK 0.2.0 adds identify(intentToken) next to the v1 identify(); sealKey is only needed for v1.
SDK 0.4.0 adds optional linkedId / tags and the result's eventId, see Event fields.
SDK 0.5.0 adds the optional sealedResult on identify answers, see Sealed results.
SDK 0.3.0: the v1 trustScore and identityConfidence are now optional (Int? / null), because servers from 0.35 omit them by default. The CLI examples take
--intent <token> (or FPC_INTENT). Wire format: PROTOCOL-V2.md; vectors:
test-vectors/protocol-v2.json.
A backend as a Cloudflare Worker (secret key in wrangler secret put FPC_SECRET_KEY):
interface Env { FPC_SECRET_KEY: string }
const API = 'https://api.fpchecker.com';
async function fpc(env: Env, path: string, body: unknown): Promise<Response> {
return fetch(`${API}${path}`, {
method: 'POST',
headers: { 'content-type': 'application/json', 'X-Secret-Key': env.FPC_SECRET_KEY },
body: JSON.stringify(body),
});
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (request.method !== 'POST') return new Response('not found', { status: 404 });
if (url.pathname === '/intent') { // the app calls this, then gives intentToken to the SDK
const clientIp = request.headers.get('CF-Connecting-IP') ?? undefined;
const res = await fpc(env, '/v2/intents', { purpose: 'claim:trial', clientIp });
const { ok, intentToken } = (await res.json()) as { ok: boolean; intentToken?: string };
return Response.json(ok ? { intentToken } : { error: 'intent_failed' }, { status: ok ? 200 : 502 });
}
if (url.pathname === '/claim') { // the app posts the SDK's attestation here
const { attestation } = (await request.json()) as { attestation: string };
const clientIp = request.headers.get('CF-Connecting-IP') ?? undefined;
const res = await fpc(env, '/v1/eligibility', { attestation, clientIp, claim: true });
const d = (await res.json()) as { eligible?: boolean; action?: string; reason?: string; powDifficulty?: number };
// Grant on your side when d.eligible is true. Return the decision only: visitorId and
// trustScore stay on your backend, so the client never learns what it is scored on.
return Response.json({ eligible: d.eligible === true, action: d.action, reason: d.reason, powDifficulty: d.powDifficulty });
}
return new Response('not found', { status: 404 });
},
};
To bind the result to one of your users, add subject (your user or session id, taken from your own session, never
from the request body) to both the /v2/intents and the /v1/eligibility call; a mismatch is denied (attestation_subject).
For a counted free allowance instead of a one-time trial, mint the intent with purpose: 'quota:<name>' and call /v1/quota.
linkedId, tags, eventId (SDK 0.4.0)Identify can attach your own identifier and free-form tags to the event it writes, and returns the event's id, as the web
agent 0.9.0 does. Both are optional; null / absent sends nothing, and existing calls are unchanged.
linkedId: a string of 1 to 256 characters, no control characters (U+0000 to U+001F, U+007F).tags: a JSON object of at most 4096 bytes (UTF-8), nested at most 3 levels (an array counts as a level).invalid_linked_id or invalid_tags.GET /v1/events/:eventId) or in a webhook.eventId on the success result is null / nil when the server predates events (v1 and v2).// Android
val r = checker.identify(intentToken, FpEventOptions(linkedId = "order_42", tags = mapOf("page" to "checkout")))
if (r is FpIdentifyResult.Success) log(r.eventId) // "ev_..."
// v1: checker.identify(FpEventOptions(linkedId = "order_42")); Java: new FpEventOptions("order_42", null)
// iOS / macOS
let result = await checker.identify(intentToken: token, linkedId: "order_42", tags: ["page": "checkout"])
if case .success(let r) = result { print(r.eventId ?? "-") } // "ev_..."
// v1: await checker.identify(linkedId: "order_42")
CLI: --linked-id order_42 --tags '{"page":"checkout"}' (see cli/).
The browser agent is not part of this repository. Download fp-agent.min.js from
https://fpchecker.com/static/fp-agent.min.js and serve it from your own domain (ad blockers often block
third-party fingerprinting scripts, and the agent's first-party id only works on your origin). Your project page in
the fpchecker.com console shows these snippets with your keys filled in:
<script src="/static/fp-agent.min.js"></script>
<script>
async function identifyBrowser() {
const { attestation } = await window.FpGuardAgent.identify({
baseUrl: 'https://api.fpchecker.com',
publishableKey: 'pk_...', // from your project
sealKey: '...', // from your project
});
return attestation; // send it to your backend unchanged
}
</script>
// Server side only. Keep the secret key in an environment variable.
const response = await fetch('https://api.fpchecker.com/v1/eligibility', {
method: 'POST',
headers: { 'content-type': 'application/json', 'X-Secret-Key': process.env.FPCHECKER_SECRET_KEY },
body: JSON.stringify({ attestation, email, claim: true, kind: 'trial', clientIp }),
});
const decision = await response.json(); // { ok, eligible, action, reason }
Add your site's origin to the project's allowed origins, or the browser call is refused.
// build.gradle.kts (Maven Central)
dependencies {
implementation("com.fpchecker:fpchecker-android:0.7.0")
}
val checker = FpChecker(
context,
FpCheckerConfig(
baseUrl = "https://api.fpchecker.com",
publishableKey = "pk_...", // from your FingerPrint Checker project
sealKey = "...", // from your FingerPrint Checker project
cloudProjectNumber = 123456789012L, // optional: enables the Play Integrity proof
powDifficulty = null, // optional: 8..28 when your project uses proof-of-work
),
)
lifecycleScope.launch {
when (val r = checker.identify()) {
is FpResult.Success -> sendToBackend(r.attestation) // verify server-side!
is FpResult.Failure -> log("identify failed: ${r.reason}")
}
}
Java callers use checker.identify(result -> { ... }) (callback on the main thread).
Requirements: minSdk 23. The library declares the INTERNET permission. Play Integrity uses
com.google.android.play:integrity and is only used when you pass cloudProjectNumber; if it is
unavailable or slow (5 s default) the request is still sent without a proof and is scored as unattested.
android/demo is a one-screen app. Provide your values without committing them, e.g. in ~/.gradle/gradle.properties:
fpcheckerPk=pk_...
fpcheckerSealKey=...
fpcheckerBaseUrl=https://api.fpchecker.com
fpcheckerCloudProject=123456789012
Build and test (JDK 17, Android SDK 35):
cd android
./gradlew :fpchecker:testDebugUnitTest :fpchecker:assembleRelease :demo:assembleDebug :demo:assembleRelease lint
The release APK is unsigned unless fpcheckerStoreFile, fpcheckerStorePassword, fpcheckerKeyAlias and
fpcheckerKeyPassword are all set.
Swift Package Manager: add https://github.com/AndrewYukon/FingerPrintChecker (product FingerPrintChecker,
iOS 14+, macOS 11+), or in Package.swift:
.package(url: "https://github.com/AndrewYukon/FingerPrintChecker", from: "0.7.1")
import FingerPrintChecker
let checker = FingerPrintChecker(config: FPConfig(
publishableKey: "pk_...", // from your FingerPrint Checker project
sealKey: "...", // from your FingerPrint Checker project
powDifficulty: nil // optional: 8...28 when your project uses proof-of-work
))
switch await checker.identify() {
case .success(let r): try await sendToBackend(r.attestation) // verify server-side!
case .failure(let reason, let httpStatus): print("identify failed: \(reason) \(httpStatus)")
}
FPConfig also has baseURL (default https://api.fpchecker.com), useAppAttest and useDeviceCheck (both default true).
Concurrent identify() calls are serialised. swift build && swift test at the repo root run the unit tests on macOS.
apple/Demo is a SwiftUI demo for both platforms, see apple/Demo/README.md.
development vs production), otherwise the proof is not checked
and the device is scored as unattested. If App Attest is unavailable or fails, identify still succeeds without a proof.
A key is remembered only after the server confirmed its attestation; if the server forgets a key, or Apple reports it
invalid (for example after a restore), the SDK attests a new one within the same identify() call (one extra request
when the server forgot it), so the caller never sees an unattested result for it.ios.simulator=true.From 0.7.0 the SDK also checks the state of the device it runs on and reports one yes / no / not-checked answer per item. It detects, on the device types that support it: a rooted or jailbroken device, hooking or instrumentation frameworks, an emulator or simulator, an attached debugger, developer options, an interception proxy or untrusted user certificates, an active VPN, mock (spoofed) locations, and a cloned or parallel-space copy of your app. A macOS app reports the debugger and hooking items only.
integrityProbes (default true) in Android FpCheckerConfig and Swift FPConfig. With false the SDK
runs no probe and reports every item as "not checked".sealedResult and the attestation do not contain them. They reach your server only through the
events API (signals.mobile) and webhooks, and can drive project rules. See Server SDKs.true = detected, false = checked and nothing found, null = not checked (older SDK, probes
switched off, an item that cannot run on this device or OS). Detection is best effort and never a guarantee either way.factoryResetAt exists in the server types but is reserved: SDK 0.7.0 does not
detect factory resets, so it is always null.debugger = true,
so your own test devices will show it. Release builds started normally do not.vpn = true. iCloud Private Relay is not a device VPN and
does not set it. With 0.7.0, vpn = false only meant that no legacy tunnel interface was found.uses-permission, no <queries>, and does not list the apps on the device.
A location check uses only a location permission your app already holds, and on Android the device-VPN check runs only when your app already holds ACCESS_NETWORK_STATE (the SDK adds no permission); otherwise vpn is null.Server side, filter the events API by rooted, hooked, emulator, mitm, locationSpoofed and clonedApp
(true / false), and read signals.mobile and signals.vpn.methods (which may contain os_vpn). Server SDKs 0.3.0 type these fields.
When your project has a sealing key, identify answers carry an optional sealedResult string (SDK 0.5.0): FpResult.Success.sealedResult /
FpIdentifyResult.Success.sealedResult on Android, FPSuccess.sealedResult / FPSuccessV2.sealedResult on iOS and macOS. It is
null / nil when the project has no key or the server predates it. Forward it to YOUR backend, which opens it with the project key
(never on the device); the format and its test vectors are in test-vectors/sealed-result-v1.json.
| Package | Where | Install |
|---|---|---|
Server SDK for Node.js, @fingerprintchecker/node | server/node | npm install @fingerprintchecker/node |
Server SDK for Python, fpchecker | server/python | pip install fingerprintchecker |
| Server SDK for Go | server/go | go get github.com/AndrewYukon/FingerPrintChecker/server/go |
Server SDK for Java, com.fpchecker:fpchecker-server-java | server/java | Maven Central: com.fpchecker:fpchecker-server-java:0.1.0 |
Server SDK for PHP, fpchecker/fpchecker-php | server/php | from the GitHub release zip, see server/php/README.md |
Server SDK for .NET, FingerPrintChecker.Server | server/dotnet | from the GitHub release .nupkg, see server/dotnet/README.md |
Browser agent loader, @fingerprintchecker/agent |
The npm packages (@fingerprintchecker/agent, react, vue, node) and the PyPI package fingerprintchecker are published (since 2026-10-26); install them with npm install / pip install fingerprintchecker. PHP and .NET are installed from the GitHub release (see their READMEs). Go: go get github.com/AndrewYukon/FingerPrintChecker/server/go. How the owner publishes them: docs/PUBLISHING.md.
Serve the agent and API first-party from your own domain with the open-source Cloudflare Worker.
No library: a CLI can be written in any language. cli/ has dependency-free
examples for Node, Python and Go. CLI devices are never attested; enable
PoW human verification for the project.
The seal key ships inside your app, so anything computed on the device can be forged by a determined attacker.
The attestation in a successful result is the only value you should trust: send it to your backend and verify it there
with the FingerPrint Checker API (for example /v1/eligibility, /v1/quota) using your secret key. Do not gate entitlements on
trustScore or visitorId as reported to the client.
Servers from 0.35 answer v1 identify() with a sealed attestation ({ "token": "fpa1.…" }) that the client cannot read, and leave
out trustScore, identityConfidence, breakdown and the other score fields unless the project's scoreDetails policy is
live or all (default none). Forward the attestation to your backend unchanged, and read visitorId, isNew,
trustScore and identityConfidence from the /v1/eligibility answer there.
Store disclosure answers (Google Play Data safety, App Store App Privacy): docs/PRIVACY.md.
What leaves the device (Android):
ANDROID_ID (app-signing-key scoped, resettable by factory reset).deviceUniqueId. The raw MediaDrm id never leaves the device.Build.FINGERPRINT, Build.HARDWARE, Build.MODEL, SDK level.The SDK does not read the advertising id, accounts, contacts, phone number or IMEI, and never sends coordinates (see the integrity paragraph below). As the integrator you are responsible for disclosing this in your privacy policy and Play Data safety form.
What leaves the device (iOS / macOS):
identifierForVendor. macOS: the hardware IOPlatformUUID.iPhone17,1, Mac15,3), OS version, simulator flag (iOS).The SDK stores one timestamp in UserDefaults (App Attest back-off). No advertising id, contacts or accounts are read, and no coordinates are sent. The package ships a PrivacyInfo.xcprivacy declaring
Device ID collection (not linked, not used for tracking) and its one required-reason API use (UserDefaults, reason CA92.1, for the App Attest back-off timestamp). Disclose this in your App Privacy details.
What the integrity probes add (SDK 0.7.0): only device-state booleans (detected / not detected / not checked) and a probe version number, used for security and fraud prevention, not for tracking. The SDK does not collect or send coordinates, the list of installed apps, certificates, proxy addresses or file paths. For Google Play Data safety this stays in the existing "Device or other IDs" answer (purpose: fraud prevention, security and compliance); for App Store App Privacy nothing new is tracked and the existing Device ID entry (not used for tracking) still describes the SDK. Details: docs/PRIVACY.md.
Apache-2.0, see LICENSE.
| Available, 0.1.0 |
web/agentnpm install @fingerprintchecker/agent |
React wrapper, @fingerprintchecker/react | web/react | npm install @fingerprintchecker/react |
Vue wrapper, @fingerprintchecker/vue | web/vue | npm install @fingerprintchecker/vue |