zooid 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +169 -29
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -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,87 @@ 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 = ["ALTER TABLE events ADD COLUMN publisher_name TEXT"];
1503
+ for (const sql of migrations) {
1504
+ try {
1505
+ wrangler(
1506
+ `d1 execute ${dbName} --remote --command="${sql}"`,
1507
+ stagingDir,
1508
+ creds
1509
+ );
1510
+ } catch {
1511
+ }
1512
+ }
1513
+ try {
1514
+ const keysOutput = wrangler(
1515
+ `d1 execute ${dbName} --remote --json --command="SELECT kid FROM trusted_keys WHERE issuer = 'local' LIMIT 1"`,
1516
+ stagingDir,
1517
+ creds
1518
+ );
1519
+ const keysResult = JSON.parse(keysOutput);
1520
+ const hasLocalKey = keysResult?.[0]?.results?.length > 0;
1521
+ if (!hasLocalKey) {
1522
+ printStep("Upgrading to EdDSA auth...");
1523
+ const keyPair = await crypto3.subtle.generateKey("Ed25519", true, [
1524
+ "sign",
1525
+ "verify"
1526
+ ]);
1527
+ const privateKeyRaw = await crypto3.subtle.exportKey(
1528
+ "pkcs8",
1529
+ keyPair.privateKey
1530
+ );
1531
+ const publicKeyRaw = await crypto3.subtle.exportKey(
1532
+ "raw",
1533
+ keyPair.publicKey
1534
+ );
1535
+ const privateKeyJwk = await crypto3.subtle.exportKey(
1536
+ "jwk",
1537
+ keyPair.privateKey
1538
+ );
1539
+ const publicKeyJwk = await crypto3.subtle.exportKey(
1540
+ "jwk",
1541
+ keyPair.publicKey
1542
+ );
1543
+ const privateKeyB64 = Buffer.from(privateKeyRaw).toString("base64");
1544
+ const publicKeyB64 = Buffer.from(publicKeyRaw).toString("base64");
1545
+ wrangler("secret put ZOOID_SIGNING_KEY", stagingDir, creds, {
1546
+ input: privateKeyB64
1547
+ });
1548
+ wrangler("secret put ZOOID_PUBLIC_KEY", stagingDir, creds, {
1549
+ input: publicKeyB64
1550
+ });
1551
+ const kid = "local-1";
1552
+ const xValue = publicKeyJwk.x;
1553
+ const insertSql = `INSERT INTO trusted_keys (kid, x, issuer) VALUES ('${kid}', '${xValue}', 'local');`;
1554
+ wrangler(
1555
+ `d1 execute ${dbName} --remote --command="${insertSql}"`,
1556
+ stagingDir,
1557
+ creds
1558
+ );
1559
+ printSuccess("EdDSA keypair generated and registered");
1560
+ adminToken = await createEdDSAAdminToken(privateKeyJwk, kid);
1561
+ printSuccess("Upgraded to EdDSA admin token");
1562
+ }
1563
+ } catch {
1564
+ printInfo(
1565
+ "Note",
1566
+ "Could not check EdDSA key status, keeping existing token"
1567
+ );
1568
+ }
1569
+ if (!adminToken) {
1570
+ const existingConfig = loadConfig();
1571
+ adminToken = existingConfig.admin_token;
1572
+ }
1444
1573
  if (!adminToken) {
1445
1574
  printError(
1446
1575
  "No admin token found in ~/.zooid/config.json for this server"
@@ -1452,7 +1581,7 @@ async function runDeploy() {
1452
1581
  process.exit(1);
1453
1582
  }
1454
1583
  }
1455
- console.log("Deploying worker...");
1584
+ printStep("Deploying worker...");
1456
1585
  const deployOutput = wranglerVerbose("deploy", stagingDir, creds);
1457
1586
  const { workerUrl, customDomain } = parseDeployUrls(deployOutput);
1458
1587
  printSuccess("Worker deployed");
@@ -1730,16 +1859,6 @@ channelCmd.command("list").description("List all channels").action(async () => {
1730
1859
  handleError("channel list", err);
1731
1860
  }
1732
1861
  });
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
1862
  channelCmd.command("delete <id>").description("Delete a channel and all its data").option("-y, --yes", "Skip confirmation prompt").action(async (id, opts) => {
1744
1863
  try {
1745
1864
  if (!opts.yes) {
@@ -1750,7 +1869,7 @@ channelCmd.command("delete <id>").description("Delete a channel and all its data
1750
1869
  });
1751
1870
  const answer = await new Promise((resolve) => {
1752
1871
  rl.question(
1753
- `Delete channel "${id}" and all its events, webhooks, and publishers? [y/N] `,
1872
+ `Delete channel "${id}" and all its events and webhooks? [y/N] `,
1754
1873
  resolve
1755
1874
  );
1756
1875
  });
@@ -1907,6 +2026,27 @@ serverCmd.command("set").description("Update server metadata").option("--name <n
1907
2026
  handleError("server set", err);
1908
2027
  }
1909
2028
  });
2029
+ 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) => {
2030
+ try {
2031
+ if (!["admin", "publish", "subscribe"].includes(scope)) {
2032
+ throw new Error(
2033
+ `Invalid scope "${scope}". Must be one of: admin, publish, subscribe`
2034
+ );
2035
+ }
2036
+ const result = await runTokenMint(
2037
+ scope,
2038
+ {
2039
+ channels: channels.length > 0 ? channels : void 0,
2040
+ sub: opts.sub,
2041
+ name: opts.name,
2042
+ expiresIn: opts.expiresIn
2043
+ }
2044
+ );
2045
+ console.log(result.token);
2046
+ } catch (err) {
2047
+ handleError("token", err);
2048
+ }
2049
+ });
1910
2050
  program.command("status").description("Check server status").action(async () => {
1911
2051
  try {
1912
2052
  const { discovery, identity } = await runStatus();
@@ -1922,7 +2062,7 @@ program.command("status").description("Check server status").action(async () =>
1922
2062
  handleError("status", err);
1923
2063
  }
1924
2064
  });
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) => {
2065
+ 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
2066
  try {
1927
2067
  const entries = runHistory();
1928
2068
  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.1.1",
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.1.1",
28
+ "@zooid/types": "^0.1.1",
29
+ "@zooid/server": "^0.1.1"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@cloudflare/vitest-pool-workers": "^0.8.34",