From d394f2b619aea8f69958f27a6c7212569b522cba Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 17 Oct 2026 21:13:49 +0300 Subject: [PATCH] fix: remove machine credential management commands --- README.md | 22 ++-- docs/command-research/platform-management.md | 29 +---- .../specs/2026-08-19-polymorfa-cli-design.md | 5 + package.json | 6 - src/commands/key/list.ts | 13 --- src/commands/key/revoke.ts | 35 ------ src/commands/project-token/list.ts | 27 ----- src/services/platform-management.ts | 55 ---------- test/commands/platform-management.test.ts | 103 ++---------------- test/commands/project-context.test.ts | 4 +- 10 files changed, 32 insertions(+), 267 deletions(-) delete mode 100644 src/commands/key/list.ts delete mode 100644 src/commands/key/revoke.ts delete mode 100644 src/commands/project-token/list.ts diff --git a/README.md b/README.md index a8167eec..ff46a418 100644 --- a/README.md +++ b/README.md @@ -196,8 +196,8 @@ polymorfa session status SESSION_ID --json Use a session ID from the list for `view` or `status`; these commands do not accept `--project`. `status` reads the stored status and optional reason from the session record. It does not probe the device or wait for a connection. -Key metadata listing and revocation are available through `key list` and -`key revoke`; the CLI does not mint or rotate organization keys. +Create, list and revoke organization keys and project tokens as a signed-in +person in the Console. These operations are not available through the CLI. ## QuickLinks @@ -1580,19 +1580,16 @@ request metadata. ## Platform access and security -Organization server API keys can inspect the credential, membership, audit, -ban, incident, saved QuickLink settings, and project-token metadata made +Organization server API keys can inspect the membership, audit, +ban, incident and saved QuickLink settings made available by the Platform API: ```shell -polymorfa key list -polymorfa key revoke key_123 --yes --idempotency-key revoke_key_123 polymorfa member list polymorfa audit-log list --action key.revoked --resource api-key --limit 100 polymorfa session-ban list --active polymorfa security-incident list polymorfa security-incident acknowledge incident_123 -polymorfa project-token list --project project_123 polymorfa widget view --project project_123 polymorfa widget update --project project_123 --input widget-patch.json polymorfa session stop session_1 session_2 --project project_123 @@ -1611,16 +1608,11 @@ apply to this kind of account, for example a business-only feature on a personal number). Before the number's first sync every value is `unknown`. It is a beta: the team must be enrolled, and it needs `sessions:read`. -Key and project-token lists contain metadata only; neither command returns raw -bearer material. The API's reported API-key scope mask is retained in JSON but -must not be treated as proof of live authorization. `key revoke` requires -`--yes` because revocation is immediate. If a successful response is lost, a -retry can return not found because the API does not persist idempotency keys for -this route. Security-incident acknowledgement is repeatable and sends no body. +Security-incident acknowledgement is repeatable and sends no body. Audit filters are exact matches, and `--limit` is bounded to 1 through 500. -These list operations return complete arrays and expose no cursor. Project-token -listing requires one explicit project ID. Follow asynchronous work with the +These list operations return complete arrays and expose no cursor. +Follow asynchronous work with the `operation` commands described in [Operations](#operations). Member invite, role update, and removal remain dashboard-session routes and are diff --git a/docs/command-research/platform-management.md b/docs/command-research/platform-management.md index 7ee1b727..438e30d1 100644 --- a/docs/command-research/platform-management.md +++ b/docs/command-research/platform-management.md @@ -1,8 +1,7 @@ # Platform management command research -- **Status:** Organization-key management commands reconciled with the unified - client; operation inspection is console-only -- **Reviewed:** 2026-09-16 +- **Status:** Public organization management commands; credential management is Console-only; operation inspection is console-only +- **Reviewed:** 2026-10-17 - **SDK baseline:** `08452992a32a392de5fc3d0fa3954ba6addd7f32` ## Sources reviewed @@ -28,10 +27,6 @@ ## Shipped command tree ```text -key - list - revoke --yes - member list @@ -48,9 +43,6 @@ security-incident operation list|view|wait|watch|cancel --owner -project-token - list --project - session stop [--project ] delete [--project ] --yes @@ -58,7 +50,7 @@ session The management commands use the public typed organization resources on the unified SDK `Client`. They require an organization server API key. Reads require -`sessions:read`; key revocation and incident acknowledgement require +`sessions:read`; incident acknowledgement requires `sessions:manage`; batch session stop/delete also require `sessions:manage`. Project tokens and Messaging browser client tokens are rejected before transport. Developer operations separately accept the principal allowed by @@ -66,20 +58,15 @@ their explicit owner and context. ## Command decisions -The API method is named `deactivate`; the CLI uses `key revoke` because it -describes the security outcome and matches established credential-management -language. It requires `--yes` and names the key ID. An optional idempotency key -is passed through, but the API does not persist it: retrying after an unseen -success can return not found. JSON preserves `scopes` as reported metadata but -neither human output nor documentation presents that mask as proof of current -authorization. +Organization keys and project tokens can only be created, listed and revoked by +a signed-in person in the Console. No machine credential command manages them. Member access is deliberately list-only. Invite, role update, and removal use dashboard sessions and are not crossed through `raw.request`. Audit action and resource filters are exact matches. The CLI validates the live handler's 1..500 limit even though the pinned OpenAPI omits those query -fields. Audit, member, ban, incident, key, and token results are complete arrays +fields. Audit, member, ban and incident results are complete arrays without cursor semantics. `session-ban list --active` keeps one discovery command while selecting the @@ -91,10 +78,6 @@ The original singular Platform operation view was removed. Operation inspection is console-only at API contract 1600403, and the pinned SDK has no operations resource, so the CLI ships no operation commands. The unpublished `--surface` selector was removed. -Project-token listing requires an explicit project because the live -organization-key handler rejects an absent project even though its OpenAPI -query field is optional. Only metadata is returned. - Batch session commands use positional IDs because they are the primary action targets. The CLI trims and validates 1..100 unique IDs before transport instead of relying on the handler's silent deduplication. `session stop` reports the diff --git a/docs/superpowers/specs/2026-08-19-polymorfa-cli-design.md b/docs/superpowers/specs/2026-08-19-polymorfa-cli-design.md index b1b24a4e..f5ffa3d5 100644 --- a/docs/superpowers/specs/2026-08-19-polymorfa-cli-design.md +++ b/docs/superpowers/specs/2026-08-19-polymorfa-cli-design.md @@ -1,3 +1,8 @@ +> Amendment (2026-10-17): organization-key listing/revocation and project-token +> listing are removed. Only signed-in Console people manage those credentials. +> Client-token routes remain available. This supersedes credential-management +> entries in the historical design and implementation record below. + # Polymorfa CLI design - **Status:** Living design; the accepted 2026-08-20 developer API RFC supersedes diff --git a/package.json b/package.json index d8a63169..2a2bb5bd 100644 --- a/package.json +++ b/package.json @@ -174,9 +174,6 @@ "identity": { "description": "Resolve phone numbers and usernames to Polymorfa user IDs." }, - "key": { - "description": "Inspect and revoke organization API keys." - }, "label": { "description": "Manage Messaging labels and chat assignments." }, @@ -228,9 +225,6 @@ "project": { "description": "Select, list, and inspect the project context." }, - "project-token": { - "description": "Inspect project-token metadata." - }, "quick-reply": { "description": "Manage observed business quick replies." }, diff --git a/src/commands/key/list.ts b/src/commands/key/list.ts deleted file mode 100644 index dc7e183a..00000000 --- a/src/commands/key/list.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {BaseCommand} from '../../base-command.js' -import {listApiKeys, renderApiKeys} from '../../services/platform-management.js' - -export default class KeyList extends BaseCommand { - static override description = - 'List organization API-key metadata (never raw key material).' - - async run(): Promise { - const {flags} = await this.parse(KeyList) - const result = await listApiKeys(await this.platformClient(flags)) - this.writeValue(result, renderApiKeys(result.data), flags) - } -} diff --git a/src/commands/key/revoke.ts b/src/commands/key/revoke.ts deleted file mode 100644 index 40b37847..00000000 --- a/src/commands/key/revoke.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {Args, Flags} from '@oclif/core' - -import {BaseCommand} from '../../base-command.js' -import {revokeApiKey} from '../../services/platform-management.js' - -export default class KeyRevoke extends BaseCommand { - static override args = { - key: Args.string({description: 'API key ID.', required: true}), - } - - static override description = 'Revoke an organization API key.' - - static override flags = { - 'idempotency-key': Flags.string({ - description: - 'Identify this attempt (a retry after an unseen success may return not found).', - }), - yes: Flags.boolean({ - description: 'Confirm immediate API-key revocation.', - required: true, - }), - } - - async run(): Promise { - const {args, flags} = await this.parse(KeyRevoke) - const result = await revokeApiKey( - await this.platformClient(flags), - args.key, - flags['idempotency-key'] === undefined - ? {} - : {idempotencyKey: flags['idempotency-key']}, - ) - this.writeValue(result, `Revoked API key ${result.data.keyId}.`, flags) - } -} diff --git a/src/commands/project-token/list.ts b/src/commands/project-token/list.ts deleted file mode 100644 index a96af432..00000000 --- a/src/commands/project-token/list.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {PolymorfaConfigurationError} from '@polymorfa/sdk' - -import {BaseCommand} from '../../base-command.js' -import { - listProjectTokens, - renderProjectTokens, -} from '../../services/platform-management.js' - -export default class ProjectTokenList extends BaseCommand { - static override description = - 'List project-token metadata (never raw token material).' - - async run(): Promise { - const {flags} = await this.parse(ProjectTokenList) - if (flags.project === undefined || flags.project.trim().length === 0) { - throw new PolymorfaConfigurationError( - 'Provide --project with the project ID whose token metadata should be listed.', - 'project', - ) - } - const result = await listProjectTokens( - await this.platformClient(flags), - flags.project, - ) - this.writeValue(result, renderProjectTokens(result.data), flags) - } -} diff --git a/src/services/platform-management.ts b/src/services/platform-management.ts index af2b706d..d61e1964 100644 --- a/src/services/platform-management.ts +++ b/src/services/platform-management.ts @@ -1,12 +1,9 @@ import { - type ApiKey, - type ApiKeyDeactivation, type AuditLog, type ClientInstance, type ListAuditLogsParams, type OrganizationMember, PolymorfaValidationError, - type ProjectToken, type RequestOptions, type ResponseMetadata, type SecurityIncident, @@ -35,22 +32,6 @@ export interface PlatformResult { } } -export async function listApiKeys( - client: ClientInstance<'organization'>, -): Promise> { - const response = await client.apiKeys.list() - return result(response.data.data, response.metadata) -} - -export async function revokeApiKey( - client: ClientInstance<'organization'>, - keyId: string, - options: RequestOptions = {}, -): Promise> { - const response = await client.apiKeys.deactivate(keyId, options) - return result(response.data.data, response.metadata) -} - export async function listMembers( client: ClientInstance<'organization'>, ): Promise> { @@ -95,14 +76,6 @@ export async function acknowledgeSecurityIncident( return result(response.data.data, response.metadata) } -export async function listProjectTokens( - client: ClientInstance<'organization'>, - projectId: string, -): Promise> { - const response = await client.projectTokens.list(projectId) - return result(response.data.data, response.metadata) -} - export async function listManagedSessions( client: ClientInstance<'organization'>, projectId?: string, @@ -234,20 +207,6 @@ export async function deleteSessions( return result(response.data.data, response.metadata) } -export function renderApiKeys(keys: readonly ApiKey[]): string { - if (keys.length === 0) return 'No API keys found.' - return keys - .map((key) => - [ - key.keyId, - key.label, - `••••${key.last4}`, - key.isActive ? 'active' : 'inactive', - ].join('\t'), - ) - .join('\n') -} - export function renderMembers(members: readonly OrganizationMember[]): string { if (members.length === 0) return 'No organization members found.' return members @@ -303,20 +262,6 @@ export function renderSecurityIncidents( .join('\n') } -export function renderProjectTokens(tokens: readonly ProjectToken[]): string { - if (tokens.length === 0) return 'No project tokens found.' - return tokens - .map((token) => - [ - token.id, - token.label ?? '', - `${token.start}••••${token.last4}`, - token.revokedAt === null ? 'active' : 'revoked', - ].join('\t'), - ) - .join('\n') -} - export async function viewWidgetSettings( client: ClientInstance<'organization'>, projectId?: string, diff --git a/test/commands/platform-management.test.ts b/test/commands/platform-management.test.ts index 2eafd47a..201581d8 100644 --- a/test/commands/platform-management.test.ts +++ b/test/commands/platform-management.test.ts @@ -23,6 +23,18 @@ describe('Platform management commands', () => { directory = undefined }) + test.each(['key list', 'key revoke key_1 --yes', 'project-token list'])( + 'removes machine credential management command %s', + async (command) => { + server = await startHttpServer({body: {data: []}}) + const result = await platformEnvironment(() => + runCommand(`${command} --json`), + ) + expect(result.error).toBeDefined() + expect(server.requests).toHaveLength(0) + }, + ) + test('lists team sessions and filters by exact project ID', async () => { server = await startHttpServer({ body: { @@ -266,72 +278,6 @@ describe('Platform management commands', () => { expect(JSON.parse(server.requests[0]!.body)).toEqual(payload) }) - test('lists API-key metadata without implying that the scope mask is authoritative', async () => { - server = await startHttpServer({ - body: { - data: [ - { - _creationTime: 1_723_000_000_000, - _id: 'row_1', - expiresAt: 0, - id: 'key_1', - isActive: true, - keyId: 'key_1', - label: 'Production', - last4: '1234', - orgId: 'org_1', - scopes: 65_535, - source: 'dashboard', - start: 'pmfa_', - }, - ], - }, - headers: {'request-id': 'req_keys'}, - }) - - const result = await platformEnvironment(() => - runCommand('key list --json'), - ) - - expect(result.error).toBeUndefined() - expect(JSON.parse(result.stdout)).toMatchObject({ - data: [{keyId: 'key_1', label: 'Production', last4: '1234'}], - metadata: {attempts: 1, requestId: 'req_keys', status: 200}, - }) - expect(server.requests[0]).toMatchObject({ - method: 'GET', - url: '/platform/keys', - }) - }) - - test('revokes one key only with confirmation and preserves an idempotency key', async () => { - server = await startHttpServer({body: {data: {keyId: 'key/1', ok: true}}}) - - const result = await platformEnvironment(() => - runCommand('key revoke key/1 --yes --idempotency-key revoke_1 --json'), - ) - - expect(result.error).toBeUndefined() - expect(server.requests[0]).toMatchObject({ - body: '', - method: 'DELETE', - url: '/platform/keys/key%2F1', - }) - expect(server.requests[0]?.headers['idempotency-key']).toBe('revoke_1') - }) - - test('refuses key revocation without explicit confirmation before transport', async () => { - server = await startHttpServer({body: {data: {keyId: 'key_1', ok: true}}}) - - const result = await platformEnvironment(() => - runCommand('key revoke key_1 --json'), - ) - - expect(exitCode(result.error)).toBe(2) - expect(result.stderr).toContain('Missing required flag yes') - expect(server.requests).toHaveLength(0) - }) - test('lists organization members through the organization-key resource', async () => { server = await startHttpServer({ body: { @@ -462,31 +408,6 @@ describe('Platform management commands', () => { expect(server.requests).toEqual([]) }) - test('lists project-token metadata for one explicit project', async () => { - server = await startHttpServer({body: {data: []}}) - - const result = await platformEnvironment(() => - runCommand('project-token list --project project/1 --json'), - ) - - expect(result.error).toBeUndefined() - expect(server.requests[0]?.url).toBe( - '/platform/tokens?projectId=project%2F1', - ) - }) - - test('requires a project for project-token metadata before transport', async () => { - server = await startHttpServer({body: {data: []}}) - - const result = await platformEnvironment(() => - runCommand('project-token list --json'), - ) - - expect(exitCode(result.error)).toBe(2) - expect(result.stderr).toContain('Provide --project') - expect(server.requests).toHaveLength(0) - }) - test("reads a number's WhatsApp capabilities", async () => { server = await startHttpServer({ body: { diff --git a/test/commands/project-context.test.ts b/test/commands/project-context.test.ts index 9092e7a4..882b1319 100644 --- a/test/commands/project-context.test.ts +++ b/test/commands/project-context.test.ts @@ -104,7 +104,7 @@ describe('link and project commands', () => { POLYMORFA_CONFIG_DIR: join(directory, 'config'), POLYMORFA_PROJECT_TOKEN: undefined, }, - () => runCommand('key list --json --verbose'), + () => runCommand('member list --json --verbose'), ) expect(result.error, result.stderr).toBeUndefined() @@ -112,7 +112,7 @@ describe('link and project commands', () => { expect(result.stderr).not.toContain('Using project') expect( server.requests.map(({method, url}) => `${method} ${url}`), - ).toEqual(['GET /platform/keys']) + ).toEqual(['GET /platform/members']) }, )