fix(cli): v0.1.8 — remove Zod dependency (bun bundler crash)
Replace Zod schemas with plain TypeScript validation in env.ts, config.ts, and invite/parse.ts. Zod 4 classes break under bun build --target=node (Class2 is not a constructor). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudemesh-cli",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.8",
|
||||
"description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
|
||||
"keywords": [
|
||||
"claude-code",
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* CLI environment config.
|
||||
*
|
||||
* Read once at startup. Overridable via env vars so users can point
|
||||
* at a self-hosted broker or a staging instance without rebuilding.
|
||||
*/
|
||||
const envSchema = z.object({
|
||||
CLAUDEMESH_BROKER_URL: z.string().default("wss://ic.claudemesh.com/ws"),
|
||||
CLAUDEMESH_CONFIG_DIR: z.string().optional(),
|
||||
CLAUDEMESH_DEBUG: z.coerce.boolean().default(false),
|
||||
});
|
||||
|
||||
export type CliEnv = z.infer<typeof envSchema>;
|
||||
export interface CliEnv {
|
||||
CLAUDEMESH_BROKER_URL: string;
|
||||
CLAUDEMESH_CONFIG_DIR: string | undefined;
|
||||
CLAUDEMESH_DEBUG: boolean;
|
||||
}
|
||||
|
||||
export function loadEnv(): CliEnv {
|
||||
const parsed = envSchema.safeParse(process.env);
|
||||
if (!parsed.success) {
|
||||
console.error("[claudemesh] invalid environment:");
|
||||
console.error(z.treeifyError(parsed.error));
|
||||
process.exit(1);
|
||||
}
|
||||
return parsed.data;
|
||||
return {
|
||||
CLAUDEMESH_BROKER_URL:
|
||||
process.env.CLAUDEMESH_BROKER_URL ?? "wss://ic.claudemesh.com/ws",
|
||||
CLAUDEMESH_CONFIG_DIR: process.env.CLAUDEMESH_CONFIG_DIR || undefined,
|
||||
CLAUDEMESH_DEBUG: process.env.CLAUDEMESH_DEBUG === "1" || process.env.CLAUDEMESH_DEBUG === "true",
|
||||
};
|
||||
}
|
||||
|
||||
export const env = loadEnv();
|
||||
|
||||
@@ -5,22 +5,19 @@
|
||||
* verification and one-time-use invite-token tracking land in Step 18.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { ensureSodium } from "../crypto/keypair";
|
||||
|
||||
const invitePayloadSchema = z.object({
|
||||
v: z.literal(1),
|
||||
mesh_id: z.string().min(1),
|
||||
mesh_slug: z.string().min(1),
|
||||
broker_url: z.string().min(1),
|
||||
expires_at: z.number().int().positive(),
|
||||
mesh_root_key: z.string().min(1),
|
||||
role: z.enum(["admin", "member"]),
|
||||
owner_pubkey: z.string().regex(/^[0-9a-f]{64}$/i),
|
||||
signature: z.string().regex(/^[0-9a-f]{128}$/i),
|
||||
});
|
||||
|
||||
export type InvitePayload = z.infer<typeof invitePayloadSchema>;
|
||||
export interface InvitePayload {
|
||||
v: 1;
|
||||
mesh_id: string;
|
||||
mesh_slug: string;
|
||||
broker_url: string;
|
||||
expires_at: number;
|
||||
mesh_root_key: string;
|
||||
role: "admin" | "member";
|
||||
owner_pubkey: string;
|
||||
signature: string;
|
||||
}
|
||||
|
||||
export interface ParsedInvite {
|
||||
payload: InvitePayload;
|
||||
@@ -28,6 +25,21 @@ export interface ParsedInvite {
|
||||
token: string; // base64url(JSON) — DB lookup key (everything after ic://join/)
|
||||
}
|
||||
|
||||
function validatePayload(obj: unknown): InvitePayload {
|
||||
if (!obj || typeof obj !== "object") throw new Error("invite payload is not an object");
|
||||
const o = obj as Record<string, unknown>;
|
||||
if (o.v !== 1) throw new Error("invite payload: v must be 1");
|
||||
if (typeof o.mesh_id !== "string" || !o.mesh_id) throw new Error("invite payload: mesh_id required");
|
||||
if (typeof o.mesh_slug !== "string" || !o.mesh_slug) throw new Error("invite payload: mesh_slug required");
|
||||
if (typeof o.broker_url !== "string" || !o.broker_url) throw new Error("invite payload: broker_url required");
|
||||
if (typeof o.expires_at !== "number" || o.expires_at <= 0) throw new Error("invite payload: expires_at must be a positive number");
|
||||
if (typeof o.mesh_root_key !== "string" || !o.mesh_root_key) throw new Error("invite payload: mesh_root_key required");
|
||||
if (o.role !== "admin" && o.role !== "member") throw new Error("invite payload: role must be admin or member");
|
||||
if (typeof o.owner_pubkey !== "string" || !/^[0-9a-f]{64}$/i.test(o.owner_pubkey)) throw new Error("invite payload: owner_pubkey must be 64 hex chars");
|
||||
if (typeof o.signature !== "string" || !/^[0-9a-f]{128}$/i.test(o.signature)) throw new Error("invite payload: signature must be 128 hex chars");
|
||||
return o as unknown as InvitePayload;
|
||||
}
|
||||
|
||||
/** Canonical invite bytes — must match broker's canonicalInvite(). */
|
||||
export function canonicalInvite(p: {
|
||||
v: number;
|
||||
@@ -96,41 +108,34 @@ export async function parseInviteLink(link: string): Promise<ParsedInvite> {
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = invitePayloadSchema.safeParse(obj);
|
||||
if (!parsed.success) {
|
||||
throw new Error(
|
||||
`invite link shape invalid: ${parsed.error.issues.map((i) => i.path.join(".") + ": " + i.message).join("; ")}`,
|
||||
);
|
||||
}
|
||||
const payload = validatePayload(obj);
|
||||
|
||||
// Expiry check (unix seconds).
|
||||
const nowSeconds = Math.floor(Date.now() / 1000);
|
||||
if (parsed.data.expires_at < nowSeconds) {
|
||||
if (payload.expires_at < nowSeconds) {
|
||||
throw new Error(
|
||||
`invite expired: expires_at=${parsed.data.expires_at}, now=${nowSeconds}`,
|
||||
`invite expired: expires_at=${payload.expires_at}, now=${nowSeconds}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Verify the ed25519 signature against the embedded owner_pubkey.
|
||||
// Client-side verification gives immediate feedback on tampered
|
||||
// links; broker re-verifies authoritatively on /join.
|
||||
const s = await ensureSodium();
|
||||
const canonical = canonicalInvite({
|
||||
v: parsed.data.v,
|
||||
mesh_id: parsed.data.mesh_id,
|
||||
mesh_slug: parsed.data.mesh_slug,
|
||||
broker_url: parsed.data.broker_url,
|
||||
expires_at: parsed.data.expires_at,
|
||||
mesh_root_key: parsed.data.mesh_root_key,
|
||||
role: parsed.data.role,
|
||||
owner_pubkey: parsed.data.owner_pubkey,
|
||||
v: payload.v,
|
||||
mesh_id: payload.mesh_id,
|
||||
mesh_slug: payload.mesh_slug,
|
||||
broker_url: payload.broker_url,
|
||||
expires_at: payload.expires_at,
|
||||
mesh_root_key: payload.mesh_root_key,
|
||||
role: payload.role,
|
||||
owner_pubkey: payload.owner_pubkey,
|
||||
});
|
||||
const sigOk = (() => {
|
||||
try {
|
||||
return s.crypto_sign_verify_detached(
|
||||
s.from_hex(parsed.data.signature),
|
||||
s.from_hex(payload.signature),
|
||||
s.from_string(canonical),
|
||||
s.from_hex(parsed.data.owner_pubkey),
|
||||
s.from_hex(payload.owner_pubkey),
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
@@ -140,7 +145,7 @@ export async function parseInviteLink(link: string): Promise<ParsedInvite> {
|
||||
throw new Error("invite signature invalid (link tampered?)");
|
||||
}
|
||||
|
||||
return { payload: parsed.data, raw: link, token: encoded };
|
||||
return { payload, raw: link, token: encoded };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,8 +160,6 @@ export function encodeInviteLink(payload: InvitePayload): string {
|
||||
|
||||
/**
|
||||
* Sign and assemble an invite payload → ic://join/... link.
|
||||
* The canonical bytes (everything except signature) are signed with
|
||||
* the mesh owner's ed25519 secret key.
|
||||
*/
|
||||
export async function buildSignedInvite(args: {
|
||||
v: 1;
|
||||
|
||||
@@ -15,38 +15,38 @@ import {
|
||||
} from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join, dirname } from "node:path";
|
||||
import { z } from "zod";
|
||||
import { env } from "../env";
|
||||
|
||||
const joinedMeshSchema = z.object({
|
||||
meshId: z.string(),
|
||||
memberId: z.string(),
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
pubkey: z.string(), // ed25519 hex (32 bytes = 64 chars)
|
||||
secretKey: z.string(), // ed25519 hex (64 bytes = 128 chars)
|
||||
brokerUrl: z.string(),
|
||||
joinedAt: z.string(),
|
||||
});
|
||||
export interface JoinedMesh {
|
||||
meshId: string;
|
||||
memberId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
pubkey: string; // ed25519 hex (32 bytes = 64 chars)
|
||||
secretKey: string; // ed25519 hex (64 bytes = 128 chars)
|
||||
brokerUrl: string;
|
||||
joinedAt: string;
|
||||
}
|
||||
|
||||
const configSchema = z.object({
|
||||
version: z.literal(1).default(1),
|
||||
meshes: z.array(joinedMeshSchema).default([]),
|
||||
});
|
||||
|
||||
export type JoinedMesh = z.infer<typeof joinedMeshSchema>;
|
||||
export type Config = z.infer<typeof configSchema>;
|
||||
export interface Config {
|
||||
version: 1;
|
||||
meshes: JoinedMesh[];
|
||||
}
|
||||
|
||||
const CONFIG_DIR = env.CLAUDEMESH_CONFIG_DIR ?? join(homedir(), ".claudemesh");
|
||||
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
||||
|
||||
export function loadConfig(): Config {
|
||||
if (!existsSync(CONFIG_PATH)) {
|
||||
return configSchema.parse({ version: 1, meshes: [] });
|
||||
return { version: 1, meshes: [] };
|
||||
}
|
||||
try {
|
||||
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
||||
return configSchema.parse(JSON.parse(raw));
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || !Array.isArray(parsed.meshes)) {
|
||||
return { version: 1, meshes: [] };
|
||||
}
|
||||
return { version: 1, meshes: parsed.meshes };
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to load ${CONFIG_PATH}: ${e instanceof Error ? e.message : String(e)}`,
|
||||
|
||||
Reference in New Issue
Block a user