@andypai/agent-kanban 0.3.5 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andypai/agent-kanban",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Agent-friendly kanban board CLI. Manage tasks via bash commands, parse structured JSON output.",
5
5
  "homepage": "https://github.com/abpai/agent-kanban#readme",
6
6
  "repository": {
@@ -1,7 +1,7 @@
1
1
  import { beforeEach, afterEach, describe, expect, test } from 'bun:test'
2
2
  import { Database } from 'bun:sqlite'
3
3
  import { createHmac } from 'node:crypto'
4
- import { verifyHmacSha256 } from '../webhooks'
4
+ import { verifyHmacSha256, verifySha256HmacSignatureHeader } from '../webhooks'
5
5
  import { JiraProvider, type JiraProviderConfig } from '../providers/jira'
6
6
  import { JiraClient } from '../providers/jira-client'
7
7
  import { LinearProvider } from '../providers/linear'
@@ -57,6 +57,20 @@ describe('verifyHmacSha256', () => {
57
57
  })
58
58
  })
59
59
 
60
+ describe('verifySha256HmacSignatureHeader', () => {
61
+ test('accepts Jira WebSub-style sha256 signatures', () => {
62
+ const body = '{"hello":"world"}'
63
+ const sig = hmac('s3cr3t', body)
64
+ expect(verifySha256HmacSignatureHeader('s3cr3t', body, `sha256=${sig}`)).toBe(true)
65
+ })
66
+
67
+ test('rejects missing method prefix', () => {
68
+ const body = '{"hello":"world"}'
69
+ const sig = hmac('s3cr3t', body)
70
+ expect(verifySha256HmacSignatureHeader('s3cr3t', body, sig)).toBe(false)
71
+ })
72
+ })
73
+
60
74
  const jiraConfig: JiraProviderConfig = {
61
75
  baseUrl: 'https://example.atlassian.net',
62
76
  email: 'u@example.com',
@@ -181,7 +195,7 @@ describe('Jira webhook', () => {
181
195
  issue: { id: '300', key: 'ENG-300', fields: {} },
182
196
  })
183
197
  const result = await provider.handleWebhook({
184
- headers: { 'x-hub-signature-256': 'sha256=deadbeef' },
198
+ headers: { 'x-hub-signature': 'sha256=deadbeef' },
185
199
  rawBody: body,
186
200
  })
187
201
  expect(result.unauthorized).toBe(true)
@@ -214,12 +228,34 @@ describe('Jira webhook', () => {
214
228
  })
215
229
  const sig = hmac('topsecret', body)
216
230
  const result = await provider.handleWebhook({
217
- headers: { 'x-hub-signature-256': sig },
231
+ headers: { 'x-hub-signature': `sha256=${sig}` },
218
232
  rawBody: body,
219
233
  })
220
234
  expect(result.handled).toBe(true)
221
235
  })
222
236
 
237
+ test('rejects the old custom x-hub-signature-256 header when secret is configured', async () => {
238
+ const db = new Database(':memory:')
239
+ seedJira(db)
240
+ process.env['JIRA_WEBHOOK_SECRET'] = 'topsecret'
241
+ const client = new JiraClient({
242
+ baseUrl: jiraConfig.baseUrl,
243
+ email: jiraConfig.email,
244
+ apiToken: jiraConfig.apiToken,
245
+ })
246
+ const provider = new JiraProvider(db, jiraConfig, client)
247
+ const body = JSON.stringify({
248
+ webhookEvent: 'jira:issue_created',
249
+ issue: { id: '401', key: 'ENG-401', fields: {} },
250
+ })
251
+ const sig = hmac('topsecret', body)
252
+ const result = await provider.handleWebhook({
253
+ headers: { 'x-hub-signature-256': `sha256=${sig}` },
254
+ rawBody: body,
255
+ })
256
+ expect(result.unauthorized).toBe(true)
257
+ })
258
+
223
259
  test('ignores issue updates from other projects', async () => {
224
260
  const db = new Database(':memory:')
225
261
  seedJira(db)
@@ -11,7 +11,12 @@ import type {
11
11
  TaskComment,
12
12
  Task,
13
13
  } from '../types'
14
- import { headerLower, verifyHmacSha256, type WebhookRequest, type WebhookResult } from '../webhooks'
14
+ import {
15
+ headerLower,
16
+ verifySha256HmacSignatureHeader,
17
+ type WebhookRequest,
18
+ type WebhookResult,
19
+ } from '../webhooks'
15
20
  import { adfToPlainText, plainTextToAdf, type AdfDocument } from './jira-adf'
16
21
  import { JIRA_CAPABILITIES } from './capabilities'
17
22
  import { providerUpstreamError, unsupportedOperation } from './errors'
@@ -712,8 +717,8 @@ export class JiraProvider implements KanbanProvider {
712
717
  async handleWebhook(payload: WebhookRequest): Promise<WebhookResult> {
713
718
  const secret = process.env['JIRA_WEBHOOK_SECRET']
714
719
  if (secret) {
715
- const sig = headerLower(payload.headers, 'x-hub-signature-256')
716
- if (!verifyHmacSha256(secret, payload.rawBody, sig)) {
720
+ const sig = headerLower(payload.headers, 'x-hub-signature')
721
+ if (!verifySha256HmacSignatureHeader(secret, payload.rawBody, sig)) {
717
722
  return { handled: false, unauthorized: true, message: 'Invalid signature' }
718
723
  }
719
724
  }
@@ -28,7 +28,12 @@ import type {
28
28
  UpdateTaskInput,
29
29
  } from './types'
30
30
  import { DEFAULT_POLLING_SYNC_INTERVAL_MS } from '../sync-config'
31
- import { headerLower, verifyHmacSha256, type WebhookRequest, type WebhookResult } from '../webhooks'
31
+ import {
32
+ headerLower,
33
+ verifySha256HmacSignatureHeader,
34
+ type WebhookRequest,
35
+ type WebhookResult,
36
+ } from '../webhooks'
32
37
 
33
38
  const FULL_RECONCILE_INTERVAL_MS = 5 * 60_000
34
39
 
@@ -1124,8 +1129,8 @@ export class PostgresJiraProvider implements KanbanProvider {
1124
1129
  async handleWebhook(payload: WebhookRequest): Promise<WebhookResult> {
1125
1130
  const secret = process.env['JIRA_WEBHOOK_SECRET']
1126
1131
  if (secret) {
1127
- const sig = headerLower(payload.headers, 'x-hub-signature-256')
1128
- if (!verifyHmacSha256(secret, payload.rawBody, sig)) {
1132
+ const sig = headerLower(payload.headers, 'x-hub-signature')
1133
+ if (!verifySha256HmacSignatureHeader(secret, payload.rawBody, sig)) {
1129
1134
  return { handled: false, unauthorized: true, message: 'Invalid signature' }
1130
1135
  }
1131
1136
  }
package/src/webhooks.ts CHANGED
@@ -27,6 +27,20 @@ export function verifyHmacSha256(
27
27
  return timingSafeEqual(macBuf, expBuf)
28
28
  }
29
29
 
30
+ export function verifySha256HmacSignatureHeader(
31
+ secret: string,
32
+ rawBody: string,
33
+ providedSignature: string | undefined | null,
34
+ ): boolean {
35
+ if (!providedSignature) return false
36
+ const eq = providedSignature.indexOf('=')
37
+ if (eq === -1) return false
38
+ const method = providedSignature.slice(0, eq).toLowerCase()
39
+ const signature = providedSignature.slice(eq + 1)
40
+ if (method !== 'sha256') return false
41
+ return verifyHmacSha256(secret, rawBody, signature)
42
+ }
43
+
30
44
  export function headerLower(headers: Record<string, string>, name: string): string | undefined {
31
45
  const target = name.toLowerCase()
32
46
  for (const [k, v] of Object.entries(headers)) {