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:
@@ -93,6 +93,9 @@ import {
|
||||
topicHistory,
|
||||
markTopicRead,
|
||||
appendTopicMessage,
|
||||
createApiKey,
|
||||
listApiKeys,
|
||||
revokeApiKey,
|
||||
} from "./broker";
|
||||
import * as serviceManager from "./service-manager";
|
||||
import { ensureBucket, meshBucketName, minioClient } from "./minio";
|
||||
@@ -2463,6 +2466,69 @@ function handleConnection(ws: WebSocket): void {
|
||||
break;
|
||||
}
|
||||
|
||||
// ── API keys (v0.2.0) ───────────────────────────────────────
|
||||
// TODO: gate to admin members only. For now any authed peer can
|
||||
// issue keys for their mesh — matches existing `share` invite
|
||||
// semantics; tighter ACL lands with the broader admin role work.
|
||||
case "apikey_create": {
|
||||
const ac = msg as Extract<WSClientMessage, { type: "apikey_create" }>;
|
||||
if (!ac.label || !ac.capabilities?.length) {
|
||||
sendError(ws, "invalid_args", "label and at least one capability required", _reqId);
|
||||
break;
|
||||
}
|
||||
const result = await createApiKey({
|
||||
meshId: conn.meshId,
|
||||
label: ac.label,
|
||||
capabilities: ac.capabilities,
|
||||
topicScopes: ac.topicScopes ?? null,
|
||||
issuedByMemberId: conn.memberId,
|
||||
expiresAt: ac.expiresAt ? new Date(ac.expiresAt) : undefined,
|
||||
});
|
||||
const resp: WSServerMessage = {
|
||||
type: "apikey_created",
|
||||
id: result.id,
|
||||
secret: result.secret,
|
||||
label: result.label,
|
||||
prefix: result.prefix,
|
||||
capabilities: result.capabilities,
|
||||
topicScopes: result.topicScopes,
|
||||
createdAt: result.createdAt.toISOString(),
|
||||
...(_reqId ? { _reqId } : {}),
|
||||
};
|
||||
conn.ws.send(JSON.stringify(resp));
|
||||
log.info("ws apikey_create", { presence_id: presenceId, label: ac.label, key_id: result.id });
|
||||
break;
|
||||
}
|
||||
|
||||
case "apikey_list": {
|
||||
const keys = await listApiKeys(conn.meshId);
|
||||
const resp: WSServerMessage = {
|
||||
type: "apikey_list_response",
|
||||
keys: keys.map((k) => ({
|
||||
id: k.id,
|
||||
label: k.label,
|
||||
prefix: k.prefix,
|
||||
capabilities: k.capabilities,
|
||||
topicScopes: k.topicScopes,
|
||||
createdAt: k.createdAt.toISOString(),
|
||||
lastUsedAt: k.lastUsedAt?.toISOString() ?? null,
|
||||
revokedAt: k.revokedAt?.toISOString() ?? null,
|
||||
expiresAt: k.expiresAt?.toISOString() ?? null,
|
||||
})),
|
||||
...(_reqId ? { _reqId } : {}),
|
||||
};
|
||||
conn.ws.send(JSON.stringify(resp));
|
||||
break;
|
||||
}
|
||||
|
||||
case "apikey_revoke": {
|
||||
const ar = msg as Extract<WSClientMessage, { type: "apikey_revoke" }>;
|
||||
if (!ar.id) { sendError(ws, "invalid_args", "id required", _reqId); break; }
|
||||
await revokeApiKey({ meshId: conn.meshId, id: ar.id });
|
||||
log.info("ws apikey_revoke", { presence_id: presenceId, key_id: ar.id });
|
||||
break;
|
||||
}
|
||||
|
||||
case "set_state": {
|
||||
const ss = msg as Extract<WSClientMessage, { type: "set_state" }>;
|
||||
// Look up the display name for attribution.
|
||||
|
||||
@@ -179,6 +179,60 @@ export interface WSLeaveGroupMessage {
|
||||
name: string;
|
||||
}
|
||||
|
||||
// ── API keys (v0.2.0) ───────────────────────────────────────────────
|
||||
// Issuance/management of bearer tokens for REST + external WS. Only the
|
||||
// mesh admin can issue; keys are scoped by capability + optional topic
|
||||
// whitelist. Spec: .artifacts/specs/2026-05-02-v0.2.0-scope.md
|
||||
|
||||
export interface WSApiKeyCreateMessage {
|
||||
type: "apikey_create";
|
||||
label: string;
|
||||
capabilities: Array<"send" | "read" | "state_write" | "admin">;
|
||||
topicScopes?: string[];
|
||||
expiresAt?: string;
|
||||
_reqId?: string;
|
||||
}
|
||||
|
||||
export interface WSApiKeyListMessage {
|
||||
type: "apikey_list";
|
||||
_reqId?: string;
|
||||
}
|
||||
|
||||
export interface WSApiKeyRevokeMessage {
|
||||
type: "apikey_revoke";
|
||||
id: string;
|
||||
_reqId?: string;
|
||||
}
|
||||
|
||||
export interface WSApiKeyCreatedMessage {
|
||||
type: "apikey_created";
|
||||
id: string;
|
||||
/** Plaintext secret — shown ONCE, never returned again. */
|
||||
secret: string;
|
||||
label: string;
|
||||
prefix: string;
|
||||
capabilities: Array<"send" | "read" | "state_write" | "admin">;
|
||||
topicScopes: string[] | null;
|
||||
createdAt: string;
|
||||
_reqId?: string;
|
||||
}
|
||||
|
||||
export interface WSApiKeyListResponseMessage {
|
||||
type: "apikey_list_response";
|
||||
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;
|
||||
}>;
|
||||
_reqId?: string;
|
||||
}
|
||||
|
||||
// ── Topics (v0.2.0) ─────────────────────────────────────────────────
|
||||
// Topics complement groups: groups are identity tags, topics are
|
||||
// conversation scopes. targetSpec for topic-tagged messages is
|
||||
@@ -1253,6 +1307,9 @@ export type WSClientMessage =
|
||||
| WSTopicMembersMessage
|
||||
| WSTopicHistoryMessage
|
||||
| WSTopicMarkReadMessage
|
||||
| WSApiKeyCreateMessage
|
||||
| WSApiKeyListMessage
|
||||
| WSApiKeyRevokeMessage
|
||||
| WSSetStateMessage
|
||||
| WSGetStateMessage
|
||||
| WSListStateMessage
|
||||
@@ -1425,6 +1482,8 @@ export type WSServerMessage =
|
||||
| WSTopicListResponseMessage
|
||||
| WSTopicMembersResponseMessage
|
||||
| WSTopicHistoryResponseMessage
|
||||
| WSApiKeyCreatedMessage
|
||||
| WSApiKeyListResponseMessage
|
||||
| WSStateChangeMessage
|
||||
| WSStateResultMessage
|
||||
| WSStateListMessage
|
||||
|
||||
Reference in New Issue
Block a user