PAR-210
Admin-scoped firm disband/rename endpoints + service credential (treasury-rest-api)
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 byjwt-secret, minted byAuthService). The token model isVerifiedToken(keyId, ownerUuid, keyType, accountId, firmId)— there is no admin/scope/role claim, and write endpoints are firm-scoped (/firms/me);POST /transfersmoves 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 (
AccountControlleris read-only: balance / by-player / transactions). - economy-explorer has no runtime client to treasury-rest-api (it reads MariaDB directly;
lib/jwt.tsonly mirrorsAuthService.buildJwtto mint user keys). So the explorer→rest-api call path + credential must be built from scratch.
Scope
- Admin scope in the token model. Add an admin/service scope claim (e.g.
scope:["admin"]or akeyType=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. - Endpoints (replicate
FirmServiceImplfaithfully — 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 thefirm_accountslink; thenUPDATE 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(markaccounts.is_archived), not raw balance writes.
- 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.
- 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→ k8sapp-secrets/equivalent in thetreasury-rest-apidev & prod namespaces). Mint one long-lived admin-scoped service token per env, signed by that env'sjwt-secret, persisted as anapi_keyrow with the admin scope (so it's revocable/rotatable like any key, via the existingAuthServiceadmin rotate). - Store each env's token in a k8s secret consumed by economy-explorer in the matching env (dev →
developmentns, 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 sendsAuthorization: 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
- commit3f8d8e1 Admin write endpoints: arbitrary transfer, account rename/owner/archive, firm details (PAR-217)MCCitiesNetwork/treasury-rest-api
- commit565fbb3 Admin firm-management tool: disband / rename (PAR-209)MCCitiesNetwork/economy-explorer
- PR · merged#24 Release develop → main: admin firm-management tool (PAR-209)MCCitiesNetwork/economy-explorer
- commited89ba1 Merge pull request #15 from MCCitiesNetwork/developMCCitiesNetwork/treasury-rest-api
- commit7915c24 Admin firm disband/rename endpoints (SERVICE-scoped, hidden from Swagger) (PAR-210)MCCitiesNetwork/treasury-rest-api
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}/disbandand/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.executeTransfercore extracted fromTransferService(existing path unchanged).- Endpoints
@Hidden→ excluded from OpenAPI/Swagger (verified byAdminFirmDocsHiddenIT). - economy-schema V15 adds the
SERVICEenum value. - Tests:
AdminFirmServiceIT+AdminFirmDocsHiddenIT; full suite green.
Remaining (deferred — the provisioning judgement call):
- Apply economy-schema V15 to each DB (manual
workflow_dispatch) before the SERVICE scope validates. - Mint a SERVICE-scoped token per env (signed by that env's
jwt-secret,api_keysrow withkey_type='SERVICE'), store as a k8s secret (TREASURY_ADMIN_TOKEN+TREASURY_API_BASE_URL) consumed by economy-explorer in dev + prod. - 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
develop→main: economy-flyway #6 (V15) and treasury-rest-api #15 (admin endpoints); prod deployedproduction-sha-ed89ba1(ArgoCDtreasury-rest-api-productionHealthy, both networks 2/2). - Schema V15 (
SERVICEenum) live on dev + both prod DBs. - SERVICE tokens minted + stored as k8s secrets: dev
economy-explorer-treasury-admin(development), prodeconomy-explorer-{democracycraft,statecraft}-treasury-admin(production) — eachTREASURY_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-docs0admin/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