@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.
|
|
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
|
|
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
|
|
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)
|
package/src/providers/jira.ts
CHANGED
|
@@ -11,7 +11,12 @@ import type {
|
|
|
11
11
|
TaskComment,
|
|
12
12
|
Task,
|
|
13
13
|
} from '../types'
|
|
14
|
-
import {
|
|
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
|
|
716
|
-
if (!
|
|
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 {
|
|
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
|
|
1128
|
-
if (!
|
|
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)) {
|