feat(ext/sw): org_switch + org read messages (grant-filtered, offline-aware, cache cleared on lock)
Wires 4 SW messages to the org-vault.ts read functions (Task 5b): - org_switch: opens org vault via openOrg(), caches OrgHandleState, sets session context; switching to 'personal' resets context without openOrg. Offline-aware: if openOrg throws a network error and a prior cached state exists, reuses it read-only (offline:true) via exported isNetworkError. - org_list_items: returns cached, grant-filtered OrgManifestEntry[] (not personal ManifestEntry[]) via listOrgItems(requireCurrentOrgState()). - org_get_item: decrypts a single org item via getOrgItem. - org_list_collections: returns granted Collection[] via listOrgCollections. Three-place rule satisfied for all 4 messages: 1. messages.ts PopupMessage union (4 variants) 2. messages.ts POPUP_ONLY_TYPES (4 entries) 3. popup-only.ts switch (4 case arms) Cache lifecycle / use-after-free guard: - clearOrgCache() added to org-handlers.ts; exports. - Called in popup-only.ts 'lock' handler (before session.clearCurrent). - Called in index.ts inactivity-timer onExpired (before clearCurrent). Plan doc fixes: - Line ~410: org_list_items return type corrected to OrgManifestEntry[]. - Line ~47: org_unwrap_key signature updated (no device_private_key_base64 param — reads DEVICE_STATE internally). 9 new tests in org-handlers.test.ts (TDD: all watched fail before passing). Full SW suite: 167 tests (17 files) green. build:all type-checks clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
This commit is contained in:
@@ -44,7 +44,7 @@
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `relicario_core::org::unwrap_org_key(wrapped: &[u8], ed25519_seed: &Zeroizing<[u8;32]>) -> Result<Zeroizing<[u8;32]>>` (`crates/relicario-core/src/org.rs:299`); `session::insert(master_key, image_secret) -> u32` (`crates/relicario-wasm/src/session.rs`).
|
||||
- Produces: `org_unwrap_key(keys_blob: &[u8], device_private_key_base64: &str) -> Result<SessionHandle, JsError>`. The returned handle is an ordinary `SessionHandle` — callers use the existing `item_decrypt`/`item_encrypt`/`manifest_decrypt`/`manifest_encrypt` with it.
|
||||
- Produces: `org_unwrap_key(keys_blob: &[u8]) -> Result<SessionHandle, JsError>` (reads `DEVICE_STATE` internally — no private-key param crosses JS). The returned handle is an ordinary `SessionHandle` — callers use the existing `item_decrypt`/`item_encrypt`/`manifest_decrypt`/`manifest_encrypt` with it.
|
||||
|
||||
- [ ] **Step 1: Confirm the device-key form.** Read how `device_private_key` is produced — `crates/relicario-wasm/src/lib.rs` `register_device`/`generate_device_keypair` and `crates/relicario-core/src/device.rs`. Determine whether `private_key_base64` is the raw 32-byte ed25519 seed or an OpenSSH blob, and write `org_unwrap_key` to decode it to the 32-byte seed `Zeroizing<[u8;32]>` that `unwrap_org_key` expects. Note the finding in a code comment.
|
||||
|
||||
@@ -407,7 +407,7 @@ git commit -m "feat(ext/sw): org-vault — unwrap, fetch, grant-filter manifest"
|
||||
- Test: `extension/src/service-worker/__tests__/org-vault.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces SW messages: `org_switch {context}` → `{ ok, data: { context, offline } }`; `org_list_items` → `{ ok, data: ManifestEntry[] }`; `org_get_item {id}` → `{ ok, data: Item }`; `org_list_collections` → `{ ok, data: Collection[] }`. On a git network error during switch, set `offline: true` and serve the last-cached manifest read-only.
|
||||
- Produces SW messages: `org_switch {context}` → `{ ok, data: { context, offline } }`; `org_list_items` → `{ ok, data: OrgManifestEntry[] }`; `org_get_item {id}` → `{ ok, data: Item }`; `org_list_collections` → `{ ok, data: Collection[] }`. On a git network error during switch, set `offline: true` and serve the last-cached manifest read-only.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
|
||||
225
extension/src/service-worker/__tests__/org-handlers.test.ts
Normal file
225
extension/src/service-worker/__tests__/org-handlers.test.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
/// Tests for org-handlers.ts (Task 6): org_switch + 3 read handlers + cache lifecycle.
|
||||
///
|
||||
/// Tests at the handler-function level, not via popup-only.handle(). Thrown
|
||||
/// errors (e.g. not_in_org_context) surface in production via index.ts's outer
|
||||
/// .catch; here we assert them directly with expect(() => ...).toThrow().
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import * as session from '../session';
|
||||
|
||||
// --- Mocks (hoisted) ---
|
||||
|
||||
vi.mock('../org-vault', () => ({
|
||||
openOrg: vi.fn(),
|
||||
listOrgItems: vi.fn(),
|
||||
getOrgItem: vi.fn(),
|
||||
listOrgCollections: vi.fn(),
|
||||
isNetworkError: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../org-config', () => ({
|
||||
loadOrgConfigs: vi.fn(),
|
||||
}));
|
||||
|
||||
import * as orgVaultMod from '../org-vault';
|
||||
import * as orgConfigMod from '../org-config';
|
||||
import {
|
||||
handleOrgSwitch,
|
||||
handleOrgListItems,
|
||||
handleOrgGetItem,
|
||||
handleOrgListCollections,
|
||||
clearOrgCache,
|
||||
} from '../router/org-handlers';
|
||||
import type { OrgHandleState } from '../org-vault';
|
||||
import type { OrgManifestEntry, Collection, Item } from '../../shared/types';
|
||||
|
||||
// --- Test data ---
|
||||
|
||||
const ORG_ID = 'org-abc123';
|
||||
|
||||
const ORG_CFG = {
|
||||
orgId: ORG_ID,
|
||||
displayName: 'Test Org',
|
||||
hostType: 'gitea' as const,
|
||||
hostUrl: 'https://git.example',
|
||||
repoPath: 'org/vault',
|
||||
apiToken: 'tok-abc',
|
||||
memberId: 'member001',
|
||||
};
|
||||
|
||||
const FILTERED_ENTRIES: OrgManifestEntry[] = [
|
||||
{ id: 'item001', type: 'login', title: 'Login A', tags: [], modified: 1000, collection: 'col-a' },
|
||||
{ id: 'item002', type: 'secure_note', title: 'Note B', tags: [], modified: 1001, collection: 'col-b' },
|
||||
];
|
||||
|
||||
const ORG_MANIFEST = { schema_version: 1, entries: FILTERED_ENTRIES };
|
||||
|
||||
const GRANTED_COLLECTIONS: Collection[] = [
|
||||
{ slug: 'col-a', display_name: 'Collection A' },
|
||||
{ slug: 'col-b', display_name: 'Collection B' },
|
||||
];
|
||||
|
||||
const DECRYPTED_ITEM: Item = {
|
||||
id: 'item001', title: 'Login A', type: 'login', tags: [], favorite: false,
|
||||
created: 100, modified: 1000,
|
||||
core: { type: 'login', username: 'user', password: 'pass' },
|
||||
sections: [], attachments: [], field_history: {},
|
||||
};
|
||||
|
||||
const FAKE_WASM = { label: 'fake-wasm-module' };
|
||||
|
||||
function makeFakeHandle() {
|
||||
return { value: 42, free: vi.fn() };
|
||||
}
|
||||
|
||||
function makeFakeOrgState(overrides: Partial<OrgHandleState> = {}): OrgHandleState {
|
||||
return {
|
||||
orgId: ORG_ID,
|
||||
host: {} as never,
|
||||
handle: makeFakeHandle() as never,
|
||||
grants: ['col-a', 'col-b'],
|
||||
manifest: ORG_MANIFEST as never,
|
||||
offline: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Suite ---
|
||||
|
||||
describe('org-handlers', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset real session module state (calls .free() on any stored handles).
|
||||
session.clearAll();
|
||||
// Reset the module-level org cache in org-handlers.
|
||||
clearOrgCache();
|
||||
// Default: one org configured, isNetworkError returns false.
|
||||
vi.mocked(orgConfigMod.loadOrgConfigs).mockResolvedValue([ORG_CFG]);
|
||||
vi.mocked(orgVaultMod.isNetworkError).mockReturnValue(false);
|
||||
});
|
||||
|
||||
// Test 1: org_switch to an orgId — openOrg called, session wired, offline:false
|
||||
it('org_switch to an org: calls openOrg, wires setOrg + setContext, returns { context, offline:false }', async () => {
|
||||
const fakeState = makeFakeOrgState({ offline: false });
|
||||
vi.mocked(orgVaultMod.openOrg).mockResolvedValue(fakeState);
|
||||
|
||||
const result = await handleOrgSwitch({ context: ORG_ID }, FAKE_WASM);
|
||||
|
||||
expect(result).toEqual({ ok: true, data: { context: ORG_ID, offline: false } });
|
||||
// openOrg called with the config + wasm module; no prior manifest (first switch)
|
||||
expect(orgVaultMod.openOrg).toHaveBeenCalledWith(ORG_CFG, FAKE_WASM, undefined);
|
||||
// Session context set to the org
|
||||
expect(session.currentContext()).toBe(ORG_ID);
|
||||
// Handle from org state registered in session
|
||||
expect(session.getOrg(ORG_ID)).toBe(fakeState.handle);
|
||||
});
|
||||
|
||||
// Test 2: org_switch to 'personal' — no openOrg call, context reset to 'personal'
|
||||
it("org_switch to 'personal': sets personal context, returns { context:'personal', offline:false }, openOrg not called", async () => {
|
||||
const result = await handleOrgSwitch({ context: 'personal' }, FAKE_WASM);
|
||||
|
||||
expect(result).toEqual({ ok: true, data: { context: 'personal', offline: false } });
|
||||
expect(orgVaultMod.openOrg).not.toHaveBeenCalled();
|
||||
expect(session.currentContext()).toBe('personal');
|
||||
});
|
||||
|
||||
// Test 3: offline fallback — openOrg throws network error + prior cached state exists → offline:true
|
||||
it('org_switch: openOrg throws network error + prior cached state → returns offline:true (reuses cache, no error)', async () => {
|
||||
// Populate cache with a successful first switch.
|
||||
const fakeState = makeFakeOrgState();
|
||||
vi.mocked(orgVaultMod.openOrg).mockResolvedValueOnce(fakeState);
|
||||
await handleOrgSwitch({ context: ORG_ID }, FAKE_WASM);
|
||||
|
||||
// Second switch: network error + isNetworkError returns true.
|
||||
const networkErr = new TypeError('Failed to fetch');
|
||||
vi.mocked(orgVaultMod.openOrg).mockRejectedValueOnce(networkErr);
|
||||
vi.mocked(orgVaultMod.isNetworkError).mockReturnValue(true);
|
||||
|
||||
const result = await handleOrgSwitch({ context: ORG_ID }, FAKE_WASM);
|
||||
|
||||
expect(result).toEqual({ ok: true, data: { context: ORG_ID, offline: true } });
|
||||
// openOrg was called with the prior cached manifest as offline fallback
|
||||
expect(orgVaultMod.openOrg).toHaveBeenLastCalledWith(ORG_CFG, FAKE_WASM, fakeState.manifest);
|
||||
// Context still set to the org
|
||||
expect(session.currentContext()).toBe(ORG_ID);
|
||||
});
|
||||
|
||||
// Test 4: org_switch to unconfigured orgId → { ok:false, error:'org_not_configured' }
|
||||
it('org_switch to unconfigured orgId: returns { ok:false, error:org_not_configured }', async () => {
|
||||
vi.mocked(orgConfigMod.loadOrgConfigs).mockResolvedValue([]); // no configs
|
||||
|
||||
const result = await handleOrgSwitch({ context: 'org-does-not-exist' }, FAKE_WASM);
|
||||
|
||||
expect(result).toEqual({ ok: false, error: 'org_not_configured' });
|
||||
expect(orgVaultMod.openOrg).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Test 5: org_list_items after a switch → returns cached filtered OrgManifestEntry[]
|
||||
it('org_list_items after org_switch: returns cached filtered OrgManifestEntry[] (not personal ManifestEntry[])', async () => {
|
||||
const fakeState = makeFakeOrgState();
|
||||
vi.mocked(orgVaultMod.openOrg).mockResolvedValue(fakeState);
|
||||
vi.mocked(orgVaultMod.listOrgItems).mockReturnValue(FILTERED_ENTRIES);
|
||||
await handleOrgSwitch({ context: ORG_ID }, FAKE_WASM);
|
||||
|
||||
const result = handleOrgListItems();
|
||||
|
||||
expect(result).toEqual({ ok: true, data: FILTERED_ENTRIES });
|
||||
// listOrgItems called with the cached OrgHandleState
|
||||
expect(orgVaultMod.listOrgItems).toHaveBeenCalledWith(fakeState);
|
||||
// The data entries have collection fields (OrgManifestEntry, not ManifestEntry)
|
||||
expect((result.data as OrgManifestEntry[])[0]).toHaveProperty('collection', 'col-a');
|
||||
});
|
||||
|
||||
// Test 6: org_get_item → decrypted Item
|
||||
it('org_get_item: calls getOrgItem with cached state + id + wasm, returns decrypted Item', async () => {
|
||||
const fakeState = makeFakeOrgState();
|
||||
vi.mocked(orgVaultMod.openOrg).mockResolvedValue(fakeState);
|
||||
vi.mocked(orgVaultMod.getOrgItem).mockResolvedValue(DECRYPTED_ITEM);
|
||||
await handleOrgSwitch({ context: ORG_ID }, FAKE_WASM);
|
||||
|
||||
const result = await handleOrgGetItem({ id: 'item001' }, FAKE_WASM);
|
||||
|
||||
expect(result).toEqual({ ok: true, data: DECRYPTED_ITEM });
|
||||
expect(orgVaultMod.getOrgItem).toHaveBeenCalledWith(fakeState, 'item001', FAKE_WASM);
|
||||
});
|
||||
|
||||
// Test 7: org_list_collections → granted Collection[]
|
||||
it('org_list_collections: calls listOrgCollections with cached state, returns granted Collection[]', async () => {
|
||||
const fakeState = makeFakeOrgState();
|
||||
vi.mocked(orgVaultMod.openOrg).mockResolvedValue(fakeState);
|
||||
vi.mocked(orgVaultMod.listOrgCollections).mockResolvedValue(GRANTED_COLLECTIONS);
|
||||
await handleOrgSwitch({ context: ORG_ID }, FAKE_WASM);
|
||||
|
||||
const result = await handleOrgListCollections();
|
||||
|
||||
expect(result).toEqual({ ok: true, data: GRANTED_COLLECTIONS });
|
||||
expect(orgVaultMod.listOrgCollections).toHaveBeenCalledWith(fakeState);
|
||||
});
|
||||
|
||||
// Test 8: org_list_items with 'personal' context (no org switch) → throws not_in_org_context
|
||||
it("org_list_items with no org switch (context is 'personal'): throws not_in_org_context", () => {
|
||||
// Default context is 'personal' (set by session.clearAll() in beforeEach).
|
||||
expect(session.currentContext()).toBe('personal');
|
||||
|
||||
expect(() => handleOrgListItems()).toThrow('not_in_org_context');
|
||||
});
|
||||
|
||||
// Test 9: clearOrgCache — empties the map; org read throws not_in_org_context after clear
|
||||
it('clearOrgCache: empties the org state cache; org read after clear throws not_in_org_context', async () => {
|
||||
// Populate cache.
|
||||
const fakeState = makeFakeOrgState();
|
||||
vi.mocked(orgVaultMod.openOrg).mockResolvedValue(fakeState);
|
||||
vi.mocked(orgVaultMod.listOrgItems).mockReturnValue(FILTERED_ENTRIES);
|
||||
await handleOrgSwitch({ context: ORG_ID }, FAKE_WASM);
|
||||
|
||||
// Confirm we can read (cache is populated, context is ORG_ID).
|
||||
expect(() => handleOrgListItems()).not.toThrow();
|
||||
|
||||
// Clear (simulates lock handler calling clearOrgCache).
|
||||
clearOrgCache();
|
||||
|
||||
// Context is still ORG_ID but cache is empty → requireCurrentOrgState throws.
|
||||
expect(session.currentContext()).toBe(ORG_ID);
|
||||
expect(() => handleOrgListItems()).toThrow('not_in_org_context');
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import { encodeBinary, decodeBinary } from '../shared/message-binary';
|
||||
import * as vault from './vault';
|
||||
import { clearCurrent } from './session';
|
||||
import * as sessionTimer from './session-timer';
|
||||
import { clearOrgCache } from './router/org-handlers';
|
||||
import { READ_ONLY_CONTENT_CALLABLE } from './session-timer';
|
||||
|
||||
// @ts-ignore TS2307 — resolved by webpack alias / copy
|
||||
@@ -52,6 +53,7 @@ const state: RouterState = {
|
||||
sessionTimer.onExpired(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[relicario sw] session expired — locking vault');
|
||||
clearOrgCache(); // prevent use-after-free: clear org cache BEFORE freeing handles
|
||||
clearCurrent();
|
||||
state.manifest = null;
|
||||
// Plan C Phase 5: don't leak the cached git-host client across a lock.
|
||||
|
||||
@@ -38,7 +38,7 @@ export interface OrgHandleState {
|
||||
|
||||
/// Detect a network-level failure (fetch() rejects with TypeError; no connectivity).
|
||||
/// Does NOT classify HTTP non-2xx errors (those are application-level, not network).
|
||||
function isNetworkError(e: unknown): boolean {
|
||||
export function isNetworkError(e: unknown): boolean {
|
||||
// fetch() rejects with TypeError when the request cannot be made at all
|
||||
// (DNS failure, no connectivity, CORS preflight failure, etc.).
|
||||
if (e instanceof TypeError) return true;
|
||||
|
||||
@@ -1,11 +1,85 @@
|
||||
/// Org message handler arms — kept separate so popup-only.ts doesn't bloat.
|
||||
///
|
||||
/// Security invariant: handlers here must NEVER return full OrgConfig objects
|
||||
/// to the popup. Always project to OrgConfigSummary (id + displayName only).
|
||||
/// Security invariants:
|
||||
/// - Handlers here must NEVER return full OrgConfig objects to the popup.
|
||||
/// Always project to OrgConfigSummary (id + displayName only).
|
||||
/// - clearOrgCache() MUST be called on every lock / inactivity-timeout path
|
||||
/// (alongside session.clearAll). The orgStates Map holds OrgHandleState
|
||||
/// objects that reference SessionHandles; session.clearAll() frees those
|
||||
/// handles in WASM. Reusing a cached state after a lock would be a
|
||||
/// use-after-free of a freed WASM handle.
|
||||
|
||||
import { openOrg, listOrgItems, getOrgItem, listOrgCollections, isNetworkError } from '../org-vault';
|
||||
import type { OrgHandleState } from '../org-vault';
|
||||
import { loadOrgConfigs } from '../org-config';
|
||||
import * as session from '../session';
|
||||
import type { OrgConfigSummary } from '../../shared/types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type WasmModule = any;
|
||||
|
||||
// Cached open-org state per orgId (host + grants + filtered manifest). The
|
||||
// SessionHandle is also held by session.setOrg; this Map holds the rest.
|
||||
const orgStates = new Map<string, OrgHandleState>();
|
||||
|
||||
// MUST be called on lock / inactivity-timeout (alongside session.clearCurrent/clearAll):
|
||||
// the cached states reference SessionHandles that clearAll() frees — reusing one
|
||||
// after a lock would be a use-after-free. Clearing the cache prevents that.
|
||||
export function clearOrgCache(): void { orgStates.clear(); }
|
||||
|
||||
function requireCurrentOrgState(): OrgHandleState {
|
||||
const ctx = session.currentContext();
|
||||
const state = ctx === 'personal' ? undefined : orgStates.get(ctx);
|
||||
if (!state) throw new Error('not_in_org_context');
|
||||
return state;
|
||||
}
|
||||
|
||||
/** Handle org_switch: open the org vault and cache its state, or restore personal context. */
|
||||
export async function handleOrgSwitch(
|
||||
{ context }: { context: string },
|
||||
w: WasmModule,
|
||||
): Promise<{ ok: true; data: { context: string; offline: boolean } } | { ok: false; error: string }> {
|
||||
if (context === 'personal') {
|
||||
session.setContext('personal');
|
||||
return { ok: true, data: { context: 'personal', offline: false } };
|
||||
}
|
||||
|
||||
const cfgs = await loadOrgConfigs();
|
||||
const cfg = cfgs.find((c) => c.orgId === context);
|
||||
if (!cfg) return { ok: false, error: 'org_not_configured' };
|
||||
|
||||
const prev = orgStates.get(context);
|
||||
try {
|
||||
const state = await openOrg(cfg, w, prev?.manifest);
|
||||
orgStates.set(context, state);
|
||||
session.setOrg(context, state.handle); // frees a displaced prior handle (Task 3)
|
||||
session.setContext(context);
|
||||
return { ok: true, data: { context, offline: state.offline } };
|
||||
} catch (e) {
|
||||
// Full-outage fallback: a network error with a prior cached state → reuse it read-only.
|
||||
if (prev && isNetworkError(e)) {
|
||||
session.setContext(context);
|
||||
return { ok: true, data: { context, offline: true } };
|
||||
}
|
||||
return { ok: false, error: e instanceof Error ? e.message : 'org_switch_failed' };
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle org_list_items: returns cached, grant-filtered OrgManifestEntry[]. */
|
||||
export function handleOrgListItems() {
|
||||
return { ok: true as const, data: listOrgItems(requireCurrentOrgState()) };
|
||||
}
|
||||
|
||||
/** Handle org_get_item: fetch and decrypt a single org item. */
|
||||
export async function handleOrgGetItem({ id }: { id: string }, w: WasmModule) {
|
||||
return { ok: true as const, data: await getOrgItem(requireCurrentOrgState(), id, w) };
|
||||
}
|
||||
|
||||
/** Handle org_list_collections: returns granted Collection[] only. */
|
||||
export async function handleOrgListCollections() {
|
||||
return { ok: true as const, data: await listOrgCollections(requireCurrentOrgState()) };
|
||||
}
|
||||
|
||||
/** Handle org_list_configs: returns only { orgId, displayName } per org. */
|
||||
export async function handleOrgListConfigs(): Promise<{ ok: true; data: OrgConfigSummary[] }> {
|
||||
const cfgs = await loadOrgConfigs();
|
||||
|
||||
@@ -14,7 +14,14 @@ import * as session from '../session';
|
||||
import * as devices from '../devices';
|
||||
import * as sessionTimer from '../session-timer';
|
||||
import { loadDeviceSettings, saveDeviceSettings, loadBlacklist, saveBlacklist } from '../storage';
|
||||
import { handleOrgListConfigs } from './org-handlers';
|
||||
import {
|
||||
handleOrgListConfigs,
|
||||
handleOrgSwitch,
|
||||
handleOrgListItems,
|
||||
handleOrgGetItem,
|
||||
handleOrgListCollections,
|
||||
clearOrgCache,
|
||||
} from './org-handlers';
|
||||
|
||||
// --- Shared ambient state owned by the SW module ---
|
||||
//
|
||||
@@ -73,6 +80,7 @@ export async function handle(
|
||||
}
|
||||
|
||||
case 'lock':
|
||||
clearOrgCache(); // prevent use-after-free: clear org cache BEFORE freeing handles
|
||||
session.clearCurrent();
|
||||
state.manifest = null;
|
||||
// Don't leak the cached git-host (incl. lastSyncAt) across a lock —
|
||||
@@ -663,6 +671,18 @@ export async function handle(
|
||||
case 'org_list_configs':
|
||||
return handleOrgListConfigs();
|
||||
|
||||
case 'org_switch':
|
||||
return handleOrgSwitch(msg, state.wasm);
|
||||
|
||||
case 'org_list_items':
|
||||
return handleOrgListItems();
|
||||
|
||||
case 'org_get_item':
|
||||
return handleOrgGetItem(msg, state.wasm);
|
||||
|
||||
case 'org_list_collections':
|
||||
return handleOrgListCollections();
|
||||
|
||||
default:
|
||||
return { ok: false, error: `unhandled popup message: ${(msg as { type: string }).type}` };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
Item, ItemId, Manifest, ManifestEntry, VaultConfig, SetupState,
|
||||
DeviceSettings, GeneratorRequest, VaultSettings, AttachmentRef, Device,
|
||||
FieldHistoryView, OrgConfigSummary,
|
||||
FieldHistoryView, OrgConfigSummary, OrgManifestEntry, Collection,
|
||||
} from './types';
|
||||
|
||||
// --- Session timeout config ---
|
||||
@@ -69,7 +69,11 @@ export type PopupMessage =
|
||||
| { type: 'attach_vault'; config: VaultConfig; passphrase: string;
|
||||
referenceImageBytes: ArrayBuffer; deviceName: string }
|
||||
| { type: 'get_vault_status' }
|
||||
| { type: 'org_list_configs' };
|
||||
| { type: 'org_list_configs' }
|
||||
| { type: 'org_switch'; context: string }
|
||||
| { type: 'org_list_items' }
|
||||
| { type: 'org_get_item'; id: string }
|
||||
| { type: 'org_list_collections' };
|
||||
|
||||
// --- Messages a content script may send ---
|
||||
|
||||
@@ -184,6 +188,7 @@ export const POPUP_ONLY_TYPES: ReadonlySet<PopupMessage['type']> = new Set([
|
||||
'generate_recovery_qr', 'unwrap_recovery_qr',
|
||||
'create_vault', 'attach_vault', 'get_vault_status',
|
||||
'org_list_configs',
|
||||
'org_switch', 'org_list_items', 'org_get_item', 'org_list_collections',
|
||||
] as PopupMessage['type'][]);
|
||||
|
||||
export interface ExportBackupResponse extends Extract<Response, { ok: true }> {
|
||||
@@ -227,6 +232,22 @@ export interface OrgListConfigsResponse extends Extract<Response, { ok: true }>
|
||||
data: OrgConfigSummary[];
|
||||
}
|
||||
|
||||
export interface OrgSwitchResponse extends Extract<Response, { ok: true }> {
|
||||
data: { context: string; offline: boolean };
|
||||
}
|
||||
|
||||
export interface OrgListItemsResponse extends Extract<Response, { ok: true }> {
|
||||
data: OrgManifestEntry[];
|
||||
}
|
||||
|
||||
export interface OrgGetItemResponse extends Extract<Response, { ok: true }> {
|
||||
data: Item;
|
||||
}
|
||||
|
||||
export interface OrgListCollectionsResponse extends Extract<Response, { ok: true }> {
|
||||
data: Collection[];
|
||||
}
|
||||
|
||||
export const CONTENT_CALLABLE_TYPES: ReadonlySet<ContentMessage['type']> = new Set([
|
||||
'get_autofill_candidates', 'get_credentials', 'check_credential', 'blacklist_site',
|
||||
'capture_save_login',
|
||||
|
||||
Reference in New Issue
Block a user