feat(iios): message senderName + dev user directory
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
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 jwt from 'jsonwebtoken';
|
||||||
import { IIOS_EVENTS } from '@insignia/iios-contracts';
|
import { IIOS_EVENTS } from '@insignia/iios-contracts';
|
||||||
import { hmacSign } from '@insignia/iios-adapter-sdk';
|
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 };
|
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<string, string>;
|
||||||
|
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 {
|
private signToken(appId: string, userId: string, name?: string, orgId?: string): string {
|
||||||
let secrets: Record<string, string>;
|
let secrets: Record<string, string>;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface MessageDto {
|
|||||||
id: string;
|
id: string;
|
||||||
threadId: string;
|
threadId: string;
|
||||||
senderActorId: string;
|
senderActorId: string;
|
||||||
|
senderName: string;
|
||||||
content: string;
|
content: string;
|
||||||
contentRef?: string;
|
contentRef?: string;
|
||||||
parentInteractionId?: string;
|
parentInteractionId?: string;
|
||||||
@@ -198,7 +199,7 @@ export class MessageService {
|
|||||||
// Idempotency: a repeat key returns the existing message (no re-increment).
|
// Idempotency: a repeat key returns the existing message (no re-increment).
|
||||||
const existing = await this.prisma.iiosInteraction.findUnique({
|
const existing = await this.prisma.iiosInteraction.findUnique({
|
||||||
where: { scopeId_idempotencyKey: { scopeId: thread.scopeId, idempotencyKey } },
|
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);
|
if (existing) return this.toDto(existing, threadId);
|
||||||
|
|
||||||
@@ -260,7 +261,7 @@ export class MessageService {
|
|||||||
|
|
||||||
return tx.iiosInteraction.findUniqueOrThrow({
|
return tx.iiosInteraction.findUniqueOrThrow({
|
||||||
where: { id: created.id },
|
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') {
|
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002') {
|
||||||
const winner = await this.prisma.iiosInteraction.findUniqueOrThrow({
|
const winner = await this.prisma.iiosInteraction.findUniqueOrThrow({
|
||||||
where: { scopeId_idempotencyKey: { scopeId: thread.scopeId, idempotencyKey } },
|
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);
|
return this.toDto(winner, threadId);
|
||||||
}
|
}
|
||||||
@@ -341,7 +342,7 @@ export class MessageService {
|
|||||||
async getMessageById(id: string, principal?: MessagePrincipal): Promise<MessageDto | null> {
|
async getMessageById(id: string, principal?: MessagePrincipal): Promise<MessageDto | null> {
|
||||||
const i = await this.prisma.iiosInteraction.findUnique({
|
const i = await this.prisma.iiosInteraction.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: { parts: { orderBy: { partIndex: 'asc' } } },
|
include: { parts: { orderBy: { partIndex: 'asc' } }, actor: { include: { sourceHandle: true } } },
|
||||||
});
|
});
|
||||||
if (!i || !i.threadId) return null;
|
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
|
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({
|
const interactions = await this.prisma.iiosInteraction.findMany({
|
||||||
where: { threadId },
|
where: { threadId },
|
||||||
orderBy: { occurredAt: 'asc' },
|
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));
|
return interactions.map((i) => this.toDto(i, threadId));
|
||||||
}
|
}
|
||||||
@@ -360,7 +361,15 @@ export class MessageService {
|
|||||||
// ─── helpers ────────────────────────────────────────────────────
|
// ─── helpers ────────────────────────────────────────────────────
|
||||||
|
|
||||||
private toDto(
|
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,
|
threadId: string,
|
||||||
): MessageDto {
|
): MessageDto {
|
||||||
const text = interaction.parts.find((p) => p.kind === 'TEXT');
|
const text = interaction.parts.find((p) => p.kind === 'TEXT');
|
||||||
@@ -369,6 +378,7 @@ export class MessageService {
|
|||||||
id: interaction.id,
|
id: interaction.id,
|
||||||
threadId,
|
threadId,
|
||||||
senderActorId: interaction.actorId ?? '',
|
senderActorId: interaction.actorId ?? '',
|
||||||
|
senderName: interaction.actor?.sourceHandle?.externalId ?? interaction.actor?.displayName ?? 'unknown',
|
||||||
content: text?.bodyText ?? '',
|
content: text?.bodyText ?? '',
|
||||||
contentRef: file?.contentRef ?? undefined,
|
contentRef: file?.contentRef ?? undefined,
|
||||||
parentInteractionId: interaction.parentInteractionId ?? undefined,
|
parentInteractionId: interaction.parentInteractionId ?? undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user