Skip to content

Commit 2ecc76b

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-24879-sharing-rule-legacy-row-org
2 parents 30629ff + fcb40ad commit 2ecc76b

26 files changed

Lines changed: 1145 additions & 64 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
fix(platform-objects): the password an administrator or user types into an action dialog is masked, no longer shown in clear text
6+
7+
Clause-②: no (an input's presentation: the param still accepts the same string, and no accept set or served shape moves)
8+
9+
Twelve credential params on the identity objects were declared `type: 'text'`, so the console's action dialog drew each as a plain text input and the typed password was readable on screen and in screenshots. Each now declares `type: 'password'`, which the dialog draws as a masked input:
10+
11+
- `sys_user`: `create_user`'s `password` and `set_user_password`'s `newPassword` (Setup › Users), `change_my_password`'s `currentPassword` and `newPassword`, and the `password` of `delete_my_account`, `enable_two_factor`, `disable_two_factor` and `generate_backup_codes`.
12+
- `sys_two_factor`: the `password` of `enable_two_factor`, `disable_two_factor` and `regenerate_backup_codes`.
13+
- `sys_sso_provider`: `register_sso_provider`'s `clientSecret`.
14+
15+
Labels, names, `required` and the posted body are unchanged: the value is still posted as the string typed, and no endpoint changes what it accepts.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
fix(platform-objects): Rotate Client Secret's result dialog states the immediate cut-off its confirmation states
6+
7+
Clause-②: no (text only: no accept set, served shape or behaviour moves)
8+
9+
- **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.
10+
- **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.
11+
- **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.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
---
4+
5+
fix(plugin-auth): the vendored auth library records and keys on the client address the server established (#24799)
6+
7+
Clause-②: no
8+
9+
better-auth reads a request's client address with its own reader, and three things are keyed on what it reads: a session's `ip_address` (and the `login` / `logout` audit rows copied from it), the address on the `login_failed` / `account_locked` audit rows, and the key of better-auth's own per-address rate limiter. That reader did not follow `server.trustProxy`. It now follows the same rule as the `allowed_ip_ranges` allow-list and `last_login_ip`: `X-Forwarded-For` (its first entry), then `X-Real-IP`, count only when the stack declares `server.trustProxy`. Otherwise the address is the connection's peer address. When neither is available, better-auth is handed no address.
10+
11+
**For operators:**
12+
13+
- Behind a reverse proxy or load balancer, declare `server: { trustProxy: true }` in the stack. Without it, sessions and audit rows record the proxy's own address, and better-auth's per-address rate limit counts every client behind the proxy as one client.
14+
- With `server.trustProxy` declared, a forwarded chain (`client, proxy`) now resolves to its first entry. Before, better-auth read a multi-entry header as no address.
15+
- A request whose client address cannot be established records no address, and better-auth's rate limiter counts it in one shared bucket.
16+
17+
**For hosts:** `AuthManager.handleRequest` now drops any `x-objectstack-client-address` request header a caller sends. The platform writes that header itself, from the address it established, and better-auth reads no other header for the client address. A programmatic `auth.api.*` call is handed whatever headers its caller passes. Route a client's request through `handleRequest` instead.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
fix(plugin-security): a platform operator's tenant-scoped insert from a session with no active organization is refused 403 naming the missing organization, as a member's is, instead of a 500
6+
7+
Clause-②: no (no accept set moves: the insert was refused before and is refused now, with the ADR-0123 D2 answer in place of an internal error)
8+
9+
- **Before.** Under `group` or `isolated`, a platform operator (`admin_full_access`) whose session had no active organization got `500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` for a create on a tenant object whose posture lets it cross the organization wall (`access.default: 'private'`). The operator crosses the wall there, so the wall let the insert through, and the engine then refused the row as a system write with no organization.
10+
- **After.** That create gets `403 PERMISSION_DENIED` with the message a member gets in the same state. The message names the missing active organization and tells the user to join or select one.
11+
- **Unchanged.** Nothing was written before and nothing is written now. The refusal judges the row as it is about to be stored, the same row the engine's own organization check reads, so it covers exactly the inserts that answered 500 and no insert that was admitted. These are as before:
12+
- a row a `beforeInsert` hook places in an organization;
13+
- the operator's reads and updates across organizations;
14+
- inserts from an operator with an active organization;
15+
- inserts into objects that opted out of tenancy;
16+
- system contexts (which still meet the engine's own refusal);
17+
- every `single` answer.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/service-settings': patch
3+
---
4+
5+
fix(service-settings): Setup → Authentication saves "Enable Google login" ON when the Google credentials come from the server (#24844)
6+
7+
Clause-②: no
8+
9+
On a deployment that sets `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` and leaves the two credential fields empty, saving `auth.google_enabled: true` was refused with `SETTINGS_VALIDATION` (`google_client_id` / `google_client_secret`: `required`). An admin who switched Google off on the page could not switch it back on there, although the server held valid credentials. The settings service reads only `OS_*` names, so it cannot see that pair.
10+
11+
`auth.google_client_id` and `auth.google_client_secret` are no longer `required`. Google sign-in needs the two credentials to resolve, saved on the page or set on the server; it never needed them stored. The `google_enabled` help already says so: Google is offered once both resolve, and the server's pair wins over saved values.
12+
13+
Who may sign in, and which providers the login page offers, do not change for any deployment. Saving the toggle ON while neither pair resolves is now accepted; the login page then shows no Google button, as it does with the toggle left at its default.

‎packages/platform-objects/src/apps/translations/en.objects.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1557,7 +1557,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
15571557
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.",
15581558
resultDialog: {
15591559
title: "Client secret rotated",
1560-
description: "Save the new secret now — it is shown only once. Update every integration before the previous secret's grace period ends.",
1560+
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.",
15611561
acknowledge: "I have updated my integrations",
15621562
fields: {
15631563
client_secret: "New Client Secret"

‎packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1557,7 +1557,7 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
15571557
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.",
15581558
resultDialog: {
15591559
title: "Secreto de cliente rotado",
1560-
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.",
1560+
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.",
15611561
acknowledge: "He actualizado mis integraciones",
15621562
fields: {
15631563
client_secret: "Nuevo secreto de cliente"

‎packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1557,7 +1557,7 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
15571557
description: "このOAuthクライアントのシークレットをローテーションしますか?以前のシークレットは直ちに使用できなくなり、それを使用している連携は新しいシークレットに更新されるまで動作しなくなります。新しいシークレットは一度しか表示されません。",
15581558
resultDialog: {
15591559
title: "クライアントシークレットをローテーションしました",
1560-
description: "新しいシークレットを今すぐ保存してください。表示は一度きりです。旧シークレットの猶予期間が終わる前にすべての連携を更新してください。",
1560+
description: "新しいシークレットを今すぐ保存してください。表示は一度きりです。以前のシークレットはすでに使用できなくなっています。すべての連携を新しいシークレットに更新してください。",
15611561
acknowledge: "連携を更新しました",
15621562
fields: {
15631563
client_secret: "新しいクライアントシークレット"

‎packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1557,7 +1557,7 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
15571557
description: "确定要轮换此 OAuth 客户端的密钥吗?旧密钥将立即失效,任何使用它的集成都将中断,直到更新为新密钥为止。新密钥仅显示一次。",
15581558
resultDialog: {
15591559
title: "Client Secret 已轮换",
1560-
description: "请立即保存新密钥——它只显示一次。请在旧密钥宽限期结束前更新所有集成。",
1560+
description: "请立即保存新密钥——它只显示一次。旧密钥已经失效:请用新密钥更新所有集成。",
15611561
acknowledge: "我已更新所有集成",
15621562
fields: {
15631563
client_secret: "新的 Client Secret"
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* `sys_oauth_application.rotate_client_secret` — the confirmation and the
5+
* result dialog say the same thing about the previous secret.
6+
*
7+
* The two texts once disagreed: the confirmation said the previous secret
8+
* "will stop working immediately", the result dialog told the admin to update
9+
* integrations "before the previous secret's grace period ends". Only the
10+
* first is true. The route is better-auth's own
11+
* (`@better-auth/oauth-provider` 1.7.3, `rotateClientSecretEndpoint`): it
12+
* overwrites the client's single stored `clientSecret`, and
13+
* `validateClientCredentials` checks a presented secret against that one value.
14+
* No previous secret is kept, so no grace period exists.
15+
*
16+
* The wording IS the contract here — it is what an admin acts on before
17+
* breaking every integration — so it is pinned in the source and in all four
18+
* shipped locales. The translated leaves are hand-written: `pnpm i18n:extract`
19+
* keeps them in merge mode and the source-hash table holds no entry for them
20+
* (they are real translations, not byte copies), so a stale translation of an
21+
* edited source is not flagged anywhere else.
22+
*/
23+
24+
import { describe, expect, it } from 'vitest';
25+
import { SysOauthApplication } from './index.js';
26+
import { enObjects } from '../apps/translations/en.objects.generated.js';
27+
import { zhCNObjects } from '../apps/translations/zh-CN.objects.generated.js';
28+
import { jaJPObjects } from '../apps/translations/ja-JP.objects.generated.js';
29+
import { esESObjects } from '../apps/translations/es-ES.objects.generated.js';
30+
31+
const rotate = (SysOauthApplication.actions ?? []).find((a) => a.name === 'rotate_client_secret');
32+
const node = (bundle: any) => bundle?.sys_oauth_application?._actions?.rotate_client_secret;
33+
34+
/** Any promise of a window during which the previous secret still works. */
35+
const GRACE_TERMS: readonly (readonly [string, any, RegExp])[] = [
36+
['en', enObjects, /grace|window|expire/i],
37+
['zh-CN', zhCNObjects, /宽限|过渡期|有效期/],
38+
['ja-JP', jaJPObjects, /猶予|移行期間|有効期限/],
39+
['es-ES', esESObjects, /gracia|transición|caduque|expire/i],
40+
];
41+
42+
describe('rotate_client_secret — both texts state the immediate cut-off', () => {
43+
it('the source confirmation and result dialog both say the previous secret stops at once', () => {
44+
expect(rotate, 'rotate_client_secret is declared').toBeDefined();
45+
const confirm = rotate?.description ?? '';
46+
const result = rotate?.resultDialog?.description ?? '';
47+
expect(confirm).toMatch(/previous secret will stop working immediately/);
48+
expect(result).toMatch(/previous secret has already stopped working/);
49+
for (const [label, text] of [['description', confirm], ['resultDialog.description', result]] as const) {
50+
expect(text, `${label} promises a grace period the route does not keep`).not.toMatch(/grace|window|expire/i);
51+
}
52+
});
53+
54+
it('the result dialog keeps its shown-once warning, acknowledgement and secret field', () => {
55+
expect(rotate?.resultDialog?.description).toMatch(/^Save the new secret now — it is shown only once\./);
56+
expect(rotate?.resultDialog?.acknowledge).toBe('I have updated my integrations');
57+
expect(rotate?.resultDialog?.fields?.map((f) => [f.path, f.format])).toEqual([['client_secret', 'secret']]);
58+
});
59+
60+
it('the English bundle mirrors the source', () => {
61+
expect(node(enObjects)?.description).toBe(rotate?.description);
62+
expect(node(enObjects)?.resultDialog?.description).toBe(rotate?.resultDialog?.description);
63+
});
64+
65+
it('no locale promises a grace period in either text, and each translates the result dialog', () => {
66+
for (const [locale, bundle, grace] of GRACE_TERMS) {
67+
const n = node(bundle);
68+
expect(n?.description, `${locale} description`).toBeTruthy();
69+
expect(n?.resultDialog?.description, `${locale} resultDialog.description`).toBeTruthy();
70+
expect(n.description, `${locale} description promises a grace period`).not.toMatch(grace);
71+
expect(n.resultDialog.description, `${locale} resultDialog.description promises a grace period`).not.toMatch(grace);
72+
if (locale !== 'en') {
73+
expect(n.resultDialog.description, `${locale} resultDialog.description is the English source`)
74+
.not.toBe(node(enObjects)?.resultDialog?.description);
75+
}
76+
}
77+
});
78+
});

0 commit comments

Comments
 (0)