Skill Tracer
Skills › gstack › browse › extension-id.ts

extension-id.ts

45 lines · browse/scripts/extension-id.ts at 3f79d81

1#!/usr/bin/env -S bun --no-env-file --config=/dev/null
2/**
3 * Derive the Chrome extension ID from the "key" field in
4 * extension/manifest.json.
5 *
6 * Chrome computes an extension's ID as the first 16 bytes of the SHA-256
7 * hash of the DER-encoded public key, with each hex nibble mapped from
8 * 0-9a-f to a-p (the "mpdecimal" alphabet). Pinning the public key in the
9 * manifest pins the ID, which lets the browse server verify the Origin
10 * header on POST /extension-token against a single known extension
11 * identity (GSTACK_EXTENSION_ID in browse/src/extension-id.ts).
12 *
13 * The private half of the keypair is intentionally NOT in the repo — the
14 * extension is loaded unpacked (or baked into Browser.app), so only the
15 * public key is needed to pin the ID. Regenerating the keypair changes
16 * the ID and requires updating both the manifest "key" and the
17 * GSTACK_EXTENSION_ID constant.
18 *
19 * Usage: bun browse/scripts/extension-id.ts [path/to/manifest.json]
20 */
22import { createHash } from 'node:crypto';
23import * as fs from 'node:fs';
24import * as path from 'node:path';
26const manifestPath = process.argv[2]
27 ?? path.join(import.meta.dir, '../../extension/manifest.json');
29const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
30if (typeof manifest.key !== 'string' || manifest.key.length === 0) {
31 console.error(`No "key" field in ${manifestPath}`);
32 process.exit(1);
35export function extensionIdFromPublicKey(publicKeyBase64: string): string {
36 const der = Buffer.from(publicKeyBase64, 'base64');
37 const hex = createHash('sha256').update(der).digest('hex').slice(0, 32);
38 let id = '';
39 for (const c of hex) {
40 id += String.fromCharCode('a'.charCodeAt(0) + parseInt(c, 16));
41 }
42 return id;
45console.log(extensionIdFromPublicKey(manifest.key));