zooid 0.14.0 → 0.14.1
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/dist/bin.js +19 -4
- package/dist/bin.js.map +1 -1
- package/package.json +9 -9
- package/src/daemon/webhook-routes.test.ts +31 -1
- package/src/daemon/webhook-routes.ts +40 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zooid",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "An open-source, self-hostable chat app for collaborating with AI agents alongside your team. Any model, any CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,14 +43,14 @@
|
|
|
43
43
|
"tar": "^7.5.16",
|
|
44
44
|
"web-push": "^3.6.7",
|
|
45
45
|
"yaml": "^2.5.0",
|
|
46
|
-
"@zooid/
|
|
47
|
-
"@zooid/
|
|
48
|
-
"@zooid/
|
|
49
|
-
"@zooid/runtime-docker": "^0.14.
|
|
50
|
-
"@zooid/
|
|
51
|
-
"@zooid/runtime-local": "^0.14.
|
|
52
|
-
"@zooid/transport-
|
|
53
|
-
"@zooid/
|
|
46
|
+
"@zooid/context-mcp": "^0.14.1",
|
|
47
|
+
"@zooid/acp-client": "^0.14.1",
|
|
48
|
+
"@zooid/transport-matrix": "^0.14.1",
|
|
49
|
+
"@zooid/runtime-docker": "^0.14.1",
|
|
50
|
+
"@zooid/core": "^0.14.1",
|
|
51
|
+
"@zooid/runtime-local": "^0.14.1",
|
|
52
|
+
"@zooid/transport-http": "^0.14.1",
|
|
53
|
+
"@zooid/pi-extension": "^0.13.1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@agentclientprotocol/sdk": "^0.21.0",
|
|
@@ -2,7 +2,7 @@ import { createHmac } from 'node:crypto'
|
|
|
2
2
|
import { describe, expect, it, vi } from 'vitest'
|
|
3
3
|
import { Hono } from 'hono'
|
|
4
4
|
import { loadZooidConfig } from '@zooid/core'
|
|
5
|
-
import { mountWebhookRoutes } from './webhook-routes.js'
|
|
5
|
+
import { eventNameFor, mountWebhookRoutes } from './webhook-routes.js'
|
|
6
6
|
|
|
7
7
|
const secret = 'route-secret'
|
|
8
8
|
const body = JSON.stringify({ action: 'opened' })
|
|
@@ -72,3 +72,33 @@ describe('webhook route contract', () => {
|
|
|
72
72
|
expect(sent).toHaveLength(0)
|
|
73
73
|
})
|
|
74
74
|
})
|
|
75
|
+
|
|
76
|
+
describe('event name binding', () => {
|
|
77
|
+
const headers = { 'x-github-event': 'pull_request' }
|
|
78
|
+
|
|
79
|
+
it('reads GitHub from the header, not the payload', () => {
|
|
80
|
+
expect(eventNameFor('github', headers, { type: 'ignored' })).toBe('pull_request')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('reads stripe and standard-webhooks from body.type', () => {
|
|
84
|
+
expect(eventNameFor('stripe', {}, { type: 'invoice.payment_failed' })).toBe(
|
|
85
|
+
'invoice.payment_failed',
|
|
86
|
+
)
|
|
87
|
+
expect(eventNameFor('standard', {}, { type: 'user.created' })).toBe('user.created')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('reads slack from body.event.type', () => {
|
|
91
|
+
expect(eventNameFor('slack', {}, { event: { type: 'app_mention' } })).toBe('app_mention')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('leaves custom unbound — only the operator knows their service shape', () => {
|
|
95
|
+
expect(eventNameFor('custom', headers, { type: 'anything' })).toBeUndefined()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('yields undefined rather than throwing on a missing or wrong-typed path', () => {
|
|
99
|
+
expect(eventNameFor('stripe', {}, undefined)).toBeUndefined()
|
|
100
|
+
expect(eventNameFor('stripe', {}, { type: 42 })).toBeUndefined()
|
|
101
|
+
expect(eventNameFor('slack', {}, { event: null })).toBeUndefined()
|
|
102
|
+
expect(eventNameFor('github', {}, {})).toBeUndefined()
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -16,8 +16,42 @@ const TRUNCATION_MARKER = '\n\n… (truncated)'
|
|
|
16
16
|
// Comfortably past any provider's redelivery window.
|
|
17
17
|
const DELIVERY_CACHE_TTL_MS = 24 * 60 * 60 * 1000
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Where each provider puts its event name. GitHub is the only one that uses
|
|
21
|
+
* a header; the rest carry it in the payload. Reading it per provider is the
|
|
22
|
+
* point — it keeps provider knowledge in code (the same knowledge the
|
|
23
|
+
* signature table encodes) so `event` means the same thing in an operator's
|
|
24
|
+
* `match:` whatever the sender. `custom` is absent on purpose: only the
|
|
25
|
+
* operator knows their service's shape, so their predicate reads `body`.
|
|
26
|
+
*/
|
|
27
|
+
const EVENT_SOURCE_BY_PROVIDER: Partial<
|
|
28
|
+
Record<WebhookTriggerConfig['provider'], { header: string } | { path: readonly string[] }>
|
|
29
|
+
> = {
|
|
30
|
+
github: { header: 'x-github-event' },
|
|
31
|
+
stripe: { path: ['type'] },
|
|
32
|
+
standard: { path: ['type'] },
|
|
33
|
+
slack: { path: ['event', 'type'] },
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Follow a dotted path through a parsed payload, yielding a string or nothing. */
|
|
37
|
+
function stringAt(body: unknown, path: readonly string[]): string | undefined {
|
|
38
|
+
let cursor: unknown = body
|
|
39
|
+
for (const key of path) {
|
|
40
|
+
if (typeof cursor !== 'object' || cursor === null) return undefined
|
|
41
|
+
cursor = (cursor as Record<string, unknown>)[key]
|
|
42
|
+
}
|
|
43
|
+
return typeof cursor === 'string' ? cursor : undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The `event` binding for `match:` and `${...}`, per provider. */
|
|
47
|
+
export function eventNameFor(
|
|
48
|
+
provider: WebhookTriggerConfig['provider'],
|
|
49
|
+
headers: Record<string, string | undefined>,
|
|
50
|
+
body: unknown,
|
|
51
|
+
): string | undefined {
|
|
52
|
+
const source = EVENT_SOURCE_BY_PROVIDER[provider]
|
|
53
|
+
if (!source) return undefined
|
|
54
|
+
return 'header' in source ? headers[source.header] : stringAt(body, source.path)
|
|
21
55
|
}
|
|
22
56
|
|
|
23
57
|
const DELIVERY_ID_HEADER_BY_PROVIDER: Partial<Record<WebhookTriggerConfig['provider'], string>> = {
|
|
@@ -111,9 +145,6 @@ async function handleDelivery(
|
|
|
111
145
|
const deliveryId = customDeliveryId ?? (idHeader ? headers[idHeader] : undefined)
|
|
112
146
|
if (deliveryId !== undefined && cache.seen(`${name}:${deliveryId}`)) return
|
|
113
147
|
|
|
114
|
-
const eventHeader = EVENT_HEADER_BY_PROVIDER[webhook.provider]
|
|
115
|
-
const event = eventHeader ? headers[eventHeader] : undefined
|
|
116
|
-
|
|
117
148
|
let body: unknown
|
|
118
149
|
try {
|
|
119
150
|
body = JSON.parse(raw)
|
|
@@ -121,6 +152,10 @@ async function handleDelivery(
|
|
|
121
152
|
body = undefined
|
|
122
153
|
}
|
|
123
154
|
|
|
155
|
+
// After parsing: every provider but GitHub carries its event name in the
|
|
156
|
+
// payload, not a header.
|
|
157
|
+
const event = eventNameFor(webhook.provider, headers, body)
|
|
158
|
+
|
|
124
159
|
const ctx: MatchContext = {
|
|
125
160
|
event,
|
|
126
161
|
body,
|