@andypai/agent-kanban 0.6.1 → 0.6.2

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.6.1",
3
+ "version": "0.6.2",
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": {
@@ -690,8 +690,8 @@ describe('Linear webhook', () => {
690
690
  })
691
691
  })
692
692
 
693
- describe('webhook fail-closed when secret is unset', () => {
694
- test('Jira rejects unauthenticated webhooks when JIRA_WEBHOOK_SECRET is unset', async () => {
693
+ describe('webhook open dev mode when secret is unset', () => {
694
+ test('Jira accepts webhooks when JIRA_WEBHOOK_SECRET is unset', async () => {
695
695
  const prev = process.env['JIRA_WEBHOOK_SECRET']
696
696
  delete process.env['JIRA_WEBHOOK_SECRET']
697
697
  try {
@@ -703,17 +703,33 @@ describe('webhook fail-closed when secret is unset', () => {
703
703
  apiToken: jiraConfig.apiToken,
704
704
  })
705
705
  const provider = new JiraProvider(db, jiraConfig, client)
706
- const body = JSON.stringify({ webhookEvent: 'jira:issue_created', issue: { id: '1' } })
706
+ const body = JSON.stringify({
707
+ webhookEvent: 'jira:issue_created',
708
+ issue: {
709
+ id: '100',
710
+ key: 'ENG-100',
711
+ fields: {
712
+ summary: 'New issue',
713
+ status: { id: '1', name: 'To Do' },
714
+ issuetype: { id: '10000', name: 'Task' },
715
+ assignee: null,
716
+ labels: [],
717
+ comment: { total: 0 },
718
+ created: '2025-02-01T00:00:00.000Z',
719
+ updated: '2025-02-01T00:00:00.000Z',
720
+ project: { id: '1', key: 'ENG' },
721
+ },
722
+ },
723
+ })
707
724
  const result = await provider.handleWebhook({ headers: {}, rawBody: body })
708
- expect(result.handled).toBe(false)
709
- expect(result.unauthorized).toBe(true)
725
+ expect(result.unauthorized).not.toBe(true)
710
726
  } finally {
711
727
  if (prev === undefined) delete process.env['JIRA_WEBHOOK_SECRET']
712
728
  else process.env['JIRA_WEBHOOK_SECRET'] = prev
713
729
  }
714
730
  })
715
731
 
716
- test('Linear rejects unauthenticated webhooks when LINEAR_WEBHOOK_SECRET is unset', async () => {
732
+ test('Linear accepts webhooks when LINEAR_WEBHOOK_SECRET is unset', async () => {
717
733
  const prev = process.env['LINEAR_WEBHOOK_SECRET']
718
734
  delete process.env['LINEAR_WEBHOOK_SECRET']
719
735
  try {
@@ -721,9 +737,9 @@ describe('webhook fail-closed when secret is unset', () => {
721
737
  seedLinear(db)
722
738
  const provider = new LinearProvider(db, 'tid', 'key')
723
739
  const body = JSON.stringify({ action: 'create', type: 'Issue', data: { id: 'i1' } })
724
- const result = await provider.handleWebhook({ headers: {}, rawBody: body })
725
- expect(result.handled).toBe(false)
726
- expect(result.unauthorized).toBe(true)
740
+ // Auth passes; downstream may fail for other reasons (e.g. API credentials)
741
+ const result = await provider.handleWebhook({ headers: {}, rawBody: body }).catch(() => null)
742
+ expect(result?.unauthorized).not.toBe(true)
727
743
  } finally {
728
744
  if (prev === undefined) delete process.env['LINEAR_WEBHOOK_SECRET']
729
745
  else process.env['LINEAR_WEBHOOK_SECRET'] = prev
@@ -855,6 +855,11 @@ export class JiraProviderCore implements KanbanProvider {
855
855
  // Shared webhook dispatch. Postgres wraps this with webhook-event auditing;
856
856
  // SQLite calls it directly via the default handleWebhook above.
857
857
  protected async handleWebhookCore(payload: WebhookRequest): Promise<WebhookResult> {
858
+ if (!process.env['JIRA_WEBHOOK_SECRET']) {
859
+ console.warn(
860
+ '[jira] JIRA_WEBHOOK_SECRET is not set — accepting webhook without signature verification (open dev mode)',
861
+ )
862
+ }
858
863
  const auth = authorizeWebhook({
859
864
  secret: process.env['JIRA_WEBHOOK_SECRET'],
860
865
  rawBody: payload.rawBody,
@@ -579,6 +579,11 @@ export class LinearProviderCore implements KanbanProvider {
579
579
  // Shared webhook dispatch. Postgres wraps this with webhook-event auditing;
580
580
  // SQLite calls it directly via the default handleWebhook above.
581
581
  protected async handleWebhookCore(payload: WebhookRequest): Promise<WebhookResult> {
582
+ if (!process.env['LINEAR_WEBHOOK_SECRET']) {
583
+ console.warn(
584
+ '[linear] LINEAR_WEBHOOK_SECRET is not set — accepting webhook without signature verification (open dev mode)',
585
+ )
586
+ }
582
587
  const auth = authorizeWebhook({
583
588
  secret: process.env['LINEAR_WEBHOOK_SECRET'],
584
589
  rawBody: payload.rawBody,
package/src/webhooks.ts CHANGED
@@ -13,10 +13,9 @@ export interface WebhookResult {
13
13
  }
14
14
 
15
15
  /**
16
- * Fail-closed webhook authorization. Returns a rejecting WebhookResult when the
17
- * secret is unset (so an anonymous caller can't inject cache writes) or when the
18
- * signature doesn't verify, and null when the request is authorized. Webhook
19
- * delivery therefore requires the provider secret to be configured.
16
+ * Webhook authorization. When secret is configured, verifies the HMAC signature
17
+ * and rejects on mismatch. When secret is unset, accepts all payloads (open dev
18
+ * mode suitable when the provider webhook is not configured with a signing secret).
20
19
  */
21
20
  export function authorizeWebhook(opts: {
22
21
  secret: string | undefined
@@ -25,13 +24,7 @@ export function authorizeWebhook(opts: {
25
24
  verify: (secret: string, rawBody: string, signature: string | undefined | null) => boolean
26
25
  }): WebhookResult | null {
27
26
  const { secret, rawBody, signature, verify } = opts
28
- if (!secret) {
29
- return {
30
- handled: false,
31
- unauthorized: true,
32
- message: 'Webhook secret not configured; rejecting unauthenticated webhook',
33
- }
34
- }
27
+ if (!secret) return null
35
28
  if (!verify(secret, rawBody, signature)) {
36
29
  return { handled: false, unauthorized: true, message: 'Invalid signature' }
37
30
  }