Paradaux

PAR-210

0

Admin-scoped firm disband/rename endpoints + service credential (treasury-rest-api)

DoneUnassignedTreasury APIFeature

Backend half of PAR-209 (economy-explorer admin firm tool). treasury-rest-api is the ledger-authoritative tier and already touches both Treasury accounts (AccountMapper) and the business firm/firm_accounts tables (FirmMapper), plus the idempotent POST /api/v1/transfers — so the disband/rename orchestration belongs here, called by the explorer.

Why this is blocked-by-design today

  • Auth is HS256 JWT API keys (JwtTokenVerifier/JwtAuthFilter, signed by jwt-secret, minted by AuthService). The token model is VerifiedToken(keyId, ownerUuid, keyType, accountId, firmId)there is no admin/scope/role claim, and write endpoints are firm-scoped (/firms/me); POST /transfers moves money only from the token's own account. So no current credential can disband an arbitrary firm.
  • There are no admin endpoints and no account-archive endpoint (AccountController is read-only: balance / by-player / transactions).
  • economy-explorer has no runtime client to treasury-rest-api (it reads MariaDB directly; lib/jwt.ts only mirrors AuthService.buildJwt to mint user keys). So the explorer→rest-api call path + credential must be built from scratch.

Scope

  1. Admin scope in the token model. Add an admin/service scope claim (e.g. scope:["admin"] or a keyType=SERVICE) to the minted JWT + VerifiedToken + verifier, and gate the new endpoints on it (reject normal firm/owner keys). Keep it distinct from PERSONAL/BUSINESS/GOVERNMENT keys.
  2. Endpoints (replicate FirmServiceImpl faithfully — see PAR-209 for the exact flow):
    • POST /api/v1/admin/firms/{firmId}/disband — resolve-or-create proprietor PERSONAL account; per firm account (firm_accounts WHERE removed_at IS NULL): transfer positive balance → proprietor personal (reuse the idempotent transfer service, requester/authorizer = proprietor, reason "Firm disbanded"), archive the Treasury account, soft-delete the firm_accounts link; then UPDATE firm SET is_archived=1, default_account_id=NULL. Return a per-account result (amount swept, dest account, idempotency key) for the caller's audit. Idempotent + resumable (skip already-archived/removed accounts).
    • POST /api/v1/admin/firms/{firmId}/rename — name rules from the plugin: length 2–32, regex [A-Za-z0-9 _.\-]{2,32}, no leading digit, case-insensitive uniqueness (allow case-only self-rename); UPDATE firm SET display_name=?.
    • Add the underlying account-archive operation (currently missing) through the service/AccountMapper (mark accounts.is_archived), not raw balance writes.
  3. Service-layer + idempotency. Keep controller→service→mapper (note PAR-113). Every transfer carries an Idempotency-Key so retries can't double-pay; the disband is safe to re-run. Treasury IPC vs firm-table writes can't share one txn — log each step for reconciliation.
  4. Rate-limit + server-side audit on the admin endpoints (they're privileged); log keyId + firmId + per-account results.

Credential scheme + dev/prod provisioning (the "set up credentials" ask)

Once (1) lands, provisioning is mechanical and per-env (no secret values need printing):

  • Each env already has an HS256 jwt-secret (application-{uat,prod}.yaml → k8s app-secrets/equivalent in the treasury-rest-api dev & prod namespaces). Mint one long-lived admin-scoped service token per env, signed by that env's jwt-secret, persisted as an api_key row with the admin scope (so it's revocable/rotatable like any key, via the existing AuthService admin rotate).
  • Store each env's token in a k8s secret consumed by economy-explorer in the matching env (dev → development ns, prod → prod ns), e.g. TREASURY_ADMIN_TOKEN + TREASURY_API_BASE_URL (the in-cluster rest-api service URL per env). economy-explorer gains a small server-only rest-api client (admin actions only) that sends Authorization: Bearer <token>.
  • Rotation: reuse AuthService.adminForceRotate(keyId); re-write the k8s secret; restart/rollout the explorer.

⚠️ Sequencing: the admin scope must exist and validate first — minting/storing a privileged service token before (1) ships would put an unvalidated prod credential in place with nothing enforcing its scope. Provision the secrets in the same change that adds + gates the endpoints, across dev and prod together.

Acceptance

  • A SERVICE/admin-scoped token can disband + rename any firm via the new endpoints; PERSONAL/BUSINESS/GOVERNMENT keys are rejected (403).
  • Disband matches the plugin outcome (balances swept via ledger, accounts archived, links soft-deleted, firm archived); idempotent/resumable; per-account results returned.
  • Admin token provisioned + stored as a k8s secret and consumed by economy-explorer in both dev and prod; revocable/rotatable.
  • Controller→service→mapper layering; rate-limited; server-side audit per call.

Sibling: PAR-209 (economy-explorer UI + orchestration + explorer_audit).

Resources

Comments

tesks · Jun 16, 2026, 8:18 PM

Sibling/parent: PAR-209 — the economy-explorer admin firm-management tool that consumes these endpoints. Full plugin control-flow to replicate is documented there.

tesks · Jun 16, 2026, 8:36 PM

Code built, tested, pushed to develop (treasury-rest-api 7915c24, economy-schema 2d78712):

  • SERVICE key scope (no acc/firm claim) in JwtTokenVerifier; admin endpoints gate on it.
  • POST /api/v1/admin/firms/{firmId}/disband and /rename — faithful to the plugin flow; disband is a single atomic DB transaction (no orphan risk), idempotent, 422 if a positive balance has no proprietor personal account.
  • executeTransfer core extracted from TransferService (existing path unchanged).
  • Endpoints @Hidden → excluded from OpenAPI/Swagger (verified by AdminFirmDocsHiddenIT).
  • economy-schema V15 adds the SERVICE enum value.
  • Tests: AdminFirmServiceIT + AdminFirmDocsHiddenIT; full suite green.

Remaining (deferred — the provisioning judgement call):

  1. Apply economy-schema V15 to each DB (manual workflow_dispatch) before the SERVICE scope validates.
  2. Mint a SERVICE-scoped token per env (signed by that env's jwt-secret, api_keys row with key_type='SERVICE'), store as a k8s secret (TREASURY_ADMIN_TOKEN + TREASURY_API_BASE_URL) consumed by economy-explorer in dev + prod.
  3. economy-explorer (PAR-209) server-side client + UI.

tesks · Jun 17, 2026, 4:15 AM

Dev provisioning blocked (infra): applying V15 to dc-test-server via the Flyway Migrate workflow fails at No database found to handle <url> — the dc-test-server FLYWAY_URL GitHub Environment secret is malformed (no valid jdbc: scheme prefix; confirmed it's not a driver issue — fails with both MySQL+MariaDB drivers present). The jdbc:mysql:// prod targets (democracycraft/statecraft) are unaffected. Fix = correct the dc-test-server FLYWAY_URL env secret to a full JDBC URL (cf. the dev app's working jdbc:mariadb://<host>/economy). Once fixed, re-run Migrate(dc-test-server) then mint the dev SERVICE token + k8s secret.

tesks · Jun 17, 2026, 4:44 AM

Dev provisioning complete (dc-test-server). Applied V15 by running Flyway locally against the in-cluster dev DB (mariadb.minecraft) via port-forward (the dc-test-server GitHub-env FLYWAY_URL is still malformed and should be fixed separately — also brought dev current from V10→V15). Minted a SERVICE token (api_keys key_id=3, no acc/firm) signed with the dev JWT_SECRET, stored as k8s secret economy-explorer-treasury-admin (development ns: TREASURY_ADMIN_TOKEN + TREASURY_API_BASE_URL=http://treasury-rest-api.development.svc.cluster.local). Verified against the live dev rest-api (already on image sha-7915c24): SERVICE token → 404 FIRM_NOT_FOUND (auth+scope OK), no token → 401, and /v3/api-docs contains 0 admin/firms (hidden). Deployment env-var wiring deferred to PAR-209 (no consumer yet). Prod (democracycraft/statecraft) not done — awaiting go-ahead.

tesks · Jun 17, 2026, 5:53 AM

Dev end-to-end verified through the deployed rest-api (sha-7915c24) with the provisioned SERVICE token, against a seeded throwaway firm: disband → 200, swept 500.00 firm→proprietor-personal via the ledger (real account_balances_mat trigger applied: firm acct 0.00, personal 500.00), firm account archived, firm↔account link soft-deleted, firm archived; rename → 200 (persisted); duplicate name → 409 NAME_TAKEN; leading-digit name → 400 INVALID_BODY. All test rows cleaned up (0 residual). Dev provisioning + functionality fully confirmed.

tesks · Jun 17, 2026, 11:02 AM

Prod rollout complete — all environments provisioned + verified.

  • Released developmain: economy-flyway #6 (V15) and treasury-rest-api #15 (admin endpoints); prod deployed production-sha-ed89ba1 (ArgoCD treasury-rest-api-production Healthy, both networks 2/2).
  • Schema V15 (SERVICE enum) live on dev + both prod DBs.
  • SERVICE tokens minted + stored as k8s secrets: dev economy-explorer-treasury-admin (development), prod economy-explorer-{democracycraft,statecraft}-treasury-admin (production) — each TREASURY_ADMIN_TOKEN + TREASURY_API_BASE_URL.
  • Verified on all three deployed instances: SERVICE token → 404 FIRM_NOT_FOUND (auth+scope OK), no token → 401, /v3/api-docs 0 admin/firms (hidden). Dev also passed a full disband/rename functional run (balance swept via ledger) with cleanup.

Deployment env-var wiring into the economy-explorer Deployments is intentionally left to PAR-209 (the consumer). Separate infra nit: dc-test-server FLYWAY_URL GitHub-env secret is malformed (worked around via local Flyway run) — fix for future dev workflow deploys.

Activity

  • ParadauxIO linked a commit — Commit 3f8d8e1 — Admin write endpoints: arbitrary transfer, account rename/owner/archive, firm details (PAR-217)
  • ParadauxIO linked a commit — Commit 3f8d8e1 — Admin write endpoints: arbitrary transfer, account rename/owner/archive, firm details (PAR-217)
  • ParadauxIO linked a commit — Commit 565fbb3 — Admin firm-management tool: disband / rename (PAR-209)
  • ParadauxIO linked a pull request — PR #24 merged — Release develop → main: admin firm-management tool (PAR-209)
  • ParadauxIO linked a pull request — PR #24 open — Release develop → main: admin firm-management tool (PAR-209)
  • ParadauxIO linked a commit — Commit 565fbb3 — Admin firm-management tool: disband / rename (PAR-209)
  • tesks commented
  • ParadauxIO linked a commit — Commit ed89ba1 — Merge pull request #15 from MCCitiesNetwork/develop
  • ParadauxIO linked a commit — Commit 7915c24 — Admin firm disband/rename endpoints (SERVICE-scoped, hidden from Swagger) (PAR-210)
  • ParadauxIO changed status to Status → Done
  • tesks commented
  • tesks commented
  • tesks commented
  • tesks changed status to Status → Pending Release
  • tesks commented
  • ParadauxIO linked a commit — Commit 7915c24 — Admin firm disband/rename endpoints (SERVICE-scoped, hidden from Swagger) (PAR-210)
  • tesks changed status to Status → In Progress
  • tesks commented
  • tesks created the issue