zooid 0.1.0 → 0.2.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/dist/index.js +196 -34
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -465,7 +465,7 @@ async function runChannelCreate(id, options, client) {
|
|
|
465
465
|
description: options.description,
|
|
466
466
|
is_public: options.public ?? true,
|
|
467
467
|
strict: options.strict,
|
|
468
|
-
|
|
468
|
+
config: options.config
|
|
469
469
|
});
|
|
470
470
|
if (!client) {
|
|
471
471
|
const config = loadConfig();
|
|
@@ -482,10 +482,6 @@ async function runChannelList(client) {
|
|
|
482
482
|
const c = client ?? createClient();
|
|
483
483
|
return c.listChannels();
|
|
484
484
|
}
|
|
485
|
-
async function runChannelAddPublisher(channelId, name, client) {
|
|
486
|
-
const c = client ?? createClient();
|
|
487
|
-
return c.addPublisher(channelId, name);
|
|
488
|
-
}
|
|
489
485
|
async function runChannelUpdate(channelId, options, client) {
|
|
490
486
|
const c = client ?? createClient();
|
|
491
487
|
return c.updateChannel(channelId, options);
|
|
@@ -637,6 +633,9 @@ function printError(message) {
|
|
|
637
633
|
function printInfo(label, value) {
|
|
638
634
|
console.log(` ${label}: ${value}`);
|
|
639
635
|
}
|
|
636
|
+
function printStep(message) {
|
|
637
|
+
console.log(` ${message}`);
|
|
638
|
+
}
|
|
640
639
|
function formatRelative(isoString) {
|
|
641
640
|
const diff = Date.now() - new Date(isoString).getTime();
|
|
642
641
|
const minutes = Math.floor(diff / 6e4);
|
|
@@ -947,6 +946,17 @@ async function runServerSet(fields, client) {
|
|
|
947
946
|
return c.updateServerMeta(fields);
|
|
948
947
|
}
|
|
949
948
|
|
|
949
|
+
// src/commands/token.ts
|
|
950
|
+
async function runTokenMint(scope, options) {
|
|
951
|
+
const client = createClient();
|
|
952
|
+
const body = { scope };
|
|
953
|
+
if (options.channels?.length) body.channels = options.channels;
|
|
954
|
+
if (options.sub) body.sub = options.sub;
|
|
955
|
+
if (options.name) body.name = options.name;
|
|
956
|
+
if (options.expiresIn) body.expires_in = options.expiresIn;
|
|
957
|
+
return client.mintToken(body);
|
|
958
|
+
}
|
|
959
|
+
|
|
950
960
|
// src/commands/dev.ts
|
|
951
961
|
import { execSync, spawn } from "child_process";
|
|
952
962
|
import crypto2 from "crypto";
|
|
@@ -986,6 +996,34 @@ async function createAdminToken(secret) {
|
|
|
986
996
|
const signature = base64url(Buffer.from(sig));
|
|
987
997
|
return `${data}.${signature}`;
|
|
988
998
|
}
|
|
999
|
+
async function createEdDSAAdminToken(privateKeyJwk, kid) {
|
|
1000
|
+
const header = base64url(
|
|
1001
|
+
Buffer.from(JSON.stringify({ alg: "EdDSA", typ: "JWT", kid }))
|
|
1002
|
+
);
|
|
1003
|
+
const payload = base64url(
|
|
1004
|
+
Buffer.from(
|
|
1005
|
+
JSON.stringify({
|
|
1006
|
+
scope: "admin",
|
|
1007
|
+
iat: Math.floor(Date.now() / 1e3)
|
|
1008
|
+
})
|
|
1009
|
+
)
|
|
1010
|
+
);
|
|
1011
|
+
const message = `${header}.${payload}`;
|
|
1012
|
+
const privateKey = await crypto.subtle.importKey(
|
|
1013
|
+
"jwk",
|
|
1014
|
+
privateKeyJwk,
|
|
1015
|
+
{ name: "Ed25519" },
|
|
1016
|
+
false,
|
|
1017
|
+
["sign"]
|
|
1018
|
+
);
|
|
1019
|
+
const sig = await crypto.subtle.sign(
|
|
1020
|
+
"Ed25519",
|
|
1021
|
+
privateKey,
|
|
1022
|
+
new TextEncoder().encode(message)
|
|
1023
|
+
);
|
|
1024
|
+
const signature = base64url(Buffer.from(sig));
|
|
1025
|
+
return `${message}.${signature}`;
|
|
1026
|
+
}
|
|
989
1027
|
|
|
990
1028
|
// src/commands/dev.ts
|
|
991
1029
|
function findServerDir() {
|
|
@@ -1337,7 +1375,7 @@ async function runDeploy() {
|
|
|
1337
1375
|
console.log("");
|
|
1338
1376
|
printInfo("Deploy type", "First deploy \u2014 setting up database and secrets");
|
|
1339
1377
|
console.log("");
|
|
1340
|
-
|
|
1378
|
+
printStep(`Creating D1 database (${dbName})...`);
|
|
1341
1379
|
const d1Output = wrangler(`d1 create ${dbName}`, stagingDir, creds);
|
|
1342
1380
|
const dbIdMatch = d1Output.match(/database_id\s*=\s*"([^"]+)"/);
|
|
1343
1381
|
if (!dbIdMatch) {
|
|
@@ -1370,7 +1408,7 @@ async function runDeploy() {
|
|
|
1370
1408
|
printSuccess("Configured wrangler.toml");
|
|
1371
1409
|
const schemaPath = path5.join(stagingDir, "src/db/schema.sql");
|
|
1372
1410
|
if (fs6.existsSync(schemaPath)) {
|
|
1373
|
-
|
|
1411
|
+
printStep("Running database schema migration...");
|
|
1374
1412
|
wrangler(
|
|
1375
1413
|
`d1 execute ${dbName} --remote --file=${schemaPath}`,
|
|
1376
1414
|
stagingDir,
|
|
@@ -1378,8 +1416,7 @@ async function runDeploy() {
|
|
|
1378
1416
|
);
|
|
1379
1417
|
printSuccess("Database schema initialized");
|
|
1380
1418
|
}
|
|
1381
|
-
|
|
1382
|
-
const jwtSecret = crypto3.randomBytes(32).toString("base64");
|
|
1419
|
+
printStep("Generating secrets...");
|
|
1383
1420
|
const keyPair = await crypto3.subtle.generateKey("Ed25519", true, [
|
|
1384
1421
|
"sign",
|
|
1385
1422
|
"verify"
|
|
@@ -1392,12 +1429,16 @@ async function runDeploy() {
|
|
|
1392
1429
|
"raw",
|
|
1393
1430
|
keyPair.publicKey
|
|
1394
1431
|
);
|
|
1432
|
+
const privateKeyJwk = await crypto3.subtle.exportKey(
|
|
1433
|
+
"jwk",
|
|
1434
|
+
keyPair.privateKey
|
|
1435
|
+
);
|
|
1436
|
+
const publicKeyJwk = await crypto3.subtle.exportKey(
|
|
1437
|
+
"jwk",
|
|
1438
|
+
keyPair.publicKey
|
|
1439
|
+
);
|
|
1395
1440
|
const privateKeyB64 = Buffer.from(privateKeyRaw).toString("base64");
|
|
1396
1441
|
const publicKeyB64 = Buffer.from(publicKeyRaw).toString("base64");
|
|
1397
|
-
wrangler("secret put ZOOID_JWT_SECRET", stagingDir, creds, {
|
|
1398
|
-
input: jwtSecret
|
|
1399
|
-
});
|
|
1400
|
-
printSuccess("Set ZOOID_JWT_SECRET");
|
|
1401
1442
|
wrangler("secret put ZOOID_SIGNING_KEY", stagingDir, creds, {
|
|
1402
1443
|
input: privateKeyB64
|
|
1403
1444
|
});
|
|
@@ -1406,8 +1447,17 @@ async function runDeploy() {
|
|
|
1406
1447
|
input: publicKeyB64
|
|
1407
1448
|
});
|
|
1408
1449
|
printSuccess("Set ZOOID_PUBLIC_KEY (Ed25519 public)");
|
|
1409
|
-
|
|
1410
|
-
|
|
1450
|
+
const kid = "local-1";
|
|
1451
|
+
const xValue = publicKeyJwk.x;
|
|
1452
|
+
const insertSql = `INSERT INTO trusted_keys (kid, x, issuer) VALUES ('${kid}', '${xValue}', 'local');`;
|
|
1453
|
+
wrangler(
|
|
1454
|
+
`d1 execute ${dbName} --remote --command="${insertSql}"`,
|
|
1455
|
+
stagingDir,
|
|
1456
|
+
creds
|
|
1457
|
+
);
|
|
1458
|
+
printSuccess(`Registered EdDSA public key (kid: ${kid})`);
|
|
1459
|
+
adminToken = await createEdDSAAdminToken(privateKeyJwk, kid);
|
|
1460
|
+
printSuccess("EdDSA admin token generated");
|
|
1411
1461
|
} else {
|
|
1412
1462
|
console.log("");
|
|
1413
1463
|
printInfo("Deploy type", "Redeploying existing server");
|
|
@@ -1439,8 +1489,99 @@ async function runDeploy() {
|
|
|
1439
1489
|
} catch {
|
|
1440
1490
|
}
|
|
1441
1491
|
fs6.writeFileSync(wranglerTomlPath, tomlContent);
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1492
|
+
const schemaPath = path5.join(stagingDir, "src/db/schema.sql");
|
|
1493
|
+
if (fs6.existsSync(schemaPath)) {
|
|
1494
|
+
printStep("Running schema migration...");
|
|
1495
|
+
wrangler(
|
|
1496
|
+
`d1 execute ${dbName} --remote --file=${schemaPath}`,
|
|
1497
|
+
stagingDir,
|
|
1498
|
+
creds
|
|
1499
|
+
);
|
|
1500
|
+
printSuccess("Schema up to date");
|
|
1501
|
+
}
|
|
1502
|
+
const migrations = [
|
|
1503
|
+
"ALTER TABLE events ADD COLUMN publisher_name TEXT",
|
|
1504
|
+
"ALTER TABLE channels ADD COLUMN config TEXT"
|
|
1505
|
+
];
|
|
1506
|
+
for (const sql of migrations) {
|
|
1507
|
+
try {
|
|
1508
|
+
wrangler(
|
|
1509
|
+
`d1 execute ${dbName} --remote --command="${sql}"`,
|
|
1510
|
+
stagingDir,
|
|
1511
|
+
creds
|
|
1512
|
+
);
|
|
1513
|
+
} catch {
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
try {
|
|
1517
|
+
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`;
|
|
1518
|
+
wrangler(
|
|
1519
|
+
`d1 execute ${dbName} --remote --command="${dataMigrationSql}"`,
|
|
1520
|
+
stagingDir,
|
|
1521
|
+
creds
|
|
1522
|
+
);
|
|
1523
|
+
} catch {
|
|
1524
|
+
}
|
|
1525
|
+
try {
|
|
1526
|
+
const keysOutput = wrangler(
|
|
1527
|
+
`d1 execute ${dbName} --remote --json --command="SELECT kid FROM trusted_keys WHERE issuer = 'local' LIMIT 1"`,
|
|
1528
|
+
stagingDir,
|
|
1529
|
+
creds
|
|
1530
|
+
);
|
|
1531
|
+
const keysResult = JSON.parse(keysOutput);
|
|
1532
|
+
const hasLocalKey = keysResult?.[0]?.results?.length > 0;
|
|
1533
|
+
if (!hasLocalKey) {
|
|
1534
|
+
printStep("Upgrading to EdDSA auth...");
|
|
1535
|
+
const keyPair = await crypto3.subtle.generateKey("Ed25519", true, [
|
|
1536
|
+
"sign",
|
|
1537
|
+
"verify"
|
|
1538
|
+
]);
|
|
1539
|
+
const privateKeyRaw = await crypto3.subtle.exportKey(
|
|
1540
|
+
"pkcs8",
|
|
1541
|
+
keyPair.privateKey
|
|
1542
|
+
);
|
|
1543
|
+
const publicKeyRaw = await crypto3.subtle.exportKey(
|
|
1544
|
+
"raw",
|
|
1545
|
+
keyPair.publicKey
|
|
1546
|
+
);
|
|
1547
|
+
const privateKeyJwk = await crypto3.subtle.exportKey(
|
|
1548
|
+
"jwk",
|
|
1549
|
+
keyPair.privateKey
|
|
1550
|
+
);
|
|
1551
|
+
const publicKeyJwk = await crypto3.subtle.exportKey(
|
|
1552
|
+
"jwk",
|
|
1553
|
+
keyPair.publicKey
|
|
1554
|
+
);
|
|
1555
|
+
const privateKeyB64 = Buffer.from(privateKeyRaw).toString("base64");
|
|
1556
|
+
const publicKeyB64 = Buffer.from(publicKeyRaw).toString("base64");
|
|
1557
|
+
wrangler("secret put ZOOID_SIGNING_KEY", stagingDir, creds, {
|
|
1558
|
+
input: privateKeyB64
|
|
1559
|
+
});
|
|
1560
|
+
wrangler("secret put ZOOID_PUBLIC_KEY", stagingDir, creds, {
|
|
1561
|
+
input: publicKeyB64
|
|
1562
|
+
});
|
|
1563
|
+
const kid = "local-1";
|
|
1564
|
+
const xValue = publicKeyJwk.x;
|
|
1565
|
+
const insertSql = `INSERT INTO trusted_keys (kid, x, issuer) VALUES ('${kid}', '${xValue}', 'local');`;
|
|
1566
|
+
wrangler(
|
|
1567
|
+
`d1 execute ${dbName} --remote --command="${insertSql}"`,
|
|
1568
|
+
stagingDir,
|
|
1569
|
+
creds
|
|
1570
|
+
);
|
|
1571
|
+
printSuccess("EdDSA keypair generated and registered");
|
|
1572
|
+
adminToken = await createEdDSAAdminToken(privateKeyJwk, kid);
|
|
1573
|
+
printSuccess("Upgraded to EdDSA admin token");
|
|
1574
|
+
}
|
|
1575
|
+
} catch {
|
|
1576
|
+
printInfo(
|
|
1577
|
+
"Note",
|
|
1578
|
+
"Could not check EdDSA key status, keeping existing token"
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
if (!adminToken) {
|
|
1582
|
+
const existingConfig = loadConfig();
|
|
1583
|
+
adminToken = existingConfig.admin_token;
|
|
1584
|
+
}
|
|
1444
1585
|
if (!adminToken) {
|
|
1445
1586
|
printError(
|
|
1446
1587
|
"No admin token found in ~/.zooid/config.json for this server"
|
|
@@ -1452,7 +1593,7 @@ async function runDeploy() {
|
|
|
1452
1593
|
process.exit(1);
|
|
1453
1594
|
}
|
|
1454
1595
|
}
|
|
1455
|
-
|
|
1596
|
+
printStep("Deploying worker...");
|
|
1456
1597
|
const deployOutput = wranglerVerbose("deploy", stagingDir, creds);
|
|
1457
1598
|
const { workerUrl, customDomain } = parseDeployUrls(deployOutput);
|
|
1458
1599
|
printSuccess("Worker deployed");
|
|
@@ -1661,18 +1802,23 @@ channelCmd.command("create <id>").description("Create a new channel").option("--
|
|
|
1661
1802
|
"Path to JSON schema file (map of event types to JSON schemas)"
|
|
1662
1803
|
).action(async (id, opts) => {
|
|
1663
1804
|
try {
|
|
1664
|
-
let
|
|
1805
|
+
let config;
|
|
1665
1806
|
if (opts.schema) {
|
|
1666
1807
|
const fs7 = await import("fs");
|
|
1667
1808
|
const raw = fs7.readFileSync(opts.schema, "utf-8");
|
|
1668
|
-
|
|
1809
|
+
const parsed = JSON.parse(raw);
|
|
1810
|
+
const types = {};
|
|
1811
|
+
for (const [eventType, schemaDef] of Object.entries(parsed)) {
|
|
1812
|
+
types[eventType] = { schema: schemaDef };
|
|
1813
|
+
}
|
|
1814
|
+
config = { types };
|
|
1669
1815
|
}
|
|
1670
1816
|
const result = await runChannelCreate(id, {
|
|
1671
1817
|
name: opts.name,
|
|
1672
1818
|
description: opts.description,
|
|
1673
1819
|
public: opts.private ? false : true,
|
|
1674
1820
|
strict: opts.strict,
|
|
1675
|
-
|
|
1821
|
+
config
|
|
1676
1822
|
});
|
|
1677
1823
|
printSuccess(`Created channel: ${id}`);
|
|
1678
1824
|
printInfo("Publish token", result.publish_token);
|
|
@@ -1696,7 +1842,12 @@ channelCmd.command("update <id>").description("Update a channel").option("--name
|
|
|
1696
1842
|
if (opts.schema) {
|
|
1697
1843
|
const fs7 = await import("fs");
|
|
1698
1844
|
const raw = fs7.readFileSync(opts.schema, "utf-8");
|
|
1699
|
-
|
|
1845
|
+
const parsed = JSON.parse(raw);
|
|
1846
|
+
const types = {};
|
|
1847
|
+
for (const [eventType, schemaDef] of Object.entries(parsed)) {
|
|
1848
|
+
types[eventType] = { schema: schemaDef };
|
|
1849
|
+
}
|
|
1850
|
+
fields.config = { types };
|
|
1700
1851
|
}
|
|
1701
1852
|
if (opts.strict !== void 0) fields.strict = opts.strict;
|
|
1702
1853
|
if (Object.keys(fields).length === 0) {
|
|
@@ -1730,16 +1881,6 @@ channelCmd.command("list").description("List all channels").action(async () => {
|
|
|
1730
1881
|
handleError("channel list", err);
|
|
1731
1882
|
}
|
|
1732
1883
|
});
|
|
1733
|
-
channelCmd.command("add-publisher <channel>").description("Add a publisher to a channel").requiredOption("--name <name>", "Publisher name").action(async (channel, opts) => {
|
|
1734
|
-
try {
|
|
1735
|
-
const result = await runChannelAddPublisher(channel, opts.name);
|
|
1736
|
-
printSuccess(`Added publisher: ${result.name}`);
|
|
1737
|
-
printInfo("Publisher ID", result.id);
|
|
1738
|
-
printInfo("Publish token", result.publish_token);
|
|
1739
|
-
} catch (err) {
|
|
1740
|
-
handleError("channel add-publisher", err);
|
|
1741
|
-
}
|
|
1742
|
-
});
|
|
1743
1884
|
channelCmd.command("delete <id>").description("Delete a channel and all its data").option("-y, --yes", "Skip confirmation prompt").action(async (id, opts) => {
|
|
1744
1885
|
try {
|
|
1745
1886
|
if (!opts.yes) {
|
|
@@ -1750,7 +1891,7 @@ channelCmd.command("delete <id>").description("Delete a channel and all its data
|
|
|
1750
1891
|
});
|
|
1751
1892
|
const answer = await new Promise((resolve) => {
|
|
1752
1893
|
rl.question(
|
|
1753
|
-
`Delete channel "${id}" and all its events
|
|
1894
|
+
`Delete channel "${id}" and all its events and webhooks? [y/N] `,
|
|
1754
1895
|
resolve
|
|
1755
1896
|
);
|
|
1756
1897
|
});
|
|
@@ -1907,6 +2048,27 @@ serverCmd.command("set").description("Update server metadata").option("--name <n
|
|
|
1907
2048
|
handleError("server set", err);
|
|
1908
2049
|
}
|
|
1909
2050
|
});
|
|
2051
|
+
program.command("token <scope>").description("Mint a new token (admin, publish, or subscribe)").argument("[channels...]", "Channels to scope the token to").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 (scope, channels, opts) => {
|
|
2052
|
+
try {
|
|
2053
|
+
if (!["admin", "publish", "subscribe"].includes(scope)) {
|
|
2054
|
+
throw new Error(
|
|
2055
|
+
`Invalid scope "${scope}". Must be one of: admin, publish, subscribe`
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
const result = await runTokenMint(
|
|
2059
|
+
scope,
|
|
2060
|
+
{
|
|
2061
|
+
channels: channels.length > 0 ? channels : void 0,
|
|
2062
|
+
sub: opts.sub,
|
|
2063
|
+
name: opts.name,
|
|
2064
|
+
expiresIn: opts.expiresIn
|
|
2065
|
+
}
|
|
2066
|
+
);
|
|
2067
|
+
console.log(result.token);
|
|
2068
|
+
} catch (err) {
|
|
2069
|
+
handleError("token", err);
|
|
2070
|
+
}
|
|
2071
|
+
});
|
|
1910
2072
|
program.command("status").description("Check server status").action(async () => {
|
|
1911
2073
|
try {
|
|
1912
2074
|
const { discovery, identity } = await runStatus();
|
|
@@ -1922,7 +2084,7 @@ program.command("status").description("Check server status").action(async () =>
|
|
|
1922
2084
|
handleError("status", err);
|
|
1923
2085
|
}
|
|
1924
2086
|
});
|
|
1925
|
-
program.command("history").description("Show tail/subscribe history
|
|
2087
|
+
program.command("history").description("Show tail/subscribe history").option("-n, --limit <n>", "Max entries to show", "20").option("--json", "Output as JSON").action((opts) => {
|
|
1926
2088
|
try {
|
|
1927
2089
|
const entries = runHistory();
|
|
1928
2090
|
const limit = parseInt(opts.limit, 10);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zooid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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.2.0",
|
|
28
|
+
"@zooid/server": "^0.2.0",
|
|
29
|
+
"@zooid/types": "^0.2.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@cloudflare/vitest-pool-workers": "^0.8.34",
|