SDK reference

One file. ES module. One HTTP call on install.

6,950 bytes uncompressed, MIT licensed. The whole source is at the bottom of this page.

Get it

Direct download /sdk/ef-sdk.js  ·  right-click → save as
GitHub (source, issues, PRs) github.com/USSupportLLC/ExtensionFeedback
Or via curl curl -O https://extensionfeedback.com/sdk/ef-sdk.js

API

ExtensionFeedback.init(options)

Call once during background-service-worker startup. Idempotent — safe to call on every wake. Both pages are independently optional.

ExtensionFeedback.init({
  apiKey:    string,             // required. "ef_live_..." from your dashboard
  install:   boolean = false,    // optional. open install page on first install
  uninstall: boolean = true,     // optional. wire chrome.runtime.setUninstallURL
});

Pass install: false (or omit) to skip the install page entirely. Pass uninstall: false to skip wiring the uninstall URL — Chrome's default uninstall behavior takes over.

ExtensionFeedback.openFeedback()

Opens the install page in a new tab on demand — useful for a "Send feedback" button in your popup.

document.querySelector("#feedback-btn")
  .addEventListener("click", () =>
    ExtensionFeedback.openFeedback()
  );

What it actually does

The whole thing, inline

Exactly what you'll get from /sdk/ef-sdk.js. Read it, audit it, modify it — it's MIT.

/**
 * ExtensionFeedback SDK — install/uninstall feedback for Chrome extensions.
 *
 * Drop this file next to your background.js, then:
 *
 *     import { ExtensionFeedback } from "./ef-sdk.js";
 *     ExtensionFeedback.init({
 *         apiKey:    "ef_live_...",
 *         install:   true,           // optional; default false. Open install page on first install.
 *         uninstall: true,           // optional; default true.  Wire uninstall URL.
 *     });
 *
 * Both pages are independently optional. Pass install: false (or omit) to skip the
 * install page; pass uninstall: false to skip wiring the uninstall URL.
 *
 * Behavior:
 *   - On first init, makes ONE HTTPS POST to {HOST}/api/v1/install to register
 *     this install and obtain a per-install signing secret. The secret is
 *     stored in chrome.storage.local under __ef_install_secret. Secrets are
 *     unique per install_id and used to HMAC-sign all subsequent feedback URLs.
 *   - On chrome.runtime.onInstalled with reason "install", opens the install page
 *     (only if init({ install: true })).
 *   - On init, calls chrome.runtime.setUninstallURL() so Chrome opens the uninstall
 *     page automatically when the user removes your extension (only if
 *     uninstall !== false; default true).
 *   - A 128-bit random installId is stored in chrome.storage.local under
 *     __ef_install_id and reused across both pages.
 *
 * Manifest requirements: "storage" permission only. The fetch() to
 * extensionfeedback.com from a service worker works without host_permissions
 * because the server returns CORS headers.
 *
 * License: MIT.
 */

const HOST = "https://extensionfeedback.com";
const STORAGE_KEY        = "__ef_install_id";
const SECRET_STORAGE_KEY = "__ef_install_secret";

let _apiKey = null;
let _installEnabled = false;
let _uninstallEnabled = true;
let _ready = null;

async function getOrCreateInstallId() {
    const stored = await chrome.storage.local.get(STORAGE_KEY);
    if (stored && typeof stored[STORAGE_KEY] === "string" && stored[STORAGE_KEY].length === 32) {
        return stored[STORAGE_KEY];
    }
    const buf = new Uint8Array(16);
    crypto.getRandomValues(buf);
    const id = Array.from(buf, b => b.toString(16).padStart(2, "0")).join("");
    await chrome.storage.local.set({ [STORAGE_KEY]: id });
    return id;
}

async function getOrFetchInstallSecret(extId, installId) {
    const stored = await chrome.storage.local.get(SECRET_STORAGE_KEY);
    if (stored && typeof stored[SECRET_STORAGE_KEY] === "string" && stored[SECRET_STORAGE_KEY].length === 64) {
        return stored[SECRET_STORAGE_KEY];
    }

    // Register this install with the server. ONE HTTP call, ever.
    const resp = await fetch(`${HOST}/api/v1/install`, {
        method:  "POST",
        headers: { "Content-Type": "application/json" },
        body:    JSON.stringify({ ext: extId, key: _apiKey, iid: installId }),
    });
    if (!resp.ok) {
        throw new Error(`[ExtensionFeedback] register failed: HTTP ${resp.status}`);
    }
    const data = await resp.json();
    if (!data || typeof data.install_secret !== "string" || data.install_secret.length !== 64) {
        throw new Error("[ExtensionFeedback] register returned invalid secret");
    }
    await chrome.storage.local.set({ [SECRET_STORAGE_KEY]: data.install_secret });
    return data.install_secret;
}

async function hmacSha256Hex(secretHex, msg) {
    const enc       = new TextEncoder();
    const keyBytes  = enc.encode(secretHex);
    const msgBytes  = enc.encode(msg);
    const cryptoKey = await crypto.subtle.importKey(
        "raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
    );
    const sig = await crypto.subtle.sign("HMAC", cryptoKey, msgBytes);
    return Array.from(new Uint8Array(sig), b => b.toString(16).padStart(2, "0")).join("");
}

async function buildUrl(prefix) {
    const extId = chrome.runtime.id;
    const v     = chrome.runtime.getManifest().version || "";
    const iid   = await getOrCreateInstallId();
    const l     = (chrome.i18n && chrome.i18n.getUILanguage) ? chrome.i18n.getUILanguage() : "en";

    const installSecret = await getOrFetchInstallSecret(extId, iid);
    const payload = `${extId}|${iid}|${v}|${l}`;
    const sig     = await hmacSha256Hex(installSecret, payload);

    const qs = new URLSearchParams({ key: _apiKey, v, iid, l, sig });
    return `${HOST}${prefix}${extId}?${qs.toString()}`;
}

async function setUninstallUrl() {
    const url = await buildUrl("/u/");
    if (chrome.runtime.setUninstallURL) {
        chrome.runtime.setUninstallURL(url);
    }
}

export const ExtensionFeedback = {
    /**
     * Initialize. Call once on service-worker startup. Idempotent.
     *
     * @param {object} options
     * @param {string}  options.apiKey               "ef_live_..." from your dashboard
     * @param {boolean} [options.install=false]      open install page on first install
     * @param {boolean} [options.uninstall=true]     wire uninstall URL (Chrome opens on uninstall)
     */
    init(options) {
        if (!options || typeof options.apiKey !== "string" || !options.apiKey.startsWith("ef_live_")) {
            console.warn("[ExtensionFeedback] init() requires a valid apiKey starting with ef_live_");
            return;
        }
        _apiKey = options.apiKey;
        _installEnabled   = !!options.install;
        _uninstallEnabled = options.uninstall !== false;  // default true

        // Wire onInstalled handler — only fires on actual install/update events.
        if (chrome.runtime && chrome.runtime.onInstalled) {
            chrome.runtime.onInstalled.addListener(async (details) => {
                if (details.reason === "install" && _installEnabled) {
                    try {
                        const url = await buildUrl("/i/");
                        chrome.tabs.create({ url });
                    } catch (e) {
                        console.warn("[ExtensionFeedback] could not open install page:", e);
                    }
                }
            });
        }

        // Set uninstall URL now (idempotent — Chrome stores the latest value).
        // Triggers the install-secret fetch on first run if missing.
        if (_uninstallEnabled) {
            _ready = setUninstallUrl().catch((e) => {
                console.warn("[ExtensionFeedback] could not set uninstall URL:", e);
            });
        }
    },

    /**
     * Open the install page in a new tab on demand. Useful for a "Send feedback"
     * button in your popup. Returns the chrome.tabs.create() promise.
     */
    async openFeedback() {
        if (!_apiKey) {
            console.warn("[ExtensionFeedback] openFeedback() called before init()");
            return;
        }
        const url = await buildUrl("/i/");
        return chrome.tabs.create({ url });
    },

    /** Internal — exposed only for tests. */
    _getInstallId: getOrCreateInstallId,
};