From 82095d34d5d065bc631904e1472a2b092f7d9ba5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:11:18 +0000 Subject: [PATCH 1/3] fix(platform-objects): rotate-secret result dialog states the immediate cut-off Claude-Session: https://claude.ai/code/session_01MrvwG9BMeMc4oCSG9FsV95 Co-authored-by: Claude --- .../src/apps/translations/es-ES.objects.generated.ts | 2 +- .../src/apps/translations/ja-JP.objects.generated.ts | 2 +- .../src/apps/translations/zh-CN.objects.generated.ts | 2 +- .../src/identity/sys-oauth-application.object.ts | 9 ++++++++- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts index ea6be3bb174..34db3f2150a 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts @@ -1557,7 +1557,7 @@ export const esESObjects: NonNullable = { description: "¿Rotar el secreto de este cliente OAuth? El secreto anterior dejará de funcionar de inmediato y cualquier integración que lo utilice fallará hasta que se actualice con el nuevo secreto. El nuevo secreto se muestra una sola vez.", resultDialog: { title: "Secreto de cliente rotado", - description: "Guarde el nuevo secreto ahora: se muestra una sola vez. Actualice todas las integraciones antes de que termine el periodo de gracia del secreto anterior.", + description: "Guarde el nuevo secreto ahora: se muestra una sola vez. El secreto anterior ya ha dejado de funcionar; actualice todas las integraciones con el nuevo secreto.", acknowledge: "He actualizado mis integraciones", fields: { client_secret: "Nuevo secreto de cliente" diff --git a/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts index f188da5d8b5..0b7797b6a4a 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts @@ -1557,7 +1557,7 @@ export const jaJPObjects: NonNullable = { description: "このOAuthクライアントのシークレットをローテーションしますか?以前のシークレットは直ちに使用できなくなり、それを使用している連携は新しいシークレットに更新されるまで動作しなくなります。新しいシークレットは一度しか表示されません。", resultDialog: { title: "クライアントシークレットをローテーションしました", - description: "新しいシークレットを今すぐ保存してください。表示は一度きりです。旧シークレットの猶予期間が終わる前にすべての連携を更新してください。", + description: "新しいシークレットを今すぐ保存してください。表示は一度きりです。以前のシークレットはすでに使用できなくなっています。すべての連携を新しいシークレットに更新してください。", acknowledge: "連携を更新しました", fields: { client_secret: "新しいクライアントシークレット" diff --git a/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts index 0b3f3e58446..65c34bca849 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts @@ -1557,7 +1557,7 @@ export const zhCNObjects: NonNullable = { description: "确定要轮换此 OAuth 客户端的密钥吗?旧密钥将立即失效,任何使用它的集成都将中断,直到更新为新密钥为止。新密钥仅显示一次。", resultDialog: { title: "Client Secret 已轮换", - description: "请立即保存新密钥——它只显示一次。请在旧密钥宽限期结束前更新所有集成。", + description: "请立即保存新密钥——它只显示一次。旧密钥已经失效:请用新密钥更新所有集成。", acknowledge: "我已更新所有集成", fields: { client_secret: "新的 Client Secret" diff --git a/packages/platform-objects/src/identity/sys-oauth-application.object.ts b/packages/platform-objects/src/identity/sys-oauth-application.object.ts index caf6e6d417c..d5ffc56c6b5 100644 --- a/packages/platform-objects/src/identity/sys-oauth-application.object.ts +++ b/packages/platform-objects/src/identity/sys-oauth-application.object.ts @@ -183,6 +183,13 @@ export const SysOauthApplication = ObjectSchema.create({ // then the `resultDialog` that reveals the new secret AFTER the rotation. // The result dialog is not part of the #7278 defect — it is a post-run // reveal for output shown only once, not a second pre-run decision. + // + // Both texts state the same cut-off, because that is what the route does: + // better-auth's `rotateClientSecretEndpoint` (@better-auth/oauth-provider + // 1.7.3) overwrites the client's single stored `clientSecret`, and + // `validateClientCredentials` checks a presented secret against that one + // value only. No previous secret is kept, so there is no grace period — + // the old secret is refused from the moment the rotation commits. description: 'Rotate this OAuth client\'s secret? The previous secret will stop working immediately and any integrations using it will break until they are updated with the new secret. The new secret is shown only once.', refreshAfter: true, params: [ @@ -190,7 +197,7 @@ export const SysOauthApplication = ObjectSchema.create({ ], resultDialog: { title: 'Client secret rotated', - description: 'Save the new secret now — it is shown only once. Update every integration before the previous secret\'s grace period ends.', + description: 'Save the new secret now — it is shown only once. The previous secret has already stopped working: update every integration with the new secret.', acknowledge: 'I have updated my integrations', fields: [ { path: 'client_secret', label: 'New Client Secret', format: 'secret' }, From 7e9040df36f93ea470544b8548da518c33b59a22 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:21:03 +0000 Subject: [PATCH 2/3] test(platform-objects): pin the rotate-secret cut-off wording in source and every locale Claude-Session: https://claude.ai/code/session_01MrvwG9BMeMc4oCSG9FsV95 Co-authored-by: Claude --- .../apps/translations/en.objects.generated.ts | 2 +- .../rotate-client-secret-copy.test.ts | 78 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 packages/platform-objects/src/identity/rotate-client-secret-copy.test.ts diff --git a/packages/platform-objects/src/apps/translations/en.objects.generated.ts b/packages/platform-objects/src/apps/translations/en.objects.generated.ts index 9ac19b36828..0876b874cf8 100644 --- a/packages/platform-objects/src/apps/translations/en.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.objects.generated.ts @@ -1557,7 +1557,7 @@ export const enObjects: NonNullable = { description: "Rotate this OAuth client's secret? The previous secret will stop working immediately and any integrations using it will break until they are updated with the new secret. The new secret is shown only once.", resultDialog: { title: "Client secret rotated", - description: "Save the new secret now — it is shown only once. Update every integration before the previous secret's grace period ends.", + description: "Save the new secret now — it is shown only once. The previous secret has already stopped working: update every integration with the new secret.", acknowledge: "I have updated my integrations", fields: { client_secret: "New Client Secret" diff --git a/packages/platform-objects/src/identity/rotate-client-secret-copy.test.ts b/packages/platform-objects/src/identity/rotate-client-secret-copy.test.ts new file mode 100644 index 00000000000..9f98e12764e --- /dev/null +++ b/packages/platform-objects/src/identity/rotate-client-secret-copy.test.ts @@ -0,0 +1,78 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * `sys_oauth_application.rotate_client_secret` — the confirmation and the + * result dialog say the same thing about the previous secret. + * + * The two texts once disagreed: the confirmation said the previous secret + * "will stop working immediately", the result dialog told the admin to update + * integrations "before the previous secret's grace period ends". Only the + * first is true. The route is better-auth's own + * (`@better-auth/oauth-provider` 1.7.3, `rotateClientSecretEndpoint`): it + * overwrites the client's single stored `clientSecret`, and + * `validateClientCredentials` checks a presented secret against that one value. + * No previous secret is kept, so no grace period exists. + * + * The wording IS the contract here — it is what an admin acts on before + * breaking every integration — so it is pinned in the source and in all four + * shipped locales. The translated leaves are hand-written: `pnpm i18n:extract` + * keeps them in merge mode and the source-hash table holds no entry for them + * (they are real translations, not byte copies), so a stale translation of an + * edited source is not flagged anywhere else. + */ + +import { describe, expect, it } from 'vitest'; +import { SysOauthApplication } from './index.js'; +import { enObjects } from '../apps/translations/en.objects.generated.js'; +import { zhCNObjects } from '../apps/translations/zh-CN.objects.generated.js'; +import { jaJPObjects } from '../apps/translations/ja-JP.objects.generated.js'; +import { esESObjects } from '../apps/translations/es-ES.objects.generated.js'; + +const rotate = (SysOauthApplication.actions ?? []).find((a) => a.name === 'rotate_client_secret'); +const node = (bundle: any) => bundle?.sys_oauth_application?._actions?.rotate_client_secret; + +/** Any promise of a window during which the previous secret still works. */ +const GRACE_TERMS: readonly (readonly [string, any, RegExp])[] = [ + ['en', enObjects, /grace|window|expire/i], + ['zh-CN', zhCNObjects, /宽限|过渡期|有效期/], + ['ja-JP', jaJPObjects, /猶予|移行期間|有効期限/], + ['es-ES', esESObjects, /gracia|transición|caduque|expire/i], +]; + +describe('rotate_client_secret — both texts state the immediate cut-off', () => { + it('the source confirmation and result dialog both say the previous secret stops at once', () => { + expect(rotate, 'rotate_client_secret is declared').toBeDefined(); + const confirm = rotate?.description ?? ''; + const result = rotate?.resultDialog?.description ?? ''; + expect(confirm).toMatch(/previous secret will stop working immediately/); + expect(result).toMatch(/previous secret has already stopped working/); + for (const [label, text] of [['description', confirm], ['resultDialog.description', result]] as const) { + expect(text, `${label} promises a grace period the route does not keep`).not.toMatch(/grace|window|expire/i); + } + }); + + it('the result dialog keeps its shown-once warning, acknowledgement and secret field', () => { + expect(rotate?.resultDialog?.description).toMatch(/^Save the new secret now — it is shown only once\./); + expect(rotate?.resultDialog?.acknowledge).toBe('I have updated my integrations'); + expect(rotate?.resultDialog?.fields?.map((f) => [f.path, f.format])).toEqual([['client_secret', 'secret']]); + }); + + it('the English bundle mirrors the source', () => { + expect(node(enObjects)?.description).toBe(rotate?.description); + expect(node(enObjects)?.resultDialog?.description).toBe(rotate?.resultDialog?.description); + }); + + it('no locale promises a grace period in either text, and each translates the result dialog', () => { + for (const [locale, bundle, grace] of GRACE_TERMS) { + const n = node(bundle); + expect(n?.description, `${locale} description`).toBeTruthy(); + expect(n?.resultDialog?.description, `${locale} resultDialog.description`).toBeTruthy(); + expect(n.description, `${locale} description promises a grace period`).not.toMatch(grace); + expect(n.resultDialog.description, `${locale} resultDialog.description promises a grace period`).not.toMatch(grace); + if (locale !== 'en') { + expect(n.resultDialog.description, `${locale} resultDialog.description is the English source`) + .not.toBe(node(enObjects)?.resultDialog?.description); + } + } + }); +}); From 31bd1308192cc0985a124e94e4e915480010f677 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:21:15 +0000 Subject: [PATCH 3/3] chore(changeset): platform-objects patch for the rotate-secret copy Claude-Session: https://claude.ai/code/session_01MrvwG9BMeMc4oCSG9FsV95 Co-authored-by: Claude --- .changeset/24774-rotate-secret-cut-off-copy.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/24774-rotate-secret-cut-off-copy.md diff --git a/.changeset/24774-rotate-secret-cut-off-copy.md b/.changeset/24774-rotate-secret-cut-off-copy.md new file mode 100644 index 00000000000..cebfe019c9a --- /dev/null +++ b/.changeset/24774-rotate-secret-cut-off-copy.md @@ -0,0 +1,11 @@ +--- +'@objectstack/platform-objects': patch +--- + +fix(platform-objects): Rotate Client Secret's result dialog states the immediate cut-off its confirmation states + +Clause-②: no (text only: no accept set, served shape or behaviour moves) + +- **Before.** On `sys_oauth_application`, the Rotate Client Secret confirmation said the previous secret "will stop working immediately". The result dialog then told the admin to "update every integration before the previous secret's grace period ends". Only the first is true. +- **What rotation does.** `POST /api/v1/auth/oauth2/client/rotate-secret` is better-auth's own route. It overwrites the client's one stored secret, and client authentication checks a presented secret against that value only. No previous secret is kept, so there is no grace period: the old secret is refused from the moment the rotation completes. +- **Now.** The result dialog reads "Save the new secret now — it is shown only once. The previous secret has already stopped working: update every integration with the new secret." The zh-CN, ja-JP and es-ES translations of that sentence are updated to match. The confirmation, the dialog's title, acknowledgement and secret field are unchanged.