From 1af10d0f4af8dfed8463361acf425347e77df21e Mon Sep 17 00:00:00 2001 From: maaz519 Date: Mon, 6 Jul 2026 17:11:20 +0530 Subject: [PATCH] feat(iios): message senderName + dev user directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Messages now carry senderName (resolved actorId → sourceHandle.externalId) so clients render usernames instead of raw actor cuids. - GET /v1/dev/users exposes the dev directory (DEV_USERS keys) so a host app can validate add-by-username; real apps validate against their user store / MDM. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../iios-service/src/dev/dev.controller.ts | 16 +++++++++++++- .../src/messaging/message.service.ts | 22 ++++++++++++++----- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/iios-service/src/dev/dev.controller.ts b/packages/iios-service/src/dev/dev.controller.ts index 8e546c9..be49491 100644 --- a/packages/iios-service/src/dev/dev.controller.ts +++ b/packages/iios-service/src/dev/dev.controller.ts @@ -1,5 +1,5 @@ import { randomUUID } from 'node:crypto'; -import { BadRequestException, Body, Controller, ForbiddenException, Headers, Param, Post, UnauthorizedException } from '@nestjs/common'; +import { BadRequestException, Body, Controller, ForbiddenException, Get, Headers, Param, Post, UnauthorizedException } from '@nestjs/common'; import jwt from 'jsonwebtoken'; import { IIOS_EVENTS } from '@insignia/iios-contracts'; import { hmacSign } from '@insignia/iios-adapter-sdk'; @@ -51,6 +51,20 @@ export class DevController { return { token: this.signToken(appId, body.username, body.username), userId: body.username }; } + /** Dev directory: the known dev usernames (from DEV_USERS). A real app validates + * add-by-username against its user store / the MDM port; this stands in for it. */ + @Get('users') + users(): { users: string[] } { + this.assertDev(); + let map: Record; + try { + map = JSON.parse(process.env.DEV_USERS ?? '{"alice":"alice","bob":"bob","carol":"carol"}'); + } catch { + map = {}; + } + return { users: Object.keys(map) }; + } + private signToken(appId: string, userId: string, name?: string, orgId?: string): string { let secrets: Record; try { diff --git a/packages/iios-service/src/messaging/message.service.ts b/packages/iios-service/src/messaging/message.service.ts index 94e98b7..4dfa4af 100644 --- a/packages/iios-service/src/messaging/message.service.ts +++ b/packages/iios-service/src/messaging/message.service.ts @@ -13,6 +13,7 @@ export interface MessageDto { id: string; threadId: string; senderActorId: string; + senderName: string; content: string; contentRef?: string; parentInteractionId?: string; @@ -198,7 +199,7 @@ export class MessageService { // Idempotency: a repeat key returns the existing message (no re-increment). const existing = await this.prisma.iiosInteraction.findUnique({ where: { scopeId_idempotencyKey: { scopeId: thread.scopeId, idempotencyKey } }, - include: { parts: { orderBy: { partIndex: 'asc' } } }, + include: { parts: { orderBy: { partIndex: 'asc' } }, actor: { include: { sourceHandle: true } } }, }); if (existing) return this.toDto(existing, threadId); @@ -260,7 +261,7 @@ export class MessageService { return tx.iiosInteraction.findUniqueOrThrow({ where: { id: created.id }, - include: { parts: { orderBy: { partIndex: 'asc' } } }, + include: { parts: { orderBy: { partIndex: 'asc' } }, actor: { include: { sourceHandle: true } } }, }); }); @@ -269,7 +270,7 @@ export class MessageService { if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002') { const winner = await this.prisma.iiosInteraction.findUniqueOrThrow({ where: { scopeId_idempotencyKey: { scopeId: thread.scopeId, idempotencyKey } }, - include: { parts: { orderBy: { partIndex: 'asc' } } }, + include: { parts: { orderBy: { partIndex: 'asc' } }, actor: { include: { sourceHandle: true } } }, }); return this.toDto(winner, threadId); } @@ -341,7 +342,7 @@ export class MessageService { async getMessageById(id: string, principal?: MessagePrincipal): Promise { const i = await this.prisma.iiosInteraction.findUnique({ where: { id }, - include: { parts: { orderBy: { partIndex: 'asc' } } }, + include: { parts: { orderBy: { partIndex: 'asc' } }, actor: { include: { sourceHandle: true } } }, }); if (!i || !i.threadId) return null; if (principal) await this.actors.assertOwns(principal, i.scopeId); // tenant fence (KG-02) when called on behalf of a caller @@ -352,7 +353,7 @@ export class MessageService { const interactions = await this.prisma.iiosInteraction.findMany({ where: { threadId }, orderBy: { occurredAt: 'asc' }, - include: { parts: { orderBy: { partIndex: 'asc' } } }, + include: { parts: { orderBy: { partIndex: 'asc' } }, actor: { include: { sourceHandle: true } } }, }); return interactions.map((i) => this.toDto(i, threadId)); } @@ -360,7 +361,15 @@ export class MessageService { // ─── helpers ──────────────────────────────────────────────────── private toDto( - interaction: { id: string; actorId: string | null; parentInteractionId?: string | null; traceId: string | null; occurredAt: Date; parts: Array<{ kind: string; bodyText: string | null; contentRef: string | null }> }, + interaction: { + id: string; + actorId: string | null; + actor?: { displayName: string | null; sourceHandle: { externalId: string } | null } | null; + parentInteractionId?: string | null; + traceId: string | null; + occurredAt: Date; + parts: Array<{ kind: string; bodyText: string | null; contentRef: string | null }>; + }, threadId: string, ): MessageDto { const text = interaction.parts.find((p) => p.kind === 'TEXT'); @@ -369,6 +378,7 @@ export class MessageService { id: interaction.id, threadId, senderActorId: interaction.actorId ?? '', + senderName: interaction.actor?.sourceHandle?.externalId ?? interaction.actor?.displayName ?? 'unknown', content: text?.bodyText ?? '', contentRef: file?.contentRef ?? undefined, parentInteractionId: interaction.parentInteractionId ?? undefined,