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.
Files changed (2) hide show
  1. package/dist/index.js +196 -34
  2. 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
- schema: options.schema
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
- console.log(`Creating D1 database (${dbName})...`);
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
- console.log("Running database schema migration...");
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
- console.log("Generating secrets...");
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
- adminToken = await createAdminToken(jwtSecret);
1410
- printSuccess("Admin token generated");
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 existingConfig = loadConfig();
1443
- adminToken = existingConfig.admin_token;
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
- console.log("Deploying worker...");
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 schema;
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
- schema = JSON.parse(raw);
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
- schema
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
- fields.schema = JSON.parse(raw);
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, webhooks, and publishers? [y/N] `,
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 across all servers").option("-n, --limit <n>", "Max entries to show", "20").option("--json", "Output as JSON").action((opts) => {
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.1.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.1.0",
28
- "@zooid/types": "^0.1.0",
29
- "@zooid/server": "^0.1.0"
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",