feat(federation): grants service CRUD + status transitions (FED-M2-06)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful

- Add 'pending' to grantStatusEnum (pending → active → revoked/expired)
- Update federationGrants default status to 'pending'
- Add migration 0009_federation_grant_pending.sql
- GrantsService: createGrant, getGrant, listGrants, activateGrant, revokeGrant, expireGrant
- Invalid transitions throw ConflictException; missing grants throw NotFoundException
- CreateGrantDto validates scope via parseFederationScope before insert
- Full unit test coverage for all status transitions and edge cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jarvis
2026-04-21 22:51:05 -05:00
parent 1038ae76e1
commit 0d9c1ce2e7
8 changed files with 3939 additions and 4 deletions

View File

@@ -0,0 +1,2 @@
ALTER TYPE "public"."grant_status" ADD VALUE 'pending' BEFORE 'active';--> statement-breakpoint
ALTER TABLE "federation_grants" ALTER COLUMN "status" SET DEFAULT 'pending';

File diff suppressed because it is too large Load Diff

View File

@@ -64,6 +64,13 @@
"when": 1776822435828,
"tag": "0008_smart_lyja",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1745280000000,
"tag": "0009_federation_grant_pending",
"breakpoints": true
}
]
}

View File

@@ -604,11 +604,12 @@ export const peerStateEnum = pgEnum('peer_state', ['pending', 'active', 'suspend
/**
* Lifecycle state of a federation grant.
* - pending: created but not yet activated (awaiting cert enrollment, M2-07)
* - active: grant is in effect
* - revoked: manually revoked before expiry
* - expired: natural expiry (expires_at passed)
*/
export const grantStatusEnum = pgEnum('grant_status', ['active', 'revoked', 'expired']);
export const grantStatusEnum = pgEnum('grant_status', ['pending', 'active', 'revoked', 'expired']);
/**
* A registered peer gateway identified by its Step-CA certificate CN.
@@ -696,7 +697,7 @@ export const federationGrants = pgTable(
scope: jsonb('scope').notNull(),
/** Current grant lifecycle state. */
status: grantStatusEnum('status').notNull().default('active'),
status: grantStatusEnum('status').notNull().default('pending'),
/** Optional hard expiry. NULL means the grant does not expire automatically. */
expiresAt: timestamp('expires_at', { withTimezone: true }),