Files
agent-desk-ng/docs/hardening/CUSTOMER_IDENTITY_UPGRADE.md
mono 2f7a491666 feat: harden support, localize knowledge and add operator availability [skip ci]
Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.

Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.

Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.

Expected source tree: 0a4a7528e9.
2026-10-16 15:21:42 +00:00

110 lines
6.3 KiB
Markdown

# Customer identity and live-access upgrade
These are operator instructions for a reviewed staging rollout, not commands
executed by this change. Back up data and verify the exact schema/engine before
any production upgrade. Existing customer and conversation records are retained.
## Additive schema and issuer boundaries
`CustomerIdentity` adds:
- `identity_scope`: indexed varchar(80), default empty for legacy rows.
- `scoped_key`: nullable varchar(64) with a unique index. Legacy rows remain NULL.
The normal GORM startup migration adds these definitions. No destructive index
replacement, automatic legacy merge, or data deletion is supplied. Test this
additive change on a backup with the deployment's actual SQLite/MySQL version.
A public identity scope is `v1:<jwt|hmac|guest>:SHA256(trimmed channel code)`.
The server derives it; clients cannot select an issuer or inject this scope.
The unique claim key is SHA-256 of the JSON array containing scope, external
source and trimmed external ID. Use the implementation's
`ScopedCustomerIdentityKey` helper rather than a hand-built delimiter format.
New public registrations insert the claim in the same customer transaction.
A competing claim rolls back rather than creating two authorized customers.
WebSocket routing uses canonical customer ID and channel record ID. Incoming
subscriptions and outgoing recipients are checked against current ownership.
Raw external IDs are not cross-channel or cross-trust routing keys.
## Existing sessions and history
Old customer-session JWTs without the new scope are rejected. The updated client
must exchange a fresh token. Until legacy mappings are explicitly reconciled,
a newly authenticated scoped identity can receive a separate customer record,
not the old account's history. The original records remain available to
appropriately authorized operators.
Before enabling traffic:
1. Back up data; stop exchanges/writes for the affected scope during reconciliation.
2. Run the read-only `scripts/audit-customer-identity-scopes.sql` reports against an
approved staging copy after the additive columns exist. Keep report output
private: it contains customer identifiers.
3. Establish channel and issuer ownership using trustworthy host-side evidence.
Legacy `user` rows alone cannot distinguish JWT from HMAC provenance.
4. For a proven unique mapping, review a transaction setting both its scope and
computed claim key while retaining its customer ID and conversation history.
Check all associated conversation channels before making a change.
5. Stop on duplicate, inactive, multi-channel or mixed-trust mappings. Never take
the first row or merge customers solely because external IDs match. If a new
scoped record already exists, reconcile both histories explicitly.
6. Reissue customer tokens and verify authorized history, isolation, and rollback
behavior on the staging copy before resuming traffic.
A channel reassigned to a different identity issuer needs a new channel code.
Ordinary signing-key rotation intentionally retains the same issuer scope.
Use a distinct host JWT signing secret for each independently trusted channel.
The existing host JWT contract validates the channel's secret and expiry, but
does not require a channel audience claim. Reusing a signing key across unrelated
channels lets the holder assert an identity to either channel; scoped storage
is not a defense against a shared issuer key. HMAC proofs bind the channel code
inside the signed payload. No mandatory JWT audience rollout is introduced here.
Legacy duplicate repair remains an operator-reviewed migration, not an automatic
startup side effect. The nullable unique claim allows dirty legacy rows to
remain isolated without blocking new correctly scoped registrations.
## Anonymous and authenticated-only channels
Unsigned exchange issues a new random server-owned anonymous identity. Supplying
an old guest external ID is not proof of ownership. Normal resume and refresh
use the signed customer-session token. Near-expiry, still-valid credentials are
renewed through the rate-limited `POST /api/customer/session_refresh` endpoint,
which authenticates the existing session and never creates an anonymous identity.
A refresh failure is not silently replaced with a fresh guest. Losing or expiring
that credential does not authorize reclaiming history by an external ID.
An optional boolean `requireSignedIdentity` in a web channel's `configJson`
rejects unsigned exchanges and existing guest sessions server-side. Default
false retains anonymous channels. The SDK's `requireUserToken` is not a substitute
for this policy.
Use the existing authenticated channel GET and
`POST /api/dashboard/channel/update` flow with `channel.update` permission,
preserving all unrelated configuration and required fields. No dedicated new UI
toggle is provided. Older editors which omit this property preserve a configured
true value; an explicit JSON false is needed to disable it. Null and nonboolean
values are rejected. This source change does not configure a live channel.
## Staff and customer live-session revocation
Staff sockets bind their original login-session ID and absolute expiry, without
serializing that internal metadata into API responses. Fresh session, account,
role and permission state is checked for incoming operations, event fanout,
actual queued writes and idle checks. Scope changes terminate the connection;
reconnection must obtain fresh authority. Idle checks run every five seconds;
data operations revalidate immediately rather than using a five-second cache.
Notification sockets cannot subscribe to conversation topics. Dashboard
conversation sockets require conversation-view permission. Customer connections
recheck token expiry, channel policy, customer state and exact identity mapping.
This implementation deliberately makes database reads at authorization
boundaries. Measure the cost under realistic fanout; any later caching must have
complete revocation invalidation rather than silently reintroducing stale access.
Channel configuration updates and token-secret resets compare the previously read
configuration bytes and update timestamp before writing. A stale writer receives
a conflict instead of silently overwriting a newly enabled signed-identity policy.
Reload the channel and reapply the intended change after reviewing that conflict.