# Customer portal ticket ownership and authenticated AI context

**Status:** source repaired; 24 targeted ownership/actor cases pass; backend and base-app typechecks pass. The r10 local API now materializes a new ticket model before persistence. A fresh Maya browser run created ticket `6ab0b80eb353e66454849212`, showed it in the portal, and showed the same ticket after reload. Customer isolation and staff-side retrieval are still pending.

## Defects found

The email-addressed ticket read was public. A caller could supply an email and ticket number without proving that identity. Portal creation also used entered email to build a synthetic customer rather than the signed-in account. Direct record helper endpoints did not consistently enforce ownership/staff permissions. The public support wrapper posted a second request after TicketForm had already saved one, and success text asserted email delivery without a delivery result.

## Repaired behavior

- Email-addressed reading is authenticated; customers can query only their own persisted identity. Customer lists include staff-created tickets addressed to that customer. Another customer’s record ID is refused.
- Current staff accounts and effective direct/group grants are reloaded for each read/create/update/delete check. Owner names do not bypass the verb permission. Internal comments, routing/status/priority and bulk mutation are staff-only; customer message reads require owned-record access.
- Portal forms use `POST client-data/tickets`. The API binds author/contact to the current customer, strips injected ownership/assignment/status/metadata fields, generates a new record and returns a customer view without internal metadata/comments/assignments. Customer updates verify stored ownership and permit only their title/description/contact-phone fields.
- The existing email-create route stays authenticated. A permitted site integration can create a bounded new request; it cannot turn an entered email or subject number into an authenticated customer or an existing-ticket reply. Anonymous callers remain denied. The narrow signed-guest `chat-request` intake remains unchanged and its seven tests still pass.
- Public support tracking directs customers to sign in; email plus ticket number no longer opens details. The public wrapper acknowledges the request TicketForm already saved instead of creating it twice. Success text confirms the saved request and tells the user to check email separately.
- Authenticated AI execute/stream/test paths propagate the server-selected customer/staff principal into a separate role-builder closure. Model args/input.data cannot override it. Customer ticket reads and staff updates use the same checks. Unauthenticated voice/channel triggers cannot read a private ticket by guessing its email; the remaining trusted-context/live-AI exercise is explicitly recorded in the AI course pending review.

Core owns the accompanying middleware repair that cryptographically verifies x-client-authorization before promoting a customer behind a site integration. The ticket layer then re-reads that persisted same-tenant identity. Both repairs are needed for end-to-end authentication.

## Canonical changed source

AppEngine:

- `src/crm/tickets.controller.ts`, `tickets.service.ts`, `tickets.ownership.spec.ts`.
- `src/crm/tickets.chat-request.spec.ts` adds only the UsersService mock required by the new dependency.
- `src/client-account/client-account.service.ts`: only createClientTicket delegates to createAuthenticatedTicket; parent owns all file-method edits.
- `src/crm/ai-assistant/ai-assistant.controller.ts`, `ai-assistant.service.ts`.
- `src/crm/ai-assistant/tools/crm-tool-registry.ts`, implementations `get-ticket.tool.ts`, `update-ticket.tool.ts`.
- `src/ai/agents/crm-assistant-role.ts`, `crm-assistant-ticket-auth.spec.ts`.

Base app:

- `src/modules/ticket-support/components/ticket-form.tsx`, `api/ticket-api.ts`.
- `src/components/my-account/tickets/create-ticket-content.tsx`, `tickets-list.tsx`.
- `src/components/ticket-support/TicketRoute.tsx`, `TicketSupportClient.tsx`.
- `src/components/my-account/Overview.tsx`: separate small Quick Actions solid purple background fallback; still needs actual portal contrast readback.

## Verification

15 ownership/permission cases, seven existing guest-intake cases and two actual AI role-handler actor-isolation cases passed. Actual backend and base-app TypeScript checks passed. Cases cover anonymous/foreign-tenant actors, another customer’s email or record ID, persisted missing/locked/guest identities, staff grants, create/update field injection, bounded app intake, metadata exclusion and model-supplied actor spoofing.

Runtime acceptance is now recorded: Maya created and reloaded ticket `6ab0b80eb353e66454849212`; Daniel's authenticated `GET /client-data/tickets` returned an empty list; the Cedar owner found Maya's ticket (`BWATMXCG`) and posted a staff reply; and the customer sign-out followed by a direct account visit redirected to `/login`. The owner read/reply evidence is in `assets/reservation-display/staff-ticket-read-reply.json`.


## Live create regression and model correction

Parent actual Maya portal ticket POST returned400 because createTicket passed a plain envelope to repository.create. Before the next live retry, createTicket now materializes a new ticket with the actual factory: payload body.data, preserved author, then collection subschema assigned on the model. A real createTicket-method plus real RepositoryCrudService.createNewData regression checks blank initial pk/sk, isNew true, datatype ticket, flat data.title, author, subschema and notification fields. Passing the whole envelope to this factory would incorrectly produce data.data; that draft was corrected before activation. [Regression output](assets/ticket-materialization/tests.txt). Actual successful POST remains pending the r10 rebuild.
