Compare commits
1 Commits
e483d976e4
...
feat/feder
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
213ea1f39a |
@@ -4,12 +4,13 @@ import { CaService } from './ca.service.js';
|
|||||||
import { EnrollmentController } from './enrollment.controller.js';
|
import { EnrollmentController } from './enrollment.controller.js';
|
||||||
import { EnrollmentService } from './enrollment.service.js';
|
import { EnrollmentService } from './enrollment.service.js';
|
||||||
import { FederationController } from './federation.controller.js';
|
import { FederationController } from './federation.controller.js';
|
||||||
|
import { CapabilitiesController } from './server/verbs/capabilities.controller.js';
|
||||||
import { GrantsService } from './grants.service.js';
|
import { GrantsService } from './grants.service.js';
|
||||||
import { FederationClientService } from './client/index.js';
|
import { FederationClientService } from './client/index.js';
|
||||||
import { FederationAuthGuard } from './server/index.js';
|
import { FederationAuthGuard } from './server/index.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [EnrollmentController, FederationController],
|
controllers: [EnrollmentController, FederationController, CapabilitiesController],
|
||||||
providers: [
|
providers: [
|
||||||
AdminGuard,
|
AdminGuard,
|
||||||
CaService,
|
CaService,
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import 'reflect-metadata';
|
||||||
|
import { RequestMethod } from '@nestjs/common';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import type { FastifyRequest } from 'fastify';
|
||||||
|
import { FederationCapabilitiesResponseSchema, FEDERATION_VERBS } from '@mosaicstack/types';
|
||||||
|
import { FederationScopeError } from '../../../scope-schema.js';
|
||||||
|
import { FederationAuthGuard } from '../../federation-auth.guard.js';
|
||||||
|
import { CapabilitiesController } from '../capabilities.controller.js';
|
||||||
|
|
||||||
|
const VALID_SCOPE = {
|
||||||
|
resources: ['tasks', 'notes'],
|
||||||
|
excluded_resources: ['credentials'],
|
||||||
|
max_rows_per_query: 250,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const DEFAULTED_SCOPE = {
|
||||||
|
resources: ['memory'],
|
||||||
|
max_rows_per_query: 10,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
function makeRequest(scope: Record<string, unknown>): FastifyRequest {
|
||||||
|
return {
|
||||||
|
federationContext: {
|
||||||
|
grantId: 'grant-1',
|
||||||
|
peerId: 'peer-1',
|
||||||
|
subjectUserId: 'user-1',
|
||||||
|
scope,
|
||||||
|
},
|
||||||
|
} as FastifyRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CapabilitiesController', () => {
|
||||||
|
it('declares GET /api/federation/v1/capabilities', () => {
|
||||||
|
expect(Reflect.getMetadata('path', CapabilitiesController)).toBe(
|
||||||
|
'api/federation/v1/capabilities',
|
||||||
|
);
|
||||||
|
expect(Reflect.getMetadata('path', CapabilitiesController.prototype.getCapabilities)).toBe('/');
|
||||||
|
expect(Reflect.getMetadata('method', CapabilitiesController.prototype.getCapabilities)).toBe(
|
||||||
|
RequestMethod.GET,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is protected only by FederationAuthGuard', () => {
|
||||||
|
const guards = Reflect.getMetadata('__guards__', CapabilitiesController) as unknown[];
|
||||||
|
|
||||||
|
expect(guards).toEqual([FederationAuthGuard]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns resources, excluded resources, max rows, and M3 supported verbs from the active grant scope', () => {
|
||||||
|
const controller = new CapabilitiesController();
|
||||||
|
|
||||||
|
const response = controller.getCapabilities(makeRequest(VALID_SCOPE));
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
resources: ['tasks', 'notes'],
|
||||||
|
excluded_resources: ['credentials'],
|
||||||
|
max_rows_per_query: 250,
|
||||||
|
supported_verbs: [...FEDERATION_VERBS],
|
||||||
|
});
|
||||||
|
expect(FederationCapabilitiesResponseSchema.safeParse(response).success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies scope defaults without RBAC or resource filtering', () => {
|
||||||
|
const controller = new CapabilitiesController();
|
||||||
|
|
||||||
|
const response = controller.getCapabilities(makeRequest(DEFAULTED_SCOPE));
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
resources: ['memory'],
|
||||||
|
excluded_resources: [],
|
||||||
|
max_rows_per_query: 10,
|
||||||
|
supported_verbs: ['list', 'get', 'capabilities'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects invalid scope state instead of returning an invalid capabilities contract', () => {
|
||||||
|
const controller = new CapabilitiesController();
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
controller.getCapabilities(
|
||||||
|
makeRequest({
|
||||||
|
resources: [],
|
||||||
|
max_rows_per_query: 0,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toThrow(FederationScopeError);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Federation capabilities verb (FED-M3-07).
|
||||||
|
*
|
||||||
|
* Returns the read-only capability envelope for the active grant attached by
|
||||||
|
* FederationAuthGuard. This endpoint intentionally does not invoke native RBAC
|
||||||
|
* or ScopeService: an active grant is enough to ask what the grant allows.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
|
||||||
|
import type { FastifyRequest } from 'fastify';
|
||||||
|
import {
|
||||||
|
FEDERATION_VERBS,
|
||||||
|
type FederationCapabilitiesResponse,
|
||||||
|
type FederationVerb,
|
||||||
|
} from '@mosaicstack/types';
|
||||||
|
import { parseFederationScope } from '../../scope-schema.js';
|
||||||
|
import { FederationAuthGuard } from '../federation-auth.guard.js';
|
||||||
|
import '../federation-context.js';
|
||||||
|
|
||||||
|
@Controller('api/federation/v1/capabilities')
|
||||||
|
@UseGuards(FederationAuthGuard)
|
||||||
|
export class CapabilitiesController {
|
||||||
|
@Get()
|
||||||
|
getCapabilities(@Req() request: FastifyRequest): FederationCapabilitiesResponse {
|
||||||
|
if (!request.federationContext) {
|
||||||
|
throw new Error('Federation context missing after auth guard');
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = parseFederationScope(request.federationContext.scope);
|
||||||
|
|
||||||
|
return {
|
||||||
|
resources: [...scope.resources],
|
||||||
|
excluded_resources: [...scope.excluded_resources],
|
||||||
|
max_rows_per_query: scope.max_rows_per_query,
|
||||||
|
supported_verbs: [...FEDERATION_VERBS] satisfies FederationVerb[],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
65
docs/scratchpads/FED-M3-07-capabilities.md
Normal file
65
docs/scratchpads/FED-M3-07-capabilities.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# FED-M3-07 — Capabilities Verb Scratchpad
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Implement `GET /api/federation/v1/capabilities` in `apps/gateway/src/federation/server/verbs/capabilities.controller.ts`.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Add read-only capabilities controller under federation server verbs.
|
||||||
|
- Use `FederationAuthGuard` only; active grant is sufficient and no native RBAC/scope-service eval runs.
|
||||||
|
- Response shape: `{ resources, excluded_resources, max_rows_per_query, supported_verbs }` derived from grant scope.
|
||||||
|
- Register controller in `FederationModule`.
|
||||||
|
- Unit-test happy path, defaults, no-context guard seam, and invalid scope handling.
|
||||||
|
|
||||||
|
## Constraints / assumptions
|
||||||
|
|
||||||
|
- Issue: #462.
|
||||||
|
- Branch: `feat/federation-m3-verb-capabilities` from `origin/main` (`3eeed04e`).
|
||||||
|
- Depends on M3-03 auth guard; guard attaches `request.federationContext.scope` after active-grant validation.
|
||||||
|
- ASSUMPTION: `supported_verbs` is the M3 verb set from `@mosaicstack/types` (`list`, `get`, `capabilities`).
|
||||||
|
- ASSUMPTION: `filters`/`rate_limit` are intentionally omitted for FED-M3-07 because the card’s response shape lists only the four required fields.
|
||||||
|
- Budget: no explicit hard cap from orchestrator; working cap ~4K-8K tokens for card implementation + tests + PR cycle.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. Write controller unit tests first.
|
||||||
|
2. Implement controller and module registration.
|
||||||
|
3. Run scoped tests + typecheck/lint/format.
|
||||||
|
4. Run Codex code/security review and remediate.
|
||||||
|
5. Commit, queue guard, push, PR via wrapper.
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
- 2026-06-24: Intake complete; fresh worktree created from origin/main.
|
||||||
|
- 2026-06-24: Added `CapabilitiesController`, registered it in `FederationModule`, and added 5 unit tests.
|
||||||
|
- 2026-06-24: Code/security reviews passed with no findings.
|
||||||
|
|
||||||
|
## Tests run
|
||||||
|
|
||||||
|
- `pnpm --filter @mosaicstack/gateway test -- capabilities.controller.spec.ts` — PASS (5 tests).
|
||||||
|
- `pnpm --filter @mosaicstack/gateway typecheck` — PASS.
|
||||||
|
- `pnpm --filter @mosaicstack/gateway lint` — PASS.
|
||||||
|
- `pnpm format:check` — PASS.
|
||||||
|
- `pnpm typecheck` — PASS (41/41 turbo tasks).
|
||||||
|
- `pnpm lint` — PASS (23/23 turbo tasks).
|
||||||
|
- `pnpm test` — FAIL in pre-existing/live-DB integration suite: `apps/gateway/src/__tests__/cross-user-isolation.test.ts` cleanup hit PostgreSQL connection/schema state for the `messages` table. Changed capabilities tests passed; failure is outside FED-M3-07 surface. No `fleet-personas.spec` flake encountered.
|
||||||
|
|
||||||
|
## Review evidence
|
||||||
|
|
||||||
|
- `~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted` — PASS/approve, no findings.
|
||||||
|
- `~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted` — PASS, risk level none, no findings.
|
||||||
|
|
||||||
|
## Risks / blockers
|
||||||
|
|
||||||
|
- Full repo `pnpm test` may hit known `fleet-personas.spec` flake per orchestrator; ignore that specific flake if encountered.
|
||||||
|
- Previous card saw local DB schema issue in `cross-user-isolation.test.ts`; scoped capabilities tests should be authoritative for this surface.
|
||||||
|
|
||||||
|
## Acceptance evidence mapping
|
||||||
|
|
||||||
|
| Acceptance criterion | Evidence |
|
||||||
|
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| GET `/api/federation/v1/capabilities` exists | Route metadata test in `capabilities.controller.spec.ts`; scoped test PASS |
|
||||||
|
| Uses active-grant auth guard and no RBAC eval | Guard metadata test confirms only `FederationAuthGuard`; controller has no service injections/RBAC calls; scoped test PASS |
|
||||||
|
| Response enumerates resources/excluded/max rows/supported verbs from scope | Happy-path/default scope tests + response schema parse; scoped test PASS |
|
||||||
|
| Read-only/no persistence side effects | Controller only parses request `federationContext.scope` and returns a DTO; no DB/service dependency; code review PASS |
|
||||||
Reference in New Issue
Block a user