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.
110 lines
6.3 KiB
Markdown
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.
|