mirror of
https://github.com/hanzoai/extension.git
synced 2026-10-31 21:57:06 +00:00
/api/ โ /v1/iam (Hanzo path convention)
----------------------------------------
Per Hanzo convention every service uses `/v1/<service>/<endpoint>` โ
never `/api/`. The IAM extension calls were still hitting Casdoor's
upstream `/api/get-account`, `/api/userinfo`, `/api/get-organizations`,
`/api/update-user`, which the gateway no longer rewrites โ every prod
request was returning the SPA HTML and silently failing.
- shared/config.ts: new `IAM_API_PATH = '/v1/iam'` constant.
- shared/auth.ts: fetchUserInfo uses `${iamApiUrl}${IAM_API_PATH}/...`.
- background.ts + background-firefox.ts: `IAM_V1` shorthand.
- shared/kms.ts: KMS still on upstream `/api/v3` (Infisical fork; no
gateway rewrite yet) โ centralised in `KMS_PATH_PREFIX` so the day
we cut over it's a one-line change. Header comment documents this.
Default inspect-modifier: Alt โ Ctrl
------------------------------------
Alt/Option on macOS is the system character-compose modifier
(Option-c โ รง). Holding Alt while clicking elements silently swallows
every text input on every page. New default is Ctrl (matches DevTools
Inspect Element). The chord is fully configurable via a new in-popup
recorder.
- shared/shortcut.ts: Shortcut type, matchesShortcut, shortcutFromEvent,
formatShortcut, loadInspectShortcut, saveInspectShortcut.
- content-script.ts: imports + uses matchesShortcut. chrome.storage
onChanged listener picks up new bindings without page reload.
- popup.html: kbd display + Record / Reset buttons in Settings.
- popup.ts: 10-second-timed recorder using shortcutFromEvent.
- cli.ts + build.js: docs reference Ctrl+Click everywhere.
parseTabId security hardening
-----------------------------
The previous regex `(?:^|tab-)(\d+)$` accepted "https://x.com/?tab-123"
as tabId 123 (URL-borne tab-injection) and treated empty string as
tab 0 (Number('') === 0). Both fixed: fully-anchored
`^(?:tab-)?(\d+)$` plus explicit empty-string reject.
Tests
-----
tests/shared-tab-id.test.ts (28 cases) covers every parseTabId edge,
tabTarget merging, and unwrapEvaluateResult for every shape Chrome /
Firefox / hanzo-tools can hand back. tests/shared-shortcut.test.ts
(19 cases) covers DEFAULT_INSPECT_SHORTCUT, matchesShortcut required-
AND-forbidden modifier semantics, shortcutFromEvent recorder, the
formatShortcut renderer (Mac and non-Mac), and load/save round-trip.
63/63 new tests pass. Existing 134 tests still pass.
Bumps 1.8.3 โ 1.8.4 across package.json, manifest.json,
manifest-firefox.json.
152 lines
6.0 KiB
TypeScript
152 lines
6.0 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
DEFAULT_INSPECT_SHORTCUT,
|
|
formatShortcut,
|
|
loadInspectShortcut,
|
|
matchesShortcut,
|
|
saveInspectShortcut,
|
|
shortcutFromEvent,
|
|
STORAGE_KEY_INSPECT,
|
|
type Shortcut,
|
|
} from '../src/shared/shortcut.js';
|
|
|
|
describe('DEFAULT_INSPECT_SHORTCUT', () => {
|
|
it('is Ctrl alone (not Alt โ Alt collides with macOS character compose)', () => {
|
|
expect(DEFAULT_INSPECT_SHORTCUT.ctrl).toBe(true);
|
|
expect(DEFAULT_INSPECT_SHORTCUT.alt).toBe(false);
|
|
expect(DEFAULT_INSPECT_SHORTCUT.meta).toBe(false);
|
|
expect(DEFAULT_INSPECT_SHORTCUT.shift).toBe(false);
|
|
expect(DEFAULT_INSPECT_SHORTCUT.key).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('matchesShortcut', () => {
|
|
const ctrl: Shortcut = { ctrl: true, alt: false, meta: false, shift: false, key: '' };
|
|
const alt: Shortcut = { ctrl: false, alt: true, meta: false, shift: false, key: '' };
|
|
const ctrlShiftK: Shortcut = { ctrl: true, alt: false, meta: false, shift: true, key: 'k' };
|
|
|
|
it('matches Ctrl-only when only ctrlKey is set', () => {
|
|
expect(matchesShortcut({ ctrlKey: true }, ctrl)).toBe(true);
|
|
});
|
|
|
|
it('does NOT match Ctrl-only when ctrlKey is unset', () => {
|
|
expect(matchesShortcut({}, ctrl)).toBe(false);
|
|
expect(matchesShortcut({ altKey: true }, ctrl)).toBe(false);
|
|
});
|
|
|
|
it('does NOT match Ctrl-only when extra modifiers are also held', () => {
|
|
// Forbidden-modifier check โ ctrl-only is exclusive, so Ctrl+Shift
|
|
// should NOT trigger the inspect shortcut.
|
|
expect(matchesShortcut({ ctrlKey: true, shiftKey: true }, ctrl)).toBe(false);
|
|
expect(matchesShortcut({ ctrlKey: true, altKey: true }, ctrl)).toBe(false);
|
|
});
|
|
|
|
it('matches Alt-only chord against altKey', () => {
|
|
expect(matchesShortcut({ altKey: true }, alt)).toBe(true);
|
|
expect(matchesShortcut({ ctrlKey: true }, alt)).toBe(false);
|
|
});
|
|
|
|
it('matches a chord with key (Ctrl+Shift+K)', () => {
|
|
expect(matchesShortcut({ ctrlKey: true, shiftKey: true, key: 'k' }, ctrlShiftK)).toBe(true);
|
|
expect(matchesShortcut({ ctrlKey: true, shiftKey: true, key: 'K' }, ctrlShiftK)).toBe(true);
|
|
expect(matchesShortcut({ ctrlKey: true, shiftKey: true, key: 'j' }, ctrlShiftK)).toBe(false);
|
|
// Wrong modifier set
|
|
expect(matchesShortcut({ ctrlKey: true, key: 'k' }, ctrlShiftK)).toBe(false);
|
|
});
|
|
|
|
it('modifier-only chord ignores subsequent letter keys', () => {
|
|
// When the chord is "Ctrl alone", a click while holding Ctrl+'a' still
|
|
// matches (because the chord doesn't care which letter).
|
|
expect(matchesShortcut({ ctrlKey: true, key: 'a' }, ctrl)).toBe(true);
|
|
expect(matchesShortcut({ ctrlKey: true, key: '' }, ctrl)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('shortcutFromEvent', () => {
|
|
it('records a letter chord (Ctrl+K)', () => {
|
|
const out = shortcutFromEvent({ ctrlKey: true, key: 'k' });
|
|
expect(out).toEqual({ ctrl: true, alt: false, meta: false, shift: false, key: 'k' });
|
|
});
|
|
|
|
it('records a modifier-only chord when keydown is the modifier itself', () => {
|
|
// Browser dispatches keydown with key='Control' for Ctrl press alone.
|
|
const out = shortcutFromEvent({ ctrlKey: true, key: 'Control' });
|
|
expect(out).toEqual({ ctrl: true, alt: false, meta: false, shift: false, key: '' });
|
|
});
|
|
|
|
it('records compound modifiers (Ctrl+Shift)', () => {
|
|
const out = shortcutFromEvent({ ctrlKey: true, shiftKey: true, key: 'Shift' });
|
|
expect(out).toEqual({ ctrl: true, alt: false, meta: false, shift: true, key: '' });
|
|
});
|
|
|
|
it('rejects Tab / Escape / Enter (these are navigation, not chords)', () => {
|
|
expect(shortcutFromEvent({ key: 'Tab' })).toBeNull();
|
|
expect(shortcutFromEvent({ key: 'Escape' })).toBeNull();
|
|
expect(shortcutFromEvent({ key: 'Enter' })).toBeNull();
|
|
expect(shortcutFromEvent({ key: '' })).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('formatShortcut', () => {
|
|
const ctrl: Shortcut = { ctrl: true, alt: false, meta: false, shift: false, key: '' };
|
|
const ctrlShiftK: Shortcut = { ctrl: true, alt: false, meta: false, shift: true, key: 'k' };
|
|
|
|
it('renders Ctrl+Click on non-Mac', () => {
|
|
expect(formatShortcut(ctrl, { mac: false, click: true })).toBe('Ctrl+Click');
|
|
});
|
|
|
|
it('renders โ+Click on Mac', () => {
|
|
expect(formatShortcut(ctrl, { mac: true, click: true })).toBe('โ+Click');
|
|
});
|
|
|
|
it('renders compound chord Ctrl+Shift+K', () => {
|
|
expect(formatShortcut(ctrlShiftK, { mac: false })).toBe('Ctrl+Shift+K');
|
|
});
|
|
|
|
it('returns "(unset)" for empty shortcut', () => {
|
|
expect(formatShortcut({ ctrl: false, alt: false, meta: false, shift: false, key: '' }))
|
|
.toBe('(unset)');
|
|
});
|
|
});
|
|
|
|
describe('loadInspectShortcut / saveInspectShortcut', () => {
|
|
function makeStorage() {
|
|
const store: Record<string, any> = {};
|
|
return {
|
|
data: store,
|
|
get: async (keys: string[]) => Object.fromEntries(keys.map((k) => [k, store[k]])),
|
|
set: async (items: Record<string, unknown>) => Object.assign(store, items),
|
|
};
|
|
}
|
|
|
|
it('returns the default when nothing is stored', async () => {
|
|
const s = await loadInspectShortcut(makeStorage());
|
|
expect(s).toEqual(DEFAULT_INSPECT_SHORTCUT);
|
|
});
|
|
|
|
it('round-trips a saved shortcut', async () => {
|
|
const storage = makeStorage();
|
|
const cust: Shortcut = { ctrl: false, alt: false, meta: true, shift: true, key: 'p' };
|
|
await saveInspectShortcut(storage, cust);
|
|
expect(storage.data[STORAGE_KEY_INSPECT]).toEqual(cust);
|
|
const loaded = await loadInspectShortcut(storage);
|
|
expect(loaded).toEqual(cust);
|
|
});
|
|
|
|
it('falls back to default when stored value is malformed', async () => {
|
|
const storage = makeStorage();
|
|
storage.data[STORAGE_KEY_INSPECT] = { ctrl: 'yes' }; // wrong type
|
|
const loaded = await loadInspectShortcut(storage);
|
|
expect(loaded).toEqual(DEFAULT_INSPECT_SHORTCUT);
|
|
});
|
|
|
|
it('survives storage.get throwing', async () => {
|
|
const storage = {
|
|
get: async () => { throw new Error('storage offline'); },
|
|
set: async () => {},
|
|
};
|
|
const loaded = await loadInspectShortcut(storage);
|
|
expect(loaded).toEqual(DEFAULT_INSPECT_SHORTCUT);
|
|
});
|
|
});
|