API پل جاوااسکریپت
گزارشهای خرابی و یکپارچگی دستگاه
خطاهایی که صفحه ثبت میکند، آخرین خرابی بومی، و اینکه گوشی روتشده یا تغییریافته به نظر میرسد یا نه، با یک توکن اختیاری Play Integrity برای سرور شما.
AppMint.integrity کمککننده#
AppMint.integrity.check() / AppMint.integrity.token(nonce, cloudProjectNumber?)
نمونه
نسخههای Promise از getDeviceIntegrity و requestIntegrityToken، پیش از اولین اسکریپت صفحه شما وقتی ساخت بررسی یکپارچگی دستگاه را روشن کرده باشد (مرحله یکپارچهسازی، مرحله ۳).
خروجی: check() با شیء ابتکارها ({rooted, emulator, debuggerAttached, signals, installer, installedFromPlay, signatureSha256, method: 'heuristic'}) حل میشود؛ token(nonce) با رشته توکن Play Integrity حل میشود، یا با یک Error رد میشود که پیامش نام خطاست (not_enabled، nonce_too_short، network_error، …) و code کد خطای Play. ابتکارها فقط راهنما هستند؛ فقط رمزگشایی توکن توسط سرور شما یک حکم است.
async function guard() {
if (!(window.AppMint && AppMint.integrity)) return; // browser, or option off
const local = await AppMint.integrity.check();
if (local.rooted) console.warn('rooted signals:', local.signals);
const { nonce } = await fetch('/api/integrity/nonce').then(r => r.json());
try {
const token = await AppMint.integrity.token(nonce); // project number from the build
await fetch('/api/integrity/verify', { method: 'POST', body: token });
} catch (e) {
console.warn('Play Integrity unavailable:', e.message, e.code);
}
}وقتی اپلیکیشن یک نسخه بستهبندیمجدد است هشدار دهید:
if (window.AppMint && AppMint.integrity) {
const d = await AppMint.integrity.check();
if (!d.installedFromPlay) console.log('installed by', d.installer || 'side-loaded');
if (d.signatureSha256 && d.signatureSha256 !== MY_CERT_SHA256) reportTamper(d.signatureSha256);
}شکست توکن را با نام مدیریت کنید:
async function getToken(nonce) {
if (!(window.AppMint && AppMint.integrity)) return null;
try {
return await AppMint.integrity.token(nonce);
} catch (e) {
if (e.message === 'not_enabled') console.warn('Device integrity checks are off in this build');
else if (e.message === 'nonce_too_short') console.warn('the server nonce must be longer');
else console.warn('Play Integrity error', e.message, e.code);
return null;
}
}getDeviceIntegrity پل#
window.WebToApk.getDeviceIntegrity(): String
ابتکارهای (HEURISTICS) محلی برای root / شبیهساز / دیباگر (DeviceIntegrity را ببینید) بهصورت JSON: `{rooted, emulator, debuggerAttached, signals:[…], installer, installedFromPlay, signatureSha256, method:"heuristic", sdk}`. نتیجه پاک یعنی «نشانهای پیدا نشد»، نه اثبات - برای حکمی که بتوان به آن اعتماد کرد، یک توکن Play Integrity به سرور خود بفرستید (requestIntegrityToken). `{"error":"not_enabled"}` مگر اینکه ساخت بررسی یکپارچگی دستگاه را روشن کرده باشد. چند میلیثانیه مسدود میکند (فایلها را میخواند): یک بار فراخوانی کنید.
نمونه
ابتکارهای (HEURISTICS) محلی root، شبیهساز و دیباگر بهصورت رشته JSON. نتیجه پاک یعنی «نشانهای پیدا نشد»، نه اثبات: کاربر مصمم با ماژول پنهانکننده root رد میشود. از آن برای هشدار یا افزودن یک بررسی استفاده کنید؛ برای حکمی که بتوان به آن اعتماد کرد، یک توکن Play Integrity به سرور خود بفرستید (requestIntegrityToken).
خروجی: '{"rooted":false,"emulator":false,"debuggerAttached":false,"signals":[],"installer":"com.android.vending","installedFromPlay":true,"signatureSha256":"AB:CD:…","method":"heuristic","sdk":34}'، یا '{"error":"not_enabled"}'. سیگنالها نام آنچه پیدا شده را میگویند: test-keys، su-binary:/system/xbin/su، root-path:/data/adb/magisk، root-app:com.topjohnwu.magisk، system-writable، ro.debuggable=1، ro.secure=0، selinux-permissive، bootloader-unlocked، emulator:*، debugger-attached. نیازمندی: بررسی یکپارچگی دستگاه را در مرحله یکپارچهسازی (مرحله ۳) روشن کنید. چند میلیثانیه طول میکشد (فایلها را میخواند): یک بار فراخوانی کنید، نه در حلقه.
function deviceCheck() {
if (!(window.WebToApk && window.WebToApk.getDeviceIntegrity)) return null;
const r = JSON.parse(WebToApk.getDeviceIntegrity());
if (r.error) return null; // 'not_enabled'
return r;
}
const d = deviceCheck();
if (d && d.rooted) {
showNotice('This phone looks rooted (' + d.signals.join(', ') + '). Some features are turned off.');
}
if (d && !d.installedFromPlay) console.log('installed by', d.installer || 'unknown (side-loaded)');امضای خودتان را بررسی کنید - نسخه بستهبندیمجدد اپلیکیشن شما با کلید دیگری امضا شده است:
const MY_CERT = 'AB:CD:EF:…'; // Play Console → App integrity → App signing key SHA-256
const d = deviceCheck();
if (d && d.signatureSha256 && d.signatureSha256 !== MY_CERT) reportTamper();با یک توکن Play Integrity از سرور حکمی بخواهید که بتوان به آن اعتماد کرد (پاسخ بهصورت رویداد میرسد).
window.addEventListener('appmint:integrity', function (e) {
if (e.detail.error) { showNotice('Check failed: ' + e.detail.error); return; }
fetch('https://api.example.com/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: e.detail.token })
});
});
if (window.WebToApk && WebToApk.requestIntegrityToken) {
const nonce = crypto.randomUUID().replace(/-/g, '') + '0000'; // your server issues and checks this
WebToApk.requestIntegrityToken(nonce, '123456789012'); // your Google Cloud project number
}getLastCrash پل#
window.WebToApk.getLastCrash(): String
جدیدترین خرابی ثبتشده در این نصب (بسته شدن اپلیکیشن بر اثر خطای گرفتهنشده، یا خرابی رندرکننده صفحه)، بهصورت JSON `{kind, message, stack, url, at, count, appVersion, user, fp}`، یا "" وقتی هیچ خرابیای نبوده (یا گزارشهای خرابی خاموش است). پس از ارسال هم نگه داشته میشود، تا صفحه بتواند بگوید «متأسفیم، دفعه قبل اپلیکیشن بسته شد».
نمونه
جدیدترین خرابی ثبتشده در این نصب - بسته شدن اپلیکیشن بر اثر خطای گرفتهنشده (kind: "native") یا خرابی رندرکننده صفحه (kind: "renderer") - بهصورت رشته JSON، یا '' وقتی هیچ خرابیای نبوده.
خروجی: '{"kind","message","stack","url","at","count","appVersion","user","fp"}' یا ''. همچنین '' وقتی ساخت گزارشهای خرابی را روشن نکرده. نیازمندی: گزارشهای خرابی روشن در مرحله یکپارچهسازی (مرحله ۳).
پس از خرابی یک بار عذرخواهی کنید:
(function () {
if (!(window.WebToApk && window.WebToApk.getLastCrash)) return;
const raw = WebToApk.getLastCrash();
if (!raw) return;
const crash = JSON.parse(raw);
if (localStorage.getItem('seenCrash') === String(crash.at)) return; // shown already
localStorage.setItem('seenCrash', String(crash.at));
showBanner('Sorry, the app closed unexpectedly last time. We have been told about it.');
})();خرابی ذخیرهشده را یک بار به سرور خودتان ارسال کنید، همراه با نسخه اپلیکیشنی که خرابی در آن رخ داد.
(function () {
if (!(window.WebToApk && WebToApk.getLastCrash)) return;
const raw = WebToApk.getLastCrash();
if (!raw) return; // no crash, or Crash reports are off
const c = JSON.parse(raw);
if (localStorage.getItem('sentCrash') === String(c.at)) return;
fetch('https://api.example.com/crashes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ kind: c.kind, message: c.message, version: c.appVersion, count: c.count })
}).then(function () { localStorage.setItem('sentCrash', String(c.at)); });
})();بر اساس نوع شاخه کنید تا پیام مناسب بدهید.
const raw = window.WebToApk && WebToApk.getLastCrash && WebToApk.getLastCrash();
if (raw) {
const c = JSON.parse(raw);
showBanner(c.kind === 'renderer'
? 'The page ran out of memory last time. Try closing other apps.'
: 'The app closed unexpectedly last time.');
}نکتهها: پس از ارسال گزارش هم میماند، پس at را با آنچه ذخیره کردهاید مقایسه کنید تا پیام فقط یک بار نشان داده شود. خطاهای صفحه و فراخوانیهای logError خرابی نیستند و هرگز اینجا نمیآیند.
logError پل#
window.WebToApk.logError(message: String, stack: String): Boolean
گزارشهای خرابی: خطایی را که صفحه شما گرفته است (فراخوانی پرداخت ناموفق، خطای تجزیه) ثبت میکند تا در صفحه گزارشهای خرابی AppMint کنار خرابیها، گروهبندی و شمردهشده ظاهر شود. [stack]: `err.stack` یا "". وقتی ساخت گزارشهای خرابی را روشن نکرده، وقتی هر دو خالی هستند، یا وقتی همان خطا در این نشست بارها ثبت شده باشد false برمیگرداند. هرگز اطلاعات شخصی (ایمیل، تلفن، توکن) در پیام نگذارید.
نمونه
خطایی را که صفحه شما گرفته است (فراخوانی پرداخت ناموفق، خطای تجزیه JSON) ثبت میکند تا در صفحه گزارشهای خرابی AppMint (بهروزرسانی اپلیکیشن ← اپلیکیشن شما ← گزارشهای خرابی) همراه با خرابیها گروهبندی و شمرده شود. خطاهای گرفتهنشده و رد شدنهای مدیریتنشده Promise برایتان ثبت میشوند؛ این برای آنهایی است که خودتان مدیریت کردهاید.
خروجی: true وقتی ثبت شد؛ false وقتی ساخت گزارشهای خرابی را روشن نکرده، وقتی هر دو آرگومان خالی هستند، یا وقتی همان خطا در این نشست بارها ثبت شده. گزارشها دفعه بعد که اپلیکیشن باز شود ارسال میشوند. نیازمندی: هنگام ساخت گزارشهای خرابی را در مرحله یکپارچهسازی (مرحله ۳) روشن کنید. هرگز اطلاعات شخصی (نشانی ایمیل، شماره تلفن، توکن) در پیام نگذارید.
یک خطای مدیریتشده را ثبت کنید:
function logError(err, context) {
if (!(window.WebToApk && window.WebToApk.logError)) return false; // browser, or an older build
var msg = (context ? context + ': ' : '') + (err && err.message ? err.message : String(err));
return WebToApk.logError(msg, (err && err.stack) || '');
}
async function checkout(cart) {
try {
await payments.charge(cart.total);
} catch (err) {
logError(err, 'checkout'); // shows up as "Logged by your page"
showToast('Payment failed, please try again');
}
}فراخوانیهای ناموفق API را گزارش کنید بدون query string نشانی (ممکن است توکن داشته باشد):
async function api(path) {
const res = await fetch('/api' + path);
if (!res.ok && window.WebToApk && WebToApk.logError) {
WebToApk.logError('API ' + res.status + ' ' + path.split('?')[0], '');
}
return res;
}یک خطای گرفتهشده را با شناسه کاربر ثبت کنید، تا بعداً بتوان مشکلات یک کاربر را پیدا کرد.
if (window.WebToApk && WebToApk.setCrashUserId) WebToApk.setCrashUserId('user_' + currentUser.id);
try {
JSON.parse(savedState);
} catch (err) {
if (window.WebToApk && WebToApk.logError) {
const logged = WebToApk.logError('state parse: ' + err.message, err.stack || '');
if (!logged) console.warn('not recorded (Crash reports off, or repeated too often)');
}
}نکتهها: گزارشهایی با علت یکسان حتی وقتی اعداد پیام فرق میکنند گروهبندی میشوند (item 42 و item 7). نشانی صفحه بدون query و fragment ذخیره میشود.
requestIntegrityToken پل#
window.WebToApk.requestIntegrityToken(nonce: String, cloudProjectNumber: String): String
از Google Play یک توکن Play Integrity میخواهد (درخواست کلاسیک). [nonce]: Base64 امن برای URL، ۱۶ تا ۵۰۰ نویسه، ساختهشده توسط سرور شما برای همین یک درخواست. [cloudProjectNumber]: شماره پروژه Google Cloud شما، یا "" برای استفاده از شماره تنظیمشده در ساخت (اپلیکیشن نصبشده از Play میتواند هر دو را خالی بگذارد). فوراً یک شناسه درخواست برمیگرداند (وقتی ساخت بررسی یکپارچگی دستگاه را روشن نکرده باشد ""); پاسخ بهصورت `appmint:integrity` / `window.onAppMintIntegrity({requestId, token})` یا `{requestId, error, code}` میرسد. توکن را به سرور خودتان بفرستید که آن را با Google رمزگشایی (playintegrity.googleapis.com … :decodeIntegrityToken) و nonce را بررسی کند.
نمونه
یک توکن Play Integrity از Google Play میخواهد (درخواست کلاسیک). توکن روی گوشی مبهم است: سرور شما آن را به Google میفرستد، حکم را میخواند (اپلیکیشن اصلی؟ دستگاه اصلی؟ دارای مجوز؟) و بررسی میکند که nonce همانی است که خودش صادر کرده. همین بررسی سرور است که پاسخ را قابل اعتماد میکند.
خروجی: فوراً یک شناسه درخواست ('' وقتی ساخت بررسی یکپارچگی دستگاه را روشن نکرده). پاسخ بهصورت رویداد appmint:integrity (و window.onAppMintIntegrity) با {requestId, token} یا {requestId, error, code} میرسد - خطاهایی مانند nonce_too_short، nonce_is_not_base64، network_error، play_store_not_found، cloud_project_number_is_invalid، too_many_requests. نیازمندی: بررسی یکپارچگی دستگاه روشن در مرحله یکپارچهسازی (مرحله ۳)، Play Integrity API فعالشده برای اپلیکیشن (Play Console ← App integrity)، و - برای اپلیکیشنی که از Google Play نصب نشده - شماره پروژه Google Cloud شما، در ویزارد یا بهعنوان آرگومان دوم ('' از مقدار ساخت استفاده میکند).
async function integrityToken() {
// 1. Your server makes a one-time nonce: URL-safe Base64, 16-500 characters.
const { nonce } = await fetch('/api/integrity/nonce').then(r => r.json());
return new Promise((resolve, reject) => {
const id = WebToApk.requestIntegrityToken(nonce, '');
if (!id) return reject(new Error('not_enabled'));
window.addEventListener('appmint:integrity', function on(e) {
if (e.detail.requestId !== id) return;
window.removeEventListener('appmint:integrity', on);
e.detail.token ? resolve(e.detail.token) : reject(new Error(e.detail.error));
});
});
}
// 2. Your server decodes it with Google and decides.
const token = await integrityToken();
const verdict = await fetch('/api/integrity/verify', { method: 'POST', body: token }).then(r => r.json());مرحله سرور - POST https://playintegrity.googleapis.com/v1/<your.package>:decodeIntegrityToken را با {"integrity_token": token} و با یک حساب سرویس پروژه Cloud متصل فراخوانی کنید، سپس بررسی کنید:
const p = decoded.tokenPayloadExternal;
const ok = p.requestDetails.nonce === issuedNonce &&
p.appIntegrity.appRecognitionVerdict === 'PLAY_RECOGNIZED' &&
p.deviceIntegrity.deviceRecognitionVerdict.includes('MEETS_DEVICE_INTEGRITY');شکل فراخوانی برگشتی، برای کدی که نمیتواند به رویدادها گوش دهد:
window.onAppMintIntegrity = function (d) {
if (d.error) console.warn('integrity failed', d.error, d.code);
else sendToServer(d.token);
};
WebToApk.requestIntegrityToken(nonceFromServer, '123456789012');setCrashUserId پل#
window.WebToApk.setCrashUserId(id: String): Boolean
گزارشهای خرابی: شناسه خودتان برای کاربر واردشده (حروف، ارقام و _ @ . : + -، تا ۱۲۸ نویسه)، از این لحظه به گزارشها وصل میشود تا بتوانید کاربران تحت تأثیر را بشمارید و خرابیهای یک کاربر را پیدا کنید. "" آن را پاک میکند. هرگز نشانی ایمیل یا شماره تلفن. برای شناسهای با نویسههای دیگر، یا وقتی گزارشهای خرابی خاموش است false برمیگرداند.
نمونه
شناسه خودتان برای کاربر واردشده را از این لحظه به گزارشهای خرابی وصل میکند، تا صفحه گزارشهای خرابی بتواند بشمارد یک مشکل چند کاربر را درگیر کرده. بعد از ورود فراخوانی کنید؛ هنگام خروج آن را با '' فراخوانی کنید.
خروجی: true وقتی ذخیره شد؛ false برای شناسهای با نویسههایی غیر از حروف، ارقام و _ @ . : + - (یا بلندتر از ۱۲۸)، یا وقتی ساخت گزارشهای خرابی را روشن نکرده. نیازمندی: گزارشهای خرابی روشن در مرحله یکپارچهسازی (مرحله ۳). از شناسه پایگاه داده خودتان استفاده کنید، هرگز نشانی ایمیل یا شماره تلفن.
function crashUser(id) {
if (!(window.WebToApk && window.WebToApk.setCrashUserId)) return false;
return WebToApk.setCrashUserId(id);
}
async function signIn(email, password) {
const user = await myBackend.signIn(email, password);
crashUser(user.id); // e.g. 'u_42'
}
function signOut() {
crashUser(''); // later reports carry no user
}شناسه ردشده، یا ساخت بدون گزارشهای خرابی، false برمیگرداند:
function setCrashUser(id) {
if (!(window.WebToApk && WebToApk.setCrashUserId)) return;
if (!WebToApk.setCrashUserId(id)) {
console.warn('Crash user id not saved: bad characters, over 128 long, or Crash reports are off');
}
}
setCrashUser('u_42'); // your database id, never an e-mail addressپس از راهاندازی مجدد، درباره آخرین خرابی به کاربر بگویید:
if (window.WebToApk && WebToApk.getLastCrash) {
const raw = WebToApk.getLastCrash(); // '' when there has been none
if (raw) {
const c = JSON.parse(raw);
showMessage('Sorry, the app closed last time (' + c.kind + ').');
}
}نکتهها: شناسه تا وقتی آن را تغییر دهید روی گوشی میماند و فقط داخل گزارشهای خرابی فرستاده میشود.