feat(broker+cli): apikey create/list/revoke verbs (v0.2.0 #71)
Issuance flow over WS for now (REST endpoints come next slice). Plaintext secret returned ONCE on create — never recoverable. - broker: 3 WS handlers (apikey_create/list/revoke), wire types in union, audit log on issuance + revoke - ws-client: apiKeyCreate/List/Revoke with resolver maps, response dispatch - CLI: claudemesh apikey create <label> [--cap a,b] [--topic c,d] [--expires ISO]; list shows status, scope, last-used; revoke by id - policy: apikey create + revoke prompt by default (issuing or disabling a credential is meaningful) Default capability set is "send,read" — least privilege for unscoped keys (admin must explicitly opt-in). Spec: .artifacts/specs/2026-05-02-v0.2.0-scope.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -166,6 +166,9 @@ export class BrokerClient {
|
||||
private topicListResolvers = new Map<string, { resolve: (topics: Array<{ id: string; name: string; description: string | null; visibility: "public" | "private" | "dm"; memberCount: number; createdAt: string }>) => void; timer: NodeJS.Timeout }>();
|
||||
private topicMembersResolvers = new Map<string, { resolve: (members: Array<{ memberId: string; pubkey: string; displayName: string; role: "lead" | "member" | "observer"; joinedAt: string; lastReadAt: string | null }>) => void; timer: NodeJS.Timeout }>();
|
||||
private topicHistoryResolvers = new Map<string, { resolve: (messages: Array<{ id: string; senderPubkey: string; nonce: string; ciphertext: string; createdAt: string }>) => void; timer: NodeJS.Timeout }>();
|
||||
// ── API keys (v0.2.0) ──
|
||||
private apiKeyCreatedResolvers = new Map<string, { resolve: (r: { id: string; secret: string; label: string; prefix: string; capabilities: Array<"send" | "read" | "state_write" | "admin">; topicScopes: string[] | null; createdAt: string } | null) => void; timer: NodeJS.Timeout }>();
|
||||
private apiKeyListResolvers = new Map<string, { resolve: (keys: Array<{ id: string; label: string; prefix: string; capabilities: Array<"send" | "read" | "state_write" | "admin">; topicScopes: string[] | null; createdAt: string; lastUsedAt: string | null; revokedAt: string | null; expiresAt: string | null }>) => void; timer: NodeJS.Timeout }>();
|
||||
/** Directories from which this peer serves files. Default: [process.cwd()]. */
|
||||
private sharedDirs: string[] = [process.cwd()];
|
||||
private _serviceCatalog: Array<{ name: string; description: string; status: string; tools: Array<{ name: string; description: string; inputSchema: object }>; deployed_by: string }> = [];
|
||||
@@ -647,6 +650,60 @@ export class BrokerClient {
|
||||
this.ws.send(JSON.stringify({ type: "topic_mark_read", topic }));
|
||||
}
|
||||
|
||||
// --- API keys (v0.2.0) ---
|
||||
|
||||
async apiKeyCreate(args: {
|
||||
label: string;
|
||||
capabilities: Array<"send" | "read" | "state_write" | "admin">;
|
||||
topicScopes?: string[];
|
||||
expiresAt?: string;
|
||||
}): Promise<{ id: string; secret: string; label: string; prefix: string; capabilities: Array<"send" | "read" | "state_write" | "admin">; topicScopes: string[] | null; createdAt: string } | null> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||
return new Promise((resolve) => {
|
||||
const reqId = this.makeReqId();
|
||||
this.apiKeyCreatedResolvers.set(reqId, {
|
||||
resolve,
|
||||
timer: setTimeout(() => {
|
||||
if (this.apiKeyCreatedResolvers.delete(reqId)) resolve(null);
|
||||
}, 5_000),
|
||||
});
|
||||
this.ws!.send(
|
||||
JSON.stringify({ type: "apikey_create", _reqId: reqId, ...args }),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async apiKeyList(): Promise<
|
||||
Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
prefix: string;
|
||||
capabilities: Array<"send" | "read" | "state_write" | "admin">;
|
||||
topicScopes: string[] | null;
|
||||
createdAt: string;
|
||||
lastUsedAt: string | null;
|
||||
revokedAt: string | null;
|
||||
expiresAt: string | null;
|
||||
}>
|
||||
> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||
return new Promise((resolve) => {
|
||||
const reqId = this.makeReqId();
|
||||
this.apiKeyListResolvers.set(reqId, {
|
||||
resolve,
|
||||
timer: setTimeout(() => {
|
||||
if (this.apiKeyListResolvers.delete(reqId)) resolve([]);
|
||||
}, 5_000),
|
||||
});
|
||||
this.ws!.send(JSON.stringify({ type: "apikey_list", _reqId: reqId }));
|
||||
});
|
||||
}
|
||||
|
||||
async apiKeyRevoke(id: string): Promise<void> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||
this.ws.send(JSON.stringify({ type: "apikey_revoke", id }));
|
||||
}
|
||||
|
||||
// --- State ---
|
||||
|
||||
/** Set a shared state value visible to all peers in the mesh. */
|
||||
@@ -1836,6 +1893,22 @@ export class BrokerClient {
|
||||
this.resolveFromMap(this.topicHistoryResolvers, msgReqId, (msg.messages as any[]) ?? []);
|
||||
return;
|
||||
}
|
||||
if (msg.type === "apikey_created") {
|
||||
this.resolveFromMap(this.apiKeyCreatedResolvers, msgReqId, {
|
||||
id: String(msg.id ?? ""),
|
||||
secret: String(msg.secret ?? ""),
|
||||
label: String(msg.label ?? ""),
|
||||
prefix: String(msg.prefix ?? ""),
|
||||
capabilities: (msg.capabilities as any[]) ?? [],
|
||||
topicScopes: (msg.topicScopes as string[] | null) ?? null,
|
||||
createdAt: String(msg.createdAt ?? ""),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (msg.type === "apikey_list_response") {
|
||||
this.resolveFromMap(this.apiKeyListResolvers, msgReqId, (msg.keys as any[]) ?? []);
|
||||
return;
|
||||
}
|
||||
if (msg.type === "push") {
|
||||
this._statsCounters.messagesIn++;
|
||||
const nonce = String(msg.nonce ?? "");
|
||||
|
||||
@@ -77,6 +77,8 @@ export const DEFAULT_POLICY: Policy = {
|
||||
{ resource: "sql", verb: "execute", decision: "prompt", reason: "raw SQL write to mesh DB" },
|
||||
{ resource: "graph", verb: "execute", decision: "prompt", reason: "graph mutation" },
|
||||
{ resource: "mesh", verb: "delete", decision: "prompt", reason: "deletes the mesh for everyone" },
|
||||
{ resource: "apikey", verb: "create", decision: "prompt", reason: "issues a long-lived credential" },
|
||||
{ resource: "apikey", verb: "revoke", decision: "prompt", reason: "irreversibly disables a credential" },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user