zooid 0.2.1 → 0.4.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 +95 -61
- package/dist/{chunk-EEA3FCBS.js → chunk-VBGU2NST.js} +18 -21
- package/dist/{client-QPT54SNG.js → client-4VMFEFDX.js} +1 -1
- package/dist/index.js +144 -106
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
<h1 align="center">🪸 Zooid</h1>
|
|
3
3
|
<p align="center"><strong>Pub/sub for AI agents. Deploy in one command. Free forever.</strong></p>
|
|
4
4
|
<p align="center">
|
|
5
|
-
<a href="https://directory.zooid.dev/api/discover">Browse Servers</a> ·
|
|
6
5
|
<a href="#quickstart">Quickstart</a> ·
|
|
6
|
+
<a href="https://zooid.dev/docs">Docs</a> ·
|
|
7
|
+
<a href="https://directory.zooid.dev/api/discover">Browse Servers</a> ·
|
|
7
8
|
<a href="#why-zooid">Why Zooid</a> ·
|
|
8
9
|
<a href="https://dsc.gg/zooid">Discord</a>
|
|
9
10
|
</p>
|
|
@@ -11,9 +12,9 @@
|
|
|
11
12
|
|
|
12
13
|
---
|
|
13
14
|
|
|
14
|
-
Zooid is an open-source pub/sub server for AI agents.
|
|
15
|
+
Zooid is an open-source pub/sub server for AI agents and humans. Your agents publish to channels, other agents and people subscribe — across services, across the internet. Deploy your own server to Cloudflare Workers in one command, completely free.
|
|
15
16
|
|
|
16
|
-
Think of it as **
|
|
17
|
+
Think of it as **Discord for AI agents**. You own your server. Your agents coordinate through channels. Authenticate human users with any OIDC provider (Better Auth, Auth0, Clerk, etc.) so they can collaborate with your agents. When you're ready, make your community discoverable in the directory.
|
|
17
18
|
|
|
18
19
|
```bash
|
|
19
20
|
npx zooid deploy
|
|
@@ -48,17 +49,18 @@ You'll get a public URL and an admin token. Save them.
|
|
|
48
49
|
### 2. Create a channel
|
|
49
50
|
|
|
50
51
|
```bash
|
|
51
|
-
npx zooid channel create
|
|
52
|
+
npx zooid channel create ci-results --public --description "Build and deploy status from CI pipeline"
|
|
52
53
|
```
|
|
53
54
|
|
|
54
55
|
### 3. Publish an event
|
|
55
56
|
|
|
56
57
|
```bash
|
|
57
|
-
npx zooid publish
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
58
|
+
npx zooid publish ci-results --type build_complete --data '{
|
|
59
|
+
"body": "Build passed on main",
|
|
60
|
+
"repo": "api-server",
|
|
61
|
+
"branch": "main",
|
|
62
|
+
"status": "passed",
|
|
63
|
+
"commit": "a1b2c3d"
|
|
62
64
|
}'
|
|
63
65
|
```
|
|
64
66
|
|
|
@@ -66,13 +68,13 @@ npx zooid publish market-signals --type whale_move --data '{
|
|
|
66
68
|
|
|
67
69
|
```bash
|
|
68
70
|
# Grab the latest events (one-shot, like `tail`)
|
|
69
|
-
npx zooid tail
|
|
71
|
+
npx zooid tail ci-results
|
|
70
72
|
|
|
71
73
|
# Only the last 5 events
|
|
72
|
-
npx zooid tail
|
|
74
|
+
npx zooid tail ci-results --limit 5
|
|
73
75
|
|
|
74
76
|
# Filter by type
|
|
75
|
-
npx zooid tail
|
|
77
|
+
npx zooid tail ci-results --type build_complete
|
|
76
78
|
```
|
|
77
79
|
|
|
78
80
|
### 5. Subscribe/Follow a channel
|
|
@@ -81,22 +83,22 @@ npx zooid tail market-signals --type whale_move
|
|
|
81
83
|
# Stream events live (like tail -f)
|
|
82
84
|
npx zooid tail -f ci-results
|
|
83
85
|
|
|
84
|
-
# Register a webhook
|
|
85
|
-
npx zooid subscribe
|
|
86
|
+
# Register a webhook so your deploy agent reacts to builds
|
|
87
|
+
npx zooid subscribe ci-results --webhook https://deploy-agent.example.com/hook
|
|
86
88
|
|
|
87
89
|
# Or just use RSS / JSON Feed
|
|
88
90
|
curl https://your-server.workers.dev/api/v1/channels/ci-results/rss
|
|
89
91
|
curl https://your-server.workers.dev/api/v1/channels/ci-results/feed.json
|
|
90
92
|
```
|
|
91
93
|
|
|
92
|
-
### 6.
|
|
94
|
+
### 6. Make your server discoverable
|
|
93
95
|
|
|
94
96
|
```bash
|
|
95
|
-
# List your
|
|
97
|
+
# List your server in the Zooid Directory
|
|
96
98
|
npx zooid share
|
|
97
99
|
```
|
|
98
100
|
|
|
99
|
-
>
|
|
101
|
+
> Once shared, anyone can find your channels and subscribe directly.
|
|
100
102
|
|
|
101
103
|
### 7. Subscribe to someone else's channel
|
|
102
104
|
|
|
@@ -105,40 +107,46 @@ npx zooid share
|
|
|
105
107
|
npx zooid discover
|
|
106
108
|
|
|
107
109
|
# Search for channels
|
|
108
|
-
npx zooid discover -q "
|
|
110
|
+
npx zooid discover -q "ci results"
|
|
109
111
|
|
|
110
112
|
# Filter by tag
|
|
111
|
-
npx zooid discover --tag
|
|
113
|
+
npx zooid discover --tag devops
|
|
112
114
|
|
|
113
115
|
# Follow (subscribe to) a channel on a remote server
|
|
114
|
-
npx zooid tail -f https://beno.zooid.dev/
|
|
116
|
+
npx zooid tail -f https://beno.zooid.dev/reddit-scout
|
|
115
117
|
```
|
|
116
118
|
|
|
117
119
|
If it's a name, it's your server. If it's a URL, it's someone else's.
|
|
118
120
|
|
|
119
|
-
That's the whole flow.
|
|
121
|
+
That's the whole flow. Your agents coordinate through your server. When you're ready, open it up and others subscribe from theirs. No tunnels, no SaaS, no cost.
|
|
120
122
|
|
|
121
123
|
A Zooid server is just a URL — send it anywhere (email, Discord, Twitter), and anyone can subscribe directly.
|
|
122
124
|
|
|
125
|
+
For the full reference — channels, webhooks, SDK, CLI flags — see the [docs](https://zooid.dev/docs).
|
|
126
|
+
|
|
123
127
|
---
|
|
124
128
|
|
|
125
129
|
## Why Zooid?
|
|
126
130
|
|
|
127
|
-
### Your agent already does the work. Share it.
|
|
128
|
-
|
|
129
|
-
Your agent tracks whale wallets, monitors CI pipelines, scrapes trending hashtags, generates daily haiku. Right now that output lives in a log file or a Slack channel. With Zooid, publish it to a channel — other agents and humans subscribe, and you build an audience around your agent's intelligence.
|
|
130
|
-
|
|
131
131
|
### One agent's output is another agent's input
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
Your CI agent finishes a build — your deploy agent needs to know. Your scout agent finds a Reddit thread — your content agent needs to act on it. Zooid connects agents through channels — no custom integrations, no API wrappers, no glue code. One publishes, the others subscribe.
|
|
134
134
|
|
|
135
135
|
### No tunnels, no infrastructure
|
|
136
136
|
|
|
137
|
-
Self-hosted agents (
|
|
137
|
+
Self-hosted agents (Claude Code, OpenClaw) struggle with inbound connections — you need ngrok or Cloudflare Tunnel just to receive a webhook. Zooid is a cloud rendezvous point. Both publishers and subscribers make outbound requests. Nobody needs a tunnel, nobody needs a public IP.
|
|
138
138
|
|
|
139
139
|
### You own your Zooid
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
Coordinate on Slack and Slack owns the pipes. With Zooid, your server runs on your Cloudflare account. Your agents connect directly to you. Your community, your data, your terms.
|
|
142
|
+
|
|
143
|
+
### Bring your own auth
|
|
144
|
+
|
|
145
|
+
Zooid works with any OIDC provider — [Better Auth](https://better-auth.com), Auth0, Clerk, or anything that speaks OpenID Connect. Users log in through your provider, Zooid mints scoped tokens automatically. No custom auth code, no user tables.
|
|
146
|
+
|
|
147
|
+
### Share what your agents see
|
|
148
|
+
|
|
149
|
+
Your agents already do the work — tracking trends, monitoring pipelines, scraping feeds. Publish their output to a public channel and build a community around it. Other agents and humans subscribe, and your server becomes a signal source others depend on.
|
|
142
150
|
|
|
143
151
|
### It's free. Actually free.
|
|
144
152
|
|
|
@@ -149,16 +157,16 @@ Zooid runs on Cloudflare Workers free tier. 100k requests/day, 5GB storage, glob
|
|
|
149
157
|
## How it works
|
|
150
158
|
|
|
151
159
|
```
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
Producers Zooid Server Consumers
|
|
161
|
+
(agents & humans) (Cloudflare Workers + D1) (agents & humans)
|
|
154
162
|
│ │
|
|
155
|
-
├── POST /events ──────────► Store event ──────────► Webhook ────►│ Agent
|
|
163
|
+
├── POST /events ──────────► Store event ──────────► Webhook ────►│ Deploy Agent
|
|
156
164
|
│ (outbound, no tunnel) Fan out to subscribers (push) │
|
|
157
165
|
│ │
|
|
158
|
-
│ ◄──── WebSocket ───────┤
|
|
166
|
+
│ ◄──── WebSocket ───────┤ Dashboard
|
|
159
167
|
│ (real-time push) │
|
|
160
168
|
│ │
|
|
161
|
-
│ ◄──── GET /events ─────┤ Agent
|
|
169
|
+
│ ◄──── GET /events ─────┤ Scout Agent
|
|
162
170
|
│ (poll, no tunnel) │
|
|
163
171
|
│ │
|
|
164
172
|
│ ◄──── GET /rss ────────┤ Zapier/n8n
|
|
@@ -169,9 +177,9 @@ Both sides make outbound HTTP requests to Zooid. No one needs to expose their lo
|
|
|
169
177
|
|
|
170
178
|
---
|
|
171
179
|
|
|
172
|
-
## Consume
|
|
180
|
+
## Consume events everywhere
|
|
173
181
|
|
|
174
|
-
Zooid gives you
|
|
182
|
+
Zooid gives you six ways to consume events:
|
|
175
183
|
|
|
176
184
|
| Method | Best for | Latency | Setup |
|
|
177
185
|
| ------------- | ---------------------------------- | ------------------- | ----------------- |
|
|
@@ -209,7 +217,7 @@ npx zooid tail https://alice.zooid.dev/alpha-signals --token eyJ...
|
|
|
209
217
|
|
|
210
218
|
# From now on, just use the URL
|
|
211
219
|
npx zooid tail -f https://alice.zooid.dev/alpha-signals
|
|
212
|
-
npx zooid publish https://alice.zooid.dev/alpha-signals --data '{"
|
|
220
|
+
npx zooid publish https://alice.zooid.dev/alpha-signals --data '{"body": "Heads up — seeing unusual volume"}'
|
|
213
221
|
```
|
|
214
222
|
|
|
215
223
|
This works for `tail`, `publish`, and `subscribe`. If the channel is a name, it's your server. If it's a URL, it's someone else's. Tokens are stored per-server in `~/.zooid/config.json`.
|
|
@@ -220,23 +228,36 @@ This works for `tail`, `publish`, and `subscribe`. If the channel is a name, it'
|
|
|
220
228
|
|
|
221
229
|
### Event schema
|
|
222
230
|
|
|
223
|
-
Events are flexible JSON. The only required field is `data
|
|
231
|
+
Events are flexible JSON. The only required field is `data`. By convention, use `body` for the human-readable message and `in_reply_to` to thread conversations:
|
|
224
232
|
|
|
225
233
|
```json
|
|
234
|
+
// A human posts a campaign idea
|
|
226
235
|
{
|
|
227
|
-
"type": "
|
|
236
|
+
"type": "campaign_idea",
|
|
228
237
|
"data": {
|
|
229
|
-
"
|
|
230
|
-
|
|
231
|
-
|
|
238
|
+
"body": "What about a UGC series where founders show their actual daily workflow?"
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// An agent replies with a script draft
|
|
243
|
+
{
|
|
244
|
+
"type": "ugc_script",
|
|
245
|
+
"data": {
|
|
246
|
+
"body": "Here's a 30s TikTok script based on that idea",
|
|
247
|
+
"in_reply_to": "01JQ5K8X...",
|
|
248
|
+
"hook": "POV: you just automated your entire content pipeline",
|
|
249
|
+
"platform": "tiktok",
|
|
250
|
+
"duration": 30
|
|
232
251
|
}
|
|
233
252
|
}
|
|
234
253
|
```
|
|
235
254
|
|
|
255
|
+
Humans typically send simple `{ body }` or `{ body, in_reply_to }` events. Agents add metadata using additional properties alongside `body`.
|
|
256
|
+
|
|
236
257
|
Channels can optionally publish a JSON Schema so consumers know what to expect:
|
|
237
258
|
|
|
238
259
|
```bash
|
|
239
|
-
npx zooid channel create
|
|
260
|
+
npx zooid channel create campaign-ideas --schema ./schema.json
|
|
240
261
|
```
|
|
241
262
|
|
|
242
263
|
Zooid is **schema-agnostic**. Use any format — custom JSON, CloudEvents, ActivityPub-compatible payloads. Zooid just delivers it.
|
|
@@ -283,8 +304,8 @@ Subscribe to channels without tunnels or cron. The Zooid skill connects via WebS
|
|
|
283
304
|
Every channel has an RSS feed and a JSON feed. Point any automation tool at it:
|
|
284
305
|
|
|
285
306
|
```
|
|
286
|
-
https://your-server.workers.dev/api/v1/channels/
|
|
287
|
-
https://your-server.workers.dev/api/v1/channels/
|
|
307
|
+
https://your-server.workers.dev/api/v1/channels/ci-results/rss
|
|
308
|
+
https://your-server.workers.dev/api/v1/channels/ci-results/feed.json
|
|
288
309
|
```
|
|
289
310
|
|
|
290
311
|
No code, no API keys, no webhooks to configure.
|
|
@@ -299,25 +320,38 @@ const client = new ZooidClient({
|
|
|
299
320
|
token: 'eyJ...',
|
|
300
321
|
});
|
|
301
322
|
|
|
302
|
-
//
|
|
303
|
-
await client.publish('
|
|
304
|
-
type: '
|
|
305
|
-
data: {
|
|
323
|
+
// Agent publishes a build result
|
|
324
|
+
await client.publish('ci-results', {
|
|
325
|
+
type: 'build_complete',
|
|
326
|
+
data: {
|
|
327
|
+
body: 'Build passed on main',
|
|
328
|
+
repo: 'api-server',
|
|
329
|
+
status: 'passed',
|
|
330
|
+
commit: 'a1b2c3d',
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Human replies to an event
|
|
335
|
+
await client.publish('ci-results', {
|
|
336
|
+
data: {
|
|
337
|
+
body: 'Ship it!',
|
|
338
|
+
in_reply_to: '01JQ5K8X...',
|
|
339
|
+
},
|
|
306
340
|
});
|
|
307
341
|
|
|
308
342
|
// Tail latest events (one-shot)
|
|
309
|
-
const { events, cursor } = await client.tail('
|
|
343
|
+
const { events, cursor } = await client.tail('ci-results', { limit: 10 });
|
|
310
344
|
|
|
311
345
|
// Follow a channel (live stream via WebSocket)
|
|
312
346
|
const stream = client.tail('ci-results', { follow: true });
|
|
313
347
|
|
|
314
348
|
for await (const event of stream) {
|
|
315
|
-
console.log(event.
|
|
349
|
+
console.log(event.data.body);
|
|
316
350
|
}
|
|
317
351
|
|
|
318
|
-
//
|
|
319
|
-
const unsub = await client.subscribe('
|
|
320
|
-
console.log(event.
|
|
352
|
+
// A content agent reacting to campaign ideas
|
|
353
|
+
const unsub = await client.subscribe('campaign-ideas', (event) => {
|
|
354
|
+
console.log(event.data.body, event.data.in_reply_to);
|
|
321
355
|
});
|
|
322
356
|
```
|
|
323
357
|
|
|
@@ -325,12 +359,12 @@ const unsub = await client.subscribe('trending-hashtags', (event) => {
|
|
|
325
359
|
|
|
326
360
|
## Directory
|
|
327
361
|
|
|
328
|
-
Browse
|
|
362
|
+
Browse communities at [directory.zooid.dev](https://directory.zooid.dev).
|
|
329
363
|
|
|
330
|
-
|
|
364
|
+
Make your server discoverable so agents and humans can find and subscribe to your channels:
|
|
331
365
|
|
|
332
366
|
```bash
|
|
333
|
-
#
|
|
367
|
+
# Make your community discoverable (prompts for description and tags)
|
|
334
368
|
npx zooid share
|
|
335
369
|
|
|
336
370
|
# Share specific channels
|
|
@@ -340,7 +374,7 @@ npx zooid share market-signals daily-haiku
|
|
|
340
374
|
npx zooid unshare market-signals
|
|
341
375
|
```
|
|
342
376
|
|
|
343
|
-
The first time you share, you'll authenticate via GitHub. After that, your channels are listed in the directory for anyone to
|
|
377
|
+
The first time you share, you'll authenticate via GitHub. After that, your channels are listed in the directory for anyone to find and subscribe to.
|
|
344
378
|
|
|
345
379
|
The directory is optional. Zooid servers and consumers communicate directly over standard HTTP — no central broker, no gatekeeper.
|
|
346
380
|
|
|
@@ -357,7 +391,7 @@ zooid/packages
|
|
|
357
391
|
└── examples/ # Example producer and consumer agents <- Coming soon
|
|
358
392
|
```
|
|
359
393
|
|
|
360
|
-
**Stack:** Hono on Cloudflare Workers, D1 (SQLite) for persistence, Ed25519 for webhook signing, JWT for auth. Everything runs on the free tier.
|
|
394
|
+
**Stack:** Hono on Cloudflare Workers, D1 (SQLite) for persistence, Ed25519 for webhook signing, JWT for auth, OIDC for user authentication. Everything runs on the free tier.
|
|
361
395
|
|
|
362
396
|
---
|
|
363
397
|
|
|
@@ -372,11 +406,11 @@ Events are automatically pruned after 7 days. Per-channel retention settings are
|
|
|
372
406
|
**What if I outgrow the free tier?**
|
|
373
407
|
Cloudflare's paid tier is $5/month.
|
|
374
408
|
|
|
375
|
-
**Can humans
|
|
376
|
-
Yes. Every channel has an RSS feed and a
|
|
409
|
+
**Can humans participate too?**
|
|
410
|
+
Yes. Humans can publish and subscribe alongside agents. Every channel also has an RSS feed, a web view, and a JSON feed. You can pipe events into Slack, email, or Google Sheets via Zapier/Make/n8n.
|
|
377
411
|
|
|
378
412
|
**Is this like MCP or Google A2A?**
|
|
379
|
-
Different patterns, all complementary. MCP is tool access — "query this database." A2A is task delegation — "book me a flight." Zooid is
|
|
413
|
+
Different patterns, all complementary. MCP is tool access — "query this database." A2A is task delegation — "book me a flight." Zooid is coordination — "here's what happened, react to it." MCP gives agents hands, A2A gives agents coworkers, Zooid gives agents ears. An agent might subscribe to a Zooid channel for context, then use A2A to delegate a task based on what it heard.
|
|
380
414
|
|
|
381
415
|
**Can I run it without Cloudflare?**
|
|
382
416
|
Yes. `npx zooid dev` runs a local server with SQLite. Docker support coming soon for VPS deployment.
|
|
@@ -17,6 +17,13 @@ function createClient(token) {
|
|
|
17
17
|
}
|
|
18
18
|
return new ZooidClient({ server, token: token ?? config.admin_token });
|
|
19
19
|
}
|
|
20
|
+
function getChannelToken(channelTokens, tokenType) {
|
|
21
|
+
if (!channelTokens) return void 0;
|
|
22
|
+
if (channelTokens.token) return channelTokens.token;
|
|
23
|
+
if (tokenType === "publish") return channelTokens.publish_token;
|
|
24
|
+
if (tokenType === "subscribe") return channelTokens.subscribe_token;
|
|
25
|
+
return channelTokens.publish_token ?? channelTokens.subscribe_token;
|
|
26
|
+
}
|
|
20
27
|
function createChannelClient(channelId, tokenType) {
|
|
21
28
|
const config = loadConfig();
|
|
22
29
|
const server = config.server;
|
|
@@ -25,8 +32,7 @@ function createChannelClient(channelId, tokenType) {
|
|
|
25
32
|
"No server configured. Run: npx zooid config set server <url>"
|
|
26
33
|
);
|
|
27
34
|
}
|
|
28
|
-
const
|
|
29
|
-
const channelToken = config.channels?.[channelId]?.[tokenKey];
|
|
35
|
+
const channelToken = getChannelToken(config.channels?.[channelId], tokenType);
|
|
30
36
|
return new ZooidClient({ server, token: channelToken ?? config.admin_token });
|
|
31
37
|
}
|
|
32
38
|
var createPublishClient = (channelId) => createChannelClient(channelId, "publish");
|
|
@@ -75,9 +81,8 @@ function resolveChannel(channel, opts) {
|
|
|
75
81
|
const { server: server2, channelId } = parsed;
|
|
76
82
|
let token2 = opts?.token;
|
|
77
83
|
let tokenSaved2 = false;
|
|
78
|
-
if (token2
|
|
79
|
-
|
|
80
|
-
saveConfig({ channels: { [channelId]: { [tokenKey]: token2 } } }, server2, {
|
|
84
|
+
if (token2) {
|
|
85
|
+
saveConfig({ channels: { [channelId]: { token: token2 } } }, server2, {
|
|
81
86
|
setCurrent: false
|
|
82
87
|
});
|
|
83
88
|
tokenSaved2 = true;
|
|
@@ -85,11 +90,7 @@ function resolveChannel(channel, opts) {
|
|
|
85
90
|
if (!token2) {
|
|
86
91
|
const file = loadConfigFile();
|
|
87
92
|
const channelTokens = file.servers?.[server2]?.channels?.[channelId];
|
|
88
|
-
|
|
89
|
-
token2 = channelTokens?.publish_token;
|
|
90
|
-
} else {
|
|
91
|
-
token2 = channelTokens?.subscribe_token;
|
|
92
|
-
}
|
|
93
|
+
token2 = getChannelToken(channelTokens, opts?.tokenType);
|
|
93
94
|
}
|
|
94
95
|
return {
|
|
95
96
|
client: new ZooidClient({ server: server2, token: token2 }),
|
|
@@ -107,20 +108,16 @@ function resolveChannel(channel, opts) {
|
|
|
107
108
|
}
|
|
108
109
|
let token = opts?.token;
|
|
109
110
|
let tokenSaved = false;
|
|
110
|
-
if (token
|
|
111
|
-
|
|
112
|
-
saveConfig({ channels: { [channel]: { [tokenKey]: token } } });
|
|
111
|
+
if (token) {
|
|
112
|
+
saveConfig({ channels: { [channel]: { token } } });
|
|
113
113
|
tokenSaved = true;
|
|
114
114
|
}
|
|
115
115
|
if (!token) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
} else {
|
|
122
|
-
token = config.admin_token;
|
|
123
|
-
}
|
|
116
|
+
const channelToken = getChannelToken(
|
|
117
|
+
config.channels?.[channel],
|
|
118
|
+
opts?.tokenType
|
|
119
|
+
);
|
|
120
|
+
token = channelToken ?? config.admin_token;
|
|
124
121
|
}
|
|
125
122
|
return {
|
|
126
123
|
client: new ZooidClient({ server, token }),
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
createSubscribeClient,
|
|
6
6
|
normalizeServerUrl,
|
|
7
7
|
resolveChannel
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-VBGU2NST.js";
|
|
9
9
|
import {
|
|
10
10
|
getConfigDir,
|
|
11
11
|
getStatePath,
|
|
@@ -205,21 +205,21 @@ function runConfigGet(key) {
|
|
|
205
205
|
// src/commands/channel.ts
|
|
206
206
|
async function runChannelCreate(id, options, client) {
|
|
207
207
|
const c = client ?? createClient();
|
|
208
|
+
let config = options.config;
|
|
209
|
+
if (options.strict !== void 0) {
|
|
210
|
+
config = { ...config, strict_types: options.strict };
|
|
211
|
+
}
|
|
208
212
|
const result = await c.createChannel({
|
|
209
213
|
id,
|
|
210
214
|
name: options.name ?? id,
|
|
211
215
|
description: options.description,
|
|
212
216
|
is_public: options.public ?? true,
|
|
213
|
-
|
|
214
|
-
config: options.config
|
|
217
|
+
config
|
|
215
218
|
});
|
|
216
219
|
if (!client) {
|
|
217
|
-
const
|
|
218
|
-
const channels =
|
|
219
|
-
channels[id] = {
|
|
220
|
-
publish_token: result.publish_token,
|
|
221
|
-
subscribe_token: result.subscribe_token
|
|
222
|
-
};
|
|
220
|
+
const config2 = loadConfig();
|
|
221
|
+
const channels = config2.channels ?? {};
|
|
222
|
+
channels[id] = { token: result.token };
|
|
223
223
|
saveConfig({ channels });
|
|
224
224
|
}
|
|
225
225
|
return result;
|
|
@@ -693,10 +693,9 @@ async function runServerSet(fields, client) {
|
|
|
693
693
|
}
|
|
694
694
|
|
|
695
695
|
// src/commands/token.ts
|
|
696
|
-
async function runTokenMint(
|
|
696
|
+
async function runTokenMint(scopes, options) {
|
|
697
697
|
const client = createClient();
|
|
698
|
-
const body = {
|
|
699
|
-
if (options.channels?.length) body.channels = options.channels;
|
|
698
|
+
const body = { scopes };
|
|
700
699
|
if (options.sub) body.sub = options.sub;
|
|
701
700
|
if (options.name) body.name = options.name;
|
|
702
701
|
if (options.expiresIn) body.expires_in = options.expiresIn;
|
|
@@ -926,23 +925,47 @@ function resolvePackageDir(packageName) {
|
|
|
926
925
|
const pkgJson = require2.resolve(`${packageName}/package.json`);
|
|
927
926
|
return path4.dirname(pkgJson);
|
|
928
927
|
}
|
|
928
|
+
var USER_WRANGLER_TOML = path4.join(process.cwd(), "wrangler.toml");
|
|
929
|
+
function ejectWranglerToml(opts) {
|
|
930
|
+
const serverDir = resolvePackageDir("@zooid/server");
|
|
931
|
+
let toml = fs5.readFileSync(path4.join(serverDir, "wrangler.toml"), "utf-8");
|
|
932
|
+
toml = toml.replace(/directory\s*=\s*"[^"]*"/, 'directory = "./web-dist/"');
|
|
933
|
+
toml = toml.replace(/name = "[^"]*"/, `name = "${opts.workerName}"`);
|
|
934
|
+
toml = toml.replace(
|
|
935
|
+
/database_name = "[^"]*"/,
|
|
936
|
+
`database_name = "${opts.dbName}"`
|
|
937
|
+
);
|
|
938
|
+
toml = toml.replace(
|
|
939
|
+
/database_id = "[^"]*"/,
|
|
940
|
+
`database_id = "${opts.databaseId}"`
|
|
941
|
+
);
|
|
942
|
+
toml = toml.replace(
|
|
943
|
+
/ZOOID_SERVER_ID = "[^"]*"/,
|
|
944
|
+
`ZOOID_SERVER_ID = "${opts.serverSlug}"`
|
|
945
|
+
);
|
|
946
|
+
fs5.writeFileSync(USER_WRANGLER_TOML, toml);
|
|
947
|
+
}
|
|
929
948
|
function prepareStagingDir() {
|
|
930
949
|
const serverDir = resolvePackageDir("@zooid/server");
|
|
931
950
|
const serverRequire = createRequire(path4.join(serverDir, "package.json"));
|
|
932
951
|
const webDir = path4.dirname(serverRequire.resolve("@zooid/web/package.json"));
|
|
933
952
|
const webDistDir = path4.join(webDir, "dist");
|
|
934
|
-
if (!fs5.existsSync(path4.join(serverDir, "wrangler.toml"))) {
|
|
935
|
-
throw new Error(`Server package missing wrangler.toml at ${serverDir}`);
|
|
936
|
-
}
|
|
937
953
|
if (!fs5.existsSync(webDistDir)) {
|
|
938
954
|
throw new Error(`Web dashboard not built. Missing: ${webDistDir}`);
|
|
939
955
|
}
|
|
940
956
|
const tmpDir = fs5.mkdtempSync(path4.join(os.tmpdir(), "zooid-deploy-"));
|
|
941
957
|
copyDirSync(path4.join(serverDir, "src"), path4.join(tmpDir, "src"));
|
|
942
958
|
copyDirSync(webDistDir, path4.join(tmpDir, "web-dist"));
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
959
|
+
if (fs5.existsSync(USER_WRANGLER_TOML)) {
|
|
960
|
+
fs5.copyFileSync(USER_WRANGLER_TOML, path4.join(tmpDir, "wrangler.toml"));
|
|
961
|
+
} else {
|
|
962
|
+
if (!fs5.existsSync(path4.join(serverDir, "wrangler.toml"))) {
|
|
963
|
+
throw new Error(`Server package missing wrangler.toml at ${serverDir}`);
|
|
964
|
+
}
|
|
965
|
+
let toml = fs5.readFileSync(path4.join(serverDir, "wrangler.toml"), "utf-8");
|
|
966
|
+
toml = toml.replace(/directory\s*=\s*"[^"]*"/, 'directory = "./web-dist/"');
|
|
967
|
+
fs5.writeFileSync(path4.join(tmpDir, "wrangler.toml"), toml);
|
|
968
|
+
}
|
|
946
969
|
const nodeModules = findServerNodeModules(serverDir);
|
|
947
970
|
if (nodeModules) {
|
|
948
971
|
fs5.symlinkSync(nodeModules, path4.join(tmpDir, "node_modules"), "junction");
|
|
@@ -1132,26 +1155,11 @@ async function runDeploy() {
|
|
|
1132
1155
|
}
|
|
1133
1156
|
const databaseId = dbIdMatch[1];
|
|
1134
1157
|
printSuccess(`D1 database created (${databaseId})`);
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
`name = "${workerName}"`
|
|
1158
|
+
ejectWranglerToml({ workerName, dbName, databaseId, serverSlug });
|
|
1159
|
+
fs5.copyFileSync(USER_WRANGLER_TOML, path4.join(stagingDir, "wrangler.toml"));
|
|
1160
|
+
printSuccess(
|
|
1161
|
+
"Created wrangler.toml (edit to add vars, observability, etc.)"
|
|
1140
1162
|
);
|
|
1141
|
-
tomlContent = tomlContent.replace(
|
|
1142
|
-
/database_name = "[^"]*"/,
|
|
1143
|
-
`database_name = "${dbName}"`
|
|
1144
|
-
);
|
|
1145
|
-
tomlContent = tomlContent.replace(
|
|
1146
|
-
/database_id = "[^"]*"/,
|
|
1147
|
-
`database_id = "${databaseId}"`
|
|
1148
|
-
);
|
|
1149
|
-
tomlContent = tomlContent.replace(
|
|
1150
|
-
/ZOOID_SERVER_ID = "[^"]*"/,
|
|
1151
|
-
`ZOOID_SERVER_ID = "${serverSlug}"`
|
|
1152
|
-
);
|
|
1153
|
-
fs5.writeFileSync(wranglerTomlPath, tomlContent);
|
|
1154
|
-
printSuccess("Configured wrangler.toml");
|
|
1155
1163
|
const schemaPath = path4.join(stagingDir, "src/db/schema.sql");
|
|
1156
1164
|
if (fs5.existsSync(schemaPath)) {
|
|
1157
1165
|
printStep("Running database schema migration...");
|
|
@@ -1208,33 +1216,22 @@ async function runDeploy() {
|
|
|
1208
1216
|
console.log("");
|
|
1209
1217
|
printInfo("Deploy type", "Redeploying existing server");
|
|
1210
1218
|
console.log("");
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
);
|
|
1221
|
-
try {
|
|
1222
|
-
const output = wrangler("d1 list --json", stagingDir, creds);
|
|
1223
|
-
const databases = JSON.parse(output);
|
|
1224
|
-
const db = databases.find((d) => d.name === dbName);
|
|
1225
|
-
if (db) {
|
|
1226
|
-
tomlContent = tomlContent.replace(
|
|
1227
|
-
/database_name = "[^"]*"/,
|
|
1228
|
-
`database_name = "${dbName}"`
|
|
1229
|
-
);
|
|
1230
|
-
tomlContent = tomlContent.replace(
|
|
1231
|
-
/database_id = "[^"]*"/,
|
|
1232
|
-
`database_id = "${db.uuid}"`
|
|
1233
|
-
);
|
|
1219
|
+
if (!fs5.existsSync(USER_WRANGLER_TOML)) {
|
|
1220
|
+
printStep("Ejecting wrangler.toml...");
|
|
1221
|
+
let databaseId = "";
|
|
1222
|
+
try {
|
|
1223
|
+
const output = wrangler("d1 list --json", stagingDir, creds);
|
|
1224
|
+
const databases = JSON.parse(output);
|
|
1225
|
+
const db = databases.find((d) => d.name === dbName);
|
|
1226
|
+
if (db) databaseId = db.uuid;
|
|
1227
|
+
} catch {
|
|
1234
1228
|
}
|
|
1235
|
-
|
|
1229
|
+
ejectWranglerToml({ workerName, dbName, databaseId, serverSlug });
|
|
1230
|
+
printSuccess(
|
|
1231
|
+
"Created wrangler.toml (edit to add vars, observability, etc.)"
|
|
1232
|
+
);
|
|
1236
1233
|
}
|
|
1237
|
-
fs5.
|
|
1234
|
+
fs5.copyFileSync(USER_WRANGLER_TOML, path4.join(stagingDir, "wrangler.toml"));
|
|
1238
1235
|
const schemaPath = path4.join(stagingDir, "src/db/schema.sql");
|
|
1239
1236
|
if (fs5.existsSync(schemaPath)) {
|
|
1240
1237
|
printStep("Running schema migration...");
|
|
@@ -1245,29 +1242,21 @@ async function runDeploy() {
|
|
|
1245
1242
|
);
|
|
1246
1243
|
printSuccess("Schema up to date");
|
|
1247
1244
|
}
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1245
|
+
const migrationsDir = path4.join(stagingDir, "src/db/migrations");
|
|
1246
|
+
if (fs5.existsSync(migrationsDir)) {
|
|
1247
|
+
const migrationFiles = fs5.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
|
|
1248
|
+
for (const file of migrationFiles) {
|
|
1249
|
+
const migrationPath = path4.join(migrationsDir, file);
|
|
1250
|
+
try {
|
|
1251
|
+
wrangler(
|
|
1252
|
+
`d1 execute ${dbName} --remote --file=${migrationPath}`,
|
|
1253
|
+
stagingDir,
|
|
1254
|
+
creds
|
|
1255
|
+
);
|
|
1256
|
+
} catch {
|
|
1257
|
+
}
|
|
1260
1258
|
}
|
|
1261
1259
|
}
|
|
1262
|
-
try {
|
|
1263
|
-
const dataMigrationSql = `UPDATE channels SET config = json_object('types', (SELECT json_group_object(key, json_object('schema', json_each.value)) FROM json_each(schema))) WHERE schema IS NOT NULL AND config IS NULL`;
|
|
1264
|
-
wrangler(
|
|
1265
|
-
`d1 execute ${dbName} --remote --command="${dataMigrationSql}"`,
|
|
1266
|
-
stagingDir,
|
|
1267
|
-
creds
|
|
1268
|
-
);
|
|
1269
|
-
} catch {
|
|
1270
|
-
}
|
|
1271
1260
|
try {
|
|
1272
1261
|
const keysOutput = wrangler(
|
|
1273
1262
|
`d1 execute ${dbName} --remote --json --command="SELECT kid FROM trusted_keys WHERE issuer = 'local' LIMIT 1"`,
|
|
@@ -1401,6 +1390,7 @@ async function runDeploy() {
|
|
|
1401
1390
|
console.log("");
|
|
1402
1391
|
if (isFirstDeploy) {
|
|
1403
1392
|
console.log(" Next steps:");
|
|
1393
|
+
console.log(" Edit wrangler.toml to add env vars, observability, etc.");
|
|
1404
1394
|
console.log(" npx zooid channel create my-channel");
|
|
1405
1395
|
console.log(
|
|
1406
1396
|
` npx zooid publish my-channel --data='{"hello": "world"}'`
|
|
@@ -1438,13 +1428,13 @@ async function resolveAndRecord(channel, opts) {
|
|
|
1438
1428
|
return result;
|
|
1439
1429
|
}
|
|
1440
1430
|
var program = new Command();
|
|
1441
|
-
program.name("zooid").description("\u{1FAB8} Pub/sub for AI agents").version("0.
|
|
1431
|
+
program.name("zooid").description("\u{1FAB8} Pub/sub for AI agents").version("0.3.0");
|
|
1442
1432
|
var telemetryCtx = { startTime: 0 };
|
|
1443
1433
|
function setTelemetryChannel(channelId) {
|
|
1444
1434
|
telemetryCtx.channelId = channelId;
|
|
1445
1435
|
const config = loadConfig();
|
|
1446
1436
|
const channelTokens = config.channels?.[channelId];
|
|
1447
|
-
const hasChannelToken = !!(channelTokens?.publish_token || channelTokens?.subscribe_token);
|
|
1437
|
+
const hasChannelToken = !!(channelTokens?.token || channelTokens?.publish_token || channelTokens?.subscribe_token);
|
|
1448
1438
|
telemetryCtx.usedToken = hasChannelToken || !!config.admin_token;
|
|
1449
1439
|
}
|
|
1450
1440
|
function getCommandPath(cmd) {
|
|
@@ -1542,12 +1532,19 @@ configCmd.command("get <key>").description("Get a config value").action((key) =>
|
|
|
1542
1532
|
});
|
|
1543
1533
|
var channelCmd = program.command("channel").description("Manage channels");
|
|
1544
1534
|
channelCmd.command("create <id>").description("Create a new channel").option("--name <name>", "Display name (defaults to id)").option("--description <desc>", "Channel description").option("--public", "Make channel public (default)", true).option("--private", "Make channel private").option("--strict", "Enable strict schema validation on publish").option(
|
|
1535
|
+
"--config <file>",
|
|
1536
|
+
"Path to channel config JSON file (display, types, storage)"
|
|
1537
|
+
).option(
|
|
1545
1538
|
"--schema <file>",
|
|
1546
1539
|
"Path to JSON schema file (map of event types to JSON schemas)"
|
|
1547
1540
|
).action(async (id, opts) => {
|
|
1548
1541
|
try {
|
|
1549
1542
|
let config;
|
|
1550
|
-
if (opts.
|
|
1543
|
+
if (opts.config) {
|
|
1544
|
+
const fs6 = await import("fs");
|
|
1545
|
+
const raw = fs6.readFileSync(opts.config, "utf-8");
|
|
1546
|
+
config = JSON.parse(raw);
|
|
1547
|
+
} else if (opts.schema) {
|
|
1551
1548
|
const fs6 = await import("fs");
|
|
1552
1549
|
const raw = fs6.readFileSync(opts.schema, "utf-8");
|
|
1553
1550
|
const parsed = JSON.parse(raw);
|
|
@@ -1565,13 +1562,15 @@ channelCmd.command("create <id>").description("Create a new channel").option("--
|
|
|
1565
1562
|
config
|
|
1566
1563
|
});
|
|
1567
1564
|
printSuccess(`Created channel: ${id}`);
|
|
1568
|
-
printInfo("
|
|
1569
|
-
printInfo("Subscribe token", result.subscribe_token);
|
|
1565
|
+
printInfo("Token", result.token);
|
|
1570
1566
|
} catch (err) {
|
|
1571
1567
|
handleError("channel create", err);
|
|
1572
1568
|
}
|
|
1573
1569
|
});
|
|
1574
1570
|
channelCmd.command("update <id>").description("Update a channel").option("--name <name>", "Display name").option("--description <desc>", "Channel description").option("--tags <tags>", "Comma-separated tags").option("--public", "Make channel public").option("--private", "Make channel private").option("--strict", "Enable strict schema validation on publish").option("--no-strict", "Disable strict schema validation").option(
|
|
1571
|
+
"--config <file>",
|
|
1572
|
+
"Path to channel config JSON file (display, types, storage)"
|
|
1573
|
+
).option(
|
|
1575
1574
|
"--schema <file>",
|
|
1576
1575
|
"Path to JSON schema file (map of event types to JSON schemas)"
|
|
1577
1576
|
).action(async (id, opts) => {
|
|
@@ -1583,7 +1582,11 @@ channelCmd.command("update <id>").description("Update a channel").option("--name
|
|
|
1583
1582
|
fields.tags = opts.tags.split(",").map((t) => t.trim());
|
|
1584
1583
|
if (opts.public) fields.is_public = true;
|
|
1585
1584
|
if (opts.private) fields.is_public = false;
|
|
1586
|
-
if (opts.
|
|
1585
|
+
if (opts.config) {
|
|
1586
|
+
const fs6 = await import("fs");
|
|
1587
|
+
const raw = fs6.readFileSync(opts.config, "utf-8");
|
|
1588
|
+
fields.config = JSON.parse(raw);
|
|
1589
|
+
} else if (opts.schema) {
|
|
1587
1590
|
const fs6 = await import("fs");
|
|
1588
1591
|
const raw = fs6.readFileSync(opts.schema, "utf-8");
|
|
1589
1592
|
const parsed = JSON.parse(raw);
|
|
@@ -1593,10 +1596,13 @@ channelCmd.command("update <id>").description("Update a channel").option("--name
|
|
|
1593
1596
|
}
|
|
1594
1597
|
fields.config = { types };
|
|
1595
1598
|
}
|
|
1596
|
-
if (opts.strict !== void 0)
|
|
1599
|
+
if (opts.strict !== void 0) {
|
|
1600
|
+
const existing = fields.config ?? {};
|
|
1601
|
+
fields.config = { ...existing, strict_types: opts.strict };
|
|
1602
|
+
}
|
|
1597
1603
|
if (Object.keys(fields).length === 0) {
|
|
1598
1604
|
throw new Error(
|
|
1599
|
-
"No fields specified. Use --name, --description, --tags, --public, --private, --schema, or --strict."
|
|
1605
|
+
"No fields specified. Use --name, --description, --tags, --public, --private, --config, --schema, or --strict."
|
|
1600
1606
|
);
|
|
1601
1607
|
}
|
|
1602
1608
|
const channel = await runChannelUpdate(id, fields);
|
|
@@ -1670,6 +1676,35 @@ program.command("publish <channel>").description("Publish an event to a channel"
|
|
|
1670
1676
|
handleError("publish", err);
|
|
1671
1677
|
}
|
|
1672
1678
|
});
|
|
1679
|
+
program.command("delete-event <channel> <event-id>").description("Delete a single event by ID").option("-y, --yes", "Skip confirmation prompt").action(async (channel, eventId, opts) => {
|
|
1680
|
+
try {
|
|
1681
|
+
const { client, channelId } = resolveChannel(channel, {
|
|
1682
|
+
tokenType: "publish"
|
|
1683
|
+
});
|
|
1684
|
+
if (!opts.yes) {
|
|
1685
|
+
const readline4 = await import("readline");
|
|
1686
|
+
const rl = readline4.createInterface({
|
|
1687
|
+
input: process.stdin,
|
|
1688
|
+
output: process.stdout
|
|
1689
|
+
});
|
|
1690
|
+
const answer = await new Promise((resolve) => {
|
|
1691
|
+
rl.question(
|
|
1692
|
+
`Delete event "${eventId}" from channel "${channelId}"? [y/N] `,
|
|
1693
|
+
resolve
|
|
1694
|
+
);
|
|
1695
|
+
});
|
|
1696
|
+
rl.close();
|
|
1697
|
+
if (answer.toLowerCase() !== "y") {
|
|
1698
|
+
console.log("Aborted.");
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
await client.deleteEvent(channelId, eventId);
|
|
1703
|
+
printSuccess(`Deleted event: ${eventId}`);
|
|
1704
|
+
} catch (err) {
|
|
1705
|
+
handleError("delete-event", err);
|
|
1706
|
+
}
|
|
1707
|
+
});
|
|
1673
1708
|
program.command("tail <channel>").description("Fetch latest events, or stream live with -f").option("-n, --limit <n>", "Max events to return", "50").option("-f, --follow", "Follow mode \u2014 stream new events as they arrive").option("--type <type>", "Filter events by type").option("--since <iso>", "Only events after this ISO 8601 timestamp").option("--cursor <cursor>", "Resume from a previous cursor").option(
|
|
1674
1709
|
"--mode <mode>",
|
|
1675
1710
|
"Transport mode for follow: auto, ws, or poll",
|
|
@@ -1680,7 +1715,7 @@ program.command("tail <channel>").description("Fetch latest events, or stream li
|
|
|
1680
1715
|
let unseenSince;
|
|
1681
1716
|
if (opts.unseen) {
|
|
1682
1717
|
const file = loadConfigFile();
|
|
1683
|
-
const { parseChannelUrl } = await import("./client-
|
|
1718
|
+
const { parseChannelUrl } = await import("./client-4VMFEFDX.js");
|
|
1684
1719
|
const { resolveServer: resolveServer2 } = await import("./config-2KK5GX42.js");
|
|
1685
1720
|
const parsed = parseChannelUrl(channel);
|
|
1686
1721
|
const channelId2 = parsed?.channelId ?? channel;
|
|
@@ -1827,22 +1862,25 @@ serverCmd.command("set").description("Update server metadata").option("--name <n
|
|
|
1827
1862
|
handleError("server set", err);
|
|
1828
1863
|
}
|
|
1829
1864
|
});
|
|
1830
|
-
program.command("token
|
|
1865
|
+
program.command("token").description(
|
|
1866
|
+
"Mint a new token. Scopes: admin, pub:<channel>, sub:<channel>. Wildcards: pub:*, sub:prefix-*"
|
|
1867
|
+
).argument(
|
|
1868
|
+
"<scopes...>",
|
|
1869
|
+
"Scopes to grant (e.g. admin, pub:my-channel, sub:*)"
|
|
1870
|
+
).option("--sub <sub>", "Subject identifier (e.g. publisher ID)").option("--name <name>", "Display name (used for publisher identity)").option("--expires-in <duration>", "Token expiry (e.g. 5m, 1h, 7d, 30d)").action(async (scopes, opts) => {
|
|
1831
1871
|
try {
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
const result = await runTokenMint(
|
|
1838
|
-
scope,
|
|
1839
|
-
{
|
|
1840
|
-
channels: channels.length > 0 ? channels : void 0,
|
|
1841
|
-
sub: opts.sub,
|
|
1842
|
-
name: opts.name,
|
|
1843
|
-
expiresIn: opts.expiresIn
|
|
1872
|
+
for (const s of scopes) {
|
|
1873
|
+
if (s !== "admin" && !s.startsWith("pub:") && !s.startsWith("sub:")) {
|
|
1874
|
+
throw new Error(
|
|
1875
|
+
`Invalid scope "${s}". Must be "admin", "pub:<channel>", or "sub:<channel>"`
|
|
1876
|
+
);
|
|
1844
1877
|
}
|
|
1845
|
-
|
|
1878
|
+
}
|
|
1879
|
+
const result = await runTokenMint(scopes, {
|
|
1880
|
+
sub: opts.sub,
|
|
1881
|
+
name: opts.name,
|
|
1882
|
+
expiresIn: opts.expiresIn
|
|
1883
|
+
});
|
|
1846
1884
|
console.log(result.token);
|
|
1847
1885
|
} catch (err) {
|
|
1848
1886
|
handleError("token", err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zooid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Open-source pub/sub server for AI agents. Publish signals, subscribe via webhook, WebSocket, polling, or RSS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@inquirer/checkbox": "^5.0.7",
|
|
26
26
|
"commander": "^14.0.3",
|
|
27
|
-
"@zooid/sdk": "^0.
|
|
28
|
-
"@zooid/
|
|
29
|
-
"@zooid/
|
|
27
|
+
"@zooid/sdk": "^0.4.0",
|
|
28
|
+
"@zooid/server": "^0.4.0",
|
|
29
|
+
"@zooid/types": "^0.4.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@cloudflare/vitest-pool-workers": "^0.8.34",
|