Skip to content

Commit 740edc5

Browse files
fix(plugin-audit,platform-objects): the audit log's default list shows what people did; the audit objects read in Chinese (#30620)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgJEWbotugYjYfjGSJZi35n7)_ domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE ## 维护者速读 - 改了什么:Setup「审计日志」打开时的默认列表(最近)只列人做的操作:登录时顺带写的「更新用户(最近登录时间)」行、设置变更顺带写的设置存储行和设置审计行、密钥存储、签名密钥、数据迁移台账都不再出现在默认列表;「平台设置」「平台设置审计记录」「评论表情回应」在中文界面显示中文名。 - 为什么改:真实用户走查发现一次登录在审计日志里写好几行,设置改一次出现三四行,管理员找不到真正的操作;「Platform Setting Audit Entry」等对象名在中文界面是英文。 - 风险与回滚:不少写任何一行,也不删数据,「全部」视图照旧列出每一行;登录戳那一行多带一个 metadata 标记 `{"event":"auth.last_login_stamp"}`。回滚即 revert 本 PR,已写入的标记无害。 - 先看:`packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts`(默认视图 recent 的过滤与排除清单),`packages/plugins/plugin-audit/src/sign-in-stamp.ts` 与 `audit-writers.ts` 里打标记的两处,`packages/platform-objects/scripts/i18n-extract.config.ts`。 - 两分钟验证:`pnpm --filter @objectstack/plugin-audit exec vitest run src/sys-audit-log-default-view.integration.test.ts`,预期 5 条全过:登录戳被标记且不进默认列表,管理员改用户仍在,设置变更行仍在,「全部」仍列出所有行。 - 需要你做的:无,席位落地。 Refs #30209 (row 3) Clause-②: no — no `packages/spec` change; the new constants are module-internal (not exported from the package entry) Metadata-protocol: none Skills-docs: none — `git grep -n -i -E "audit log|last_login|sys_platform_setting|comment reaction" -- skills` on `ef370d07e` answers two lines (`skills/objectstack-ui/rules/list-views.md:222`, an example grid labelled `Audit Log`; `skills/objectstack-platform/references/plugin-development.md:211`, a plugin's log line), neither about the shipped audit log's views or names, so this PR makes neither false. ## What changes **Row 3, the audit log's default list.** Measured on a fresh `pnpm dev -- --fresh` showcase boot (`c23eafa8`), one act wrote several ledger rows: | Act | Rows on `sys_audit_log` | | --- | --- | | A sign-in | `login` on `sys_session`, and `update` on the person's `sys_user` stamping `last_login_at` / `last_login_ip` (plugin-auth `stampLastLogin`, under the person's own id) | | The first sign-in through the console | the two above, and `create` on `sys_jwks` (the token-signing key) | | A tenant setting change | `create`/`update` on `sys_setting`, `config_change` naming `sys_setting`, `create` on `sys_setting_audit` | | A global secret change | `create` on `sys_secret`, `create` on `sys_platform_setting`, `config_change` naming `sys_platform_setting`, `create` on `sys_platform_setting_audit` | | A boot | rows on `sys_migration` | - `plugin-audit`: the CRUD ledger writer marks the sign-in stamp. `isSignInStamp` (`sign-in-stamp.ts`) answers true for an update of `sys_user` whose changed fields (the writer's own diff) are `last_login_at` / `last_login_ip` and nothing else; the row then carries `metadata: {"event":"auth.last_login_stamp"}`, the place `config_change` rows already name their event (`settings.set`). An administrator's edit of a user, or any write that changes another field beside the stamp, carries no mark. The row is still written. A delegated write's `performed_by` / `on_behalf_of` keys are kept beside the mark when both apply. - `sys_audit_log.listViews.recent` (the first declared view, which Setup opens) gains a filter: rows on `sys_secret`, `sys_setting_audit`, `sys_platform_setting_audit`, `sys_jwks`, `sys_migration` and `sys_migration_journal` are left out; rows on `sys_setting` / `sys_platform_setting` are left out unless the action is `config_change` (that row names the store as its object); and a row whose `metadata` contains the stamp's event is left out (an any-of group with `metadata` is null, so rows without metadata stay). `metadata` is not one of the value-bearing columns `parent-field-query-guard.ts` refuses to filter on, so every reader can run the view. `all_events` is unchanged and lists every row. Object names are read from the definitions (`@objectstack/platform-objects/system`, `SystemObjectName.JWKS`), not a name prefix: `sys_user`, `sys_member`, sharing rules and the like stay in, because their writes are acts. **Row 3, the audit objects' names in Chinese.** `sys_platform_setting` and `sys_platform_setting_audit` (registered by `service-settings`, defined in `platform-objects`) were in no extract config, so no bundle carried them in any locale. They join `platform-objects`' extract config (and its `bundle-ownership` list); the extract added them to every bundle, and zh-CN is written by hand (object, field, help, option and view names, following `sys_setting` / `sys_setting_audit`). `sys_comment_reaction`'s zh-CN leaves in `plugin-audit` held the English fill and are translated; the regenerate dropped their fill records. ## Tests - `plugin-audit/src/sys-audit-log-default-view.integration.test.ts` (new; real kernel, `AuditPlugin`, SQLite driver, the view's filter lowered the way the Console lowers it, `or` group included, and parsed by `parseFilterAST`): the stamp is marked and the two controls are not; `recent` keeps `login`, both `config_change` rows and the administrator's edits, and leaves out the stamp and every machinery row; `recent` reads newest first; `all_events` still lists all of them. - `platform-objects/src/system/platform-setting-zh-cn.test.ts` (new): served zh-CN (`SetupAppTranslations`) names both objects and every declared field and option in Chinese. - `plugin-audit/src/translations/comment-reaction-zh-cn.test.ts` (new): served zh-CN (`AuditTranslations`) names `sys_comment_reaction`, its fields and helps in Chinese. Red first on `main` `2ffdb649` (pin commit `ccb7a5f8`): default view 2 failed / 3 passed (stamp unmarked; stamp in `recent`), platform settings zh-CN 4/4 failed, comment reaction zh-CN 2/2 failed. Ablation (once, `scripts/ablation-replace.mjs`, anchor hit x1, restored blob equals HEAD, `git diff HEAD` empty): with `signInStamp = isSignInStamp(…)` replaced by `false`, the default-view file goes 2 failed / 3 passed (the mark pin and the `recent` pin). ## Acceptance notes - **Rows 4, 5 and 6 are not in this PR** (`Refs`, not a closing keyword). Row 4 (config change old/new values) and row 5 (the sharing-rule object picker) are in the card report as decisions; row 6 (queue members) is a reading only. - **ja-JP and es-ES** carry the extract's English fill for the two settings objects, recorded in their `source-hashes` files; `sys_comment_reaction` in those locales is unchanged. - **The sign-in stamp is recognised by its fields.** A future write that changes only `last_login_at` / `last_login_ip` on `sys_user` for another reason would be marked too; today plugin-auth's stamp is the only writer of those two fields. - Other `sys_*` objects that still read English under zh-CN (approvals tokens and approvers, connector and flow credentials, metadata commit and activation, webhook, trusted key, identity email, automation run, assignment cursor, audience binding suggestion, migration journal) belong to other packages and are not touched. ## Verification <details><summary>Package suites and typechecks (head ef370d0, after merging origin/main with #30551)</summary> - `pnpm --filter @objectstack/platform-objects --filter @objectstack/plugin-audit test` under the verify lock: platform-objects 114 files / 1567 tests, plugin-audit 73 files / 1144 tests, VERDICT command-exit 0. - `pnpm --filter @objectstack/plugin-audit --filter @objectstack/platform-objects typecheck`: VERDICT command-exit 0. - `node scripts/check-i18n-bundles.mjs --write` after the merge: byte-identical (no file changed); `pnpm check:i18n`: all ten bundle sets in sync. - eslint `--no-inline-config` on the changed TypeScript files: exit 0. </details> <details><summary>Derived gate families</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on `ef370d07e` (after the `origin/main` merge) derived 64 commands. Each ran with its exit code captured before any pipe: all 64 exited 0. `--ran`: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN. - On the pre-merge tree (`77f2050b`) the same 64 ran green except `node scripts/check-plugin-teardown-shape.mjs --self-test`, exit 3 on the shallow clone; it exited 0 after `git fetch` of its fixture commit, and exited 0 on the merged head too. - `pnpm lint` (repository-wide, `eslint . --no-inline-config`) on `ef370d07e`: exit 0. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CbEH94Gqn2583kuuzH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6e8ec73 commit 740edc5

18 files changed

Lines changed: 1111 additions & 27 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/plugin-audit': patch
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
fix(plugin-audit,platform-objects): the audit log's default list shows what people did, not the rows the platform writes beside it; the audit objects read in Chinese
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
Setup's 审计日志 opens the first `sys_audit_log` list view, `recent`. One sign-in used to put two rows there (the `login` row and an `update` of the person's `sys_user` stamping `last_login_at` / `last_login_ip`), the first sign-in through the console a third (the token-signing key in `sys_jwks`), and one settings change three or four (the `config_change` row, the stored setting, a secret's ciphertext, and the settings trail's own entry).
12+
13+
- The ledger writer now marks the sign-in stamp row with `metadata: {"event":"auth.last_login_stamp"}`, the way `config_change` rows carry `settings.set`. Only an update of `sys_user` whose changes are the stamp's fields alone is marked; an administrator's edit of a user is not. The row is still written.
14+
- `recent` leaves out the marked row, the rows on `sys_secret`, `sys_setting_audit`, `sys_platform_setting_audit`, `sys_jwks`, `sys_migration` and `sys_migration_journal`, and the rows on `sys_setting` / `sys_platform_setting` other than `config_change`. Nothing is removed from storage, and `all_events` (全部) still lists every row.
15+
- `sys_platform_setting` and `sys_platform_setting_audit` were in no translation bundle, so they read English in every locale; they now ship in `@objectstack/platform-objects`' bundles with zh-CN translations (ja-JP and es-ES carry the English fill). `sys_comment_reaction`'s zh-CN bundle is translated.

‎packages/platform-objects/scripts/i18n-extract.config.ts‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,18 @@ import {
188188
} from '../src/metadata/index.js';
189189

190190
// ── System ────────────────────────────────────────────────────────────────
191-
import { SysSetting, SysSecret, SysSettingAudit, SysMigration, SysExchangeRate } from '../src/system/index.js';
191+
// `SysPlatformSetting` / `SysPlatformSettingAudit` are the settings global
192+
// rung (ADR-0131 D7). service-settings registers them, and their labels are
193+
// served from this package's bundles like `sys_setting`'s.
194+
import {
195+
SysSetting,
196+
SysPlatformSetting,
197+
SysSecret,
198+
SysSettingAudit,
199+
SysPlatformSettingAudit,
200+
SysMigration,
201+
SysExchangeRate,
202+
} from '../src/system/index.js';
192203

193204
// ── Existing Setup app + dashboards + translations ────────────────────────
194205
import { SETUP_APP } from '../src/apps/setup.app.js';
@@ -316,8 +327,10 @@ const config: ObjectStackDefinition = defineStack({
316327

317328
// System
318329
SysSetting,
330+
SysPlatformSetting,
319331
SysSecret,
320332
SysSettingAudit,
333+
SysPlatformSettingAudit,
321334
SysMigration,
322335
SysExchangeRate,
323336
] as any,

‎packages/platform-objects/src/apps/translations/bundle-ownership.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const OWNED_OBJECTS = new Set([
3838
// metadata (sys_view_definition retired as inert, ADR-0131 D13)
3939
'sys_metadata', 'sys_metadata_history', 'sys_metadata_audit',
4040
// system
41-
'sys_setting', 'sys_secret', 'sys_setting_audit', 'sys_migration', 'sys_exchange_rate',
41+
'sys_setting', 'sys_platform_setting', 'sys_secret', 'sys_setting_audit', 'sys_platform_setting_audit', 'sys_migration', 'sys_exchange_rate',
4242
]);
4343

4444
describe('objects translation bundle ownership (ADR-0029 D8)', () => {

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

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3446,6 +3446,54 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
34463446
}
34473447
}
34483448
},
3449+
sys_platform_setting: {
3450+
label: "Platform Setting",
3451+
pluralLabel: "Platform Settings",
3452+
description: "Deployment-wide settings values: the global rung of the settings cascade.",
3453+
fields: {
3454+
id: {
3455+
label: "Setting ID"
3456+
},
3457+
created_at: {
3458+
label: "Created At"
3459+
},
3460+
updated_at: {
3461+
label: "Updated At"
3462+
},
3463+
namespace: {
3464+
label: "Namespace",
3465+
help: "Manifest namespace (e.g. mail, storage, ai)."
3466+
},
3467+
key: {
3468+
label: "Key",
3469+
help: "Specifier key inside the namespace (snake_case)."
3470+
},
3471+
value: {
3472+
label: "Value",
3473+
help: "JSON-encoded value. Null when encrypted=true (see value_enc)."
3474+
},
3475+
encrypted: {
3476+
label: "Encrypted",
3477+
help: "When true, the value is stored encrypted-at-rest in value_enc; value column is null."
3478+
},
3479+
locked: {
3480+
label: "Locked",
3481+
help: "When true, tenant and user rows cannot override this value; writes against those scopes return 409."
3482+
},
3483+
locked_reason: {
3484+
label: "Lock Reason",
3485+
help: "Human-readable explanation surfaced in the UI tooltip when locked=true."
3486+
},
3487+
value_enc: {
3488+
label: "Encrypted Value",
3489+
help: "Handle of the sys_secret row holding the ciphertext. Set only when encrypted=true."
3490+
},
3491+
updated_by: {
3492+
label: "Updated By",
3493+
help: "Last actor who wrote this row through the settings service."
3494+
}
3495+
}
3496+
},
34493497
sys_secret: {
34503498
label: "Secret",
34513499
pluralLabel: "Secrets",
@@ -3573,6 +3621,84 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
35733621
}
35743622
}
35753623
},
3624+
sys_platform_setting_audit: {
3625+
label: "Platform Setting Audit Entry",
3626+
pluralLabel: "Platform Setting Audit",
3627+
description: "Append-only audit trail for SettingsService mutations of global-scope keys.",
3628+
fields: {
3629+
id: {
3630+
label: "ID"
3631+
},
3632+
created_at: {
3633+
label: "Created At",
3634+
help: "When the mutation was recorded."
3635+
},
3636+
namespace: {
3637+
label: "Namespace"
3638+
},
3639+
key: {
3640+
label: "Key"
3641+
},
3642+
scope: {
3643+
label: "Scope",
3644+
help: "Cascade layer the row was written to — always the global rung here.",
3645+
options: {
3646+
global: "Global"
3647+
}
3648+
},
3649+
action: {
3650+
label: "Action",
3651+
help: "Mutation kind.",
3652+
options: {
3653+
set: "Set",
3654+
reset: "Reset",
3655+
lock: "Lock",
3656+
unlock: "Unlock",
3657+
rotate: "Rotate"
3658+
}
3659+
},
3660+
actor_id: {
3661+
label: "Actor",
3662+
help: "User who performed the mutation; null for system jobs."
3663+
},
3664+
source: {
3665+
label: "Source",
3666+
help: "Mutation entry-point.",
3667+
options: {
3668+
ui: "UI",
3669+
api: "API",
3670+
migration: "Migration",
3671+
import: "Import",
3672+
system: "System"
3673+
}
3674+
},
3675+
reason: {
3676+
label: "Reason",
3677+
help: "Free-text justification provided by the actor (optional)."
3678+
},
3679+
old_hash: {
3680+
label: "Old Hash",
3681+
help: "Digest of the previous value, never the plaintext: sha256: hex for a plain value, the keyed hmac-sha256: digest for a secret. Null when previously unset, or for a secret with no keyed digest."
3682+
},
3683+
new_hash: {
3684+
label: "New Hash",
3685+
help: "Digest of the new value, never the plaintext: sha256: hex for a plain value, the keyed hmac-sha256: digest for a secret. Null on reset, or for a secret with no keyed digest."
3686+
},
3687+
encrypted: {
3688+
label: "Encrypted",
3689+
help: "True when the field carries secret material (rotation is interesting)."
3690+
},
3691+
request_id: {
3692+
label: "Request ID",
3693+
help: "Correlates with sys_audit_log / tracing."
3694+
}
3695+
},
3696+
_views: {
3697+
recent: {
3698+
label: "Recent"
3699+
}
3700+
}
3701+
},
35763702
sys_migration: {
35773703
label: "Data Migration",
35783704
pluralLabel: "Data Migrations",

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

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3446,6 +3446,54 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
34463446
}
34473447
}
34483448
},
3449+
sys_platform_setting: {
3450+
label: "Platform Setting",
3451+
pluralLabel: "Platform Settings",
3452+
description: "Deployment-wide settings values: the global rung of the settings cascade.",
3453+
fields: {
3454+
id: {
3455+
label: "Setting ID"
3456+
},
3457+
created_at: {
3458+
label: "Created At"
3459+
},
3460+
updated_at: {
3461+
label: "Updated At"
3462+
},
3463+
namespace: {
3464+
label: "Namespace",
3465+
help: "Manifest namespace (e.g. mail, storage, ai)."
3466+
},
3467+
key: {
3468+
label: "Key",
3469+
help: "Specifier key inside the namespace (snake_case)."
3470+
},
3471+
value: {
3472+
label: "Value",
3473+
help: "JSON-encoded value. Null when encrypted=true (see value_enc)."
3474+
},
3475+
encrypted: {
3476+
label: "Encrypted",
3477+
help: "When true, the value is stored encrypted-at-rest in value_enc; value column is null."
3478+
},
3479+
locked: {
3480+
label: "Locked",
3481+
help: "When true, tenant and user rows cannot override this value; writes against those scopes return 409."
3482+
},
3483+
locked_reason: {
3484+
label: "Lock Reason",
3485+
help: "Human-readable explanation surfaced in the UI tooltip when locked=true."
3486+
},
3487+
value_enc: {
3488+
label: "Encrypted Value",
3489+
help: "Handle of the sys_secret row holding the ciphertext. Set only when encrypted=true."
3490+
},
3491+
updated_by: {
3492+
label: "Updated By",
3493+
help: "Last actor who wrote this row through the settings service."
3494+
}
3495+
}
3496+
},
34493497
sys_secret: {
34503498
label: "Secreto",
34513499
pluralLabel: "Secretos",
@@ -3573,6 +3621,84 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
35733621
}
35743622
}
35753623
},
3624+
sys_platform_setting_audit: {
3625+
label: "Platform Setting Audit Entry",
3626+
pluralLabel: "Platform Setting Audit",
3627+
description: "Append-only audit trail for SettingsService mutations of global-scope keys.",
3628+
fields: {
3629+
id: {
3630+
label: "ID"
3631+
},
3632+
created_at: {
3633+
label: "Created At",
3634+
help: "When the mutation was recorded."
3635+
},
3636+
namespace: {
3637+
label: "Namespace"
3638+
},
3639+
key: {
3640+
label: "Key"
3641+
},
3642+
scope: {
3643+
label: "Scope",
3644+
help: "Cascade layer the row was written to — always the global rung here.",
3645+
options: {
3646+
global: "Global"
3647+
}
3648+
},
3649+
action: {
3650+
label: "Action",
3651+
help: "Mutation kind.",
3652+
options: {
3653+
set: "Set",
3654+
reset: "Reset",
3655+
lock: "Lock",
3656+
unlock: "Unlock",
3657+
rotate: "Rotate"
3658+
}
3659+
},
3660+
actor_id: {
3661+
label: "Actor",
3662+
help: "User who performed the mutation; null for system jobs."
3663+
},
3664+
source: {
3665+
label: "Source",
3666+
help: "Mutation entry-point.",
3667+
options: {
3668+
ui: "UI",
3669+
api: "API",
3670+
migration: "Migration",
3671+
import: "Import",
3672+
system: "System"
3673+
}
3674+
},
3675+
reason: {
3676+
label: "Reason",
3677+
help: "Free-text justification provided by the actor (optional)."
3678+
},
3679+
old_hash: {
3680+
label: "Old Hash",
3681+
help: "Digest of the previous value, never the plaintext: sha256: hex for a plain value, the keyed hmac-sha256: digest for a secret. Null when previously unset, or for a secret with no keyed digest."
3682+
},
3683+
new_hash: {
3684+
label: "New Hash",
3685+
help: "Digest of the new value, never the plaintext: sha256: hex for a plain value, the keyed hmac-sha256: digest for a secret. Null on reset, or for a secret with no keyed digest."
3686+
},
3687+
encrypted: {
3688+
label: "Encrypted",
3689+
help: "True when the field carries secret material (rotation is interesting)."
3690+
},
3691+
request_id: {
3692+
label: "Request ID",
3693+
help: "Correlates with sys_audit_log / tracing."
3694+
}
3695+
},
3696+
_views: {
3697+
recent: {
3698+
label: "Recent"
3699+
}
3700+
}
3701+
},
35763702
sys_migration: {
35773703
label: "Migración de datos",
35783704
pluralLabel: "Migraciones de datos",

0 commit comments

Comments
 (0)