@@ -161,7 +161,7 @@ body {
161161
162162Call ` app.dispose() ` when your UI unmounts.
163163
164- ## Server: the two auth flows
164+ ## Server: the auth flows
165165
166166### 1. ` selfRegisterApp ` — register without the UI
167167
@@ -213,6 +213,8 @@ Nothing to copy, and **never print the secret**. Log `credentialsPath` instead.
213213- Records are keyed by Tolgee instance, so credentials issued by one instance
214214 are never handed to another.
215215- A re-registration that returns ` clientSecret: null ` keeps the stored secret.
216+ - ` secretIssuedAt ` records when Tolgee issued the stored secret, which is what
217+ ` ensureAppCredentialsFresh() ` ages out.
216218- Writes go through a temp file and a rename, so an interrupted or concurrent
217219 write cannot leave a half-written file behind; an unreadable file reads as
218220 "nothing stored" rather than throwing.
@@ -255,7 +257,62 @@ const { data, error } = await tolgee.GET('/v2/projects/{projectId}/activity', {
255257Inside an iframe you don't need this at all: the install token from
256258` TolgeeAppContext ` already authenticates calls as the install + user.
257259
258- ### 3. ` fetchAppInstallations ` — what am I installed for?
260+ ### 3. ` rotateAppClientSecret ` — replace the secret without anyone copying it
261+
262+ A client secret ends up in the hands of whoever set the app up. When that person
263+ leaves, the organization needs the old credential dead — without deleting the
264+ install, which would take its granted scopes, its availability and every
265+ per-project enablement with it.
266+
267+ Rotation is therefore two deliberate steps, and an install may hold ** several
268+ live secrets at once** (up to five):
269+
270+ 1 . ** Issue.** A new secret is minted. Every existing one keeps working.
271+ 2 . ** Revoke.** The old one is invalidated, on the operator's schedule, once
272+ Tolgee's ` lastUsedAt ` shows nothing is using it any more.
273+
274+ Step one is the app's own job, and needs no human:
275+
276+ ``` ts
277+ import { rotateAppClientSecret } from ' @tolgee/apps-sdk/server'
278+
279+ await rotateAppClientSecret ()
280+ ```
281+
282+ The call authenticates with the secret the app already holds, asks Tolgee for a
283+ new one, and writes it to the state file in place of the old one — atomically,
284+ and ** never returned and never logged** . The previous secret still
285+ authenticates, so a failed write leaves the app running on what it had.
286+
287+ ` ensureAppCredentialsFresh() ` is the same thing on a timer, meant for boot:
288+
289+ ``` ts
290+ await selfRegisterApp ({ ... })
291+ await ensureAppCredentialsFresh () // rotates only if the stored secret is > 30 days old
292+ ```
293+
294+ Pass ` { maxAgeMs } ` to change the age. It reports rather than throws when there
295+ is nothing to do, and it is a ** no-op when the credentials come from
296+ ` TOLGEE_APP_CLIENT_ID ` / ` TOLGEE_APP_CLIENT_SECRET ` ** — those win over the state
297+ file, so rotating would store a secret the app would never read. Rotate a
298+ deployment by issuing a secret in Tolgee and injecting it.
299+
300+ Run several replicas off one install? Only one of them should rotate: every call
301+ mints another secret, and Tolgee caps how many an install may hold.
302+
303+ Revoking is not the SDK's to do — one replica revoking would cut off its
304+ siblings — so step two happens in Tolgee, under ** Organization → Apps** (or
305+ ** Administration → Apps** for a native app). An app that genuinely owns its own
306+ lifecycle can still call ` DELETE /v2/apps/self/secrets/{id} ` ; Tolgee refuses to
307+ let it revoke its own last live secret, which would lock it out permanently.
308+
309+ > ** A leaked secret is recovered from per install.** There is no publisher
310+ > identity behind a distributed app — every install has credentials of its own
311+ > and there is nobody to authenticate as across all of them. The recourse for a
312+ > mass leak is to rotate each install, which is what the self-service endpoints
313+ > above exist to make scriptable.
314+
315+ ### 4. ` fetchAppInstallations ` — what am I installed for?
259316
260317An app backend with no iframe and no user has no idea which projects it may
261318touch: an org admin makes the app available, a project owner enables it, and
@@ -360,7 +417,8 @@ points).
360417
361418** ` @tolgee/apps-sdk/server ` ** — ` renderManifest() ` , ` tolgeeAppCorsHeaders() ` ,
362419` decodeContextToken() ` , ` loadTolgeeAppConfig() ` , ` selfRegisterApp() ` ,
363- ` fetchAppAccessToken() ` , ` createTolgeeAppServerClient() ` ,
420+ ` fetchAppAccessToken() ` , ` rotateAppClientSecret() ` ,
421+ ` ensureAppCredentialsFresh() ` , ` createTolgeeAppServerClient() ` ,
364422` fetchAppInstallations() ` (` AppInstallation ` ,
365423` AppEnabledProject ` , ` AppInstallationOrganization ` , ` AppInstallationsInput ` ),
366424` appInstallStatePath() ` , ` readStoredAppInstall() ` , ` saveAppInstall() ` .
0 commit comments