@andypai/agent-kanban 0.3.5 → 0.3.7

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.7",
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": {
@@ -181,6 +181,13 @@ describe('plainTextToAdf / adfToPlainText', () => {
181
181
  expect(adfToPlainText(doc)).toBe('before\n\nafter')
182
182
  })
183
183
 
184
+ test('Jira webhook plain string descriptions pass through unchanged', () => {
185
+ const description =
186
+ '*Repo:* https://github.com/abpai/smoke-test\n\nPlease make one minimal change.'
187
+
188
+ expect(adfToPlainText(description)).toBe(description)
189
+ })
190
+
184
191
  test('strong + link marks survive on read as markdown', () => {
185
192
  const doc: AdfDocument = {
186
193
  version: 1,
@@ -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',
@@ -125,6 +139,45 @@ describe('Jira webhook', () => {
125
139
  expect(tasks.find((t) => t.externalRef === 'ENG-100')?.title).toBe('New issue')
126
140
  })
127
141
 
142
+ test('issue_created accepts Jira webhook string descriptions', async () => {
143
+ const db = new Database(':memory:')
144
+ seedJira(db)
145
+ delete process.env['JIRA_WEBHOOK_SECRET']
146
+ const client = new JiraClient({
147
+ baseUrl: jiraConfig.baseUrl,
148
+ email: jiraConfig.email,
149
+ apiToken: jiraConfig.apiToken,
150
+ })
151
+ const provider = new JiraProvider(db, jiraConfig, client)
152
+ const description = '*Repo:* https://github.com/abpai/smoke-test\n\nString webhook body.'
153
+ const body = JSON.stringify({
154
+ webhookEvent: 'jira:issue_created',
155
+ issue: {
156
+ id: '101',
157
+ key: 'ENG-101',
158
+ fields: {
159
+ summary: 'String description issue',
160
+ description,
161
+ status: { id: '1', name: 'To Do' },
162
+ issuetype: { id: '10000', name: 'Task' },
163
+ assignee: null,
164
+ labels: ['alpha'],
165
+ comment: { total: 0 },
166
+ created: '2025-02-01T00:00:00.000Z',
167
+ updated: '2025-02-01T00:00:00.000Z',
168
+ project: { id: '1', key: 'ENG' },
169
+ },
170
+ },
171
+ })
172
+
173
+ const result = await provider.handleWebhook({ headers: {}, rawBody: body })
174
+
175
+ expect(result.handled).toBe(true)
176
+ expect(getCachedJiraTasks(db).find((t) => t.externalRef === 'ENG-101')?.description).toBe(
177
+ description,
178
+ )
179
+ })
180
+
128
181
  test('issue_deleted removes the task', async () => {
129
182
  const db = new Database(':memory:')
130
183
  seedJira(db)
@@ -181,7 +234,7 @@ describe('Jira webhook', () => {
181
234
  issue: { id: '300', key: 'ENG-300', fields: {} },
182
235
  })
183
236
  const result = await provider.handleWebhook({
184
- headers: { 'x-hub-signature-256': 'sha256=deadbeef' },
237
+ headers: { 'x-hub-signature': 'sha256=deadbeef' },
185
238
  rawBody: body,
186
239
  })
187
240
  expect(result.unauthorized).toBe(true)
@@ -214,12 +267,34 @@ describe('Jira webhook', () => {
214
267
  })
215
268
  const sig = hmac('topsecret', body)
216
269
  const result = await provider.handleWebhook({
217
- headers: { 'x-hub-signature-256': sig },
270
+ headers: { 'x-hub-signature': `sha256=${sig}` },
218
271
  rawBody: body,
219
272
  })
220
273
  expect(result.handled).toBe(true)
221
274
  })
222
275
 
276
+ test('rejects the old custom x-hub-signature-256 header when secret is configured', async () => {
277
+ const db = new Database(':memory:')
278
+ seedJira(db)
279
+ process.env['JIRA_WEBHOOK_SECRET'] = 'topsecret'
280
+ const client = new JiraClient({
281
+ baseUrl: jiraConfig.baseUrl,
282
+ email: jiraConfig.email,
283
+ apiToken: jiraConfig.apiToken,
284
+ })
285
+ const provider = new JiraProvider(db, jiraConfig, client)
286
+ const body = JSON.stringify({
287
+ webhookEvent: 'jira:issue_created',
288
+ issue: { id: '401', key: 'ENG-401', fields: {} },
289
+ })
290
+ const sig = hmac('topsecret', body)
291
+ const result = await provider.handleWebhook({
292
+ headers: { 'x-hub-signature-256': `sha256=${sig}` },
293
+ rawBody: body,
294
+ })
295
+ expect(result.unauthorized).toBe(true)
296
+ })
297
+
223
298
  test('ignores issue updates from other projects', async () => {
224
299
  const db = new Database(':memory:')
225
300
  seedJira(db)
@@ -434,6 +434,8 @@ function renderBlocks(nodes: AdfBlockNode[]): string {
434
434
  return out.join('\n\n')
435
435
  }
436
436
 
437
- export function adfToPlainText(doc: AdfDocument): string {
437
+ export function adfToPlainText(doc: AdfDocument | string | null | undefined): string {
438
+ if (typeof doc === 'string') return doc
439
+ if (!doc || !Array.isArray(doc.content)) return ''
438
440
  return renderBlocks(doc.content)
439
441
  }
@@ -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)) {