mirror of
https://github.com/hanzoai/extension.git
synced 2026-11-01 18:02:17 +00:00
Post-review corrections verified against Clio's own developer docs and the official clio/example-third-party-application, fixing three ways the first cut would have failed against real Clio: 1. OAuth: removed access_type=offline (a Google-ism Clio ignores — it would have yielded NO refresh_token). Clio returns a refresh_token on the authorization_code grant by default; added the real redirect_on_decline option instead. Wired the refresh path into the SPA (ensureClioToken → /oauth/refresh) so a long matter session survives token expiry — the refresh apparatus is no longer dead code. 2. Custom Action nonce: the nonce is NOT a locally-verifiable HMAC. It is a single-use, 60s code that the app must REPLAY to the Clio API, which validates server-side (403 on bad/stale). Removed the wrong local HMAC verify (and node:crypto from the service); /custom-action now parses the deep-link and redirects into the SPA carrying matter_id + the nonce, and getMatter replays it (custom_action_nonce query param) on the first authenticated call. Corrected the deep-link params to Clio's actual set (custom_action_id/user_id/subject_url/custom_action_nonce; subject_url is URL-encoded) — subject_id was never sent. 3. Documents: gate downloads on content_type (isTextExtractable) — PDF/DOCX (the bulk of legal docs) would have been fed to the model as mojibake, wasting the context budget. Binary docs now contribute name + type only. The MIME check matches the subtype so DOCX (whose vendor type contains the substring "xml") is correctly binary. tsc clean, 89 vitest tests pass, esbuild build succeeds; re-verified the browser bundle has zero secret/crypto refs.
59 lines
2.3 KiB
TypeScript
59 lines
2.3 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { readEnv, customActionRedirect } from '../src/server';
|
|
|
|
describe('readEnv', () => {
|
|
const good = {
|
|
CLIO_CLIENT_ID: 'CID',
|
|
CLIO_CLIENT_SECRET: 'SECRET',
|
|
CLIO_REDIRECT_URI: 'https://clio.hanzo.ai/oauth/callback',
|
|
};
|
|
|
|
it('reads a complete environment with region + port defaults', () => {
|
|
const env = readEnv(good as any);
|
|
expect(env.clientId).toBe('CID');
|
|
expect(env.clientSecret).toBe('SECRET');
|
|
expect(env.redirectUri).toBe('https://clio.hanzo.ai/oauth/callback');
|
|
expect(env.region).toBe('us');
|
|
expect(env.port).toBe(8787);
|
|
});
|
|
|
|
it('honors CLIO_REGION and PORT overrides', () => {
|
|
const env = readEnv({ ...good, CLIO_REGION: 'eu', PORT: '9000' } as any);
|
|
expect(env.region).toBe('eu');
|
|
expect(env.port).toBe(9000);
|
|
});
|
|
|
|
it('throws fast when a required secret is missing (no silent default)', () => {
|
|
expect(() => readEnv({ CLIO_CLIENT_ID: 'x' } as any)).toThrow(/required/);
|
|
expect(() => readEnv({ CLIO_CLIENT_ID: 'x', CLIO_CLIENT_SECRET: 'y' } as any)).toThrow(/required/);
|
|
});
|
|
});
|
|
|
|
describe('customActionRedirect', () => {
|
|
it('redirects a matter deep-link to the SPA carrying matter_id + nonce', () => {
|
|
const search = new URLSearchParams({
|
|
custom_action_id: '7',
|
|
user_id: '42',
|
|
subject_url: encodeURIComponent('/api/v4/matters/321'),
|
|
custom_action_nonce: 'NONCE1',
|
|
});
|
|
const dest = customActionRedirect(search);
|
|
const u = new URL(dest, 'https://clio.hanzo.ai');
|
|
expect(u.pathname).toBe('/index.html');
|
|
expect(u.searchParams.get('matter_id')).toBe('321');
|
|
expect(u.searchParams.get('custom_action_nonce')).toBe('NONCE1');
|
|
});
|
|
|
|
it('omits the nonce param when Clio sent none', () => {
|
|
const search = new URLSearchParams({ subject_url: '/api/v4/matters/9' });
|
|
const u = new URL(customActionRedirect(search), 'https://clio.hanzo.ai');
|
|
expect(u.searchParams.get('matter_id')).toBe('9');
|
|
expect(u.searchParams.has('custom_action_nonce')).toBe(false);
|
|
});
|
|
|
|
it('returns empty for a non-matter deep-link (nothing to open)', () => {
|
|
expect(customActionRedirect(new URLSearchParams({ subject_url: '/api/v4/contacts/9' }))).toBe('');
|
|
expect(customActionRedirect(new URLSearchParams())).toBe('');
|
|
});
|
|
});
|