diff --git a/.changeset/26603-setting-audit-hash-description.md b/.changeset/26603-setting-audit-hash-description.md new file mode 100644 index 00000000000..6ef24db026c --- /dev/null +++ b/.changeset/26603-setting-audit-hash-description.md @@ -0,0 +1,16 @@ +--- +'@objectstack/platform-objects': patch +--- + +fix(platform-objects): the settings audit `old_hash` / `new_hash` columns describe both digest formats they carry + +Clause-②: no +Metadata-protocol: none + +`sys_setting_audit` described `old_hash` and `new_hash` as "SHA-256 of the … value (canonicalised)", and `sys_platform_setting_audit` named no format. A secret row never carries an unkeyed SHA-256, so an operator or agent checking a row against the declared format read every secret row as malformed. Both objects now state what the settings audit writer records: + +- **A non-secret value:** `sha256:` followed by the hex SHA-256 of its canonical JSON. +- **A secret value (`encrypted: true`):** the crypto provider's keyed digest, `hmac-sha256:…`. Never an unkeyed hash, which would let anyone holding the trail confirm a guessed secret. +- **Null:** `old_hash` when the previous value was unset, `new_hash` on reset, and either one for a secret when no keyed digest is available. + +Rows written before the `sha256:` digest may carry `fnv32:`; compare only values with the same prefix. Only the description text changes: no column type, length or name, and nothing about what is written. 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 e0a569e11f3..3c10dcf93a5 100644 --- a/packages/platform-objects/src/apps/translations/en.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.objects.generated.ts @@ -3457,11 +3457,11 @@ export const enObjects: NonNullable = { }, old_hash: { label: "Old Hash", - help: "SHA-256 of the previous value (canonicalised). Null when previously unset." + 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." }, new_hash: { label: "New Hash", - help: "SHA-256 of the new value (canonicalised). Null on reset." + 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." }, encrypted: { label: "Encrypted", 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 f0cac66c7f0..9a732d8e98a 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 @@ -3457,11 +3457,11 @@ export const esESObjects: NonNullable = { }, old_hash: { label: "Hash anterior", - help: "SHA-256 del valor anterior (canonicalizado). Null cuando antes no estaba establecido." + help: "Resumen del valor anterior, nunca el texto plano: sha256: en hexadecimal para un valor normal, el resumen con clave hmac-sha256: para un valor secreto. Null cuando antes no estaba establecido, o para un valor secreto sin resumen con clave." }, new_hash: { label: "Hash nuevo", - help: "SHA-256 del valor nuevo (canonicalizado). Null al restablecer." + help: "Resumen del valor nuevo, nunca el texto plano: sha256: en hexadecimal para un valor normal, el resumen con clave hmac-sha256: para un valor secreto. Null al restablecer, o para un valor secreto sin resumen con clave." }, encrypted: { label: "Cifrado", 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 5532b684250..3d835cd861e 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 @@ -3457,11 +3457,11 @@ export const jaJPObjects: NonNullable = { }, old_hash: { label: "変更前ハッシュ", - help: "変更前の値の SHA-256(正規化済み)。未設定の場合は null。" + help: "変更前の値のダイジェスト(平文ではない):通常の値は sha256: の16進ダイジェスト、機密値は鍵付きの hmac-sha256: ダイジェスト。未設定の場合、または鍵付きダイジェストを得られない機密値の場合は null。" }, new_hash: { label: "変更後ハッシュ", - help: "新しい値の SHA-256(正規化済み)。リセット時は null。" + help: "新しい値のダイジェスト(平文ではない):通常の値は sha256: の16進ダイジェスト、機密値は鍵付きの hmac-sha256: ダイジェスト。リセット時、または鍵付きダイジェストを得られない機密値の場合は null。" }, encrypted: { label: "暗号化", 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 0ea28f2ea22..879f0bd3e15 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 @@ -3457,11 +3457,11 @@ export const zhCNObjects: NonNullable = { }, old_hash: { label: "旧哈希", - help: "旧值(规范化后)的 SHA-256。此前未设置时为 Null。" + help: "旧值的摘要,绝不是明文:普通值为 sha256: 十六进制摘要,机密值为带密钥的 hmac-sha256: 摘要。此前未设置时为 Null,机密值没有可用的带密钥摘要时也为 Null。" }, new_hash: { label: "新哈希", - help: "新值(规范化后)的 SHA-256。重置时为 Null。" + help: "新值的摘要,绝不是明文:普通值为 sha256: 十六进制摘要,机密值为带密钥的 hmac-sha256: 摘要。重置时为 Null,机密值没有可用的带密钥摘要时也为 Null。" }, encrypted: { label: "已加密", diff --git a/packages/platform-objects/src/system/sys-platform-setting-audit.object.ts b/packages/platform-objects/src/system/sys-platform-setting-audit.object.ts index 20192f9dc6a..ff2c6252296 100644 --- a/packages/platform-objects/src/system/sys-platform-setting-audit.object.ts +++ b/packages/platform-objects/src/system/sys-platform-setting-audit.object.ts @@ -134,18 +134,26 @@ export const SysPlatformSettingAudit = ObjectSchema.create({ description: 'Free-text justification provided by the actor (optional).', }), + /** + * Same formats as `sys_setting_audit.old_hash`: `sha256:` + hex SHA-256 + * of the canonical JSON for a non-secret value, the crypto provider's keyed + * `hmac-sha256:` digest for a secret (never an unkeyed hash). Null when + * previously unset, and null for a secret when no keyed digest is + * available. Older rows may carry `fnv32:`; compare only same-prefix values. + */ old_hash: Field.text({ label: 'Old Hash', readonly: true, maxLength: 128, - description: 'Digest of the previous value. Never the plaintext. Null when previously unset.', + description: '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.', }), + /** Same formats as `old_hash`. Null on `reset`, and for a secret with no keyed digest. */ new_hash: Field.text({ label: 'New Hash', readonly: true, maxLength: 128, - description: 'Digest of the new value. Never the plaintext. Null on reset.', + description: '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.', }), encrypted: Field.boolean({ diff --git a/packages/platform-objects/src/system/sys-setting-audit.object.ts b/packages/platform-objects/src/system/sys-setting-audit.object.ts index c638fb5d986..421184f012a 100644 --- a/packages/platform-objects/src/system/sys-setting-audit.object.ts +++ b/packages/platform-objects/src/system/sys-setting-audit.object.ts @@ -137,22 +137,33 @@ export const SysSettingAudit = ObjectSchema.create({ /** * Content digest of the previous value. Never the plaintext — * lets operators detect duplicate writes without leaking secrets. - * Format: hex SHA-256 of the canonicalised JSON, or null when - * the previous value was unset. + * The format depends on the value: + * - a non-secret value: `sha256:` + hex SHA-256 of its canonical JSON; + * - a secret value (`encrypted: true`): the crypto provider's keyed + * digest, `hmac-sha256:…`. Never an unkeyed hash: one of a short + * secret lets anyone holding the trail confirm a guess. + * Null when the previous value was unset, and null for a secret when + * no keyed digest is available. Rows written before the SHA-256 digest + * (#26465) may carry `fnv32:`; compare only values with the same prefix. */ old_hash: Field.text({ label: 'Old Hash', readonly: true, maxLength: 128, - description: 'SHA-256 of the previous value (canonicalised). Null when previously unset.', + description: '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.', }), - /** Content digest of the new value. Null on `reset`. */ + /** + * Content digest of the new value, in the same formats as `old_hash` + * (`sha256:` for a non-secret value, the keyed `hmac-sha256:` digest + * for a secret). Null on `reset`, and null for a secret when no keyed + * digest is available. + */ new_hash: Field.text({ label: 'New Hash', readonly: true, maxLength: 128, - description: 'SHA-256 of the new value (canonicalised). Null on reset.', + description: '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.', }), /** True when the field is encrypted — flags secret rotation events. */