Files
zeekay 7f26cb96cd fix(clio): correct OAuth refresh + Custom Action nonce to real Clio v4 contract
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.
2026-07-03 17:21:01 -07:00

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();
});
});