diff --git a/src/__tests__/starter-kit-alignment.test.ts b/src/__tests__/starter-kit-alignment.test.ts index 0af4c5a..7c52336 100644 --- a/src/__tests__/starter-kit-alignment.test.ts +++ b/src/__tests__/starter-kit-alignment.test.ts @@ -5,6 +5,7 @@ const STARTER_KIT_ENV_URL = const CLI_CANONICAL_KEYS = [ 'PORT', + 'VERIFY_TOKEN', 'WEBHOOK_HMAC_SECRET', 'WHATSAPP_ACCESS_TOKEN', 'WHATSAPP_API_URL', diff --git a/src/__tests__/webhook.test.ts b/src/__tests__/webhook.test.ts index d666c23..497642f 100644 --- a/src/__tests__/webhook.test.ts +++ b/src/__tests__/webhook.test.ts @@ -60,6 +60,7 @@ vi.stubGlobal('fetch', mockFetch); describe('webhook commands', () => { let runChannelWebhookShow: typeof import('../commands/webhook.js').runChannelWebhookShow; + let runChannelWebhookHmacShow: typeof import('../commands/webhook.js').runChannelWebhookHmacShow; let runChannelWebhookSet: typeof import('../commands/webhook.js').runChannelWebhookSet; let runChannelWebhookClear: typeof import('../commands/webhook.js').runChannelWebhookClear; @@ -72,6 +73,7 @@ describe('webhook commands', () => { const mod = await import('../commands/webhook.js'); runChannelWebhookShow = mod.runChannelWebhookShow; + runChannelWebhookHmacShow = mod.runChannelWebhookHmacShow; runChannelWebhookSet = mod.runChannelWebhookSet; runChannelWebhookClear = mod.runChannelWebhookClear; }); @@ -91,6 +93,18 @@ describe('webhook commands', () => { expect(mockedOutput).toHaveBeenCalledWith(config, expect.objectContaining({})); }); + it('hmac show calls apiClient /webhook-config/:channelId/hmac — a surface separate from webhook show', async () => { + const secret = { channelId: 'ch_TEST0001', hmacSecret: 'hmac-secret-1' }; + mockedApiClient + .mockResolvedValueOnce(fakeChannels) + .mockResolvedValueOnce(secret); + + await runChannelWebhookHmacShow('ch_TEST0001'); + + expect(mockedApiClient).toHaveBeenCalledWith('/webhook-config/ch_TEST0001/hmac'); + expect(mockedOutput).toHaveBeenCalledWith(secret, expect.objectContaining({ kind: 'read' })); + }); + it('setWebhook calls apiClient to configure webhook for channel', async () => { // resolveChannel -> /meta/channels mockedApiClient diff --git a/src/commands/channels.ts b/src/commands/channels.ts index 0d6e69c..bcaef33 100644 --- a/src/commands/channels.ts +++ b/src/commands/channels.ts @@ -16,6 +16,7 @@ import { runChannelToken } from './token.js'; import { runChannelHealth } from './health.js'; import { runChannelWebhookShow, + runChannelWebhookHmacShow, runChannelWebhookSet, runChannelWebhookClear, type WebhookSetOptions, @@ -616,6 +617,18 @@ export function registerChannelsCommand(program: Command): void { await runChannelWebhookClear(channelRef, { json: !!program.opts().json }); }); + const channelsWebhookHmac = channelsWebhook + .command('hmac') + .description('Manage the HMAC signing secret for delivered payloads (X-HookMyApp-Signature-256)'); + + const channelsWebhookHmacShow = channelsWebhookHmac + .command('show') + .description('Show the HMAC signing secret for a channel (distinct from the verify token)') + .argument('', 'Channel ID (ch_xxxxxxxx), phone number, or @') + .action(async (channelRef: string) => { + await runChannelWebhookHmacShow(channelRef, { json: !!program.opts().json }); + }); + addExamples( channels, ` @@ -738,6 +751,7 @@ EXAMPLES: $ hookmyapp channels webhook show ch_AAAAAAAA $ hookmyapp channels webhook set ch_AAAAAAAA --url https://example.com/hook $ hookmyapp channels webhook clear ch_AAAAAAAA + $ hookmyapp channels webhook hmac show ch_AAAAAAAA `, ); @@ -765,6 +779,24 @@ EXAMPLES: EXAMPLES: $ hookmyapp channels webhook clear ch_AAAAAAAA $ hookmyapp channels webhook clear ch_AAAAAAAA --json +`, + ); + + addExamples( + channelsWebhookHmac, + ` +EXAMPLES: + $ hookmyapp channels webhook hmac show ch_AAAAAAAA + $ hookmyapp channels webhook hmac show ch_AAAAAAAA --json +`, + ); + + addExamples( + channelsWebhookHmacShow, + ` +EXAMPLES: + $ hookmyapp channels webhook hmac show ch_AAAAAAAA + $ hookmyapp channels webhook hmac show ch_AAAAAAAA --json `, ); } diff --git a/src/commands/webhook.ts b/src/commands/webhook.ts index 138bc37..20ce51d 100644 --- a/src/commands/webhook.ts +++ b/src/commands/webhook.ts @@ -20,6 +20,21 @@ export async function runChannelWebhookShow( output(data, { json: !!opts.json, kind: 'read' }); } +/** + * Canonical handler for `hookmyapp channels webhook hmac show `. + * The HMAC signing secret (X-HookMyApp-Signature-256) is a DISTINCT secret + * from the verify token, so it has its own surface — never bundled into + * `webhook show` (AIT-183). + */ +export async function runChannelWebhookHmacShow( + channelRef: string, + opts: { json?: boolean } = {}, +): Promise { + const channel = await resolveChannel(channelRef); + const data = await apiClient(`/webhook-config/${channel.id}/hmac`); + output(data, { json: !!opts.json, kind: 'read' }); +} + /** * Canonical handler for `hookmyapp channels webhook set `. */