Files
AutistMask/src/content/inpage.js
clawbot 9dcd875dd4
All checks were successful
check / check (push) Successful in 28s
fix: carry EIP-1193 error codes through to the page (closes #274)
The provider rebuilt every rejection as a bare Error carrying only a message,
so a dApp checking err.code === 4001 saw undefined and could not tell a user's
deliberate refusal from a failure. Well-behaved sites therefore showed an error
or retried instead of accepting the refusal. The code was produced correctly
and did cross the extension boundary; it was lost in the last hop.

Rejections now reach the page as a ProviderRpcError carrying code, and data
where present. The code is passed through verbatim rather than matched against
a whitelist, so a code added upstream later needs no change here. An error that
genuinely has no code stays a plain Error with no code property at all, rather
than advertising code: undefined -- 'code' in err is what a careful dApp asks.

Messages are unchanged for every path, verified byte-for-byte against the
previous provider across every background error shape.

The end-to-end assertion that printed the observed code now requires it.
2026-08-12 13:47:33 +02:00

256 lines
8.9 KiB
JavaScript

// AutistMask inpage script — injected into the page's JS context.
// Creates window.ethereum (EIP-1193 provider) and announces via EIP-6963.
(function () {
// Defaults to mainnet; updated dynamically via eth_chainId on init and
// chainChanged events from the extension.
let currentChainId = "0x1";
let currentNetworkVersion = "1";
const listeners = {};
let nextId = 1;
const pending = {};
// EIP-1193 ProviderRpcError: `code`, `message`, optional `data`. A class
// rather than properties bolted onto an Error because this object crosses
// no boundary after construction — it is built in the page's own realm and
// handed straight to the caller's catch — so the prototype survives and
// `error.name` is a stable thing for a dApp to see.
class ProviderRpcError extends Error {
constructor(code, message, data) {
super(message);
this.name = "ProviderRpcError";
this.code = code;
if (data !== undefined) this.data = data;
}
}
// Rebuild a boundary error as the error the page catches, carrying the
// code (and data) the extension reported. Without this a dApp cannot tell
// a user's refusal (4001) from a wallet that broke, and retries or shows
// an error instead of accepting the refusal.
//
// Whatever code arrived is passed through verbatim rather than being
// matched against a list: the extension emits 4001, 4100 and 4902 today,
// and a code this file has never heard of is still the truth about what
// happened. An error reported with no code at all stays a plain Error —
// a ProviderRpcError whose `code` is undefined would advertise a
// conformance it does not have. `message` is untouched in every case.
function toPageError(error) {
const message = (error && error.message) || "Request failed";
if (error && error.code !== undefined && error.code !== null) {
return new ProviderRpcError(error.code, message, error.data);
}
return new Error(message);
}
// Listen for responses from the content script
window.addEventListener("message", function onUuid(event) {
if (event.source !== window) return;
if (event.data?.type !== "AUTISTMASK_RESPONSE") return;
const { id, result, error } = event.data;
const p = pending[id];
if (!p) return;
delete pending[id];
if (error) {
p.reject(toPageError(error));
} else {
p.resolve(result);
}
});
// Listen for events pushed from the extension
window.addEventListener("message", function onUuid(event) {
if (event.source !== window) return;
if (event.data?.type !== "AUTISTMASK_EVENT") return;
const { eventName, data } = event.data;
if (eventName === "chainChanged") {
currentChainId = data;
currentNetworkVersion = String(parseInt(data, 16));
provider.chainId = currentChainId;
provider.networkVersion = currentNetworkVersion;
}
emit(eventName, data);
});
function emit(eventName, data) {
const cbs = listeners[eventName];
if (!cbs) return;
for (const cb of cbs) {
try {
cb(data);
} catch (e) {
// ignore listener errors
}
}
}
function sendRequest(args) {
return new Promise((resolve, reject) => {
const id = nextId++;
pending[id] = { resolve, reject };
window.postMessage(
{ type: "AUTISTMASK_REQUEST", id, ...args },
"*",
);
});
}
const provider = {
isAutistMask: true,
isMetaMask: true, // compatibility — many dApps check this
chainId: currentChainId,
networkVersion: currentNetworkVersion,
selectedAddress: null,
async request(args) {
const result = await sendRequest({
method: args.method,
params: args.params || [],
});
if (
args.method === "eth_requestAccounts" ||
args.method === "eth_accounts"
) {
provider.selectedAddress =
Array.isArray(result) && result.length > 0
? result[0]
: null;
}
if (args.method === "eth_chainId" && result) {
currentChainId = result;
currentNetworkVersion = String(parseInt(result, 16));
provider.chainId = currentChainId;
provider.networkVersion = currentNetworkVersion;
}
return result;
},
// Legacy methods (still used by some dApps)
enable() {
return this.request({ method: "eth_requestAccounts" });
},
send(methodOrPayload, paramsOrCallback) {
// Handle both send(method, params) and send({method, params})
if (typeof methodOrPayload === "string") {
return this.request({
method: methodOrPayload,
params: paramsOrCallback || [],
});
}
return this.request({
method: methodOrPayload.method,
params: methodOrPayload.params || [],
});
},
sendAsync(payload, callback) {
this.request({
method: payload.method,
params: payload.params || [],
})
.then((result) =>
callback(null, { id: payload.id, jsonrpc: "2.0", result }),
)
.catch((err) => callback(err));
},
on(event, cb) {
if (!listeners[event]) listeners[event] = [];
listeners[event].push(cb);
return this;
},
removeListener(event, cb) {
if (!listeners[event]) return this;
listeners[event] = listeners[event].filter((c) => c !== cb);
return this;
},
removeAllListeners(event) {
if (event) {
delete listeners[event];
} else {
for (const key of Object.keys(listeners)) {
delete listeners[key];
}
}
return this;
},
// Some dApps (wagmi) check this to confirm MetaMask-like behavior
_metamask: {
isUnlocked() {
return Promise.resolve(provider.selectedAddress !== null);
},
},
};
// Set window.ethereum if no other wallet has claimed it
if (typeof window.ethereum === "undefined") {
window.ethereum = provider;
}
window.dispatchEvent(new Event("ethereum#initialized"));
// EIP-6963: Multi Injected Provider Discovery
const ICON_SVG =
"data:image/svg+xml," +
encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">' +
'<rect width="32" height="32" rx="6" fill="#000"/>' +
'<text x="16" y="23" text-anchor="middle" font-family="monospace" font-size="20" font-weight="bold" fill="#fff">A</text>' +
"</svg>",
);
let providerUuid = crypto.randomUUID(); // fallback until real UUID arrives
function buildProviderInfo() {
return {
uuid: providerUuid,
name: "AutistMask",
icon: ICON_SVG,
rdns: "berlin.sneak.autistmask",
};
}
function announceProvider() {
window.dispatchEvent(
new CustomEvent("eip6963:announceProvider", {
detail: Object.freeze({
info: buildProviderInfo(),
provider,
}),
}),
);
}
// Listen for the persisted UUID from the content script
function onProviderUuid(event) {
if (event.source !== window) return;
if (event.data?.type !== "AUTISTMASK_PROVIDER_UUID") return;
window.removeEventListener("message", onProviderUuid);
providerUuid = event.data.uuid;
announceProvider();
}
window.addEventListener("message", onProviderUuid);
window.addEventListener("eip6963:requestProvider", announceProvider);
announceProvider();
// Fetch the current chain ID from the extension on load so the provider
// reflects the selected network immediately (covers Sepolia etc.).
sendRequest({ method: "eth_chainId", params: [] })
.then((chainId) => {
if (chainId) {
currentChainId = chainId;
currentNetworkVersion = String(parseInt(chainId, 16));
provider.chainId = currentChainId;
provider.networkVersion = currentNetworkVersion;
}
})
.catch(() => {
// Best-effort — keep defaults.
});
})();