mirror of
https://github.com/hanzoai/extension.git
synced 2026-10-31 21:57:06 +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.
69 lines
2.5 KiB
TypeScript
69 lines
2.5 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { parseSubjectUrl, parseCustomAction } from '../src/custom-action';
|
|
|
|
describe('parseSubjectUrl', () => {
|
|
it('extracts the type + numeric id from a Clio matter URL', () => {
|
|
expect(parseSubjectUrl('https://app.clio.com/api/v4/matters/1234.json')).toEqual({
|
|
subjectType: 'matters',
|
|
recordId: 1234,
|
|
});
|
|
});
|
|
it('works without the .json suffix and for other record types', () => {
|
|
expect(parseSubjectUrl('https://app.clio.com/api/v4/contacts/9')).toEqual({
|
|
subjectType: 'contacts',
|
|
recordId: 9,
|
|
});
|
|
expect(parseSubjectUrl('https://eu.app.clio.com/api/v4/documents/55.json')).toEqual({
|
|
subjectType: 'documents',
|
|
recordId: 55,
|
|
});
|
|
});
|
|
it('tolerates a bare path', () => {
|
|
expect(parseSubjectUrl('/api/v4/matters/77')).toEqual({ subjectType: 'matters', recordId: 77 });
|
|
});
|
|
it('decodes a URL-encoded subject_url (how Clio sends it)', () => {
|
|
expect(parseSubjectUrl(encodeURIComponent('/api/v4/matters/1234'))).toEqual({
|
|
subjectType: 'matters',
|
|
recordId: 1234,
|
|
});
|
|
});
|
|
it('returns empty for a non-matching URL or empty input', () => {
|
|
expect(parseSubjectUrl('https://app.clio.com/nope')).toEqual({});
|
|
expect(parseSubjectUrl('')).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe('parseCustomAction', () => {
|
|
it('parses the four Clio deep-link params and resolves matter_id', () => {
|
|
const params = new URLSearchParams({
|
|
custom_action_id: '7',
|
|
user_id: '42',
|
|
subject_url: encodeURIComponent('/api/v4/matters/321'),
|
|
custom_action_nonce: 'abc123',
|
|
});
|
|
const ctx = parseCustomAction(params);
|
|
expect(ctx.customActionId).toBe('7');
|
|
expect(ctx.userId).toBe('42');
|
|
expect(ctx.nonce).toBe('abc123');
|
|
expect(ctx.subjectType).toBe('matters');
|
|
expect(ctx.matterId).toBe(321);
|
|
});
|
|
|
|
it('leaves matterId undefined when the action fired on a non-matter record', () => {
|
|
const ctx = parseCustomAction(
|
|
new URLSearchParams({ subject_url: '/api/v4/contacts/9', user_id: '1', custom_action_nonce: 'n' }),
|
|
);
|
|
expect(ctx.subjectType).toBe('contacts');
|
|
expect(ctx.matterId).toBeUndefined();
|
|
});
|
|
|
|
it('accepts a plain record and fills missing params with empty strings', () => {
|
|
const ctx = parseCustomAction({ user_id: '5' });
|
|
expect(ctx.userId).toBe('5');
|
|
expect(ctx.subjectUrl).toBe('');
|
|
expect(ctx.nonce).toBe('');
|
|
expect(ctx.customActionId).toBe('');
|
|
expect(ctx.matterId).toBeUndefined();
|
|
});
|
|
});
|