eruda

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

3posts · 30d
Nov 40 posts
  1. Qiita · · @reizouko

    スマホで使える簡易的なデベロッパーツール「Eruda」の使い方 はじめに スマートフォンでウェブサイトを閲覧しているときに、ページ内の要素を確認したり、JavaScriptを実行したいと思ったことはないでしょうか。 例えば、次のような場面です。 - サイトのHTMLやDOMを確認したい - JavaScriptを実行して、ページ内の情報を抽出したい - localStorageやCookieに保存されている値を確認したい PCならブラウザのデベロッパーツールを使って簡単にできることも、スマートフォンでは難しい場合があります。 そこで便利なのが、Eruda です。 Erudaは、スマートフォンなどのブラウザ上で利用できる開発者向けツールです。JavaScriptの実行やDOMの確認、ストレージの参照など、ウェブサイトの調査に役立つ機能を備えています。 この記事では、Android版Chromeを使って、Erudaをブックマークレットから起動する方法を紹介します。 Erudaとは Erudaは、モバイルブラウザ上で動作する開発者向けツールです。 主に次のような機能を利用できます。 - Console: JavaScriptを実行 - Elements: ページのDOMを確認 - Network: 通信状況を確認 - Resources: localStorage、Cookie、読み込まれたスクリプトとCSSなどを確認 - Sources: ソースコードの確認 - Info: ブラウザや端末の情報、Erudaのバージョンなどを確認 - Snippets: 境界線を表示する、サイトのテキストを編集可能にするなどのユーティリティを集めたもの PCのデベロッパーツールを完全に再現するものではありませんが、スマートフォンからウェブサイトを調査したいときに便利です。 公式サイト: eruda.liriliri.io GitHub: liriliri/eruda Erudaの使い方 今回は、ブックマークレットを利用してErudaを起動する方法を紹介します。 ブックマークレットとは、ブックマークからJavaScriptを実行する仕組みです。 一度登録しておけば、対応するサイトを開いた状態でErudaを呼び出せます。 1. ブックマークを作成する まず、Chromeで適当なウェブサイトを開き、ブックマークに登録します。 !1000008140.png 2. 作成したブックマークを編集する ブックマーク一覧を開き、先ほど登録したブックマークを編集します。 !1000008143.png 3. ブックマークレットを設定する ブックマークのタイトルを Erudaなど、好きなタイトルに変更します。 次に、URL欄に以下のコードを貼り付けます。 javascript:(function () { var script = document.createElement('script'); script.src="https://cdn.jsdelivr.net/npm/eruda"; document.body.append(script); script.onload = function () { eruda.init(); } })(); 入力できたら、←を押して戻ります。 !1000008146.png 4. 調べたいサイトを開く Erudaを使用したいサイトをChromeで開きます。 5. アドレスバーからブックマークレットを実行する ブックマーク一覧から直接実行するのではなく、Chromeのアドレスバーから呼び出します。 1. Chromeのアドレスバーをタップします。 2. ブックマークレットに設定したタイトル(今回は Eruda)を入力します。 3. 表示された候補をタップします。 !1000008131.png 正常に実行されると、画面の右下にErudaの歯車アイコンが表示されます。 6. Erudaを開く 右下に表示された歯車アイコンをタップすると、Erudaのパネルが開きます。 !1000008129.png パネル内のメニューから、ConsoleやElementsなどの機能を選択できます。 これで、Erudaが使えるようになりました。 注意点 Erudaは、すべてのウェブサイトで動作するわけではありません。 ウェブサイト側のCSP (Content Security Policy) などによって、Erudaが正常に起動しない場合があります。 また、ErudaはPCのデベロッパーツールと完全に同じ機能を備えているわけではありません。 おわりに Erudaを使えば、パソコンを起動させなくても、スマートフォンからConsoleでJavaScriptを実行したり、サイトのDOMやストレージの値などを簡単に確認できます。 スマートフォンでウェブサイトを調査したいときに、試してみてはいかがでしょうか。

  2. Passkeys in In-App Browsers: The API Is There, but It Won't Work I recently added passkeys to a member login page, and LINE's in-app browser gave me one of the sneakiest bugs I've dealt with in a while. The passkey button showed up. Tapping it did nothing. No system prompt, no error, nothing in the console. This post walks through what was going on, plus three things I think anyone shipping passkeys should know: how to tell whether passkeys will actually work, why NotAllowedError doesn't mean the user cancelled, and why a null from your backend can make Safari throw before the ceremony even starts. All the code is ready to copy. If your site gets opened from inside LINE, Facebook, Instagram or similar apps, this probably applies to you. The symptom While testing on staging, I opened the login page inside LINE. The passkey button was there, but tapping it did nothing at all. With no error to look at, I started with a screen recording and stepped through it frame by frame. The "verifying" state showed up for exactly one frame at 30fps. So navigator.credentials.get() was being called, and it was failing within a few dozen milliseconds. Why was the screen blank? Because our code treated every NotAllowedError as "the user cancelled", and a cancel shouldn't show an error. The failure was being swallowed. Next I added logging on a real device. On iOS 26.6 with LINE 26.12.1, I got this: supported() = true // the shallow check passes IUVPAA = false (12ms) // the platform honestly says no get() -> NotAllowedError after 4ms Nobody cancels anything in 4ms. The system prompt hadn't even been drawn yet. LINE's WebView exposes the WebAuthn API, but it doesn't actually let you use it. That's decided by the host app and the OS, and nothing on the page can change it. 1. Checking that the API exists isn't enough A lot of tutorials decide whether to offer passkeys like this: if (window.PublicKeyCredential) { // show the passkey button } That's fine in a regular browser. In an in-app browser it isn't. The WebView usually runs on the system's browser engine, so the API is there, but whether a ceremony can actually run is up to the app wrapping it. The more reliable approach is to ask the platform whether it can do this right now. There are two APIs for that: - PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() asks whether the device has a platform authenticator that can verify the user (Face ID, a fingerprint reader, Windows Hello). It's widely supported. - PublicKeyCredential.getClientCapabilities() is newer, from WebAuthn Level 3, and returns a whole set of capabilities at once. It needs Chrome or Edge 133, Firefox 135, or Safari 17.4. Inside LINE, the first one honestly returns false. Use it to decide whether to show the passkey option, and the dead button goes away. Here's the version I ended up with: const PROBE_TIMEOUT_MS = 3000; let capability: Promise<boolean> | undefined; // Cached, so the page only asks once export function canUsePasskey(): Promise<boolean> { capability ??= probe(); return capability; } async function probe(): Promise<boolean> { const PKC = window.PublicKeyCredential as any; if (!PKC || !navigator.credentials?.get) return false; // Newer API: prefer it when it's there if (typeof PKC.getClientCapabilities === 'function') { const caps = await withTimeout(PKC.getClientCapabilities(), PROBE_TIMEOUT_MS); if (typeof caps?.userVerifyingPlatformAuthenticator === 'boolean') { return caps.userVerifyingPlatformAuthenticator; } } // Older API: much wider support if (typeof PKC.isUserVerifyingPlatformAuthenticatorAvailable !== 'function') { return false; } return ( (await withTimeout( PKC.isUserVerifyingPlatformAuthenticatorAvailable(), PROBE_TIMEOUT_MS, )) === true ); } // Some restricted WebViews return a promise that never settles function withTimeout<T>(p: Promise<T>, ms: number): Promise<T | undefined> { return Promise.race([ p.catch(() => undefined), new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), ms)), ]); } A few notes on why it looks like this: - The timeout isn't optional. Some restricted WebViews hand back a promise that never resolves or rejects. Without a timeout, your login page just hangs on the check. - Cache it. Login, enrollment and passkey management all need the answer, so ask once. - getClientCapabilities() can return undefined for a key, which means the browser doesn't know. In that case it falls back to the older API. In my own project I only use the older API, and I've confirmed it returns false inside LINE. I haven't verified what the newer one returns across different apps' WebViews, so test it on real devices before relying on it. Why not just sniff the User-Agent for LINE? Because the platform already gives you an honest answer, and UA rules rot as apps ship new versions. If LINE ever enables passkeys, a UA check would keep those users locked out. The trade-off is that a desktop with only a USB security key and no Touch ID or Windows Hello gets filtered out too. For a phone-first product, I'm fine with that. 2. NotAllowedError doesn't mean the user cancelled The reason the failure was invisible in the first place: the code mapped every NotAllowedError to "user cancelled". That's a really common pattern, but NotAllowedError covers a lot more than that. The user closing the prompt, the request timing out, no usable credential, and, as here, the environment refusing outright can all come back as the same error. That's on purpose. If every failure had its own error, a site could use them to figure out whether a given credential exists on the device, and identify the user without their consent. So the browser won't tell you the exact reason, and you have to infer it. The most useful signal turns out to be time. To cancel, a person has to see the system prompt and tap cancel. That can't happen in a few milliseconds. So I time the call, and anything rejected too quickly counts as "passkeys don't work here", and the user gets pointed to another way to sign in: // Nobody can see a system prompt and dismiss it in under 300ms const HUMAN_CANCEL_FLOOR_MS = 300; export type PasskeyFailure = 'cancelled' | 'unavailable' | 'error'; export async function getPasskeyAssertion(options: CredentialRequestOptions) { const started = performance.now(); try { return await navigator.credentials.get(options); } catch (err) { const elapsed = performance.now() - started; console.warn(`[passkey] rejected after ${Math.round(elapsed)}ms`, err); if (err instanceof DOMException && err.name === 'NotAllowedError') { const reason: PasskeyFailure = elapsed < HUMAN_CANCEL_FLOOR_MS ? 'unavailable' : 'cancelled'; throw Object.assign(new Error(reason), { reason }); } throw Object.assign(new Error('error'), { reason: 'error' as PasskeyFailure }); } } On unavailable, show something like "Passkeys aren't supported here, try signing in with SMS". Only on cancelled should you stay quiet. Where does 300ms come from? The rejection I measured in LINE took 4ms, so 300ms leaves about 75 times that margin and still sits well below how long a real person takes to cancel. It's a heuristic, though, not something the spec guarantees. It also only makes sense for the modal flow. If you use conditional mediation (passkeys in the autofill dropdown), there's no prompt to time, so don't apply it there. In theory the check from section 1 already filters these environments out. This is the backstop for any WebView that lies about its capabilities, so you never ship another button that does nothing. 3. A null from the backend, a TypeError from Safari Around the same time I hit a second passkey bug. During registration, Safari and Chrome on iOS 18 threw a TypeError before the system prompt ever appeared. The backend was serializing unset WebAuthn fields as explicit null instead of leaving them out: { "authenticatorSelection": { "residentKey": null, "userVerification": "preferred" } } In JavaScript, null and "not there" feel interchangeable. The browser doesn't see it that way. It converts your options into WebAuthn's types using WebIDL rules, and some of those fields don't accept null. An explicit null fails the conversion and throws, while a missing field just gets its default. The fix is simple. Strip the nulls before handing the options to the browser: function stripNulls<T>(value: T): T { if (Array.isArray(value)) return value.map(stripNulls) as T; if (value && typeof value === 'object' && !ArrayBuffer.isView(value) && !(value instanceof ArrayBuffer)) { return Object.fromEntries( Object.entries(value) .filter(([, v]) => v !== null) .map(([k, v]) => [k, stripNulls(v)]), ) as T; } return value; } await navigator.credentials.create({ publicKey: stripNulls(creationOptions) }); The real fix is for the backend to stop sending nulls. But a small guard on the frontend means you don't have to worry about the next empty field someone adds. If you really need passkeys inside LINE Once you've detected that passkeys won't work, falling back to SMS or email isn't the only option. You can also send the user to their default browser. LINE supports a URL parameter, openExternalBrowser=1, that makes a link tapped inside LINE open in the external browser instead: https://example.com/login?openExternalBrowser=1 It doesn't apply to LIFF apps, and it takes the user out of LINE, so whether it's worth it depends on your product. Debugging inside in-app browsers The hard part this time wasn't the fix. It was not being able to see the error. You can't attach DevTools to an in-app browser, so here's what helped: - Turn on eruda outside production. It drops a small console onto the page so you can read logs on the phone itself. Load it after the page's load event so it doesn't interfere with what you're trying to observe. - Log the environment first. On page load, dump the UA, the origin, whether PublicKeyCredential and friends exist, and what the capability check returned. That one set of numbers is what cracked this bug. - Count frames in a screen recording. Before you have any logging, step through a recording frame by frame to see how long a state actually lasts. A one-frame "verifying" state is what told me the call was failing, rather than the button not being wired up. - Log the decision before you act on it. For the 300ms threshold, I first had the code just log what the threshold would have decided. Only after the numbers looked right on real devices did I let it change behavior. Checklist If your site gets opened inside in-app browsers, here's what to check when you ship passkeys: - [ ] Use getClientCapabilities() or isUserVerifyingPlatformAuthenticatorAvailable() to decide whether to show passkeys, not just whether the API exists - [ ] Put a timeout on the check and cache the result - [ ] Don't treat every NotAllowedError as a cancel; give users a way out when it's rejected too fast - [ ] Strip null fields before calling navigator.credentials - [ ] Test inside the actual apps on real devices; desktop emulation won't catch any of this References: - Simpler WebAuthn feature detection with getClientCapabilities() (web.dev) - Review privacy concerns around error conditions (W3C public-webauthn) - Opening LINE URLs in the external browser (Classmethod, Japanese)

  3. @autobullbear 参考になります ちなみにOSSでした よく使われてたので調べる機会ができて良かった 私てる感じないなら大丈夫かなありがとう!! liriliri/eruda

    17 views

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

1
PackageVersionDownloads · 30dTrend · 30d
erudav3.4.3
v3.4.3
≥205.3K

Live mentions badge

Show how many posts mention your project in the last 30 days.

Live mentions · last 30 days
liriliri/eruda mentions in the last 30 days