Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/__tests__/starter-kit-alignment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
14 changes: 14 additions & 0 deletions src/__tests__/webhook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
});
Expand All @@ -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
Expand Down
32 changes: 32 additions & 0 deletions src/commands/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { runChannelToken } from './token.js';
import { runChannelHealth } from './health.js';
import {
runChannelWebhookShow,
runChannelWebhookHmacShow,
runChannelWebhookSet,
runChannelWebhookClear,
type WebhookSetOptions,
Expand Down Expand Up @@ -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>', 'Channel ID (ch_xxxxxxxx), phone number, or @<username>')
.action(async (channelRef: string) => {
await runChannelWebhookHmacShow(channelRef, { json: !!program.opts().json });
});

addExamples(
channels,
`
Expand Down Expand Up @@ -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
`,
);

Expand Down Expand Up @@ -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
`,
);
}
15 changes: 15 additions & 0 deletions src/commands/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ export async function runChannelWebhookShow(
output(data, { json: !!opts.json, kind: 'read' });
}

/**
* Canonical handler for `hookmyapp channels webhook hmac show <channel>`.
* 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<void> {
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 <channel>`.
*/
Expand Down
Loading