feat(ever-connect): Sign in with Ever ID on connected installations, and a person's deletion and export requests
What
The Gauzy side of Sign in with Ever ID on a connected installation and of a person's deletion and export requests, in @gauzy/plugin-ever-connect and @gauzy/plugin-auth-zitadel. Everything stays off unless EVER_CONNECT_ENABLED=true, the installation is connected, and ever_id_login is enabled in app.ever.co (Ever Platform still lists it as coming soon, so nothing of it runs yet).
Sign in with Ever ID: the installation's own sign-in client
- When
ever_id_loginis enabled (consent in app.ever.co + the operator's accept), the plugin asks Ever Platform (and the worker asks again while nothing was asked for, e.g. after a failed request and a restart) for this installation's sign-in client (POST /v1/instances/me/oidc-client) with the Ever ID callback and back-channel logout addresses of this API (ZITADEL_CALLBACK_URL, else${API_BASE_URL}/api/auth/zitadel/callback; https on one origin, otherwise nothing is sent), then reads it (GET) until it is ready: 5 s, then less often, at most every 5 min, by the API process holding the connection's lease. - The secret is handed over once; it is stored sealed (AES-256-GCM, like the connection key) in
ever_connect_integration.configEncryptedand given to the Ever ID sign-in plugin through the existingEVER_CONNECT_CONFIGport (environment settings still win, key by key). A secret that cannot be read back reads "enable again in app.ever.co" on the Integrations & data tab (sign_in_clienton the integration view). - Switching the integration off, its revocation and a disconnect forget the client here; Ever ID links and Gauzy accounts stay.
ever_id_loginis nowavailablein the definitions: its state follows Ever Platform's, so it reads coming soon while Ever Platform says so. No new environment variable.
Ever Platform's view of an identity
auth-zitadel asks it (POST /v1/identity/resolve, through the connection plugin) only when an ID token carries none of the urn:ever:* claims, only for Ever Platform's own issuer, with a 6 s limit; a sign-in never waits longer and goes on without hints on any error.
Ever ID link reports (retry table)
ever-connectlistens toauth-zitadel'sZitadelAccountLinkedEvent/ZitadelAccountUnlinkedEventon the core event bus. A link or unlink of a Gauzy user of a tenant linked to an Ever organization, for Ever Platform's issuer, while Sign in with Ever ID is ready, becomes a row ofever_connect_person_link_report, sent by the lease holder within seconds (POST/DELETE /v1/instances/me/person-links).- Sent: the Gauzy user id, issuer, subject, link method (
explicit/confirmed; a sign-up counts as explicit,provisionedis never reported), tenant and organization ids, tenant link, tenant name (omitted if it looks like an e-mail address). Never an e-mail address or a person's name. - Each report is sent as the link is when it goes: the sender asks
auth-zitadel(findLink) and sends a link if it exists, an unlink if not, so event order across API processes cannot leave Ever Platform out of date. Ever Platform removes person links by user, so before an unlink the user's other Ever ID links are queued again (linksOfUser) and go right after it. One user's reports go out in order (createdAt, then a per-userseq), and the due query skips reports that must wait before its limit. 422 subject_unknownand no answer back off from 30 s doubling to a day, then the report is given up and audited; a403drops it; switching the integration off drops what is pending; each report and the integration's state are read again right before it is sent.- Once the client is ready, the existing links of the linked tenants only are reported (paged, one query per page over those tenants), by the worker until done, tracked per linked tenant in the sealed client state (so a tenant linked later is reported too), against the latest report of each link.
A person's deletion and export requests
- The feed dispatcher now handles
ever.registry.person.deletion_requestedand...export_requested(they were acknowledged and ignored before). Each request is recorded by its event id inever_connect_person_requestwith the identities it names, claimed by one handling (compare and set with an owner token, renewed while the participant works; only the holder writes the answer), and the Gauzy users linked to it are stored before any link is removed: a handling that stops half-way is taken up again by the worker for the same accounts (neverno_account). Answered once (POST /v1/instances/me/ack); an answer that cannot be sent is sent again with the same back-off. Answers are not gated by any integration. - Deletion: the link reports of the identities are removed;
auth-zitadel(newEVER_ID_ACCOUNTSport) ends the Gauzy sessions opened through Ever ID, removes thezitadel_accountrows of the named identities (sha256(<issuer>#<subject>)), and ends the sessions again (endSessionsfor a resumed request); then the newEVER_PERSON_DATA_PARTICIPANTport (for the account-deletion feature) answersdeleted/anonymised/retained_legal+ legal basis. Unbound (today):failedwithreason_code: account_deletion_pendingβ the links are gone but the accounts still hold the person's data, so the request stays open. No linked account:no_account. - Export: the participant answers
exportedwith an https link (stored sealed); unbound:failed/export_not_available; no account:no_account.
/.well-known/ever-connect
Registered on the HTTP server outside /api, only when the module is loaded. It answers a short-lived statement signed with the connection key ({iss, aud: "urn:ever:instance-url", url, iat, exp β€ iat + 600}) only while an address check is pending (at most 10 minutes), and otherwise falls through to the same 404 as an address that does not exist. This release does not send the installation's address yet, so it always answers 404.
The client state is written only while the integration is enabled under the consent it was asked for (compare and set), so a client asked for while it was switched off is not kept.
Migration
1791000003500-EverConnectIdentity (Postgres, MySQL, SQLite): two new tables (ever_connect_person_link_report, ever_connect_person_request) and one nullable column (ever_connect_connection.publicUrlCheckUntil). Additive, IF NOT EXISTS (the column only when missing), advisory lock on Postgres, tested down.
SDK
@ever-co/connect-sdk, @ever-co/connect-contracts and @ever-co/connect-tools move to 1.0.0-rc.8 (the release that pins these calls; ever-connect and ever-stats, one version in yarn.lock). With it: oidc-client and person-links POST go without an Idempotency-Key (the pinned contract takes none: the same request again answers the same result), the ack body is checked against the pinned InstanceAck (so retained_legal goes with its legal_basis), and ever_id_login is pinned at its scope version 2 (the consent text app.ever.co shows). tools/egress-audit keeps its own lockfile and an rc.7-specific mock-clock patch, so it stays on rc.7 here.
Ever Platform takes a person link only for a subject that signed in through this installation's own Ever ID client (422 subject_unknown otherwise): a link reported right after such a sign-in is unaffected; a link that existed before (the backfill) and is refused that way is final at once (origin = backfill), not retried for a day, and is reported again when its user signs in with Ever ID (auth-zitadel now publishes a ZitadelAccountSignedInEvent per linked user; ids and the tenant name only, never failing the sign-in).
Tests
On 1.0.0-rc.8, head 58e16abf4cf: the full ever-connect suite (223 passed, 13 mock-platform skipped), auth-zitadel (156 passed); earlier on rc.8: the mock-platform end-to-end suite against rc.8's mock (13 passed), ever-stats (134 passed).
ever-connectspecs/ever-id-login.spec.ts,specs/person-link-reporter.spec.ts,specs/person-requests.spec.ts: SQLite with the real migrations, the real SDK client (every request body checked against the SDK's request schemas before it is sent) against a recording stand-in for Ever Platform: pending then ready, the secret stored sealed (never in a row, an audit row or a log line), cleared on revoke, resolve for Ever Platform's issuer only and given up after 6 s; the link report body (no e-mail key), back-off to a day then failed and audited, nothing sent while off, links of linked tenants only; one answer for a replayed envelope,failedreason codes while unbound, re-sending,/.well-known/ever-connect404 by default and a statement that verifies with the connection key during a check.specs/identity-migration.db.spec.ts: up twice, down, up again (SQLite; Postgres and MySQL with the usual test database variables).auth-zitadelspecs/claims-resolve.spec.ts: resolve only without platform claims, strict reading, 6 s limit, errors sign in without hints; the accounts port (hash matching across pages, erase, tenant-scoped pages).- Updated: config defaults, definitions drift (ever_id_login available), README table of requests (rows 14, 19, 20, 22), routes manifest (
/.well-known/ever-connect).
AI review
All 33 threads (Greptile 3, Copilot 19, CodeRabbit 11) fixed and answered on their threads with the spec that covers each: e4c322b32da (durable deletion/export handling with stored users and a claim, report purge on deletion, sessions ended before and after the link removal, worker-driven client request and backfill, per-user report order), 435b9409f6a (consent-bound client writes, ordered events, per-report enablement check), 14743665f04 (fenced and renewed claims, per-user seq, due query skips blocked reports, per-tenant backfill), 020a3a69008 (reports reconciled with the current link at send time), 9e088783cf1 + 6d76cdab90e (a user's other Ever ID links queued again before a user-scoped unlink), b682a4b1072 (a refused backfilled link reported again after its user signs in), d64b1243c92 (an identity's reports ordered by createdAt, then seq), 58e16abf4cf (unique per-user seq, nudges never postpone the worker, a request's users kept under its claim before their links go). The SDK moved to rc.8 in 8c15b887c95. CodeRabbit was rate-limited on the last two commits; both are self-reviewed in the PR comments. Codacy's markdown line-length notes on the two plugin docs: afd6fc3e6e3.
Not in this PR
- Binding
EVER_PERSON_DATA_PARTICIPANT(the account-deletion and export features). - Starting an address check (the installation does not send its address yet).
- The web part (
ever-connect-ui) showingsign_in_client.
π€ Generated with Claude Code