zooid 0.13.0 → 0.14.0
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/README.md +6 -1
- package/dist/bin.js +674 -171
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-YZ4IO5MR.js → chunk-R5S26T7B.js} +20523 -507
- package/dist/chunk-R5S26T7B.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/package.json +11 -9
- package/src/build-registry.context.test.ts +3 -3
- package/src/build-registry.ts +67 -24
- package/src/daemon/delivery-cache.test.ts +34 -0
- package/src/daemon/delivery-cache.ts +36 -0
- package/src/daemon/load-custom-verifiers.ts +43 -0
- package/src/daemon/start-daemon.ts +162 -38
- package/src/daemon/task-journal.test.ts +19 -0
- package/src/daemon/task-journal.ts +32 -0
- package/src/daemon/trigger-rooms.test.ts +60 -0
- package/src/daemon/trigger-rooms.ts +0 -0
- package/src/daemon/trigger-runner.test.ts +71 -0
- package/src/daemon/trigger-runner.ts +41 -0
- package/src/daemon/trigger-scheduler.ts +55 -0
- package/src/daemon/webhook-routes.test.ts +74 -0
- package/src/daemon/webhook-routes.ts +202 -0
- package/src/daemon/webhook-verify.test.ts +155 -0
- package/src/daemon/webhook-verify.ts +155 -0
- package/src/pi-extension-install.test.ts +65 -0
- package/src/pi-extension-install.ts +56 -0
- package/dist/chunk-YZ4IO5MR.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -16,13 +16,13 @@ interface BuildAcpRegistryOptions {
|
|
|
16
16
|
agentsDir?: string;
|
|
17
17
|
/**
|
|
18
18
|
* Per-spawn binding store for the zooid-context MCP server. When set
|
|
19
|
-
* together with `
|
|
19
|
+
* together with `daemonSockPaths`, agents bound to a transport that owns
|
|
20
20
|
* conversation context get a `contextSpawn` factory threaded into their
|
|
21
21
|
* AcpClient so `session/new mcpServers` includes the zooid-context entry.
|
|
22
22
|
*/
|
|
23
23
|
contextSpawnRegistry?: SpawnRegistry;
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/** Agent → host path to that agent's context Unix socket. */
|
|
25
|
+
daemonSockPaths?: Record<string, string | undefined>;
|
|
26
26
|
/**
|
|
27
27
|
* Directory containing zooid.yaml. Required when any agent uses the
|
|
28
28
|
* workspace auto-mount (the default under `runtime: docker | podman`):
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zooid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
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",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
27
|
"zooid": {
|
|
28
|
-
"webVersion": "0.
|
|
28
|
+
"webVersion": "0.12.0"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
31
31
|
"node": ">=22"
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"@inquirer/prompts": "^8.4.3",
|
|
36
36
|
"cac": "^6.7.14",
|
|
37
37
|
"chalk": "^5.4.1",
|
|
38
|
+
"croner": "^10.0.1",
|
|
38
39
|
"hono": "^4.6.0",
|
|
39
40
|
"listr2": "^8.2.5",
|
|
40
41
|
"marked": "^18.0.4",
|
|
@@ -42,13 +43,14 @@
|
|
|
42
43
|
"tar": "^7.5.16",
|
|
43
44
|
"web-push": "^3.6.7",
|
|
44
45
|
"yaml": "^2.5.0",
|
|
45
|
-
"@zooid/acp-client": "^0.
|
|
46
|
-
"@zooid/
|
|
47
|
-
"@zooid/
|
|
48
|
-
"@zooid/
|
|
49
|
-
"@zooid/
|
|
50
|
-
"@zooid/runtime-
|
|
51
|
-
"@zooid/transport-
|
|
46
|
+
"@zooid/acp-client": "^0.14.0",
|
|
47
|
+
"@zooid/core": "^0.14.0",
|
|
48
|
+
"@zooid/pi-extension": "^0.13.1",
|
|
49
|
+
"@zooid/runtime-docker": "^0.14.0",
|
|
50
|
+
"@zooid/context-mcp": "^0.14.0",
|
|
51
|
+
"@zooid/runtime-local": "^0.14.0",
|
|
52
|
+
"@zooid/transport-matrix": "^0.14.0",
|
|
53
|
+
"@zooid/transport-http": "^0.14.0"
|
|
52
54
|
},
|
|
53
55
|
"devDependencies": {
|
|
54
56
|
"@agentclientprotocol/sdk": "^0.21.0",
|
|
@@ -44,7 +44,7 @@ describe('buildAcpRegistry — context provider wiring', () => {
|
|
|
44
44
|
const registry = buildAcpRegistry(matrixCfg, {
|
|
45
45
|
approvals: { register: vi.fn(), on: vi.fn() } as never,
|
|
46
46
|
contextSpawnRegistry: {} as never,
|
|
47
|
-
|
|
47
|
+
daemonSockPaths: { architect: '/tmp/zooid-test.sock' },
|
|
48
48
|
})
|
|
49
49
|
expect(registry.hasContextSpawn('architect')).toBe(true)
|
|
50
50
|
})
|
|
@@ -59,7 +59,7 @@ describe('buildAcpRegistry — context provider wiring', () => {
|
|
|
59
59
|
const registry = buildAcpRegistry(httpCfg, {
|
|
60
60
|
approvals: { register: vi.fn(), on: vi.fn() } as never,
|
|
61
61
|
contextSpawnRegistry: {} as never,
|
|
62
|
-
|
|
62
|
+
daemonSockPaths: { architect: '/tmp/zooid-test.sock' },
|
|
63
63
|
})
|
|
64
64
|
expect(registry.hasContextSpawn('architect')).toBe(false)
|
|
65
65
|
})
|
|
@@ -77,7 +77,7 @@ describe('buildAcpRegistry — context provider in a container runtime', () => {
|
|
|
77
77
|
const opts = {
|
|
78
78
|
approvals: { register: vi.fn(), on: vi.fn() } as never,
|
|
79
79
|
contextSpawnRegistry: { register: vi.fn(() => 'spawn-1') } as never,
|
|
80
|
-
|
|
80
|
+
daemonSockPaths: { architect: '/home/ubuntu/hq/data/run/context.sock' },
|
|
81
81
|
daemonHome: '/home/ubuntu',
|
|
82
82
|
}
|
|
83
83
|
|
package/src/build-registry.ts
CHANGED
|
@@ -14,7 +14,12 @@ import {
|
|
|
14
14
|
type TransportContextProvider,
|
|
15
15
|
} from '@zooid/core'
|
|
16
16
|
import { MatrixClient, MatrixContextProvider } from '@zooid/transport-matrix'
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
CONTEXT_CONTAINER_SOCK,
|
|
19
|
+
SpawnRegistry,
|
|
20
|
+
buildContextServerSpec,
|
|
21
|
+
contextContainerMounts,
|
|
22
|
+
} from '@zooid/context-mcp'
|
|
18
23
|
import { PRESETS, type PresetMount, type PresetName } from '@zooid/acp-client'
|
|
19
24
|
|
|
20
25
|
export interface BuildAcpRegistryOptions {
|
|
@@ -32,13 +37,13 @@ export interface BuildAcpRegistryOptions {
|
|
|
32
37
|
agentsDir?: string
|
|
33
38
|
/**
|
|
34
39
|
* Per-spawn binding store for the zooid-context MCP server. When set
|
|
35
|
-
* together with `
|
|
40
|
+
* together with `daemonSockPaths`, agents bound to a transport that owns
|
|
36
41
|
* conversation context get a `contextSpawn` factory threaded into their
|
|
37
42
|
* AcpClient so `session/new mcpServers` includes the zooid-context entry.
|
|
38
43
|
*/
|
|
39
44
|
contextSpawnRegistry?: SpawnRegistry
|
|
40
|
-
/**
|
|
41
|
-
|
|
45
|
+
/** Agent → host path to that agent's context Unix socket. */
|
|
46
|
+
daemonSockPaths?: Record<string, string | undefined>
|
|
42
47
|
/**
|
|
43
48
|
* Directory containing zooid.yaml. Required when any agent uses the
|
|
44
49
|
* workspace auto-mount (the default under `runtime: docker | podman`):
|
|
@@ -78,10 +83,7 @@ function presetOf(agent: AgentConfig): PresetName | undefined {
|
|
|
78
83
|
return undefined
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
function resolveAgentImage(
|
|
82
|
-
agent: AgentConfig,
|
|
83
|
-
cfg: ZooidConfig,
|
|
84
|
-
): ResolvedImage {
|
|
86
|
+
function resolveAgentImage(agent: AgentConfig, cfg: ZooidConfig): ResolvedImage {
|
|
85
87
|
if (agent.container?.image) return { image: agent.container.image, source: 'agent' }
|
|
86
88
|
if (cfg.container?.image) return { image: cfg.container.image, source: 'workforce' }
|
|
87
89
|
const preset = presetOf(agent)
|
|
@@ -183,9 +185,7 @@ function composeAgentMounts(
|
|
|
183
185
|
composed.push(out)
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
const mkdirs: string[] = composed
|
|
187
|
-
.filter((m) => m.create)
|
|
188
|
-
.map((m) => m.host)
|
|
188
|
+
const mkdirs: string[] = composed.filter((m) => m.create).map((m) => m.host)
|
|
189
189
|
|
|
190
190
|
return {
|
|
191
191
|
mounts: composed,
|
|
@@ -227,8 +227,9 @@ export function buildAcpRegistry(
|
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
if (missing.length > 0) {
|
|
230
|
-
const lines = missing.map(
|
|
231
|
-
|
|
230
|
+
const lines = missing.map(
|
|
231
|
+
({ name, preset }) =>
|
|
232
|
+
` - ${name}${preset ? ` (preset: ${preset})` : ''} — no preset-default image`,
|
|
232
233
|
)
|
|
233
234
|
throw new Error(
|
|
234
235
|
`runtime: ${cfg.runtime} requires a container image for each agent. Unresolved:\n` +
|
|
@@ -260,9 +261,10 @@ export function buildAcpRegistry(
|
|
|
260
261
|
mkdirByAgent[name] = composed.mkdirs
|
|
261
262
|
cwdByAgent[name] = composed.cwd
|
|
262
263
|
if (resolvedImage && cfg.runtime !== 'local') {
|
|
263
|
-
const mountSummary =
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
const mountSummary =
|
|
265
|
+
composed.mounts.length === 0
|
|
266
|
+
? 'mounts=[]'
|
|
267
|
+
: `mounts=[${composed.mounts.map((m) => m.id ?? m.target).join(',')}]`
|
|
266
268
|
log(
|
|
267
269
|
`[zooid] agent ${name.padEnd(12)} image=${resolvedImage} source=${source} ${mountSummary}`,
|
|
268
270
|
)
|
|
@@ -271,16 +273,33 @@ export function buildAcpRegistry(
|
|
|
271
273
|
|
|
272
274
|
const contextSpawns = buildContextSpawns(cfg, opts)
|
|
273
275
|
|
|
276
|
+
// Extensions run in the agent process (rather than an MCP child), so all
|
|
277
|
+
// context-enabled agents inherit the daemon socket address directly.
|
|
278
|
+
if (contextSpawns) {
|
|
279
|
+
for (const name of Object.keys(cfg.agents)) {
|
|
280
|
+
if (!contextSpawns[name]) continue
|
|
281
|
+
const agentSock = opts.daemonSockPaths?.[name]
|
|
282
|
+
if (!agentSock) continue
|
|
283
|
+
env[name] = {
|
|
284
|
+
ZOOID_DAEMON_SOCK:
|
|
285
|
+
cfg.runtime === 'local' ? agentSock : CONTEXT_CONTAINER_SOCK,
|
|
286
|
+
...env[name],
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
274
291
|
// Container runtimes: opencode spawns the zooid-context MCP subprocess INSIDE
|
|
275
292
|
// its container, so the daemon socket + the (self-contained) bin must be
|
|
276
293
|
// bind-mounted in. Local runtime needs neither — the host spec resolves
|
|
277
294
|
// directly. Only agents that actually got a context factory get the mounts.
|
|
278
|
-
if (cfg.runtime !== 'local' &&
|
|
295
|
+
if (cfg.runtime !== 'local' && contextSpawns) {
|
|
279
296
|
for (const name of Object.keys(cfg.agents)) {
|
|
280
297
|
if (!contextSpawns[name]) continue
|
|
298
|
+
const agentSock = opts.daemonSockPaths?.[name]
|
|
299
|
+
if (!agentSock) continue
|
|
281
300
|
mountsByAgent[name] = [
|
|
282
301
|
...(mountsByAgent[name] ?? []),
|
|
283
|
-
...contextContainerMounts({ sockPath:
|
|
302
|
+
...contextContainerMounts({ sockPath: agentSock }),
|
|
284
303
|
]
|
|
285
304
|
}
|
|
286
305
|
}
|
|
@@ -297,6 +316,8 @@ export function buildAcpRegistry(
|
|
|
297
316
|
onTap: opts.onTap,
|
|
298
317
|
agentsDir: opts.agentsDir,
|
|
299
318
|
contextSpawns,
|
|
319
|
+
onSessionEstablished: (agentName, sessionKey, sessionId) =>
|
|
320
|
+
opts.contextSpawnRegistry?.linkSession(agentName, sessionKey, sessionId),
|
|
300
321
|
})
|
|
301
322
|
}
|
|
302
323
|
|
|
@@ -304,9 +325,8 @@ function buildContextSpawns(
|
|
|
304
325
|
cfg: ZooidConfig,
|
|
305
326
|
opts: BuildAcpRegistryOptions,
|
|
306
327
|
): Record<string, ContextSpawnFactory | undefined> | undefined {
|
|
307
|
-
if (!opts.contextSpawnRegistry || !opts.
|
|
328
|
+
if (!opts.contextSpawnRegistry || !opts.daemonSockPaths) return undefined
|
|
308
329
|
const registry = opts.contextSpawnRegistry
|
|
309
|
-
const sockPath = opts.daemonSockPath
|
|
310
330
|
|
|
311
331
|
const matrixClients = new Map<string, MatrixClient>()
|
|
312
332
|
const agentBots = new Map<string, string>()
|
|
@@ -323,18 +343,23 @@ function buildContextSpawns(
|
|
|
323
343
|
|
|
324
344
|
const result: Record<string, ContextSpawnFactory | undefined> = {}
|
|
325
345
|
for (const [name, agent] of Object.entries(cfg.agents)) {
|
|
326
|
-
|
|
346
|
+
const sockPath = opts.daemonSockPaths[name]
|
|
347
|
+
if (agent.matrix && matrixClients.has(agent.matrix.transport) && sockPath) {
|
|
327
348
|
const client = matrixClients.get(agent.matrix.transport)!
|
|
328
349
|
const provider: TransportContextProvider = new MatrixContextProvider({
|
|
329
350
|
client,
|
|
330
351
|
asUserId: agent.matrix.user_id,
|
|
331
352
|
agentBots,
|
|
353
|
+
// Same array BotPool.bootstrap rewrites `.alias` on in place — reads
|
|
354
|
+
// through this after bootstrap see canonical room IDs.
|
|
355
|
+
rooms: agent.matrix.rooms,
|
|
332
356
|
})
|
|
333
|
-
result[name] = async (threadId: string, channelId?: string) => {
|
|
357
|
+
result[name] = async (threadId: string, channelId?: string, sessionKey?: string) => {
|
|
334
358
|
const spawnId = registry.register({
|
|
335
359
|
agentName: name,
|
|
336
360
|
threadRef: { channelId: channelId ?? threadId, threadId },
|
|
337
361
|
provider,
|
|
362
|
+
sessionKey: sessionKey ?? threadId,
|
|
338
363
|
})
|
|
339
364
|
return buildContextServerSpec({
|
|
340
365
|
spawnId,
|
|
@@ -349,13 +374,31 @@ function buildContextSpawns(
|
|
|
349
374
|
return result
|
|
350
375
|
}
|
|
351
376
|
|
|
377
|
+
/** Agents on configured Matrix transports get a context listener and binding. */
|
|
378
|
+
export function contextEligibleAgents(cfg: ZooidConfig): string[] {
|
|
379
|
+
const matrixTransports = new Set(
|
|
380
|
+
Object.entries(cfg.transports)
|
|
381
|
+
.filter(([, transport]) => transport.type === 'matrix')
|
|
382
|
+
.map(([name]) => name),
|
|
383
|
+
)
|
|
384
|
+
return Object.entries(cfg.agents)
|
|
385
|
+
.filter(([, agent]) => agent.matrix && matrixTransports.has(agent.matrix.transport))
|
|
386
|
+
.map(([name]) => name)
|
|
387
|
+
}
|
|
388
|
+
|
|
352
389
|
function defaultRuntimeFor(cfg: ZooidConfig): AcpRuntime {
|
|
353
390
|
if (cfg.runtime === 'local') return new LocalAcpRuntime()
|
|
354
391
|
if (cfg.runtime === 'docker') {
|
|
355
|
-
return new DockerAcpRuntime({
|
|
392
|
+
return new DockerAcpRuntime({
|
|
393
|
+
defaultImage: cfg.container?.image,
|
|
394
|
+
engine: 'docker',
|
|
395
|
+
})
|
|
356
396
|
}
|
|
357
397
|
if (cfg.runtime === 'podman') {
|
|
358
|
-
return new DockerAcpRuntime({
|
|
398
|
+
return new DockerAcpRuntime({
|
|
399
|
+
defaultImage: cfg.container?.image,
|
|
400
|
+
engine: 'podman',
|
|
401
|
+
})
|
|
359
402
|
}
|
|
360
403
|
throw new Error(`unsupported runtime: ${cfg.runtime}`)
|
|
361
404
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { DeliveryCache } from './delivery-cache.js'
|
|
3
|
+
|
|
4
|
+
describe('DeliveryCache', () => {
|
|
5
|
+
beforeEach(() => vi.useFakeTimers())
|
|
6
|
+
afterEach(() => vi.useRealTimers())
|
|
7
|
+
|
|
8
|
+
it('accepts an id once and rejects the replay', () => {
|
|
9
|
+
const c = new DeliveryCache(60_000)
|
|
10
|
+
expect(c.seen('abc')).toBe(false)
|
|
11
|
+
expect(c.seen('abc')).toBe(true)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('treats ids from different triggers as distinct', () => {
|
|
15
|
+
const c = new DeliveryCache(60_000)
|
|
16
|
+
expect(c.seen('t1:abc')).toBe(false)
|
|
17
|
+
expect(c.seen('t2:abc')).toBe(false)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('forgets an id after the TTL, so the cache cannot grow without bound', () => {
|
|
21
|
+
const c = new DeliveryCache(60_000)
|
|
22
|
+
c.seen('abc')
|
|
23
|
+
vi.advanceTimersByTime(61_000)
|
|
24
|
+
expect(c.seen('abc')).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('evicts expired entries rather than retaining them forever', () => {
|
|
28
|
+
const c = new DeliveryCache(1_000)
|
|
29
|
+
for (let i = 0; i < 1000; i++) c.seen(`id-${i}`)
|
|
30
|
+
vi.advanceTimersByTime(2_000)
|
|
31
|
+
c.seen('trigger-eviction')
|
|
32
|
+
expect(c.size).toBeLessThan(10)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TTL-bounded replay defence for webhook delivery ids. GitHub sends no
|
|
3
|
+
* timestamp, so freshness-window rejection (used for Stripe/Slack in
|
|
4
|
+
* `webhook-verify.ts`) is not available — dedupe on delivery id instead.
|
|
5
|
+
* Keys are namespaced by trigger name (e.g. `t1:abc`) so two triggers never
|
|
6
|
+
* collide on the same upstream id.
|
|
7
|
+
*/
|
|
8
|
+
export class DeliveryCache {
|
|
9
|
+
private readonly ttlMs: number
|
|
10
|
+
private readonly expiryById = new Map<string, number>()
|
|
11
|
+
|
|
12
|
+
constructor(ttlMs: number) {
|
|
13
|
+
this.ttlMs = ttlMs
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get size(): number {
|
|
17
|
+
return this.expiryById.size
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Returns true if `id` was already seen (and still within its TTL). */
|
|
21
|
+
seen(id: string): boolean {
|
|
22
|
+
this.evictExpired()
|
|
23
|
+
const now = Date.now()
|
|
24
|
+
const expiry = this.expiryById.get(id)
|
|
25
|
+
if (expiry !== undefined && expiry > now) return true
|
|
26
|
+
this.expiryById.set(id, now + this.ttlMs)
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private evictExpired(): void {
|
|
31
|
+
const now = Date.now()
|
|
32
|
+
for (const [id, expiry] of this.expiryById) {
|
|
33
|
+
if (expiry <= now) this.expiryById.delete(id)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url'
|
|
2
|
+
import type { TriggerConfig } from '@zooid/core'
|
|
3
|
+
import type { CustomVerifier } from './webhook-verify.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Import the `verify:` module of every `provider: custom` trigger, keyed by
|
|
7
|
+
* trigger name.
|
|
8
|
+
*
|
|
9
|
+
* Loaded once at daemon start rather than per delivery: a bad path or a
|
|
10
|
+
* module that exports the wrong thing is a configuration error, and it
|
|
11
|
+
* should surface when the operator starts the daemon, not silently as a 401
|
|
12
|
+
* the first time the service fires. Config has already resolved the path to
|
|
13
|
+
* an absolute one against the zooid.yaml directory.
|
|
14
|
+
*/
|
|
15
|
+
export async function loadCustomVerifiers(
|
|
16
|
+
triggers: Record<string, TriggerConfig>,
|
|
17
|
+
): Promise<Record<string, CustomVerifier>> {
|
|
18
|
+
const out: Record<string, CustomVerifier> = {}
|
|
19
|
+
for (const [name, trigger] of Object.entries(triggers)) {
|
|
20
|
+
const path = trigger.webhook?.provider === 'custom' ? trigger.webhook.verify : undefined
|
|
21
|
+
if (!path) continue
|
|
22
|
+
|
|
23
|
+
let mod: Record<string, unknown>
|
|
24
|
+
try {
|
|
25
|
+
mod = (await import(pathToFileURL(path).href)) as Record<string, unknown>
|
|
26
|
+
} catch (err) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`triggers.${name}.webhook.verify: cannot load ${path} — ${(err as Error).message}`,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
// `export default` is the documented shape; a named `verify` export is
|
|
32
|
+
// accepted so a module can hold more than one provider's verifier.
|
|
33
|
+
const fn = mod.default ?? mod.verify
|
|
34
|
+
if (typeof fn !== 'function') {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`triggers.${name}.webhook.verify: ${path} must export a function as \`default\` ` +
|
|
37
|
+
`(or as \`verify\`), got ${typeof fn}`,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
out[name] = fn as CustomVerifier
|
|
41
|
+
}
|
|
42
|
+
return out
|
|
43
|
+
}
|