feat(iios): policy-enforced membership + threaded replies + dev IdP (generic)
Enriches the platform PLANE, not the kernel: - DevOpaPort: the dev OPA stub now evaluates a small policy table (behind the same opa.decide) — DM capped at 2, add-participant requires member/group-admin, governed self-join for membership threads. Real OPA swaps in unchanged. - Dev IdP login (POST /v1/dev/login) issues the same JWT claims a real IdP would. - PolicyDeniedFilter maps fail-closed denials to HTTP 403. Generic kernel additions (no chat vocabulary — 'dm'/'group' live only as OPA policy + an opaque thread attribute): - MessageService.addParticipant (governed membership by userId), governed openThread self-join (scoped to threads with a membership attribute), parentInteractionId on send (reply link), and a generic listThreads. - REST: GET /v1/threads, POST /v1/threads, POST /v1/threads/:id/participants; socket add_participant + membership/parentInteractionId. ensureParticipant gains a role. Tests: dev-opa.port.spec + message.spec (DM cap / group admin / governed join / listThreads / reply). smoke-membership.mjs; realtime smokes updated for governed join. 175 unit tests + all smokes green; kernel free of dm/group literals. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,26 @@ export class ThreadsController {
|
||||
private readonly session: SessionVerifier,
|
||||
) {}
|
||||
|
||||
/** Generic "my threads" — every thread the caller participates in (last message + unread). */
|
||||
@Get()
|
||||
async listThreads(@Headers('authorization') auth?: string) {
|
||||
return this.messages.listThreads(this.principal(auth));
|
||||
}
|
||||
|
||||
/** Create a thread; `membership`/`creatorRole` are opaque, app-supplied thread attributes the kernel stores but never interprets. */
|
||||
@Post()
|
||||
@HttpCode(201)
|
||||
async createThread(@Body() body: { membership?: string; creatorRole?: string }, @Headers('authorization') auth?: string) {
|
||||
return this.messages.openThread(null, this.principal(auth), { membership: body?.membership, creatorRole: body?.creatorRole });
|
||||
}
|
||||
|
||||
/** Governed membership: add a user (by userId) to a thread — policy enforces DM cap / roles. */
|
||||
@Post(':id/participants')
|
||||
@HttpCode(201)
|
||||
async addParticipant(@Param('id') id: string, @Body() body: { userId: string; role?: string }, @Headers('authorization') auth?: string) {
|
||||
return this.messages.addParticipant(id, this.principal(auth), body.userId, body.role);
|
||||
}
|
||||
|
||||
@Get(':id/messages')
|
||||
async listMessages(@Param('id') id: string) {
|
||||
return this.threads.getMessages(id);
|
||||
@@ -36,14 +56,19 @@ export class ThreadsController {
|
||||
@Headers('authorization') authorization?: string,
|
||||
@Headers('idempotency-key') idempotencyKey?: string,
|
||||
) {
|
||||
const token = (authorization ?? '').replace(/^Bearer\s+/i, '');
|
||||
if (!token) throw new BadRequestException('Authorization bearer token is required');
|
||||
const principal = this.session.verify(token);
|
||||
return this.messages.send(
|
||||
id,
|
||||
principal,
|
||||
this.principal(authorization),
|
||||
{ content: body.content, contentRef: body.contentRef },
|
||||
idempotencyKey ?? randomUUID(),
|
||||
undefined,
|
||||
body.parentInteractionId,
|
||||
);
|
||||
}
|
||||
|
||||
private principal(authorization?: string) {
|
||||
const token = (authorization ?? '').replace(/^Bearer\s+/i, '');
|
||||
if (!token) throw new BadRequestException('Authorization bearer token is required');
|
||||
return this.session.verify(token);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user