SmtpProvider is now scope-aware: it resolves the tenant's own SMTP identity from
the IiosProviderCredential store (providerType SMTP) and sends from it, falling
back to the platform env identity when unset (env fallback applies only to the
platform identity, never a tenant's server). Registered whenever env SMTP OR the
credential store is present; fails closed as NOT_CONFIGURED when neither exists.
Credential endpoints accept SMTP with per-type validation. 18 SMTP tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Every message now shows a Slack-style time (clock today, then 'Yesterday',
weekday, else a date; full timestamp on hover).
- New ConversationSettings panel for groups AND channels (public + private):
rename, member list, add people (from the directory), remove, and leave.
Opened from a new thread header gear; DMs show no gear. Adapter gains
addMember/removeMember/renameConversation (optional, OPA still gates writes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the messaging Attachment type with optional storage fields so an adapter's
upload() can return a sendable reference (the socket needs contentRef/mimeType/
sizeBytes to persist the message part) while keeping the display url. Enables
messenger attachments end-to-end in host adapters.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Mail reader: .miu-mail-msg gets flex:0 0 auto so cards keep their natural height
in the scrolling column instead of being compressed + clipped by overflow:hidden.
- Reaction/emoji picker renders through a PopoverPortal to <body> (positioned +
themed) so it floats above the message list instead of being cut by the scroll
container's overflow. 72 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Attachments upload direct-to-storage on attach, so an abandoned attach (removed,
cancelled, tab closed) would linger forever. Add IiosMediaObject: a row is recorded
when bytes land (MediaService.put); a scheduled sweepOrphans() reaps objects past a
grace window (IIOS_MEDIA_ORPHAN_GRACE_MS, default 24h) that no IiosMessagePart
references (derived live — no flag to drift). Storage delete via the StoragePort;
best-effort + idempotent. Gated on IIOS_MEDIA_GC_INTERVAL_MS (off by default).
Migration 20260722193958_media_object_tracking. 10 media tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picking a file now shows an immediate chip with the filename + a spinner while
the bytes upload (mail compose, mail reply, and the messenger composer), instead
of only appearing once upload finishes. Respects prefers-reduced-motion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Attachment chips in the mail reader are now clickable: new InboxAdapter
downloadAttachment() resolves a short-lived URL, opened in a new tab.
- Mail reader scrolls again: convert the .miu-detail/.miu-mail height:100%
chain to flex fill so a long thread scrolls within the pane instead of
being clipped. MockInboxAdapter implements download. Bump to 0.1.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The new-conversation + channel-create forms are flex columns, so the submit
button stretched to full width and read as thin. Give it align-self:flex-start
+ a 38px height so it sizes to its label like a normal button.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a 'New message' + on the Direct messages section that opens a people picker
(NewConversation): pick one person -> DM, two or more -> group with an optional
name, via the adapter's existing openThread({participantIds, membership, subject}).
Expose directory() on MessagingAdapter (org people list); MockAdapter implements
it + dedupes 1:1 DMs. 72 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A host wrapper like .view{position:relative;z-index:1} creates a stacking context
that traps the modal's fixed overlay below a sibling sticky header, no matter its
z-index. Render the modal through a ModalPortal (createPortal to document.body) so
it escapes the host stacking context + overflow entirely; the portal root copies
the SDK theme tokens from the live surface (synchronously, no unstyled paint) and
carries dark defaults as a fallback. Adds react-dom peer dep. 67 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the hand-authored migration with one produced by `prisma migrate diff`
(offline, engine-generated) and verified by replaying the full 24-migration chain
into a throwaway Postgres DB. Same DDL, but generated through Prisma tooling so the
migration history stays consistent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
IIOS becomes the tenant's integration/credential hub for anything it sends.
New IiosProviderCredential (one row per scope+providerType) seals the secret
with AES-256-GCM under IIOS_CRED_KEY (secret-crypto.ts); only non-secret
displayHints are ever read back. ProviderCredentialService upsert/resolve/status;
TwilioSmsProvider resolves the caller's own creds per scope at send time and
POSTs to Twilio (fail-closed NOT_CONFIGURED when unset). Registered over the
SMS sandbox only when the platform key + store are present. PUT/GET
/v1/providers/:type/credentials (scope-fenced, masked reads). 16 new unit tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
InboxAdapter gains uploadAttachment + attachment params on mailReply/
composeInternal/composeExternal. MailReader reply and ComposeModal grow an
attach affordance (paperclip + pending chips); mail history renders sent
attachments. MockInboxAdapter implements upload. 67 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The emoji picker used bottom:100% and was clipped by the message list's
overflow when reacting to a message near the top (and could spill past the
right edge). Open it downward + edge-anchored (right for others' messages,
left for my own) with width:max-content so all emojis stay visible.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replies (messages with parentInteractionId) now open in a right-hand ThreadPane
instead of inline quoting:
- extracted a shared Composer (draft + @mention autocomplete + attach) and
MessageItem (bubble + mentions + attachment + reactions + reply affordance)
- Thread shows top-level messages only; a message with replies gets a
'💬 N replies' link, and hovering shows 💬 'Reply in thread'
- ThreadPane renders the root + its replies + a composer that posts back with
parentInteractionId; Messenger becomes 3-column (list | thread | pane),
pane closes on conversation switch
- no contract/adapter/backend change (parentInteractionId was already wired)
- thread-pane render test (open → reply → parent shows the count); 58 green
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- useMessages exposes upload(); Thread gains a hover reaction picker (react to
a message) and an attach button (upload → stage → send), plus inline image /
file rendering of message attachments — all capability-degrading (hidden when
the adapter lacks react/upload)
- themeable styles for the picker, attach button, staged chip, and attachments
- 57 tests still green
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- iios-kernel-client: RestClient.discoverThreads (GET /v1/threads/discover) +
leaveThread (DELETE /v1/threads/:id/me); DiscoveredThread type
- KernelClientAdapter implements the four channel methods: browseChannels maps
discovered public channels → ChannelSummary; createChannel → createThread
(membership=channel, ADMIN, visibility/topic metadata); joinChannel →
socket.openThread (governed public self-join); leaveChannel → rest.leaveThread
- adapter channel test over the fake transport (13 kernel-adapter tests); 57 green
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The generic primitives channels need beyond dm/group, kept policy-governed and
scope-fenced (kernel never interprets 'channel'/'public'):
- discoverThreads(principal, filter): scope-wide browse (NOT membership-scoped)
matching an opaque metadata filter, each result flagged joined; governed by
iios.thread.discover (scope fence in the query). REST: GET /v1/threads/discover
- open self-join for PUBLIC channels: openThread now passes visibility into the
join decision; dev-OPA iios.thread.join allows membership=channel+visibility=
public (dm/group/private-channel stay invite-only)
- leaveThread + iios.thread.leave + REST DELETE /v1/threads/:id/me
- tests: public self-join vs private denied, discover joined-flags + group
excluded + leave; dev-opa join-public/discover/leave rules (29 green)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a third membership beyond dm/group: a discoverable, joinable room.
- contract: Membership += 'channel'; Conversation.topic; ChannelSummary +
CreateChannelInput; optional adapter methods browseChannels/createChannel/
joinChannel/leaveChannel (capability-degrading — absent hides the UI)
- MockAdapter implements them (seeds a joined public, a joinable public, and a
private channel); listConversations now returns only threads I'm in
- useChannels hook (browse/create/join/leave + supported flag)
- UI: sidebar sections (Channels vs Direct messages), a + that opens a
ChannelBrowser (join + create), # / lock glyphs; themeable styles
- KernelClientAdapter passes channel membership + topic through
- 4 channel tests (mock browse/join/create + render browse→join); 52 total green
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turns the SDK from hooks-only into a real UI SDK: <Messenger> (conversation
list + open thread + composer), plus <ConversationList> and <Thread>, all
driven by the existing useConversations/useMessages hooks over the injected
adapter — zero transport imports (boundary intact).
- themeable via --miu-* CSS variables; default theme ships as ./styles.css
- optimistic send, typing indicator, seen ticks, reactions display, unread badges
- render smoke tests (jsdom + MockAdapter): mounts, auto-selects first thread,
sends a message, switches threads (3 tests) — 48 total green
- tsup: css bundled to dist/styles.css; dts scoped to TS entries
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements MessagingAdapter over @insignia/iios-kernel-client (browser → IIOS,
token-in): listConversations/openThread/history/send/subscribe/typing/markRead
/react, mapping kernel Message/ThreadSummary/annotations onto the SDK's neutral
types. currentActorId comes from the host's session (senderId space), never
inferred from history — the exact bug the conformance suite kills.
- lives in adapters/ (transport boundary intact — core stays transport-free)
- ships from the ./adapters/kernel-client subpath (kernel-client is an optional
peer dep; excluded from the main bundle)
- connectKernelAdapter({serviceUrl, token, currentUserId}) convenience
- passes the shared adapter conformance suite (12 tests) over the real
MessageSocket facade with only the socket.io layer faked
- media/attachments deferred (kernel-client has no presign yet)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- media upload policy now allows text/html, text/markdown, text/x-markdown,
text/csv (in addition to images/av, pdf, txt, zip, office docs)
- blob endpoint adds X-Content-Type-Options: nosniff, and forces
Content-Disposition: attachment for script-capable types (html, xhtml, svg,
xml) so an uploaded file can't render/execute inline from the IIOS origin
(stored-XSS). Images/video/audio/pdf still serve inline for preview.
- dev-opa test covering the allowed types + unknown/oversize denials
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- MailInternalDto accepts attachments[]; deposit() stores each as a media
part (image/video→MEDIA_REF, audio→VOICE_REF, else FILE_REF)
- ingest now persists part sizeBytes; contract + DTO carry it
- threads.getMessages returns mimeType + sizeBytes so mail readers can
render inline images / file chips
- test: internal mail stores an image attachment as a MEDIA_REF part
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ingest() puts metadata on the interaction, not the thread, and listThreads filters
THREAD metadata — so MailService now tags the thread directly after deposit
(source=crm-mail). Lets the CRM list mail threads apart from Messenger chat
(source=crm-messenger). 7 tests. NOTE: requires an IIOS redeploy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop-in S3-compatible backend for object storage (media + email attachments) —
the swap the StoragePort seam was designed for. MinIO/self-hosted/R2/Supabase all
work via endpoint + path-style.
- S3Storage implements StoragePort (put/get/remove); sha256 computed locally on put
(matches LocalDiskStorage); a missing object reads back as null, not an error.
- Env-driven binding in MediaModule: IIOS_S3_BUCKET + keys set → S3Storage, else
LocalDiskStorage. forcePathStyle defaults true (MinIO); endpoint omitted → AWS.
- S3 client is injectable so tests run with no live server.
6 unit tests (config parse, put size/sha, get round-trip, NoSuchKey→null, remove).
Full suite 300/300, boundary + build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Email can now carry attachments (e.g. an invoice PDF). The kernel already stored
attachments as message parts + the media StoragePort holds the bytes; the gap was
the email envelope.
- EMAIL payload carries attachment REFS ({filename, contentRef, mimeType}), not
bytes — the ledger + T8 PII redaction stay small; bytes are fetched at send time.
- SmtpProvider takes an AttachmentResolver; resolves each ref via storage and attaches
(nodemailer). FAILS CLOSED if a declared attachment can't be resolved (or no resolver
is wired) — never send a receipt/invoice missing its file; a FAILED command retries.
- CapabilityProviderRegistry injects the resolver from STORAGE_PORT (@Optional);
MediaModule exports STORAGE_PORT, CapabilityModule imports MediaModule. No cycle.
- TemplatedSender + MailService + the /v1/mail/send DTO pass attachments through.
Verified: 6 new unit tests (attach, fail-closed x2, plain-unaffected, resolver,
pass-through) + a REAL Ethereal SMTP send WITH a PDF attachment (SENT). Full suite
294/294, boundary + build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MailService renders a template then deposits it as an EMAIL interaction on a
per-email thread (thread model: one thread per email), reusing IngestService.
Because ingest adds no participants and a thread is only visible to its
participants, it ensureParticipant()s BOTH sender and recipient — so the mirror
is actually visible.
- postInternal: app-to-app mail, no SMTP → recipient's in-app inbox.
- sendExternalWithMirror: SMTP send (TemplatedSender) + mirror an interaction
ONLY for a registered recipient (pre-registration sends are email-only — no
inbox exists yet). Idempotent across both the send and the mirror.
- Writes Interactions, NEVER InboxItems (the projector owns those — KG-15).
- POST /v1/mail/internal, POST /v1/mail/send. MailModule in AppModule.
Verified over HTTP: internal mail → recipient SEES the thread in their inbox;
external send → command + mirror; no-source/no-auth 400. 7 unit tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>