feat(p8): /v1/calendar REST + kernel-client methods + @insignia/iios-meeting-web

Task 8.6: CalendarController exposes schedule/request(+fromCallback/fromClaim)/
list/get/consent/transcript/summarize/action-items/providers(+sync) (session-auth).
kernel-client RestClient gains scheduleMeeting/requestMeeting/listMeetings/
getMeeting/setConsent/generateTranscript/summarizeMeeting/listActionItems/
connect+syncCalendarProvider + Meeting/MeetingParticipant/MeetingActionItem types.
New @insignia/iios-meeting-web (MeetingProvider + useMeetings/useMeeting/useSchedule/
useConsent/useSummarize/useActionItems). Boundary: meeting-web -> contracts +
kernel-client (fails on meeting-web->service).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 18:09:18 +05:30
parent 83369f2055
commit ca9498ce83
12 changed files with 366 additions and 1 deletions
@@ -0,0 +1,89 @@
import { BadRequestException, Body, Controller, Get, Headers, Param, Post, Query } from '@nestjs/common';
import { IiosConsentStatus, IiosMeetingType } from '@prisma/client';
import { CalendarService } from './calendar.service';
import { CalendarAdapter } from './calendar-adapter';
import { SessionVerifier } from '../platform/session.verifier';
import type { MessagePrincipal } from '../identity/actor.resolver';
import { ConsentDto, RequestMeetingDto, ScheduleMeetingDto } from './calendar.dto';
@Controller('v1/calendar')
export class CalendarController {
constructor(
private readonly calendar: CalendarService,
private readonly adapter: CalendarAdapter,
private readonly session: SessionVerifier,
) {}
@Post('meetings')
async schedule(@Body() body: ScheduleMeetingDto, @Headers('authorization') auth?: string) {
return this.calendar.scheduleDirect(this.principal(auth), { ...body, meetingType: body.meetingType as IiosMeetingType });
}
@Post('requests')
async request(
@Body() body: RequestMeetingDto,
@Query('fromCallback') fromCallback?: string,
@Query('fromClaim') fromClaim?: string,
@Headers('authorization') auth?: string,
) {
const principal = this.principal(auth);
if (fromCallback) return this.calendar.fromCallback(principal, fromCallback, body.requestedWindow);
if (fromClaim) return this.calendar.fromEventClaim(principal, fromClaim, body.requestedWindow);
return this.calendar.requestMeeting(principal, {
requestedWindow: body.requestedWindow,
meetingType: body.meetingType as IiosMeetingType | undefined,
interactionId: body.interactionId,
targetActorId: body.targetActorId,
});
}
@Get('meetings')
async list(@Headers('authorization') auth?: string) {
return this.calendar.listMeetings(this.principal(auth));
}
@Get('meetings/:id')
async get(@Param('id') id: string, @Headers('authorization') auth?: string) {
this.principal(auth);
return this.calendar.getMeeting(id);
}
@Post('meetings/:id/consent')
async consent(@Param('id') id: string, @Body() body: ConsentDto, @Headers('authorization') auth?: string) {
this.principal(auth);
return this.calendar.setConsent(id, body.actorRefId, body.status as IiosConsentStatus);
}
@Post('meetings/:id/transcript')
async transcript(@Param('id') id: string, @Headers('authorization') auth?: string) {
return this.calendar.generateTranscript(id, this.principal(auth));
}
@Post('meetings/:id/summarize')
async summarize(@Param('id') id: string, @Headers('authorization') auth?: string) {
return this.calendar.summarize(id, this.principal(auth));
}
@Get('meetings/:id/action-items')
async actionItems(@Param('id') id: string, @Headers('authorization') auth?: string) {
this.principal(auth);
return this.calendar.listActionItems(id);
}
@Post('providers')
async connect(@Headers('authorization') auth?: string) {
return this.adapter.connectProvider(this.principal(auth));
}
@Post('providers/:id/sync')
async sync(@Param('id') id: string, @Headers('authorization') auth?: string) {
this.principal(auth);
return this.adapter.syncOnce(id);
}
private principal(authorization?: string): MessagePrincipal {
const token = (authorization ?? '').replace(/^Bearer\s+/i, '');
if (!token) throw new BadRequestException('Authorization bearer token is required');
return this.session.verify(token);
}
}
@@ -0,0 +1,26 @@
import { IsArray, IsIn, IsObject, IsOptional, IsString } from 'class-validator';
const MEETING_TYPES = ['ZOOM', 'PHONE', 'IN_PERSON', 'CALLBACK', 'INTERNAL'] as const;
const CONSENT = ['UNKNOWN', 'GRANTED', 'DENIED', 'REVOKED'] as const;
export class ScheduleMeetingDto {
@IsIn(MEETING_TYPES) meetingType!: (typeof MEETING_TYPES)[number];
@IsString() title!: string;
@IsString() startAt!: string;
@IsOptional() @IsString() endAt?: string;
@IsOptional() @IsString() timezone?: string;
@IsOptional() @IsArray() attendees?: { userId: string; displayName?: string; role?: 'REQUIRED' | 'OPTIONAL'; visibility?: 'FULL' | 'LIMITED' | 'NONE' }[];
@IsOptional() @IsString() requestId?: string;
}
export class RequestMeetingDto {
@IsObject() requestedWindow!: Record<string, unknown>;
@IsOptional() @IsIn(MEETING_TYPES) meetingType?: (typeof MEETING_TYPES)[number];
@IsOptional() @IsString() interactionId?: string;
@IsOptional() @IsString() targetActorId?: string;
}
export class ConsentDto {
@IsString() actorRefId!: string;
@IsIn(CONSENT) status!: (typeof CONSENT)[number];
}
@@ -5,6 +5,7 @@ import { AiModule } from '../ai/ai.module';
import { CalendarService } from './calendar.service';
import { CalendarProjector } from './calendar.projector';
import { CalendarAdapter } from './calendar-adapter';
import { CalendarController } from './calendar.controller';
/**
* Calendar & Meeting SDK (P8). Imports OutboxModule (projector/events),
@@ -13,6 +14,7 @@ import { CalendarAdapter } from './calendar-adapter';
*/
@Module({
imports: [OutboxModule, AdaptersModule, AiModule],
controllers: [CalendarController],
providers: [CalendarService, CalendarProjector, CalendarAdapter],
exports: [CalendarService, CalendarAdapter],
})