Skip to content

Commit f515124

Browse files
fix(service-settings): settings audit writes old_hash and the declared SHA-256 digest (#26532)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XFdELkVjeF5csUMkbrNabaW)_ Fixes #26465 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:设置审计表 `sys_setting_audit` / `sys_platform_setting_audit` 的 `old_hash` 现在记录写入前那个值的摘要;非密钥值的摘要从 FNV-32 改为列声明的 SHA-256(`sha256:` 前缀)。 - **为什么改**:列声明「旧值的 SHA-256,未设置时为空」,实际 `old_hash` 永远为空、新值是 FNV-32,运维无法从审计轨迹判断一次写入是否真的改了值(重复写入检测失效)。 - **风险与回滚**:不新增任何数据库读取(旧值取自 upsert 已有的存在性探测);密钥值的带密钥摘要路径不变;历史 `fnv32:` 行不回填。回滚即还原本 PR。 - **先看**:`packages/services/service-settings/src/settings-service.ts`(`upsertRow` 返回旧行、`previousAuditDigest`),`src/crypto-adapter.ts`(`settingAuditDigest`),`src/settings-audit-old-hash.test.ts`。 - **两分钟验证**:`pnpm --filter @objectstack/service-settings exec vitest run src/settings-audit-old-hash.test.ts`,预期 7 个用例全绿。 - **需要你做的**:无,席位落地。 ## What changes - **`old_hash` is written.** `upsertRow` already probes the row at its identity before it updates; it now returns that previous row (snapshotted before the update) beside the `previousEnc` handle it already returned. `setMany` passes `oldHash: previousAuditDigest(previous)` instead of a hard-coded `null`. The read happens under the same identity the write targets: the global rung's `(namespace, key)` in `sys_platform_setting`, and `(organization_id, namespace, key, scope, user_id)` in `sys_setting`. A write with no previous row costs no extra round-trip, because the probe was already there. `old_hash` is null when there was no row, when the row was reset (`value` null), and when the previous row held a secret. - **The non-secret digest is the declared SHA-256.** A new `settingAuditDigest(canonicalJson)` in `crypto-adapter.ts` returns `sha256:` + hex SHA-256 of the canonical JSON (`stableStringify`, unchanged). The write path uses it for both the new and the previous value instead of `this.crypto.digest(...)`, so an injected `CryptoAdapter` cannot change the column's format. The same `digest` variable feeds the `config_change` row in `sys_audit_log`, so both ledgers keep recording one fingerprint per write. - **Secret path untouched.** `secretAuditDigest` (the provider's keyed `hmac-sha256:` digest, or null) and its two call sites are byte-identical; a secret write keeps `oldHash: null`. - **`CryptoAdapter.digest`**: the only non-test caller in the repository was `settings-service.ts:2428`. The interface member stays, so existing adapters still satisfy it. Its docblock and the `NoopCryptoAdapter` FNV comment now say the settings audit no longer reads it. - **Claim amendment, granted by the seat:** `docs/qa/platform-checklist/areas/platform-core.json`, item `platform-core.settings-audit-secret-fingerprint`, revision 4 → 5. Its step-7 positive control computed `fnv32:` for the non-secret `mail.from_email`. After this change that control could never match, and it could no longer prove the offline search works. The control and acceptance[4] now compute `sha256:`. The secret search keeps FNV-1a 32 as a second net, so the item is not loosened. The `history` row says the same. - **In-package docblock (same claim):** `sys-secret-orphan-report.ts` said the service passes the adapter's `digest()` for non-secret keys. It now names `settingAuditDigest()`. - Changeset: `@objectstack/service-settings` patch. `packages/platform-objects` is not touched; the declared columns stand. ## Tests New `src/settings-audit-old-hash.test.ts` drives `PUT /api/settings/:namespace` through `registerSettingsRoutes` over a real `ObjectQL` with the real `SysSetting` / `SysPlatformSetting` / `SysSecret` / `SysSettingAudit` / `SysPlatformSettingAudit` objects and the plugin's own `buildSettingAuditWriter`. It reads the audit rows back out of the driver store, and computes the expected digests with `node:crypto`, not the implementation: - tenant rung (`sys_setting_audit`): first set (old null, new `sha256:`), overwrite (old = digest of previous, chained to the prior row's `new_hash`), same-value write (old == new), set → reset → set (reset: old = previous digest, new null; the next set: old null). A null value is the family's removal; the settings routes have no DELETE verb, so reset is the delete case. - scope: a user-scope write reads its own previous row, not another user's. - global rung (`sys_platform_setting_audit`): first set, overwrite, same-value write. - CONTROL, secret value: keyed `hmac-sha256:` `new_hash`, null `old_hash` on both writes, no unkeyed SHA-256 of the secret anywhere in the row. Updated: `settings-audit-secret-digest.test.ts`, non-secret case: it pinned the adapter's FNV digest; it now pins the `sha256:` digest on both ledgers and that it is not the adapter's. Ablation, once, on the old-value read: `node scripts/ablation-replace.mjs` replaced `oldHash: isEncrypted ? null : SettingsService.previousAuditDigest(previous),` with `oldHash: null,`. The tool's own check confirmed it landed (anchor 1 → 0, blob `7425ed93` → `3b7fd19c`). Result: **5 of 7 red** (overwrite, same-value, set→reset→set, user scope, global rung). The two green are the first-set case and the secret control, which do not depend on the read. Restore was verified: blob == HEAD and `git diff HEAD` is empty. The test imports `./settings-service.js` from `src/` (no dist hop), so no rebuild was needed between the legs. Package run on the final head `a397e85a77` (after merging `origin/main` `b7f2b0c290`), under the verify lock: `pnpm --filter @objectstack/service-settings build && … typecheck && … exec vitest run --maxWorkers=2` → `Test Files 61 passed (61)`, `Tests 1007 passed (1007)`, `VERDICT command-exit 0`. `tsc --listFiles` includes both edited test files. `eslint --no-inline-config packages/services/service-settings/src` exits 0. `test:repo` and dogfood are left to CI: the title has no `!` and there is no `(narrowing)` arm. ## Acceptance notes - **Mixed history.** Rows written before this change carry `fnv32:` (non-secret) and `hmac-sha256:` (secret). New non-secret rows carry `sha256:`. No reader in the repository compares hashes across rows: `git grep` for `old_hash` / `new_hash` / `newHash` outside service-settings finds only fixtures, docs and the platform-object declarations. A reader that does compare them must compare only same-prefix values. A `fnv32:` row next to a `sha256:` row of the same value reads as "different algorithm", not "different value", because the prefix says so. The first write after upgrading over an `fnv32:` row records `old_hash` as the `sha256:` digest of the stored previous value. It is computed from the value, not copied from the old row, so it does not equal that row's `new_hash` string. Stored rows are not rewritten (ruling). - **Column docblock vs prefix.** `sys-setting-audit.object.ts:137-142` says "Format: hex SHA-256 of the canonicalised JSON". The stored form is `sha256:` + that hex (71 chars, under `maxLength: 128`). The prefix follows the convention already in the column: `hmac-sha256:` for secrets, `fnv32:` before. The `description` strings ("SHA-256 of the previous value (canonicalised)") hold as written. The TSDoc line does not mention the prefix. It is left as is, because platform-objects is outside the claim. - **Secret rows keep `old_hash` null.** Writing the previous secret's keyed digest would need a `sys_secret` read plus a decrypt of the previous value on every secret write. The ruling keeps the secret path out of scope, so the "null only when previously unset" declaration is still not met for `encrypted: true` rows. - **`CryptoAdapter.digest` now has zero non-test readers in this repository.** It is a published interface member, so retiring it is a separate API decision and is not done here. - Open PRs touching `packages/services/service-settings/**`: none of the 18 open non-bot PRs at start (first page of each PR's files). ## Verification <details><summary>Derived gate families: 64 of 64 exit 0 on a397e85, reconciled with --ran</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 64 commands from the tree at `a397e85a77`. Each one was run with its exit code captured before any pipe, and all 64 answered `exit 0`. `dispatch-gates --ran`: "64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED". The changed-lines reading is 484 (+458 / -26), under the 3000 human-merge threshold. On the first pass, before the merge, two runs were prerequisite refusals. `check-plugin-teardown-shape --self-test` exited 3 on the shallow clone, and after `git fetch --unshallow origin main` it passed. `check:dual-build-cjs-loads` exited 3 because other packages had no `dist/`. Both answered exit 0 in the final-head pass above, after the merge rebuild. Every family outside the derivation (artifact rosters, wide-population, the type-check lanes) is left to this PR's CI check-runs. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5ojTc5Fbj8xxXSfo1K)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f486e9c commit f515124

7 files changed

Lines changed: 458 additions & 26 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/service-settings': patch
3+
---
4+
5+
fix(service-settings): the settings audit trail records the previous value's digest in `old_hash`, and a non-secret value's digest is the declared SHA-256
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`sys_setting_audit` and `sys_platform_setting_audit` declare `old_hash` as the SHA-256 of the previous value (null when it was unset) and `new_hash` as the SHA-256 of the new value (null on a reset). The write path passed `old_hash: null` on every write and recorded the adapter's FNV-1a 32 (`fnv32:…`) as the new hash, so an operator could not tell from the trail whether a write changed anything.
11+
12+
- **`old_hash`** now holds the digest of the value the row held before the write, read off the row the upsert already looks up: no extra read, and none at all for a first write. It is null for a first write, after a reset, and for a row that held a secret.
13+
- **A non-secret value's digest** is now `sha256:` followed by the hex SHA-256 of its canonical JSON, on both `new_hash` and the `config_change` row's digest in `sys_audit_log`. It no longer goes through `CryptoAdapter.digest`, so an injected adapter cannot change the format. The prefix keeps it distinct from the `fnv32:` rows already stored, which are left as they are: compare hashes only between rows that carry the same prefix.
14+
- **Secret values are unchanged.** They keep the crypto provider's keyed digest (`hmac-sha256:…`) as `new_hash`, or none, and `old_hash` stays null for them.

‎docs/qa/platform-checklist/areas/platform-core.json‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3136,7 +3136,7 @@
31363136
"title": "The settings audit trail keeps no fingerprint of a secret-valued setting that a reader can match offline: both ledgers record the platform's keyed digest or none, never the value or an unkeyed hash, so a caller who can only read the audit rows learns that the secret changed and nothing about its value",
31373137
"since": "v17",
31383138
"status": "active",
3139-
"revision": 4,
3139+
"revision": 5,
31403140
"priority": "P1",
31413141
"surface": "api",
31423142
"personas": [
@@ -3159,10 +3159,10 @@
31593159
"generate a fresh random value V1 for this run (never a value used anywhere else). As admin, PUT /api/settings/mail { provider: 'resend', api_key: V1, from_email: 'ops@example.com' }",
31603160
"read the per-key ledger: GET /api/v1/data/sys_platform_setting_audit filtered to namespace=mail and key=api_key; capture the newest row (action set, encrypted true, new_hash). mail is a GLOBAL-scope namespace, and the per-key ledger of the global rung is the deployment-level sys_platform_setting_audit (ADR-0131 D7, packages/services/service-settings/src/settings-service-plugin.ts#settingAuditObjectFor); a tenant- or user-scope key's row lands in sys_setting_audit",
31613161
"read the generic ledger: GET /api/v1/data/sys_audit_log filtered to action=config_change; capture the newest row for mail.api_key (object_name 'sys_platform_setting' — mail is a GLOBAL-scope namespace, and a global-scope value's row names the tenant-less table it lives in, packages/services/service-settings/src/config-change-audit.ts#CONFIG_CHANGE_GLOBAL_OBJECT_NAME; a tenant-scope key's row names 'sys_setting'; new_value is a JSON string carrying namespace, key, scope and digest; metadata carries encrypted: true)",
3162-
"OFFLINE, with no server key: compute the unkeyed digests a leak would carry — the FNV-1a 32 of V1's canonical JSON (the quoted string), spelled 'fnv32:' + 8 lowercase hex, which is the unkeyed digest this settings service writes for a non-secret key (packages/services/service-settings/src/crypto-adapter.ts#digest), and, belt and braces, the SHA-256 hex of V1 (bare, and with a 'sha256:' prefix) — and search both captured rows, every field, for each of them and for V1 itself",
3162+
"OFFLINE, with no server key: compute the unkeyed digests a leak would carry — the SHA-256 of V1's canonical JSON (the quoted string), spelled 'sha256:' + 64 lowercase hex, which is the unkeyed digest this settings service writes for a non-secret key (packages/services/service-settings/src/crypto-adapter.ts#settingAuditDigest), and, belt and braces, the bare SHA-256 hex of V1 and of its canonical JSON, and the FNV-1a 32 of its canonical JSON ('fnv32:' + 8 lowercase hex, the non-secret digest rows written before #26465 carry) — and search both captured rows, every field, for each of them and for V1 itself",
31633163
"write V1 again, then a different value V2, then V1 a third time; re-read both ledgers and compare the fingerprints of the three writes",
31643164
"reset the key: PUT /api/settings/mail { provider: 'log', api_key: null } (a null value is the reset; 'log' needs no api_key), then read the reset rows",
3165-
"control: write the NON-secret key mail.from_email and compute OFFLINE the FNV-1a 32 of its canonical JSON (the quoted string), spelled 'fnv32:' + 8 lowercase hex. Its rows must carry exactly that unkeyed digest. This proves step 4's offline search — which computes the same algorithm — WOULD have found an unkeyed digest, and that the keyed rule is scoped to secret-valued keys"
3165+
"control: write the NON-secret key mail.from_email and compute OFFLINE the SHA-256 of its canonical JSON (the quoted string), spelled 'sha256:' + 64 lowercase hex. Its rows must carry exactly that unkeyed digest. This proves step 4's offline search — which computes the same algorithm — WOULD have found an unkeyed digest, and that the keyed rule is scoped to secret-valued keys"
31663166
],
31673167
"acceptance": [
31683168
{
@@ -3172,7 +3172,7 @@
31723172
"evidence": "both captured rows, quoted in full"
31733173
},
31743174
{
3175-
"clause": "a reader of the audit rows who holds no server key cannot confirm a guess: no fingerprint in either row can be reproduced from the value alone. No offline unkeyed digest of V1 — the FNV-1a 32 of its canonical JSON ('fnv32:…', the algorithm the service uses for non-secret keys), nor the SHA-256 of V1 with or without the sha256 prefix — matches anything in either row",
3175+
"clause": "a reader of the audit rows who holds no server key cannot confirm a guess: no fingerprint in either row can be reproduced from the value alone. No offline unkeyed digest of V1 — the SHA-256 of its canonical JSON ('sha256:…', the algorithm the service uses for non-secret keys), the bare SHA-256 of V1, nor the FNV-1a 32 of its canonical JSON ('fnv32:…') — matches anything in either row",
31763176
"oracle": "api",
31773177
"verify": "the step 4 offline search over both rows finds no hit, AND the step 7 control's offline digest DOES match the non-secret row. Without the control, a no-hit proves nothing about the search",
31783178
"evidence": "the offline digest values, the two secret rows, and the matching control row"
@@ -3190,9 +3190,9 @@
31903190
"evidence": "the two reset rows"
31913191
},
31923192
{
3193-
"clause": "non-secret settings are unchanged: a non-encrypted key keeps the unkeyed digest of its canonical JSON on both ledgers. The keyed rule applies to secret-valued keys only",
3193+
"clause": "non-secret settings record the unkeyed digest of their canonical JSON on both ledgers, the SHA-256 the per-key ledger's new_hash column declares. The keyed rule applies to secret-valued keys only",
31943194
"oracle": "api",
3195-
"verify": "mail.from_email's rows carry the FNV-1a 32 digest of the quoted JSON string ('fnv32:' + 8 lowercase hex), computed offline in step 7",
3195+
"verify": "mail.from_email's rows carry the SHA-256 digest of the quoted JSON string ('sha256:' + 64 lowercase hex), computed offline in step 7",
31963196
"evidence": "the control rows and the offline digest"
31973197
},
31983198
{
@@ -3203,7 +3203,7 @@
32033203
}
32043204
],
32053205
"negative": [
3206-
"an unkeyed digest of a secret-valued setting (the 'fnv32:' FNV-1a 32 of its canonical JSON, or a plain SHA-256 of the value, with or without a prefix) on EITHER ledger is a FAIL. Anyone who can read the audit rows could confirm a guessed value offline, and settings secrets are short, guessable values",
3206+
"an unkeyed digest of a secret-valued setting (the 'sha256:' SHA-256 of its canonical JSON, a plain SHA-256 of the value, or the 'fnv32:' FNV-1a 32 of its canonical JSON) on EITHER ledger is a FAIL. Anyone who can read the audit rows could confirm a guessed value offline, and settings secrets are short, guessable values",
32073207
"the plaintext of a secret-valued setting in any field of either ledger is a FAIL",
32083208
"a host with no keyed digest falling back to an unkeyed one is a FAIL; the rule is to record no fingerprint",
32093209
"a secret write REFUSED because its audit fingerprint could not be computed is a FAIL: the audit trail never vetoes a settings save",
@@ -3214,7 +3214,7 @@
32143214
],
32153215
"automated": {
32163216
"kind": "unit",
3217-
"ref": "packages/services/service-settings/src/settings-audit-secret-digest.test.ts — acceptance[5] and negative[2]–[3] ONLY: the no-keyed-digest arm a stock boot cannot reach (knownGaps[0]), through 'legacy inline-adapter path with no provider records no fingerprint and reports it once' and 'a provider that refuses a keyed digest leaves the write intact and records no fingerprint'. The pin drives SettingsService over stub audit sinks. So its other cases — 'records the provider keyed digest on both ledgers, not the unkeyed hash of the value', 'is stable for equal values and distinct for different ones', 'records no fingerprint on a reset', the provider-wired inline-adapter case, and the non-secret 'keep the unkeyed adapter digest of the canonical JSON, unchanged' — are service-layer evidence for acceptance[0]–[4] and negative[0]–[1], never their pass: those score the ledger rows read back over the API (steps[0]–[6])"
3217+
"ref": "packages/services/service-settings/src/settings-audit-secret-digest.test.ts — acceptance[5] and negative[2]–[3] ONLY: the no-keyed-digest arm a stock boot cannot reach (knownGaps[0]), through 'legacy inline-adapter path with no provider records no fingerprint and reports it once' and 'a provider that refuses a keyed digest leaves the write intact and records no fingerprint'. The pin drives SettingsService over stub audit sinks. So its other cases — 'records the provider keyed digest on both ledgers, not the unkeyed hash of the value', 'is stable for equal values and distinct for different ones', 'records no fingerprint on a reset', the provider-wired inline-adapter case, and the non-secret 'record the declared SHA-256 of the canonical JSON on both ledgers, not the adapter digest (#26465)' — are service-layer evidence for acceptance[0]–[4] and negative[0]–[1], never their pass: those score the ledger rows read back over the API (steps[0]–[6])"
32183218
},
32193219
"source": [
32203220
"packages/services/service-settings/src/settings-service.ts#secretAuditDigest (one helper supplies a secret write's fingerprint on both write branches, the sys_secret path and the legacy inline-adapter path: the provider's keyedDigest, or null with a once-per-key warn; never the adapter's unkeyed digest)",
@@ -3248,6 +3248,12 @@
32483248
"date": "2026-10-20",
32493249
"change": "automated.ref narrowed per clause — the sweep-wide rule-6 contradiction QA run #24840 (18.0 sweep round 4, Tier 1 at 307ce4c4) names at settings-audit acceptance[0]–[4], round 15 of wave anchor #24579. The ref listed seven cases whose names read as acceptance[0], [2], [3] and [4]. The pin drives SettingsService over stub audit sinks and never reads sys_platform_setting_audit or sys_audit_log over the API, which is what those clauses score. By RUNNER rule 6 (a pin scores only the layer it drives) it scores acceptance[5] and its negatives [2] and [3] only (QA run #24838 scored acceptance[0]–[4] live). Read at base b1676441ba. No clause changed",
32503250
"ref": "#24891"
3251+
},
3252+
{
3253+
"revision": 5,
3254+
"date": "2026-10-27",
3255+
"change": "steps[3] and [6], acceptance[1].clause, [4] (clause, verify), negative[0] and automated.ref re-pointed: a non-secret setting's audit digest is now the SHA-256 the sys_setting_audit / sys_platform_setting_audit columns declare, spelled 'sha256:' + 64 hex (crypto-adapter.ts settingAuditDigest), no longer the adapter's FNV-1a 32 ('fnv32:'). The step-7 control computes the algorithm the service now writes, so it can still prove the offline search would find an unkeyed digest. Not loosened: the secret search keeps the FNV-1a 32 as a second net, so a secret row carrying either unkeyed form still fails",
3256+
"ref": "#26465"
32513257
}
32523258
]
32533259
},

‎packages/services/service-settings/src/crypto-adapter.ts‎

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3+
import { createHash } from 'node:crypto';
4+
35
/**
46
* Pluggable adapter for at-rest encryption of `Specifier.encrypted: true`
57
* values. The default {@link NoopCryptoAdapter} provides a transparent
@@ -14,8 +16,14 @@ export interface CryptoAdapter {
1416
/** Returns the plaintext used by the resolver. */
1517
decrypt(ciphertext: string, ctx: { namespace: string; key: string }): Promise<string>;
1618
/**
17-
* Stable, short, non-reversible digest used for audit-log entries so
18-
* operators can correlate value changes without leaking secrets.
19+
* Stable, short, non-reversible digest of a plaintext.
20+
*
21+
* Not read by the settings audit trail since #26465: `sys_setting_audit`
22+
* and `sys_platform_setting_audit` declare SHA-256 for `old_hash` /
23+
* `new_hash`, so the non-secret fingerprint is {@link settingAuditDigest}
24+
* whatever adapter is injected, and a secret is fingerprinted only by the
25+
* crypto provider's keyed digest. Kept on the interface so existing
26+
* adapters still satisfy it.
1927
*/
2028
digest(plaintext: string): string;
2129
/**
@@ -79,7 +87,8 @@ export class NoopCryptoAdapter implements CryptoAdapter {
7987
return Buffer.from(ciphertext.slice(4), 'base64').toString('utf8');
8088
}
8189
digest(plaintext: string): string {
82-
// FNV-1a 32-bit — short, stable, non-cryptographic. Audit-only.
90+
// FNV-1a 32-bit — short, stable, non-cryptographic. The settings audit
91+
// no longer records it (#26465, see `settingAuditDigest`).
8392
let h = 0x811c9dc5;
8493
for (let i = 0; i < plaintext.length; i++) {
8594
h ^= plaintext.charCodeAt(i);
@@ -89,6 +98,24 @@ export class NoopCryptoAdapter implements CryptoAdapter {
8998
}
9099
}
91100

101+
/**
102+
* The fingerprint the settings audit trail records for a NON-secret value
103+
* (#26465): `sha256:` followed by the hex SHA-256 of the value's canonical
104+
* JSON, which is the algorithm `old_hash` / `new_hash` declare on
105+
* `sys_setting_audit` and `sys_platform_setting_audit`.
106+
*
107+
* Computed here rather than through {@link CryptoAdapter.digest}, so an
108+
* injected adapter cannot change the column's format. The prefix keeps the
109+
* value self-describing beside the rows written before it (`fnv32:` from the
110+
* old adapter digest) and the keyed secret fingerprints (`hmac-sha256:`), so a
111+
* reader comparing two rows can tell a different algorithm from a different
112+
* value. Never used for a secret: an unkeyed hash of a short secret lets any
113+
* reader of the audit rows confirm a guess offline.
114+
*/
115+
export function settingAuditDigest(canonicalJson: string): string {
116+
return 'sha256:' + createHash('sha256').update(canonicalJson, 'utf8').digest('hex');
117+
}
118+
92119
/**
93120
* Is this adapter allowed to hold a declared-secret value at rest (#8026)?
94121
*

0 commit comments

Comments
 (0)