Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,66 @@ order-history command. The source exposes no catalog-independent list, product
or collection search/history, binary upload, resumability, or Business App
support for project credentials, client tokens, or Cloud API sessions.

## BanSafe

Inspect current and historical Health, the bounded signal catalogue, collected
telemetry, collection evidence, and durable Health actions with a Platform
server API key:

```shell
polymorfa bansafe health list --project project_123
polymorfa bansafe health view session_123
polymorfa bansafe health history session_123 --since 2026-09-01T00:00:00Z
polymorfa bansafe signal list
polymorfa bansafe telemetry view session_123
polymorfa bansafe telemetry history session_123 --limit 20
polymorfa bansafe collection list --project project_123
polymorfa bansafe health-action list --project project_123 --status failed
polymorfa bansafe health-policy view --project project_123
polymorfa bansafe health-policy update \
--project project_123 \
--input health-policy.json \
--idempotency-key health-policy-v4
polymorfa bansafe finding list --project project_123 --status open
polymorfa bansafe enforcement list --project project_123
polymorfa bansafe incident list --project project_123
polymorfa bansafe incident report \
--session session_123 \
--idempotency-key customer-incident-123
polymorfa bansafe incident retract incident_123 \
--idempotency-key retract-incident-123
polymorfa bansafe claim list --project project_123
polymorfa bansafe claim view claim_123
polymorfa bansafe safe-mode view --project project_123
polymorfa bansafe safe-mode view session_123
polymorfa bansafe safe-mode update --project project_123 \
--input safe-mode.json --idempotency-key safe-mode-v2
polymorfa bansafe warmup view --project project_123
polymorfa bansafe warmup update --project project_123 \
--input warmup.json --idempotency-key warmup-v2
polymorfa bansafe insurance-evidence view --project project_123
polymorfa bansafe insurance-evidence update --project project_123 \
--input insurance-evidence.json --idempotency-key insurance-evidence-v2
```

Collection output reports observed ingestion state: `fresh`, `stale`,
`not_collected`, or `unsupported`. It does not infer whether collection is
configured. Telemetry history returns bounded snapshots and explicit
per-signal measurement state. Missing signals are not reported as healthy
measurements.

Health policy updates replace the complete policy and require its current
version. The CLI validates action and notification combinations before sending
the request. Health policy, Safe Mode, warmup, and insurance-evidence updates
require an idempotency key. Safe Mode and warmup updates accept bounded JSON
patches. Customer incident reports and retractions also require an idempotency key. Finding
acknowledgement and enforcement appeals remain dashboard-only and are not
Comment thread
purpshell marked this conversation as resolved.
Resolved
available through server-key commands. Claim `amountCents`, `measuredCents`, and
`capCents` are credit quantities with up to six decimal places, not cash minor
units; text output labels them as credits. Use `--json` to retain explanations,
observed account state, collection metadata, action outcomes, cursors, and
request metadata.

## Platform access and security

Organization server API keys can inspect the credential, membership, audit,
Expand Down
59 changes: 59 additions & 0 deletions docs/development/bansafe-sdk-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# BanSafe SDK integration check

The BanSafe CLI commands depend on the matching typed SDK revision pinned in
`package.json`. During coordinated SDK and CLI development, verify both
worktrees without replacing that immutable dependency in either manifest:

The pinned SDK revision is `polymorfa/sdks@8803f0554314214519971cb3306e0c0efe21cb76`.
Its coverage ledger is reconciled against the API contracts at
`polymorfa/polymorfa@aca849cda44ad8582d7ae87489404d2483173a53` (Platform
snapshot SHA-256
`2a30e8e0af8902703609f8ed0f5bb2c108b39c5c11863d5ebd8907e56a9f25d2`).

```shell
export POLYMORFA_SDK_CHECKOUT=/absolute/path/to/bansafe-health-sdk
export POLYMORFA_CLI_CHECKOUT=/absolute/path/to/bansafe-health-cli

cd "$POLYMORFA_SDK_CHECKOUT"
npm install --ignore-scripts
npm run build
npm run typecheck
npx vitest run packages/typescript/test/bansafe.test.ts

cd "$POLYMORFA_CLI_CHECKOUT"
npm install --ignore-scripts
npm install --no-save --package-lock=false "$POLYMORFA_SDK_CHECKOUT"
npm run typecheck
npx vitest run test/commands/bansafe.test.ts
npm run build
```

Before publishing the CLI, confirm that the pinned SDK revision remains
available, run `npm ci`, and run `npm run verify` from a fresh checkout. A
local-path install is integration evidence only; it is not a publishable
dependency.

The CLI mapping covers all 25 Platform BanSafe operations in the SDK coverage
ledger. The SDK also covers the 10 equivalent Messaging settings routes through
`MessagingClient.banSafe`; the CLI uses the Platform routes for those settings
because BanSafe commands authenticate with an organization server API key.

| SDK operations | CLI commands |
| ------------------------------------------------------------ | ---------------------------------------------------------- |
| `banSafe.listHealth`, `getHealth`, `listHealthHistory` | `bansafe health list`, `view`, `history` |
| `banSafe.listSignals` | `bansafe signal list` |
| `banSafe.getTelemetry`, `listTelemetryHistory` | `bansafe telemetry view`, `history` |
| `banSafe.listCollection` | `bansafe collection list` |
| `banSafe.listHealthActions` | `bansafe health-action list` |
| `banSafe.listFindings` | `bansafe finding list` |
| `banSafe.listEnforcement` | `bansafe enforcement list` |
| `banSafe.listIncidents`, `createIncident`, `retractIncident` | `bansafe incident list`, `report`, `retract` |
| `banSafe.listClaims`, `getClaim` | `bansafe claim list`, `view` |
| `projects.getSafeMode`, `updateSafeMode` | `bansafe safe-mode view`, `update` with `--project` |
| `sessions.getSafeMode`, `updateSafeMode` | `bansafe safe-mode view`, `update` with a session argument |
| `projects.getWarmupPlan`, `updateWarmupPlan` | `bansafe warmup view`, `update` |
| `projects.getHealthPolicy`, `updateHealthPolicy` | `bansafe health-policy view`, `update` |
| `projects.getInsuranceEvidence`, `updateInsuranceEvidence` | `bansafe insurance-evidence view`, `update` |

Finding acknowledgement and enforcement appeal are dashboard-identity
operations. They are intentionally absent from both the server SDK and CLI.
39 changes: 39 additions & 0 deletions src/commands/bansafe/claim/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {Flags} from '@oclif/core'
import type {BanSafeClaimStatus} from '@polymorfa/sdk'

import {BaseCommand} from '../../../base-command.js'
import {listClaims, renderClaims} from '../../../services/bansafe.js'

export default class BanSafeClaimList extends BaseCommand {
static override description =
'List BanSafe attribution claims for one project.'
static override flags = {
cursor: Flags.string({description: 'Continue from an opaque page cursor.'}),
limit: Flags.integer({max: 100, min: 1}),
project: Flags.string({description: 'Project ID.', required: true}),
session: Flags.string({description: 'Match one session ID or name.'}),
status: Flags.string({
options: [
'filed',
'under_review',
'approved',
'denied',
'paid',
'reversed',
],
}),
}
async run(): Promise<void> {
const {flags} = await this.parse(BanSafeClaimList)
const result = await listClaims(await this.platformClient(flags), {
projectId: flags.project,
...(flags.cursor === undefined ? {} : {cursor: flags.cursor}),
...(flags.limit === undefined ? {} : {limit: flags.limit}),
...(flags.session === undefined ? {} : {session: flags.session}),
...(flags.status === undefined
? {}
: {status: flags.status as BanSafeClaimStatus}),
})
this.writeValue(result, renderClaims(result.data), flags)
}
}
16 changes: 16 additions & 0 deletions src/commands/bansafe/claim/view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {Args} from '@oclif/core'

import {BaseCommand} from '../../../base-command.js'
import {getClaim, renderClaim} from '../../../services/bansafe.js'

export default class BanSafeClaimView extends BaseCommand {
static override args = {
claim: Args.string({description: 'Claim ID.', required: true}),
}
static override description = 'View one BanSafe attribution claim.'
async run(): Promise<void> {
const {args, flags} = await this.parse(BanSafeClaimView)
const result = await getClaim(await this.platformClient(flags), args.claim)
this.writeValue(result, renderClaim(result.data), flags)
}
}
29 changes: 29 additions & 0 deletions src/commands/bansafe/collection/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {Flags} from '@oclif/core'

import {BaseCommand} from '../../../base-command.js'
import {listCollection, renderCollection} from '../../../services/bansafe.js'

export default class BanSafeCollectionList extends BaseCommand {
static override description =
'List BanSafe telemetry collection status for one project.'

static override flags = {
cursor: Flags.string({description: 'Continue from an opaque page cursor.'}),
limit: Flags.integer({
description: 'Maximum sessions to return (default 25).',
max: 100,
min: 1,
}),
project: Flags.string({description: 'Project ID.', required: true}),
}

async run(): Promise<void> {
const {flags} = await this.parse(BanSafeCollectionList)
const result = await listCollection(await this.platformClient(flags), {
projectId: flags.project,
...(flags.cursor === undefined ? {} : {cursor: flags.cursor}),
...(flags.limit === undefined ? {} : {limit: flags.limit}),
})
this.writeValue(result, renderCollection(result.data), flags)
}
}
30 changes: 30 additions & 0 deletions src/commands/bansafe/enforcement/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {Flags} from '@oclif/core'
import type {BanSafeEnforcementRung} from '@polymorfa/sdk'

import {BaseCommand} from '../../../base-command.js'
import {listEnforcement, renderEnforcement} from '../../../services/bansafe.js'

export default class BanSafeEnforcementList extends BaseCommand {
static override description =
'List recorded BanSafe enforcement for one project.'
static override flags = {
cursor: Flags.string({description: 'Continue from an opaque page cursor.'}),
limit: Flags.integer({max: 100, min: 1}),
project: Flags.string({description: 'Project ID.', required: true}),
rung: Flags.string({
options: ['none', 'notify', 'throttle', 'block_cold', 'suspend'],
}),
}
async run(): Promise<void> {
const {flags} = await this.parse(BanSafeEnforcementList)
const result = await listEnforcement(await this.platformClient(flags), {
projectId: flags.project,
...(flags.cursor === undefined ? {} : {cursor: flags.cursor}),
...(flags.limit === undefined ? {} : {limit: flags.limit}),
...(flags.rung === undefined
? {}
: {rung: flags.rung as BanSafeEnforcementRung}),
})
this.writeValue(result, renderEnforcement(result.data), flags)
}
}
39 changes: 39 additions & 0 deletions src/commands/bansafe/finding/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {Flags} from '@oclif/core'
import type {BanSafeFindingSeverity, BanSafeFindingStatus} from '@polymorfa/sdk'

import {BaseCommand} from '../../../base-command.js'
import {listFindings, renderFindings} from '../../../services/bansafe.js'

export default class BanSafeFindingList extends BaseCommand {
static override description =
'List measured BanSafe findings for one project.'
static override flags = {
cursor: Flags.string({description: 'Continue from an opaque page cursor.'}),
limit: Flags.integer({max: 100, min: 1}),
project: Flags.string({description: 'Project ID.', required: true}),
session: Flags.string({description: 'Match one session ID or name.'}),
severity: Flags.string({options: ['info', 'warning', 'critical']}),
status: Flags.string({options: ['open', 'acknowledged', 'resolved']}),
}
async run(): Promise<void> {
const {flags} = await this.parse(BanSafeFindingList)
const result = await listFindings(await this.platformClient(flags), {
projectId: flags.project,
...(flags.cursor === undefined ? {} : {cursor: flags.cursor}),
...(flags.limit === undefined ? {} : {limit: flags.limit}),
...(flags.session === undefined ? {} : {session: flags.session}),
...(flags.severity === undefined
? {}
: {severity: flags.severity as BanSafeFindingSeverity}),
...(flags.status === undefined
? {}
: {
status: flags.status as Exclude<
BanSafeFindingStatus,
'not_measured'
>,
}),
})
this.writeValue(result, renderFindings(result.data), flags)
}
}
42 changes: 42 additions & 0 deletions src/commands/bansafe/health-action/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {Flags} from '@oclif/core'
import type {BanSafeHealthActionStatus} from '@polymorfa/sdk'

import {BaseCommand} from '../../../base-command.js'
import {
listHealthActions,
renderHealthActions,
} from '../../../services/bansafe.js'

export default class BanSafeHealthActionList extends BaseCommand {
static override description =
'List durable BanSafe Health actions for one project.'

static override flags = {
cursor: Flags.string({description: 'Continue from an opaque page cursor.'}),
limit: Flags.integer({
description: 'Maximum actions to return (default 25).',
max: 100,
min: 1,
}),
project: Flags.string({description: 'Project ID.', required: true}),
session: Flags.string({description: 'Match one session ID or name.'}),
status: Flags.string({
description: 'Match one action status.',
options: ['pending', 'running', 'succeeded', 'failed', 'cancelled'],
}),
}

async run(): Promise<void> {
const {flags} = await this.parse(BanSafeHealthActionList)
const result = await listHealthActions(await this.platformClient(flags), {
projectId: flags.project,
...(flags.cursor === undefined ? {} : {cursor: flags.cursor}),
...(flags.limit === undefined ? {} : {limit: flags.limit}),
...(flags.session === undefined ? {} : {session: flags.session}),
...(flags.status === undefined
? {}
: {status: flags.status as BanSafeHealthActionStatus}),
})
this.writeValue(result, renderHealthActions(result.data), flags)
}
}
36 changes: 36 additions & 0 deletions src/commands/bansafe/health-policy/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {Flags} from '@oclif/core'

import {readJsonInput} from '../../../api/request.js'
import {BaseCommand} from '../../../base-command.js'
import {
renderHealthPolicy,
updateHealthPolicy,
} from '../../../services/bansafe.js'

export default class BanSafeHealthPolicyUpdate extends BaseCommand {
static override description =
'Replace the BanSafe Health rule for one project.'
static override flags = {
'idempotency-key': Flags.string({
description: 'Identify retries of this convergent policy update.',
required: true,
}),
Comment thread
purpshell marked this conversation as resolved.
Resolved
input: Flags.string({
description:
'Read the complete policy JSON object from a file or standard input (-).',
required: true,
}),
project: Flags.string({description: 'Project ID.', required: true}),
}

async run(): Promise<void> {
const {flags} = await this.parse(BanSafeHealthPolicyUpdate)
const result = await updateHealthPolicy(
await this.platformClient(flags),
flags.project,
await readJsonInput(flags.input),
{idempotencyKey: flags['idempotency-key']},
)
this.writeValue(result, renderHealthPolicy(result.data), flags)
}
}
20 changes: 20 additions & 0 deletions src/commands/bansafe/health-policy/view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {Flags} from '@oclif/core'

import {BaseCommand} from '../../../base-command.js'
import {getHealthPolicy, renderHealthPolicy} from '../../../services/bansafe.js'

export default class BanSafeHealthPolicyView extends BaseCommand {
static override description = 'View the BanSafe Health rule for one project.'
static override flags = {
project: Flags.string({description: 'Project ID.', required: true}),
}

async run(): Promise<void> {
const {flags} = await this.parse(BanSafeHealthPolicyView)
const result = await getHealthPolicy(
await this.platformClient(flags),
flags.project,
)
this.writeValue(result, renderHealthPolicy(result.data), flags)
}
}
Loading