Files
Fabrizio Rinaldi 2b48317e44 fix(typefully): improve invalid API key UX (#27)
## TL;DR

Broken or expired Typefully API keys now produce a clear authentication
failure instead of a generic `HTTP 401` or a softened setup warning. The
skill instructions also tell agents to stop and ask the user to refresh
setup, rather than falling back to browser, web UI, or localhost access.

## Root Cause

The CLI only had hard-stop handling for missing API keys. Invalid keys
came back as generic 401 API errors, and setup/default-social-set paths
wrapped those errors as softer fetch/access failures, leaving agents
room to improvise another access path.

## Summary

- Fix **401 auth handling** by adding a shared authentication failure
message and details for Typefully API 401 responses.
- Fix **setup and default social set flows** so invalid keys also
hard-stop with setup guidance instead of being wrapped as generic
fetch/access failures.
- Update **agent-facing docs and changelog** to cover missing, invalid,
and expired keys, and explicitly forbid web UI/browser/localhost
fallback for draft access.
- Clarify **X Article Markdown review guidance** so agents do not flag
Typefully-normalized bold/link Markdown as broken when the rendered
article is correct.
- Clean up **dated changelog sections** so shipped Typefully skill
entries use plain date headings instead of `Unreleased` or bracketed
dates.
- Add **regression coverage** for direct API calls, setup default
validation, setup social-set discovery, and default social set
configuration.

## Validation

- [x] `git diff --check`
- [x] `npm run test:common`
- [x] `npm run test:config`
- [x] `npm test`

## After Merge

- Users with an installed copy of the Typefully skill should update it
through their normal skills.sh / Claude plugin flow to receive the new
behavior.
2026-07-08 14:45:07 +01:00

112 lines
3.8 KiB
JavaScript

const {
describe,
it,
assert,
withCliHarness,
authAssertFactory,
expectCliOk,
expectCliError,
parseJsonOrNull,
} = require('./typefully-cli.test-helpers');
describe('argument parsing', () => {
it('errors cleanly when a value-taking alias is missing its value', withCliHarness(async ({ server, run }) => {
const result = await run(['create-draft', 'hi', '--social-set-id', '123', '--tags', '--share']);
expectCliError(result, { error: '--tags requires a value' });
assert.equal(server.requests.length, 0);
}));
it('errors cleanly when --social-set-id is missing a value', withCliHarness(async ({ server, run }) => {
const result = await run(['drafts:list', '--social-set-id']);
expectCliError(result, { error: '--social-set-id (or --social_set_id) requires a value' });
assert.equal(server.requests.length, 0);
}));
it('errors when positional social_set_id conflicts with --social-set-id', withCliHarness(async ({ server, run }) => {
const result = await run(['drafts:list', '111', '--social-set-id', '222']);
expectCliError(result, {
error: 'Conflicting social_set_id values',
positional: '111',
flag: '222',
});
assert.equal(server.requests.length, 0);
}));
});
describe('help', () => {
it('prints usage for help command', withCliHarness(async ({ run }) => {
const result = await run(['help'], { env: { TYPEFULLY_API_KEY: '' } });
expectCliOk(result);
assert.ok(result.stdout.includes('Typefully CLI - Manage social media posts'));
assert.ok(result.stdout.includes('USAGE:'));
}));
it('supports --help and -h aliases', withCliHarness(async ({ run }) => {
const longResult = await run(['--help']);
expectCliOk(longResult);
assert.ok(longResult.stdout.includes('Typefully CLI'));
const shortResult = await run(['-h']);
expectCliOk(shortResult);
assert.ok(shortResult.stdout.includes('Typefully CLI'));
}));
});
describe('global flag behavior', () => {
it('supports --api-base-url and appends /v2 when omitted', withCliHarness(async ({ server, apiKey, baseUrl, run }) => {
const rootUrl = baseUrl.replace(/\/v2$/, '');
server.expect('GET', '/v2/social-sets/9/tags', {
assert: (req) => {
authAssertFactory(apiKey)(req);
assert.equal(req.search, '?limit=50');
},
json: { results: [] },
});
const result = await run(['--api-base-url', rootUrl, 'tags:list', '--social-set-id', '9'], {
env: { TYPEFULLY_API_BASE: 'https://example.invalid/v2' },
});
expectCliOk(result, { results: [] });
}));
it('allows tags:list with --social-set-id and no positional social set', withCliHarness(async ({ server, apiKey, run }) => {
server.expect('GET', '/v2/social-sets/9/tags', {
assert: (req) => {
authAssertFactory(apiKey)(req);
assert.equal(req.search, '?limit=50');
},
json: { results: [] },
});
const result = await run(['tags:list', '--social-set-id', '9']);
expectCliOk(result, { results: [] });
}));
});
describe('api errors', () => {
it('explains 401 responses as authentication failures', withCliHarness(async ({ server, apiKey, run }) => {
server.expect('GET', '/v2/me', {
assert: authAssertFactory(apiKey),
status: 401,
json: { error: 'Invalid token' },
});
const result = await run(['me:get']);
expectCliError(result);
const out = parseJsonOrNull(result.stdout);
assert.equal(
out.error,
`Authentication failed: Typefully API key is invalid, expired, or lacks access. Run 'typefully.js setup' to configure a valid key.`,
);
assert.equal(out.action, 'Run: typefully.js setup');
assert.equal(out.api_key_url, 'https://typefully.com/?settings=api');
assert.deepEqual(out.response, { error: 'Invalid token' });
}));
});