staff-portal-cli
v0.1.0
Published
Permission-scoped Nummus Staff Portal CLI for people and automation.
Downloads
401
Readme
Staff Portal CLI
A standalone Node.js 22.14+ CLI for Nummus Staff Portal. It follows the published
afx-markets and igi-cli patterns: Clerk OAuth browser login with PKCE, a local
credential store, discoverable operations and JSON output. The portal remains the
source of truth for staff permissions.
Install and discover
After this package is published:
npx staff-portal-cli --help
npx staff-portal-cli commands list
npx staff-portal-cli commands describe assignments.set
npm install -g staff-portal-cli
staff-portal --versionBefore publication, build and run the package from the repository:
npm pack ./cli
npx --yes --package ./staff-portal-cli-0.1.0.tgz staff-portal commands listBoth staff-portal and staff-portal-cli executable names are provided. Discovery
works offline without authentication. Each command has a description, permission
key and JSON input schema. permission describes the boundary; ticket ownership,
status, company membership and external reply requirements also apply.
Login, status and logout
staff-portal auth login
staff-portal auth login --no-open
staff-portal auth status
staff-portal auth logoutSign into the portal once first so Clerk is linked to your active staff record.
Login opens the configured Clerk consent page, checks a random OAuth state and
exchanges the authorization code with PKCE S256. The callback binds only to
127.0.0.1:8766; close competing logins if that port is busy. With --no-open, copy
the URL from stderr into a browser on the same machine as the CLI. Login times
out after two minutes. An existing credential file must be logged out before a
new login, so an existing grant is not silently abandoned.
Credentials are saved atomically in ~/.staff-portal-cli/auth.json, with mode 0600
and a 0700 parent directory on POSIX systems. The store rejects symlink files,
foreign ownership and readable-by-others files. Use an account with a private home
directory. On Windows, the CLI uses built-in Windows PowerShell to set a protected
NTFS DACL granting access only to the current user, before writing tokens. Reads
validate ownership and the file/parent ACLs and reject permissions for any other
principal; unavailable native ACL operations fail closed. Administrator privileges
can bypass ordinary user access controls, as with POSIX root.
Access tokens refresh before expiry when the provider supplies a refresh
token. There are no tokens or client secrets in command arguments, stdout or logs.
A grant that fails staff eligibility is retained locally so auth logout can
revoke it.
Login, refresh and logout share a credential lock across CLI processes, so logout
waits for an in-flight refresh before revoking and deleting its resulting tokens.
A competing process waits up to 60 seconds, then returns storage_locked; retry
after the other process finishes. Locks are never taken over automatically. If a
process crashes, stop all Staff Portal CLI processes before removing only the
~/.staff-portal-cli/auth.json.lock directory. Keep auth.json so logout can revoke
the saved grant, then retry the command. Never remove a lock held by a live process.
auth status verifies live provider and staff access and writes a
cli.status_checked audit before returning identity and permission fields. If the
audit cannot be saved, those fields are not returned. Without saved credentials it
returns authenticated: false. Expired/revoked sessions and access denial produce
JSON errors. auth logout revokes the OAuth grant (including its refresh token)
on the server and deletes local credentials. If the server cannot be reached or
refresh fails, it still deletes local credentials and returns a revocation_unverified JSON error
(exit 1). Its error details contain revoked: false and
localCredentialsRemoved: true; remote revocation has not been verified.
Use --url https://portal.example or STAFF_PORTAL_URL for another portal.
Credentials are bound to that exact origin. HTTPS is required except for local
loopback development. Log out with the saved portal URL before changing portals.
OAuth issuer configuration must use HTTPS even in local development. No demo or
skip-auth flag is supported by the CLI API.
Tickets, assignments and comments
Inputs are JSON objects supplied through --input or --input-file:
staff-portal tickets list --input '{"page":1,"pageSize":25,"status":"open"}'
staff-portal tickets get --input '{"ticketId":"TICKET_ID"}'
staff-portal tickets create --input '{"subject":"Account query","body":"Please help with account access.","companyId":"COMPANY_ID"}'
staff-portal assignments options
staff-portal assignments set --input-file assignment.json
staff-portal comments list --input '{"ticketId":"TICKET_ID"}'
staff-portal comments add --input '{"ticketId":"TICKET_ID","body":"Internal investigation note"}'
staff-portal comments reply --input '{"ticketId":"TICKET_ID","body":"Public reply to the requester"}'
staff-portal tickets priority --input '{"ticketId":"TICKET_ID","priority":"high"}'
staff-portal tickets resolve --input '{"ticketId":"TICKET_ID"}'
staff-portal tickets close --input '{"ticketId":"TICKET_ID"}'assignments.set replaces the primary owner and full collaborator list. Fetch the
current ticket first and copy its assignmentSnapshot string into the input:
{
"ticketId": "TICKET_ID",
"companyId": "COMPANY_ID",
"assignedToStaffId": null,
"collaboratorStaffIds": ["STAFF_ID"],
"assignmentSnapshot": "COPY_EXACTLY_FROM_TICKETS_GET"
}A null owner clears the primary assignment. A stale snapshot returns a conflict
instead of replacing newer choices. Whitespace-only identifiers are rejected
before dispatch; clear collaborators with an explicit empty array. Eligible staff
come from assignments.options and must belong to the selected company. Customer
tickets retain their company.
Ticket creation uses company assignment defaults. Internal notes remain internal;
public replies use the existing email/API delivery paths and notification rules.
The existing participant and primary-assignee restrictions also apply to users
who can view every ticket. Replies that were saved but whose email delivery failed
return completed: true, delivery: "failed"; check the portal before resending.
Closing or resolving a ticket whose customer email could not be queued returns
completed: true, notification: "email-pending" with a warning message. The ticket
update succeeded; check the portal before retrying the notification.
SLAs
staff-portal sla policies
staff-portal sla hours
staff-portal sla report --input '{"company":"COMPANY_ID"}'
staff-portal sla pause --input '{"ticketId":"TICKET_ID"}'
staff-portal sla resume --input '{"ticketId":"TICKET_ID"}'
staff-portal sla start
staff-portal sla policy-set --input '{"companyId":"COMPANY_ID","priority":"high","firstResponseMinutes":30,"nextResponseMinutes":60,"resolutionMinutes":480,"enabled":true}'
staff-portal sla policy-delete --input '{"companyId":"COMPANY_ID","priority":"high"}'
staff-portal commands describe sla.hours-set
staff-portal sla hours-set --input-file business-hours.jsonHours input supplies companyId, timezone and all seven days' monEnabled,
monStart, monEnd, through sunEnabled, sunStart, sunEnd. Times use HH:MM;
disabled days still require start/end values. Duration limits and timezone/hour
validation match the portal. SLA reports require tickets.view_sla_reports, and
remain participant-scoped unless the actor has tickets.view_all. Settings require
tickets.manage_sla; pausing/resuming requires tickets.manage_sla_clock and ticket
visibility. Reading an SLA report writes the existing export audit event.
Automation contract
Stdout contains exactly one JSON envelope. Browser instructions go to stderr.
--json is accepted; JSON is always the output format.
{"ok":true,"data":{"completed":true,"ticketId":"TICKET_ID","location":"/tickets/TICKET_ID?created=1"}}{"ok":false,"error":{"code":"forbidden","message":"Service rejected the request (HTTP 403)","status":403}}Success exits 0; errors exit 1. Error codes include invalid_arguments,
unknown_command, invalid_input, unauthenticated, invalid_token,
staff_access_denied, forbidden, not_found, conflict, operation_rejected,
origin_mismatch, network_error, revocation_unverified and auth_not_configured. Unexpected errors and
non-JSON provider responses are redacted. Reads and OAuth transport time out after
15 seconds, including portal login/logout POSTs; ticket and SLA mutations have no
client abort timer. Requests never follow redirects
carrying credentials. Writes are not automatically retried: a lost response may
follow a committed operation. Inspect the ticket before retrying.
The CLI API uses /api/v1/cli/auth-config, /auth/login, /auth/status,
/auth/logout, /commands, and /run (relative to /api/v1/cli). /run accepts
{"command":"tickets.get","input":{"ticketId":"..."}}. Inputs are strict and
bounded to 32 KiB. The server uses the same schemas returned by discovery.
Uploads, attachments download, rich text, mentions, reply drafts and external requester creation are not CLI commands in this release; use the portal for those. SLA reports use the portal's existing report size/selection behavior. Temporary break-glass grants are unavailable through OAuth because it does not prove recent MFA; use the authenticated browser workflow for emergency recovery.
Portal configuration and release
Create a public Clerk OAuth application in the portal's existing Clerk
instance, enable profile email offline_access scopes and register exactly:
http://127.0.0.1:8766/callbackConfigure these server-only values as Worker secrets on nummus-staff-portal
(and as environment variables in .env.local for local work), using the values
from that application:
STAFF_CLI_OAUTH_ISSUER=https://clerk.your-portal-domain.example
STAFF_CLI_OAUTH_CLIENT_ID=the-public-client-id
STAFF_CLI_OAUTH_APPLICATION_ID=the-clerk-oauth-application-idFrom the repository root, set each value using Wrangler's interactive prompt:
npx wrangler secret put STAFF_CLI_OAUTH_ISSUER --config wrangler.jsonc
npx wrangler secret put STAFF_CLI_OAUTH_CLIENT_ID --config wrangler.jsonc
npx wrangler secret put STAFF_CLI_OAUTH_APPLICATION_ID --config wrangler.jsoncWrangler preserves Worker secrets across portal deployments. Do not configure
these as plaintext dashboard variables: deployment replaces variables absent
from wrangler.jsonc. Worker secrets are the persistent production source for
this configuration, like the existing CLERK_SECRET_KEY; the deployment workflow
does not delete them. Values are not committed to the repository or CLI package.
After configuration and deployment, complete the live OAuth checks below before
enabling the npm release gate.
The application ID is used only for server-side revocation. No client secret,
additional D1 permission, migration or CLI-specific role grant is needed. The CLI
requests offline_access so Clerk issues a refresh token for renewing the session.
The client requires this exact scope set as well as the registered callback.
The API fails closed with auth_not_configured until configured. Each request verifies the
OAuth token with Clerk, rejects wrong-client/expired/revoked tokens and reloads
current ordinary D1 staff permissions. It does not use the customer support API.
Mutations reuse protected portal actions and their audits and notifications.
Protected ticket detail/comment reads write ticket.cli_viewed audits; ticket
lists, assignment options, SLA policies/hours and reports also write success
audits before returning records. These reads, successful mutations and audited
permission denials refresh the audit page. If a success audit cannot be written,
no protected record is returned.
Ticket and comment results omit raw email-provider and scanner failure messages;
delivery and scan statuses remain available without internal diagnostics.
Staff-authored replies omit staff email addresses; external-author contact
addresses remain available to authorised ticket viewers.
The API reserves hashed source and credential-candidate budgets before contacting
Clerk. It reuses the existing Cloudflare rate-limit bindings with separate CLI
keys, so product support API quotas are unaffected. Authentication traffic is
limited to 60 requests per source per minute and 120 per credential candidate;
public OAuth configuration has a separate 120-per-source read budget. Verified
actors share separate budgets of 120 reads and 30 mutations per minute across
their tokens and source addresses. Read commands use the read budget even though
the CLI transports them with POST. Cloudflare limits apply per location; responses
use JSON rate_limit_exceeded errors, HTTP 429 and Retry-After: 60.
Authenticated quota denials write cli.rate_limit_denied; denied delegated
mutations write cli.mutation_denied with the actor and ticket/command identity.
Both refresh audit history without changing the permission or quota decision.
Discovery includes readOnly for each command. Read requests and OAuth transport
use a 15-second timeout. Portal writes do not impose a client abort timer: aborting
fetch cannot cancel a server transaction or an email send. The CLI never retries
a write automatically. Lost connections, malformed results and server failures
return outcome_unknown because the operation may already have committed. Check
the ticket and its replies in the portal before retrying. Writes are not
idempotent, and cancelling the CLI also leaves their outcome uncertain.
Authentication success and denial audits refresh /audit only after the audit
entry is written.
Run npm test, npm run typecheck, npm run lint, npm run cli:pack and the portal
build gates before release. The CLI's dependency-free Node tests can also run with
npm --prefix cli test. Configure the matching portal API and verify OAuth with
an actual staff login before releasing it for staff use.
Automatic npm publishing
The publishing workflow validates the CLI
on pull requests and publishes staff-portal-cli to npm on pushes to main that
change cli/, its release-status script or the workflow itself. It can also be
started manually from main in GitHub Actions. Other branches cannot publish.
The publish step uses the repository's existing NPM_TOKEN secret as
NODE_AUTH_TOKEN. The token must have permission to publish this package and,
for a granular token, allow automated publishing with bypass 2FA. Credentials are
available only to the publish step. No token belongs in the repository or package.
The workflow runs the dependency-free CLI and release tests, checks package
contents, then queries npm for the exact version in cli/package.json. An existing
version is skipped; registry failures stop the release. New stable versions are
published with public access and the latest tag, then verified on the registry.
Concurrent publishing runs are serialised. Before publishing a missing version,
the job also checks npm's current latest version and skips equal or older
versions, so a slower validation run cannot move latest backwards. Failure to
read or validate latest stops publication; verification checks that the exact
version exists, independently of whether it would now be eligible to publish.
Publication is disabled until the repository Actions variable
STAFF_CLI_RELEASE_READY is set to exactly true. After deploying the matching
portal API and configuring Clerk, verify a real staff login, status, a permitted
command, a denied command and logout against that deployment, then set this
variable in GitHub Settings → Secrets and variables → Actions → Variables. This
records the operator's release-readiness approval. Both automatic and manual
runs respect the gate; keep it unset while deployment or verification is blocked.
The initial merge therefore validates the package without publishing it.
To release a change, bump the stable x.y.z version in cli/package.json and merge
it into main. npm versions are immutable, so fixing a published release requires
another version bump. Once the readiness variable is enabled, dispatch the
workflow from main to publish 0.1.0 if it is still available. Later matching
main pushes publish new versions automatically. Clear the variable before a
release that needs a new portal deployment, and re-enable it after verification.
