Skip to content

Commit bcf8a37

Browse files
test(service-automation): sealed-envelope pin no longer matches short fixture strings against random ciphertext (#30998)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoCRAyW1nbzvK9mcvgPRf542)_ Fixes #30957 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:只改一个测试文件。密封信封钉子的夹具值都换成带 `.` 的长字符串,并加一条断言:存进 KV 的信封里一个 `.` 都没有。 - 为什么改:原夹具连接器名是 `crm`,随机 base64url 密文大约每 1,100 次运行就会碰巧含有它,`Test Core` 在无关 PR 上偶发变红。 - 风险与回滚:只动测试,不动生产代码、不发布;回滚即 revert 本 PR。 - 先看:`packages/services/service-automation/src/oauth2-grant/authorization.test.ts` 中 sealed 那一组 describe 的 `begun` 夹具与第一条用例。 - 两分钟验证:`pnpm --filter @objectstack/service-automation exec vitest run src/oauth2-grant/authorization.test.ts`,预期 11 个用例全绿。 - 需要你做的:无,席位落地。 ## What changes `ADR-0143 D5 — a begun connect is held sealed` in `service-automation`'s `oauth2-grant/authorization.test.ts`: - The describe's `begun` fixture now gives every plaintext field the pin looks for (connector, organization, user, verifier, redirect URI) and the state a value that carries a `.`. Neither base64url nor the envelope's JSON frame (`{"v":1,"iv":…,"ct":…,"tag":…}`) ever emits a `.`, so random `iv` / `ct` / `tag` text can never contain one of those values by chance. - The pin asserts that precondition (every looked-for value contains `.`) and that the stringified stored envelope contains no `.` at all, then keeps the per-field `not.toContain` loop and the `['ct', 'iv', 'tag', 'v']` key check. What the pin proves is unchanged: if the store writes any plaintext field into the KV, it goes red (ablation below). The short `org_a` / `usr_1` values the card also names are replaced in this describe as well. Test only, so no changeset (the file is not in the package's `files`; `skip-changeset`). Siblings checked (`grep -rn "not.toContain" packages/services/service-automation/src/oauth2-grant/`): the other hits match fixed strings against error messages (`app-credential`, `grant-handle`, `mcp-authorization`, `token-endpoint`), or `'raw-state-value'` against a fixed prefix plus a hex SHA-256 digest (`authorization.test.ts`, the KV-key pin). None matches a short value against random text, so none shares the flake; no other file changed. ## Tests - `pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/oauth2-grant/authorization.test.ts`: Test Files 1 passed, Tests 11 passed. <details><summary>Ablation (one leg): the store writes the plaintext connector beside the envelope</summary> Via `node scripts/ablation-replace.mjs` on `src/oauth2-grant/authorization.ts`, `kvPendingAuthorizationStore().put` changed from `sealPendingAuthorization(state, pending)` to `{ ...sealPendingAuthorization(state, pending), connector: pending.connector }`: ``` ablation-replace: ok mutation landed: anchor 1 -> 0, blob 75c236c -> aa62fda5748e × the KV holds no verifier, user, organization, instance or redirect URI — only an envelope 7ms FAIL src/oauth2-grant/authorization.test.ts > ADR-0143 D5 — a begun connect is held sealed (the KV trust boundary) > the KV holds no verifier, user, organization, instance or redirect URI — only an envelope AssertionError: expected '{"v":1,"iv":"Lzv2WBDpLmRysSC_","ct":"…' not to contain '.' Tests 1 failed | 10 passed (11) ablation-replace: ok restored: blob == HEAD (75c236c) and `git diff HEAD` is empty ``` The test imports `./authorization.js` from source, so no dist rebuild sits between the mutation and the run. </details> ## Verification All readings on head `bdbe3023`. - `dispatch-gates --commands` derived 51 families for the one-file diff (21 changed lines); all 51 ran locally with exit 0, and `dispatch-gates --ran` reconciles: "51 derived famil(ies) accounted for — 51 run, 0 NOT-MEASURED". - First-pass exit 3s were prerequisites, re-run after meeting them: `check-plugin-teardown-shape --self-test` needed its pinned fixture commit fetched into the shallow clone; `check:dual-build-cjs-loads` needed a full workspace build (`turbo run build`, 82/82 tasks). `check:type-check-debt` hit my own 300 s per-command timeout on the first pass and passed on re-run. - `pnpm --filter @objectstack/service-automation typecheck`: exit 0 (includes `check:test-typecheck` over the test layer). `pnpm exec eslint` on the changed file: exit 0. - Whole package: `pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2`: Test Files 264 passed, Tests 3431 passed. <details><summary>Gate record (command :: exit)</summary> ``` node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-adoption.mjs :: exit 0 node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: exit 0 node scripts/check-issue-citations.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 node scripts/check-tenant-audit-census.mjs :: exit 0 node scripts/check-tenant-audit-census.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 node scripts/docs-audit/check-affected-docs.mjs :: exit 0 node scripts/docs-audit/check-drift-comment.mjs :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:issue-citations :: exit 0 pnpm check:keyed-text-bounds :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:logger-receiver-detach :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:objectql-double-limit :: exit 0 pnpm check:org-identifier :: exit 0 pnpm check:package-naming :: exit 0 pnpm check:page-declaration-shape :: exit 0 pnpm check:platform-object-tenancy-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:registry-log-declared :: exit 0 pnpm check:rest-log-spy-declared :: exit 0 pnpm check:slot-lookup :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:system-context-census :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 ``` </details> ## Acceptance notes - The new `expect(stored).not.toContain('.')` is deterministic: the envelope is `v` (the number 1) plus three base64url strings inside a JSON frame, none of which can emit `.`. If the envelope format ever gains a field that may hold a `.`, this line goes red and the fixture choice needs revisiting, which is the intended signal. --- _Generated by [Claude Code](https://claude.ai/code/session_01L9qibyj2zTjgu4ZNg8WmHE)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 26f5719 commit bcf8a37

1 file changed

Lines changed: 18 additions & 3 deletions

File tree

‎packages/services/service-automation/src/oauth2-grant/authorization.test.ts‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,30 @@ describe('ADR-0143 D5 — a begun connect is taken once', () => {
126126
});
127127

128128
describe('ADR-0143 D5 — a begun connect is held sealed (the KV trust boundary)', () => {
129-
const begun = pending({ verifier: 'dummy-verifier-7Qx', redirectUri: 'https://app.example.com/cb-sealed' });
129+
// Every plaintext value the pin below looks for carries a `.`, a character
130+
// neither base64url nor the envelope's JSON frame ever emits, so the random
131+
// `iv` / `ct` / `tag` can never contain one by chance: only a store that
132+
// writes the field itself turns the pin red. (A short value such as `crm`
133+
// turns up in random base64url about once in a thousand runs.)
134+
const begun = pending({
135+
connector: 'connector.sealed-fixture',
136+
organizationId: 'org.sealed-fixture',
137+
userId: 'usr.sealed-fixture',
138+
verifier: 'dummy-verifier.sealed-fixture',
139+
redirectUri: 'https://app.example.com/cb-sealed',
140+
});
141+
const sealedState = 'st.sealed-fixture';
130142
const keyOf = (state: string) => `connector-oauth2:pending:${createHash('sha256').update(state).digest('hex')}`;
131143

132144
it('the KV holds no verifier, user, organization, instance or redirect URI — only an envelope', async () => {
133145
const { kv, held } = fakeKv();
134-
await kvPendingAuthorizationStore(kv).put('st-sealed', begun);
146+
await kvPendingAuthorizationStore(kv).put(sealedState, begun);
135147
const [, entry] = [...held][0];
136148
const stored = JSON.stringify(entry.value);
137-
for (const plain of [begun.verifier, begun.userId, begun.organizationId, begun.connector, begun.redirectUri, 'st-sealed']) {
149+
const plains = [begun.verifier, begun.userId, begun.organizationId, begun.connector, begun.redirectUri, sealedState];
150+
for (const plain of plains) expect(plain).toContain('.');
151+
expect(stored).not.toContain('.');
152+
for (const plain of plains) {
138153
expect(stored).not.toContain(plain);
139154
}
140155
expect(Object.keys(entry.value as object).sort()).toEqual(['ct', 'iv', 'tag', 'v']);

0 commit comments

Comments
 (0)