fix(cli): harden scoped board key summaries

Recognize validated preset scopes independent of permission ordering and document the scoped create and expiry contract.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-19 20:00:46 +00:00
1 parent d262fb5f0d
commit 3834fa70f3
3 files changed
+54 -4

No files matched your search

+33
View File
@@ -1,5 +1,6 @@
import { Command } from "commander";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { BOARD_API_KEY_SCOPE_PRESETS } from "@paperclipai/shared";
import { registerTokenCommands } from "../commands/client/token.js";
const COMPANY_ID = "22222222-2222-4222-8222-222222222222";
@@ -326,4 +327,36 @@ describe("token commands", () => {
expect(fetchMock.mock.calls[1]?.[0]).toBe("http://localhost:3100/api/board-api-keys/board-key-1");
expect(fetchMock.mock.calls[1]?.[1]?.method).toBe("DELETE");
});
it("recognizes a preset scope when its valid permissions use a different order", async () => {
const permissions = [...BOARD_API_KEY_SCOPE_PRESETS.read_only.permissions].reverse();
const fetchMock = vi.fn().mockResolvedValueOnce(new Response(JSON.stringify([{
id: "board-key-1",
name: "reader",
tokenPrefix: "pcp_board_123456789abc",
scopeConfig: {
version: 1,
kind: "scoped",
companyIds: [COMPANY_ID],
permissions,
instanceCapabilities: [],
},
legacyUnrestricted: false,
status: "active",
createdAt: "2026-05-23T00:00:00.000Z",
lastUsedAt: null,
expiresAt: "2026-06-22T00:00:00.000Z",
revokedAt: null,
}]), { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
const log = vi.spyOn(console, "log").mockImplementation(() => {});
await createProgram().parseAsync([
"token", "board", "list",
"--api-base", "http://localhost:3100",
"--api-key", "board-token",
], { from: "user" });
expect(String(log.mock.calls[0]?.[0])).toContain("scope=read_only:1_companies");
});
});
+2 -2
View File
@@ -340,10 +340,10 @@ function summarizeBoardKeyScope(scopeConfig: BoardApiKeyScopeConfig | null): str
[BoardApiKeyScopePreset, (typeof BOARD_API_KEY_SCOPE_PRESETS)[BoardApiKeyScopePreset]]
>).find(([, preset]) =>
preset.permissions.length === scopeConfig.permissions.length
&& preset.permissions.every((permission, index) => permission === scopeConfig.permissions[index])
&& preset.permissions.every((permission) => scopeConfig.permissions.includes(permission))
&& preset.instanceCapabilities.length === scopeConfig.instanceCapabilities.length
&& preset.instanceCapabilities.every(
(capability, index) => capability === scopeConfig.instanceCapabilities[index],
(capability) => scopeConfig.instanceCapabilities.includes(capability),
));
const label = matchingPreset?.[0] ?? "custom";
return `${label}:${scopeConfig.companyIds.length}_companies`;
+19 -2
View File
@@ -508,11 +508,28 @@ npx paperclipai token agent list --company-id <company-id> --agent <agent-id-or-
npx paperclipai token agent revoke --company-id <company-id> --agent <agent-id-or-name> <key-id>
```
Named board API keys use the board authorization model, support revocation and expiration metadata, and are audited server-side.
Named board API keys use the board authorization model. Each new key requires a
strict scope and pins at least one company. The default preset is
`company_automation`. Select `read_only` or `full_instance_admin` with
`--scope-preset`. Add more company pins by repeating `--scope-company-id`.
Use `--scope-config-json` for a custom scope. The JSON must pass the same strict
shared validator as the REST API. It cannot be combined with a preset or an
additional company flag, and it must include the `--company-id` value.
Keys expire after 30 days by default. Use `--ttl-days` or `--expires-at` for a
different finite expiry. Use `--never-expires` only when a non-expiring key is
required. These three expiry options are mutually exclusive. The service returns
the plaintext token only from the successful create response. List output contains
safe metadata only, and revoke targets only a key owned by the current board user.
```sh
npx paperclipai token board create --company-id <company-id> --name external-admin
npx paperclipai token board create --name short-lived --ttl-days 7
npx paperclipai token board create --company-id <company-id> --name reader --scope-preset read_only
npx paperclipai token board create --company-id <company-id> --name multi-company --scope-company-id <second-company-id>
npx paperclipai token board create --company-id <company-id> --name short-lived --ttl-days 7
npx paperclipai token board create --company-id <company-id> --name long-lived --never-expires
npx paperclipai token board create --company-id <company-id> --name custom --scope-config-json '<scope-json>'
npx paperclipai token board list
npx paperclipai token board revoke <key-id>
```