mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 02:00:52 +01:00
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:
1 parent
d262fb5f0d
commit
3834fa70f3
3 files changed
+54
-4
No files matched your search
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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>
|
||||
```
|
||||
|
||||
Reference in new issue
Block a user